// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. // // SPDX-License-Identifier: LicenseRef-CCPL package dan200.computercraft.client.render.monitor; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.platform.MemoryTracker; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.*; import com.mojang.math.Axis; import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.client.FrameInfo; import dan200.computercraft.client.integration.ShaderMod; import dan200.computercraft.client.render.RenderTypes; import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer; import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.client.render.vbo.DirectBuffers; import dan200.computercraft.client.render.vbo.DirectVertexBuffer; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.peripheral.monitor.ClientMonitor; import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity; import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer; import dan200.computercraft.shared.util.DirectionUtil; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.world.phys.AABB; import org.joml.Matrix3f; import org.joml.Matrix4f; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL31; import javax.annotation.Nullable; import java.nio.ByteBuffer; import java.util.function.Consumer; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT; import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH; import static dan200.computercraft.core.util.Nullability.assertNonNull; public class MonitorBlockEntityRenderer implements BlockEntityRenderer { /** * {@link MonitorBlockEntity#RENDER_MARGIN}, but a tiny bit of additional padding to ensure that there is no space between * the monitor frame and contents. */ private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1); private static final Matrix3f IDENTITY_NORMAL = new Matrix3f().identity(); private static @Nullable ByteBuffer backingBuffer; private static long lastFrame = -1; public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) { } @Override public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) { // Render from the origin monitor var originTerminal = monitor.getOriginClientMonitor(); if (originTerminal == null) return; var origin = originTerminal.getOrigin(); var renderState = originTerminal.getRenderState(MonitorRenderState::new); var monitorPos = monitor.getBlockPos(); // Ensure each monitor terminal is rendered only once. We allow rendering a specific tile // multiple times in a single frame to ensure compatibility with shaders which may run a // pass multiple times. var renderFrame = FrameInfo.getRenderFrame(); if (renderState.lastRenderFrame == renderFrame && !monitorPos.equals(renderState.lastRenderPos)) { return; } lastFrame = renderFrame; renderState.lastRenderFrame = renderFrame; renderState.lastRenderPos = monitorPos; var originPos = origin.getBlockPos(); // Determine orientation var dir = origin.getDirection(); var front = origin.getFront(); var yaw = dir.toYRot(); var pitch = DirectionUtil.toPitchAngle(front); // Setup initial transform transform.pushPose(); transform.translate( originPos.getX() - monitorPos.getX() + 0.5, originPos.getY() - monitorPos.getY() + 0.5, originPos.getZ() - monitorPos.getZ() + 0.5 ); transform.mulPose(Axis.YN.rotationDegrees(yaw)); transform.mulPose(Axis.XP.rotationDegrees(pitch)); transform.translate( -0.5 + MonitorBlockEntity.RENDER_BORDER + MonitorBlockEntity.RENDER_MARGIN, origin.getHeight() - 0.5 - (MonitorBlockEntity.RENDER_BORDER + MonitorBlockEntity.RENDER_MARGIN) + 0, 0.5 ); var xSize = origin.getWidth() - 2.0 * (MonitorBlockEntity.RENDER_MARGIN + MonitorBlockEntity.RENDER_BORDER); var ySize = origin.getHeight() - 2.0 * (MonitorBlockEntity.RENDER_MARGIN + MonitorBlockEntity.RENDER_BORDER); // Draw the contents var terminal = originTerminal.getTerminal(); if (terminal != null && !ShaderMod.get().isRenderingShadowPass()) { // Draw a terminal int width = terminal.getWidth(), height = terminal.getHeight(); int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; var xScale = xSize / pixelWidth; var yScale = ySize / pixelHeight; transform.pushPose(); transform.scale((float) xScale, (float) -yScale, 1.0f); var matrix = transform.last().pose(); renderTerminal(matrix, originTerminal, renderState, terminal, (float) (MARGIN / xScale), (float) (MARGIN / yScale)); transform.popPose(); } else { FixedWidthFontRenderer.drawEmptyTerminal( FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)), -MARGIN, MARGIN, (float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2) ); } transform.popPose(); } private static void renderTerminal( Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin ) { int width = terminal.getWidth(), height = terminal.getHeight(); int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; var renderType = currentRenderer(); var redraw = monitor.pollTerminalChanged(); if (renderState.createBuffer(renderType)) redraw = true; switch (renderType) { case TBO -> { if (redraw) { var terminalBuffer = getBuffer(width * height * 3); MonitorTextureBufferShader.setTerminalData(terminalBuffer, terminal); DirectBuffers.setBufferData(GL31.GL_TEXTURE_BUFFER, renderState.tboBuffer, terminalBuffer, GL20.GL_STATIC_DRAW); var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE); MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal); DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW); } // Nobody knows what they're doing! var active = GlStateManager._getActiveTexture(); RenderSystem.activeTexture(MonitorTextureBufferShader.TEXTURE_INDEX); GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, renderState.tboTexture); RenderSystem.activeTexture(active); var shader = RenderTypes.getMonitorTextureBufferShader(); shader.setupUniform(renderState.tboUniform); var buffer = Tesselator.getInstance().getBuilder(); buffer.begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format()); tboVertex(buffer, matrix, -xMargin, -yMargin); tboVertex(buffer, matrix, -xMargin, pixelHeight + yMargin); tboVertex(buffer, matrix, pixelWidth + xMargin, -yMargin); tboVertex(buffer, matrix, pixelWidth + xMargin, pixelHeight + yMargin); RenderTypes.MONITOR_TBO.end(buffer, VertexSorting.DISTANCE_TO_ORIGIN); } case VBO -> { var backgroundBuffer = assertNonNull(renderState.backgroundBuffer); var foregroundBuffer = assertNonNull(renderState.foregroundBuffer); if (redraw) { var size = DirectFixedWidthFontRenderer.getVertexCount(terminal); // In an ideal world we could upload these both into one buffer. However, we can't render VBOs with // and starting and ending offset, and so need to use two buffers instead. renderToBuffer(backgroundBuffer, size, sink -> DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin)); renderToBuffer(foregroundBuffer, size, sink -> { DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal); // If the cursor is visible, we append it to the end of our buffer. When rendering, we can either // render n or n+1 quads and so toggle the cursor on and off. DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal); }); } // Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will // use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the // normal render distance (~200), and the edges of the monitor fade out due to fog. // There's not really a good way around this, at least without using a custom render type (which the VBO // renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an // absurdly high value. var oldFogStart = RenderSystem.getShaderFogStart(); RenderSystem.setShaderFogStart(1e4f); RenderTypes.TERMINAL.setupRenderState(); // Compose the existing model view matrix with our transformation matrix. var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix); // Render background geometry backgroundBuffer.bind(); backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader()); // Render foreground geometry with glPolygonOffset enabled. RenderSystem.polygonOffset(-1.0f, -10.0f); RenderSystem.enablePolygonOffset(); foregroundBuffer.bind(); foregroundBuffer.drawWithShader( modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(), // As mentioned in the above comment, render the extra cursor quad if it is visible this frame. Each // // quad has an index count of 6. FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink() ? foregroundBuffer.getIndexCount() + 6 : foregroundBuffer.getIndexCount() ); // Clear state RenderSystem.polygonOffset(0.0f, -0.0f); RenderSystem.disablePolygonOffset(); RenderTypes.TERMINAL.clearRenderState(); VertexBuffer.unbind(); RenderSystem.setShaderFogStart(oldFogStart); } case BEST -> throw new IllegalStateException("Impossible: Should never use BEST renderer"); } } private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer draw) { var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer); var buffer = sink.buffer(); draw.accept(sink); buffer.flip(); vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer); } private static void tboVertex(VertexConsumer builder, Matrix4f matrix, float x, float y) { // We encode position in the UV, as that's not transformed by the matrix. builder.vertex(matrix, x, y, 0).uv(x, y).endVertex(); } private static ByteBuffer getBuffer(int capacity) { var buffer = backingBuffer; if (buffer == null || buffer.capacity() < capacity) { buffer = backingBuffer = buffer == null ? MemoryTracker.create(capacity) : MemoryTracker.resize(buffer, capacity); } buffer.clear(); return buffer; } @Override public int getViewDistance() { return Config.monitorDistance; } @ForgeOverride public AABB getRenderBoundingBox(MonitorBlockEntity monitor) { return monitor.getRenderBoundingBox(); } /** * Determine if any monitors were rendered this frame. * * @return Whether any monitors were rendered. */ public static boolean hasRenderedThisFrame() { return FrameInfo.getRenderFrame() == lastFrame; } /** * Get the current renderer to use. * * @return The current renderer. Will not return {@link MonitorRenderer#BEST}. */ public static MonitorRenderer currentRenderer() { var current = Config.monitorRenderer; if (current == MonitorRenderer.BEST) current = Config.monitorRenderer = bestRenderer(); return current; } private static MonitorRenderer bestRenderer() { return MonitorRenderer.VBO; } }