Use actual projection rendering for projectors.

This commit is contained in:
Florian Nücke
2022-02-01 20:18:55 +01:00
parent 59a3a2b127
commit 2361a84b8e
12 changed files with 938 additions and 562 deletions

View File

@@ -1,16 +1,12 @@
package li.cil.oc2.client.renderer;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import li.cil.oc2.api.API;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
@OnlyIn(Dist.CLIENT)
public abstract class ModRenderType extends RenderType {
@@ -38,7 +34,6 @@ public abstract class ModRenderType extends RenderType {
true,
CompositeState.builder()
.setShaderState(RENDERTYPE_LIGHTNING_SHADER)
.setOutputState(WEATHER_TARGET)
.setTransparencyState(LIGHTNING_TRANSPARENCY)
.setWriteMaskState(COLOR_WRITE)
.setCullState(NO_CULL)
@@ -59,7 +54,6 @@ public abstract class ModRenderType extends RenderType {
final RenderType.CompositeState state = RenderType.CompositeState.builder()
.setShaderState(POSITION_TEX_SHADER)
.setTextureState(texture)
.setOutputState(TRANSLUCENT_TARGET)
.setTransparencyState(ADDITIVE_TRANSPARENCY)
.setCullState(NO_CULL)
.createCompositeState(false);
@@ -91,38 +85,8 @@ public abstract class ModRenderType extends RenderType {
state);
}
public static RenderType getProjector(final DynamicTexture texture) {
final RenderType.CompositeState state = RenderType.CompositeState.builder()
.setShaderState(POSITION_TEX_SHADER)
.setTextureState(new DynamicTextureStateShard(texture))
.setOutputState(TRANSLUCENT_TARGET)
.setTransparencyState(ADDITIVE_TRANSPARENCY)
.setCullState(NO_CULL)
.createCompositeState(false);
return create(
API.MOD_ID + "/projector",
DefaultVertexFormat.POSITION_TEX,
VertexFormat.Mode.QUADS,
256,
false,
true,
state);
}
///////////////////////////////////////////////////////////////////
private static final class DynamicTextureStateShard extends EmptyTextureStateShard {
public DynamicTextureStateShard(final DynamicTexture texture) {
super(() -> {
RenderSystem.enableTexture();
// TODO client side setting
// texture.setFilter(true, false);
RenderSystem.setShaderTexture(0, texture.getId());
}, () -> {
});
}
}
private ModRenderType(final String name, final VertexFormat format, final VertexFormat.Mode drawMode, final int bufferSize, final boolean useDelegate, final boolean needsSorting, final Runnable setupTask, final Runnable clearTask) {
super(name, format, drawMode, bufferSize, useDelegate, needsSorting, setupTask, clearTask);
}

View File

@@ -0,0 +1,66 @@
package li.cil.oc2.client.renderer;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.math.Matrix4f;
import li.cil.oc2.api.API;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterShadersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import javax.annotation.Nullable;
import java.io.IOException;
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public final class ModShaders {
public static final int MAX_PROJECTORS = 3;
private static final ResourceLocation PROJECTORS_SHADER_LOCATION = new ResourceLocation(API.MOD_ID, "projectors");
private static final String[] PROJECTOR_CAMERA_NAMES = {"ProjectorCamera0", "ProjectorCamera1", "ProjectorCamera2"};
///////////////////////////////////////////////////////////////////
private static ShaderInstance projectorsShader;
///////////////////////////////////////////////////////////////////
@Nullable
public static ShaderInstance getProjectorsShader() {
return projectorsShader;
}
public static void configureProjectorsShader(
final RenderTarget target,
final Matrix4f inverseCameraMatrix,
final DynamicTexture[] colors,
final RenderTarget[] depths,
final Matrix4f[] projectorCameraMatrices,
final int count
) {
final int projectorCount = Math.min(count, MAX_PROJECTORS);
projectorsShader.safeGetUniform("Count").set(projectorCount);
RenderSystem.setShaderTexture(0, target.getDepthTextureId());
projectorsShader.safeGetUniform("InverseMainCamera").set(inverseCameraMatrix);
for (int i = 0; i < projectorCount; i++) {
RenderSystem.setShaderTexture(1 + i * 2, colors[i].getId());
RenderSystem.setShaderTexture(1 + i * 2 + 1, depths[i].getDepthTextureId());
projectorsShader.safeGetUniform(PROJECTOR_CAMERA_NAMES[i]).set(projectorCameraMatrices[i]);
}
}
@SubscribeEvent
public static void handleRegisterShaders(final RegisterShadersEvent event) throws IOException {
event.registerShader(new ShaderInstance(
event.getResourceManager(),
PROJECTORS_SHADER_LOCATION,
DefaultVertexFormat.POSITION_TEX
), instance -> projectorsShader = instance);
}
}

View File

@@ -1,14 +1,28 @@
package li.cil.oc2.client.renderer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import com.mojang.blaze3d.pipeline.MainTarget;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.platform.TextureUtil;
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;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice;
import li.cil.oc2.common.ext.MinecraftExt;
import li.cil.oc2.common.util.FakePlayerUtils;
import li.cil.oc2.jcodec.common.model.Picture;
import li.cil.oc2.jcodec.scale.Yuv420jToRgb;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
@@ -16,115 +30,146 @@ import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.FogRenderer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.client.event.EntityViewRenderEvent;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.client.event.RenderLevelLastEvent;
import net.minecraftforge.client.event.RenderNameplateEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.lwjgl.opengl.GL11.GL_NONE;
import static org.lwjgl.opengl.GL11.glDrawBuffer;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
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];
private static final DynamicTexture[] PROJECTOR_COLOR_TARGETS = new DynamicTexture[ModShaders.MAX_PROJECTORS];
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 Cache<ProjectorBlockEntity, RenderInfo> RENDER_INFO = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofSeconds(5))
.removalListener(ProjectorDepthRenderer::handleProjectorNoLongerRendering)
.build();
private static final TextureTarget projectorDepthTarget = new TextureTarget(DEPTH_CAPTURE_SIZE, DEPTH_CAPTURE_SIZE, true, Minecraft.ON_OSX);
private static final ProjectorDepthCamera camera = new ProjectorDepthCamera();
private static boolean isRenderingProjectorDepth;
private static HitResult hitResultBak;
private static boolean entityShadowsBak;
private static void handleProjectorNoLongerRendering(final RemovalNotification<ProjectorBlockEntity, RenderInfo> notification) {
final ProjectorBlockEntity projector = notification.getKey();
if (projector != null) {
projector.setFrameConsumer(null);
}
final RenderInfo renderInfo = notification.getValue();
if (renderInfo != null) {
renderInfo.close();
}
}
static {
for (int i = 0; i < ModShaders.MAX_PROJECTORS; i++) {
PROJECTOR_DEPTH_TARGETS[i] = new DepthOnlyRenderTarget(DEPTH_CAPTURE_SIZE, DEPTH_CAPTURE_SIZE);
PROJECTOR_CAMERA_MATRICES[i] = new Matrix4f();
}
}
///////////////////////////////////////////////////////////////////
/**
* Adds a projector that is being rendered this frame. This is called every frame a projector is rendering,
* the list of rendering projectors is cleared at the end of every frame.
*/
public static void addProjector(final ProjectorBlockEntity projector) {
VISIBLE_PROJECTORS.add(projector);
}
/**
* Whether we're currently rendering projector depth maps.
* <p>
* This is used in a couple of events and mixins, used to suppress regular rendering of things not needed in the
* depth buffer.
*/
public static boolean isIsRenderingProjectorDepth() {
return isRenderingProjectorDepth;
}
/**
* Called from a mixin in the {@link LevelRenderer#renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f)}
* method to grab the current depth buffer. This is necessary, because the depth buffer may be messed up by other
* render passes when using the "Fabulous!" graphics mode.
* <p>
* Called before {@link #handleRenderLevelLast(RenderLevelLastEvent)} every frame.
*/
public static void captureMainCameraDepth() {
final RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget();
if (mainRenderTarget.width != MAIN_CAMERA_DEPTH.width || mainRenderTarget.height != MAIN_CAMERA_DEPTH.height) {
MAIN_CAMERA_DEPTH.resize(mainRenderTarget.width, mainRenderTarget.height, Minecraft.ON_OSX);
}
MAIN_CAMERA_DEPTH.copyDepthFrom(mainRenderTarget);
mainRenderTarget.bindWrite(false);
}
/**
* Renders the projected images of {@link ProjectorBlockEntity} instances that were registered via
* {@link #addProjector(ProjectorBlockEntity)} this frame.
*/
@SubscribeEvent
public static void handleRenderLevelLast(final RenderLevelLastEvent event) {
final Minecraft minecraft = Minecraft.getInstance();
if (!(minecraft instanceof MinecraftExt minecraftExt)) {
if (VISIBLE_PROJECTORS.isEmpty()) {
return;
}
final ClientLevel level = minecraft.level;
final LocalPlayer player = minecraft.player;
if (level == null || player == null) {
return;
}
try {
isRenderingProjectorDepth = true;
RenderSystem.backupProjectionMatrix();
final BlockPos blockPos = player.eyeBlockPosition();
camera.configure(level, blockPos.relative(Direction.UP), player.getDirection().toYRot());
minecraft.setCameraEntity(camera.getEntity());
final PoseStack viewModelStack;
viewModelStack = new PoseStack();
viewModelStack.mulPose(Vector3f.XP.rotationDegrees(camera.getXRot()));
viewModelStack.mulPose(Vector3f.YP.rotationDegrees(camera.getYRot() + 180));
final Matrix3f viewRotationMatrix = viewModelStack.last().normal().copy();
if (viewRotationMatrix.invert()) {
RenderSystem.setInverseViewRotationMatrix(viewRotationMatrix);
final Minecraft minecraft = Minecraft.getInstance();
final ClientLevel level = minecraft.level;
final LocalPlayer player = minecraft.player;
if (level == null || player == null) {
return;
}
projectorDepthTarget.bindWrite(true);
minecraftExt.setMainRenderTargetOverride(projectorDepthTarget);
VISIBLE_PROJECTORS.sort((projector1, projector2) -> {
final double distance1 = player.distanceToSqr(Vec3.atCenterOf(projector1.getBlockPos()));
final double distance2 = player.distanceToSqr(Vec3.atCenterOf(projector2.getBlockPos()));
return Double.compare(distance1, distance2);
});
final Matrix4f projectionMatrix = Matrix4f.perspective(90, 1, 0.05f, 64f);
RenderSystem.setProjectionMatrix(projectionMatrix);
final LevelRenderer levelRenderer = event.getLevelRenderer();
levelRenderer.prepareCullFrustum(viewModelStack, camera.getPosition(), projectionMatrix);
levelRenderer.renderLevel(
viewModelStack,
event.getPartialTick(),
event.getStartNanos(),
/* shouldRenderBlockOutline: */ false,
camera,
minecraft.gameRenderer,
minecraft.gameRenderer.lightTexture(),
projectionMatrix
);
final int projectorCount = Math.min(VISIBLE_PROJECTORS.size(), ModShaders.MAX_PROJECTORS);
renderProjectorDepths(minecraft, level, player, event.getPartialTick(), event.getStartNanos(), projectorCount);
renderProjectorColors(minecraft, event.getPoseStack().last().pose(), event.getProjectionMatrix(), projectorCount);
} finally {
RenderSystem.restoreProjectionMatrix();
minecraftExt.setMainRenderTargetOverride(null);
minecraft.getMainRenderTarget().bindWrite(true);
minecraft.setCameraEntity(player);
isRenderingProjectorDepth = false;
VISIBLE_PROJECTORS.clear();
}
}
// TODO [PERF] override any vertex formats to only use pos
@SubscribeEvent
public static void handleRenderGameOverlay(final RenderGameOverlayEvent.Post event) {
if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) {
return;
}
final PoseStack stack = event.getMatrixStack();
stack.pushPose();
final Tesselator tesselator = Tesselator.getInstance();
final BufferBuilder builder = tesselator.getBuilder();
RenderSystem.setShader(GameRenderer::getPositionTexShader);
RenderSystem.setShaderTexture(0, projectorDepthTarget.getDepthTextureId());
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
builder.vertex(50, 50, 0).uv(0, 0).color(0xFFFF0000).endVertex(); // top left
builder.vertex(50, 50 + DEPTH_CAPTURE_SIZE / 2, 0).uv(0, 1).color(0xFF00FF00).endVertex(); // bottom left
builder.vertex(50 + DEPTH_CAPTURE_SIZE / 2, 50 + DEPTH_CAPTURE_SIZE / 2, 0).uv(1, 1).color(0xFF0000FF).endVertex(); // bottom right
builder.vertex(50 + DEPTH_CAPTURE_SIZE / 2, 50, 0).uv(1, 0).color(0xFFFFFFFF).endVertex(); // top right
tesselator.end();
stack.popPose();
}
/**
* Suppresses fog rendering while rendering depth buffer for projectors.
*/
@SubscribeEvent
public static void handleFog(final EntityViewRenderEvent.RenderFogEvent event) {
if (isRenderingProjectorDepth) {
@@ -132,36 +177,368 @@ public final class ProjectorDepthRenderer {
}
}
private static final class ProjectorDepthCamera extends Camera {
public void configure(final Level level, final BlockPos blockPos, final float rotationY) {
final ProjectorEntity entity = new ProjectorEntity(level, blockPos, rotationY);
setup(level, entity, false, false, 0);
/**
* Suppresses nameplate rendering while rendering depth buffer for projectors.
*/
@SubscribeEvent
public static void handleNameplate(final RenderNameplateEvent event) {
if (isRenderingProjectorDepth) {
event.setResult(Event.Result.DENY);
}
}
/**
* Updates cached rendering info, such as textures holding image data for projectors, to allow expiration.
*/
@SubscribeEvent
public static void handleClientTick(final TickEvent.ClientTickEvent event) {
RENDER_INFO.cleanUp();
}
///////////////////////////////////////////////////////////////////
/**
* Stage one of projector rendering, render scene depths from the perspective of all projectors that should
* bre rendered. The output is the list of depth buffers, MVP matrices that were used to render them, and the
* associated color texture for the projector.
*/
private static void renderProjectorDepths(final Minecraft minecraft, final ClientLevel level, final Player player,
final float partialTicks, final long startNanos,
final int projectorCount) {
prepareDepthBufferRendering(minecraft);
try {
final PoseStack viewModelStack = new PoseStack();
for (int projectorIndex = 0; projectorIndex < projectorCount; projectorIndex++) {
final ProjectorBlockEntity projector = VISIBLE_PROJECTORS.get(projectorIndex);
final Direction facing = projector.getBlockState().getValue(ProjectorBlock.FACING);
final Vec3 projectorPos = Vec3
.atCenterOf(projector.getBlockPos())
.add(new Vec3(facing.step()).scale(7 / 16f));
configureProjectorDepthCamera(level, projectorPos, facing.toYRot());
RenderSystem.setProjectionMatrix(DEPTH_CAMERA_PROJECTION_MATRIX);
setupViewModelMatrix(viewModelStack);
storeProjectorMatrix(projectorIndex, projectorPos, viewModelStack);
bindProjectorDepthRenderTarget(projectorIndex, minecraft);
renderProjectorDepthBuffer(minecraft, partialTicks, startNanos, viewModelStack);
storeProjectorColorBuffer(projectorIndex, projector);
}
} finally {
finishDepthBufferRendering(minecraft, player);
}
}
private static void prepareDepthBufferRendering(final Minecraft minecraft) {
isRenderingProjectorDepth = true;
// Suppresses hit outlines being rendered.
hitResultBak = minecraft.hitResult;
minecraft.hitResult = null;
// Skip shadow rendering for perf.
entityShadowsBak = minecraft.options.entityShadows;
minecraft.options.entityShadows = false;
minecraft.setCameraEntity(PROJECTOR_DEPTH_CAMERA.getEntity());
RenderSystem.backupProjectionMatrix();
}
private static void finishDepthBufferRendering(final Minecraft minecraft, final Player player) {
minecraft.hitResult = hitResultBak;
minecraft.options.entityShadows = entityShadowsBak;
RenderSystem.restoreProjectionMatrix();
((MinecraftExt) minecraft).setMainRenderTargetOverride(null);
minecraft.getMainRenderTarget().bindWrite(true);
minecraft.setCameraEntity(player);
isRenderingProjectorDepth = false;
}
private static void configureProjectorDepthCamera(final ClientLevel level, final Vec3 pos, final float rotationY) {
PROJECTOR_DEPTH_CAMERA.setup(level, ProjectorCameraEntity.get(level, pos, rotationY), false, false, 0);
}
private static void setupViewModelMatrix(final PoseStack viewModelStack) {
final Quaternion rotation = Vector3f.YP.rotationDegrees(PROJECTOR_DEPTH_CAMERA.getYRot() + 180);
viewModelStack.setIdentity();
viewModelStack.mulPose(rotation);
final Matrix3f viewRotationMatrix = viewModelStack.last().normal().copy();
if (viewRotationMatrix.invert()) {
RenderSystem.setInverseViewRotationMatrix(viewRotationMatrix);
}
}
private static void storeProjectorMatrix(final int projectorIndex, final Vec3 projectorPos, final PoseStack viewModelStack) {
// Save model-view-projection matrix for mapping in compositing shader. Shifted by position of the
// projector, to ensure we use the same origin for projectors and the main camera in compositing.
PROJECTOR_CAMERA_MATRICES[projectorIndex].load(DEPTH_CAMERA_PROJECTION_MATRIX);
viewModelStack.pushPose();
viewModelStack.translate(-projectorPos.x(), -projectorPos.y(), -projectorPos.z());
PROJECTOR_CAMERA_MATRICES[projectorIndex].multiply(viewModelStack.last().pose());
viewModelStack.popPose();
}
private static void bindProjectorDepthRenderTarget(final int projectorIndex, final Minecraft minecraft) {
final DepthOnlyRenderTarget projectorDepthTarget = PROJECTOR_DEPTH_TARGETS[projectorIndex];
projectorDepthTarget.bindWrite(true);
((MinecraftExt) minecraft).setMainRenderTargetOverride(projectorDepthTarget);
}
private static void renderProjectorDepthBuffer(final Minecraft minecraft, final float partialTicks, final long startNanos, final PoseStack viewModelStack) {
final LevelRenderer levelRenderer = minecraft.levelRenderer;
levelRenderer.prepareCullFrustum(
viewModelStack,
PROJECTOR_DEPTH_CAMERA.getPosition(),
DEPTH_CAMERA_PROJECTION_MATRIX
);
levelRenderer.renderLevel(
viewModelStack,
partialTicks,
startNanos,
/* shouldRenderBlockOutline: */ false,
PROJECTOR_DEPTH_CAMERA,
minecraft.gameRenderer,
minecraft.gameRenderer.lightTexture(),
DEPTH_CAMERA_PROJECTION_MATRIX
);
}
private static void storeProjectorColorBuffer(final int projectorIndex, final ProjectorBlockEntity projector) {
PROJECTOR_COLOR_TARGETS[projectorIndex] = getRenderInfo(projector).texture();
}
/**
* Stage two or projector rendering, this composes the projections of the projectors being rendered into the
* main render target, using the camera matrices and depth information to determine where to render. This is
* essentially a post-processing pass, i.e. it renders a screen-filling rectangle blending the projector light
* into the existing main render target output.
*/
private static void renderProjectorColors(final Minecraft minecraft, final Matrix4f modelViewMatrix, final Matrix4f projectionMatrix, final int projectorCount) {
prepareColorBufferRendering();
try {
prepareOrthographicRendering(minecraft);
RenderSystem.setShader(ModShaders::getProjectorsShader);
ModShaders.configureProjectorsShader(
MAIN_CAMERA_DEPTH,
constructInverseMainCameraMatrix(minecraft, modelViewMatrix, projectionMatrix),
PROJECTOR_COLOR_TARGETS,
PROJECTOR_DEPTH_TARGETS,
PROJECTOR_CAMERA_MATRICES,
projectorCount
);
renderIntoScreenRect();
} finally {
finishColorBufferRendering();
}
}
private static void prepareColorBufferRendering() {
RenderSystem.backupProjectionMatrix();
RenderSystem.getModelViewStack().pushPose();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE);
RenderSystem.colorMask(true, true, true, false);
RenderSystem.disableDepthTest();
RenderSystem.depthMask(false);
}
private static void finishColorBufferRendering() {
RenderSystem.depthMask(true);
RenderSystem.enableDepthTest();
RenderSystem.colorMask(true, true, true, true);
RenderSystem.disableBlend();
RenderSystem.restoreProjectionMatrix();
RenderSystem.getModelViewStack().popPose();
RenderSystem.applyModelViewMatrix();
}
private static void prepareOrthographicRendering(final Minecraft minecraft) {
final Matrix4f screenProjectionMatrix = Matrix4f.orthographic(
minecraft.getWindow().getWidth(),
-minecraft.getWindow().getHeight(),
1000, 3000
);
RenderSystem.setProjectionMatrix(screenProjectionMatrix);
final PoseStack modelViewStack = RenderSystem.getModelViewStack();
modelViewStack.setIdentity();
modelViewStack.translate(0, 0, -2000);
RenderSystem.applyModelViewMatrix();
}
private static Matrix4f constructInverseMainCameraMatrix(final Minecraft minecraft, final Matrix4f modelViewMatrix, final Matrix4f projectionMatrix) {
final Camera mainCamera = minecraft.gameRenderer.getMainCamera();
final Vec3 mainCameraPosition = mainCamera.getPosition();
final Matrix4f inverseModelViewMatrix = projectionMatrix.copy();
inverseModelViewMatrix.multiply(modelViewMatrix);
inverseModelViewMatrix.multiplyWithTranslation(-(float) mainCameraPosition.x(), -(float) mainCameraPosition.y(), -(float) mainCameraPosition.z());
inverseModelViewMatrix.invert();
return inverseModelViewMatrix;
}
private static void renderIntoScreenRect() {
final Tesselator tesselator = Tesselator.getInstance();
final BufferBuilder builder = tesselator.getBuilder();
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
builder.vertex(0, 0, 0).uv(0, 1).endVertex();
builder.vertex(0, MAIN_CAMERA_DEPTH.height, 0).uv(0, 0).endVertex();
builder.vertex(MAIN_CAMERA_DEPTH.width, MAIN_CAMERA_DEPTH.height, 0).uv(1, 0).endVertex();
builder.vertex(MAIN_CAMERA_DEPTH.width, 0, 0).uv(1, 1).endVertex();
tesselator.end();
}
private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) {
try {
return RENDER_INFO.get(projector, () -> {
final DynamicTexture texture = new DynamicTexture(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, false);
final RenderInfo renderInfo = new RenderInfo(texture);
projector.setFrameConsumer(renderInfo);
return renderInfo;
});
} catch (final ExecutionException e) {
throw new RuntimeException(e);
}
}
///////////////////////////////////////////////////////////////////
/**
* Tracks a render texture holding color info for rendering projectors.
* <p>
* Automatically updated by projectors when new data arrives (from a worker thread).
*/
private record RenderInfo(DynamicTexture texture) implements ProjectorBlockEntity.FrameConsumer {
private static final ThreadLocal<byte[]> RGB = ThreadLocal.withInitial(() -> new byte[3]);
public synchronized void close() {
texture.close();
}
private static final class ProjectorEntity extends Player {
public ProjectorEntity(final Level level, final BlockPos blockPos, final float rotationY) {
super(level, blockPos, rotationY, FakePlayerUtils.getFakePlayerProfile());
@Override
public synchronized void processFrame(final Picture picture) {
final NativeImage image = texture.getPixels();
if (image == null) {
return;
}
@Override
public float getViewYRot(final float partialTicks) {
return yRotO;
final byte[] y = picture.getPlaneData(0);
final byte[] u = picture.getPlaneData(1);
final byte[] v = picture.getPlaneData(2);
// Convert in quads, based on the half resolution of UV. As such, skip every other row, since
// we're setting the current and the next.
int lumaIndex = 0, chromaIndex = 0;
for (int halfRow = 0; halfRow < ProjectorVMDevice.HEIGHT / 2; halfRow++, lumaIndex += ProjectorVMDevice.WIDTH * 2) {
final int row = halfRow * 2;
for (int halfCol = 0; halfCol < ProjectorVMDevice.WIDTH / 2; halfCol++, chromaIndex++) {
final int col = halfCol * 2;
final int yIndex = lumaIndex + col;
final byte cb = u[chromaIndex];
final byte cr = v[chromaIndex];
setFromYUV420(image, col, row, y[yIndex], cb, cr);
setFromYUV420(image, col + 1, row, y[yIndex + 1], cb, cr);
setFromYUV420(image, col, row + 1, y[yIndex + ProjectorVMDevice.WIDTH], cb, cr);
setFromYUV420(image, col + 1, row + 1, y[yIndex + ProjectorVMDevice.WIDTH + 1], cb, cr);
}
}
@Override
public float getViewXRot(final float partialTicks) {
return xRotO;
}
texture.upload();
}
@Override
public boolean isSpectator() {
return false;
}
private static void setFromYUV420(final NativeImage image, final int col, final int row, final byte y, final byte cb, final byte cr) {
final byte[] bytes = RGB.get();
Yuv420jToRgb.YUVJtoRGB(y, cb, cr, bytes, 0);
final int r = bytes[0] + 128;
final int g = bytes[1] + 128;
final int b = bytes[2] + 128;
image.setPixelRGBA(col, row, r | (g << 8) | (b << 16) | (0xFF << 24));
}
}
@Override
public boolean isCreative() {
return true;
/**
* Optimized texture target that doesn't have a color texture, so we can completely skip that when rendering
* projector depth buffers.
*/
private static final class DepthOnlyRenderTarget extends TextureTarget {
public DepthOnlyRenderTarget(final int width, final int height) {
super(width, height, true, Minecraft.ON_OSX);
}
@Override
public void createBuffers(final int width, final int height, final boolean isOnOSX) {
super.createBuffers(width, height, isOnOSX);
if (colorTextureId > -1) {
if (frameBufferId > -1) {
glBindFramebuffer(GL_FRAMEBUFFER, frameBufferId);
glDrawBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
TextureUtil.releaseTextureId(this.colorTextureId);
this.colorTextureId = -1;
}
}
}
/**
* Fake entity used as rendering context when rendering projector depth buffers.
*/
private static final class ProjectorCameraEntity extends Player {
private static ProjectorCameraEntity instance;
/**
* Singleton getter for the fake render entity, to avoid unnecessary allocations.
*/
public static ProjectorCameraEntity get(final Level level, final Vec3 pos, final float rotationY) {
if (instance == null) {
instance = new ProjectorCameraEntity(level, BlockPos.ZERO, rotationY);
}
instance.level = level;
instance.moveTo(pos.x(), pos.y(), pos.z(), rotationY, 0);
return instance;
}
private ProjectorCameraEntity(final Level level, final BlockPos blockPos, final float rotationY) {
super(level, blockPos, rotationY, FakePlayerUtils.getFakePlayerProfile());
}
@Override
public float getViewYRot(final float partialTicks) {
return yRotO;
}
@Override
public float getViewXRot(final float partialTicks) {
return xRotO;
}
@Override
public boolean isSpectator() {
return false;
}
@Override
public boolean isCreative() {
return true;
}
}
}

View File

@@ -1,10 +1,5 @@
package li.cil.oc2.client.renderer.blockentity;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Matrix4f;
@@ -12,26 +7,16 @@ import com.mojang.math.Quaternion;
import com.mojang.math.Vector3f;
import li.cil.oc2.api.API;
import li.cil.oc2.client.renderer.ModRenderType;
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
import li.cil.oc2.common.block.ProjectorBlock;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.time.Duration;
import java.util.BitSet;
import java.util.concurrent.ExecutionException;
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlockEntity> {
private static final int LIGHT_COLOR_NEAR = 0x22FFFFFF;
@@ -44,11 +29,6 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
private static final float LENS_BOTTOM = 0 + 4 / 16f;
private static final float LENS_TOP = 1 - 4 / 16f;
private static final Cache<ProjectorBlockEntity, RenderInfo> textures = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofSeconds(5))
.removalListener(ProjectorRenderer::handleNoLongerRendering)
.build();
///////////////////////////////////////////////////////////////////
public ProjectorRenderer(final BlockEntityRendererProvider.Context ignored) {
@@ -71,7 +51,7 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
stack.translate(0.5f, 0, 0.5f);
stack.mulPose(rotation);
renderProjections(projector, stack, bufferSource);
ProjectorDepthRenderer.addProjector(projector);
renderProjectorLight(stack, bufferSource);
@@ -80,70 +60,6 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
///////////////////////////////////////////////////////////////////
private void renderProjections(final ProjectorBlockEntity projector, final PoseStack stack, final MultiBufferSource bufferSource) {
final ProjectorBlockEntity.VisibilityData visibilityData = projector.getVisibilityData();
final BitSet[] visibilities = visibilityData.visibilities();
if (hasNoVisibleTiles(visibilities)) {
return;
}
final BlockPos projectorPos = projector.getBlockPos();
final Frustum frustum = new Frustum(stack.last().pose(), RenderSystem.getProjectionMatrix());
frustum.prepare(projectorPos.getX(), projectorPos.getY(), projectorPos.getZ());
if (!frustum.isVisible(visibilityData.visibilityBounds())) {
return;
}
stack.pushPose();
stack.translate(0, 0, 0.49);
final RenderType renderType = getUpdatedRenderType(projector);
final VertexConsumer consumer = bufferSource.getBuffer(renderType);
for (int distance = 0; distance < visibilities.length; distance++) {
final BitSet visibility = visibilities[distance];
if (!visibility.isEmpty()) {
stack.pushPose();
stack.translate(0, 0, distance + 1);
renderProjection(stack, consumer, ProjectorBlockEntity.getLayerSize(distance), visibility);
stack.popPose();
}
}
stack.popPose();
}
private void renderProjection(final PoseStack stack, final VertexConsumer consumer, final ProjectorBlockEntity.LayerSize layerSize, final BitSet visibility) {
final float width = layerSize.width();
final float height = layerSize.height();
final int discreteWidth = layerSize.discreteWidth();
final int discreteHeight = layerSize.discreteHeight();
final float uOffset = (discreteHeight / height - 1) / 2f;
final float vOffset = (discreteWidth / width - 1) / 2f;
stack.translate(-width / 2.0, (discreteHeight - height) / 2.0, 0);
stack.scale(width, height, 1);
final Matrix4f matrix = stack.last().pose();
for (int index = visibility.nextSetBit(0); index >= 0; index = visibility.nextSetBit(index + 1)) {
final int x = index % discreteWidth;
final int y = index / discreteWidth;
final float u0 = Math.max(0, x / width - vOffset);
final float u1 = Math.min(1, (x + 1) / width - vOffset);
final float v0 = Math.max(0, y / height - uOffset);
final float v1 = Math.min(1, (y + 1) / height - uOffset);
consumer.vertex(matrix, u0, v0, 0).uv(1 - u0, 1 - v0).endVertex();
consumer.vertex(matrix, u1, v0, 0).uv(1 - u1, 1 - v0).endVertex();
consumer.vertex(matrix, u1, v1, 0).uv(1 - u1, 1 - v1).endVertex();
consumer.vertex(matrix, u0, v1, 0).uv(1 - u0, 1 - v1).endVertex();
}
}
private void renderProjectorLight(final PoseStack stack, final MultiBufferSource bufferSource) {
stack.pushPose();
@@ -206,54 +122,4 @@ public final class ProjectorRenderer implements BlockEntityRenderer<ProjectorBlo
consumer.vertex(matrix, ledLeft, ledTop, ledDepth).color(LED_COLOR).endVertex();
consumer.vertex(matrix, ledRight, ledTop, ledDepth).color(LED_COLOR).endVertex();
}
private static boolean hasNoVisibleTiles(final BitSet[] visibilities) {
for (final BitSet visibility : visibilities) {
if (!visibility.isEmpty()) {
return false;
}
}
return true;
}
private static RenderType getUpdatedRenderType(final ProjectorBlockEntity projector) {
final RenderInfo renderInfo = getRenderInfo(projector);
final NativeImage image = renderInfo.texture().getPixels();
assert image != null;
if (projector.updateRenderTexture(image::setPixelRGBA)) {
renderInfo.texture().upload();
}
return renderInfo.renderType();
}
private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) {
try {
return ProjectorRenderer.textures.get(projector, () -> {
final DynamicTexture texture = new DynamicTexture(
ProjectorVMDevice.WIDTH,
ProjectorVMDevice.HEIGHT,
false
);
return new RenderInfo(texture, ModRenderType.getProjector(texture));
}
);
} catch (final ExecutionException e) {
throw new RuntimeException(e);
}
}
@SubscribeEvent
public static void updateCache(final TickEvent.ClientTickEvent event) {
textures.cleanUp();
}
private static void handleNoLongerRendering(final RemovalNotification<ProjectorBlockEntity, RenderInfo> notification) {
final RenderInfo renderInfo = notification.getValue();
assert renderInfo != null;
renderInfo.texture().close();
}
private record RenderInfo(DynamicTexture texture, RenderType renderType) { }
}

View File

@@ -1,8 +1,7 @@
package li.cil.oc2.common.block;
import li.cil.oc2.common.blockentity.BlockEntities;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.util.BlockEntityUtils;
import li.cil.oc2.common.blockentity.TickableBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.context.BlockPlaceContext;
@@ -40,11 +39,7 @@ public final class ProjectorBlock extends HorizontalDirectionalBlock implements
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(final Level level, final BlockState state, final BlockEntityType<T> type) {
if (!level.isClientSide()) {
return BlockEntityUtils.createTicker(type, BlockEntities.PROJECTOR.get(), ProjectorBlockEntity::serverTick);
} else {
return null;
}
return TickableBlockEntity.createServerTicker(level, type, BlockEntities.PROJECTOR.get());
}
@Override

View File

@@ -13,23 +13,18 @@ import li.cil.oc2.jcodec.codecs.h264.H264Encoder;
import li.cil.oc2.jcodec.codecs.h264.encode.CQPRateControl;
import li.cil.oc2.jcodec.common.model.ColorSpace;
import li.cil.oc2.jcodec.common.model.Picture;
import li.cil.oc2.jcodec.scale.Yuv420jToRgb;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.BitSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -41,18 +36,19 @@ import java.util.zip.Inflater;
// TODO Only send frames to watching clients (have clients send "keepalive" packets when rendering this).
// TODO Throttle update speed by distance to closest player and max number any player watching this projector is watching in total.
public final class ProjectorBlockEntity extends ModBlockEntity {
public final class ProjectorBlockEntity extends ModBlockEntity implements TickableBlockEntity {
@FunctionalInterface
public interface FramebufferPixelSetter {
void set(final int x, final int y, final int rgba);
public interface FrameConsumer {
void processFrame(final Picture picture);
}
///////////////////////////////////////////////////////////////
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.
private static final LayerSize[] LAYER_SIZES = computeLayerSizes();
private static final int FRAME_EVERY_N_TICKS = 5;
@@ -74,7 +70,7 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
// Video transfer.
private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12));
private Future<?> runningEncode; // To allow waiting for previous frame to finish.
@Nullable private Future<?> runningEncode; // To allow waiting for previous frame to finish.
private final Picture picture = Picture.create(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, ColorSpace.YUV420J);
private boolean needsIDR = true; // Whether we need to send a keyframe next.
@@ -83,12 +79,10 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
// Client only data.
private final H264Decoder decoder = new H264Decoder();
private Future<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
@Nullable private Future<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
private final ByteBuffer decoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer.
private volatile boolean isBufferDirty; // Whether buffer has changed and renderers need to update their texture.
@Nullable private FrameConsumer frameConsumer; // Where to throw received frames.
private final BitSet[] visibilities = new BitSet[MAX_RENDER_DISTANCE]; // Index of blocks we're projecting onto.
private AABB visibilityBounds; // Bounds of all blocks we're projecting onto.
private AABB renderBounds; // Overall render bounds, disregarding projection surface, to allow growing if necessary.
///////////////////////////////////////////////////////////////
@@ -98,20 +92,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
encoder.setKeyInterval(100);
for (int i = 0; i < visibilities.length; i++) {
visibilities[i] = new BitSet(MAX_WIDTH * MAX_HEIGHT);
}
visibilityBounds = super.getRenderBoundingBox();
updateRenderBounds();
}
///////////////////////////////////////////////////////////////
public static void serverTick(final Level ignoredLevel, final BlockPos ignoredPos, final BlockState ignoredState, final ProjectorBlockEntity projector) {
projector.serverTick();
}
public ProjectorVMDevice getProjectorDevice() {
return projectorDevice;
}
@@ -128,12 +113,86 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
Arrays.fill(picture.getPlaneData(1), (byte) 0);
Arrays.fill(picture.getPlaneData(2), (byte) 0);
needsIDR = true;
isBufferDirty = true;
}
sendRunningState();
}
public void setFrameConsumer(@Nullable final FrameConsumer consumer) {
if (consumer == frameConsumer) {
return;
}
synchronized (picture) {
this.frameConsumer = consumer;
if (frameConsumer != null) {
frameConsumer.processFrame(picture);
}
}
}
@Override
public void serverTick() {
if (!isProjecting()) {
return;
}
if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) {
if (hasEnergy) {
hasEnergy = false;
sendRunningState();
}
return;
} else if (!hasEnergy) {
hasEnergy = true;
sendRunningState();
}
sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick));
nextFrameIn = Math.max(0, nextFrameIn - 1);
if (sendBudget.get() < 0 || nextFrameIn > 0) {
return;
}
if (runningEncode != null && !runningEncode.isDone()) {
return;
}
joinWorkerAndLogErrors(runningEncode);
nextFrameIn = FRAME_EVERY_N_TICKS;
if (level == null || !(level.getChunk(getBlockPos()) instanceof LevelChunk chunk)) {
return;
}
runningEncode = FRAME_WORKERS.submit(() -> {
final boolean hasChanges = projectorDevice.applyChanges(picture);
if (!hasChanges && !needsIDR) {
return;
}
final ByteBuffer frameData;
if (needsIDR) {
frameData = encoder.encodeIDRFrame(picture, ByteBuffer.allocateDirect(256 * 1024));
needsIDR = false;
} else {
frameData = encoder.encodeFrame(picture, ByteBuffer.allocateDirect(256 * 1024)).data();
}
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
deflater.setInput(frameData);
deflater.finish();
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
deflater.end();
compressedFrameData.flip();
sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize);
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData);
Network.sendToClientsTrackingChunk(message, chunk);
});
}
@Override
public CompoundTag getUpdateTag() {
final CompoundTag tag = super.getUpdateTag();
@@ -178,64 +237,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
updateRenderBounds();
}
public record LayerSize(float width, float height, int discreteWidth, int discreteHeight) { }
public static LayerSize getLayerSize(final int distance) {
return LAYER_SIZES[distance];
}
public record VisibilityData(AABB visibilityBounds, BitSet[] visibilities) { }
public VisibilityData getVisibilityData() {
updateVisibilities();
return new VisibilityData(visibilityBounds, visibilities);
}
public boolean updateRenderTexture(final FramebufferPixelSetter setter) {
assert level != null;
final ProfilerFiller profiler = level.getProfiler();
profiler.push("updateRenderTexture");
if (!isBufferDirty) {
profiler.pop();
return false;
}
isBufferDirty = false;
final byte[] y = picture.getPlaneData(0);
final byte[] u = picture.getPlaneData(1);
final byte[] v = picture.getPlaneData(2);
// Convert in quads, based on the half resolution of UV. As such, skip every other row, since
// we're setting the current and the next.
int lumaIndex = 0, chromaIndex = 0;
for (int halfRow = 0; halfRow < ProjectorVMDevice.HEIGHT / 2; halfRow++, lumaIndex += ProjectorVMDevice.WIDTH * 2) {
final int row = halfRow * 2;
for (int halfCol = 0; halfCol < ProjectorVMDevice.WIDTH / 2; halfCol++, chromaIndex++) {
final int col = halfCol * 2;
final int yIndex = lumaIndex + col;
final byte cb = u[chromaIndex];
final byte cr = v[chromaIndex];
setFromYUV420(setter, col, row, y[yIndex], cb, cr);
setFromYUV420(setter, col + 1, row, y[yIndex + 1], cb, cr);
setFromYUV420(setter, col, row + 1, y[yIndex + ProjectorVMDevice.WIDTH], cb, cr);
setFromYUV420(setter, col + 1, row + 1, y[yIndex + ProjectorVMDevice.WIDTH + 1], cb, cr);
}
}
profiler.pop();
return true;
}
public void applyNextFrame(final ByteBuffer frameData) {
final Future<?> lastDecode = runningDecode;
runningDecode = FRAME_WORKERS.submit(() -> {
try {
if (lastDecode != null) {
lastDecode.get();
}
joinWorkerAndLogErrors(lastDecode);
final Inflater inflater = new Inflater();
inflater.setInput(frameData);
@@ -246,8 +252,12 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
decoder.decodeFrame(decoderBuffer, picture.getData());
isBufferDirty = true;
} catch (final InterruptedException | ExecutionException | DataFormatException ignored) {
synchronized (picture) {
if (frameConsumer != null) {
frameConsumer.processFrame(picture);
}
}
} catch (final DataFormatException ignored) {
}
});
}
@@ -267,62 +277,6 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
return Config.projectorEnergyStorage > 0 && Config.projectorEnergyPerTick > 0;
}
private void serverTick() {
if (!isProjecting()) {
return;
}
if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) {
if (hasEnergy) {
hasEnergy = false;
sendRunningState();
}
return;
} else if (!hasEnergy) {
hasEnergy = true;
sendRunningState();
}
sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick));
nextFrameIn = Math.max(0, nextFrameIn - 1);
if (sendBudget.get() < 0 || nextFrameIn > 0) {
return;
}
if (runningEncode != null && !runningEncode.isDone()) {
return;
}
nextFrameIn = FRAME_EVERY_N_TICKS;
runningEncode = FRAME_WORKERS.submit(() -> {
final boolean hasChanges = projectorDevice.applyChanges(picture);
if (!hasChanges && !needsIDR) {
return;
}
final ByteBuffer frameData;
if (needsIDR) {
frameData = encoder.encodeIDRFrame(picture, ByteBuffer.allocateDirect(256 * 1024));
needsIDR = false;
} else {
frameData = encoder.encodeFrame(picture, ByteBuffer.allocateDirect(256 * 1024)).data();
}
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
deflater.setInput(frameData);
deflater.finish();
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
deflater.end();
compressedFrameData.flip();
sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize);
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData);
Network.sendToClientsTrackingBlockEntity(message, this);
});
}
private void sendRunningState() {
if (level != null && !level.isClientSide()) {
Network.sendToClientsTrackingBlockEntity(new ProjectorStateMessage(this, isProjecting && hasEnergy), this);
@@ -337,118 +291,23 @@ public final class ProjectorBlockEntity extends ModBlockEntity {
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 screenMaxPos = screenBasePos.relative(screenLeft, MAX_WIDTH / 2)
// -1 for the MAX_HEIGHT padding, -1 for auto-expansion of AABB constructor
.relative(screenUp, MAX_HEIGHT - 2);
renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos));
}
/**
* Rebuild "stencil buffer" of blocks hit in projection direction, per block layer, up to max distance.
*/
private void updateVisibilities() {
final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING);
final Direction screenUp = Direction.UP;
final Direction screenLeft = blockFacing.getCounterClockWise();
final BlockPos projectorPos = getBlockPos();
final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE + 1);
final BlockPos screenOriginPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2);
final Vec3 toFaceCenter = new Vec3(blockFacing.getOpposite().step()).scale(0.45);
final Vec3 clipStartPos = Vec3.atCenterOf(projectorPos.relative(blockFacing)).subtract(toFaceCenter);
final Vec3 stepOrigin = Vec3.atCenterOf(screenOriginPos).add(toFaceCenter);
final Vec3 upStep = new Vec3(screenUp.step());
final Vec3 leftStep = new Vec3(screenLeft.step());
for (final BitSet bitSet : visibilities) {
bitSet.clear();
}
AABB bounds = new AABB(getBlockPos()).minmax(new AABB(getBlockPos().relative(blockFacing)));
final Level level = getLevel();
if (level == null) {
private static void joinWorkerAndLogErrors(@Nullable final Future<?> job) {
if (job == null) {
return;
}
for (int y = 0; y < MAX_HEIGHT + 1; y++) {
for (int x = 0; x < MAX_WIDTH; x++) {
final Vec3 clipEndPos = stepOrigin.add(upStep.scale(y)).add(leftStep.scale(x));
final ClipContext context = new ClipContext(clipStartPos, clipEndPos, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, null);
final BlockHitResult hit = level.clip(context);
if (hit.getType() == HitResult.Type.MISS) {
continue;
}
if (hit.getDirection() != blockFacing.getOpposite()) {
continue;
}
final BlockState blockState = level.getBlockState(hit.getBlockPos());
if (!blockState.isFaceSturdy(level, hit.getBlockPos(), blockFacing.getOpposite())) {
continue;
}
final BlockPos delta = hit.getBlockPos().subtract(projectorPos);
final int distance = Math.abs(delta.get(blockFacing.getAxis())) - 2;
if (distance >= visibilities.length) {
continue;
}
final int globalX = delta.get(screenLeft.getAxis());
final int globalY = delta.get(screenUp.getAxis());
final ProjectorBlockEntity.LayerSize layerSize = getLayerSize(distance);
final int discreteWidth = layerSize.discreteWidth();
final int discreteHeight = layerSize.discreteHeight();
if (globalY < 0 || globalY >= discreteHeight) {
continue;
}
final int localX = globalX + discreteWidth / 2;
if (localX < 0 || localX >= discreteWidth) {
continue;
}
bounds = bounds.minmax(new AABB(hit.getBlockPos()));
final int index = localX + globalY * discreteWidth;
visibilities[distance].set(index);
}
try {
job.get();
} catch (final InterruptedException ignored) {
} catch (final ExecutionException e) {
LOGGER.error(e);
}
visibilityBounds = bounds;
}
private static final ThreadLocal<byte[]> rgb = ThreadLocal.withInitial(() -> new byte[3]);
private static void setFromYUV420(final FramebufferPixelSetter setter, final int col, final int row, final byte y, final byte cb, final byte cr) {
final byte[] bytes = rgb.get();
Yuv420jToRgb.YUVJtoRGB(y, cb, cr, bytes, 0);
final int r = bytes[0] + 128;
final int g = bytes[1] + 128;
final int b = bytes[2] + 128;
setter.set(col, row, r | (g << 8) | (b << 16) | (0xFF << 24));
}
private static LayerSize[] computeLayerSizes() {
final LayerSize[] layerSizes = new LayerSize[MAX_RENDER_DISTANCE];
for (int distance = 0; distance < layerSizes.length; distance++) {
final float bufferWidth = ProjectorVMDevice.WIDTH;
final float bufferHeight = ProjectorVMDevice.HEIGHT;
final float ratio = bufferHeight / bufferWidth;
final float width = (MAX_WIDTH - 1) * (float) (distance + 1) / MAX_RENDER_DISTANCE;
final float height = width * ratio;
int discreteWidth = (int) Math.ceil(width);
discreteWidth += 1 - (discreteWidth & 1); // we center, so even values eat up one more
int discreteHeight = (int) Math.ceil(height); // we align, so actual height is correct
if (Math.abs(discreteHeight - height) < 0.001) discreteHeight++;
layerSizes[distance] = new LayerSize(width, height, discreteWidth, discreteHeight);
}
return layerSizes;
}
}

View File

@@ -1,32 +1,120 @@
package li.cil.oc2.common.mixin;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Matrix4f;
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.culling.Frustum;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import javax.annotation.Nullable;
@Mixin(LevelRenderer.class)
public abstract class LevelRendererMixin {
@Inject(method = {"shouldShowEntityOutlines"}, at = @At("HEAD"), cancellable = true)
@Shadow
@Final
private RenderBuffers renderBuffers;
@Shadow
@Final
private Minecraft minecraft;
@Shadow
private Frustum cullingFrustum;
@Shadow
@Nullable
private RenderTarget itemEntityTarget;
@Nullable
private RenderTarget itemEntityTargetBak;
@Shadow
@Nullable
private RenderTarget weatherTarget;
@Nullable
private RenderTarget weatherTargetBak;
@Inject(method = "renderLevel", at = @At("HEAD"))
private void prepareDepthRendering(final CallbackInfo ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
itemEntityTargetBak = itemEntityTarget;
itemEntityTarget = null;
weatherTargetBak = weatherTarget;
weatherTarget = null;
}
}
@Inject(method = "renderLevel", at = @At("TAIL"))
private void cleanupDepthRendering(final CallbackInfo ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
cleanupDepthRendering();
}
}
private void cleanupDepthRendering() {
weatherTarget = weatherTargetBak;
itemEntityTarget = itemEntityTargetBak;
}
@Inject(method = "renderLevel", at = @At(value = "INVOKE_STRING", target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", args = {"ldc=destroyProgress"}), cancellable = true)
private void captureDepthAndEarlyExit(
final PoseStack stack,
final float partialTicks,
final long startNanos,
final boolean shouldRenderBlockOutline,
final Camera camera,
final GameRenderer gameRenderer,
final LightTexture lightTexture,
final Matrix4f projectionMatrix,
final CallbackInfo ci
) {
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.
cleanupDepthRendering();
// We do want particles though, because that's a neat effect.
final MultiBufferSource.BufferSource bufferSource = renderBuffers.bufferSource();
minecraft.particleEngine.render(stack, bufferSource, lightTexture, camera, partialTicks, cullingFrustum);
// Clean up anything regular return would also clean up.
bufferSource.endBatch();
RenderSystem.depthMask(true);
RenderSystem.disableBlend();
RenderSystem.applyModelViewMatrix();
FogRenderer.setupNoFog();
ci.cancel();
} else {
// Otherwise, we grab the depth buffer of the main render target here, before
// fabulous shading breaks it.
ProjectorDepthRenderer.captureMainCameraDepth();
}
}
@Inject(method = "shouldShowEntityOutlines", at = @At("HEAD"), cancellable = true)
private void skipOutlines(final CallbackInfoReturnable<Boolean> cir) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
cir.setReturnValue(false);
}
}
@Inject(method = {"renderSky", "renderClouds", "renderDebug"}, at = @At("HEAD"), cancellable = true)
private void skipExtraStuff(final CallbackInfo ci) {
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
private void skipSky(final CallbackInfo ci) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
ci.cancel();
}
}
@Inject(method = {"entityTarget", "getItemEntityTarget", "getParticlesTarget"}, at = @At("HEAD"), cancellable = true)
@Inject(method = {"entityTarget", "getItemEntityTarget"}, at = @At("HEAD"), cancellable = true)
private void redirectToMainTarget(final CallbackInfoReturnable<RenderTarget> cir) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
cir.setReturnValue(Minecraft.getInstance().getMainRenderTarget());

