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);
// 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 so the ray passes through to that geometry instead of drawing a grey fallback cube.
if (isBlockEntityRendered(material)) {
@@ -120,7 +120,6 @@ public final class BlockModelRegistry {
String n = m.name();
boolean match = n.endsWith("_SIGN") // signs: standing/wall/(wall_)hanging
|| n.endsWith("_BANNER") // banners: standing/wall
|| n.endsWith("_BED")
|| n.endsWith("SHULKER_BOX") // SHULKER_BOX + <color>_SHULKER_BOX
|| ((n.endsWith("_SKULL") || n.endsWith("_HEAD")) && m != Material.PISTON_HEAD)
|| m == Material.CHEST
@@ -1,146 +1,240 @@
package eu.mhsl.minecraft.pixelpics.render.entity;
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Maps a {@link BlockEntityState} to its bundled CEM model name and ordered texture-path candidates.
* Block-entity geometry ships in the same CEM set as mobs (chest/sign/banner/…); textures live under
* {@code assets/minecraft/textures/entity/}. Mirrors the role {@link EntityModels} plays for mobs.
* Maps a {@link BlockEntityState} to the models its vanilla BlockEntityRenderer draws (from the client
* dump, see {@code tools/VanillaModelDump.java}) and to the renderer's PoseStack. Every pose below is
* 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 {
private BlockEntityModels() {}
/** The CEM ({@code .jem}) model name for a block-entity. */
public static String cemModel(BlockEntityState s) {
return switch (s.kind()) {
case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> switch (chestKind(s)) {
case LEFT -> "chest_left";
case RIGHT -> "chest_right";
case SINGLE -> "chest";
};
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());
};
/** How a part's texture is produced. */
public enum Paint {
/** First loadable texture of {@link Part#textures}. */
PLAIN,
/** Banner flag: {@code banner_base}, the base-colour {@code base} mask, then each pattern (BannerRenderer). */
BANNER_FLAG,
/** Player head: the owner's skin, else {@link Part#textures} (default Steve). */
SKIN
}
/**
* A block-entity model from the vanilla client dump ({@code layer/<layer>#<part>}, see
* {@code tools/VanillaModelDump.java}) plus the vanilla BlockEntityRenderer pose: {@code pose} maps
* Java model space (already scaled to blocks) into the block cell, origin at the cell's min corner —
* exactly the PoseStack the renderer sets up before {@code submitModel}.
* One {@code submitModel}/{@code submitModelPart} call: a dump model ({@code layer/<model>#<layer>}
* in rest pose, or {@code posed/…} posed by its setupAnim), its texture candidates and the top-level
* parts it leaves out (pot sides each carry their own sherd texture).
*/
public record VanillaModel(String layer, Affine pose) {}
/**
* The vanilla model + renderer pose for a block-entity, or null when the type still uses the CEM
* template path. To migrate another type, add its case here (layer name from the dump + the pose its
* vanilla renderer applies); the baker needs no further changes.
*/
public static VanillaModel vanillaModel(BlockEntityState s) {
// ChestRenderer: rotationAround(Y, -facing.toYRot(), 0.5, 0, 0.5); no Y flip (model y is up).
return switch (s.kind()) {
case CHEST, TRAPPED_CHEST, ENDER_CHEST, COPPER_CHEST -> new VanillaModel(
switch (chestKind(s)) {
case LEFT -> "layer/double_chest_left#main";
case RIGHT -> "layer/double_chest_right#main";
case SINGLE -> "layer/chest#main";
},
rotationAroundY(-s.facingDeg(), 0.5, 0.5));
default -> null;
};
}
/** JOML {@code rotationAround}: a Y rotation (deg) about the vertical line through (cx, ·, cz). */
private static Affine rotationAroundY(double deg, double cx, double cz) {
return Affine.translation(cx, 0, cz)
.mul(Affine.rotY(Math.toRadians(deg)))
.mul(Affine.translation(-cx, 0, -cz));
}
private static String headModel(String headType) {
if (headType == null) return "head";
return switch (headType) {
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());
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);
}
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) {
return s.chestKind() == null ? BlockEntityState.ChestKind.SINGLE : s.chestKind();
}
private static void chestTextures(List<String> paths, BlockEntityState s, String base) {
switch (chestKind(s)) {
case LEFT -> paths.add("entity/chest/" + base + "_left");
case RIGHT -> paths.add("entity/chest/" + base + "_right");
case SINGLE -> paths.add("entity/chest/" + base);
}
paths.add("entity/chest/" + base); // fallback to the single texture
private static List<String> chestTextures(BlockEntityState s) {
String base =
switch (s.kind()) {
case TRAPPED_CHEST -> "trapped";
case ENDER_CHEST -> "ender";
case COPPER_CHEST -> s.colorName() == null ? "copper" : "copper_" + s.colorName();
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) {
if (headType == null) {
paths.add("entity/skeleton/skeleton");
return;
}
switch (headType) {
case "wither_skeleton" -> paths.add("entity/skeleton/wither_skeleton");
case "zombie" -> paths.add("entity/zombie/zombie");
case "creeper" -> paths.add("entity/creeper/creeper");
case "piglin" -> paths.add("entity/piglin/piglin");
case "dragon" -> paths.add("entity/enderdragon/dragon");
case "player" -> paths.add("entity/player/wide/steve"); // skin handled separately by the baker
default -> paths.add("entity/skeleton/skeleton");
}
/** JOML {@code rotationAround}: {@code T(c) · rot · T(-c)}. */
private static Affine rotationAround(Affine rot, double cx, double cy, double cz) {
return Affine.translation(cx, cy, cz).mul(rot).mul(Affine.translation(-cx, -cy, -cz));
}
/** Minecraft {@code Direction.getRotation()} (JOML rotationXYZ = Rx·Ry·Rz); null/unknown = UP. */
private static Affine directionRotation(String direction) {
double h = Math.PI / 2;
if (direction == null) return Affine.identity();
return switch (direction) {
case "down" -> Affine.rotX(Math.PI);
case "north" -> Affine.rotX(h).mul(Affine.rotZ(Math.PI));
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
* off-thread. Block-entities (chests, signs, banners, beds, heads, …) use vanilla {@code builtin/entity}
* block models with no geometry; their real shape lives in the bundled CEM models, so they are rendered
* through the same baking/intersection path as mobs. Bukkit-free so the baker needs no world access.
* off-thread. Block-entities drawn by a vanilla BlockEntityRenderer (chests, banners, heads, shulker
* boxes, …) have no block-model geometry; their shape is the renderer's entity model (dumped from the
* 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
* facing/rotation, vanilla convention). Type-specific fields are null/0/empty when unused.
* <p>{@code facingDeg} is the vanilla yaw of the block's facing/rotation (0 = south, 90 = west; a
* 16-step rotation is {@code segment · 22.5}). Type-specific fields are null/0/false/empty when unused.
*/
public record BlockEntityState(
Kind kind,
@@ -17,16 +18,15 @@ public record BlockEntityState(
int by,
int bz,
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)
int baseColorArgb, // banner base tint (white texture); 0 = none
String colorName, // bed/shulker/banner colour ("red", …) or copper-chest stage ("exposed", …); null = default
String wood, // sign/hanging-sign wood ("oak", "spruce", …); null = default
BedPart bedPart, // bed half
int baseColorArgb, // banner base colour; 0 = none
String colorName, // shulker colour ("red", …) or copper-chest stage ("exposed", …); null = default
String headType, // "skeleton","wither_skeleton","zombie","creeper","piglin","dragon","player"
String skinUrl, // player-head owner skin URL; null otherwise
List<BannerPattern> patterns, // banner overlay patterns (may be empty)
List<String> sherds, // decorated-pot sherds: front/back/left/right item keys (may be empty)
BellAttach bellAttach, // bell attachment; null when not a bell
List<String> sherds, // decorated-pot sherds: front/left/right/back item keys (may be empty)
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
) {
@@ -40,13 +40,13 @@ public record BlockEntityState(
HANGING_SIGN,
BANNER,
WALL_BANNER,
BED,
SHULKER_BOX,
HEAD,
WALL_HEAD,
CONDUIT,
DECORATED_POT,
BELL
BELL,
LECTERN
}
public enum ChestKind {
@@ -55,18 +55,6 @@ public record BlockEntityState(
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. */
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.TextureOps;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Bakes a {@link BlockEntityState} into world-space cubes using the bundled CEM models, reusing
* {@link CemGeometry}. Unlike mobs, block-entities are not dropped to the ground by their model bounds;
* each type is placed by an affine that reproduces the vanilla BlockEntityRenderer: the block cell
* centre, a yaw from the block's facing/rotation, and a per-type local offset/scale.
*
* <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.
* Bakes a {@link BlockEntityState} into world-space cubes: the models its vanilla BlockEntityRenderer
* draws (dumped from the client, see {@link BlockEntityModels#vanillaModel}), placed by that renderer's
* own pose — {@code T(cell min corner) · pose · S(1/16) · CEM_TO_JAVA} — so position, rotation and box-UV
* 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).
*/
public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
private static final double SIGN_SCALE = 0.6666667; // vanilla SignRenderer model scale
// Sign-text placement (model px in the board's local frame; calibrated against the test harness).
// The text block is scaled to fill the board's writable area (W×H) so few-line signs are rendered as
// large as possible (legibility on low-res maps), capped so a single line isn't blown up absurdly.
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 HANG_TEXT_W = 13.0, HANG_TEXT_H = 9.0, HANG_TEXT_CAP = 0.42; // board 14x10
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
// 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
// blown up absurdly. Vanilla: 1 font px per text px, lines 90 (hanging 60) px wide, 10 (9) px apart.
private static final double SIGN_TEXT_W = 88, SIGN_TEXT_H = 44, SIGN_TEXT_CAP = 2.0;
private static final double HANG_TEXT_W = 58, HANG_TEXT_H = 40, HANG_TEXT_CAP = 1.87;
private static final double TEXT_THICK =
0.2; // slab behind the text plane (text px) so the ray test never degenerates
private final CemModelLoader models;
private final TextureCache textures;
@@ -64,120 +50,117 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
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
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<>();
if (model != null)
for (Layer layer : layers) {
for (CemGeometry.Baked b : CemGeometry.bakeModel(
model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), placement.mul(b.world())));
}
}
addSignText(s, placement.mul(pre), cubes);
Affine cell = Affine.translation(s.bx(), s.by(), s.bz());
BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s);
if (vm != null) bakeParts(s, vm, cell.mul(vm.pose()), cubes);
addSignText(s, cell, cubes);
return cubes.isEmpty() ? null : RenderedEntity.of(cubes);
}
/**
* Vanilla path: the model dumped from the client ({@link BlockEntityModels#vanillaModel}) placed by its
* renderer's pose, {@code T(cell min corner) · pose · S(1/16) · CEM_TO_JAVA}. Position, rotation and
* box-UV then match vanilla by construction (no per-type calibration offsets). Returns null when the
* type has no vanilla mapping or the dump lacks the layer, so the caller falls back to the CEM template.
* The block-entity's models WITHOUT the block pose, in Java model space (blocks) mapped by
* {@code toWorld}: what the special item renderers and the worn-skull layer submit (the item model's
* transformation / the head frame place it).
*/
private RenderedEntity bakeVanilla(BlockEntityState s, List<Layer> layers) {
public void bakeModel(BlockEntityState s, Affine toWorld, List<EntityCube> out) {
BlockEntityModels.VanillaModel vm = BlockEntityModels.vanillaModel(s);
if (vm == null || layers.isEmpty()) return null;
CemModelLoader.CemModel model = models.vanilla(vm.layer());
if (model == null) return null;
Affine pre = Affine.translation(s.bx(), s.by(), s.bz())
.mul(vm.pose())
.mul(Affine.scale(1 / 16.0))
.mul(CemGeometry.CEM_TO_JAVA);
List<EntityCube> cubes = new ArrayList<>();
for (Layer layer : layers) {
for (CemGeometry.Baked b :
CemGeometry.bakeModel(model, layer.tex, pre, layer.hidden, layer.texW, layer.texH, layer.boxUv)) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), b.world()));
if (vm != null) bakeParts(s, vm, toWorld, out);
}
private void bakeParts(BlockEntityState s, BlockEntityModels.VanillaModel vm, Affine pose, List<EntityCube> out) {
Affine pre = pose.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
for (BlockEntityModels.Part part : vm.parts()) {
CemModelLoader.CemModel model = models.vanilla(part.model());
int[][] tex = model == null ? null : texture(s, part);
if (tex == null) continue;
for (CemGeometry.Baked b : CemGeometry.bakeModel(model, tex, pre, part.hidden())) {
out.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
* {@link BlockEntityState.SignText}. Built in the board's model-px frame and transformed by the same
* {@code placement·pre} as the CEM board, so the text rides exactly on the board.
*
* <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.
* BannerRenderer flag: {@code banner_base}, then the {@code base} mask dyed with the base colour, then
* each pattern mask ({@code entity/banner/<key>}) dyed with its colour, composited into one texture
* (one geometry, no coplanar layers).
*/
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;
boolean hanging = s.kind() == BlockEntityState.Kind.HANGING_SIGN;
boolean sign = hanging || s.kind() == BlockEntityState.Kind.SIGN || s.kind() == BlockEntityState.Kind.WALL_SIGN;
if (!sign) return;
double cy = hanging ? HANGING_TEXT_CY : SIGN_TEXT_CY;
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;
addSide(s.frontText(), Direction.NORTH, tw, th, cap, cy, toWorld, cubes);
addSide(s.backText(), Direction.SOUTH, tw, th, cap, cy, toWorld, cubes);
BlockEntityState.Kind k = s.kind();
boolean hanging = k == BlockEntityState.Kind.HANGING_SIGN;
if (!hanging && k != BlockEntityState.Kind.SIGN && k != BlockEntityState.Kind.WALL_SIGN) return;
addSide(s, s.frontText(), true, hanging, cell, cubes);
addSide(s, s.backText(), false, hanging, cell, cubes);
}
private void addSide(
BlockEntityState s,
BlockEntityState.SignText t,
Direction faceDir,
double tw,
double th,
double cap,
double cy,
Affine toWorld,
boolean front,
boolean hanging,
Affine cell,
List<EntityCube> cubes) {
if (t == null) return;
int[][] bmp = SignTextRasterizer.rasterize(
trimBlankLines(t.lines()), font, t.fillArgb(), t.outlineArgb(), t.glowing());
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 blockW = bmp[0].length * fpm;
double blockH = bmp.length * fpm;
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;
}
double w = bmp[0].length * fpm, h = bmp.length * fpm;
double[] px = new double[1];
Affine frame = BlockEntityModels.signTextFrame(s, front, px);
Face[] faces = new Face[6];
faces[faceDir.ordinal()] = new Face(bmp, 0, 0, 1, 1, 0, -1);
double[] from = {-blockW / 2, cy - blockH / 2, z0};
double[] to = {blockW / 2, cy + blockH / 2, z1};
cubes.add(new EntityCube(from, to, faces, toWorld));
faces[Direction.SOUTH.ordinal()] = new Face(bmp, 0, 0, 1, 1, 0, -1);
double[] from = {-w / 2, -h / 2, -TEXT_THICK};
double[] to = {w / 2, h / 2, 0};
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. */
@@ -187,158 +170,4 @@ public final class BlockEntityBaker implements EntityBaker<BlockEntityState> {
while (hi > lo && lines.get(hi - 1).isBlank()) 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) {
Affine hj = h.mul(CemGeometry.JAVA_LOCAL);
if (head.blockEntity() != null && head.blockEntity().kind() == BlockEntityState.Kind.HEAD) {
items.bakeBlockEntity(
head.blockEntity(),
hj.mul(Affine.scale(1.1875, -1.1875, -1.1875)).mul(Affine.translation(-0.5, 0, -0.5)),
out);
// CustomHeadLayer: head frame · S(1.1875) · SkullBlockRenderer.submitSkull (bare skull model).
items.bakeBlockEntityModel(head.blockEntity(), hj.mul(Affine.scale(1.1875)), out);
} else {
// Snow golems wear their pumpkin a bit lower (SnowGolemHeadLayer) than CustomHeadLayer items.
double dy = s.typeKey().equals("snow_golem") ? -0.34375 : -0.25;
@@ -10,8 +10,8 @@ import java.util.Map;
import java.util.Set;
/**
* Shared, stateless CEM geometry baking used by both {@link CemBaker} (mobs) and the block-entity
* baker. It turns a {@link CemModelLoader.CemModel} into a list of {@link Baked} boxes (local cube +
* Shared, stateless CEM geometry baking used by {@link CemBaker} (mobs), the block-entity baker and
* 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
* 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. */
static List<Baked> bakeModel(CemModelLoader.CemModel model, int[][] tex, Affine pre, Set<String> hidden) {
return bakeModel(model, tex, pre, hidden, 0, 0, false);
}
/**
* 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);
return bake(model, tex, pre, hidden, Set.of(), Map.of(), null);
}
/**
@@ -75,7 +57,7 @@ final class CemGeometry {
Set<String> show,
Map<String, CemModelLoader.PartPose> overrides,
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(
@@ -84,14 +66,9 @@ final class CemGeometry {
Affine pre,
Set<String> hidden,
Set<String> show,
int texW,
int texH,
boolean ignoreFaceUv,
Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) {
int nw = texW > 0 ? texW : model.texW();
int nh = texH > 0 ? texH : model.texH();
Ctx ctx = new Ctx(hidden, show, nw, nh, tex, ignoreFaceUv, new ArrayList<>(), overrides, frames);
Ctx ctx = new Ctx(hidden, show, model.texW(), model.texH(), tex, new ArrayList<>(), overrides, frames);
for (CemModelLoader.CemPart p : model.parts()) {
if (model.absolute()) {
bakeAbsolute(p, pre, ctx);
@@ -112,7 +89,6 @@ final class CemGeometry {
int texW,
int texH,
int[][] tex,
boolean ignoreFaceUv,
List<Baked> out,
Map<String, CemModelLoader.PartPose> overrides,
Map<String, Affine> frames) {}
@@ -173,8 +149,7 @@ final class CemGeometry {
double[] inf = b.inflate();
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[][] faceUv = ctx.ignoreFaceUv ? null : b.faceUv();
ModelCube mc = new ModelCube(org, b.size(), inf[0], b.uv(), b.mirror(), faceUv);
ModelCube mc = new ModelCube(org, b.size(), inf[0], b.uv(), b.mirror(), b.faceUv());
// Vanilla texScale stretches the UV normalisation (texture treated as texSize*scale).
int tw = ctx.texW, th = ctx.texH;
if (b.texScale() != null) {
@@ -71,7 +71,7 @@ public final class CemModelLoader {
private final Map<String, CemModel> models = new HashMap<>();
// 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, 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) {
// Placed as in the world: cell (0,0,0) with the renderer's block pose.
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;
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 --------------------------------------------------------
private Info info(String id, String context) {
@@ -346,8 +352,12 @@ public final class ItemGeometry {
}
}
default -> {
// Block-entity specials (chest, head, banner, shulker box, bed, conduit, pot, signs, …).
if (item.blockEntity() != null) bakeBlockEntity(item.blockEntity(), m, out);
// Block-entity specials (chest, head, banner, shulker box, conduit, pot, …): like the vanilla
// 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.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.Kind;
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.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.Conduit;
import org.bukkit.block.DecoratedPot;
import org.bukkit.block.Sign;
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.Directional;
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.Lectern;
import org.bukkit.block.sign.Side;
import org.bukkit.block.sign.SignSide;
import org.bukkit.profile.PlayerProfile;
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
* loaded chunk's tile-entities rather than scanning every block, then keeps only those inside the
* camera's bounding box.
@@ -120,19 +118,12 @@ public final class BlockEntitySnapshotBuilder {
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 ---
if (n.endsWith("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)
.build();
}
@@ -165,7 +156,6 @@ public final class BlockEntitySnapshotBuilder {
// --- signs ---
if (n.endsWith("_SIGN")) {
String wood = signWood(n);
Kind kind;
float yaw;
if (n.endsWith("_WALL_SIGN")) {
@@ -178,7 +168,7 @@ public final class BlockEntitySnapshotBuilder {
kind = Kind.SIGN;
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) {
b.frontText(signText(sign.getSide(Side.FRONT)));
b.backText(signText(sign.getSide(Side.BACK)));
@@ -200,7 +190,16 @@ public final class BlockEntitySnapshotBuilder {
// --- 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 ---
@@ -210,19 +209,9 @@ public final class BlockEntitySnapshotBuilder {
return b.build();
}
// --- bell ---
// --- bell (frame = block model; the renderer draws the bell body) ---
if (mat == Material.BELL) {
BellAttach attach = BellAttach.FLOOR;
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 base(Kind.BELL, bx, by, bz, 0).build();
}
return null;
@@ -295,17 +284,6 @@ public final class BlockEntitySnapshotBuilder {
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) {
String s = name.replace("_WALL_", "_");
return switch (s) {
@@ -320,7 +298,7 @@ public final class BlockEntitySnapshotBuilder {
}
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);
for (DecoratedPot.Side side : new DecoratedPot.Side[] {
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();
}
// --- 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) {
return new Builder(kind, bx, by, bz, yaw);
@@ -359,16 +337,15 @@ public final class BlockEntitySnapshotBuilder {
private final Kind kind;
private final int bx, by, bz;
private final float yaw;
private String direction;
private boolean active;
private ChestKind chestKind;
private int baseColorArgb;
private String colorName;
private String wood;
private BedPart bedPart;
private String headType;
private String skinUrl;
private List<BannerPattern> patterns = List.of();
private List<String> sherds = List.of();
private BellAttach bellAttach;
private BlockEntityState.SignText frontText;
private BlockEntityState.SignText backText;
@@ -395,13 +372,13 @@ public final class BlockEntitySnapshotBuilder {
return this;
}
Builder wood(String v) {
this.wood = v;
Builder direction(String v) {
this.direction = v;
return this;
}
Builder bedPart(BedPart v) {
this.bedPart = v;
Builder active(boolean v) {
this.active = v;
return this;
}
@@ -425,11 +402,6 @@ public final class BlockEntitySnapshotBuilder {
return this;
}
Builder bellAttach(BellAttach v) {
this.bellAttach = v;
return this;
}
Builder frontText(BlockEntityState.SignText v) {
this.frontText = v;
return this;
@@ -447,16 +419,15 @@ public final class BlockEntitySnapshotBuilder {
by,
bz,
yaw,
direction,
active,
chestKind,
baseColorArgb,
colorName,
wood,
bedPart,
headType,
skinUrl,
patterns,
sherds,
bellAttach,
frontText,
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 DecorationBaker decoBaker;
static BlockEntityState be(Kind kind, int bx, int by, int bz, float yaw) {
return new BlockEntityState(kind, bx, by, bz, yaw, ChestKind.SINGLE, 0, null, "oak",
null, null, null, List.of(), List.of(), null, null, null);
/** A block-entity state at (bx, 0, bz); unused fields default. */
static BlockEntityState st(Kind kind, int bx, int bz, float yaw, String direction, boolean active, String color,
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) {
return new BlockEntityState(kind, 0, 0, 0, yaw, null, 0, null, "oak",
null, null, null, List.of(), List.of(), null, front, back);
return new BlockEntityState(kind, 0, 0, 0, yaw, null, false, null, 0, null, null, null,
List.of(), List.of(), front, back);
}
static SignText txt(org.bukkit.DyeColor dye, boolean glow, String... lines) {
@@ -57,7 +63,7 @@ public class BlockEntityTestRender {
BlockModelRegistry registry = new BlockModelRegistry(reader, textures);
BiomeTintProvider tint = new BiomeTintProvider(textures);
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);
SkinCache skins = new SkinCache();
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, -1, 0, 0, ChestKind.RIGHT, "oxidized")));
frontCam.put("copper_oxidized_double", 0f);
scenes.put("sign", List.of(be(Kind.SIGN, 0, 0, 0, 0)));
scenes.put("wall_sign", List.of(be(Kind.WALL_SIGN, 0, 0, 0, 0)));
scenes.put("hanging_sign", List.of(be(Kind.HANGING_SIGN, 0, 0, 0, 0)));
// --- sign text (calibration) --- facingDeg=180 → front (model −Z) faces the camera (north).
// --- sign text (boards are block models, not drawn here) --- facingDeg 180 → front faces north.
org.bukkit.DyeColor B = org.bukkit.DyeColor.BLACK;
scenes.put("sign_text", List.of(sign(Kind.SIGN, 180,
txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)));
scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180,
txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)));
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_text_red", List.of(sign(Kind.SIGN, 180,
txt(org.bukkit.DyeColor.RED, false, "RED INK", "colored", "sign text", ""), null)));
scenes.put("sign_text_glow", List.of(sign(Kind.SIGN, 180,
txt(org.bukkit.DyeColor.LIME, true, "GLOWING", "lime glow", "outline!", ""), null)));
scenes.put("sign_text_front", List.of(sign(Kind.SIGN, 180,
txt(B, false, "FRONT", "side", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACK", "side", "", ""))));
// View the BACK side head-on (facingDeg=0 turns the back face toward the camera).
scenes.put("sign_view_back", List.of(sign(Kind.SIGN, 0,
txt(B, false, "FRONTXY", "f2", "", ""), txt(org.bukkit.DyeColor.BLUE, false, "BACKXY", "b2", "", ""))));
String[] D = {"S", "W", "N", "E"};
for (int i = 0; i < 4; i++) {
float yaw = i * 90;
scenes.put("sign_text_" + D[i], List.of(sign(Kind.SIGN, yaw, txt(B, false, "Hello World", "zweite Zeile", "Gruesse: AÖÜ", "ßäöü 12345"), null)));
frontCam.put("sign_text_" + D[i], yaw);
}
scenes.put("sign_text_diag", List.of(sign(Kind.SIGN, 22.5f * 3, txt(B, false, "rot 3", "67.5 deg", "", ""), null)));
frontCam.put("sign_text_diag", 22.5f * 3);
scenes.put("wall_sign_text", List.of(sign(Kind.WALL_SIGN, 180, txt(B, false, "Wall Sign", "Line 2", "Line 3", "Line 4"), null)));
frontCam.put("wall_sign_text", 180f);
scenes.put("wall_sign_text_E", List.of(sign(Kind.WALL_SIGN, 270, txt(B, false, "Wall East", "Line 2", "", ""), null)));
frontCam.put("wall_sign_text_E", 270f);
scenes.put("hanging_sign_text", List.of(sign(Kind.HANGING_SIGN, 180, txt(B, false, "Hanging", "sign text", "row 3", "row 4"), null)));
frontCam.put("hanging_sign_text", 180f);
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_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("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)));
scenes.put("banner_patterned", List.of(new BlockEntityState(Kind.BANNER, 0, 0, 0, 0, null, 0xFFFFFFFF, null, null, null, null, null,
List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020)), List.of(), null, null, null)));
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)));
scenes.put("bed", List.of(
new BlockEntityState(Kind.BED, 0, 0, 0, 0, null, 0, "red", null, BedPart.FOOT, null, null, List.of(), List.of(), null, null, null),
new BlockEntityState(Kind.BED, 0, 0, 1, 0, null, 0, "red", null, BedPart.HEAD, null, null, List.of(), List.of(), null, null, null)));
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("decorated_pot", List.of(be(Kind.DECORATED_POT, 0, 0, 0, 0)));
scenes.put("decorated_pot_sherds", List.of(new BlockEntityState(Kind.DECORATED_POT, 0, 0, 0, 0, null, 0, null, null, null, null, null,
List.of(), List.of("angler_pottery_sherd", "heart_pottery_sherd", "skull_pottery_sherd", "brick"), null, null, null)));
scenes.put("bell", List.of(be(Kind.BELL, 0, 0, 0, 0)));
scenes.put("head_skeleton", List.of(head("skeleton")));
scenes.put("head_zombie", List.of(head("zombie")));
scenes.put("head_creeper", List.of(head("creeper")));
scenes.put("head_dragon", List.of(head("dragon")));
scenes.put("head_piglin", List.of(head("piglin")));
scenes.put("wither_skull", List.of(headKind(Kind.HEAD, "wither_skeleton")));
scenes.put("wall_head", List.of(headKind(Kind.WALL_HEAD, "skeleton")));
frontCam.put("sign_long", 180f);
// --- banners: standing (rotation 0/4/6), wall (4 facings), patterned ---
List<BannerPattern> pats = List.of(new BannerPattern("stripe_bottom", 0xFFCC2020), new BannerPattern("cross", 0xFF2040CC), new BannerPattern("border", 0xFF20A020));
for (float yaw : new float[] {0, 90, 135}) {
String k = "banner_" + (int) yaw;
scenes.put(k, List.of(st(Kind.BANNER, 0, 0, yaw, null, false, null, 0xFFCC2020, null, List.of(), List.of())));
frontCam.put(k, yaw);
}
scenes.put("banner_patterned", List.of(st(Kind.BANNER, 0, 0, 0, null, false, null, 0xFFFFFFFF, null, pats, List.of())));
frontCam.put("banner_patterned", 0f);
scenes.put("banner_side", scenes.get("banner_patterned"));
frontCam.put("banner_side", 90f);
for (int i = 0; i < 4; i++) {
scenes.put("wall_banner_" + D[i], List.of(st(Kind.WALL_BANNER, 0, 0, i * 90, null, false, null, 0xFF2040CC, null, pats, List.of())));
frontCam.put("wall_banner_" + D[i], i * 90f);
}
// --- shulker boxes: 6 facings ---
for (String d : new String[] {"up", "down", "north", "south", "east", "west"}) {
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())));
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.
LinkedHashMap<String, List<DecorationState>> decoScenes = new LinkedHashMap<>();
@@ -206,13 +249,12 @@ public class BlockEntityTestRender {
static final Map<String, Float> frontCam = new HashMap<>();
static BlockEntityState chest(Kind kind, int bx, int bz, float yaw, ChestKind ck, String variant) {
return new BlockEntityState(kind, bx, 0, bz, yaw, ck, 0, variant, null, null, null, null,
List.of(), List.of(), null, null, null);
return new BlockEntityState(kind, bx, 0, bz, yaw, null, false, ck, 0, variant, null, null,
List.of(), List.of(), null, null);
}
static BlockEntityState head(String type) { return headKind(Kind.HEAD, type); }
static BlockEntityState headKind(Kind kind, String type) {
return new BlockEntityState(kind, 0, 0, 0, 0, null, 0, null, null, null, type, null, List.of(), List.of(), null, null, null);
static BlockEntityState head(Kind kind, String type, float yaw) {
return st(kind, 0, 0, yaw, null, false, null, 0, type, List.of(), List.of());
}
static BufferedImage renderScene(DefaultScreenRenderer renderer, WorldSnapshot world, SkyContext sky,
+43
View File
@@ -156,6 +156,29 @@ public class VanillaModelDump {
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();
root.addProperty("version", SharedConstants.getCurrentVersion().name());
JsonObject models = new JsonObject();
@@ -171,6 +194,26 @@ public class VanillaModelDump {
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
* 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_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));
// 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));
}
@@ -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,
# 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,
# heads, banners, armor_layer_*). Mob models come from the vanilla client dump — regenerate that with
# tools/update-vanilla-models.sh <version> on every Minecraft update.
# NOTE: this set is now only the FALLBACK (rest-pose geometry) for entity types missing from the vanilla
# client dump (+ the elytra fallback). Mob AND block-entity models come from that dump — regenerate it
# with tools/update-vanilla-models.sh <version> on every Minecraft update.
#
# Usage: tools/update-cem-models.sh # update if newer
# tools/update-cem-models.sh --force # always overwrite