render all block entities from the dumped vanilla models with decompiled renderer poses, drop the cem block-entity path

This commit is contained in:
MineTec committed 2026-11-04 15:13:55 +01:00
1 parent c66ebaa942
commit 022b080fce
14 files changed
+659 -567

No files matched your search

@@ -79,7 +79,7 @@ public final class BlockModelRegistry {
int avg = fallbackColor(lastFlat); int avg = fallbackColor(lastFlat);
// Block-entities (chests, signs, banners, beds, heads, …) use builtin/entity models with no // Block-entities (chests, banners, heads, shulker boxes, …) use builtin/entity models with no
// geometry; their real shape is rendered separately through the entity scene. Return empty // geometry; their real shape is rendered separately through the entity scene. Return empty
// geometry so the ray passes through to that geometry instead of drawing a grey fallback cube. // geometry so the ray passes through to that geometry instead of drawing a grey fallback cube.
if (isBlockEntityRendered(material)) { if (isBlockEntityRendered(material)) {
@@ -120,7 +120,6 @@ public final class BlockModelRegistry {
String n = m.name(); String n = m.name();
boolean match = n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging boolean match = n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging
|| n.endsWith("_BANNER") // banners: standing/wall || n.endsWith("_BANNER") // banners: standing/wall
|| n.endsWith("_BED")
|| n.endsWith("SHULKER_BOX") // SHULKER_BOX + <color>_SHULKER_BOX || n.endsWith("SHULKER_BOX") // SHULKER_BOX + <color>_SHULKER_BOX
|| ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD) || ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD)
|| m == Material.CHEST || m == Material.CHEST
@@ -1,146 +1,240 @@
package eu.mhsl.minecraft.pixelpics.render.entity; package eu.mhsl.minecraft.pixelpics.render.entity;
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Maps a {@link BlockEntityState} to its bundled CEM model name and ordered texture-path candidates. * Maps a {@link BlockEntityState} to the models its vanilla BlockEntityRenderer draws (from the client
* Block-entity geometry ships in the same CEM set as mobs (chest/sign/banner/…); textures live under * dump, see {@code tools/VanillaModelDump.java}) and to the renderer's PoseStack. Every pose below is
* {@code assets/minecraft/textures/entity/}. Mirrors the role {@link EntityModels} plays for mobs. * transcribed from the decompiled 26.3 client renderers (ChestRenderer, SkullBlockRenderer,
* BannerRenderer, ShulkerBoxRenderer, DecoratedPotRenderer, BellRenderer, ConduitRenderer,
* LecternRenderer, Standing/HangingSignRenderer) — no calibration offsets. Mirrors the role
* {@link EntityModels} plays for mobs.
*
* <p>Beds and sign boards are plain block models since 26.x (no renderer model): the block-model
* pipeline draws them; only the sign TEXT is added here.
*/ */
public final class BlockEntityModels { public final class BlockEntityModels {
private BlockEntityModels() {} private BlockEntityModels() {}
/** The CEM ({@code .jem}) model name for a block-entity. */ /** How a part's texture is produced. */
public static String cemModel(BlockEntityState s) { public enum Paint {
return switch (s.kind()) { /** First loadable texture of {@link Part#textures}. */
case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> switch (chestKind(s)) { PLAIN,
case LEFT -> "chest_left"; /** Banner flag: {@code banner_base}, the base-colour {@code base} mask, then each pattern (BannerRenderer). */
case RIGHT -> "chest_right"; BANNER_FLAG,
case SINGLE -> "chest"; /** Player head: the owner's skin, else {@link Part#textures} (default Steve). */
}; SKIN
case SIGN -> "sign";
case WALL_SIGN -> "wall_sign";
case HANGING_SIGN -> "hanging_sign";
case BANNER, WALL_BANNER -> "banner";
case BED -> "bed";
case SHULKER_BOX -> "shulker_box";
case CONDUIT -> "conduit";
case DECORATED_POT -> "decorated_pot";
case BELL -> "bell";
case HEAD, WALL_HEAD -> headModel(s.headType());
};
} }
/** /**
* A block-entity model from the vanilla client dump ({@code layer/<layer>#<part>}, see * One {@code submitModel}/{@code submitModelPart} call: a dump model ({@code layer/<model>#<layer>}
* {@code tools/VanillaModelDump.java}) plus the vanilla BlockEntityRenderer pose: {@code pose} maps * in rest pose, or {@code posed/…} posed by its setupAnim), its texture candidates and the top-level
* Java model space (already scaled to blocks) into the block cell, origin at the cell's min corner — * parts it leaves out (pot sides each carry their own sherd texture).
* exactly the PoseStack the renderer sets up before {@code submitModel}.
*/ */
public record VanillaModel(String layer, Affine pose) {} public record Part(String model, List<String> textures, Set<String> hidden, Paint paint) {
static Part of(String model, String texture) {
/** return new Part(model, List.of(texture), Set.of(), Paint.PLAIN);
* 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) {
case "dragon" -> "head_dragon";
case "piglin" -> "head_piglin";
case "player" -> "head_player";
case "wither_skeleton" -> "wither_skull";
default -> "head"; // skeleton, zombie, creeper share the plain humanoid skull box
};
}
/** Ordered texture-path candidates; the baker uses the first that loads. */
public static List<ResourceLocation> textureCandidates(BlockEntityState s) {
List<String> paths = new ArrayList<>();
switch (s.kind()) {
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);
paths.add("entity/sign"); // legacy single-texture fallback
}
case HANGING_SIGN -> {
String wood = s.wood() == null ? "oak" : s.wood();
paths.add("entity/signs/hanging/" + wood);
}
case BANNER, WALL_BANNER -> paths.add("entity/banner/banner_base"); // tinted by baseColorArgb
case BED -> {
String c = s.colorName() == null ? "red" : s.colorName();
paths.add("entity/bed/" + c);
}
case SHULKER_BOX -> {
if (s.colorName() != null) paths.add("entity/shulker/shulker_" + s.colorName());
paths.add("entity/shulker/shulker"); // uncoloured (purpur) default
}
case CONDUIT -> paths.add("entity/conduit/base");
case DECORATED_POT -> paths.add("entity/decorated_pot/decorated_pot_base");
case BELL -> paths.add("entity/bell/bell_body");
case HEAD, WALL_HEAD -> headTextures(paths, s.headType());
} }
List<ResourceLocation> out = new ArrayList<>(paths.size()); }
for (String p : paths) out.add(ResourceLocation.parse(p));
return out; /**
* The renderer's models plus its block 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 submitting. Items and worn heads use the parts WITHOUT this pose (the special item
* renderers submit the bare model; the item model's own transformation places it).
*/
public record VanillaModel(Affine pose, List<Part> parts) {}
/** The vanilla model + pose of a block-entity, or null when nothing model-based is drawn (signs). */
public static VanillaModel vanillaModel(BlockEntityState s) {
double deg = s.facingDeg();
return switch (s.kind()) {
// ChestRenderer: rotationAround(Y, -facing.toYRot(), 0.5, 0, 0.5); model y is up (no flip).
case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> {
String layer =
switch (chestKind(s)) {
case LEFT -> "layer/double_chest_left#main";
case RIGHT -> "layer/double_chest_right#main";
case SINGLE -> "layer/chest#main";
};
yield new VanillaModel(
rotationAround(Affine.rotY(Math.toRadians(-deg)), 0.5, 0, 0.5),
List.of(new Part(layer, chestTextures(s), Set.of(), Paint.PLAIN)));
}
// SkullBlockRenderer ground: T(0.5,0,0.5)·rotY(-segment·22.5)·S(-1,-1,1); wall: T(0.5 - step·0.25,
// 0.25, …)·rotY(-opposite.toYRot())·S(-1,-1,1).
case HEAD -> new VanillaModel(
Affine.translation(0.5, 0, 0.5)
.mul(Affine.rotY(Math.toRadians(-deg)))
.mul(Affine.scale(-1, -1, 1)),
List.of(headPart(s.headType())));
case WALL_HEAD -> {
double sx = -Math.sin(Math.toRadians(deg)), sz = Math.cos(Math.toRadians(deg)); // facing step
yield new VanillaModel(
Affine.translation(0.5 - sx * 0.25, 0.25, 0.5 - sz * 0.25)
.mul(Affine.rotY(Math.toRadians(-(deg + 180))))
.mul(Affine.scale(-1, -1, 1)),
List.of(headPart(s.headType())));
}
// BannerRenderer: T(0.5,0,0.5)·rotY(-angle)·S(2/3,-2/3,-2/3), angle = rotation or wall facing; the
// pole/bar use banner_base, the flag (wave phase 0) gets base colour + patterns.
case BANNER, WALL_BANNER -> {
String model = s.kind() == BlockEntityState.Kind.BANNER ? "standing_banner" : "wall_banner";
double k = 2 / 3.0;
yield new VanillaModel(
Affine.translation(0.5, 0, 0.5)
.mul(Affine.rotY(Math.toRadians(-deg)))
.mul(Affine.scale(k, -k, -k)),
List.of(
Part.of("layer/" + model + "#main", "entity/banner/banner_base"),
new Part(
"posed/" + model + "#flag",
List.of("entity/banner/banner_base"),
Set.of(),
Paint.BANNER_FLAG)));
}
// ShulkerBoxRenderer: T(.5,.5,.5)·S(0.9995)·direction.getRotation()·S(1,-1,-1)·T(0,-1,0); lid closed.
case SHULKER_BOX -> new VanillaModel(
Affine.translation(0.5, 0.5, 0.5)
.mul(Affine.scale(0.9995))
.mul(directionRotation(s.direction()))
.mul(Affine.scale(1, -1, -1))
.mul(Affine.translation(0, -1, 0)),
List.of(new Part(
"layer/shulker_box#main",
s.colorName() == null
? List.of("entity/shulker/shulker")
: List.of("entity/shulker/shulker_" + s.colorName(), "entity/shulker/shulker"),
Set.of(),
Paint.PLAIN)));
// DecoratedPotRenderer: rotateAround(Y, 180 - facing.toYRot(), .5,.5,.5); neck/top/bottom from the
// base sheet, each side its sherd pattern (or the blank side).
case DECORATED_POT -> new VanillaModel(
rotationAround(Affine.rotY(Math.toRadians(180 - deg)), 0.5, 0.5, 0.5), potParts(s));
// BellRenderer: the model as is (pivot 8,12,8 in block px); the frame is the block model.
case BELL -> new VanillaModel(
Affine.identity(), List.of(Part.of("layer/bell#main", "entity/bell/bell_body")));
// ConduitRenderer: inactive → shell at T(.5,.5,.5); active → cage at T(.5, 0.3 + hh·0.2, .5) with
// hh at animation time 0 (= 0.75), unrotated. Wind and the camera-facing eye are not drawn.
case CONDUIT -> s.active()
? new VanillaModel(
Affine.translation(0.5, 0.3 + 0.75 * 0.2, 0.5),
List.of(Part.of("layer/conduit#cage", "entity/conduit/cage")))
: new VanillaModel(
Affine.translation(0.5, 0.5, 0.5),
List.of(Part.of("layer/conduit#shell", "entity/conduit/base")));
// LecternRenderer (only with a book): T(.5,1.0625,.5)·rotY(-clockwise(facing).toYRot())·rotZ(67.5°)·
// T(0,-0.125,0), book open as BOOK_STATE (dumped posed).
case LECTERN -> s.active()
? new VanillaModel(
Affine.translation(0.5, 1.0625, 0.5)
.mul(Affine.rotY(Math.toRadians(-(deg + 90))))
.mul(Affine.rotZ(Math.toRadians(67.5)))
.mul(Affine.translation(0, -0.125, 0)),
List.of(Part.of("posed/book#main", "entity/enchantment/enchanting_table_book")))
: null;
case SIGN, WALL_SIGN, HANGING_SIGN -> null;
};
}
/**
* The sign renderer's text transformation (Standing/HangingSignRenderer.textTransformation) WITHOUT
* its final {@code scale(s, -s, s)}: returns the frame whose origin is the text anchor (centre of the
* four lines), x right / y up / +z towards the reader, and the font-pixel size {@code s} in blocks via
* {@code out[0]}. Back text is the front transform turned by 180°.
*/
public static Affine signTextFrame(BlockEntityState s, boolean front, double[] pxSize) {
double deg = s.facingDeg();
Affine m;
if (s.kind() == BlockEntityState.Kind.HANGING_SIGN) {
m = Affine.translation(0.5, 0.9375, 0.5)
.mul(Affine.rotY(Math.toRadians(-deg)))
.mul(Affine.translation(0, -0.3125, 0));
if (!front) m = m.mul(Affine.rotY(Math.PI));
m = m.mul(Affine.translation(0, -0.32, 0.073));
pxSize[0] = 0.0140625;
} else {
m = Affine.translation(0.5, 0.5, 0.5).mul(Affine.rotY(Math.toRadians(-deg)));
if (s.kind() == BlockEntityState.Kind.WALL_SIGN) m = m.mul(Affine.translation(0, -0.3125, -0.4375));
if (!front) m = m.mul(Affine.rotY(Math.PI));
m = m.mul(Affine.translation(0, 0.33333334, 0.046666667));
pxSize[0] = 0.010416667;
}
return m;
}
private static Part headPart(String headType) {
String t = headType == null ? "skeleton" : headType;
return switch (t) {
case "wither_skeleton" -> Part.of("layer/wither_skeleton_skull#main", "entity/skeleton/wither_skeleton");
case "zombie" -> Part.of("layer/zombie_head#main", "entity/zombie/zombie");
case "creeper" -> Part.of("layer/creeper_head#main", "entity/creeper/creeper");
case "dragon" -> Part.of("posed/dragon_skull#main", "entity/enderdragon/dragon");
case "piglin" -> Part.of("posed/piglin_head#main", "entity/piglin/piglin");
case "player" -> new Part(
"layer/player_head#main", List.of("entity/player/wide/steve"), Set.of(), Paint.SKIN);
default -> Part.of("layer/skeleton_skull#main", "entity/skeleton/skeleton");
};
}
private static final List<String> POT_SIDES = List.of("front", "left", "right", "back"); // = sherd order
private static List<Part> potParts(BlockEntityState s) {
List<Part> parts = new ArrayList<>(5);
parts.add(Part.of("layer/decorated_pot_base#main", "entity/decorated_pot/decorated_pot_base"));
for (int i = 0; i < POT_SIDES.size(); i++) {
List<String> tex = new ArrayList<>(2);
String sherd = i < s.sherds().size() ? s.sherds().get(i) : null;
if (sherd != null && sherd.endsWith("_pottery_sherd")) {
tex.add("entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern"));
}
tex.add("entity/decorated_pot/decorated_pot_side");
List<String> others = new ArrayList<>(POT_SIDES);
others.remove(i);
parts.add(new Part("layer/decorated_pot_sides#main", tex, Set.copyOf(others), Paint.PLAIN));
}
return parts;
} }
private static BlockEntityState.ChestKind chestKind(BlockEntityState s) { private static BlockEntityState.ChestKind chestKind(BlockEntityState s) {
return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind(); return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind();
} }
private static void chestTextures(List<String> paths, BlockEntityState s, String base) { private static List<String> chestTextures(BlockEntityState s) {
switch (chestKind(s)) { String base =
case LEFT -> paths.add("entity/chest/" + base + "_left"); switch (s.kind()) {
case RIGHT -> paths.add("entity/chest/" + base + "_right"); case TRAPPED_CHEST -> "trapped";
case SINGLE -> paths.add("entity/chest/" + base); case ENDER_CHEST -> "ender";
} case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
paths.add("entity/chest/" + base); // fallback to the single texture default -> "normal";
};
if (s.kind() == BlockEntityState.Kind.ENDER_CHEST) return List.of("entity/chest/ender");
return switch (chestKind(s)) {
case LEFT -> List.of("entity/chest/" + base + "_left", "entity/chest/" + base);
case RIGHT -> List.of("entity/chest/" + base + "_right", "entity/chest/" + base);
case SINGLE -> List.of("entity/chest/" + base);
};
} }
private static void headTextures(List<String> paths, String headType) { /** JOML {@code rotationAround}: {@code T(c) · rot · T(-c)}. */
if (headType == null) { private static Affine rotationAround(Affine rot, double cx, double cy, double cz) {
paths.add("entity/skeleton/skeleton"); return Affine.translation(cx, cy, cz).mul(rot).mul(Affine.translation(-cx, -cy, -cz));
return; }
}
switch (headType) { /** Minecraft {@code Direction.getRotation()} (JOML rotationXYZ = Rx·Ry·Rz); null/unknown = UP. */
case "wither_skeleton" -> paths.add("entity/skeleton/wither_skeleton"); private static Affine directionRotation(String direction) {
case "zombie" -> paths.add("entity/zombie/zombie"); double h = Math.PI / 2;
case "creeper" -> paths.add("entity/creeper/creeper"); if (direction == null) return Affine.identity();
case "piglin" -> paths.add("entity/piglin/piglin"); return switch (direction) {
case "dragon" -> paths.add("entity/enderdragon/dragon"); case "down" -> Affine.rotX(Math.PI);
case "player" -> paths.add("entity/player/wide/steve"); // skin handled separately by the baker case "north" -> Affine.rotX(h).mul(Affine.rotZ(Math.PI));
default -> paths.add("entity/skeleton/skeleton"); case "south" -> Affine.rotX(h);
} case "west" -> Affine.rotX(h).mul(Affine.rotZ(h));
case "east" -> Affine.rotX(h).mul(Affine.rotZ(-h));
default -> Affine.identity();
};
} }
} }
@@ -4,12 +4,13 @@ import java.util.List;
/** /**
* Immutable snapshot of one block-entity captured on the main thread, sufficient to bake and place it * Immutable snapshot of one block-entity captured on the main thread, sufficient to bake and place it
* off-thread. Block-entities (chests, signs, banners, beds, heads, …) use vanilla {@code builtin/entity} * off-thread. Block-entities drawn by a vanilla BlockEntityRenderer (chests, banners, heads, shulker
* block models with no geometry; their real shape lives in the bundled CEM models, so they are rendered * boxes, …) have no block-model geometry; their shape is the renderer's entity model (dumped from the
* through the same baking/intersection path as mobs. Bukkit-free so the baker needs no world access. * client, see {@link BlockEntityModels}), rendered through the same baking/intersection path as mobs.
* Bukkit-free so the baker needs no world access.
* *
* <p>{@code facingDeg} is the world yaw the model should face (already converted from the block's * <p>{@code facingDeg} is the vanilla yaw of the block's facing/rotation (0 = south, 90 = west; a
* facing/rotation, vanilla convention). Type-specific fields are null/0/empty when unused. * 16-step rotation is {@code segment · 22.5}). Type-specific fields are null/0/false/empty when unused.
*/ */
public record BlockEntityState( public record BlockEntityState(
Kind kind, Kind kind,
@@ -17,16 +18,15 @@ public record BlockEntityState(
int by, int by,
int bz, int bz,
float facingDeg, float facingDeg,
String direction, // 6-way facing ("up", "north", …) of shulker boxes; null otherwise
boolean active, // lectern: has a book; conduit: active
ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/COPPER_CHEST) ChestKind chestKind, // double-chest half (CHEST/TRAPPED_CHEST/COPPER_CHEST)
int baseColorArgb, // banner base tint (white texture); 0 = none int baseColorArgb, // banner base colour; 0 = none
String colorName, // bed/shulker/banner colour ("red", …) or copper-chest stage ("exposed", …); null = default String colorName, // shulker 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" String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player"
String skinUrl, // player-head owner skin URL; null otherwise String skinUrl, // player-head owner skin URL; null otherwise
List<BannerPattern> patterns, // banner overlay patterns (may be empty) List<BannerPattern> patterns, // banner overlay patterns (may be empty)
List<String> sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty) List<String> sherds, // decorated-pot sherds: front/left/right/back item keys (may be empty)
BellAttach bellAttach, // bell attachment; null when not a bell
SignText frontText, // sign front-side text; null when not a sign or blank SignText frontText, // sign front-side text; null when not a sign or blank
SignText backText // sign back-side text; null when not a sign or blank SignText backText // sign back-side text; null when not a sign or blank
) { ) {
@@ -40,13 +40,13 @@ public record BlockEntityState(
HANGING_SIGN, HANGING_SIGN,
BANNER, BANNER,
WALL_BANNER, WALL_BANNER,
BED,
SHULKER_BOX, SHULKER_BOX,
HEAD, HEAD,
WALL_HEAD, WALL_HEAD,
CONDUIT, CONDUIT,
DECORATED_POT, DECORATED_POT,
BELL BELL,
LECTERN
} }
public enum ChestKind { public enum ChestKind {
@@ -55,18 +55,6 @@ public record BlockEntityState(
RIGHT RIGHT
} }
public enum BedPart {
HEAD,
FOOT
}
public enum BellAttach {
FLOOR,
CEILING,
SINGLE_WALL,
DOUBLE_WALL
}
/** One banner overlay layer: a pattern key (e.g. "stripe_top") and the ARGB colour it is dyed with. */ /** One banner overlay layer: a pattern key (e.g. "stripe_top") and the ARGB colour it is dyed with. */
public record BannerPattern(String patternKey, int colorArgb) {} public record BannerPattern(String patternKey, int colorArgb) {}
@@ -14,38 +14,24 @@ import eu.mhsl.minecraft.pixelpics.render.entity.EntityCube;
import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity; import eu.mhsl.minecraft.pixelpics.render.entity.RenderedEntity;
import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps; import eu.mhsl.minecraft.pixelpics.render.entity.TextureOps;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Bakes a {@link BlockEntityState} into world-space cubes using the bundled CEM models, reusing * Bakes a {@link BlockEntityState} into world-space cubes: the models its vanilla BlockEntityRenderer
* {@link CemGeometry}. Unlike mobs, block-entities are not dropped to the ground by their model bounds; * draws (dumped from the client, see {@link BlockEntityModels#vanillaModel}), placed by that renderer's
* each type is placed by an affine that reproduces the vanilla BlockEntityRenderer: the block cell * own pose — {@code T(cell min corner) · pose · S(1/16) · CEM_TO_JAVA} — so position, rotation and box-UV
* centre, a yaw from the block's facing/rotation, and a per-type local offset/scale. * match vanilla by construction (no per-type calibration). Signs add their text quads in the renderer's
* * text frame (the board itself is a block model).
* <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> { public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
private static final double SIGN_SCALE = 0.6666667; // vanilla SignRenderer model scale // Sign text is scaled to fill the board's writable area (W×H, font px of the renderer's text frame) so
// few-line signs render as large as possible (legibility on low-res maps), capped so a single line isn't
// Sign-text placement (model px in the board's local frame; calibrated against the test harness). // blown up absurdly. Vanilla: 1 font px per text px, lines 90 (hanging 60) px wide, 10 (9) px apart.
// The text block is scaled to fill the board's writable area (W×H) so few-line signs are rendered as private static final double SIGN_TEXT_W = 88, SIGN_TEXT_H = 44, SIGN_TEXT_CAP = 2.0;
// large as possible (legibility on low-res maps), capped so a single line isn't blown up absurdly. private static final double HANG_TEXT_W = 58, HANG_TEXT_H = 40, HANG_TEXT_CAP = 1.87;
private static final double SIGN_TEXT_W = 22.0, SIGN_TEXT_H = 11.0, SIGN_TEXT_CAP = 0.5; // board 24x12 private static final double TEXT_THICK =
private static final double HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10 0.2; // slab behind the text plane (text px) so the ray test never degenerates
private static final double SIGN_TEXT_CY = 20.0; // board Y∈[14,26], centre 20
private static final double HANGING_TEXT_CY = 5.0; // board Y∈[0,10], centre 5
private static final double BOARD_FRONT_Z = 1.0; // both boards: front/back faces at Z=±1
private static final double TEXT_Z_EPS = 0.05; // lift the text just off the board face
private static final double TEXT_THICK = 0.5; // slab thickness so the ray test never degenerates
private final CemModelLoader models; private final CemModelLoader models;
private final TextureCache textures; private final TextureCache textures;
@@ -64,120 +50,117 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
this.font = font; this.font = font;
} }
/** Returns the baked block-entity, or null when it has no model/texture (then nothing renders). */ /** The placed block-entity in its cell, or null when nothing renders (unmapped/missing assets). */
@Override @Override
public RenderedEntity bake(BlockEntityState s) { 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));
// Since 26.x signs (and beds) are real block models (textures under block/, no entity/signs/): the
// board then comes from the block-model pipeline and only the sign TEXT is added here.
boolean signKind = s.kind() == BlockEntityState.Kind.SIGN
|| s.kind() == BlockEntityState.Kind.WALL_SIGN
|| s.kind() == BlockEntityState.Kind.HANGING_SIGN;
if (model == null && !signKind) return null;
Place p = place(s);
Affine pre = Affine.scale(p.scale / 16.0);
Affine placement = Affine.translation(s.bx() + 0.5, s.by(), s.bz() + 0.5)
.mul(Affine.rotY(Math.toRadians(p.yaw)))
.mul(Affine.translation(p.lx, p.ly, p.lz));
List<EntityCube> cubes = new ArrayList<>(); List<EntityCube> cubes = new ArrayList<>();
if (model != null) Affine cell = Affine.translation(s.bx(), s.by(), s.bz());
for (Layer layer : layers) { BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s);
for (CemGeometry.Baked b : CemGeometry.bakeModel( if (vm != null) bakeParts(s, vm, cell.mul(vm.pose()), cubes);
model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) { addSignText(s, cell, cubes);
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), placement.mul(b.world())));
}
}
addSignText(s, placement.mul(pre), cubes);
return cubes.isEmpty() ? null : RenderedEntity.of(cubes); return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
} }
/** /**
* Vanilla path: the model dumped from the client ({@link BlockEntityModels#vanillaModel}) placed by its * The block-entity's models WITHOUT the block pose, in Java model space (blocks) mapped by
* renderer's pose, {@code T(cell min corner) · pose · S(1/16) · CEM_TO_JAVA}. Position, rotation and * {@code toWorld}: what the special item renderers and the worn-skull layer submit (the item model's
* box-UV then match vanilla by construction (no per-type calibration offsets). Returns null when the * transformation / the head frame place it).
* 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) { public void bakeModel(BlockEntityState s, Affine toWorld, List<EntityCube> out) {
BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s); BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s);
if (vm == null || layers.isEmpty()) return null; if (vm != null) bakeParts(s, vm, toWorld, out);
CemModelLoader.CemModel model = models.vanilla(vm.layer()); }
if (model == null) return null;
Affine pre = Affine.translation(s.bx(), s.by(), s.bz()) private void bakeParts(BlockEntityState s, BlockEntityModels.VanillaModel vm, Affine pose, List<EntityCube> out) {
.mul(vm.pose()) Affine pre = pose.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
.mul(Affine.scale(1 / 16.0)) for (BlockEntityModels.Part part : vm.parts()) {
.mul(CemGeometry.CEM_TO_JAVA); CemModelLoader.CemModel model = models.vanilla(part.model());
List<EntityCube> cubes = new ArrayList<>(); int[][] tex = model == null ? null : texture(s, part);
for (Layer layer : layers) { if (tex == null) continue;
for (CemGeometry.Baked b : for (CemGeometry.Baked b : CemGeometry.bakeModel(model, tex, pre, part.hidden())) {
CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) { out.add(new EntityCube(b.from(), b.to(), b.faces(), b.world()));
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), b.world()));
} }
} }
return cubes.isEmpty() ? null : RenderedEntity.of(cubes); }
// --- textures ---
private int[][] texture(BlockEntityState s, BlockEntityModels.Part part) {
if (part.paint() == BlockEntityModels.Paint.SKIN && s.skinUrl() != null) {
int[][] skin = skins.get(s.skinUrl()).orElse(null);
if (skin != null) return skin;
}
for (String path : part.textures()) {
int[][] t = textures.get(ResourceLocation.parse(path)).orElse(null);
if (t == null) continue;
return part.paint() == BlockEntityModels.Paint.BANNER_FLAG ? bannerFlag(t, s) : t;
}
return null;
} }
/** /**
* Appends flat text quads in front of (and behind) the sign board, rasterized from the captured * BannerRenderer flag: {@code banner_base}, then the {@code base} mask dyed with the base colour, then
* {@link BlockEntityState.SignText}. Built in the board's model-px frame and transformed by the same * each pattern mask ({@code entity/banner/<key>}) dyed with its colour, composited into one texture
* {@code placement·pre} as the CEM board, so the text rides exactly on the board. * (one geometry, no coplanar layers).
*
* <p>The CEM sign model faces north by default and is aimed with {@code rotY(180−facingDeg)} where
* {@code facingDeg} is the direction the sign's FRONT faces — so the model's −Z (NORTH) face ends up
* being the front the player reads, and +Z (SOUTH) is the back. Each face uses an unflipped UV: the
* intersector already mirrors NORTH relative to SOUTH ({@code s=1−fx} vs {@code s=fx}), so both read
* left-to-right when viewed from outside.
*/ */
private void addSignText(BlockEntityState s, Affine toWorld, List<EntityCube> cubes) { private int[][] bannerFlag(int[][] base, BlockEntityState s) {
int[][] out = TextureOps.deepCopy(base);
if (s.baseColorArgb() != 0) overlayMask(out, "entity/banner/base", s.baseColorArgb());
for (BlockEntityState.BannerPattern pat : s.patterns()) {
overlayMask(out, "entity/banner/" + pat.patternKey(), pat.colorArgb());
}
return out;
}
private void overlayMask(int[][] out, String path, int argb) {
int[][] mask = textures.get(ResourceLocation.parse(path)).orElse(null);
if (mask == null || mask.length != out.length || mask[0].length != out[0].length) return;
int[][] dyed = TextureOps.deepCopy(mask);
TextureOps.tint(dyed, argb);
TextureOps.overlay(out, dyed);
}
// --- sign text ---
/**
* Front and back text as flat quads in the sign renderer's text frame
* ({@link BlockEntityModels#signTextFrame}): the text block is centred on the frame origin and faces
* +Z (the reader). The quad's +Z (SOUTH) face is unflipped: viewed from +Z the intersector reads it
* left to right ({@code s = fx}).
*/
private void addSignText(BlockEntityState s, Affine cell, List<EntityCube> cubes) {
if (font == null || font.isEmpty()) return; if (font == null || font.isEmpty()) return;
boolean hanging = s.kind() == BlockEntityState.Kind.HANGING_SIGN; BlockEntityState.Kind k = s.kind();
boolean sign = hanging || s.kind() == BlockEntityState.Kind.SIGN || s.kind() == BlockEntityState.Kind.WALL_SIGN; boolean hanging = k == BlockEntityState.Kind.HANGING_SIGN;
if (!sign) return; if (!hanging && k != BlockEntityState.Kind.SIGN && k != BlockEntityState.Kind.WALL_SIGN) return;
double cy = hanging ? HANGING_TEXT_CY : SIGN_TEXT_CY; addSide(s, s.frontText(), true, hanging, cell, cubes);
double tw = hanging ? HANG_TEXT_W : SIGN_TEXT_W; addSide(s, s.backText(), false, hanging, cell, cubes);
double th = hanging ? HANG_TEXT_H : SIGN_TEXT_H;
double cap = hanging ? HANG_TEXT_CAP : SIGN_TEXT_CAP;
addSide(s.frontText(), Direction.NORTH, tw, th, cap, cy, toWorld, cubes);
addSide(s.backText(), Direction.SOUTH, tw, th, cap, cy, toWorld, cubes);
} }
private void addSide( private void addSide(
BlockEntityState s,
BlockEntityState.SignText t, BlockEntityState.SignText t,
Direction faceDir, boolean front,
double tw, boolean hanging,
double th, Affine cell,
double cap,
double cy,
Affine toWorld,
List<EntityCube> cubes) { List<EntityCube> cubes) {
if (t == null) return; if (t == null) return;
int[][] bmp = SignTextRasterizer.rasterize( int[][] bmp = SignTextRasterizer.rasterize(
trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing()); trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing());
if (bmp == null) return; if (bmp == null) return;
// Scale so the text block fills the board's writable area, whichever dimension binds, capped. double tw = hanging ? HANG_TEXT_W : SIGN_TEXT_W;
double th = hanging ? HANG_TEXT_H : SIGN_TEXT_H;
double cap = hanging ? HANG_TEXT_CAP : SIGN_TEXT_CAP;
// Scale so the text block fills the writable area, whichever dimension binds, capped.
double fpm = Math.min(cap, Math.min(tw / bmp[0].length, th / bmp.length)); double fpm = Math.min(cap, Math.min(tw / bmp[0].length, th / bmp.length));
double blockW = bmp[0].length * fpm; double w = bmp[0].length * fpm, h = bmp.length * fpm;
double blockH = bmp.length * fpm; double[] px = new double[1];
Affine frame = BlockEntityModels.signTextFrame(s, front, px);
double z0, z1;
if (faceDir == Direction.NORTH) { // front: −Z face, text sits just past −1
z1 = -BOARD_FRONT_Z - TEXT_Z_EPS;
z0 = z1 - TEXT_THICK;
} else { // back: +Z face, text sits just past +1
z0 = BOARD_FRONT_Z + TEXT_Z_EPS;
z1 = z0 + TEXT_THICK;
}
Face[] faces = new Face[6]; Face[] faces = new Face[6];
faces[faceDir.ordinal()] = new Face(bmp, 0, 0, 1, 1, 0, -1); faces[Direction.SOUTH.ordinal()] = new Face(bmp, 0, 0, 1, 1, 0, -1);
double[] from = {-blockW / 2, cy - blockH / 2, z0}; double[] from = {-w / 2, -h / 2, -TEXT_THICK};
double[] to = {blockW / 2, cy + blockH / 2, z1}; double[] to = {w / 2, h / 2, 0};
cubes.add(new EntityCube(from, to, faces, toWorld)); cubes.add(new EntityCube(from, to, faces, cell.mul(frame).mul(Affine.scale(px[0]))));
} }
/** Drops leading/trailing blank lines so short text fills the board; keeps interior blanks. */ /** Drops leading/trailing blank lines so short text fills the board; keeps interior blanks. */
@@ -187,158 +170,4 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
while (hi > lo && lines.get(hi - 1).isBlank()) hi--; while (hi > lo && lines.get(hi - 1).isBlank()) hi--;
return lines.subList(lo, hi); return lines.subList(lo, hi);
} }
/** The CEM model name; for heads it depends on the texture's aspect (64x32 vs square 64x64). */
private String modelName(BlockEntityState s, int[][] tex) {
if (s.kind() != BlockEntityState.Kind.HEAD && s.kind() != BlockEntityState.Kind.WALL_HEAD) {
return BlockEntityModels.cemModel(s);
}
String t = s.headType();
if ("dragon".equals(t)) return "head_dragon";
if ("piglin".equals(t)) return "head_piglin";
// Square textures (zombie, player) carry a hat overlay → use the 64x64 player-head model; the
// 2:1 skull textures (skeleton, wither_skeleton, creeper) use the 64x32 skull model.
boolean square = tex.length > 0 && tex.length == tex[0].length;
return square ? "head_player" : "head";
}
/** A single bake pass: one texture applied to the parts not in {@code hidden}, with optional UV size
* and a flag forcing box-UV (for standalone part textures not matching the model's per-face UV). */
private record Layer(int[][] tex, Set<String> hidden, int texW, int texH, boolean boxUv) {
Layer(int[][] tex, Set<String> hidden) {
this(tex, hidden, 0, 0, false);
}
}
/** Some types paint different parts with different textures (pot sherds, conduit cage/heart). */
private List<Layer> layers(BlockEntityState s) {
if (s.kind() == BlockEntityState.Kind.DECORATED_POT) {
return potLayers(s);
}
if (s.kind() == BlockEntityState.Kind.CONDUIT) {
return conduitLayers();
}
int[][] tex = resolveTexture(s);
return tex == null ? List.of() : List.of(new Layer(tex, hiddenParts(s)));
}
private static final Set<String> CONDUIT_PARTS = Set.of("eye", "cage", "base", "wind");
/** The conduit's cage and inner heart use separate textures; the eye/wind (active state) are skipped. */
private List<Layer> conduitLayers() {
int[][] cage =
textures.get(ResourceLocation.parse("entity/conduit/cage")).orElse(null);
int[][] base =
textures.get(ResourceLocation.parse("entity/conduit/base")).orElse(null);
// Each part's texture is authored at its own native size (32x16) with a box-UV layout, so force
// box-UV and normalise by the texture's own size (the model's per-face UV assumes a combined sheet).
List<Layer> layers = new ArrayList<>(2);
if (cage != null)
layers.add(new Layer(cage, onlyPart("cage", CONDUIT_PARTS), cage[0].length, cage.length, true));
if (base != null)
layers.add(new Layer(base, onlyPart("base", CONDUIT_PARTS), base[0].length, base.length, true));
return layers;
}
/** Hidden set that leaves only {@code keep} visible out of {@code all}. */
private static Set<String> onlyPart(String keep, Set<String> all) {
Set<String> hidden = new HashSet<>(all);
hidden.remove(keep);
return hidden;
}
private static final Set<String> POT_PARTS = Set.of("neck", "top", "bottom", "front", "back", "left", "right");
private static final String[] POT_FACES = {"front", "left", "right", "back"}; // matches sherd capture order
private List<Layer> potLayers(BlockEntityState s) {
int[][] base = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_base"))
.orElse(null);
int[][] side = textures.get(ResourceLocation.parse("entity/decorated_pot/decorated_pot_side"))
.orElse(null);
if (base == null) return List.of();
List<Layer> layers = new ArrayList<>();
// Structure (rim/neck/foot) comes from the combined base texture; the four sides are NOT in it.
layers.add(new Layer(base, new HashSet<>(List.of("front", "back", "left", "right"))));
// Each side: its sherd pattern if set, else the plain brick side. The model's per-face UV maps the
// centre of the 16x16 texture onto the face (centred, edges intact).
for (int i = 0; i < POT_FACES.length; i++) {
int[][] tex = side;
if (i < s.sherds().size()) {
String sherd = s.sherds().get(i);
if (sherd != null && sherd.endsWith("_pottery_sherd")) {
int[][] pat = textures.get(ResourceLocation.parse(
"entity/decorated_pot/" + sherd.replace("_pottery_sherd", "_pottery_pattern")))
.orElse(null);
if (pat != null) tex = pat;
}
}
if (tex == null) continue;
layers.add(new Layer(tex, onlyPart(POT_FACES[i], POT_PARTS)));
}
return layers;
}
// --- placement parameters per type ---
/** Local placement: applied yaw (deg), model scale, and a local-frame offset (blocks). */
private record Place(double yaw, double scale, double lx, double ly, double lz) {}
private Place place(BlockEntityState s) {
double yaw = 180 - s.facingDeg(); // model default faces north; rotate by the block's facing
return switch (s.kind()) {
case SIGN -> new Place(yaw, SIGN_SCALE, 0, 0, 0);
case WALL_SIGN -> new Place(yaw, SIGN_SCALE, 0, -0.3125, 0.4375); // drop to mid-block, push to wall
case WALL_HEAD -> new Place(yaw, 1.0, 0, 0.25, 0.25); // mid-height, against the wall
case WALL_BANNER -> new Place(yaw, 1.0, 0, -0.16, 0.4375);
default -> new Place(yaw, 1.0, 0, 0, 0);
};
}
/** Parts to omit (the unused bed half / its legs, the conduit's open-state shell). */
private Set<String> hiddenParts(BlockEntityState s) {
return switch (s.kind()) {
case BED -> s.bedPart() == BlockEntityState.BedPart.HEAD
? Set.of("foot", "leg3", "leg4")
: Set.of("head", "leg1", "leg2");
case CONDUIT -> Set.of("eye", "wind");
default -> Set.of();
};
}
// --- texture resolution ---
private int[][] resolveTexture(BlockEntityState s) {
// Player heads use the owner's skin when available.
if (s.skinUrl() != null
&& (s.kind() == BlockEntityState.Kind.HEAD || s.kind() == BlockEntityState.Kind.WALL_HEAD)) {
int[][] skin = skins.get(s.skinUrl()).orElse(null);
if (skin != null) return skin;
}
boolean isBanner = s.kind() == BlockEntityState.Kind.BANNER || s.kind() == BlockEntityState.Kind.WALL_BANNER;
for (ResourceLocation rl : BlockEntityModels.textureCandidates(s)) {
int[][] t = textures.get(rl).orElse(null);
if (t == null) continue;
if (isBanner) return bakeBanner(t, s);
return t;
}
return null;
}
/**
* Composites a banner texture: tint the white {@code banner_base} cloth with the base colour, then
* alpha-overlay each pattern mask ({@code entity/banner/<key>}) dyed with its own colour, in order.
*/
private int[][] bakeBanner(int[][] base, BlockEntityState s) {
int[][] out = TextureOps.deepCopy(base);
if (s.baseColorArgb() != 0) TextureOps.tint(out, s.baseColorArgb());
for (BlockEntityState.BannerPattern pat : s.patterns()) {
int[][] mask = textures.get(ResourceLocation.parse("entity/banner/" + pat.patternKey()))
.orElse(null);
if (mask == null) continue;
int[][] dyed = TextureOps.deepCopy(mask);
TextureOps.tint(dyed, pat.colorArgb());
TextureOps.overlay(out, dyed);
}
return out;
}
} }
@@ -274,10 +274,8 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (head != null && h != null) { if (head != null && h != null) {
Affine hj = h.mul(CemGeometry.JAVA_LOCAL); Affine hj = h.mul(CemGeometry.JAVA_LOCAL);
if (head.blockEntity() != null && head.blockEntity().kind() == BlockEntityState.Kind.HEAD) { if (head.blockEntity() != null && head.blockEntity().kind() == BlockEntityState.Kind.HEAD) {
items.bakeBlockEntity( // CustomHeadLayer: head frame · S(1.1875) · SkullBlockRenderer.submitSkull (bare skull model).
head.blockEntity(), items.bakeBlockEntityModel(head.blockEntity(), hj.mul(Affine.scale(1.1875)), out);
hj.mul(Affine.scale(1.1875, -1.1875, -1.1875)).mul(Affine.translation(-0.5, 0, -0.5)),
out);
} else { } else {
// Snow golems wear their pumpkin a bit lower (SnowGolemHeadLayer) than CustomHeadLayer items. // Snow golems wear their pumpkin a bit lower (SnowGolemHeadLayer) than CustomHeadLayer items.
double dy = s.typeKey().equals("snow_golem") ? -0.34375 : -0.25; double dy = s.typeKey().equals("snow_golem") ? -0.34375 : -0.25;
@@ -10,8 +10,8 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* Shared, stateless CEM geometry baking used by both {@link CemBaker} (mobs) and the block-entity * Shared, stateless CEM geometry baking used by {@link CemBaker} (mobs), the block-entity baker and
* baker. It turns a {@link CemModelLoader.CemModel} into a list of {@link Baked} boxes (local cube + * item geometry. It turns a {@link CemModelLoader.CemModel} into a list of {@link Baked} boxes (local cube +
* its model→pre-space transform), faithfully reproducing the OptiFine/Blockbench CEM transform. The * its model→pre-space transform), faithfully reproducing the OptiFine/Blockbench CEM transform. The
* placement into the world (yaw, ground-snap, block cell) is the caller's responsibility. * placement into the world (yaw, ground-snap, block cell) is the caller's responsibility.
*/ */
@@ -40,25 +40,7 @@ final class CemGeometry {
/** Bake all parts of a model with the given pre-transform; parts in {@code hidden} are skipped. */ /** Bake all parts of a model with the given pre-transform; parts in {@code hidden} are skipped. */
static List<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> hidden) { static List<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> hidden) {
return bakeModel(model, tex, pre, hidden, 0, 0, false); return bake(model, tex, pre, hidden, Set.of(), Map.of(), null);
}
/**
* Bake all parts with an explicit texture size for UV normalisation ({@code texW}/{@code texH} of 0
* fall back to the model's declared size; use a real size when the applied texture differs from the
* model's declared {@code textureSize}, e.g. a 16x16 sherd on a 32x32-authored pot face). When
* {@code ignoreFaceUv} is set, box-UV is forced even if the model declares per-face UV — used for a
* standalone texture (e.g. the conduit cage) whose layout is box-UV, not the combined-sheet layout.
*/
static List<Baked> bakeModel(
CemModelLoader.CemModel model,
int[][] tex,
Affine pre,
Set<String> hidden,
int texW,
int texH,
boolean ignoreFaceUv) {
return bake(model, tex, pre, hidden, Set.of(), texW, texH, ignoreFaceUv, Map.of(), null);
} }
/** /**
@@ -75,7 +57,7 @@ final class CemGeometry {
Set<String> show, Set<String> show,
Map<String, CemModelLoader.PartPose> overrides, Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) { Map<String, Affine> frames) {
return bake(model, tex, pre, Set.of(), show, 0, 0, false, overrides, frames); return bake(model, tex, pre, Set.of(), show, overrides, frames);
} }
private static List<Baked> bake( private static List<Baked> bake(
@@ -84,14 +66,9 @@ final class CemGeometry {
Affine pre, Affine pre,
Set<String> hidden, Set<String> hidden,
Set<String> show, Set<String> show,
int texW,
int texH,
boolean ignoreFaceUv,
Map<String, CemModelLoader.PartPose> overrides, Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) { Map<String, Affine> frames) {
int nw = texW > 0 ? texW : model.texW(); Ctx ctx = new Ctx(hidden, show, model.texW(), model.texH(), tex, new ArrayList<>(), overrides, frames);
int nh = texH > 0 ? texH : model.texH();
Ctx ctx = new Ctx(hidden, show, nw, nh, tex, ignoreFaceUv, new ArrayList<>(), overrides, frames);
for (CemModelLoader.CemPart p : model.parts()) { for (CemModelLoader.CemPart p : model.parts()) {
if (model.absolute()) { if (model.absolute()) {
bakeAbsolute(p, pre, ctx); bakeAbsolute(p, pre, ctx);
@@ -112,7 +89,6 @@ final class CemGeometry {
int texW, int texW,
int texH, int texH,
int[][] tex, int[][] tex,
boolean ignoreFaceUv,
List<Baked> out, List<Baked> out,
Map<String, CemModelLoader.PartPose> overrides, Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) {} Map<String, Affine> frames) {}
@@ -173,8 +149,7 @@ final class CemGeometry {
double[] inf = b.inflate(); double[] inf = b.inflate();
double[] from = {org[0] - inf[0], org[1] - inf[1], org[2] - inf[2]}; double[] from = {org[0] - inf[0], org[1] - inf[1], org[2] - inf[2]};
double[] to = {org[0] + b.size()[0] + inf[0], org[1] + b.size()[1] + inf[1], org[2] + b.size()[2] + inf[2]}; double[] to = {org[0] + b.size()[0] + inf[0], org[1] + b.size()[1] + inf[1], org[2] + b.size()[2] + inf[2]};
double[][] faceUv = ctx.ignoreFaceUv ? null : b.faceUv(); ModelCube mc = new ModelCube(org, b.size(), inf[0], b.uv(), b.mirror(), b.faceUv());
ModelCube mc = new ModelCube(org, b.size(), inf[0], b.uv(), b.mirror(), faceUv);
// Vanilla texScale stretches the UV normalisation (texture treated as texSize*scale). // Vanilla texScale stretches the UV normalisation (texture treated as texSize*scale).
int tw = ctx.texW, th = ctx.texH; int tw = ctx.texW, th = ctx.texH;
if (b.texScale() != null) { if (b.texScale() != null) {
@@ -71,7 +71,7 @@ public final class CemModelLoader {
private final Map<String, CemModel> models = new HashMap<>(); private final Map<String, CemModel> models = new HashMap<>();
// Vanilla dump models live in their own namespace so they never shadow CEM-template names the // Vanilla dump models live in their own namespace so they never shadow CEM-template names the
// block-entity path relies on (e.g. "wither_skull" head vs. projectile). // legacy entity fallback relies on (the two sets use different names for different models).
private final Map<String, CemModel> vanilla = new HashMap<>(); private final Map<String, CemModel> vanilla = new HashMap<>();
private final Map<String, EntityInfo> entities = new HashMap<>(); private final Map<String, EntityInfo> entities = new HashMap<>();
@@ -120,14 +120,20 @@ public final class ItemGeometry {
} }
} }
/** A block-entity block (chest, head, banner, …) in the block cube [0,1]³ via the block-entity baker. */ /** A placed block-entity block (chest, head, banner, …) in the block cube [0,1]³ (block displays, …). */
public void bakeBlockEntity(BlockEntityState state, Affine toWorld, List<EntityCube> out) { public void bakeBlockEntity(BlockEntityState state, Affine toWorld, List<EntityCube> out) {
// Placed as in the world: cell (0,0,0) with the renderer's block pose.
if (state == null || blockEntities == null) return; if (state == null || blockEntities == null) return;
RenderedEntity r = blockEntities.bake(state); // placed at cell (bx,by,bz) = (0,0,0) RenderedEntity r = blockEntities.bake(state);
if (r == null) return; if (r == null) return;
for (EntityCube c : r.cubes) out.add(new EntityCube(c.from, c.to, c.faces, toWorld.mul(c.toWorld))); for (EntityCube c : r.cubes) out.add(new EntityCube(c.from, c.to, c.faces, toWorld.mul(c.toWorld)));
} }
/** A block-entity's bare models (no block pose) in Java model space mapped by {@code toWorld}. */
public void bakeBlockEntityModel(BlockEntityState state, Affine toWorld, List<EntityCube> out) {
if (state != null && blockEntities != null) blockEntities.bakeModel(state, toWorld, out);
}
// --- item definition / model resolution -------------------------------------------------------- // --- item definition / model resolution --------------------------------------------------------
private Info info(String id, String context) { private Info info(String id, String context) {
@@ -346,8 +352,12 @@ public final class ItemGeometry {
} }
} }
default -> { default -> {
// Block-entity specials (chest, head, banner, shulker box, bed, conduit, pot, signs, …). // Block-entity specials (chest, head, banner, shulker box, conduit, pot, …): like the vanilla
if (item.blockEntity() != null) bakeBlockEntity(item.blockEntity(), m, out); // special renderers, the bare model (no block pose) placed by the item's transformation.
if (item.blockEntity() != null && blockEntities != null) {
Affine xf = info.specialTransform != null ? m.mul(info.specialTransform) : m;
blockEntities.bakeModel(item.blockEntity(), xf, out);
}
} }
} }
} }
@@ -2,8 +2,6 @@ package eu.mhsl.minecraft.pixelpics.render.snapshot;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BannerPattern; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BannerPattern;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BedPart;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.BellAttach;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.ChestKind;
import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.Kind; import eu.mhsl.minecraft.pixelpics.render.entity.BlockEntityState.Kind;
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil; import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
@@ -19,6 +17,7 @@ import org.bukkit.World;
import org.bukkit.block.Banner; import org.bukkit.block.Banner;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.block.Conduit;
import org.bukkit.block.DecoratedPot; import org.bukkit.block.DecoratedPot;
import org.bukkit.block.Sign; import org.bukkit.block.Sign;
import org.bukkit.block.Skull; import org.bukkit.block.Skull;
@@ -26,16 +25,15 @@ import org.bukkit.block.banner.Pattern;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional; import org.bukkit.block.data.Directional;
import org.bukkit.block.data.Rotatable; import org.bukkit.block.data.Rotatable;
import org.bukkit.block.data.type.Bed;
import org.bukkit.block.data.type.Bell;
import org.bukkit.block.data.type.Chest; import org.bukkit.block.data.type.Chest;
import org.bukkit.block.data.type.Lectern;
import org.bukkit.block.sign.Side; import org.bukkit.block.sign.Side;
import org.bukkit.block.sign.SignSide; import org.bukkit.block.sign.SignSide;
import org.bukkit.profile.PlayerProfile; import org.bukkit.profile.PlayerProfile;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
/** /**
* Captures block-entities (chests, signs, banners, beds, heads, …) near the view frustum into immutable * Captures block-entities (chests, signs, banners, heads, pots, lecterns, …) near the view frustum into immutable
* {@link BlockEntityState}s. MUST run on the main thread (live {@link BlockState} access). Reads each * {@link BlockEntityState}s. MUST run on the main thread (live {@link BlockState} access). Reads each
* loaded chunk's tile-entities rather than scanning every block, then keeps only those inside the * loaded chunk's tile-entities rather than scanning every block, then keeps only those inside the
* camera's bounding box. * camera's bounding box.
@@ -120,19 +118,12 @@ public final class BlockEntitySnapshotBuilder {
return b.build(); return b.build();
} }
// --- beds ---
if (data instanceof Bed bed) {
BedPart part = bed.getPart() == Bed.Part.HEAD ? BedPart.HEAD : BedPart.FOOT;
return base(Kind.BED, bx, by, bz, faceToYaw(bed.getFacing()))
.bedPart(part)
.colorName(stripColor(n, "_BED"))
.build();
}
// --- shulker boxes --- // --- shulker boxes ---
if (n.endsWith("SHULKER_BOX")) { if (n.endsWith("SHULKER_BOX")) {
String color = n.equals("SHULKER_BOX") ? null : stripColor(n, "_SHULKER_BOX"); String color = n.equals("SHULKER_BOX") ? null : stripColor(n, "_SHULKER_BOX");
return base(Kind.SHULKER_BOX, bx, by, bz, facingYaw(data)) String dir = data instanceof Directional d ? d.getFacing().name().toLowerCase(Locale.ROOT) : "up";
return base(Kind.SHULKER_BOX, bx, by, bz, 0)
.direction(dir)
.colorName(color) .colorName(color)
.build(); .build();
} }
@@ -165,7 +156,6 @@ public final class BlockEntitySnapshotBuilder {
// --- signs --- // --- signs ---
if (n.endsWith("_SIGN")) { if (n.endsWith("_SIGN")) {
String wood = signWood(n);
Kind kind; Kind kind;
float yaw; float yaw;
if (n.endsWith("_WALL_SIGN")) { if (n.endsWith("_WALL_SIGN")) {
@@ -178,7 +168,7 @@ public final class BlockEntitySnapshotBuilder {
kind = Kind.SIGN; kind = Kind.SIGN;
yaw = rotationYaw(data); yaw = rotationYaw(data);
} }
Builder b = base(kind, bx, by, bz, yaw).wood(wood); Builder b = base(kind, bx, by, bz, yaw);
if (ts instanceof Sign sign) { if (ts instanceof Sign sign) {
b.frontText(signText(sign.getSide(Side.FRONT))); b.frontText(signText(sign.getSide(Side.FRONT)));
b.backText(signText(sign.getSide(Side.BACK))); b.backText(signText(sign.getSide(Side.BACK)));
@@ -200,7 +190,16 @@ public final class BlockEntitySnapshotBuilder {
// --- conduit --- // --- conduit ---
if (mat == Material.CONDUIT) { if (mat == Material.CONDUIT) {
return base(Kind.CONDUIT, bx, by, bz, 0).build(); return base(Kind.CONDUIT, bx, by, bz, 0)
.active(ts instanceof Conduit c && c.isActive())
.build();
}
// --- lectern (the block is a block model; the renderer only adds the open book) ---
if (data instanceof Lectern lectern) {
return base(Kind.LECTERN, bx, by, bz, facingYaw(data))
.active(lectern.hasBook())
.build();
} }
// --- decorated pot --- // --- decorated pot ---
@@ -210,19 +209,9 @@ public final class BlockEntitySnapshotBuilder {
return b.build(); return b.build();
} }
// --- bell --- // --- bell (frame = block model; the renderer draws the bell body) ---
if (mat == Material.BELL) { if (mat == Material.BELL) {
BellAttach attach = BellAttach.FLOOR; return base(Kind.BELL, bx, by, bz, 0).build();
if (data instanceof Bell bd) {
attach = switch (bd.getAttachment()) {
case FLOOR -> BellAttach.FLOOR;
case CEILING -> BellAttach.CEILING;
case SINGLE_WALL -> BellAttach.SINGLE_WALL;
case DOUBLE_WALL -> BellAttach.DOUBLE_WALL;};
}
return base(Kind.BELL, bx, by, bz, facingYaw(data))
.bellAttach(attach)
.build();
} }
return null; return null;
@@ -295,17 +284,6 @@ public final class BlockEntitySnapshotBuilder {
lines, ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow); lines, ColorUtil.signFillArgb(dye, glow), ColorUtil.signOutlineArgb(dye), glow);
} }
private static String signWood(String name) {
String s = name;
for (String suf : new String[] {"_WALL_HANGING_SIGN", "_HANGING_SIGN", "_WALL_SIGN", "_SIGN"}) {
if (s.endsWith(suf)) {
s = s.substring(0, s.length() - suf.length());
break;
}
}
return s.toLowerCase(Locale.ROOT);
}
private static String headType(String name) { private static String headType(String name) {
String s = name.replace("_WALL_", "_"); String s = name.replace("_WALL_", "_");
return switch (s) { return switch (s) {
@@ -320,7 +298,7 @@ public final class BlockEntitySnapshotBuilder {
} }
private static List<String> sherds(DecoratedPot pot) { private static List<String> sherds(DecoratedPot pot) {
// Order: front, left, right, back — matches the CEM decorated_pot face parts. // Order: front, left, right, back — the DecoratedPotRenderer side parts (BlockEntityModels.potParts).
List<String> out = new ArrayList<>(4); List<String> out = new ArrayList<>(4);
for (DecoratedPot.Side side : new DecoratedPot.Side[] { for (DecoratedPot.Side side : new DecoratedPot.Side[] {
DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK DecoratedPot.Side.FRONT, DecoratedPot.Side.LEFT, DecoratedPot.Side.RIGHT, DecoratedPot.Side.BACK
@@ -349,7 +327,7 @@ public final class BlockEntitySnapshotBuilder {
return p.getPattern().getKey().getKey(); return p.getPattern().getKey().getKey();
} }
// --- small fluent builder to keep the 15-field record construction readable --- // --- small fluent builder to keep the record construction readable ---
private static Builder base(Kind kind, int bx, int by, int bz, float yaw) { private static Builder base(Kind kind, int bx, int by, int bz, float yaw) {
return new Builder(kind, bx, by, bz, yaw); return new Builder(kind, bx, by, bz, yaw);
@@ -359,16 +337,15 @@ public final class BlockEntitySnapshotBuilder {
private final Kind kind; private final Kind kind;
private final int bx, by, bz; private final int bx, by, bz;
private final float yaw; private final float yaw;
private String direction;
private boolean active;
private ChestKind chestKind; private ChestKind chestKind;
private int baseColorArgb; private int baseColorArgb;
private String colorName; private String colorName;
private String wood;
private BedPart bedPart;
private String headType; private String headType;
private String skinUrl; private String skinUrl;
private List<BannerPattern> patterns = List.of(); private List<BannerPattern> patterns = List.of();
private List<String> sherds = List.of(); private List<String> sherds = List.of();
private BellAttach bellAttach;
private BlockEntityState.SignText frontText; private BlockEntityState.SignText frontText;
private BlockEntityState.SignText backText; private BlockEntityState.SignText backText;
@@ -395,13 +372,13 @@ public final class BlockEntitySnapshotBuilder {
return this; return this;
} }
Builder wood(String v) { Builder direction(String v) {
this.wood = v; this.direction = v;
return this; return this;
} }
Builder bedPart(BedPart v) { Builder active(boolean v) {
this.bedPart = v; this.active = v;
return this; return this;
} }
@@ -425,11 +402,6 @@ public final class BlockEntitySnapshotBuilder {
return this; return this;
} }
Builder bellAttach(BellAttach v) {
this.bellAttach = v;
return this;
}
Builder frontText(BlockEntityState.SignText v) { Builder frontText(BlockEntityState.SignText v) {
this.frontText = v; this.frontText = v;
return this; return this;
@@ -447,16 +419,15 @@ public final class BlockEntitySnapshotBuilder {
by, by,
bz, bz,
yaw, yaw,
direction,
active,
chestKind, chestKind,
baseColorArgb, baseColorArgb,
colorName, colorName,
wood,
bedPart,
headType, headType,
skinUrl, skinUrl,
patterns, patterns,
sherds, sherds,
bellAttach,
frontText, frontText,
backText); backText);
} }
@@ -1 +1 @@
{"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated {"version":"26.3","models":{"acacia_boat":{"model":"{\"textureSize\":[128,64],\"absolute\":true,\"layer\":\"boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]}]}]}"},"acacia_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"acacia_chest_boat":{"model":"{\"textureSize\":[128,128],\"absolute\":true,\"layer\":\"chest_boat/acacia#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"chest_bottom\",\"pivot\":[2.0,29.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,21.0,-6.0,12.0,8.0,12.0],\"textureOffset\":[0.0,76.0]}]},{\"part\":\"right_paddle\",\"pivot\":[-3.0,29.0,-9.0],\"rotate\":[37.5,-142.1347,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,-14.0,2.0,2.0,18.0],\"textureOffset\":[62.0,20.0]},{\"coordinates\":[-4.001,26.0,-1.0,1.0,6.0,7.0],\"textureOffset\":[62.0,20.0]}]},{\"part\":\"left_paddle\",\"pivot\":[-3.0,29.0,9.0],\"rotate\":[37.5,-37.8653,11.25],\"boxes\":[{\"coordinates\":[-4.0,27.0,4.0,2.0,2.0,18.0],\"textureOffset\":[62.0,0.0]},{\"coordinates\":[-2.999,26.0,17.0,1.0,6.0,7.0],\"textureOffset\":[62.0,0.0]}]},{\"part\":\"left\",\"pivot\":[0.0,20.0,9.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,8.0,28.0,6.0,2.0],\"textureOffset\":[0.0,43.0]}]},{\"part\":\"bottom\",\"pivot\":[0.0,21.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,14.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"back\",\"pivot\":[15.0,20.0,4.0],\"rotate\":[0.0,-270.0,0.0],\"boxes\":[{\"coordinates\":[10.0,21.0,3.0,18.0,6.0,2.0],\"textureOffset\":[0.0,19.0]}]},{\"part\":\"chest_lid\",\"pivot\":[2.0,33.0,-6.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-10.0,29.0,-6.0,12.0,4.0,12.0],\"textureOffset\":[0.0,59.0]}]},{\"part\":\"front\",\"pivot\":[-15.0,20.0,0.0],\"rotate\":[0.0,-90.0,0.0],\"boxes\":[{\"coordinates\":[-23.0,21.0,-1.0,16.0,6.0,2.0],\"textureOffset\":[0.0,27.0]}]},{\"part\":\"right\",\"pivot\":[0.0,20.0,-9.0],\"rotate\":[0.0,-180.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,21.0,-10.0,28.0,6.0,2.0],\"textureOffset\":[0.0,35.0]}]},{\"part\":\"chest_lock\",\"pivot\":[1.0,30.0,-1.0],\"rotate\":[0.0,90.0,0.0],\"boxes\":[{\"coordinates\":[-1.0,26.0,-1.0,2.0,4.0,1.0],\"textureOffset\":[0.0,59.0]}]}]}]}"},"acacia_chest_boat/waterPatchModel":{"model":"{\"textureSize\":[0,0],\"absolute\":true,\"layer\":\"boat#water_patch\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,24.0,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"water_patch\",\"pivot\":[0.0,27.0,1.0],\"rotate\":[-90.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-14.0,20.0,-2.0,28.0,16.0,3.0],\"textureOffset\":[0.0,0.0]}]}]}]}"},"allay":{"model":"{\"textureSize\":[32,32],\"absolute\":true,\"layer\":\"allay#main\",\"models\":[{\"part\":\"root\",\"pivot\":[0.0,0.25,0.0],\"rotate\":[0.0,0.0,0.0],\"submodels\":[{\"part\":\"head\",\"pivot\":[0.0,4.24,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-2.5,4.24,-2.5,5.0,5.0,5.0],\"textureOffset\":[0.0,0.0]}]},{\"part\":\"body\",\"pivot\":[0.0,4.25,0.0],\"rotate\":[0.0,0.0,0.0],\"boxes\":[{\"coordinates\":[-1.5,0.25,-1.0,3.0,4.0,2.0],\"textureOffset\":[0.0,10.0]},{\"coordinates\":[-1.5,-0.75,-1.0,3.0,5.0,2.0],\"textureOffset\":[0.0,16.0],\"sizeAdd\":-0.2}],\"submodels\":[{\"part\":\"right_arLine truncated
+93 -51
View File
@@ -33,14 +33,20 @@ public class BlockEntityTestRender {
static BlockEntityBaker beBaker; static BlockEntityBaker beBaker;
static DecorationBaker decoBaker; static DecorationBaker decoBaker;
static BlockEntityState be(Kind kind, int bx, int by, int bz, float yaw) { /** A block-entity state at (bx, 0, bz); unused fields default. */
return new BlockEntityState(kind, bx, by, bz, yaw, ChestKind.SINGLE, 0, null, "oak", static BlockEntityState st(Kind kind, int bx, int bz, float yaw, String direction, boolean active, String color,
null, null, null, List.of(), List.of(), null, null, null); int baseColor, String head, List<BannerPattern> pats, List<String> sherds) {
return new BlockEntityState(kind, bx, 0, bz, yaw, direction, active, ChestKind.SINGLE, baseColor, color,
head, null, pats, sherds, null, null);
}
static BlockEntityState be(Kind kind, float yaw) {
return st(kind, 0, 0, yaw, null, false, null, 0, null, List.of(), List.of());
} }
static BlockEntityState sign(Kind kind, float yaw, SignText front, SignText back) { static BlockEntityState sign(Kind kind, float yaw, SignText front, SignText back) {
return new BlockEntityState(kind, 0, 0, 0, yaw, null, 0, null, "oak", return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, null, null, null,
null, null, null, List.of(), List.of(), null, front, back); List.of(), List.of(), front, back);
} }
static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) { static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) {
@@ -57,7 +63,7 @@ public class BlockEntityTestRender {
BlockModelRegistry registry = new BlockModelRegistry(reader, textures); BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures); BiomeTintProvider tint = new BiomeTintProvider(textures);
CemModelLoader geo = new CemModelLoader(); CemModelLoader geo = new CemModelLoader();
geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); geo.load(new java.io.FileInputStream("src/main/resources/cem/cem_template_models.json"), log); // entity fallback only
geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log); geo.load(new java.io.FileInputStream("src/main/resources/cem/vanilla_entity_models.json"), log);
SkinCache skins = new SkinCache(); SkinCache skins = new SkinCache();
eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font = eu.mhsl.minecraft.pixelpics.assets.font.BitmapFont font =
@@ -113,49 +119,86 @@ public class BlockEntityTestRender {
chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"), chest(Kind.COPPER_CHEST, 0, 0, 0, ChestKind.LEFT, "oxidized"),
chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized"))); chest(Kind.COPPER_CHEST, -1, 0, 0, ChestKind.RIGHT, "oxidized")));
frontCam.put("copper_oxidized_double", 0f); frontCam.put("copper_oxidized_double", 0f);
scenes.put("sign", List.of(be(Kind.SIGN, 0, 0, 0, 0))); // --- sign text (boards are block models, not drawn here) --- facingDeg 180 → front faces north.
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)));
// --- sign text (calibration) --- facingDeg=180 → front (model −Z) faces the camera (north).
org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK; org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK;
scenes.put("sign_text", List.of(sign(Kind.SIGN, 180, String[] D = {"S", "W", "N", "E"};
txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null))); for (int i = 0; i < 4; i++) {
scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180, float yaw = i * 90;
txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null))); scenes.put("sign_text_" + D[i], List.of(sign(Kind.SIGN, yaw, txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)));
scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, frontCam.put("sign_text_" + D[i], yaw);
txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null))); }
scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180, scenes.put("sign_text_diag", List.of(sign(Kind.SIGN, 22.5f * 3, txt(B, false, "rot 3", "67.5 deg", "", ""), null)));
txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null))); frontCam.put("sign_text_diag", 22.5f * 3);
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180, scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180, txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)));
txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null))); frontCam.put("wall_sign_text", 180f);
scenes.put("sign_text_front", List.of(sign(Kind.SIGN, 180, scenes.put("wall_sign_text_E", List.of(sign(Kind.WALL_SIGN, 270, txt(B, false, "Wall East", "Line 2", "", ""), null)));
txt(B, false, "FRONT", "side", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACK", "side", "", "")))); frontCam.put("wall_sign_text_E", 270f);
// View the BACK side head-on (facingDeg=0 turns the back face toward the camera). scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null)));
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 0, frontCam.put("hanging_sign_text", 180f);
txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", "")))); scenes.put("sign_text_red", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null)));
frontCam.put("sign_text_red", 180f);
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180, txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null)));
frontCam.put("sign_text_glow", 180f);
// Back side viewed head-on: the camera looks at the side opposite the front.
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 180, txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", ""))));
frontCam.put("sign_view_back", 0f);
scenes.put("sign_1line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "SHOP", "", ""), null))); scenes.put("sign_1line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "SHOP", "", ""), null)));
scenes.put("sign_2line", List.of(sign(Kind.SIGN, 180, txt(B, false, "", "Welcome", "home!", ""), null))); frontCam.put("sign_1line", 180f);
scenes.put("sign_long", List.of(sign(Kind.SIGN, 180, txt(B, false, "a very long line here", "", "", ""), null))); scenes.put("sign_long", List.of(sign(Kind.SIGN, 180, txt(B, false, "a very long line here", "", "", ""), null)));
scenes.put("banner", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFCC2020, null, null, null, null, null, List.of(), List.of(), null, null, null))); frontCam.put("sign_long", 180f);
scenes.put("banner_patterned", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFFFFFFF, null, null, null, null, null, // --- banners: standing (rotation 0/4/6), wall (4 facings), patterned ---
List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)), List.of(), null, null, null))); List<BannerPattern> pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020));
scenes.put("wall_banner", List.of(new BlockEntityState(Kind.WALL_BANNER, 0, 0, 0, 0, null, 0xFF2040CC, null, null, null, null, null, List.of(), List.of(), null, null, null))); for (float yaw : new float[] {0, 90, 135}) {
scenes.put("bed", List.of( String k = "banner_" + (int) yaw;
new BlockEntityState(Kind.BED, 0, 0, 0, 0, null, 0, "red", null, BedPart.FOOT, null, null, List.of(), List.of(), null, null, null), scenes.put(k, List.of(st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())));
new BlockEntityState(Kind.BED, 0, 0, 1, 0, null, 0, "red", null, BedPart.HEAD, null, null, List.of(), List.of(), null, null, null))); frontCam.put(k, yaw);
scenes.put("shulker_box", List.of(new BlockEntityState(Kind.SHULKER_BOX, 0, 0, 0, 0, null, 0, null, null, null, null, null, List.of(), List.of(), null, null, null))); }
scenes.put("conduit", List.of(be(Kind.CONDUIT, 0, 0, 0, 0))); scenes.put("banner_patterned", List.of(st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of())));
scenes.put("decorated_pot", List.of(be(Kind.DECORATED_POT, 0, 0, 0, 0))); frontCam.put("banner_patterned", 0f);
scenes.put("decorated_pot_sherds", List.of(new BlockEntityState(Kind.DECORATED_POT, 0, 0, 0, 0, null, 0, null, null, null, null, null, scenes.put("banner_side", scenes.get("banner_patterned"));
List.of(), List.of("angler_pottery_sherd", "heart_pottery_sherd", "skull_pottery_sherd", "brick"), null, null, null))); frontCam.put("banner_side", 90f);
scenes.put("bell", List.of(be(Kind.BELL, 0, 0, 0, 0))); for (int i = 0; i < 4; i++) {
scenes.put("head_skeleton", List.of(head("skeleton"))); scenes.put("wall_banner_" + D[i], List.of(st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of())));
scenes.put("head_zombie", List.of(head("zombie"))); frontCam.put("wall_banner_" + D[i], i * 90f);
scenes.put("head_creeper", List.of(head("creeper"))); }
scenes.put("head_dragon", List.of(head("dragon"))); // --- shulker boxes: 6 facings ---
scenes.put("head_piglin", List.of(head("piglin"))); for (String d : new String[] {"up", "down", "north", "south", "east", "west"}) {
scenes.put("wither_skull", List.of(headKind(Kind.HEAD, "wither_skeleton"))); scenes.put("shulker_" + d, List.of(st(Kind.SHULKER_BOX, 0, 0, 0, d, false, d.equals("up") ? null : "red", 0, null, List.of(), List.of())));
scenes.put("wall_head", List.of(headKind(Kind.WALL_HEAD, "skeleton"))); frontCam.put("shulker_" + d, 0f);
}
scenes.put("conduit", List.of(be(Kind.CONDUIT, 0)));
scenes.put("conduit_active", List.of(st(Kind.CONDUIT, 0, 0, 0, null, true, null, 0, null, List.of(), List.of())));
List<String> sherds = List.of("heart_pottery_sherd", "skull_pottery_sherd", "angler_pottery_sherd", "brick");
for (int i = 0; i < 4; i++) {
scenes.put("pot_" + D[i], List.of(st(Kind.DECORATED_POT, 0, 0, i * 90, null, false, null, 0, null, List.of(), sherds)));
frontCam.put("pot_" + D[i], i * 90f);
}
scenes.put("pot_plain", List.of(be(Kind.DECORATED_POT, 0)));
frontCam.put("pot_plain", 0f);
scenes.put("bell", List.of(be(Kind.BELL, 0)));
for (int i = 0; i < 4; i++) {
scenes.put("lectern_book_" + D[i], List.of(st(Kind.LECTERN, 0, 0, i * 90, null, true, null, 0, null, List.of(), List.of())));
frontCam.put("lectern_book_" + D[i], i * 90f);
}
// --- heads: every type facing the camera (rotation 0), plus rotations and wall mounts ---
for (String t : new String[] {"skeleton", "wither_skeleton", "zombie", "creeper", "player", "piglin", "dragon"}) {
scenes.put("head_" + t, List.of(head(Kind.HEAD, t, 0)));
frontCam.put("head_" + t, 0f);
}
scenes.put("head_dragon_side", List.of(head(Kind.HEAD, "dragon", 0)));
frontCam.put("head_dragon_side", 90f);
for (float yaw : new float[] {90, 157.5f, 270}) {
scenes.put("head_creeper_" + yaw, List.of(head(Kind.HEAD, "creeper", yaw)));
frontCam.put("head_creeper_" + yaw, yaw);
}
for (int i = 0; i < 4; i++) {
scenes.put("wall_head_" + D[i], List.of(head(Kind.WALL_HEAD, "skeleton", i * 90)));
frontCam.put("wall_head_" + D[i], i * 90f);
}
scenes.put("wall_dragon_N", List.of(head(Kind.WALL_HEAD, "dragon", 180)));
frontCam.put("wall_dragon_N", 180f);
scenes.put("wall_piglin_E", List.of(head(Kind.WALL_HEAD, "piglin", 270)));
frontCam.put("wall_piglin_E", 270f);
// Decoration scenes (paintings + item frames), rendered via the same scene path. // Decoration scenes (paintings + item frames), rendered via the same scene path.
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>(); LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
@@ -206,13 +249,12 @@ public class BlockEntityTestRender {
static final Map<String, Float> frontCam = new HashMap<>(); static final Map<String, Float> frontCam = new HashMap<>();
static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) { 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, return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null,
List.of(), List.of(), null, null, null); List.of(), List.of(), null, null);
} }
static BlockEntityState head(String type) { return headKind(Kind.HEAD, type); } static BlockEntityState head(Kind kind, String type, float yaw) {
static BlockEntityState headKind(Kind kind, String type) { return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of());
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, static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
+43
View File
@@ -156,6 +156,29 @@ public class VanillaModelDump {
failed.add(name + " (layer: " + t + ")"); failed.add(name + " (layer: " + t + ")");
} }
} }
// Block-entity models posed by their own setupAnim with the static state the vanilla
// BlockEntityRenderer uses ("posed/<layer>"): skulls at animation 0 (dragon jaw, piglin ears), banner
// flags at wave phase 0 (item/static pose), the lectern's open book (LecternRenderer.BOOK_STATE).
// Rest-pose-only models (chest, bell, shulker box, pot, conduit) use "layer/<layer>" directly.
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.DRAGON_SKULL,
m -> new net.minecraft.client.model.object.skull.DragonHeadModel(m),
new net.minecraft.client.model.object.skull.SkullModelBase.State());
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.PIGLIN_HEAD,
m -> new net.minecraft.client.model.object.skull.PiglinHeadModel(m),
new net.minecraft.client.model.object.skull.SkullModelBase.State());
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.STANDING_BANNER_FLAG,
m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F);
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.WALL_BANNER_FLAG,
m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F);
try {
Field bookState = Class.forName("net.minecraft.client.renderer.blockentity.LecternRenderer")
.getDeclaredField("BOOK_STATE");
bookState.setAccessible(true);
posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.BOOK,
m -> new net.minecraft.client.model.object.book.BookModel(m), bookState.get(null));
} catch (ReflectiveOperationException e) {
failed.add("posed/book (" + e + ")");
}
JsonObject root = new JsonObject(); JsonObject root = new JsonObject();
root.addProperty("version", SharedConstants.getCurrentVersion().name()); root.addProperty("version", SharedConstants.getCurrentVersion().name());
JsonObject models = new JsonObject(); JsonObject models = new JsonObject();
@@ -171,6 +194,26 @@ public class VanillaModelDump {
for (String f : failed) System.out.println("FAILED " + f); for (String f : failed) System.out.println("FAILED " + f);
} }
/** One block-entity model posed with {@code state} (see main), stored as {@code posed/<model>#<layer>}. */
static void posedBlockEntity(
Map<String, JsonObject> out,
List<String> failed,
EntityModelSet set,
ModelLayerLocation loc,
java.util.function.Function<ModelPart, Model<?>> factory,
Object state) {
String name = "posed/" + loc.model().getPath() + "#" + loc.layer();
try {
JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed);
if (jem == null) return;
JsonObject entry = new JsonObject();
entry.addProperty("model", new Gson().toJson(jem));
out.put(name, entry);
} catch (Throwable t) {
failed.add(name + " (" + t + ")");
}
}
/** /**
* Small, name-based tweaks so the default render state shows the typical idle look: illagers stand * Small, name-based tweaks so the default render state shows the typical idle look: illagers stand
* with crossed arms (their peaceful pose) instead of the empty-handed "neutral" pose. * with crossed arms (their peaceful pose) instead of the empty-handed "neutral" pose.
+143
View File
@@ -91,6 +91,31 @@ public class PPTest extends JavaPlugin {
scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5)); scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5));
scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1)); scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1));
scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1)); scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1));
// Block entities (vanilla BlockEntityRenderer poses): signs, banners, heads in all 16 rotations + wall
// mounts, and bells/pots/shulker boxes/lecterns/conduits, each layout seen from two sides.
scenes.put("be_signs_s", new Scene("be_signs_s", this::beSigns, 7, 3, 8, 7, 0.5, 1));
scenes.put("be_signs_n", new Scene("be_signs_n", this::beSigns, 7, 1.3, -8, 7, 0.6, 1));
scenes.put("be_signs_close", new Scene("be_signs_close", this::beSigns, 3, 1.8, 4.2, 3, 0.8, 2));
scenes.put("be_signs_wall", new Scene("be_signs_wall", this::beSigns, 4.6, 2.2, 10.2, 3.5, 0.9, 7));
scenes.put("be_signs_wall_n", new Scene("be_signs_wall_n", this::beSigns, 1.4, 2.2, 3.8, 3.5, 0.9, 7));
scenes.put("be_signs_wall2", new Scene("be_signs_wall2", this::beSigns, 12.5, 2.4, 10.2, 10, 1.2, 7));
scenes.put("be_signs_hang", new Scene("be_signs_hang", this::beSigns, 4, 1.3, 0.2, 4, 1.4, -3));
scenes.put("be_banners_s", new Scene("be_banners_s", this::beBanners, 7, 3, 10, 7, 1, 1.5));
scenes.put("be_banners_n", new Scene("be_banners_n", this::beBanners, 7, 3, -6, 7, 1, 1.5));
scenes.put("be_banners_wall", new Scene("be_banners_wall", this::beBanners, 13, 3.5, 9, 16, 1.5, 5));
scenes.put("be_banners_wall_n", new Scene("be_banners_wall_n", this::beBanners, 13, 3.5, 1, 16, 1.5, 5));
scenes.put("be_heads_s", new Scene("be_heads_s", this::beHeads, 5, 5, 11, 5, 0, 3));
scenes.put("be_heads_n", new Scene("be_heads_n", this::beHeads, 5, 5, -6, 5, 0, 3));
scenes.put("be_heads_close", new Scene("be_heads_close", this::beHeads, 4, 2.2, 9.5, 4, 0.3, 5));
scenes.put("be_heads_wall_w", new Scene("be_heads_wall_w", this::beHeads, 7, 3, 2.5, 12, 1, 2.5));
scenes.put("be_heads_wall_e", new Scene("be_heads_wall_e", this::beHeads, 17, 3, 2.5, 12, 1, 2.5));
scenes.put("be_misc_s", new Scene("be_misc_s", this::beMisc, 6, 5, 11, 6, 0.5, 3));
scenes.put("be_misc_n", new Scene("be_misc_n", this::beMisc, 6, 5, -6, 6, 0.5, 3));
scenes.put("be_misc_close", new Scene("be_misc_close", this::beMisc, 3, 2.5, 3, 3, 0.5, 0));
scenes.put("be_misc_lectern", new Scene("be_misc_lectern", this::beMisc, 3, 2.2, 9.5, 3, 0.6, 6));
scenes.put("be_misc_shulker", new Scene("be_misc_shulker", this::beMisc, 11, 2.8, 9.5, 11, 0.3, 4));
scenes.put("be_items", new Scene("be_items", this::beItems, 4.5, 2.6, 11, 4.5, 0.8, 3));
scenes.put("be_items_back", new Scene("be_items_back", this::beItems, 4.5, 2.6, -5, 4.5, 0.8, 3));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2)); scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
} }
@@ -741,4 +766,122 @@ public class PPTest extends JavaPlugin {
} }
} }
} }
// --- block-entity scenes ------------------------------------------------------------------------
private void put(Location o, int x, int y, int z, String data) {
o.clone().add(x, y, z).getBlock().setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
}
private void signText(Location o, int x, int y, int z, String front, String back) {
if (!(o.clone().add(x, y, z).getBlock().getState() instanceof org.bukkit.block.Sign sg)) return;
sg.getSide(org.bukkit.block.sign.Side.FRONT).setLine(1, front);
sg.getSide(org.bukkit.block.sign.Side.BACK).setLine(1, back);
sg.getSide(org.bukkit.block.sign.Side.BACK).setColor(DyeColor.BLUE);
sg.update(true, false);
}
private static final String[] DIRS = {"north", "east", "south", "west"};
private static final int[][] STEP = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
/** A stone pillar with one wall-mounted block on each of its four sides ({@code %s} = facing). */
private void pillar(Location o, int x, int z, int y, String fmt) {
put(o, x, y, z, "stone");
for (int i = 0; i < 4; i++) put(o, x + STEP[i][0], y, z + STEP[i][1], String.format(fmt, DIRS[i]));
}
/** Standing signs rot 0..15 (two rows), ceiling hanging signs rot 0..15, wall + wall-hanging signs. */
private void beSigns(Location o) {
for (int r = 0; r < 16; r++) {
int x = (r % 8) * 2, z = r < 8 ? 0 : 2;
put(o, x, 0, z, "oak_sign[rotation=" + r + "]");
signText(o, x, 0, z, "F" + r, "B" + r);
put(o, r, 2, -3, "stone");
put(o, r, 1, -3, "spruce_hanging_sign[rotation=" + r + ",attached=true]");
signText(o, r, 1, -3, "F" + r, "B" + r);
}
pillar(o, 3, 7, 0, "birch_wall_sign[facing=%s]");
pillar(o, 3, 7, 1, "cherry_wall_hanging_sign[facing=%s]");
for (int i = 0; i < 4; i++) {
signText(o, 3 + STEP[i][0], 0, 7 + STEP[i][1], DIRS[i], "back");
signText(o, 3 + STEP[i][0], 1, 7 + STEP[i][1], DIRS[i], "back");
}
pillar(o, 10, 7, 1, "dark_oak_wall_sign[facing=%s]");
for (int i = 0; i < 4; i++) signText(o, 10 + STEP[i][0], 1, 7 + STEP[i][1], DIRS[i].toUpperCase(), "");
}
/** Standing banners rot 0..15 with patterns, wall banners on a pillar. */
private void beBanners(Location o) {
DyeColor[] cols = DyeColor.values();
for (int r = 0; r < 16; r++) {
int x = (r % 8) * 2, z = r < 8 ? 0 : 3;
put(o, x, 0, z, cols[r].name().toLowerCase() + "_banner[rotation=" + r + "]");
if (o.clone().add(x, 0, z).getBlock().getState() instanceof org.bukkit.block.Banner b) {
b.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.BLACK, org.bukkit.block.banner.PatternType.STRIPE_TOP));
b.addPattern(new org.bukkit.block.banner.Pattern(DyeColor.WHITE, org.bukkit.block.banner.PatternType.CREEPER));
b.update(true, false);
}
}
pillar(o, 16, 5, 2, "red_wall_banner[facing=%s]");
put(o, 16, 1, 5, "stone");
put(o, 16, 0, 5, "stone");
}
/** Every head type in five rotations (0, 2, 4, 8, 12) per row, wall heads on pillars. */
private void beHeads(Location o) {
String[] t = {"skeleton_skull", "wither_skeleton_skull", "zombie_head", "creeper_head", "player_head", "piglin_head", "dragon_head"};
int[] rots = {0, 2, 4, 8, 12};
for (int i = 0; i < t.length; i++)
for (int j = 0; j < rots.length; j++) put(o, j * 2, 0, i, t[i] + "[rotation=" + rots[j] + "]");
String[] wall = {"skeleton_wall_skull", "dragon_wall_head", "piglin_wall_head", "player_wall_head"};
for (int i = 0; i < wall.length; i++) pillar(o, 12, i * 3 - 2, 1, wall[i] + "[facing=%s]");
for (int i = 0; i < wall.length; i++) put(o, 12, 0, i * 3 - 2, "stone");
}
/** Bells (all attachments/facings), decorated pots with sherds, shulker boxes (6 facings), lecterns, conduit. */
private void beMisc(Location o) {
for (int i = 0; i < 4; i++) {
put(o, i * 2, 0, 0, "bell[attachment=floor,facing=" + DIRS[i] + "]");
put(o, i * 2, 2, 2, "stone");
put(o, i * 2, 1, 2, "bell[attachment=ceiling,facing=" + DIRS[i] + "]");
put(o, i * 2, 0, 4, "decorated_pot[facing=" + DIRS[i] + "]");
if (o.clone().add(i * 2, 0, 4).getBlock().getState() instanceof org.bukkit.block.DecoratedPot pot) {
pot.setSherd(org.bukkit.block.DecoratedPot.Side.FRONT, Material.HEART_POTTERY_SHERD);
pot.setSherd(org.bukkit.block.DecoratedPot.Side.LEFT, Material.SKULL_POTTERY_SHERD);
pot.setSherd(org.bukkit.block.DecoratedPot.Side.RIGHT, Material.ANGLER_POTTERY_SHERD);
pot.update(true, false);
}
put(o, i * 2, 0, 6, "lectern[facing=" + DIRS[i] + ",has_book=true]");
}
put(o, 9, 1, 0, "stone");
put(o, 10, 1, 0, "bell[attachment=single_wall,facing=east]");
put(o, 12, 1, 0, "stone");
put(o, 11, 1, 0, "bell[attachment=double_wall,facing=east]");
String[] f6 = {"up", "down", "north", "south", "east", "west"};
String[] c6 = {"", "red_", "blue_", "lime_", "black_", "white_"};
for (int i = 0; i < 6; i++) put(o, 9 + (i % 3) * 2, 0, 3 + (i / 3) * 2, c6[i] + "shulker_box[facing=" + f6[i] + "]");
put(o, 9, 0, 7, "conduit");
put(o, 8, 0, 6, "lectern[facing=south,has_book=false]");
}
/** Block-entity items: held by armor stands, worn on heads, as item displays and as block displays. */
private void beItems(Location o) {
Material[] m = {Material.CHEST, Material.RED_BANNER, Material.SHULKER_BOX, Material.DECORATED_POT,
Material.CONDUIT, Material.DRAGON_HEAD, Material.PIGLIN_HEAD, Material.CREEPER_HEAD, Material.PLAYER_HEAD};
for (int i = 0; i < m.length; i++) {
ArmorStand as = spawn(o, i + 0.5, 0, 6.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(it(m[i]));
if (m[i].name().endsWith("_HEAD")) as.getEquipment().setHelmet(it(m[i]));
ItemDisplay d = spawn(o, i + 0.5, 0.6, 3, 0, ItemDisplay.class);
d.setItemStack(it(m[i]));
d.setTransformation(new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(0.6f), new Quaternionf()));
}
String[] blocks = {"chest[facing=south]", "red_banner[rotation=0]", "shulker_box[facing=up]", "decorated_pot[facing=south]",
"skeleton_skull[rotation=8]", "lectern[facing=south,has_book=true]"};
for (int i = 0; i < blocks.length; i++) {
BlockDisplay b = spawn(o, i * 1.5, 0, 0, 0, BlockDisplay.class);
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
}
}
} }
+3 -3
View File
@@ -4,9 +4,9 @@
# Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON, # Source: the CEM Template Loader data (Ewan Howell / wynem) — vanilla Java entity models as JSON,
# already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes. # already correctly posed. These ultimately mirror Mojang's hardcoded Java EntityModel classes.
# #
# NOTE: this set is now only the FALLBACK (rest-pose geometry) + the block-entity models (chests, signs, # NOTE: this set is now only the FALLBACK (rest-pose geometry) for entity types missing from the vanilla
# heads, banners, armor_layer_*). Mob models come from the vanilla client dump — regenerate that with # client dump (+ the elytra fallback). Mob AND block-entity models come from that dump — regenerate it
# tools/update-vanilla-models.sh <version> on every Minecraft update. # with tools/update-vanilla-models.sh <version> on every Minecraft update.
# #
# Usage: tools/update-cem-models.sh # update if newer # Usage: tools/update-cem-models.sh # update if newer
# tools/update-cem-models.sh --force # always overwrite # tools/update-cem-models.sh --force # always overwrite