render emissive eye/glow layers, per-model vanilla render types (cutout vs translucent, unshaded), sheep undercoat and vanilla wool colour, angry wolves, tipped arrow and invulnerable wither textures, small armor stand armor textures, elytra rest pose, vanilla mob arm poses (piglins, illagers), layer test scenes

This commit is contained in:
MineTec committed 2026-11-04 18:59:14 +01:00
1 parent 45d033bd89
commit 2c2e9e2e32
12 files changed
+896 -99

No files matched your search

@@ -14,6 +14,7 @@ public final class EntityCube {
public final Affine toWorld;
public final Affine toLocal; // inverse
public final boolean additive; // emissive, added to what lies behind (energy swirl) instead of blended
public final boolean unshaded; // no directional face shading (vanilla NO_CARDINAL_LIGHTING: eyes, breeze wind)
public final double[] aabbMin = {Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE};
public final double[] aabbMax = {-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};
@@ -22,7 +23,12 @@ public final class EntityCube {
}
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive) {
this(from, to, faces, toWorld, additive, false);
}
public EntityCube(double[] from, double[] to, Face[] faces, Affine toWorld, boolean additive, boolean unshaded) {
this.additive = additive;
this.unshaded = unshaded;
this.from = from;
this.to = to;
this.faces = faces;
@@ -120,7 +120,7 @@ public final class EntityIntersector {
Vector normal = new Vector(n[0], n[1], n[2]).normalize();
// A far/back face is seen from inside: shade it as facing the viewer.
if (normal.getX() * dx + normal.getY() * dy + normal.getZ() * dz > 0) normal.multiply(-1);
return new FaceHit(t, world, normal, h.color, -1, cube.additive);
return new FaceHit(t, world, normal, h.color, -1, cube.additive, cube.unshaded);
}
private static Direction direction(int axis, boolean neg) {
@@ -145,24 +145,29 @@ public final class CemBaker implements EntityBaker<EntityState> {
// Body: baked even when invisible — not drawn, but it yields the part frames (and legacy ground snap).
Pose pose = new Pose(pre, vanilla ? withBindings(sel.parts(), s, info) : Map.of());
Map<String, Affine> frames = new java.util.HashMap<>();
List<CemGeometry.Baked> body =
(model != null && tex != null) ? CemGeometry.bakePosed(model, tex, pre, pose.parts, frames) : List.of();
List<CemGeometry.Baked> body = (model != null && tex != null)
? CemGeometry.bakePosed(model, renderTexture(model, tex), pre, pose.parts, frames)
: List.of();
List<CemGeometry.Baked> baked = new ArrayList<>();
List<CemGeometry.Baked> glow = new ArrayList<>(); // additive energy-swirl layers
Out out = new Out();
if (!invisible) {
baked.addAll(body);
if (vanilla) addVanillaLayers(s, ch, info, pose, tex, baked, glow);
(vanilla && unshaded(model) ? out.unshaded : out.lit).addAll(body);
if (vanilla) addVanillaLayers(s, ch, info, pose, tex, out);
}
// Humanoid armor / elytra (renderers with a HumanoidArmorLayer): drawn even when invisible (vanilla
// "floating armor").
if (vanilla && s.equipment() != null && info != null && info.humanoidArmor()) {
addArmorLayers(s, ch, pose, baked);
addArmorLayers(s, ch, pose, out);
}
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, baked);
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, out);
if (vanilla) addEmissiveLayers(s, ch, info, pose, out);
List<CemGeometry.Baked> baked = out.lit;
List<CemGeometry.Baked> glow = out.glow;
List<EntityCube> itemCubes = new ArrayList<>();
if (vanilla && items != null) addItems(s, frames, isIllager(info), itemCubes);
if (baked.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) return invisible ? null : fallbackBox(s, tex);
if (baked.isEmpty() && out.unshaded.isEmpty() && glow.isEmpty() && itemCubes.isEmpty()) {
return invisible ? null : fallbackBox(s, tex);
}
// Legacy CEM models: snap the lowest point to the ground (their rest geometry has no reliable origin).
if (place == null) {
@@ -176,6 +181,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
List<EntityCube> cubes = new ArrayList<>(baked.size());
for (CemGeometry.Baked b : baked) cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world())));
for (CemGeometry.Baked b : out.unshaded) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), false, true));
}
for (CemGeometry.Baked b : glow) {
cubes.add(new EntityCube(b.from(), b.to(), b.faces(), place.mul(b.world()), true));
}
@@ -192,6 +200,80 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
}
/**
* Baked boxes of one entity by how they are drawn: shaded (normal), unshaded (render types without cardinal
* lighting: eyes, breeze wind) and additive (energy swirl).
*/
private static final class Out {
final List<CemGeometry.Baked> lit = new ArrayList<>();
final List<CemGeometry.Baked> unshaded = new ArrayList<>();
final List<CemGeometry.Baked> glow = new ArrayList<>();
}
/**
* Bakes a vanilla model the way its render type draws it: texels processed by {@link #renderTexture}, routed to
* the unshaded boxes when the pipeline has no cardinal lighting.
*/
private void add(Out out, Pose pose, CemModelLoader.CemModel m, int[][] tex) {
(unshaded(m) ? out.unshaded : out.lit).addAll(pose.bake(m, renderTexture(m, tex)));
}
// Overlay layers that cover surfaces of the body (EyesLayer draws the parent model itself; the warden / copper
// golem / creaking emissive layers and the sheep undercoat draw the body mesh or subsets of it, no inflation).
// Vanilla draws them afterwards and the depth test passes on equal depth, i.e. on top of the coplanar body; here
// their boxes grow by this much (model px) so they lie just in front of it — well above the tracer's layer
// epsilon, far below a visible size (on coplanar ties the earlier-baked body would win).
private static final double OVERLAY_INFLATE = 1 / 256.0;
/** {@code baked} boxes grown by {@link #OVERLAY_INFLATE}, added to {@code target}. */
private static void addOverlay(List<CemGeometry.Baked> target, List<CemGeometry.Baked> baked) {
for (CemGeometry.Baked b : baked) {
double[] from = b.from().clone(), to = b.to().clone();
for (int i = 0; i < 3; i++) {
from[i] -= OVERLAY_INFLATE;
to[i] += OVERLAY_INFLATE;
}
target.add(new CemGeometry.Baked(from, to, b.faces(), b.world()));
}
}
private boolean unshaded(CemModelLoader.CemModel m) {
CemModelLoader.RenderType rt = m == null ? null : models.renderType(m.renderType());
return rt != null && !rt.shaded();
}
/**
* A texture as the model's vanilla render type sees it: texels below the shader's ALPHA_CUTOUT (0.1 → alpha
* &lt; 26) are discarded; cutout and solid types write the rest opaque (partial alpha in e.g. llama or
* tropical-fish pattern textures is NOT blended in vanilla), solid types draw even fully transparent texels;
* only blending types (entity_translucent: slime skin, vex, allay, player skins, …) keep the alpha for the
* tracer to blend. Unknown render type (legacy models, old dumps) or additive: unchanged. Cached; returns
* {@code tex} itself when nothing changes.
*/
private int[][] renderTexture(CemModelLoader.CemModel m, int[][] tex) {
return renderTexture(m == null ? null : models.renderType(m.renderType()), tex);
}
private int[][] renderTexture(CemModelLoader.RenderType rt, int[][] tex) {
if (rt == null || tex == null || "additive".equals(rt.blend())) return tex;
boolean blend = rt.blend() != null;
int min = rt.cutout() < 0 ? 0 : (int) Math.ceil(rt.cutout() * 255 - 1e-6);
return derived.get(DerivedTextureCache.key("rendertype", tex, min, blend), () -> {
int[][] out = null;
for (int y = 0; y < tex.length; y++) {
for (int x = 0; x < tex[y].length; x++) {
int p = tex[y][x];
int a = p >>> 24;
int na = a < min ? 0 : blend ? a : 255;
if (na == a) continue;
if (out == null) out = TextureOps.deepCopy(tex);
out[y][x] = (na << 24) | (p & 0xFFFFFF);
}
}
return out == null ? tex : out;
});
}
/** A chosen pose variant: its name, part overrides and (when it changes it) the renderer transform. */
private record SelectedPose(
String name, Map<String, CemModelLoader.PartPose> parts, CemModelLoader.PoseTransform transform) {
@@ -219,13 +301,11 @@ public final class CemBaker implements EntityBaker<EntityState> {
: ch.name.substring(ch.base.length() + 1);
CemModelLoader.PoseSet set = info.poseSet(sub);
if (set.poses().isEmpty()) return SelectedPose.NONE;
// Held-item arm poses (players' ITEM pose) only exist where vanilla has them; mob arm poses (illagers'
// crossbow hold, piglins' admiring/attacking, …) come as "arms_<pose>" state flags from the snapshot.
List<String> flags = new ArrayList<>();
if (isIllager(info)) {
if (isCrossbow(s.rightHand()) || isCrossbow(s.leftHand())) flags.add("arms_crossbow_hold");
} else {
if (s.rightHand() != null) flags.add("item_r");
if (s.leftHand() != null) flags.add("item_l");
}
if (s.rightHand() != null) flags.add("item_r");
if (s.leftHand() != null) flags.add("item_l");
if (s.passenger()) flags.add("passenger");
if (s.arms()) flags.add("arms");
if (s.chest()) flags.add("chest");
@@ -324,10 +404,6 @@ public final class CemBaker implements EntityBaker<EntityState> {
return out == null ? parts : out;
}
private static boolean isCrossbow(ItemSpec i) {
return i != null && i.id().equals("crossbow");
}
/**
* Held items (vanilla ItemInHandLayer: hand frame · rotX(-90) · rotY(180) · T(±1/16, 0.125, -0.625),
* third-person display context), crossed-arms items (villagers/witches: root · T(0, 0.4, -0.4) ·
@@ -416,7 +492,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
* Parrots on a player's shoulders (vanilla ParrotOnShoulderLayer): the parrot model in its ON_SHOULDER pose,
* in the player's model space at T(±0.4, -1.5 [crouching -1.3], 0), textured by its variant.
*/
private void addShoulderParrots(EntityState s, Choice ch, Affine t0, List<CemGeometry.Baked> baked) {
private void addShoulderParrots(EntityState s, Choice ch, Affine t0, Out out) {
if (t0 == null || (s.shoulderLeft() == null && s.shoulderRight() == null)) return;
if (ch == null || !ch.base.startsWith("player")) return;
CemModelLoader.CemModel pm = models.vanilla("parrot");
@@ -437,7 +513,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
Affine pre = t0.mul(Affine.translation(side == 0 ? 0.4 : -0.4, y, 0))
.mul(Affine.scale(1 / 16.0))
.mul(CemGeometry.CEM_TO_JAVA);
baked.addAll(CemGeometry.bakePosed(pm, tex, pre, parts, null));
out.lit.addAll(CemGeometry.bakePosed(pm, renderTexture(pm, tex), pre, parts, null));
}
}
@@ -536,13 +612,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
// --- vanilla render layers -----------------------------------------------------------------------
private void addVanillaLayers(
EntityState s,
Choice ch,
CemModelLoader.EntityInfo info,
Pose pose,
int[][] bodyTex,
List<CemGeometry.Baked> baked,
List<CemGeometry.Baked> glow) {
EntityState s, Choice ch, CemModelLoader.EntityInfo info, Pose pose, int[][] bodyTex, Out out) {
String base = ch.base;
boolean baby = ch.babyModel;
for (String n : layerModels(base)) {
@@ -553,29 +623,39 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (lm == null) continue;
switch (layerClass) {
case "SheepWoolLayer", "SheepWoolUndercoatLayer" -> {
if ("sheared".equals(s.variant()) || lm.texture() == null) continue;
// Wool: unless sheared. Undercoat (SheepWoolUndercoatLayer): only adults (no baby model) with
// non-white or jeb_ wool ("undercoat" flag from the snapshot) — sheared or not.
boolean undercoat = layerClass.equals("SheepWoolUndercoatLayer");
if (undercoat ? !s.hasPose("undercoat") : "sheared".equals(s.variant())) continue;
if (lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t == null) continue;
baked.addAll(pose.bake(lm, tinted(t, s.tint())));
if (undercoat || baby) {
// Undercoat and baby wool have the body's mesh (no inflation; coloredCutoutModelCopyLayerRender
// draws them on top): an overlay on its surface.
addOverlay(out.lit, pose.bake(lm, renderTexture(lm, tinted(t, s.tint()))));
} else {
add(out, pose, lm, tinted(t, s.tint()));
}
}
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, baked);
case "TropicalFishPatternLayer" -> addTropicalPattern(s, n, lm, pose, out);
case "CatCollarLayer" -> {
// Own collar model (inflated by 0.01), collar texture tinted with the collar's dye colour.
if (s.collar() == 0 || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t == null) continue;
baked.addAll(pose.bake(lm, tinted(t, s.collar())));
add(out, pose, lm, tinted(t, s.collar()));
}
case "CreeperPowerLayer", "WitherArmorLayer" -> {
// EnergySwirlLayer: charged creeper / wither below half health; drawn additively.
if (!s.powered() || lm.texture() == null) continue;
int[][] t = energySwirl(lm.texture(), layerClass.equals("WitherArmorLayer"));
if (t != null) glow.addAll(pose.bake(lm, t));
if (t != null) out.glow.addAll(pose.bake(lm, t));
}
case "SlimeOuterLayer" -> {
// Translucent outer cube on the body texture (alpha < 255 there; blended by the tracer).
int[][] t = lm.texture() != null ? texture(lm.texture()) : bodyTex;
if (t != null) baked.addAll(pose.bake(lm, t));
if (t != null) add(out, pose, lm, t);
}
case "LlamaDecorLayer", "WolfArmorLayer" -> {
// Body equipment drawn by a dedicated layer: texture folder <base>_body (trader llama: llama_body).
@@ -585,19 +665,19 @@ public final class CemBaker implements EntityBaker<EntityState> {
t = equipmentTexture(
base.substring(base.indexOf('_') + 1) + "_body", s.bodyEquip(), baby, s.bodyDye());
}
if (t != null) baked.addAll(pose.bake(lm, t));
if (t != null) add(out, pose, lm, t);
}
case "RopesLayer" -> {
if (s.bodyEquip() == null || lm.texture() == null) continue; // ropes only with a harness
int[][] t = texture(lm.texture());
if (t != null) baked.addAll(pose.bake(lm, t));
if (t != null) add(out, pose, lm, t);
}
default -> {
// Layers vanilla draws for the default state (drowned/stray/bogged outer, breeze wind, …);
// state-dependent ones (charged, tamed, equipped, …) are handled above or not drawn.
if (info == null || !info.layerDrawnByDefault(layerClass) || lm.texture() == null) continue;
int[][] t = texture(lm.texture());
if (t != null) baked.addAll(pose.bake(lm, t));
if (t != null) add(out, pose, lm, t);
}
}
}
@@ -615,20 +695,19 @@ public final class CemBaker implements EntityBaker<EntityState> {
&& !s.bodyEquip().equals("trader_llama")) {
t = equipmentTexture(layerType, s.bodyEquip(), baby, s.bodyDye());
}
if (t != null) baked.addAll(pose.bake(em, t));
if (t != null) add(out, pose, em, t);
}
}
/** Tropical fish pattern overlay: pattern texture (tinted with the pattern colour) on the pattern model. */
private void addTropicalPattern(
EntityState s, String name, CemModelLoader.CemModel lm, Pose pose, List<CemGeometry.Baked> baked) {
private void addTropicalPattern(EntityState s, String name, CemModelLoader.CemModel lm, Pose pose, Out out) {
int idx = EntityModels.tropicalPattern(s.variant());
if (idx < 0) return;
boolean small = idx < 6; // patterns 0-5 use shape A (small model), 6-11 shape B (large model)
if (name.toLowerCase(Locale.ROOT).contains("small") != small) return;
int[][] t = texture("entity/fish/tropical_" + (small ? "a" : "b") + "_pattern_" + (idx % 6 + 1));
if (t == null) return;
baked.addAll(pose.bake(lm, tinted(t, s.tint2())));
add(out, pose, lm, tinted(t, s.tint2()));
}
/** entity/equipment/<layerType>/<asset> (+ a "_carpet"-stripped name; leather dye + overlay); cached. */
@@ -721,6 +800,51 @@ public final class CemBaker implements EntityBaker<EntityState> {
});
}
// --- emissive layers -----------------------------------------------------------------------------
/**
* Emissive layers of the dump ({@link CemModelLoader.EmissiveLayer}: spider / enderman / phantom / breeze /
* copper-golem / creaking eyes, warden bioluminescence): the layer's texture (per variant, e.g. copper-golem
* weathering) on its model, processed by its render type (eyes: blended and unshaded; translucent emissive:
* blended, shaded), times the static layer alpha. Skipped: animated layers (warden pulsating spots), layers
* whose state flag is off (creaking eyes glow only while active), and on invisible entities those vanilla
* hides (EyesLayer draws even then).
*/
private void addEmissiveLayers(EntityState s, Choice ch, CemModelLoader.EntityInfo info, Pose pose, Out out) {
if (info == null || ch == null) return;
for (CemModelLoader.EmissiveLayer e : info.emissive()) {
if (e.animated() || e.alpha() <= 1e-5) continue;
if (e.when() != null && !s.hasPose(e.when())) continue;
if (s.invisible() && !e.invisible()) continue;
// A layer drawing the parent model follows the chosen body model.
CemModelLoader.CemModel m = e.model().equals(ch.base) ? ch.model : models.vanilla(e.model());
if (m == null) continue;
String variantTex =
s.variant() == null ? null : e.textures().get(s.variant().toLowerCase(Locale.ROOT));
int[][] t = texture(variantTex != null ? variantTex : e.texture());
if (t == null) continue;
CemModelLoader.RenderType rt = models.renderType(e.renderType());
t = renderTexture(rt, t);
if (e.alpha() < 1) t = withAlpha(t, e.alpha());
// Overlay on the coplanar body surface (see OVERLAY_INFLATE).
addOverlay(rt != null && !rt.shaded() ? out.unshaded : out.lit, pose.bake(m, t));
}
}
/** {@code src} with every texel's alpha multiplied by {@code alpha} (vertex alpha of a layer); cached. */
private int[][] withAlpha(int[][] src, double alpha) {
return derived.get(DerivedTextureCache.key("alpha", src, alpha), () -> {
int[][] out = TextureOps.deepCopy(src);
for (int[] row : out) {
for (int x = 0; x < row.length; x++) {
int a = (int) Math.round((row[x] >>> 24) * alpha);
row[x] = (a << 24) | (row[x] & 0xFFFFFF);
}
}
return out;
});
}
// --- texture resolution --------------------------------------------------------------------------
private int[][] texture(String path) {
@@ -728,7 +852,9 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
/**
* Player skin, else the data-driven variant's asset (baby / tame aware), else the variant-aware candidates
* Player skin, else a render-state texture of the dump ({@code stateTextures}: tipped arrow, invulnerable
* wither) whose flag the entity has, else the data-driven variant's asset (baby / tame / angry aware), else the
* variant-aware candidates
* ({@link EntityModels}), the vanilla renderer's default texture as a generic fallback, and — for a
* dedicated baby model — the {@code _baby} counterparts first.
*/
@@ -740,15 +866,22 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (def != null) return def;
}
boolean babyModel = ch != null && ch.babyModel;
CemModelLoader.EntityInfo info = models.entityInfo(s.typeKey());
if (info == null && ch != null) info = models.entityInfo(ch.base);
if (info != null && s.pose() != null) {
for (Map.Entry<String, String> st : info.stateTextures().entrySet()) {
if (!s.hasPose(st.getKey())) continue;
int[][] t = texture(st.getValue());
if (t != null) return t;
}
}
// Data-driven variant (cat/wolf/frog/cow/pig/chicken/…): the texture its definition names.
CemModelLoader.VariantAssets va = models.variant(s.typeKey(), s.variant());
String asset = va == null ? null : va.texture(babyModel, s.collar() != 0);
String asset = va == null ? null : va.texture(babyModel, s.collar() != 0, s.hasPose("angry"));
if (asset != null) {
int[][] t = texture(asset);
if (t != null) return t;
}
CemModelLoader.EntityInfo info = models.entityInfo(s.typeKey());
if (info == null && ch != null) info = models.entityInfo(ch.base);
List<String> adult = new ArrayList<>();
for (ResourceLocation rl : EntityModels.textureCandidates(s.typeKey(), s.variant())) adult.add(rl.path());
if (info != null && info.texture() != null && !info.texture().equals("missingno")) {
@@ -813,35 +946,35 @@ public final class CemBaker implements EntityBaker<EntityState> {
// like the body — raised zombie arms included.
private static final int GLINT_COLOR = 0xFF8040CC; // approximated enchantment-glint purple
private void addArmorLayers(EntityState s, Choice ch, Pose pose, List<CemGeometry.Baked> baked) {
private void addArmorLayers(EntityState s, Choice ch, Pose pose, Out out) {
EntityState.Equipment eq = s.equipment();
String base = ch == null ? s.typeKey() : ch.base;
boolean baby = ch != null && ch.babyModel;
String set = base + "/HumanoidArmorLayer." + (baby ? "babyModelSet" : "modelSet") + ".";
String folder = baby ? "humanoid_baby" : "humanoid";
bakeArmorPiece(eq.head(), set + "head", folder, pose, baked);
// HumanoidArmorLayer: babies use the humanoid_baby equipment textures — except small armor stands, whose
// armor models are the adult ones scaled (ARMOR_STAND_SMALL_ARMOR = BABY_TRANSFORMER over the adult
// layer, adult UVs) and keep humanoid / humanoid_leggings.
boolean babyTextures = baby && !s.typeKey().equals("armor_stand");
String folder = babyTextures ? "humanoid_baby" : "humanoid";
bakeArmorPiece(eq.head(), set + "head", folder, pose, out);
if (eq.chest() != null && eq.chest().asset().equals("elytra")) {
bakeElytra(eq.chest(), base, baby, pose, baked);
bakeElytra(eq.chest(), base, baby, pose, out);
} else {
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, baked);
bakeArmorPiece(eq.chest(), set + "chest", folder, pose, out);
}
bakeArmorPiece(eq.legs(), set + "legs", baby ? "humanoid_baby" : "humanoid_leggings", pose, baked);
bakeArmorPiece(eq.feet(), set + "feet", folder, pose, baked);
bakeArmorPiece(eq.legs(), set + "legs", babyTextures ? "humanoid_baby" : "humanoid_leggings", pose, out);
bakeArmorPiece(eq.feet(), set + "feet", folder, pose, out);
}
/** Bake one armor slot: its vanilla per-slot model, textured for the material. */
private void bakeArmorPiece(
EntityState.EquipPiece piece,
String modelName,
String layerFolder,
Pose pose,
List<CemGeometry.Baked> baked) {
EntityState.EquipPiece piece, String modelName, String layerFolder, Pose pose, Out out) {
if (piece == null) return;
CemModelLoader.CemModel model = models.vanilla(modelName);
if (model == null) return;
int[][] tex = derived.get(
DerivedTextureCache.key("armor", piece, layerFolder), () -> buildArmorTexture(piece, layerFolder));
if (tex != null) baked.addAll(pose.bake(model, tex));
if (tex != null) add(out, pose, model, tex);
}
/** Resolve + composite a single armor slot's texture: material base (+leather dye/overlay), trim, glint. */
@@ -888,8 +1021,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
/** Elytra in the chest slot: the wearer's own wings model (vanilla WingsLayer). */
private void bakeElytra(
EntityState.EquipPiece piece, String base, boolean baby, Pose pose, List<CemGeometry.Baked> baked) {
private void bakeElytra(EntityState.EquipPiece piece, String base, boolean baby, Pose pose, Out out) {
CemModelLoader.CemModel model =
models.vanilla(base + "/WingsLayer." + (baby ? "elytraBabyModel" : "elytraModel"));
if (model == null) return; // renderer without a WingsLayer
@@ -897,12 +1029,12 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (tex == null) return;
if (piece.glint()) {
tex = derived.get(DerivedTextureCache.key("elytra_glint", tex), () -> {
int[][] out = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
applyGlint(out);
return out;
int[][] glinted = TextureOps.deepCopy(texture("entity/equipment/wings/elytra"));
applyGlint(glinted);
return glinted;
});
}
baked.addAll(pose.bake(model, tex));
add(out, pose, model, tex);
}
private RenderedEntity fallbackBox(EntityState s, int[][] tex) {
@@ -53,9 +53,36 @@ public final class CemModelLoader {
/**
* A whole model: declared texture size and its top-level parts. {@code absolute} models (dumped from
* the vanilla client by {@code tools/VanillaModelDump.java}) carry absolute part pivots + boxes.
* the vanilla client by {@code tools/VanillaModelDump.java}) carry absolute part pivots + boxes and the name
* of the render type vanilla draws them with ({@code renderType}, see {@link #renderType}; null = unknown).
*/
public record CemModel(int texW, int texH, List<CemPart> parts, boolean absolute, String texture) {}
public record CemModel(
int texW, int texH, List<CemPart> parts, boolean absolute, String texture, String renderType) {}
/**
* How a vanilla render type's pipeline draws texels: {@code cutout} = shader ALPHA_CUTOUT threshold (0..1,
* texels below it are discarded; -1 = none), {@code blend} = alpha-blended ("translucent") / added
* ("additive") / null = written opaque (cutout and solid types: alpha ignored), {@code shaded} = directional
* face shading (false with NO_CARDINAL_LIGHTING: eyes, breeze wind).
*/
public record RenderType(double cutout, String blend, boolean shaded) {}
/**
* An emissive render layer (EyesLayer, LivingEntityEmissiveLayer, BreezeEyesLayer): the dumped model it
* draws (the body model's name for layers that draw the parent model), its texture (per render-state enum
* constant in {@code textures}, e.g. copper-golem weathering), render type, the constant vertex alpha of
* its alpha function, the state flag it needs ({@code when}, null = always), whether it is still drawn on an
* invisible entity, and whether its alpha is animated (not drawn: a photo has no age).
*/
public record EmissiveLayer(
String model,
String texture,
Map<String, String> textures,
String renderType,
double alpha,
String when,
boolean invisible,
boolean animated) {}
/** Per-entity-type renderer info from the vanilla dump: extra render scale and default textures. */
public record EntityInfo(
@@ -69,7 +96,9 @@ public final class CemModelLoader {
boolean humanoidArmor, // the renderer has a HumanoidArmorLayer (wears humanoid armor)
Map<String, PoseTransform> poseTransforms, // pose -> renderer transform when it differs (sleeping, …)
Map<String, PoseSet> modelPoses, // body sub-model ("baby", "smallModel", …) -> its own poses
Map<String, RotationBinding> rotationBindings) { // state field ("headPose", "xRot", …) -> part rotations
Map<String, RotationBinding> rotationBindings, // state field ("headPose", "xRot", …) -> part rotations
List<EmissiveLayer> emissive, // emissive layers (eyes, glow), in vanilla layer order
Map<String, String> stateTextures) { // state flag ("tipped", "invulnerable") -> body texture
/** Whether the vanilla renderer draws this layer for an entity in its default state. */
public boolean layerDrawnByDefault(String layerClass) {
@@ -132,9 +161,12 @@ public final class CemModelLoader {
str(o, "model"));
}
/** Texture id for the entity's state: wolves by wild/tame, others by asset id; baby first if asked. */
public String texture(boolean baby, boolean tame) {
String state = tame ? "tame" : "wild";
/**
* Texture id for the entity's state: wolves by tame / angry / wild (vanilla Wolf.getTexture: a tame wolf
* keeps its tame texture when angry), others by asset id; baby first if asked.
*/
public String texture(boolean baby, boolean tame, boolean angry) {
String state = tame ? "tame" : angry ? "angry" : "wild";
if (baby && babyAssets.containsKey(state)) return babyAssets.get(state);
if (baby && babyAsset != null) return babyAsset;
if (assets.containsKey(state)) return assets.get(state);
@@ -162,6 +194,7 @@ public final class CemModelLoader {
// 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<>();
private final Map<String, RenderType> renderTypes = new HashMap<>();
// entity type -> variant key ("tabby", or "<ns>:<key>" outside minecraft) -> assets
private final Map<String, Map<String, VariantAssets>> variants = new java.util.concurrent.ConcurrentHashMap<>();
@@ -191,6 +224,11 @@ public final class CemModelLoader {
return vanilla.keySet();
}
/** Pipeline properties of a vanilla render type by name (see {@link CemModel#renderType()}), or null. */
public RenderType renderType(String name) {
return name == null ? null : renderTypes.get(name);
}
/** Vanilla renderer info for an entity type key, or null (not in the dump). */
public EntityInfo entityInfo(String typeKey) {
return entities.get(typeKey);
@@ -230,7 +268,21 @@ public final class CemModelLoader {
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean(),
poseTransforms(o.getAsJsonObject("poseTransforms")),
modelPoses(o.getAsJsonObject("modelPoses")),
rotationBindings(o.getAsJsonObject("rotationBindings"))));
rotationBindings(o.getAsJsonObject("rotationBindings")),
emissive(o.getAsJsonArray("emissive")),
strings(o.getAsJsonObject("stateTextures"))));
}
if (root.has("renderTypes")) {
for (Map.Entry<String, JsonElement> t :
root.getAsJsonObject("renderTypes").entrySet()) {
JsonObject o = t.getValue().getAsJsonObject();
renderTypes.put(
t.getKey(),
new RenderType(
o.has("cutout") ? o.get("cutout").getAsDouble() : -1,
o.has("blend") ? o.get("blend").getAsString() : null,
!o.has("shaded") || o.get("shaded").getAsBoolean()));
}
}
if (root.has("variants")) {
for (Map.Entry<String, JsonElement> t :
@@ -262,7 +314,9 @@ public final class CemModelLoader {
boolean absolute =
model.has("absolute") && model.get("absolute").getAsBoolean();
String texture = model.has("texture") ? model.get("texture").getAsString() : null;
target.put(e.getKey(), new CemModel(tw, th, parts, absolute, texture));
String renderType =
model.has("renderType") ? model.get("renderType").getAsString() : null;
target.put(e.getKey(), new CemModel(tw, th, parts, absolute, texture, renderType));
} catch (Exception ex) {
if (logger != null) logger.warning("Failed to parse CEM model " + e.getKey() + ": " + ex.getMessage());
}
@@ -420,6 +474,32 @@ public final class CemModelLoader {
return out;
}
private static List<EmissiveLayer> emissive(JsonArray a) {
List<EmissiveLayer> out = new ArrayList<>();
if (a == null) return out;
for (JsonElement el : a) {
JsonObject o = el.getAsJsonObject();
out.add(new EmissiveLayer(
o.get("model").getAsString(),
o.get("texture").getAsString(),
strings(o.getAsJsonObject("textures")),
o.has("renderType") ? o.get("renderType").getAsString() : null,
o.has("alpha") ? o.get("alpha").getAsDouble() : 1,
o.has("when") ? o.get("when").getAsString() : null,
o.has("invisible") && o.get("invisible").getAsBoolean(),
o.has("animated") && o.get("animated").getAsBoolean()));
}
return out;
}
private static Map<String, String> strings(JsonObject o) {
Map<String, String> out = new HashMap<>();
if (o == null) return out;
for (Map.Entry<String, JsonElement> e : o.entrySet())
out.put(e.getKey(), e.getValue().getAsString());
return out;
}
private static Map<String, Boolean> layers(JsonObject o) {
Map<String, Boolean> out = new HashMap<>();
if (o.has("layers")) {
@@ -149,7 +149,7 @@ public final class ElementIntersector {
double t = sc.t[k];
Vector world = new Vector(bx + ox + dx * t, by + oy + dy * t, bz + oz + dz * t);
Vector normal = new Vector(sc.nx[k], sc.ny[k], sc.nz[k]);
return new FaceHit(t, world, normal, color, face.tintIndex, false);
return new FaceHit(t, world, normal, color, face.tintIndex, false, false);
}
return null;
}
@@ -4,7 +4,9 @@ import org.bukkit.util.Vector;
/**
* The result of intersecting a ray with a block's geometry: the world-space hit point and normal,
* the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none) and whether the
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind).
* the sampled ARGB color (before shading/tinting), the face's tint index ({@code -1} = none), whether the
* surface is drawn additively (emissive energy-swirl layers: its colour is added to what lies behind) and whether
* it skips directional face shading (entity layers without cardinal lighting: eyes, breeze wind).
*/
public record FaceHit(double t, Vector point, Vector normal, int color, int tintIndex, boolean additive) {}
public record FaceHit(
double t, Vector point, Vector normal, int color, int tintIndex, boolean additive, boolean unshaded) {}
@@ -102,9 +102,11 @@ public final class SnapshotRaytracer {
double t;
boolean additive = false;
if (eh != null) {
// Emissive additive surfaces (energy swirl) are unshaded: vanilla NO_CARDINAL_LIGHTING + EMISSIVE.
// Emissive additive surfaces (energy swirl) and layers without cardinal lighting (eyes, breeze
// wind) are unshaded: vanilla NO_CARDINAL_LIGHTING (+ EMISSIVE).
additive = eh.additive();
color = additive ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
boolean flat = additive || eh.unshaded();
color = flat ? eh.color() : ColorUtil.shade(eh.color(), shadeFactor(eh.normal()));
t = eh.t();
} else if (pending != null) {
color = pending.color;
@@ -16,6 +16,7 @@ import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.function.DoubleSupplier;
import org.bukkit.DyeColor;
import org.bukkit.Keyed;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -164,7 +165,7 @@ public final class EntitySnapshotBuilder {
if (e instanceof Slime sl) sizeScale = sl.getSize();
// MushroomCow extends Cow, ZombieVillager does not extend Villager — order matters.
if (e instanceof Sheep sh) {
tint = ColorUtil.dyeArgb(sh.getColor(), 0);
tint = isJebSheep(sh) ? jebWoolColor(sh.getTicksLived()) : sheepWoolColor(sh.getColor());
if (sh.isSheared()) variant = "sheared";
} else if (e instanceof TropicalFish tf) {
variant = keyOf(tf.getPattern());
@@ -187,6 +188,9 @@ public final class EntitySnapshotBuilder {
// WitherBoss.isPowered(): health <= max health / 2 (wither armor layer).
AttributeInstance max = wi.getAttribute(Attribute.MAX_HEALTH);
powered = max != null && wi.getHealth() <= max.getValue() / 2;
// WitherBossRenderer.scale: 2 - invulnerableTicks / 220 · 0.5 (the dumped transform holds the 2).
int inv = wi.getInvulnerableTicks();
if (inv > 0) sizeScale = (2 - inv / 220f * 0.5f) / 2;
} else if (e instanceof Axolotl a) {
variant = keyOf(a.getVariant());
} else if (e instanceof Parrot p) {
@@ -386,12 +390,128 @@ public final class EntitySnapshotBuilder {
} else if (e instanceof Sniffer sn && sn.getState() == Sniffer.State.DIGGING) {
f.add("digging");
}
renderFlags(e, f);
} catch (Throwable ignored) {
// API missing on this server version: render the flags found so far
}
return f.isEmpty() ? null : String.join("+", f);
}
/**
* Render-state flags beyond poses (the baker ignores those a type doesn't use): mob arm poses
* ({@code arms_<pose>}, the dump's IllagerArmPose / PiglinArmPose constants), angry wolves ({@code angry}:
* angry texture + raised tail), tipped arrows ({@code tipped}), the invulnerable (blinking) wither
* ({@code invulnerable}), active creakings ({@code eyes_glowing}) and sheep with an undercoat
* ({@code undercoat}).
*/
private static void renderFlags(Entity e, java.util.Set<String> f) {
String arms = armPose(e);
if (arms != null) f.add("arms_" + arms);
if (e instanceof Wolf w && w.isAngry()) f.add("angry");
// TippableArrowRenderer: isTipped = Arrow.getColor() > 0 — the raw ARGB int, so only colours without the
// alpha bit (custom RGB colours) count; potion colours are opaque ARGB (negative) and keep the plain arrow.
if (e instanceof org.bukkit.entity.Arrow ar
&& ar.getColor() != null
&& ar.getColor().asARGB() > 0) {
f.add("tipped");
}
if (e instanceof Wither wi) {
// WitherBossRenderer.getTextureLocation: invulnerable texture above 80 ticks, then blinking every 5.
int t = wi.getInvulnerableTicks();
if (t > 0 && (t > 80 || t / 5 % 2 != 1)) f.add("invulnerable");
}
if (e instanceof org.bukkit.entity.Creaking cr && cr.isActive()) f.add("eyes_glowing");
// SheepWoolUndercoatLayer: jeb_ or non-white wool (adults only, decided by the baker), sheared or not.
if (e instanceof Sheep sh && (isJebSheep(sh) || sh.getColor() != DyeColor.WHITE)) f.add("undercoat");
}
/**
* Arm pose of a mob whose renderer reads one from the entity (lower-case enum constant; null = the idle
* pose): Piglin/PiglinBrute.getArmPose (dancing, admiring a loved off-hand item, aggressive with a melee
* weapon, charging / holding a charged crossbow) and the illagers' getArmPose (pillager: crossbow charge /
* hold, else attacking or neutral; vindicator: attacking / celebrating; spellcasters: spellcasting,
* illusioner bow, celebrating; otherwise crossed).
*/
private static String armPose(Entity e) {
boolean aggressive = e instanceof Mob m && m.isAggressive();
if (e instanceof org.bukkit.entity.PiglinAbstract pa) {
EntityEquipment eq = pa.getEquipment();
ItemStack main = eq == null ? null : eq.getItemInMainHand();
ItemStack off = eq == null ? null : eq.getItemInOffHand();
boolean melee = main != null && !main.getType().isAir() && main.hasData(DataComponentTypes.TOOL);
if (e instanceof org.bukkit.entity.Piglin p) {
if (p.isDancing()) return "dancing";
if (off != null && org.bukkit.Tag.ITEMS_PIGLIN_LOVED.isTagged(off.getType())) return "admiring_item";
if (aggressive && melee) return "attacking_with_melee_weapon";
if (p.isChargingCrossbow()) return "crossbow_charge";
boolean holding = (main != null && main.getType() == Material.CROSSBOW)
|| (off != null && off.getType() == Material.CROSSBOW);
boolean charged = main != null
&& main.getItemMeta() instanceof org.bukkit.inventory.meta.CrossbowMeta cm
&& cm.hasChargedProjectiles();
return holding && charged ? "crossbow_hold" : null;
}
return aggressive && melee ? "attacking_with_melee_weapon" : null;
}
if (e instanceof org.bukkit.entity.Pillager pi) {
if (pi.isChargingAttack()) return "crossbow_charge";
EntityEquipment eq = pi.getEquipment();
boolean crossbow = eq != null
&& (eq.getItemInMainHand().getType() == Material.CROSSBOW
|| eq.getItemInOffHand().getType() == Material.CROSSBOW);
return crossbow ? "crossbow_hold" : aggressive ? "attacking" : "neutral";
}
if (e instanceof org.bukkit.entity.Vindicator v) {
return aggressive ? "attacking" : v.isCelebrating() ? "celebrating" : null;
}
if (e instanceof org.bukkit.entity.Spellcaster sc) {
if (sc.getSpell() != org.bukkit.entity.Spellcaster.Spell.NONE) return "spellcasting";
if (e instanceof org.bukkit.entity.Illusioner) return aggressive ? "bow_and_arrow" : null;
return sc.isCelebrating() ? "celebrating" : null;
}
return null;
}
/** SheepRenderer: a sheep named "jeb_" (checkMagicName) cycles through all wool colours. */
private static boolean isJebSheep(Sheep sh) {
net.kyori.adventure.text.Component name = sh.customName();
return name != null
&& "jeb_"
.equals(net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText()
.serialize(name));
}
/**
* Vanilla wool colour (ColorLerper.Type.SHEEP): the dye's texture-diffuse colour × 0.75, white 0xE6E6E6.
*/
private static int sheepWoolColor(DyeColor dye) {
if (dye == null || dye == DyeColor.WHITE) return 0xFFE6E6E6;
org.bukkit.Color c = dye.getColor();
return ColorUtil.argb(
0xFF,
Math.clamp((int) Math.floor(c.getRed() * 0.75f), 0, 255),
Math.clamp((int) Math.floor(c.getGreen() * 0.75f), 0, 255),
Math.clamp((int) Math.floor(c.getBlue() * 0.75f), 0, 255));
}
/**
* jeb_ wool (ColorLerper.getLerpedColor(SHEEP, ageInTicks)) at the entity's age: 25 ticks per dye colour, linearly
* blended to the next one.
*/
private static int jebWoolColor(int ticks) {
DyeColor[] dyes = DyeColor.values();
int value = ticks / 25;
int c1 = sheepWoolColor(dyes[value % dyes.length]);
int c2 = sheepWoolColor(dyes[(value + 1) % dyes.length]);
float step = (ticks % 25) / 25f;
int out = 0xFF000000;
for (int sh = 0; sh <= 16; sh += 8) {
int a = (c1 >> sh) & 0xFF, b = (c2 >> sh) & 0xFF;
out |= (a + (int) Math.floor(step * (b - a))) << sh;
}
return out;
}
/** Cat.isLyingOnTopOfSleepingPlayer: a sleeping player within the cat's block box inflated by 2. */
private static boolean sleepingPlayerNear(Entity cat) {
Location l = cat.getLocation();
@@ -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_arm","pivot":[1.75,3.75,0.0],"rotate":[0.0,0.0,25.0],"boxes":[{"coordinates":[1.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,0.0],"sizeAdd":-0.01}]},{"part":"left_arm","pivot":[-1.75,3.75,0.0],"rotate":[0.0,0.0,-25.0],"boxes":[{"coordinates":[-2.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,6.0],"sizeAdd":-0.01}]},{"part":"right_wing","pivot":[0.5,4.25,0.6],"rotate":[-25.0,18.0,0.0],"boxes":[{"coordinates":[0.5,-1.75,0.6,0.0,Line 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]}]}]}],"renderType":"entity_cutout"}},"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]}]}]}],"renderType":"entity_cutout"}},"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_arm","pivot":[1.75,3.75,0.0],"rotate":[0.0,0.0,25.0],"boxes":[{"coordinates":[1.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,0.0],"sizeAdd":-0.01}]},{"part":"left_arm","pivot":[-1.75,3.75,0.0],"rotate":[0.0,0.0,-25.0],"boxes":[{"coordinates":[-2.5,0.25,-1.0,1.0,4.0,2.0],"textureOffset":[23.0,6.0],"sizeAdd":-0.01}]},{"part":"right_wing","pivot":[0.5,4.25,0.6],"rotate":[-Line truncated
+22 -1
View File
@@ -82,6 +82,18 @@ public class EntityTestRender {
"baby:drowned", "baby:zombie_villager", "villager", "pillager", "vindicator", "witch", "cow", "pig",
"creeper", "enderman", "evoker", "illusioner", "wandering_trader", "copper_golem", "fox", "panda"})
VARIANT_CASES.add(t + "+armor");
// Layer / texture states: emissive eyes, sheep undercoat, angry wolves, tipped arrows, invulnerable
// wither, partial-alpha cutout textures, held items (piglin arm poses), elytra.
for (String c : new String[] {"spider", "cave_spider", "enderman", "phantom", "breeze", "warden",
"copper_golem@oxidized", "creaking", "creaking+p:eyes_glowing", "sheep@sheared", "sheep+red",
"sheep@sheared+red", "baby:sheep+red", "sheep+p:jeb", "wolf@pale+p:angry", "baby:wolf@pale+p:angry",
"wolf@pale+tame+p:angry", "arrow+p:tipped", "spectral_arrow", "wither+p:invulnerable",
"llama@gray", "llama@white", "tropical_fish@brinely", "piglin+sword", "piglin+crossbow",
"piglin+sword+crossbowl", "piglin+crossbow+p:arms_crossbow_hold", "piglin+sword+p:arms_attacking_with_melee_weapon",
"zombified_piglin+sword", "piglin_brute+sword", "pillager+crossbow",
"vindicator+sword", "armor_stand+elytra", "zombie+elytra", "baby:armor_stand+armor",
"baby:armor_stand+arms+elytra"})
VARIANT_CASES.add(c);
VARIANT_CASES.addAll(List.of(ENTITIES));
}
@@ -258,12 +270,14 @@ public class EntityTestRender {
if (variant == null) variant = VAR.get(key);
boolean isPlayer = key.equals("player");
int tint = key.equals("tropical_fish") ? 0xFFF9801D : (key.equals("sheep") ? 0xFFF9FFFE : 0);
if (key.equals("sheep") && flags.contains("red")) tint = 0xFFB02E26;
int tint2 = key.equals("tropical_fish") ? 0xFF3AB3DA : 0;
EntityState.Equipment armor = plain ? (baby ? null : ARMOR.get(key)) : null;
if (flags.contains("armor")) {
EntityState.EquipPiece d = P("diamond");
armor = new EntityState.Equipment(d, d, d, d);
}
if (flags.contains("elytra")) armor = new EntityState.Equipment(null, P("elytra"), null, null);
boolean chest = plain ? CHEST.contains(key) : flags.contains("chest");
boolean saddle = plain ? SADDLE.contains(key) : flags.contains("saddle");
String equip = plain ? EQUIP.get(key) : null;
@@ -277,6 +291,9 @@ public class EntityTestRender {
if (f.startsWith("p:")) poses.add(f.substring(2));
if (f.startsWith("pitch:")) pitch = Float.parseFloat(f.substring(6));
}
// Flags the snapshot derives from the entity: sheep undercoat (non-white wool), pillager crossbow hold.
if (key.equals("sheep") && flags.contains("red")) poses.add("undercoat");
if (key.equals("pillager") && flags.contains("crossbow")) poses.add("arms_crossbow_hold");
Collections.sort(poses);
Map<String, float[]> partPoses = null;
if (flags.contains("stand")) {
@@ -290,10 +307,14 @@ public class EntityTestRender {
}
boolean shoulder = flags.contains("shoulder");
ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", null, null, false) : null;
// Held items: "sword" (right hand), "crossbow" (right hand), "crossbowl" (left hand).
ItemSpec right = flags.contains("sword") ? new ItemSpec("iron_sword", null, null, false)
: flags.contains("crossbow") ? new ItemSpec("crossbow", null, null, false) : null;
ItemSpec left = flags.contains("crossbowl") ? new ItemSpec("crossbow", null, null, false) : null;
return new EntityState(key, 0, 0, 0, yaw, baby, 0.8, 1.0,
isPlayer, null, false, variant, tint, 1.0, PROF.get(key), LVL.getOrDefault(key, 0),
plain ? MARK.get(key) : null, saddle, chest, arms, equip, armor,
plain && !baby && INVISIBLE.contains(key), pitch, 0, tint2, flags.contains("passenger"), null, null, null,
plain && !baby && INVISIBLE.contains(key), pitch, 0, tint2, flags.contains("passenger"), right, left, null,
collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
shoulder ? "red_blue" : null, shoulder ? "blue" : null);
}
+354 -17
View File
@@ -107,8 +107,9 @@ public class VanillaModelDump {
addTransform(info, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
Map<Model<?>, String> types = addRenderInfo(info, layers, found, key, r);
entities.put(key, info);
emit(key, found, textures, state, out, failed);
emit(key, found, textures, state, types, out, failed);
}
// Players & mannequins use the avatar renderers (one per skin model type: wide/slim).
Map<?, ?> avatars = (Map<?, ?>) Class.forName("net.minecraft.client.renderer.entity.EntityRenderers")
@@ -129,8 +130,11 @@ public class VanillaModelDump {
addTransform(info, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
Map<Model<?>, String> types = addRenderInfo(info, layers, found, key, r);
entities.put(key, info);
emit(key, found, textures, r.createRenderState(), out, failed);
Object avatarState = r.createRenderState();
idleTweaks(avatarState);
emit(key, found, textures, avatarState, types, out, failed);
}
// Every layer definition in its rest pose as "layer/<model>#<layer>" (special item models such as the
// shield, block-entity models, …) — same absolute CEM frame as the entity models.
@@ -190,6 +194,9 @@ public class VanillaModelDump {
entities.forEach(ents::add);
root.add("entities", ents);
root.add("variants", registryVariants());
JsonObject rts = new JsonObject();
RENDER_TYPES.forEach(rts::add);
root.add("renderTypes", rts);
try (FileWriter w = new FileWriter(args.length > 0 ? args[0] : "vanilla_entity_models.json")) {
new GsonBuilder().create().toJson(root, w);
}
@@ -233,7 +240,7 @@ public class VanillaModelDump {
}
idleTweaks(state);
boolean[] submitted = {false};
Object proxy = recordingCollector(m -> {
Object proxy = recordingCollector((m, a) -> {
if (m.getName().startsWith("submit")) submitted[0] = true;
});
Method submit = null;
@@ -250,7 +257,7 @@ public class VanillaModelDump {
}
/** A SubmitNodeCollector proxy that reports every call and otherwise does nothing. */
static Object recordingCollector(java.util.function.Consumer<Method> onCall) {
static Object recordingCollector(java.util.function.BiConsumer<Method, Object[]> onCall) {
Class<?> collector;
try {
collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector");
@@ -260,7 +267,7 @@ public class VanillaModelDump {
return java.lang.reflect.Proxy.newProxyInstance(
VanillaModelDump.class.getClassLoader(), new Class<?>[] {collector}, (px, m, a) -> {
if (m.getName().equals("order")) return px;
onCall.accept(m);
onCall.accept(m, a);
Class<?> rt = m.getReturnType();
if (rt == boolean.class) return false;
if (rt.isPrimitive() && rt != void.class) return 0;
@@ -268,6 +275,296 @@ public class VanillaModelDump {
});
}
// ---- render types, emissive layers, state textures ----------------------------------------------------
/** Render type name -> how its pipeline draws texels (see {@link #renderTypeJson}); written as root "renderTypes". */
static final Map<String, JsonObject> RENDER_TYPES = new TreeMap<>();
/**
* Named render-state assignments ({@link #STATE_POSES} format) whose effect is a different body texture
* ({@code getTextureLocation}): {@code stateTextures: {name: texture}}. Tipped arrows (TippableArrowRenderer:
* isTipped = Arrow.getColor() > 0), the wither while invulnerable (WitherBossRenderer: invulnerableTicks > 80,
* or blinking below).
*/
static final String[][] TEXTURE_STATES = {
{"tipped", "isTipped", "true"},
{"invulnerable", "invulnerableTicks", "100"},
};
/**
* Assignments that switch an emissive layer on that the default state leaves off: creaking eyes glow while the
* creaking is active (CreakingRenderState.eyesGlowing).
*/
static final String[][] LAYER_STATES = {
{"eyes_glowing", "eyesGlowing", "true"},
};
/** One recorded {@code submitModel} call: model, render type, vertex colour (ARGB, -1 = white). */
record Submission(Model<?> model, Object renderType, int color) {}
/**
* Runs the renderer's (or, with {@code layer}, only that layer's) real {@code submit} for {@code state} against
* a recording collector and returns its model submissions.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static List<Submission> submissions(EntityRenderer<?, ?> r, Object layer, Object state) {
List<Submission> out = new ArrayList<>();
Object proxy = recordingCollector((m, a) -> {
if (!m.getName().equals("submitModel") || a == null) return;
Model<?> model = null;
Object rt = null;
for (Object arg : a) {
if (arg instanceof Model<?> mm && model == null) model = mm;
else if (arg instanceof net.minecraft.client.renderer.rendertype.RenderType t && rt == null) rt = t;
else if (arg instanceof net.minecraft.resources.Identifier id && rt == null && model != null)
rt = ((Model) model).renderType(id);
}
int color = a.length >= 9 && a[6] instanceof Integer c ? c : -1;
if (model != null && rt != null) out.add(new Submission(model, rt, color));
});
try {
if (layer == null) {
Method submit = null;
for (Method m : r.getClass().getMethods())
if (m.getName().equals("submit") && m.getParameterCount() == 4 && !m.isBridge()) submit = m;
if (submit != null) submit.invoke(r, state, new com.mojang.blaze3d.vertex.PoseStack(), proxy, null);
} else {
Method submit = null;
for (Method m : layer.getClass().getMethods())
if (m.getName().equals("submit") && m.getParameterCount() == 6 && !m.isBridge()) submit = m;
if (submit != null) {
submit.invoke(layer, new com.mojang.blaze3d.vertex.PoseStack(), proxy, 0xF000F0, state, 0f, 0f);
}
}
} catch (Throwable ignored) {
// partial submissions still count (some layers fail on the null context)
}
return out;
}
/** Name of a RenderType (registered in {@link #RENDER_TYPES} with its pipeline properties). */
static String renderTypeName(Object renderType) {
String name = (String) fieldValue(renderType, "name");
if (name == null) return null;
RENDER_TYPES.computeIfAbsent(name, n -> renderTypeJson(renderType));
return name;
}
/** The render type a model draws with by default ({@code Model.renderType(texture)}). */
@SuppressWarnings({"unchecked", "rawtypes"})
static String renderTypeName(Model<?> model) {
try {
return renderTypeName(((Model) model).renderType(
net.minecraft.resources.Identifier.withDefaultNamespace("textures/entity/dummy.png")));
} catch (Throwable t) {
return null;
}
}
/**
* Pipeline properties of a render type: {@code cutout} (shader ALPHA_CUTOUT threshold; absent = none),
* {@code blend} ("translucent" = src-alpha blending, "additive"; absent = opaque write), {@code emissive}
* (EMISSIVE: no lightmap) and {@code shaded} (false with NO_CARDINAL_LIGHTING: no directional face shading).
*/
static JsonObject renderTypeJson(Object renderType) {
JsonObject o = new JsonObject();
Object setup = fieldValue(renderType, "state");
com.mojang.renderpearl.api.pipeline.RenderPipeline p =
(com.mojang.renderpearl.api.pipeline.RenderPipeline) fieldValue(setup, "pipeline");
if (p == null) return o;
o.addProperty("pipeline", p.getLocation().getPath());
net.minecraft.client.renderer.ShaderDefines defs = p.getShaderDefines();
String cut = defs.values().get("ALPHA_CUTOUT");
if (cut != null) o.addProperty("cutout", Float.parseFloat(cut.replace("f", "").replace("F", "")));
for (com.mojang.renderpearl.api.pipeline.ColorTargetState c : p.getColorTargetStates()) {
if (c == null || c.blendFunction().isEmpty()) continue;
Object bf = c.blendFunction().get();
if (bf.equals(com.mojang.renderpearl.api.pipeline.BlendFunction.ADDITIVE)) o.addProperty("blend", "additive");
else o.addProperty("blend", "translucent");
}
o.addProperty("emissive", defs.flags().contains("EMISSIVE"));
o.addProperty("shaded", !defs.flags().contains("NO_CARDINAL_LIGHTING"));
return o;
}
/** Texture (path without "textures/" and ".png") a render type samples as Sampler0, or null. */
@SuppressWarnings("unchecked")
static String renderTypeTexture(Object renderType) {
Object setup = fieldValue(renderType, "state");
Map<String, ?> tex = setup == null ? null : (Map<String, ?>) fieldValue(setup, "textures");
Object binding = tex == null ? null : tex.get("Sampler0");
Object id = binding == null ? null : fieldValue(binding, "location");
return id == null ? null : texturePath(id);
}
static String texturePath(Object id) {
return id.toString().replaceFirst("^minecraft:", "").replaceFirst("^textures/", "").replaceFirst("\\.png$", "");
}
static boolean emissive(Object renderType) {
JsonObject o = RENDER_TYPES.get(renderTypeName(renderType));
return o != null && o.has("emissive") && o.get("emissive").getAsBoolean();
}
/**
* Render info of one renderer: the render type of every model the default state submits (returned, written
* per model as {@code renderType} by {@link #emit}; other models use their own {@code Model.renderType}),
* {@code emissive} layers and {@code stateTextures} ({@link #TEXTURE_STATES}).
*
* <p>{@code emissive: [{model, texture, renderType, alpha[, textures{<enum constant>: texture}][, when][,
* invisible][, animated]}]}: every layer that submits with an EMISSIVE pipeline (EyesLayer = RenderTypes.eyes,
* BreezeEyesLayer, LivingEntityEmissiveLayer: warden glow, copper-golem / creaking eyes), with its texture, the
* vertex alpha of its alpha function, texture variants per render-state enum constant (copper golem
* weathering), the {@link #LAYER_STATES} flag it needs, whether it still draws on an invisible entity
* (EyesLayer: yes) and whether its alpha changes with the entity age (warden pulsating spots, …).
*/
static Map<Model<?>, String> addRenderInfo(
JsonObject info, Object layers, Map<String, Model<?>> found, String key, EntityRenderer<?, ?> r) {
Map<Model<?>, String> types = new IdentityHashMap<>();
Object st0 = freshState(r, List.of());
if (st0 == null) return types;
for (Submission sub : submissions(r, null, st0)) {
types.putIfAbsent(sub.model(), renderTypeName(sub.renderType()));
}
// Equipment (humanoid armor, elytra, saddles, body armor, harnesses) is drawn by the EquipmentLayerRenderer
// with RenderTypes.armorCutoutNoCull, whatever the model's own render type (PlayerModel/PiglinModel:
// translucent).
if (layers instanceof List<?> ls) {
String armor = renderTypeName((Object) net.minecraft.client.renderer.rendertype.RenderTypes.armorCutoutNoCull(
net.minecraft.resources.Identifier.withDefaultNamespace("textures/entity/dummy.png")));
for (Object layer : ls) {
boolean equipment = false;
for (Class<?> c = layer.getClass(); c != null && c != Object.class; c = c.getSuperclass())
for (Field f : c.getDeclaredFields())
equipment |= f.getType().getSimpleName().equals("EquipmentLayerRenderer");
if (!equipment) continue;
Map<String, Model<?>> mine = new LinkedHashMap<>();
collectModels(layer, "", mine, 0);
for (Model<?> m : mine.values()) types.put(m, armor);
}
}
Map<Model<?>, String> names = new IdentityHashMap<>();
for (Map.Entry<String, Model<?>> m : found.entrySet()) names.putIfAbsent(m.getValue(), modelName(key, m.getKey()));
JsonArray emissive = new JsonArray();
java.util.Set<String> seen = new java.util.HashSet<>();
if (layers instanceof List<?> ls) {
for (Object layer : ls) {
String when = null;
List<Submission> subs = emissiveSubmissions(r, layer, freshState(r, List.of()));
if (subs.isEmpty()) {
for (String[] a : LAYER_STATES) {
if (!assignmentApplies(st0, a)) continue;
subs = emissiveSubmissions(r, layer, freshState(r, List.<String[]>of(a)));
if (!subs.isEmpty()) {
when = a[0];
break;
}
}
}
for (Submission sub : subs) {
String name = names.get(sub.model());
String tex = renderTypeTexture(sub.renderType());
if (name == null || tex == null || !seen.add(name + "|" + tex)) continue;
JsonObject e = new JsonObject();
e.addProperty("model", name);
e.addProperty("texture", tex);
e.addProperty("renderType", renderTypeName(sub.renderType()));
e.addProperty("alpha", Math.round((sub.color() >>> 24) / 255f * 1000f) / 1000f);
if (when != null) e.addProperty("when", when);
String[] whenA = when == null ? null : java.util.Arrays.stream(LAYER_STATES)
.filter(a -> a[0].equals(e.get("when").getAsString())).findFirst().orElse(null);
List<String[]> base = whenA == null ? List.of() : List.<String[]>of(whenA);
// Animated alpha (pulsating warden spots, …): a photo has no age, such layers are skipped.
boolean animated = false;
for (float age : new float[] {10, 25, 40, 63, 90}) {
Object st = freshState(r, base);
setFloat(st, "ageInTicks", age);
int a = -2;
for (Submission s2 : emissiveSubmissions(r, layer, st))
if (s2.model() == sub.model()) a = s2.color() >>> 24;
if (a != (sub.color() >>> 24)) animated = true;
}
if (animated) e.addProperty("animated", true);
Object inv = freshState(r, base);
setBoolean(inv, "isInvisible", true);
if (emissiveSubmissions(r, layer, inv).stream().anyMatch(s2 -> s2.model() == sub.model()))
e.addProperty("invisible", true);
JsonObject variants = textureVariants(r, layer, sub.model(), base, tex);
if (!variants.isEmpty()) e.add("textures", variants);
emissive.add(e);
}
}
}
if (!emissive.isEmpty()) info.add("emissive", emissive);
JsonObject stateTex = new JsonObject();
String def = textureOf(r, st0);
for (String[] a : TEXTURE_STATES) {
if (!assignmentApplies(st0, a)) continue;
String t = textureOf(r, freshState(r, List.<String[]>of(a)));
if (t != null && !t.equals(def)) stateTex.addProperty(a[0], t);
}
if (!stateTex.isEmpty()) info.add("stateTextures", stateTex);
return types;
}
static List<Submission> emissiveSubmissions(EntityRenderer<?, ?> r, Object layer, Object state) {
if (state == null) return List.of();
return submissions(r, layer, state).stream().filter(s -> emissive(s.renderType())).toList();
}
/** Textures an emissive layer uses per constant of the render state's enum fields (when they differ). */
@SuppressWarnings({"unchecked", "rawtypes"})
static JsonObject textureVariants(EntityRenderer<?, ?> r, Object layer, Model<?> model, List<String[]> base, String def) {
JsonObject out = new JsonObject();
Object probe = freshState(r, base);
for (Field f : probe.getClass().getFields()) {
if (Modifier.isStatic(f.getModifiers()) || !f.getType().isEnum()) continue;
for (Object c : f.getType().getEnumConstants()) {
Object st = freshState(r, base);
try {
f.set(st, c);
} catch (Throwable t) {
continue;
}
for (Submission s : emissiveSubmissions(r, layer, st)) {
if (s.model() != model) continue;
String t = renderTypeTexture(s.renderType());
if (t != null && !t.equals(def)) out.addProperty(((Enum<?>) c).name().toLowerCase(), t);
}
}
}
return out;
}
/** The renderer's body texture for a state ({@code getTextureLocation}), or null. */
static String textureOf(EntityRenderer<?, ?> r, Object state) {
if (state == null) return null;
try {
Method gt = null;
for (Class<?> c = r.getClass(); c != null && gt == null; c = c.getSuperclass()) {
for (Method dm : c.getDeclaredMethods())
if (dm.getName().equals("getTextureLocation") && dm.getParameterCount() == 1 && !dm.isBridge())
gt = dm;
}
if (gt == null) return null;
gt.setAccessible(true);
Object id = gt.invoke(r, state);
return id == null ? null : texturePath(id);
} catch (Throwable t) {
return null;
}
}
static void setBoolean(Object o, String field, boolean v) {
try {
Field f = o.getClass().getField(field);
if (f.getType() == boolean.class) f.setBoolean(o, v);
} catch (Throwable ignored) {
}
}
/**
* The built-in data-driven mob variants ({@code data/minecraft/<type>_variant/<key>.json}, cat/wolf/frog/
* cow/pig/chicken/zombie_nautilus …) with their client assets: {@code variants: {<type>: {<key>: {asset_id,
@@ -324,7 +621,9 @@ public class VanillaModelDump {
/**
* 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; worn elytra rest at
* the ElytraAnimationState idle targets (xRot π/12, zRot -π/12; the render state's 0 is a never-ticked
* entity, the wings would hang flat).
*/
static void idleTweaks(Object state) {
if (state == null) return;
@@ -344,6 +643,10 @@ public class VanillaModelDump {
}
} catch (Throwable ignored) {
}
if (hasField(state, "elytraRotX") && hasField(state, "elytraRotZ")) {
setFloat(state, "elytraRotX", (float) (Math.PI / 12));
setFloat(state, "elytraRotZ", (float) (-Math.PI / 12));
}
}
/** Writes every collected model of one entity type; adult/normal/plain "model" fields become the main model. */
@@ -352,21 +655,20 @@ public class VanillaModelDump {
Map<String, Model<?>> found,
Map<String, String> textures,
Object state,
Map<Model<?>, String> renderTypes,
Map<String, JsonObject> out,
List<String> failed) {
for (Map.Entry<String, Model<?>> m : found.entrySet()) {
String f = m.getKey();
String name =
switch (f) {
case "model", "adultModel", "normalModel" -> key;
case "babyModel" -> key + "/baby";
default -> key + "/" + f;
};
String name = modelName(key, f);
if (out.containsKey(name)) continue;
JsonObject jem = dump(m.getValue(), state, name, failed);
if (jem != null) {
String tex = textures.get(f);
if (tex != null) jem.addProperty("texture", tex);
String rt = renderTypes.get(m.getValue());
if (rt == null) rt = renderTypeName(m.getValue());
if (rt != null) jem.addProperty("renderType", rt);
JsonObject entry = new JsonObject();
entry.add("model", jem);
out.put(name, entry);
@@ -374,6 +676,15 @@ public class VanillaModelDump {
}
}
/** Dump name of a collected model field of entity type {@code key} (see {@link #emit}). */
static String modelName(String key, String field) {
return switch (field) {
case "model", "adultModel", "normalModel" -> key;
case "babyModel" -> key + "/baby";
default -> key + "/" + field;
};
}
/**
* A render layer's models (prefixed by the layer class; equipment layers by their equipment layer type,
* e.g. {@code equip.horse_saddle}) plus the texture the layer draws them with, when it holds one.
@@ -382,6 +693,11 @@ public class VanillaModelDump {
String prefix = layer.getClass().getSimpleName() + ".";
Object type = fieldValue(layer, "layer");
if (type instanceof Enum<?> en) prefix = "equip." + en.name().toLowerCase() + ".";
// A renderer with several layers of one class (warden: 5 LivingEntityEmissiveLayers): "<Layer>#2.", …
String base = prefix;
for (int n = 2; !prefix.startsWith("equip.") && hasPrefix(found, prefix); n++) {
prefix = base.substring(0, base.length() - 1) + "#" + n + ".";
}
Map<String, Model<?>> mine = new LinkedHashMap<>();
collectModels(layer, prefix, mine, 0);
// Texture identifiers held by the layer (static constants or instance fields): baby vs adult.
@@ -411,6 +727,11 @@ public class VanillaModelDump {
}
}
static boolean hasPrefix(Map<String, Model<?>> found, String prefix) {
for (String k : found.keySet()) if (k.startsWith(prefix)) return true;
return false;
}
/**
* Runs the renderer's real {@code submit} (yaw 0, pitch 0) against a recording collector and returns the
* pose matrix (column-major 4x4, Java model units -> entity-local blocks) of its first model submission:
@@ -494,6 +815,8 @@ public class VanillaModelDump {
{"party", "pose", "PARTY"}, // dancing parrot
{"digging", "pose", "DIGGING"},
{"digging", "diggingAnimationState", "anim:72"}, // sniffer: head and body down in the ground
{"angry", "isAngry", "true"}, // wolf: tail straight (WolfModel), raised (Wolf.getTailAngle)
{"angry", "tailAngle", "1.5393804"},
};
/** State poses that combine (every subset of a group with 2+ applicable members is dumped too). */
@@ -526,10 +849,14 @@ public class VanillaModelDump {
for (String f : new String[] {"isPassenger", "isRiding"})
if (hasField(probe, f)) passenger.add(new String[] {"passenger", f, "true"});
if (!passenger.isEmpty()) toggles.add(passenger);
// HumanoidModel.ArmPose.ITEM (arm raised by a held item) comes only from AvatarRenderer.getArmPose
// (players, mannequins): HumanoidMobRenderer (zombies, skeletons, piglins, …) extracts EMPTY (SPEAR,
// aggressive bow/trident aside), other renderers never set it.
boolean avatar = r instanceof net.minecraft.client.renderer.entity.player.AvatarRenderer<?>;
List<List<String[]>> items = new ArrayList<>();
if (hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose"))
if (avatar && hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose"))
items.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
if (hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose"))
if (avatar && hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose"))
items.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"}));
toggles.addAll(items);
// State flags that switch model parts on in setupAnim: armor-stand arms, donkey/mule/llama chests.
@@ -543,9 +870,19 @@ public class VanillaModelDump {
for (int i = 0; i < toggles.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(toggles.get(i));
combos.add(c);
}
// Illagers: the arm pose enum replaces the crossed arms (crossbow hold, attacking, …).
if (hasField(probe, "armPose")) {
for (String v : new String[] {"CROSSBOW_HOLD", "ATTACKING", "NEUTRAL", "BOW_AND_ARROW"}) {
// Mob arm-pose enums (illagers: IllagerArmPose, idle CROSSED; piglins: PiglinArmPose, idle DEFAULT): every
// other constant (crossbow hold, attacking, admiring, …) as "arms_<constant>".
if (hasField(probe, "armPose") && probeEnum(probe, "armPose")) {
Object idle = fieldValue(freshState(r, List.of()), "armPose");
List<String> armPoses = new ArrayList<>();
Object[] constants;
try {
constants = probe.getClass().getField("armPose").getType().getEnumConstants();
} catch (NoSuchFieldException e) {
constants = new Object[0];
}
for (Object c : constants) if (c != idle) armPoses.add(((Enum<?>) c).name());
for (String v : armPoses) {
List<String[]> c = new ArrayList<>();
c.add(new String[] {"arms_" + v.toLowerCase(), "armPose", v});
combos.add(c);
+97
View File
@@ -168,6 +168,103 @@ public class PPTest extends JavaPlugin {
scenes.put("pose_beds", new Scene("pose_beds", this::poseHumans, 1.3, 2.6, -3.2, 1.3, 0.4, 0.2));
scenes.put("pose_stands", new Scene("pose_stands", this::poseStands, 4, 2.4, -5, 4, 0.9, 0));
scenes.put("pose_stands_side", new Scene("pose_stands_side", this::poseStands, -3, 2.4, -3, 4, 0.9, 0));
// Entity texture / layer states (rfSpawn, deterministic): emissive eyes (spider, cave spider, enderman,
// phantom, breeze, copper golem, warden), sheep wool colour / undercoat / baby wool / jeb_, angry wolves,
// partial-alpha cutout textures (llama, tropical fish); small armor stands with armor, elytra at rest,
// piglin / illager arm poses, skeleton holding a sword, invulnerable wither, tipped / potion arrows.
scenes.put("rf_vlayers", new Scene("rf_vlayers", this::rfVisualLayers, 5.5, 3.4, -6, 5.5, 0.6, 2.5));
scenes.put("rf_vlayers_close", new Scene("rf_vlayers_close", this::rfVisualLayers, 3.2, 1.5, -2.4, 3.2, 0.5, 1));
scenes.put("rf_vstates", new Scene("rf_vstates", this::rfVisualStates, 5.5, 3.4, -6, 5.5, 0.9, 2.5));
scenes.put("rf_vstates_close", new Scene("rf_vstates_close", this::rfVisualStates, 3.2, 1.7, -2.6, 3.2, 0.9, 0.5));
}
private void rfVisualLayers(Location o) {
// Row 1: emissive eye layers.
rfSpawn(o, 0, 0, 0, 180, Spider.class);
rfSpawn(o, 1.6, 0, 0, 180, CaveSpider.class);
rfSpawn(o, 3.2, 0, 0, 180, Enderman.class);
rfSpawn(o, 4.8, 1.2, 0, 180, Phantom.class);
rfSpawn(o, 6.4, 0, 0, 180, Breeze.class);
rfSpawn(o, 8.0, 0, 0, 180, CopperGolem.class)
.setWeatheringState(io.papermc.paper.world.WeatheringCopperState.OXIDIZED);
rfSpawn(o, 10.2, 0, 0.5, 180, Warden.class);
// Row 2: sheep (white, red, red sheared, baby red, jeb_, blue sheared), angry wolves (wild, tame).
rfSpawn(o, 0, 0, 2.6, 180, Sheep.class);
rfSpawn(o, 1.4, 0, 2.6, 180, Sheep.class).setColor(DyeColor.RED);
Sheep sr = rfSpawn(o, 2.8, 0, 2.6, 180, Sheep.class);
sr.setColor(DyeColor.RED);
sr.setSheared(true);
Sheep sb = rfSpawn(o, 4.2, 0, 2.6, 180, Sheep.class);
sb.setColor(DyeColor.RED);
sb.setBaby();
Sheep jeb = rfSpawn(o, 5.6, 0, 2.6, 180, Sheep.class);
jeb.customName(Component.text("jeb_"));
Sheep sbl = rfSpawn(o, 7.0, 0, 2.6, 180, Sheep.class);
sbl.setColor(DyeColor.BLUE);
sbl.setSheared(true);
rfSpawn(o, 8.6, 0, 2.6, 180, Wolf.class).setAngry(true);
Wolf wt = rfSpawn(o, 10.0, 0, 2.6, 180, Wolf.class);
tame(wt, DyeColor.RED);
wt.setAngry(true);
// Row 3: partial-alpha cutout textures.
rfSpawn(o, 0.5, 0, 5.2, 180, Llama.class).setColor(Llama.Color.GRAY);
rfSpawn(o, 2.0, 0, 5.2, 180, Llama.class).setColor(Llama.Color.WHITE);
TropicalFish tf = rfSpawn(o, 3.4, 0.3, 5.2, 180, TropicalFish.class);
tf.setPattern(TropicalFish.Pattern.BRINELY);
tf.setBodyColor(DyeColor.ORANGE);
tf.setPatternColor(DyeColor.LIGHT_BLUE);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 1, 2), 9, 4, 6)) {
if (e instanceof LivingEntity le) le.setBodyYaw(180);
}
}
private void rfVisualStates(Location o) {
// Row 1: small armor stands with armor / elytra, elytra at rest, piglin arm poses.
ArmorStand small = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
small.setSmall(true);
small.setArms(true);
diamondArmor(small);
ArmorStand smallE = rfSpawn(o, 1.2, 0, 0, 180, ArmorStand.class);
smallE.setSmall(true);
smallE.getEquipment().setChestplate(it(Material.ELYTRA));
rfSpawn(o, 2.4, 0, 0, 180, ArmorStand.class).getEquipment().setChestplate(it(Material.ELYTRA));
rfSpawn(o, 3.7, 0, 0, 180, Zombie.class).getEquipment().setChestplate(it(Material.ELYTRA));
Piglin p1 = rfSpawn(o, 5.0, 0, 0, 180, Piglin.class);
p1.getEquipment().setItemInMainHand(it(Material.GOLDEN_SWORD));
Piglin p2 = rfSpawn(o, 6.3, 0, 0, 180, Piglin.class);
p2.getEquipment().setItemInMainHand(it(Material.GOLDEN_SWORD));
p2.setAggressive(true);
Piglin p3 = rfSpawn(o, 7.6, 0, 0, 180, Piglin.class);
ItemStack cb = it(Material.CROSSBOW);
org.bukkit.inventory.meta.CrossbowMeta cm = (org.bukkit.inventory.meta.CrossbowMeta) cb.getItemMeta();
cm.addChargedProjectile(it(Material.ARROW));
cb.setItemMeta(cm);
p3.getEquipment().setItemInMainHand(cb);
Piglin p4 = rfSpawn(o, 8.9, 0, 0, 180, Piglin.class);
p4.getEquipment().setItemInOffHand(it(Material.GOLD_INGOT));
PiglinBrute pb = rfSpawn(o, 10.2, 0, 0, 180, PiglinBrute.class);
pb.getEquipment().setItemInMainHand(it(Material.GOLDEN_AXE));
pb.setAggressive(true);
for (Entity e : List.of(p1, p2, p3, p4, pb)) ((PiglinAbstract) e).setImmuneToZombification(true);
// Row 2: illagers, skeleton with a sword, invulnerable wither.
rfSpawn(o, 0, 0, 2.6, 180, Pillager.class);
rfSpawn(o, 1.3, 0, 2.6, 180, Pillager.class).getEquipment().setItemInMainHand(it(Material.CROSSBOW));
rfSpawn(o, 2.6, 0, 2.6, 180, Vindicator.class).getEquipment().setItemInMainHand(it(Material.IRON_AXE));
Vindicator va = rfSpawn(o, 3.9, 0, 2.6, 180, Vindicator.class);
va.getEquipment().setItemInMainHand(it(Material.IRON_AXE));
va.setAggressive(true);
rfSpawn(o, 5.2, 0, 2.6, 180, Skeleton.class).getEquipment().setItemInMainHand(it(Material.IRON_SWORD));
rfSpawn(o, 8.5, 0, 3.2, 180, Wither.class).setInvulnerableTicks(100);
// Row 3: hovering arrows: plain, tipped (custom colour without alpha bit), potion (opaque ARGB colour), spectral.
projectile(o.clone().add(0.5, 1, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
Arrow tipped = (Arrow) projectile(o.clone().add(0.5, 1.6, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
tipped.setColor(Color.fromARGB(0x00FF2020));
Arrow potion = (Arrow) projectile(o.clone().add(0.5, 2.2, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
potion.setBasePotionType(org.bukkit.potion.PotionType.POISON);
projectile(o.clone().add(0.5, 2.8, 5.5), new Vector(1, 0, 0), 0.0001, false, SpectralArrow.class);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 1, 2), 9, 4, 6)) {
if (e instanceof LivingEntity le && !(e instanceof ArmorStand)) le.setBodyYaw(180);
}
}
private void perfFarm(Location o) {