From 5852d4b3675c977eef0706e23b64bdcd9c09df08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 4 Nov 2026 13:57:35 +0100 Subject: [PATCH] render chests from the dumped vanilla models (single, double, trapped, ender, copper) --- .../pixelpics/assets/BlockModelRegistry.java | 1 + .../render/entity/BlockEntityModels.java | 46 +++++++++++++++-- .../render/entity/BlockEntityState.java | 5 +- .../render/entity/cem/BlockEntityBaker.java | 31 +++++++++++ .../snapshot/BlockEntitySnapshotBuilder.java | 20 ++++++-- tools/BlockEntityTestRender.java | 51 +++++++++++++++---- tools/edgecase-plugin/pptest/PPTest.java | 28 ++++++++++ 7 files changed, 161 insertions(+), 21 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java index 83f6da3..5652f43 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/assets/BlockModelRegistry.java @@ -126,6 +126,7 @@ public final class BlockModelRegistry { || m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.ENDER_CHEST + || n.endsWith("COPPER_CHEST") // copper chests incl. weathered/waxed stages || m == Material.CONDUIT || m == Material.DECORATED_POT || m == Material.BELL; diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java index 7dc724f..108eb65 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityModels.java @@ -16,9 +16,7 @@ public final class BlockEntityModels { /** The CEM ({@code .jem}) model name for a block-entity. */ public static String cemModel(BlockEntityState s) { return switch (s.kind()) { - case CHEST, TRAPPED_CHEST, ENDER_CHEST -> switch (s.chestKind() == null - ? BlockEntityState.ChestKind.SINGLE - : s.chestKind()) { + case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> switch (chestKind(s)) { case LEFT -> "chest_left"; case RIGHT -> "chest_right"; case SINGLE -> "chest"; @@ -36,6 +34,40 @@ public final class BlockEntityModels { }; } + /** + * A block-entity model from the vanilla client dump ({@code layer/#}, see + * {@code tools/VanillaModelDump.java}) plus the vanilla BlockEntityRenderer pose: {@code pose} maps + * Java model space (already scaled to blocks) into the block cell, origin at the cell's min corner — + * exactly the PoseStack the renderer sets up before {@code submitModel}. + */ + public record VanillaModel(String layer, Affine pose) {} + + /** + * The vanilla model + renderer pose for a block-entity, or null when the type still uses the CEM + * template path. To migrate another type, add its case here (layer name from the dump + the pose its + * vanilla renderer applies); the baker needs no further changes. + */ + public static VanillaModel vanillaModel(BlockEntityState s) { + // ChestRenderer: rotationAround(Y, -facing.toYRot(), 0.5, 0, 0.5); no Y flip (model y is up). + return switch (s.kind()) { + case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> new VanillaModel( + switch (chestKind(s)) { + case LEFT -> "layer/double_chest_left#main"; + case RIGHT -> "layer/double_chest_right#main"; + case SINGLE -> "layer/chest#main"; + }, + rotationAroundY(-s.facingDeg(), 0.5, 0.5)); + default -> null; + }; + } + + /** JOML {@code rotationAround}: a Y rotation (deg) about the vertical line through (cx, ·, cz). */ + private static Affine rotationAroundY(double deg, double cx, double cz) { + return Affine.translation(cx, 0, cz) + .mul(Affine.rotY(Math.toRadians(deg))) + .mul(Affine.translation(-cx, 0, -cz)); + } + private static String headModel(String headType) { if (headType == null) return "head"; return switch (headType) { @@ -54,6 +86,7 @@ public final class BlockEntityModels { case CHEST -> chestTextures(paths, s, "normal"); case TRAPPED_CHEST -> chestTextures(paths, s, "trapped"); case ENDER_CHEST -> paths.add("entity/chest/ender"); + case COPPER_CHEST -> chestTextures(paths, s, s.colorName() == null ? "copper" : "copper_" + s.colorName()); case SIGN, WALL_SIGN -> { String wood = s.wood() == null ? "oak" : s.wood(); paths.add("entity/signs/" + wood); @@ -82,9 +115,12 @@ public final class BlockEntityModels { return out; } + private static BlockEntityState.ChestKind chestKind(BlockEntityState s) { + return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind(); + } + private static void chestTextures(List paths, BlockEntityState s, String base) { - BlockEntityState.ChestKind ck = s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind(); - switch (ck) { + switch (chestKind(s)) { case LEFT -> paths.add("entity/chest/" + base + "_left"); case RIGHT -> paths.add("entity/chest/" + base + "_right"); case SINGLE -> paths.add("entity/chest/" + base); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java index 615ef87..d512675 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/BlockEntityState.java @@ -17,9 +17,9 @@ public record BlockEntityState( int by, int bz, float facingDeg, - ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/ENDER_CHEST) + ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/COPPER_CHEST) int baseColorArgb, // banner base tint (white texture); 0 = none - String colorName, // bed/shulker/banner colour variant ("red", "white", …); null = default + String colorName, // bed/shulker/banner colour ("red", …) or copper-chest stage ("exposed", …); null = default String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default BedPart bedPart, // bed half String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player" @@ -34,6 +34,7 @@ public record BlockEntityState( CHEST, TRAPPED_CHEST, ENDER_CHEST, + COPPER_CHEST, SIGN, WALL_SIGN, HANGING_SIGN, diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java index bd01bfa..5c805b7 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/entity/cem/BlockEntityBaker.java @@ -27,6 +27,10 @@ import java.util.Set; *

The CEM block-entity models are authored centred on X/Z with their base near Y=0 (after the px→ * block scale), so the placement is {@code T(cell centre) · rotY(yaw) · T(localOffset)} and most types * only need defaults. Wall-mounted and scaled types (signs, wall heads, beds) override via {@link Place}. + * + *

Types mapped in {@link BlockEntityModels#vanillaModel} (currently chests) instead use the model + * dumped from the vanilla client, placed by the vanilla renderer's own pose (see {@code bakeVanilla}); + * the CEM template path above stays as the fallback for everything else. */ public final class BlockEntityBaker implements EntityBaker { @@ -64,6 +68,8 @@ public final class BlockEntityBaker implements EntityBaker { @Override public RenderedEntity bake(BlockEntityState s) { List layers = layers(s); + RenderedEntity vanilla = bakeVanilla(s, layers); + if (vanilla != null) return vanilla; // Head model depends on the texture aspect (skeleton 64x32 vs zombie/player 64x64), so resolve it // from the chosen texture rather than statically. CemModelLoader.CemModel model = layers.isEmpty() ? null : models.get(modelName(s, layers.getFirst().tex)); @@ -92,6 +98,31 @@ public final class BlockEntityBaker implements EntityBaker { return cubes.isEmpty() ? null : RenderedEntity.of(cubes); } + /** + * Vanilla path: the model dumped from the client ({@link BlockEntityModels#vanillaModel}) placed by its + * renderer's pose, {@code T(cell min corner) · pose · S(1/16) · CEM_TO_JAVA}. Position, rotation and + * box-UV then match vanilla by construction (no per-type calibration offsets). Returns null when the + * type has no vanilla mapping or the dump lacks the layer, so the caller falls back to the CEM template. + */ + private RenderedEntity bakeVanilla(BlockEntityState s, List layers) { + BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s); + if (vm == null || layers.isEmpty()) return null; + CemModelLoader.CemModel model = models.vanilla(vm.layer()); + if (model == null) return null; + Affine pre = Affine.translation(s.bx(), s.by(), s.bz()) + .mul(vm.pose()) + .mul(Affine.scale(1 / 16.0)) + .mul(CemGeometry.CEM_TO_JAVA); + List cubes = new ArrayList<>(); + for (Layer layer : layers) { + for (CemGeometry.Baked b : + CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) { + cubes.add(new EntityCube(b.from(), b.to(), b.faces(), b.world())); + } + } + return cubes.isEmpty() ? null : RenderedEntity.of(cubes); + } + /** * Appends flat text quads in front of (and behind) the sign board, rasterized from the captured * {@link BlockEntityState.SignText}. Built in the board's model-px frame and transformed by the same diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java index f4c9422..1e27923 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/snapshot/BlockEntitySnapshotBuilder.java @@ -96,10 +96,13 @@ public final class BlockEntitySnapshotBuilder { String n = mat.name(); // --- chests --- - if (mat == Material.CHEST || mat == Material.TRAPPED_CHEST || mat == Material.ENDER_CHEST) { - Kind kind = mat == Material.TRAPPED_CHEST - ? Kind.TRAPPED_CHEST - : mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST; + boolean copper = n.endsWith("COPPER_CHEST"); + if (mat == Material.CHEST || mat == Material.TRAPPED_CHEST || mat == Material.ENDER_CHEST || copper) { + Kind kind = copper + ? Kind.COPPER_CHEST + : mat == Material.TRAPPED_CHEST + ? Kind.TRAPPED_CHEST + : mat == Material.ENDER_CHEST ? Kind.ENDER_CHEST : Kind.CHEST; ChestKind ck = ChestKind.SINGLE; if (data instanceof Chest cd) { ck = switch (cd.getType()) { @@ -107,7 +110,14 @@ public final class BlockEntitySnapshotBuilder { case RIGHT -> ChestKind.RIGHT; case SINGLE -> ChestKind.SINGLE;}; } - return base(kind, bx, by, bz, facingYaw(data)).chestKind(ck).build(); + Builder b = base(kind, bx, by, bz, facingYaw(data)).chestKind(ck); + // Copper stage from the name: [WAXED_][EXPOSED_|WEATHERED_|OXIDIZED_]COPPER_CHEST. + if (copper) { + String stage = + n.replace("WAXED_", "").replace("COPPER_CHEST", "").replace("_", ""); + b.colorName(stage.isEmpty() ? null : stage.toLowerCase(Locale.ROOT)); + } + return b.build(); } // --- beds --- diff --git a/tools/BlockEntityTestRender.java b/tools/BlockEntityTestRender.java index 5aa78f1..8709778 100644 --- a/tools/BlockEntityTestRender.java +++ b/tools/BlockEntityTestRender.java @@ -86,13 +86,33 @@ public class BlockEntityTestRender { // Scenes: label -> list of block-entity states. LinkedHashMap> scenes = new LinkedHashMap<>(); - scenes.put("chest_S", List.of(be(Kind.CHEST, 0, 0, 0, 0))); - scenes.put("chest_N", List.of(be(Kind.CHEST, 0, 0, 0, 180))); - scenes.put("chest_E", List.of(be(Kind.CHEST, 0, 0, 0, 270))); - scenes.put("chest_W", List.of(be(Kind.CHEST, 0, 0, 0, 90))); - scenes.put("double_chest", List.of( - new BlockEntityState(Kind.CHEST, 0, 0, 0, 0, ChestKind.LEFT, 0, null, null, null, null, null, List.of(), List.of(), null, null, null), - new BlockEntityState(Kind.CHEST, 1, 0, 0, 0, ChestKind.RIGHT, 0, null, null, null, null, null, List.of(), List.of(), null, null, null))); + // Chests: single + double (all four facings), trapped/ender/copper. A double chest's LEFT half has + // its partner (RIGHT) on facing.getClockWise(): S→west, W→north, N→east, E→south. + String[] dirs = {"S", "W", "N", "E"}; + int[][] partner = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; + for (int i = 0; i < 4; i++) { + float yaw = i * 90; + scenes.put("chest_" + dirs[i], List.of(chest(Kind.CHEST, 0, 0, yaw, ChestKind.SINGLE, null))); + frontCam.put("chest_" + dirs[i], yaw); + scenes.put("double_chest_" + dirs[i], List.of( + chest(Kind.CHEST, 0, 0, yaw, ChestKind.LEFT, null), + chest(Kind.CHEST, partner[i][0], partner[i][1], yaw, ChestKind.RIGHT, null))); + frontCam.put("double_chest_" + dirs[i], yaw); + } + scenes.put("double_chest_back", scenes.get("double_chest_S")); + frontCam.put("double_chest_back", 180f); + scenes.put("trapped_double", List.of( + chest(Kind.TRAPPED_CHEST, 0, 0, 0, ChestKind.LEFT, null), + chest(Kind.TRAPPED_CHEST, -1, 0, 0, ChestKind.RIGHT, null))); + frontCam.put("trapped_double", 0f); + scenes.put("ender_chest", List.of(chest(Kind.ENDER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); + frontCam.put("ender_chest", 0f); + scenes.put("copper_chest", List.of(chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.SINGLE, null))); + frontCam.put("copper_chest", 0f); + scenes.put("copper_oxidized_double", List.of( + chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"), + chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized"))); + frontCam.put("copper_oxidized_double", 0f); scenes.put("sign", List.of(be(Kind.SIGN, 0, 0, 0, 0))); scenes.put("wall_sign", List.of(be(Kind.WALL_SIGN, 0, 0, 0, 0))); scenes.put("hanging_sign", List.of(be(Kind.HANGING_SIGN, 0, 0, 0, 0))); @@ -156,7 +176,7 @@ public class BlockEntityTestRender { List cells = new ArrayList<>(); List names = new ArrayList<>(scenes.keySet()); for (String name : names) { - cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name)))); + cells.add(labelCell(name, renderScene(renderer, empty, sky, scenes.get(name), frontCam.get(name)))); log.info("rendered " + name); } for (String name : decoScenes.keySet()) { @@ -182,13 +202,21 @@ public class BlockEntityTestRender { System.out.println("WROTE " + f); } + /** Scenes viewed from the front (camera along the facing yaw, raised, slightly to the side). */ + static final Map frontCam = new HashMap<>(); + + static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) { + return new BlockEntityState(kind, bx, 0, bz, yaw, ck, 0, variant, null, null, null, null, + List.of(), List.of(), null, null, null); + } + static BlockEntityState head(String type) { return headKind(Kind.HEAD, type); } static BlockEntityState headKind(Kind kind, String type) { return new BlockEntityState(kind, 0, 0, 0, 0, null, 0, null, null, null, type, null, List.of(), List.of(), null, null, null); } static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky, - List states) { + List states, Float frontYaw) { double[] min = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE}; double[] max = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; for (BlockEntityState s : states) { @@ -204,6 +232,11 @@ public class BlockEntityTestRender { Vector center = new Vector(cx, cy, cz); Vector cam = new Vector(cx - dist, cy + dist * 0.45, cz - dist * 0.35); + if (frontYaw != null) { + // Facing yaw (0 = south, 90 = west) → front direction (-sin, cos); offset 25° to the side. + double a = Math.toRadians(frontYaw + 25); + cam = new Vector(cx - Math.sin(a) * dist, cy + dist * 0.55, cz + Math.cos(a) * dist); + } Location loc = new Location(null, cam.getX(), cam.getY(), cam.getZ()); loc.setDirection(center.clone().subtract(cam)); List rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA); diff --git a/tools/edgecase-plugin/pptest/PPTest.java b/tools/edgecase-plugin/pptest/PPTest.java index 34682fa..7c85f08 100644 --- a/tools/edgecase-plugin/pptest/PPTest.java +++ b/tools/edgecase-plugin/pptest/PPTest.java @@ -56,6 +56,11 @@ public class PPTest extends JavaPlugin { scenes.put("pistondown", new Scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5)); scenes.put("pistonup", new Scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5)); scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5)); + // Chests: same layout seen from all four sides (single/double per facing, trapped, ender, copper). + scenes.put("chests_s", new Scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3)); + scenes.put("chests_n", new Scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3)); + scenes.put("chests_e", new Scene("chests_e", this::chests, 16, 5, 3, 4.5, 0, 3)); + scenes.put("chests_w", new Scene("chests_w", this::chests, -7, 5, 3, 4.5, 0, 3)); scenes.put("translucent", new Scene("translucent", this::translucent, 4.5, 2.8, -5.5, 4.5, 0.8, 2)); } @@ -430,6 +435,29 @@ public class PPTest extends JavaPlugin { o.clone().add(9, 0, 4).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false); } + /** + * Row z=0: single chests facing S/W/N/E (x=0,3,6,9). Row z=3: double chests per facing — the LEFT half + * at x, its RIGHT partner on facing.getClockWise(). Row z=6: trapped double (S), ender, copper, oxidized + * copper double (N). + */ + private void chests(Location o) { + String[] f = {"south", "west", "north", "east"}; + int[][] cw = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; + java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock() + .setBlockData(Bukkit.createBlockData("minecraft:" + data), false); + for (int i = 0; i < 4; i++) { + put.accept(new int[] {i * 3, 0, 0}, "chest[facing=" + f[i] + ",type=single]"); + put.accept(new int[] {i * 3, 0, 3}, "chest[facing=" + f[i] + ",type=left]"); + put.accept(new int[] {i * 3 + cw[i][0], 0, 3 + cw[i][1]}, "chest[facing=" + f[i] + ",type=right]"); + } + put.accept(new int[] {0, 0, 6}, "trapped_chest[facing=south,type=left]"); + put.accept(new int[] {-1, 0, 6}, "trapped_chest[facing=south,type=right]"); + put.accept(new int[] {3, 0, 6}, "ender_chest[facing=south]"); + put.accept(new int[] {6, 0, 6}, "copper_chest[facing=east,type=single]"); + put.accept(new int[] {9, 0, 6}, "oxidized_copper_chest[facing=north,type=left]"); + put.accept(new int[] {10, 0, 6}, "oxidized_copper_chest[facing=north,type=right]"); + } + /** Redstone wire (dot, line, corner, climbing a wall, powered/unpowered) next to glass and panes. */ private void redstone(Location o) { java.util.function.BiConsumer put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()