entity state poses from the vanilla dump: sleeping/sitting/crouching/swimming/gliding, fox/cat/panda/parrot/bat/camel/sniffer poses with pose transforms, per-sub-model poses (baby pivots), armor-stand part poses and body pitch via rotation bindings, fish in water, beds, shoulder parrots, pose test scenes

This commit is contained in:
MineTec committed 2026-11-04 17:51:01 +01:00
1 parent 6660ebe557
commit 45d033bd89
10 files changed
+1108 -110

No files matched your search

+4 -4
View File
@@ -166,7 +166,7 @@ public class EntityBenchmark {
false, null, false, variant(type), tint, 1.0,
type.equals("villager") ? "farmer" : null, type.equals("villager") ? 2 : 0,
null, false, false, false, null, null,
false, 0, 0, 0, false, null, null, null, 0, false, null);
false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null);
}
/** Vanilla DyeColor texture colours (white … black). */
@@ -177,7 +177,7 @@ public class EntityBenchmark {
return new EntityState("villager", x, 1, z, yaw, baby, 0.6, 1.95,
false, null, false, type, 0, 1.0, prof, level,
null, false, false, false, null, null,
false, 0, 0, 0, false, null, null, null, 0, false, null);
false, 0, 0, 0, false, null, null, null, 0, false, null, null, null, null, null);
}
/** {@code s} with wool tint, humanoid armor and collar dye replaced. */
@@ -186,7 +186,7 @@ public class EntityBenchmark {
s.player(), s.skinUrl(), s.slim(), s.variant(), tint, s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), eq, s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), collar, s.powered(),
s.mouthItem());
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
}
/** The variant the game always supplies (cow/pig/chicken have no texture without one). */
@@ -204,7 +204,7 @@ public class EntityBenchmark {
s.player(), s.skinUrl(), s.slim(), s.variant(), s.tint(), s.sizeScale(), s.profession(), s.villagerLevel(),
s.markings(), s.saddle(), s.chest(), s.arms(), s.bodyEquip(), s.equipment(), s.invisible(), s.pitch(),
s.bodyDye(), s.tint2(), s.passenger(), s.rightHand(), s.leftHand(), s.headItem(), s.collar(), true,
s.mouthItem());
s.mouthItem(), s.pose(), s.partPoses(), s.shoulderLeft(), s.shoulderRight());
}
/**
+24 -1
View File
@@ -269,10 +269,33 @@ public class EntityTestRender {
String equip = plain ? EQUIP.get(key) : null;
boolean arms = flags.contains("arms");
int collar = flags.contains("tame") ? 0xFFB02E26 : 0;
// Pose flags: "p:<state pose>" (sleeping, sitting, lying, …), "pitch:<deg>", "passenger", "shoulder"
// (red + blue parrot on the shoulders), "stand" (custom armor-stand part poses), "mouth" (sweet berries).
List<String> poses = new ArrayList<>();
float pitch = 0;
for (String f : flags) {
if (f.startsWith("p:")) poses.add(f.substring(2));
if (f.startsWith("pitch:")) pitch = Float.parseFloat(f.substring(6));
}
Collections.sort(poses);
Map<String, float[]> partPoses = null;
if (flags.contains("stand")) {
partPoses = new HashMap<>();
partPoses.put("headPose", new float[] {-20, 30, 0});
partPoses.put("bodyPose", new float[] {10, 20, 0});
partPoses.put("rightArmPose", new float[] {-90, 0, 0});
partPoses.put("leftArmPose", new float[] {-30, 0, -40});
partPoses.put("leftLegPose", new float[] {30, 0, -10});
partPoses.put("rightLegPose", new float[] {-40, 0, 0});
}
boolean shoulder = flags.contains("shoulder");
ItemSpec mouth = flags.contains("mouth") ? new ItemSpec("sweet_berries", 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), 0, 0, tint2, false, null, null, null, collar, false, null);
plain && !baby && INVISIBLE.contains(key), pitch, 0, tint2, flags.contains("passenger"), null, null, null,
collar, false, mouth, poses.isEmpty() ? null : String.join("+", poses), partPoses,
shoulder ? "red_blue" : null, shoulder ? "blue" : null);
}
static BufferedImage labelCell(String key, BufferedImage v1, BufferedImage v2) {
+413 -51
View File
@@ -105,8 +105,8 @@ public class VanillaModelDump {
JsonObject info = entityInfo(r, state);
addLayerInfo(info, layers, r);
addTransform(info, r);
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
if (main != null) addPoseVariants(info, main, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
entities.put(key, info);
emit(key, found, textures, state, out, failed);
}
@@ -127,8 +127,8 @@ public class VanillaModelDump {
JsonObject info = new JsonObject();
addLayerInfo(info, layers, r);
addTransform(info, r);
Model<?> main = found.get("model");
if (main != null) addPoseVariants(info, main, r);
addPoseVariants(info, found, key, r);
addRotationBindings(info, found, r);
entities.put(key, info);
emit(key, found, textures, r.createRenderState(), out, failed);
}
@@ -446,15 +446,74 @@ public class VanillaModelDump {
}
}
/**
* Named STATE poses (the render-state fields vanilla's extractRenderState fills from the entity's pose/flags):
* {@code {pose, field, value[, "unless:<field>"]}}. A pose applies to a renderer when at least one of its
* assignments finds its field; the others are skipped (one name covers fox/cat/wolf/panda/parrot/camel
* sitting, …). Values: boolean, float, enum constant name, or {@code anim:<ticks>} = start that
* AnimationState that many ticks before the frame (static keyframe pose). The snapshot maps the entity's
* Bukkit state onto these names; the dump only keeps poses that change parts or the renderer transform.
*/
static final String[][] STATE_POSES = {
{"sleeping", "isSleeping", "true"},
{"sleeping", "pose", "SLEEPING", "unless:isSleeping"}, // LivingEntityRenderer: lying in bed
{"sitting", "isSitting", "true"},
{"sitting", "sitAmount", "1"}, // panda
{"sitting", "pose", "SITTING"}, // parrot (ParrotModel.Pose), camel
{"sitting", "sitPoseAnimationState", "anim:0"}, // camel
{"crouching", "isCrouching", "true"},
{"crouching", "crouchAmount", "3"}, // fox (Fox.getCrouchAmount max)
{"crouching", "pose", "CROUCHING"},
{"crouching", "elytraRotX", "0.6981317"}, // ElytraAnimationState crouching targets
{"crouching", "elytraRotY", "0.08726646"},
{"crouching", "elytraRotZ", "-0.7853982"},
{"pouncing", "isPouncing", "true"}, // fox leap
{"faceplanted", "isFaceplanted", "true"}, // fox stuck in snow
{"lying", "lieDownAmount", "1"}, // cat
{"lying", "lieDownAmountTail", "1"},
{"head_up", "relaxStateOneAmount", "1"}, // cat
{"on_player", "isLyingOnTopOfSleepingPlayer", "true"}, // cat on its sleeping owner
{"on_back", "lieOnBackAmount", "1"}, // panda
{"resting", "isResting", "true"}, // bat hanging
{"resting", "restAnimationState", "anim:0"},
{"swimming", "isVisuallySwimming", "true"},
{"swimming", "swimAmount", "1"},
{"swimming", "isInWater", "true"},
{"swimming", "pose", "SWIMMING"},
{"crawling", "isVisuallySwimming", "true"}, // swimming pose out of water
{"crawling", "swimAmount", "1"},
{"crawling", "pose", "SWIMMING"},
{"fall_flying", "isFallFlying", "true"},
{"fall_flying", "fallFlyingTimeInTicks", "20"},
{"fall_flying", "pose", "FALL_FLYING"},
{"fall_flying", "elytraRotX", "0.34906584"}, // level glide
{"fall_flying", "elytraRotZ", "-1.5707964"},
{"in_water", "isInWater", "true"}, // fish lie on their side only out of water
{"standing", "pose", "STANDING"}, // parrot on the ground (the render-state default is FLYING)
{"on_shoulder", "pose", "ON_SHOULDER"}, // parrot on a player's shoulder (ParrotOnShoulderLayer)
{"party", "pose", "PARTY"}, // dancing parrot
{"digging", "pose", "DIGGING"},
{"digging", "diggingAnimationState", "anim:72"}, // sniffer: head and body down in the ground
};
/** State poses that combine (every subset of a group with 2+ applicable members is dumped too). */
static final List<List<String>> POSE_GROUPS =
List.of(List.of("crouching", "pouncing"), List.of("head_up", "lying", "on_player"));
/**
* State-dependent poses as per-part overrides (only parts that differ from the idle pose): sitting as a
* passenger ({@code isPassenger}/{@code isRiding}), holding an item in the right/left hand
* ({@code right/leftArmPose = ITEM}) and their combinations, plus illager arm poses. Stored per entity
* type as {@code poses: {variant: {part: {pivot, rotate, visible}}}}; the baker applies them by part
* name to the body AND its layer models (armor follows the arms, like vanilla's copyPropertiesTo).
* ({@code right/leftArmPose = ITEM}) and their combinations, illager arm poses, and the {@link #STATE_POSES}
* (alone, in {@link #POSE_GROUPS} combinations and with held items). Stored per entity type as
* {@code poses: {variant: {part: {pivot, rotate, visible[, scale]}}}} for the main body model and
* {@code modelPoses: {<sub-model>: {poses, poseTransforms}}} for the other body models (baby, small, …: own
* pivots); the baker applies them by part name to the body AND its layer models (armor follows the arms,
* like vanilla's copyPropertiesTo). State poses also carry the parts of layer models the body lacks (elytra
* wings) and, when the renderer transform changes (sleeping, swimming, fish in water, …),
* {@code poseTransforms: {variant: {transform, yawSign[, pitchAxis]}}} measured like {@link #addTransform}.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static void addPoseVariants(JsonObject info, Model<?> model, EntityRenderer<?, ?> r) {
static void addPoseVariants(
JsonObject info, Map<String, Model<?>> found, String key, EntityRenderer<?, ?> r) {
Object probe;
try {
probe = r.createRenderState();
@@ -467,8 +526,12 @@ 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);
if (hasField(probe, "rightArmPose") && probeEnum(probe, "rightArmPose")) toggles.add(java.util.Collections.singletonList(new String[] {"item_r", "rightArmPose", "ITEM"}));
if (hasField(probe, "leftArmPose") && probeEnum(probe, "leftArmPose")) toggles.add(java.util.Collections.singletonList(new String[] {"item_l", "leftArmPose", "ITEM"}));
List<List<String[]>> items = new ArrayList<>();
if (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"))
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.
for (String[] f : new String[][] {{"arms", "showArms"}, {"chest", "hasChest"}}) {
if (hasField(probe, f[1]))
@@ -493,40 +556,294 @@ public class VanillaModelDump {
}
}
}
if (combos.isEmpty()) return;
Map<String, float[]> base = poseOf(model, freshState(r, List.of()));
if (base == null) return;
JsonObject poses = new JsonObject();
for (List<String[]> c : combos) {
Map<String, float[]> v = poseOf(model, freshState(r, c));
if (v == null) continue;
JsonObject diff = new JsonObject();
for (Map.Entry<String, float[]> e : v.entrySet()) {
float[] b = base.get(e.getKey());
float[] x = e.getValue();
boolean same = b != null;
for (int i = 0; same && i < x.length; i++) same = Math.abs(b[i] - x[i]) < 1e-4f;
if (same) continue;
JsonObject po = new JsonObject();
boolean moved = b == null;
for (int i = 0; !moved && i < 6; i++) moved = Math.abs(b[i] - x[i]) >= 1e-4f;
if (moved) {
// Absolute pivot + rotation; omitted for pure visibility toggles (arms, chests), which then
// apply to every model of the type (small/baby models have other pivots).
po.add("pivot", arr(x[0], x[1], x[2]));
po.add("rotate", arr(x[3], x[4], x[5]));
}
po.addProperty("visible", x[6] > 0.5f);
diff.add(e.getKey(), po);
// State poses: alone, grouped, each with every held-item subset.
List<List<String[]>> stateCombos = new ArrayList<>();
java.util.Set<String> applicable = new java.util.LinkedHashSet<>();
for (String[] a : STATE_POSES) if (assignmentApplies(probe, a)) applicable.add(a[0]);
List<List<String>> stateNames = new ArrayList<>();
for (String n : applicable) stateNames.add(List.of(n));
for (List<String> g : POSE_GROUPS) {
List<String> members = g.stream().filter(applicable::contains).toList();
for (int mask = 1; mask < (1 << members.size()); mask++) {
if (Integer.bitCount(mask) < 2) continue;
List<String> sub = new ArrayList<>();
for (int i = 0; i < members.size(); i++) if ((mask & (1 << i)) != 0) sub.add(members.get(i));
stateNames.add(sub);
}
if (diff.isEmpty()) continue;
StringBuilder name = new StringBuilder();
c.stream().map(t -> t[0]).distinct().sorted().forEach(n -> name.append(name.isEmpty() ? "" : "+").append(n));
poses.add(name.toString(), diff);
}
if (!poses.isEmpty()) info.add("poses", poses);
// leave the model in its idle pose for the dump
poseOf(model, freshState(r, List.of()));
for (List<String> names : stateNames) {
List<String[]> base = new ArrayList<>();
for (String n : names) for (String[] a : STATE_POSES) if (a[0].equals(n)) base.add(a);
for (int mask = 0; mask < (1 << items.size()); mask++) {
List<String[]> c = new ArrayList<>(base);
for (int i = 0; i < items.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(items.get(i));
stateCombos.add(c);
}
}
if (combos.isEmpty() && stateCombos.isEmpty()) return;
// Every body model (main, baby, small, variant models) gets its own overrides: pivots differ per model.
float[][] t0 = transformTriple(r, List.of(), false);
for (Map.Entry<String, Model<?>> bm : found.entrySet()) {
String f = bm.getKey();
if (f.contains("Layer.") || f.startsWith("equip.")) continue;
boolean main = f.equals("model") || f.equals("adultModel") || f.equals("normalModel");
if (main && bm.getValue() != (found.containsKey("model") ? found.get("model") : found.get("adultModel")))
continue;
boolean baby = f.toLowerCase().contains("baby");
List<Model<?>> layerModels = new ArrayList<>();
for (Map.Entry<String, Model<?>> lm : found.entrySet()) {
String ln = lm.getKey();
if (!ln.contains("Layer.") || ln.toLowerCase().contains("baby") != baby) continue;
layerModels.add(lm.getValue());
}
JsonObject poses = new JsonObject();
JsonObject transforms = new JsonObject();
Model<?> model = bm.getValue();
// Main model: the dumped idle state (unchanged poses). Baby models: a baby's state (isBaby, ageScale
// 0.5 — e.g. the wolf's sitting offsets scale with it).
boolean babyState = baby && !main;
Map<String, float[]> base = poseOf(model, freshState(r, List.of(), babyState));
if (base == null) continue;
for (List<String[]> c : combos) {
if (!c.stream().allMatch(a -> assignmentApplies(probe, a))) continue; // e.g. piglin arm poses
addPose(poses, c, model, base, poseOf(model, freshState(r, c, babyState)), false);
}
Map<String, float[]> baseAll = withLayers(base, layerModels, freshState(r, List.of(), babyState));
java.util.Set<String> emitted = new java.util.HashSet<>();
for (List<String[]> c : stateCombos) {
// Held-item variants only for state poses that exist on their own (no item_r+standing duplicates).
String states = c.stream().filter(a -> !a[0].startsWith("item_")).map(a -> a[0]).distinct().sorted()
.reduce((x, y) -> x + "+" + y).orElse("");
boolean withItems = c.stream().anyMatch(a -> a[0].startsWith("item_"));
if (withItems && !emitted.contains(states)) continue;
Map<String, float[]> v = withLayers(
poseOf(model, freshState(r, c, babyState)), layerModels, freshState(r, c, babyState));
float[][] tr = transformTriple(r, c, baby);
boolean newTransform = tr != null && t0 != null && !sameTriple(tr, t0);
String name = addPose(poses, c, model, baseAll, v, newTransform);
if (name != null) emitted.add(name);
if (name != null && newTransform) transforms.add(name, tripleJson(tr));
}
poseOf(model, freshState(r, List.of())); // leave the model in its idle pose for the dump
if (main) {
if (!poses.isEmpty()) info.add("poses", poses);
if (!transforms.isEmpty()) info.add("poseTransforms", transforms);
} else {
// Always present (also when empty): the baker then never falls back to the main model's pivots.
String sub = f.equals("babyModel") ? "baby" : f;
if (!info.has("modelPoses")) info.add("modelPoses", new JsonObject());
JsonObject mp = new JsonObject();
mp.add("poses", poses);
if (!transforms.isEmpty()) mp.add("poseTransforms", transforms);
info.getAsJsonObject("modelPoses").add(sub, mp);
}
}
for (Model<?> lm : found.values()) poseOf(lm, freshState(r, List.of()));
}
/**
* Adds the pose {@code v} of combo {@code c} as a diff against {@code base}; returns its name, or null when
* nothing differs (and {@code keepEmpty} is false — state poses that only move the renderer transform).
*/
static String addPose(
JsonObject poses,
List<String[]> c,
Model<?> model,
Map<String, float[]> base,
Map<String, float[]> v,
boolean keepEmpty) {
if (v == null) return null;
JsonObject diff = new JsonObject();
for (Map.Entry<String, float[]> e : v.entrySet()) {
float[] b = base.get(e.getKey());
float[] x = e.getValue();
boolean same = b != null;
for (int i = 0; same && i < x.length; i++) same = Math.abs(b[i] - x[i]) < 1e-4f;
if (same) continue;
JsonObject po = new JsonObject();
boolean moved = b == null;
for (int i = 0; !moved && i < 6; i++) moved = Math.abs(b[i] - x[i]) >= 1e-4f;
if (moved) {
// Absolute pivot + rotation; omitted for pure visibility toggles (arms, chests), which then
// apply to every model of the type (small/baby models have other pivots).
po.add("pivot", arr(x[0], x[1], x[2]));
po.add("rotate", arr(x[3], x[4], x[5]));
}
po.addProperty("visible", x[6] > 0.5f);
boolean scaled = b == null ? (x[7] != 1 || x[8] != 1 || x[9] != 1) : false;
for (int i = 7; b != null && i < 10; i++) scaled |= Math.abs(b[i] - x[i]) >= 1e-4f;
if (scaled) po.add("scale", arr(x[7], x[8], x[9]));
diff.add(e.getKey(), po);
}
if (diff.isEmpty() && !keepEmpty) return null;
StringBuilder name = new StringBuilder();
c.stream().map(t -> t[0]).distinct().sorted().forEach(n -> name.append(name.isEmpty() ? "" : "+").append(n));
poses.add(name.toString(), diff);
return name.toString();
}
/** {@code parts} plus the posed parts of layer models that the body model lacks (elytra wings, …). */
static Map<String, float[]> withLayers(Map<String, float[]> parts, List<Model<?>> layers, Object state) {
if (parts == null) return null;
Map<String, float[]> out = new LinkedHashMap<>(parts);
for (Model<?> lm : layers) {
Map<String, float[]> lp = poseOf(lm, state);
if (lp == null) continue;
lp.remove("root");
lp.forEach(out::putIfAbsent);
}
return out;
}
/** Renderer transform, yaw sign and pitch axis for a posed state (see {@link #addTransform}); null = none. */
static float[][] transformTriple(EntityRenderer<?, ?> r, List<String[]> assigns, boolean baby) {
float[] t = captureTransform(r, 0, 0, assigns, baby);
if (t == null) return null;
org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t).get3x3(new org.joml.Matrix3f()).invert();
float[] yaw = {0};
float[] ty = captureTransform(r, 90, 0, assigns, baby);
if (ty != null) {
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0);
float sinA = -rel.m02;
yaw[0] = Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1);
}
float[] axis = null;
float[] tp = captureTransform(r, 0, 90, assigns, baby);
if (tp != null) {
org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0);
float ax = (rel.m12 - rel.m21) / 2, ay = (rel.m20 - rel.m02) / 2, az = (rel.m01 - rel.m10) / 2;
float len = (float) Math.sqrt(ax * ax + ay * ay + az * az);
if (len > 0.5f) axis = new float[] {ax / len, ay / len, az / len};
}
return new float[][] {t, yaw, axis};
}
static boolean sameTriple(float[][] a, float[][] b) {
for (int i = 0; i < 16; i++) if (Math.abs(a[0][i] - b[0][i]) > 1e-4f) return false;
if (a[1][0] != b[1][0]) return false;
if ((a[2] == null) != (b[2] == null)) return false;
if (a[2] != null) for (int i = 0; i < 3; i++) if (Math.abs(a[2][i] - b[2][i]) > 1e-3f) return false;
return true;
}
static JsonObject tripleJson(float[][] tr) {
JsonObject o = new JsonObject();
JsonArray ta = new JsonArray();
for (float v : tr[0]) ta.add(Math.round(v * 100000f) / 100000f);
o.add("transform", ta);
o.addProperty("yawSign", (int) tr[1][0]);
if (tr[2] != null) o.add("pitchAxis", arr(tr[2][0], tr[2][1], tr[2][2]));
return o;
}
/**
* Continuous state inputs bound linearly to part rotations: every {@code Rotations} field (armor-stand
* head/body/arm/leg poses) and {@code xRot} (entity pitch: dolphin body, heads, …), measured by
* re-running setupAnim with each component +10°. Stored as {@code rotationBindings: {field: {default:
* [x,y,z], parts: {part: [9 coefficients, row = CEM rotate axis, column = field component]}}}}; the baker
* adds {@code coeff · (value − default)} to each named part's rotation (body and layer models alike).
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static void addRotationBindings(JsonObject info, Map<String, Model<?>> found, EntityRenderer<?, ?> r) {
Object probe;
try {
probe = r.createRenderState();
} catch (Throwable t) {
return;
}
Model<?> main = found.containsKey("model") ? found.get("model") : found.get("adultModel");
if (main == null) return;
List<Model<?>> layers = new ArrayList<>();
for (Map.Entry<String, Model<?>> lm : found.entrySet())
if (lm.getKey().contains("Layer.") && !lm.getKey().toLowerCase().contains("baby")) layers.add(lm.getValue());
JsonObject out = new JsonObject();
for (Field f : probe.getClass().getFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
boolean rot = f.getType().getSimpleName().equals("Rotations");
if (!rot && !(f.getName().equals("xRot") && f.getType() == float.class)) continue;
try {
Object st0 = freshState(r, List.of());
Map<String, float[]> base = withLayers(poseOf(main, st0), layers, freshState(r, List.of()));
if (base == null) continue;
float[] def = new float[3];
if (rot) {
Object v = f.get(st0);
for (int j = 0; j < 3; j++) def[j] = (float) v.getClass().getMethod(new String[] {"x", "y", "z"}[j]).invoke(v);
} else {
def[0] = f.getFloat(st0);
}
Map<String, float[]> coeff = new TreeMap<>();
for (int j = 0; j < (rot ? 3 : 1); j++) {
Object st = freshState(r, List.of());
if (rot) {
float[] d = def.clone();
d[j] += 10;
f.set(st, f.getType().getConstructor(float.class, float.class, float.class).newInstance(d[0], d[1], d[2]));
} else {
f.setFloat(st, def[0] + 10);
}
Map<String, float[]> v = withLayers(poseOf(main, st), layers, st);
if (v == null) continue;
for (Map.Entry<String, float[]> e : v.entrySet()) {
float[] b = base.get(e.getKey());
if (b == null) continue;
for (int i = 0; i < 3; i++) {
float c = (e.getValue()[3 + i] - b[3 + i]) / 10f;
if (Math.abs(c) < 1e-4f) continue;
coeff.computeIfAbsent(e.getKey(), k -> new float[9])[i * 3 + j] = c;
}
}
}
// Pitch tilts whole bodies only (dolphin, strider, nautilus): head look is left out, PixelPics has
// no head yaw to go with it.
if (!rot) {
java.util.Set<String> heads = new java.util.HashSet<>();
headParts(main.root(), "root", false, heads);
for (Model<?> lm : layers) headParts(lm.root(), "root", false, heads);
coeff.keySet().removeAll(heads);
}
if (coeff.isEmpty()) continue;
JsonObject fb = new JsonObject();
fb.add("default", rot ? arr(def) : arr(def[0]));
JsonObject parts = new JsonObject();
coeff.forEach((p, c) -> parts.add(p, arr(c)));
fb.add("parts", parts);
out.add(f.getName(), fb);
} catch (Throwable t) {
System.out.println("rotation binding " + f.getName() + " (" + r.getClass().getSimpleName() + "): " + t);
}
}
poseOf(main, freshState(r, List.of()));
for (Model<?> lm : layers) poseOf(lm, freshState(r, List.of()));
if (!out.isEmpty()) info.add("rotationBindings", out);
}
/** Names of every part named "*head*" and of everything below one. */
@SuppressWarnings("unchecked")
static void headParts(ModelPart p, String name, boolean inHead, java.util.Set<String> out) {
inHead |= name.contains("head");
if (inHead) out.add(name);
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
if (children == null) return;
for (Map.Entry<String, ModelPart> c : children.entrySet()) headParts(c.getValue(), c.getKey(), inHead, out);
}
/** Whether a {@link #STATE_POSES} assignment finds its field (and an enum value its constant). */
static boolean assignmentApplies(Object st, String[] a) {
if (a.length > 3 && a[3].startsWith("unless:") && hasField(st, a[3].substring(7))) return false;
Field f;
try {
f = st.getClass().getField(a[1]);
} catch (NoSuchFieldException e) {
return false;
}
if (a[2].startsWith("anim:")) return f.getType().getSimpleName().equals("AnimationState");
if (f.getType().isEnum()) {
for (Object c : f.getType().getEnumConstants()) if (((Enum<?>) c).name().equals(a[2])) return true;
return false;
}
return f.getType() == boolean.class || f.getType() == float.class || f.getType() == int.class;
}
/** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */
@@ -547,22 +864,41 @@ public class VanillaModelDump {
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets) {
return freshState(r, sets, false);
}
/** {@code baby}: also isBaby + ageScale 0.5 like a baby's extracted state (baby models' pose offsets). */
static Object freshState(EntityRenderer<?, ?> r, List<String[]> sets, boolean baby) {
try {
Object st = r.createRenderState();
idleTweaks(st);
for (String[] t : sets) {
Field f = st.getClass().getField(t[1]);
if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2]));
else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2]));
}
for (String[] t : sets) assign(st, t);
if (baby) babyState(st);
return st;
} catch (Throwable t) {
return null;
}
}
static void babyState(Object st) throws ReflectiveOperationException {
if (hasField(st, "isBaby")) st.getClass().getField("isBaby").setBoolean(st, true);
if (hasField(st, "ageScale")) st.getClass().getField("ageScale").setFloat(st, 0.5f);
}
/** Applies one {@code {name, field, value[, cond]}} assignment when it applies (see {@link #STATE_POSES}). */
@SuppressWarnings({"unchecked", "rawtypes"})
static void assign(Object st, String[] t) throws ReflectiveOperationException {
if (!assignmentApplies(st, t)) return;
Field f = st.getClass().getField(t[1]);
if (t[2].startsWith("anim:")) {
f.getType().getMethod("start", int.class).invoke(f.get(st), -Integer.parseInt(t[2].substring(5)));
} else if (f.getType() == boolean.class) f.setBoolean(st, Boolean.parseBoolean(t[2]));
else if (f.getType() == float.class) f.setFloat(st, Float.parseFloat(t[2]));
else if (f.getType() == int.class) f.setInt(st, Integer.parseInt(t[2]));
else if (f.getType().isEnum()) f.set(st, Enum.valueOf((Class) f.getType(), t[2]));
}
/** setupAnim with the state, then every part's absolute CEM pivot, CEM rotation and visibility, by name. */
@SuppressWarnings({"unchecked", "rawtypes"})
static Map<String, float[]> poseOf(Model<?> model, Object state) {
@@ -583,7 +919,7 @@ public class VanillaModelDump {
out.put(name, new float[] {
-ax, 24 - ay, az,
(float) Math.toDegrees(-p.xRot), (float) Math.toDegrees(-p.yRot), (float) Math.toDegrees(p.zRot),
p.visible ? 1 : 0
p.visible ? 1 : 0, p.xScale, p.yScale, p.zScale
});
Map<String, ModelPart> children = (Map<String, ModelPart>) fieldValue(p, "children");
if (children != null) children.forEach((n, c) -> collectPose(c, n, ax, ay, az, out));
@@ -598,13 +934,27 @@ public class VanillaModelDump {
}
static float[] captureTransform(EntityRenderer<?, ?> r, float yaw, float pitch) {
return captureTransform(r, yaw, pitch, null, false);
}
/**
* {@code assigns != null}: a posed state ({@link #STATE_POSES}, {@code isBaby} when {@code baby}); the
* renderer's {@code getRenderOffset} (crouching players sink 2 px) is then included like the dispatcher does.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static float[] captureTransform(
EntityRenderer<?, ?> r, float yaw, float pitch, List<String[]> assigns, boolean baby) {
Object state;
try {
state = r.createRenderState();
idleTweaks(state);
if (assigns != null) {
for (String[] a : assigns) assign(state, a);
if (baby) babyState(state);
}
} catch (Throwable t) {
return null;
}
idleTweaks(state);
setFloat(state, "bodyRot", yaw);
setFloat(state, "yRot", yaw);
setFloat(state, "xRot", pitch);
@@ -641,6 +991,18 @@ public class VanillaModelDump {
} catch (Throwable ignored) {
// partial submissions still count (layers may fail on the null context)
}
if (assigns != null && first[0] != null) {
try {
net.minecraft.world.phys.Vec3 off = ((EntityRenderer) r).getRenderOffset((net.minecraft.client.renderer.entity.state.EntityRenderState) state);
if (off != null && off.lengthSqr() > 0) {
first[0] = new org.joml.Matrix4f().set(first[0])
.translateLocal((float) off.x, (float) off.y, (float) off.z)
.get(new float[16]);
}
} catch (Throwable ignored) {
// no render offset for this state
}
}
return first[0];
}
+208 -1
View File
@@ -154,6 +154,20 @@ public class PPTest extends JavaPlugin {
// leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic).
scenes.put("perf_village", new Scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0));
scenes.put("perf_village_close", new Scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4));
// Entity poses (vanilla setupAnim/renderer state from the dump): sitting/sleeping/crouching pets with mouth
// items, cats lying, parrots, pandas; camel/sniffer/bat/dolphin pitch/fish in water/sleeping villager/
// mannequin poses/baby rider; armor stands with custom part poses. All rfSpawn (deterministic).
scenes.put("pose_pets", new Scene("pose_pets", this::posePets, 4.3, 3.6, -6, 4.3, 0.2, 2));
scenes.put("pose_pets_side", new Scene("pose_pets_side", this::posePets, -3, 3, -3, 4, 0, 2));
scenes.put("pose_fox_close", new Scene("pose_fox_close", this::posePets, 2.2, 1.4, -2.2, 2.2, 0.2, 0));
scenes.put("pose_misc", new Scene("pose_misc", this::poseMisc, 6.5, 5, -10, 6.5, 0.5, 0));
scenes.put("pose_misc_side", new Scene("pose_misc_side", this::poseMisc, -4, 3, -7, 6, 0.5, -1));
scenes.put("pose_humans", new Scene("pose_humans", this::poseHumans, 6, 3.5, -7, 6, 0.6, 1));
scenes.put("pose_humans_side", new Scene("pose_humans_side", this::poseHumans, -3, 3, -4, 5, 0.5, 1));
scenes.put("pose_humans_close", new Scene("pose_humans_close", this::poseHumans, 4.8, 1.6, -5.2, 4.8, 0.5, -2.5));
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));
}
private void perfFarm(Location o) {
@@ -234,6 +248,199 @@ public class PPTest extends JavaPlugin {
}
}
// --- poses ------------------------------------------------------------------------------------
private void posePets(Location o) {
// Row 1 (z=0): foxes — standing, sitting, sleeping, crouching, crouching+leaping, faceplanted.
Fox f0 = rfSpawn(o, 0, 0, 0, 180, Fox.class);
hands(f0, Material.SWEET_BERRIES, null);
Fox f1 = rfSpawn(o, 1.2, 0, 0, 180, Fox.class);
f1.setSitting(true);
hands(f1, Material.SWEET_BERRIES, null);
Fox f2 = rfSpawn(o, 2.4, 0, 0, 180, Fox.class);
f2.setSleeping(true);
hands(f2, Material.SWEET_BERRIES, null);
Fox f3 = rfSpawn(o, 3.6, 0, 0, 180, Fox.class);
f3.setCrouching(true);
hands(f3, Material.FEATHER, null);
Fox f4 = rfSpawn(o, 4.8, 0, 0, 180, Fox.class);
f4.setCrouching(true);
f4.setLeaping(true);
Fox f5 = rfSpawn(o, 6.0, 0, 0, 180, Fox.class);
f5.setFaceplanted(true);
f5.setFoxType(Fox.Type.SNOW);
Fox fb = rfSpawn(o, 7.2, 0, 0, 180, Fox.class);
fb.setBaby();
fb.setSleeping(true);
hands(fb, Material.SWEET_BERRIES, null);
Fox fb2 = rfSpawn(o, 8.2, 0, 0, 180, Fox.class);
fb2.setBaby();
fb2.setSitting(true);
// Row 2 (z=1.8): cats & wolves — sitting, lying, lying + head up, baby sitting; wolf/pup sitting.
Cat c0 = rfSpawn(o, 0, 0, 1.8, 180, Cat.class);
c0.setSitting(true);
Cat c1 = rfSpawn(o, 1.2, 0, 1.8, 180, Cat.class);
c1.setLyingDown(true);
c1.setCatType(Cat.Type.BLACK);
Cat c2 = rfSpawn(o, 2.6, 0, 1.8, 180, Cat.class);
c2.setLyingDown(true);
c2.setHeadUp(true);
c2.setCatType(Cat.Type.SIAMESE);
Cat c3 = rfSpawn(o, 4.0, 0, 1.8, 180, Cat.class);
c3.setBaby();
c3.setSitting(true);
tame(c3, DyeColor.RED);
Wolf w0 = rfSpawn(o, 5.2, 0, 1.8, 180, Wolf.class);
w0.setSitting(true);
Wolf w1 = rfSpawn(o, 6.6, 0, 1.8, 180, Wolf.class);
tame(w1, DyeColor.BLUE);
w1.setSitting(true);
Wolf w2 = rfSpawn(o, 8.0, 0, 1.8, 180, Wolf.class);
w2.setBaby();
w2.setSitting(true);
// Row 3 (z=3.6): parrots (flying default, sitting, standing on the ground), pandas.
Parrot p0 = rfSpawn(o, 0, 0, 3.6, 180, Parrot.class);
p0.setVariant(Parrot.Variant.BLUE);
Parrot p1 = rfSpawn(o, 0.8, 0, 3.6, 180, Parrot.class);
p1.setSitting(true);
p1.setVariant(Parrot.Variant.GREEN);
Parrot p2 = rfSpawn(o, 1.6, 0, 3.6, 180, Parrot.class);
p2.setVariant(Parrot.Variant.CYAN);
if (p2 instanceof Mob m) {
m.setAI(true);
m.setAware(false); // no goals, but physics: lands -> on ground -> standing pose
}
Panda pa0 = rfSpawn(o, 3.2, 0, 3.6, 180, Panda.class);
pa0.setSitting(true);
Panda pa1 = rfSpawn(o, 5.2, 0, 3.6, 180, Panda.class);
pa1.setOnBack(true);
Panda pa2 = rfSpawn(o, 7.2, 0, 3.6, 180, Panda.class);
pa2.setBaby();
pa2.setSitting(true);
Panda pa3 = rfSpawn(o, 8.6, 0, 3.6, 180, Panda.class);
pa3.setBaby();
pa3.setOnBack(true);
faceNorth(o);
}
private void poseMisc(Location o) {
Camel ca = rfSpawn(o, 0, 0, 0, 180, Camel.class);
ca.setSitting(true);
rfSpawn(o, 2.6, 0, 0, 180, Camel.class);
Sniffer sn = rfSpawn(o, 5.4, 0, 0, 180, Sniffer.class);
sn.setState(Sniffer.State.DIGGING);
rfSpawn(o, 8.6, 0, 0, 180, Sniffer.class);
// Bats: hanging under a block (resting) and flying.
put(o, 11, 2, 0, "stone");
Bat b0 = rfSpawn(o, 11.5, 1.1, 0.5, 180, Bat.class);
b0.setAwake(false);
Bat b1 = rfSpawn(o, 12.8, 1.0, 0.5, 180, Bat.class);
b1.setAwake(true);
// Dolphins pitched down/up/level, a strider looking up, fish in water vs. flopping on land.
Dolphin d0 = rfSpawn(o, 0, 0.6, -3.5, 180, Dolphin.class);
d0.setRotation(180, 35);
Dolphin d1 = rfSpawn(o, 2, 0.6, -3.5, 180, Dolphin.class);
d1.setRotation(180, -35);
hands(d1, Material.COD, null);
Dolphin d2 = rfSpawn(o, 4, 0.6, -3.5, 180, Dolphin.class);
hands(d2, Material.COD, null);
Strider st = rfSpawn(o, 6, 0, -3.5, 180, Strider.class);
st.setRotation(180, -30);
for (int i = 0; i < 3; i++) put(o, 8 + i, 0, -4, "water");
rfSpawn(o, 8.5, 0.3, -3.5, 180, Cod.class);
rfSpawn(o, 9.5, 0.3, -3.5, 180, Salmon.class);
TropicalFish tf = rfSpawn(o, 10.5, 0.3, -3.5, 180, TropicalFish.class);
tf.setPattern(TropicalFish.Pattern.BETTY);
rfSpawn(o, 8.5, 0, -2, 180, Cod.class);
rfSpawn(o, 9.5, 0, -2, 180, Salmon.class);
TropicalFish tf2 = rfSpawn(o, 10.5, 0, -2, 180, TropicalFish.class);
tf2.setPattern(TropicalFish.Pattern.BETTY);
faceNorth(o);
d0.setRotation(180, 35);
d1.setRotation(180, -35);
st.setRotation(180, -30);
}
private void poseHumans(Location o) {
// Villager asleep in a bed (head at z=0 facing south -> lies along z), a baby villager in a bed facing east.
put(o, 0, 0, 0, "red_bed[facing=south,part=head]");
put(o, 0, 0, -1, "red_bed[facing=south,part=foot]");
Villager v = rfSpawn(o, 0.5, 0, 1.5, 180, Villager.class);
v.setProfession(Villager.Profession.LIBRARIAN);
v.sleep(o.clone().add(0, 0, 0));
put(o, 2, 0, 0, "blue_bed[facing=east,part=head]");
put(o, 1, 0, 0, "blue_bed[facing=east,part=foot]");
Villager vb = rfSpawn(o, 2.5, 0, 1.5, 180, Villager.class);
vb.setBaby();
vb.sleep(o.clone().add(2, 0, 0));
// Mannequins: standing, crouching with a sword, crawling, swimming in water, gliding with elytra, sleeping.
Mannequin m0 = rfSpawn(o, 4, 0, 0, 180, Mannequin.class);
hands(m0, Material.IRON_SWORD, null);
Mannequin m1 = rfSpawn(o, 5.2, 0, 0, 180, Mannequin.class);
m1.setPose(Pose.SNEAKING, true);
hands(m1, Material.IRON_SWORD, null);
Mannequin m2 = rfSpawn(o, 6.6, 0, 0, 180, Mannequin.class);
m2.setPose(Pose.SWIMMING, true);
put(o, 8, 0, 0, "water");
put(o, 8, 0, 1, "water");
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
m3.setPose(Pose.SWIMMING, true);
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
m4.getEquipment().setChestplate(it(Material.ELYTRA));
m4.setPose(Pose.FALL_FLYING, true);
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
m5.setPose(Pose.SLEEPING, true);
// Baby zombie riding a chicken (jockey: baby model pivots in the passenger pose), crouching zombie.
Chicken ch = rfSpawn(o, 4, 0, -2.5, 180, Chicken.class);
Zombie bz = rfSpawn(o, 4, 0, -2.5, 180, Zombie.class);
bz.setBaby();
ch.addPassenger(bz);
Zombie zc = rfSpawn(o, 5.5, 0, -2.5, 180, Zombie.class);
zc.setPose(Pose.SNEAKING, true);
faceNorth(o);
}
private void poseStands(Location o) {
// Custom armor-stand poses (head/body/arms/legs, radians), with arms, armor and held items.
ArmorStand a = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
a.setArms(true);
a.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(-20), Math.toRadians(30), 0));
a.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-90), 0, 0));
a.setLeftArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-30), 0, Math.toRadians(-40)));
Ah(a);
ArmorStand b = rfSpawn(o, 1.6, 0, 0, 180, ArmorStand.class);
b.setArms(true);
b.setBodyPose(new org.bukkit.util.EulerAngle(Math.toRadians(15), Math.toRadians(20), 0));
b.setRightLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(-40), 0, 0));
b.setLeftLegPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, Math.toRadians(-10)));
diamondArmor(b);
hands(b, Material.IRON_SWORD, Material.SHIELD);
ArmorStand c = rfSpawn(o, 3.2, 0, 0, 180, ArmorStand.class);
c.setArms(true);
c.setSmall(true);
c.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-120), Math.toRadians(20), 0));
c.setHeadPose(new org.bukkit.util.EulerAngle(0, 0, Math.toRadians(25)));
diamondArmor(c);
hands(c, Material.TORCH, null);
ArmorStand d = rfSpawn(o, 4.8, 0, 0, 180, ArmorStand.class);
d.setHeadPose(new org.bukkit.util.EulerAngle(Math.toRadians(30), 0, 0));
d.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN));
ArmorStand e = rfSpawn(o, 6.4, 0, 0, 180, ArmorStand.class);
e.setArms(true);
e.setInvisible(true);
e.setRightArmPose(new org.bukkit.util.EulerAngle(Math.toRadians(-60), Math.toRadians(-30), 0));
e.setBodyPose(new org.bukkit.util.EulerAngle(0, Math.toRadians(30), 0));
diamondArmor(e);
hands(e, Material.DIAMOND_SWORD, null);
ArmorStand f = rfSpawn(o, 8, 0, 0, 180, ArmorStand.class); // default pose (must match rf_stands)
f.setArms(true);
}
private static void Ah(ArmorStand a) {
a.getEquipment().setHelmet(it(Material.GOLDEN_HELMET));
hands(a, Material.BOW, Material.APPLE);
}
private void perfItems(Location o) {
Random r = new Random(7);
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,
@@ -283,7 +490,7 @@ public class PPTest extends JavaPlugin {
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");