Use frustum matrix for projector depth rendering, to allow flat projection onto surfaces while still offsetting upwards.

Ensure render bounds for projectors always work.
Render weather into projector depth so that rain casts shadows.
This commit is contained in:
Florian Nücke
2022-02-02 01:06:09 +01:00
parent 6c6bcadee5
commit 05c2f43abd
7 changed files with 126 additions and 38 deletions

View File

@@ -13,7 +13,6 @@ import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
import com.mojang.math.Matrix3f;
import com.mojang.math.Matrix4f;
import com.mojang.math.Quaternion;
import com.mojang.math.Vector3f;
import li.cil.oc2.api.API;
import li.cil.oc2.common.block.ProjectorBlock;
@@ -59,8 +58,6 @@ import static org.lwjgl.opengl.GL30.glBindFramebuffer;
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class ProjectorDepthRenderer {
private static final int DEPTH_CAPTURE_SIZE = 256;
private static final float PROJECTOR_FOV = 50;
private static final float PROJECTOR_ASPECT_RATIO = ProjectorVMDevice.WIDTH / (float) ProjectorVMDevice.HEIGHT;
private static final List<ProjectorBlockEntity> VISIBLE_PROJECTORS = new ArrayList<>();
private static final DepthOnlyRenderTarget[] PROJECTOR_DEPTH_TARGETS = new DepthOnlyRenderTarget[ModShaders.MAX_PROJECTORS];
@@ -68,9 +65,16 @@ public final class ProjectorDepthRenderer {
private static final Matrix4f[] PROJECTOR_CAMERA_MATRICES = new Matrix4f[ModShaders.MAX_PROJECTORS];
private static final Camera PROJECTOR_DEPTH_CAMERA = new Camera();
private static final DepthOnlyRenderTarget MAIN_CAMERA_DEPTH = new DepthOnlyRenderTarget(MainTarget.DEFAULT_WIDTH, MainTarget.DEFAULT_HEIGHT);
private static final float PROJECTOR_NEAR = 1 / 16f;
private static final float PROJECTOR_FAR = 32f;
private static final Matrix4f DEPTH_CAMERA_PROJECTION_MATRIX = Matrix4f.perspective(PROJECTOR_FOV, PROJECTOR_ASPECT_RATIO, PROJECTOR_NEAR, PROJECTOR_FAR);
private static final float PROJECTOR_FORWARD_SHIFT = 7 / 16f; // From center of projector block.
private static final float PROJECTOR_NEAR = 0.5f - PROJECTOR_FORWARD_SHIFT - 0.5f / 16f; // Not quite forward edge, to allow occluding with neighboring block.
private static final float PROJECTOR_FAR = ProjectorBlockEntity.MAX_RENDER_DISTANCE;
private static final int HALF_FRUSTUM_WIDTH = (ProjectorBlockEntity.MAX_WIDTH - 1) / 2;
private static final int FRUSTUM_HEIGHT = ProjectorBlockEntity.MAX_HEIGHT - 1;
private static final Matrix4f DEPTH_CAMERA_PROJECTION_MATRIX = getFrustumMatrix(
PROJECTOR_NEAR, PROJECTOR_FAR,
ProjectorBlockEntity.MAX_GOOD_RENDER_DISTANCE,
-HALF_FRUSTUM_WIDTH, HALF_FRUSTUM_WIDTH,
FRUSTUM_HEIGHT, 0);
private static final Cache<ProjectorBlockEntity, RenderInfo> RENDER_INFO = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofSeconds(5))
@@ -213,7 +217,7 @@ public final class ProjectorDepthRenderer {
final Direction facing = projector.getBlockState().getValue(ProjectorBlock.FACING);
final Vec3 projectorPos = Vec3
.atCenterOf(projector.getBlockPos())
.add(new Vec3(facing.step()).scale(7 / 16f));
.add(new Vec3(facing.step()).scale(PROJECTOR_FORWARD_SHIFT));
configureProjectorDepthCamera(level, projectorPos, facing.toYRot());
@@ -268,9 +272,8 @@ public final class ProjectorDepthRenderer {
}
private static void setupViewModelMatrix(final PoseStack viewModelStack) {
final Quaternion rotation = Vector3f.YP.rotationDegrees(PROJECTOR_DEPTH_CAMERA.getYRot() + 180);
viewModelStack.setIdentity();
viewModelStack.mulPose(rotation);
viewModelStack.mulPose(Vector3f.YP.rotationDegrees(PROJECTOR_DEPTH_CAMERA.getYRot() + 180));
final Matrix3f viewRotationMatrix = viewModelStack.last().normal().copy();
if (viewRotationMatrix.invert()) {
@@ -405,6 +408,17 @@ public final class ProjectorDepthRenderer {
tesselator.end();
}
private static Matrix4f getFrustumMatrix(final float near, final float far, final float dist,
final float left, final float right,
final float top, final float bottom) {
return new Matrix4f(new float[]{
2 * dist / (right - left), 0, (right + left) / (right - left), 0,
0, 2 * dist / (top - bottom), (top + bottom) / (top - bottom), 0,
0, 0, -(far + near) / (far - near), -(2 * far * near) / (far - near),
0, 0, -1, 0,
});
}
private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) {
try {
return RENDER_INFO.get(projector, () -> {

View File

@@ -38,7 +38,16 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
@Override
public boolean shouldRender(final ProjectorBlockEntity projector, final Vec3 position) {
return projector.isProjecting() && BlockEntityRenderer.super.shouldRender(projector, position);
return !ProjectorDepthRenderer.isIsRenderingProjectorDepth() &&
projector.isProjecting() &&
BlockEntityRenderer.super.shouldRender(projector, position);
}
@Override
public boolean shouldRenderOffScreen(final ProjectorBlockEntity p_112306_) {
// Render bounding box of projectors (vastly) exceeds their block position, so they need
// to be treated as global renderers, and cannot be culled with their chunk.
return true;
}
@Override

View File

@@ -46,9 +46,10 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
private static final Logger LOGGER = LogManager.getLogger();
private static final int MAX_RENDER_DISTANCE = 12;
private static final int MAX_WIDTH = MAX_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center.
private static final int MAX_HEIGHT = (MAX_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin.
public static final int MAX_RENDER_DISTANCE = 16;
public static final int MAX_GOOD_RENDER_DISTANCE = 12;
public static final int MAX_WIDTH = MAX_GOOD_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center.
public static final int MAX_HEIGHT = (MAX_GOOD_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin.
private static final int FRAME_EVERY_N_TICKS = 5;
@@ -285,15 +286,15 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
private void updateRenderBounds() {
final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING);
final Direction screenUp = Direction.UP;
final Direction screenLeft = blockFacing.getCounterClockWise();
final Direction canvasUp = Direction.UP;
final Direction canvasLeft = blockFacing.getCounterClockWise();
final BlockPos projectorPos = getBlockPos();
final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE);
final BlockPos screenMinPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2);
final BlockPos screenMaxPos = screenBasePos.relative(screenLeft, MAX_WIDTH / 2)
final BlockPos screenMinPos = screenBasePos.relative(canvasLeft.getOpposite(), MAX_WIDTH / 2);
final BlockPos screenMaxPos = screenBasePos.relative(canvasLeft, MAX_WIDTH / 2)
// -1 for the MAX_HEIGHT padding, -1 for auto-expansion of AABB constructor
.relative(screenUp, MAX_HEIGHT - 2);
.relative(canvasUp, MAX_HEIGHT - 2);
renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos));
}

View File

@@ -0,0 +1,24 @@
package li.cil.oc2.common.mixin;
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
import net.minecraft.client.renderer.culling.Frustum;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Frustum.class)
public abstract class FrustumMixin {
/**
* Skip offsetting the frustum to fully contain camera cube, since we have a very
* tight frustum when rendering projector depth; so tight that the cube may never
* fit inside, which would then leave to an endless loop (would shift out of bounds
* and never get back in).
*/
@Inject(method = "offsetToFullyIncludeCameraCube", at = @At("HEAD"), cancellable = true)
private void skipOffset(final CallbackInfoReturnable<Frustum> ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
ci.setReturnValue((Frustum) (Object) this);
}
}
}

View File

@@ -9,6 +9,7 @@ import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -42,6 +43,8 @@ public abstract class LevelRendererMixin {
@Nullable
private RenderTarget weatherTargetBak;
@Shadow
protected abstract void renderSnowAndRain(final LightTexture lightTexture, final float partialTicks, final double cameraX, final double cameraY, final double cameraZ);
@Inject(method = "renderLevel", at = @At("HEAD"))
private void prepareDepthRendering(final CallbackInfo ci) {
@@ -79,15 +82,18 @@ public abstract class LevelRendererMixin {
) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
// If we're rendering depth, we can skip most of the rest here: we don't need destruction progress,
// transparency, hit result, debug stuff, clouds or weather.
// transparency, hit result, debug stuff, clouds.
cleanupDepthRendering();
// We do want particles though, because that's a neat effect.
// We do want particles and weather (rain) though, because that's a neat effect.
final MultiBufferSource.BufferSource bufferSource = renderBuffers.bufferSource();
minecraft.particleEngine.render(stack, bufferSource, lightTexture, camera, partialTicks, cullingFrustum);
bufferSource.endBatch();
final Vec3 cameraPosition = camera.getPosition();
renderSnowAndRain(lightTexture, partialTicks, cameraPosition.x(), cameraPosition.y(), cameraPosition.z());
// Clean up anything regular return would also clean up.
bufferSource.endBatch();
RenderSystem.depthMask(true);
RenderSystem.disableBlend();
RenderSystem.applyModelViewMatrix();
@@ -100,6 +106,19 @@ public abstract class LevelRendererMixin {
}
}
/**
* Make sure weather effects are rendered with depth, so they cause "shadows" in our projection.
*/
@Inject(method = "renderSnowAndRain", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;depthMask(Z)V", shift = At.Shift.AFTER))
private void enableDepthForWeatherInDepthBuffer(final CallbackInfo ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
RenderSystem.depthMask(true);
}
}
/**
* Don't render outlines while rendering projector depth.
*/
@Inject(method = "shouldShowEntityOutlines", at = @At("HEAD"), cancellable = true)
private void skipOutlines(final CallbackInfoReturnable<Boolean> cir) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
@@ -107,6 +126,9 @@ public abstract class LevelRendererMixin {
}
}
/**
* Skip rendering the sky when rendering projector depth.
*/
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
private void skipSky(final CallbackInfo ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {

View File

@@ -1,6 +1,7 @@
package li.cil.oc2.common.mixin;
import com.mojang.blaze3d.pipeline.RenderTarget;
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
import li.cil.oc2.common.ext.MinecraftExt;
import net.minecraft.client.Minecraft;
import org.jetbrains.annotations.Nullable;
@@ -18,10 +19,26 @@ public abstract class MinecraftMixin implements MinecraftExt {
mainRenderTargetOverride = renderTarget;
}
@Inject(method = "getMainRenderTarget", at = @At(value = "HEAD"), cancellable = true)
/**
* Redirect everything into the correct buffer while rendering projector depth.
* <p>
* Some things in level rendering may try to re-bind the main render target, so
* we catch that and ensure we bind the projector depth buffer again.
*/
@Inject(method = "getMainRenderTarget", at = @At("HEAD"), cancellable = true)
private void getMainRenderTargetOverride(final CallbackInfoReturnable<RenderTarget> cir) {
if (mainRenderTargetOverride != null) {
cir.setReturnValue(mainRenderTargetOverride);
}
}
/**
* Avoid access to render targets that are null while rendering projector depth to skip some work.
*/
@Inject(method = "useShaderTransparency", at = @At("HEAD"), cancellable = true)
private static void noTransparencyWhileRenderingProjectorDepth(final CallbackInfoReturnable<Boolean> cir) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
cir.setReturnValue(false);
}
}
}

View File

@@ -1,18 +1,19 @@
{
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"required": true,
"package": "li.cil.oc2.common.mixin",
"refmap": "oc2.refmap.json",
"mixins": [
"ChunkAccessMixin",
"ChunkMapMixin"
],
"client": [
"LevelRendererMixin",
"MinecraftMixin"
],
"injectors": {
"defaultRequire": 1
}
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"required": true,
"package": "li.cil.oc2.common.mixin",
"refmap": "oc2.refmap.json",
"mixins": [
"ChunkAccessMixin",
"ChunkMapMixin"
],
"client": [
"FrustumMixin",
"LevelRendererMixin",
"MinecraftMixin"
],
"injectors": {
"defaultRequire": 1
}
}