View File

@@ -1,20 +0,0 @@
package li.cil.oc2.common.mixin;
import com.mojang.blaze3d.systems.RenderSystem;
import li.cil.oc2.client.renderer.ProjectorDepthRenderer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ShaderInstance;
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(RenderSystem.class)
public abstract class RenderSystemMixin {
@Inject(method = "getShader", at = @At("HEAD"), cancellable = true)
private static void overrideShader(final CallbackInfoReturnable<ShaderInstance> cir) {
if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) {
cir.setReturnValue(GameRenderer.getPositionShader());
}
}
}

View File

@@ -0,0 +1,139 @@
#version 150
uniform int Count;
// Projector 1
uniform sampler2D Sampler0; // Main Depth
uniform mat4 InverseMainCamera; // Inverse Main View Projection Matrix
// Projector 2
uniform sampler2D Sampler1; // Color
uniform sampler2D Sampler2; // Depth
uniform mat4 ProjectorCamera0;
// Projector 3
uniform sampler2D Sampler3; // Color
uniform sampler2D Sampler4; // Depth
uniform mat4 ProjectorCamera1;
// Projector 4
uniform sampler2D Sampler5; // Color
uniform sampler2D Sampler6; // Depth
uniform mat4 ProjectorCamera2;
const float PROJECTOR_NEAR = 1.0/16.0;
const float PROJECTOR_FAR = 32.0;
const float MAX_DISTANCE = 16;
const float START_FADE_DISTANCE = 12;
const float DOT_EPSILON = 0.25;
const mat4 CLIP_TO_TEX = mat4(
0.5, 0, 0, 0,
0, 0.5, 0, 0,
0, 0, 0.5, 0,
0.5, 0.5, 0.5, 1
);
in vec2 texCoord;
out vec4 fragColor;
vec3 getWorldPos(vec2 uv, float clipDepth) {
vec2 clipUv = uv * 2 - 1;
vec4 clipPos = vec4(clipUv, clipDepth, 1);
vec4 worldPos = InverseMainCamera * clipPos;
return worldPos.xyz / worldPos.w;
}
vec3 getWorldPos(vec2 uv) {
float depth = texture2D(Sampler1, uv).r;
float clipDepth = depth * 2 - 1;
return getWorldPos(uv, clipDepth);
}
vec3 getNormal(vec3 worldPos) {
return normalize(cross(dFdx(worldPos), dFdy(worldPos)));
}
bool isInClipBounds(vec3 v) {
return v.x >= -1 && v.x <= 1 &&
v.y >= -1 && v.y <= 1 &&
v.z >= -1 && v.z <= 1;
}
float toLinearDepth(float depth, float zNear, float zFar) {
return zNear * zFar / (zFar + depth * (zNear - zFar));
}
bool getProjectorColor(vec3 worldPos, vec3 worldNormal, mat4 projectorCamera,
sampler2D projectorColorSampler, sampler2D projectorDepthSampler,
out vec4 result) {
// Project world normal into projector clip space.
vec3 projectorClipNormal = (projectorCamera * vec4(worldNormal, 0)).xyz;
// Cull sides and back-faces.
float d = dot(projectorClipNormal, vec3(0, 0, -1));
if (d <= DOT_EPSILON) {
return false;
}
vec4 projectorClipPosPrediv = projectorCamera * vec4(worldPos, 1);
float linearDepth = projectorClipPosPrediv.z;
if (linearDepth >= MAX_DISTANCE) {
return false;
}
vec3 projectorClipPos = projectorClipPosPrediv.xyz / projectorClipPosPrediv.w;
if (!isInClipBounds(projectorClipPos)) {
return false;
}
vec4 projectorUvPrediv = CLIP_TO_TEX * projectorClipPosPrediv;
vec2 projectorUv = projectorUvPrediv.xy / projectorUvPrediv.w;
float projectorDepth = texture2D(projectorDepthSampler, projectorUv).r;
float projectorClipDepth = projectorDepth * 2 - 1;
if (projectorClipPos.z < projectorClipDepth + 0.005) {
vec4 projectorColor = texture2D(projectorColorSampler, vec2(projectorUv.s, 1 - projectorUv.t));
float dotAttenuation = clamp((d - DOT_EPSILON) / (1 - DOT_EPSILON), 0, 1);
float distanceAttenuation = clamp(1 - (linearDepth - START_FADE_DISTANCE) / (MAX_DISTANCE - START_FADE_DISTANCE), 0, 1);
result = projectorColor * dotAttenuation * distanceAttenuation;
return true;
}
return false;
}
void main() {
float depth = texture2D(Sampler0, texCoord).r;
// Check for no hit, e.g. sky, for early exit.
if (depth >= 1) {
discard;
}
float clipDepth = depth * 2 - 1;
vec3 worldPos = getWorldPos(texCoord, clipDepth);
vec3 worldNormal = getNormal(worldPos);
vec4 colorAcc = vec4(0, 0, 0, 0);
vec4 color;
int accCount = 0;
if (Count > 0 && getProjectorColor(worldPos, worldNormal, ProjectorCamera0, Sampler1, Sampler2, color)) {
colorAcc += color;
accCount += 1;
}
if (Count > 1 && getProjectorColor(worldPos, worldNormal, ProjectorCamera1, Sampler3, Sampler4, color)) {
colorAcc += color;
accCount += 1;
}
if (Count > 2 && getProjectorColor(worldPos, worldNormal, ProjectorCamera2, Sampler5, Sampler6, color)) {
colorAcc += color;
accCount += 1;
}
// Check if we had any projections at all.
if (accCount == 0) {
discard;
}
fragColor = colorAcc / accCount;
}

View File

@@ -0,0 +1,28 @@
{
"blend": {
"func": "add",
"srcrgb": "srcalpha",
"dstrgb": "1-srcalpha"
},
"vertex": "oc2:projectors",
"fragment": "oc2:projectors",
"attributes": ["Position", "UV0"],
"samplers": [
{"name": "Sampler0"},
{"name": "Sampler1"},
{"name": "Sampler2"},
{"name": "Sampler3"},
{"name": "Sampler4"},
{"name": "Sampler5"},
{"name": "Sampler6"}
],
"uniforms": [
{"name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "InverseMainCamera", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "ProjectorCamera0", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "ProjectorCamera1", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "ProjectorCamera2", "type": "matrix4x4", "count": 16, "values": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]},
{"name": "Count", "type": "int", "count": 1, "values": [0]}
]
}

View File

@@ -0,0 +1,15 @@
#version 150
in vec3 Position;
in vec2 UV0;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec2 texCoord;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
texCoord = UV0;
}

View File

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