diff --git a/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java b/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java index c5bab26d..c2f3ac75 100644 --- a/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java +++ b/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java @@ -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); } diff --git a/src/main/java/li/cil/oc2/client/renderer/ModShaders.java b/src/main/java/li/cil/oc2/client/renderer/ModShaders.java new file mode 100644 index 00000000..1a71277a --- /dev/null +++ b/src/main/java/li/cil/oc2/client/renderer/ModShaders.java @@ -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); + } +} diff --git a/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java b/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java index d8b2e580..f8ddb9da 100644 --- a/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java +++ b/src/main/java/li/cil/oc2/client/renderer/ProjectorDepthRenderer.java @@ -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 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 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 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. + *

+ * 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. + *

+ * 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. + *

+ * Automatically updated by projectors when new data arrives (from a worker thread). + */ + private record RenderInfo(DynamicTexture texture) implements ProjectorBlockEntity.FrameConsumer { + private static final ThreadLocal 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; + } + } } diff --git a/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java b/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java index 264715b9..1f1a8a98 100644 --- a/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java +++ b/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java @@ -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 { private static final int LIGHT_COLOR_NEAR = 0x22FFFFFF; @@ -44,11 +29,6 @@ public final class ProjectorRenderer implements BlockEntityRenderer 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= 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 { - 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 notification) { - final RenderInfo renderInfo = notification.getValue(); - assert renderInfo != null; - renderInfo.texture().close(); - } - - private record RenderInfo(DynamicTexture texture, RenderType renderType) { } } diff --git a/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java b/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java index 60a05c41..457817c1 100644 --- a/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java +++ b/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java @@ -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 BlockEntityTicker getTicker(final Level level, final BlockState state, final BlockEntityType 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 diff --git a/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java index b0c5dddf..0ee04e74 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java @@ -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 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; } } diff --git a/src/main/java/li/cil/oc2/common/mixin/LevelRendererMixin.java b/src/main/java/li/cil/oc2/common/mixin/LevelRendererMixin.java index d43c9503..be2b2f4e 100644 --- a/src/main/java/li/cil/oc2/common/mixin/LevelRendererMixin.java +++ b/src/main/java/li/cil/oc2/common/mixin/LevelRendererMixin.java @@ -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 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 cir) { if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) { cir.setReturnValue(Minecraft.getInstance().getMainRenderTarget()); diff --git a/src/main/java/li/cil/oc2/common/mixin/RenderSystemMixin.java b/src/main/java/li/cil/oc2/common/mixin/RenderSystemMixin.java deleted file mode 100644 index 87297257..00000000 --- a/src/main/java/li/cil/oc2/common/mixin/RenderSystemMixin.java +++ /dev/null @@ -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 cir) { - if (ProjectorDepthRenderer.isIsRenderingProjectorDepth()) { - cir.setReturnValue(GameRenderer.getPositionShader()); - } - } -} diff --git a/src/main/resources/assets/oc2/shaders/core/projectors.fsh b/src/main/resources/assets/oc2/shaders/core/projectors.fsh new file mode 100644 index 00000000..7b198d81 --- /dev/null +++ b/src/main/resources/assets/oc2/shaders/core/projectors.fsh @@ -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; +} diff --git a/src/main/resources/assets/oc2/shaders/core/projectors.json b/src/main/resources/assets/oc2/shaders/core/projectors.json new file mode 100644 index 00000000..e2138806 --- /dev/null +++ b/src/main/resources/assets/oc2/shaders/core/projectors.json @@ -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]} + ] +} diff --git a/src/main/resources/assets/oc2/shaders/core/projectors.vsh b/src/main/resources/assets/oc2/shaders/core/projectors.vsh new file mode 100644 index 00000000..cf9a5f99 --- /dev/null +++ b/src/main/resources/assets/oc2/shaders/core/projectors.vsh @@ -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; +} diff --git a/src/main/resources/oc2.mixins.json b/src/main/resources/oc2.mixins.json index 3a99a293..b1f01a19 100644 --- a/src/main/resources/oc2.mixins.json +++ b/src/main/resources/oc2.mixins.json @@ -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 + } }