@@ -3,27 +3,35 @@
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import io.fangorn.alacrittymc.block.ScreenGroup;
import io.fangorn.alacrittymc.block.TerminalBlock;
import io.fangorn.alacrittymc.block.TerminalBlockEntity;
import net.minecraft.client.renderer.LightTexture;
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.core.Direction;
import org.joml.Matrix4f;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* Renders the terminal texture on the front face of the terminal block.
* Supports multi-block screens: extension blocks render their sub-region
* The block model has no north face — this BER handles it entirely.
* When the terminal is off, a dark quad is drawn. When on, the terminal texture.
* of the controller's texture.
*/
public class TerminalBlockRenderer implements BlockEntityRenderer<TerminalBlockEntity> {
private final Map<Long, TerminalTexture> textures = new HashMap<>();
// Matrix rain state for idle screens
private static final int RAIN_W = 48, RAIN_H = 32;
private static final Random RAIN_RNG = new Random();
private final Map<Long, int[]> rainColumns = new HashMap<>(); // per-block column positions
private long lastRainTick = 0;
public TerminalBlockRenderer(BlockEntityRendererProvider.Context context) {
}
@@ -35,18 +43,12 @@
Direction facing = entity.getBlockState().getValue(TerminalBlock.FACING);
long posKey = entity.getBlockPos().asLong();
poseStack.pushPose();
// Determine the actual data source (controller or self)
TerminalBlockEntity dataSource = entity.getController();
if (dataSource == null) dataSource = entity;
poseStack.pushPose();
applyFacingRotation(poseStack, facing);
// Rotate quad to face the correct direction
poseStack.translate(0.5, 0.5, 0.5);
float yRot = switch (facing) {
case SOUTH -> 180f;
case WEST -> 90f;
case EAST -> -90f;
default -> 0f;
};
poseStack.mulPose(Axis.YP.rotationDegrees(yRot));
poseStack.translate(-0.5, -0.5, -0.5);
Matrix4f mat = poseStack.last().pose();
int light = LightTexture.FULL_BRIGHT;
@@ -54,43 +56,79 @@
float x0 = m, x1 = 1f - m, y0 = m, y1 = 1f - m;
float z = 0.001f;
if (entity.isTerminalRunning() && entity.getPixelWidth() > 0 && entity.getPixelHeight() > 0) {
// Terminal is on — render the terminal texture
TerminalTexture termTex = textures.get(posKey);
if (termTex == null || termTex.getWidth() != entity.getPixelWidth() ||
if (dataSource.isTerminalRunning() && dataSource.getPixelWidth() > 0 && dataSource.getPixelHeight() > 0) {
// Use the controller's position as the texture key
long texKey = dataSource.getBlockPos().asLong();
TerminalTexture termTex = textures.get(texKey);
if (termTex == null || termTex.getWidth() != dataSource.getPixelWidth() ||
termTex.getHeight() != entity.getPixelHeight()) {
termTex.getHeight() != dataSource.getPixelHeight()) {
if (termTex != null) termTex.close();
termTex = new TerminalTexture(dataSource.getPixelWidth(), dataSource.getPixelHeight());
textures.put(texKey, termTex);
termTex = new TerminalTexture(entity.getPixelWidth(), entity.getPixelHeight());
textures.put(posKey, termTex);
}
if (entity.getPixelBuffer() != null) {
termTex.upload(entity.getPixelBuffer(), entity.getPixelWidth(), entity.getPixelHeight());
entity.clearTextureUpdateFlag();
if (dataSource.getPixelBuffer() != null) {
termTex.upload(dataSource.getPixelBuffer(), dataSource.getPixelWidth(), dataSource.getPixelHeight());
dataSource.clearTextureUpdateFlag();
}
// Calculate UV sub-region for multi-block
float u0 = 0f, v0 = 0f, u1 = 1f, v1 = 1f;
ScreenGroup group = entity.getScreenGroup();
if (group != null && entity.isExtension()) {
float[] uv = group.getSubRegion(entity.getBlockPos());
u0 = uv[0]; v0 = uv[1]; u1 = uv[2]; v1 = uv[3];
}
VertexConsumer vc = bufferSource.getBuffer(termTex.getRenderType());
// UVs flipped horizontally for north-face viewing
// UVs flipped horizontally: when looking at the north face from outside,
// world X increases to the LEFT, so u=0 maps to x1 (right edge)
vc.vertex(mat, x0, y1, z).color(255, 255, 255, 255).uv(1f, 0f).uv2(light).endVertex();
vc.vertex(mat, x1, y1, z).color(255, 255, 255, 255).uv(0f, 0f).uv2(light).endVertex();
vc.vertex(mat, x1, y0, z).color(255, 255, 255, 255).uv(0f, 1f).uv2(light).endVertex();
vc.vertex(mat, x0, y0, z).color(255, 255, 255, 255).uv(1f, 1f).uv2(light).endVertex();
vc.vertex(mat, x0, y1, z).color(255, 255, 255, 255).uv(u1, v0).uv2(light).endVertex();
vc.vertex(mat, x1, y1, z).color(255, 255, 255, 255).uv(u0, v0).uv2(light).endVertex();
vc.vertex(mat, x1, y0, z).color(255, 255, 255, 255).uv(u0, v1).uv2(light).endVertex();
vc.vertex(mat, x0, y0, z).color(255, 255, 255, 255).uv(u1, v1).uv2(light).endVertex();
} else {
// Terminal is off — render Matrix-style falling code rain
TerminalTexture rainTex = textures.computeIfAbsent(posKey,
k -> new TerminalTexture(RAIN_W, RAIN_H));
// Terminal is off — render a dark "screen off" quad
TerminalTexture old = textures.remove(posKey);
if (old != null) old.close();
// Use a 1x1 pixel dark TerminalTexture as the "off screen"
TerminalTexture offTex = textures.computeIfAbsent(posKey, k -> {
TerminalTexture t = new TerminalTexture(1, 1);
// Upload a single dark pixel
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocateDirect(4);
buf.put((byte) 20).put((byte) 22).put((byte) 20).put((byte) 255).flip();
t.upload(buf, 1, 1);
return t;
int[] cols = rainColumns.computeIfAbsent(posKey, k -> {
int[] c = new int[RAIN_W];
for (int i = 0; i < RAIN_W; i++) c[i] = RAIN_RNG.nextInt(RAIN_H);
return c;
});
// Animate: advance columns every ~100ms (2 ticks)
long tick = System.currentTimeMillis() / 100;
if (tick != lastRainTick) {
lastRainTick = tick;
VertexConsumer vc = bufferSource.getBuffer(offTex.getRenderType());
ByteBuffer buf = ByteBuffer.allocateDirect(RAIN_W * RAIN_H * 4);
for (int ry = 0; ry < RAIN_H; ry++) {
for (int rx = 0; rx < RAIN_W; rx++) {
int colHead = cols[rx];
int dist = (colHead - ry + RAIN_H) % RAIN_H;
int g; // green intensity
if (dist == 0) {
g = 255; // bright head
} else if (dist < 8) {
g = 180 - dist * 20; // fading trail
} else {
g = RAIN_RNG.nextInt(10); // dim random flicker
}
// ABGR format
buf.put((byte) 255); // A
buf.put((byte) 0); // B
buf.put((byte) g); // G
buf.put((byte) 0); // R
}
}
buf.flip();
rainTex.upload(buf, RAIN_W, RAIN_H);
// Advance columns
for (int i = 0; i < RAIN_W; i++) {
cols[i] = (cols[i] + (RAIN_RNG.nextInt(3) == 0 ? 1 : 0)) % RAIN_H;
}
}
VertexConsumer vc = bufferSource.getBuffer(rainTex.getRenderType());
vc.vertex(mat, x0, y1, z).color(255, 255, 255, 255).uv(1f, 0f).uv2(light).endVertex();
vc.vertex(mat, x1, y1, z).color(255, 255, 255, 255).uv(0f, 0f).uv2(light).endVertex();
vc.vertex(mat, x1, y0, z).color(255, 255, 255, 255).uv(0f, 1f).uv2(light).endVertex();
@@ -98,6 +136,18 @@
}
poseStack.popPose();
}
private void applyFacingRotation(PoseStack poseStack, Direction facing) {
poseStack.translate(0.5, 0.5, 0.5);
float yRot = switch (facing) {
case SOUTH -> 180f;
case WEST -> 90f;
case EAST -> -90f;
default -> 0f;
};
poseStack.mulPose(Axis.YP.rotationDegrees(yRot));
poseStack.translate(-0.5, -0.5, -0.5);
}
@Override