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

+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) {