render chests from the dumped vanilla models (single, double, trapped, ender, copper)

This commit is contained in:
MineTec committed 2026-11-04 13:57:35 +01:00
1 parent 9a6c8b538d
commit 5852d4b367
7 files changed
+161 -21

No files matched your search

@@ -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;
@@ -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/<layer>#<part>}, 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<String> 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);
@@ -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,
@@ -27,6 +27,10 @@ import java.util.Set;
* <p>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}.
*
* <p>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<BlockEntityState> {
@@ -64,6 +68,8 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
@Override
public RenderedEntity bake(BlockEntityState s) {
List<Layer> 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<BlockEntityState> {
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<Layer> 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<EntityCube> 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
@@ -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 ---
+42 -9
View File
@@ -86,13 +86,33 @@ public class BlockEntityTestRender {
// Scenes: label -> list of block-entity states.
LinkedHashMap<String, List<BlockEntityState>> 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<BufferedImage> cells = new ArrayList<>();
List<String> 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<String, Float> 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<BlockEntityState> states) {
List<BlockEntityState> 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<Vector> rayMap = buildRayMap(loc, TW * SSAA, TH * SSAA);
+28
View File
@@ -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<int[], String> 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<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()