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

@@ -39,9 +39,22 @@ public record EntityState(
ItemSpec headItem, // non-armor item worn in the head slot (pumpkin, skull, block, …); null = none
int collar, // ARGB dye of a tamed cat's/wolf's collar; 0 = no collar (untamed)
boolean powered, // energy-swirl layer: charged creeper, wither below half health
ItemSpec mouthItem // item carried in the mouth (fox/dolphin main hand); null = none
ItemSpec mouthItem, // item carried in the mouth (fox/dolphin main hand); null = none
String pose, // state pose flags, sorted and "+"-joined ("sitting", "lying+head_up", "sleeping", "in_water",
// …; the dump's STATE_POSES names, unknown ones are ignored per type); null = none (idle)
java.util.Map<String, float[]> partPoses, // continuous part poses by vanilla render-state field (armor stand
// "headPose"/"bodyPose"/"leftArmPose"/…: x/y/z degrees); null = defaults
String shoulderLeft, // parrot variant on a player's left shoulder (e.g. "red"); null = none
String shoulderRight // parrot variant on the right shoulder; null = none
) {
/** Whether the state pose flags contain {@code flag}. */
public boolean hasPose(String flag) {
if (pose == null) return false;
for (String f : pose.split("\\+")) if (f.equals(flag)) return true;
return false;
}
/** Worn armor (4 slots) of a humanoid wearer; any field may be null. */
public record Equipment(EquipPiece head, EquipPiece chest, EquipPiece legs, EquipPiece feet) {
public boolean isEmpty() {
@@ -111,23 +111,30 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (info == null) info = models.entityInfo(base);
boolean halfBaby = s.baby() && (ch == null || !ch.babyModel); // no dedicated baby model -> 0.5x adult
double extra = (halfBaby ? 0.5 : 1.0) * s.sizeScale();
SelectedPose sel = vanilla ? selectPose(s, info, ch) : SelectedPose.NONE;
Affine t0 = null;
Affine pre;
Affine place;
if (vanilla) {
// Vanilla placement: the renderer's own captured pose (incl. its scale / offsets), yaw and pitch
// applied the way that renderer applies them. CEM px -> Java model units: F·T(0,-24,0)/16.
// applied the way that renderer applies them — the pose's own when it changes them (sleeping in a
// bed, swimming, fish in water, …). CEM px -> Java model units: F·T(0,-24,0)/16.
CemModelLoader.PoseTransform pt = sel.transform();
boolean captured = info != null && info.transform() != null;
Affine t0 = captured ? Affine.fromColumnMajor(info.transform()) : DEFAULT_LIVING_TRANSFORM;
double[] tr = pt != null && pt.transform() != null ? pt.transform() : captured ? info.transform() : null;
t0 = tr != null ? Affine.fromColumnMajor(tr) : DEFAULT_LIVING_TRANSFORM;
pre = t0.mul(Affine.scale(1 / 16.0)).mul(CemGeometry.CEM_TO_JAVA);
int yawSign = captured ? info.yawSign() : -1;
int yawSign = pt != null ? pt.yawSign() : captured ? info.yawSign() : -1;
double[] pitchAxis = pt != null ? pt.pitchAxis() : info != null ? info.pitchAxis() : null;
place = Affine.translation(s.x(), s.y(), s.z())
.mul(Affine.scale(extra))
.mul(Affine.rotY(yawSign * Math.toRadians(s.bodyYaw())));
// Projectiles (verified against ArrowRenderer/ThrownTridentRenderer 26.3): T0 = rotY(-90)[·rotZ(90)],
// pitchAxis = -X, so rotY(yaw)·rotAxis(-X, pitch)·T0 == vanilla rotY(yRot-90)·rotZ(xRot[+90]) with the
// raw projectile rotation (yaw atan2(dx,dz), pitch positive up); origin = entity position, no offset.
if (info != null && info.pitchAxis() != null && s.pitch() != 0) {
double[] a = info.pitchAxis();
// Mob poses: the pitch rotation follows the body yaw directly (swimming/gliding players, pouncing fox).
if (pitchAxis != null && s.pitch() != 0) {
double[] a = pitchAxis;
place = place.mul(Affine.rotAxis(a[0], a[1], a[2], Math.toRadians(s.pitch())));
}
} else {
@@ -136,7 +143,7 @@ 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 ? selectPose(s, info) : Map.of());
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();
@@ -152,6 +159,7 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (vanilla && s.equipment() != null && info != null && info.humanoidArmor()) {
addArmorLayers(s, ch, pose, baked);
}
if (vanilla && !invisible) addShoulderParrots(s, ch, t0, baked);
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);
@@ -184,16 +192,33 @@ public final class CemBaker implements EntityBaker<EntityState> {
}
}
/** 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) {
static final SelectedPose NONE = new SelectedPose(null, Map.of(), null);
}
// Toggle flags that switch parts on/off or seat the entity; kept when held-item flags are dropped.
private static final java.util.Set<String> PART_TOGGLES = java.util.Set.of("passenger", "arms", "chest");
/**
* Picks the vanilla pose variant for the entity's state flags — the render-state fields vanilla's
* {@code setupAnim} reads, dumped per type: sitting ({@code passenger}), holding items ({@code item_r}/
* {@code item_l}), illagers' crossbow hold ({@code arms_*}), armor-stand arms ({@code arms}), chests
* ({@code chest}). Falls back to fewer flags when the exact combination has no own variant (identical to
* a simpler pose): first without the held-item flags (e.g. zombies' arms ignore held items), then with
* the part-toggling flags only.
* {@code setupAnim} / renderer read, dumped per type and per body model (baby/small models have their own
* pivots): sitting as a passenger ({@code passenger}), holding items ({@code item_r}/{@code item_l}),
* illagers' crossbow hold ({@code arms_*}), armor-stand arms ({@code arms}), chests ({@code chest}) and the
* state poses of {@link EntityState#pose()} (sleeping, sitting, crouching, lying, swimming, in_water, …;
* flags the type's dump doesn't know are ignored). Falls back to fewer flags when the exact combination has
* no own variant (identical to a simpler pose): state-pose subsets from all to none, each first with all
* other flags, then without the held-item flags (e.g. zombies' arms ignore held items), then with the
* part-toggling flags only.
*/
private static Map<String, CemModelLoader.PartPose> selectPose(EntityState s, CemModelLoader.EntityInfo info) {
if (info == null || info.poses().isEmpty()) return Map.of();
private SelectedPose selectPose(EntityState s, CemModelLoader.EntityInfo info, Choice ch) {
if (info == null) return SelectedPose.NONE;
String sub = ch == null || ch.name.equals(ch.base) || !ch.name.startsWith(ch.base + "/")
? null
: ch.name.substring(ch.base.length() + 1);
CemModelLoader.PoseSet set = info.poseSet(sub);
if (set.poses().isEmpty()) return SelectedPose.NONE;
List<String> flags = new ArrayList<>();
if (isIllager(info)) {
if (isCrossbow(s.rightHand()) || isCrossbow(s.leftHand())) flags.add("arms_crossbow_hold");
@@ -204,19 +229,99 @@ public final class CemBaker implements EntityBaker<EntityState> {
if (s.passenger()) flags.add("passenger");
if (s.arms()) flags.add("arms");
if (s.chest()) flags.add("chest");
List<String> states = new ArrayList<>();
if (s.pose() != null) {
java.util.Set<String> known = knownPoseFlags(set);
for (String f : s.pose().split("\\+")) if (known.contains(f) && !states.contains(f)) states.add(f);
}
List<List<String>> tries = new ArrayList<>();
tries.add(flags);
tries.add(flags.stream().filter(f -> !f.startsWith("item_")).toList());
tries.add(flags.stream()
.filter(f -> f.equals("passenger") || f.equals("arms") || f.equals("chest"))
.toList());
for (List<String> st : subsetsBySize(states)) {
List<String> all = new ArrayList<>(st);
all.addAll(flags);
tries.add(all);
List<String> noItems = new ArrayList<>(st);
flags.stream().filter(f -> !f.startsWith("item_")).forEach(noItems::add);
tries.add(noItems);
List<String> toggles = new ArrayList<>(st);
flags.stream().filter(PART_TOGGLES::contains).forEach(toggles::add);
tries.add(toggles);
}
for (List<String> t : tries) {
if (t.isEmpty()) continue;
Map<String, CemModelLoader.PartPose> p =
info.poses().get(String.join("+", t.stream().sorted().toList()));
if (p != null) return p;
String name = String.join("+", t.stream().sorted().toList());
Map<String, CemModelLoader.PartPose> p = set.poses().get(name);
if (p != null) return new SelectedPose(name, p, set.transforms().get(name));
}
return Map.of();
return SelectedPose.NONE;
}
/** Every flag that occurs in a pose name of the set (cached per loaded pose map). */
private final Map<Map<String, Map<String, CemModelLoader.PartPose>>, java.util.Set<String>> knownFlags =
java.util.Collections.synchronizedMap(new java.util.IdentityHashMap<>());
private java.util.Set<String> knownPoseFlags(CemModelLoader.PoseSet set) {
return knownFlags.computeIfAbsent(set.poses(), poses -> {
java.util.Set<String> out = new java.util.HashSet<>();
for (String n : poses.keySet()) out.addAll(List.of(n.split("\\+")));
return out;
});
}
/** All subsets of {@code l}, largest first (input order within a size); the empty set last. */
private static List<List<String>> subsetsBySize(List<String> l) {
List<List<String>> out = new ArrayList<>();
int n = Math.min(l.size(), 6);
for (int size = n; size >= 0; size--) {
for (int mask = 0; mask < (1 << n); mask++) {
if (Integer.bitCount(mask) != size) continue;
List<String> sub = new ArrayList<>();
for (int i = 0; i < n; i++) if ((mask & (1 << i)) != 0) sub.add(l.get(i));
out.add(sub);
}
}
return out;
}
/**
* The pose overrides plus the dump's rotation bindings: armor-stand part poses ({@code headPose}, …, from
* {@link EntityState#partPoses()}) and body pitch ({@code xRot} = {@link EntityState#pitch()}: dolphins,
* striders, nautili), each added as {@code coeff · (value − default)} to the bound parts' rotation.
*/
private static Map<String, CemModelLoader.PartPose> withBindings(
Map<String, CemModelLoader.PartPose> parts, EntityState s, CemModelLoader.EntityInfo info) {
if (info == null || info.rotationBindings().isEmpty()) return parts;
Map<String, CemModelLoader.PartPose> out = null;
for (Map.Entry<String, CemModelLoader.RotationBinding> b :
info.rotationBindings().entrySet()) {
double[] value;
if (b.getKey().equals("xRot")) {
value = new double[] {s.pitch()};
} else {
float[] v = s.partPoses() == null ? null : s.partPoses().get(b.getKey());
if (v == null) continue;
value = new double[] {v[0], v[1], v[2]};
}
double[] def = b.getValue().def();
double[] delta = new double[3];
boolean any = false;
for (int j = 0; j < value.length && j < 3; j++) {
delta[j] = value[j] - (def != null && j < def.length ? def[j] : 0);
any |= Math.abs(delta[j]) > 1e-6;
}
if (!any) continue;
if (out == null) out = new java.util.HashMap<>(parts);
for (Map.Entry<String, double[]> pc : b.getValue().parts().entrySet()) {
double[] c = pc.getValue();
double[] add = new double[3];
for (int i = 0; i < 3; i++)
add[i] = c[i * 3] * delta[0] + c[i * 3 + 1] * delta[1] + c[i * 3 + 2] * delta[2];
CemModelLoader.PartPose cur = out.get(pc.getKey());
out.put(
pc.getKey(),
cur != null ? cur.plusRotation(add) : new CemModelLoader.PartPose(null, null, null, null, add));
}
}
return out == null ? parts : out;
}
private static boolean isCrossbow(ItemSpec i) {
@@ -274,25 +379,68 @@ public final class CemBaker implements EntityBaker<EntityState> {
/**
* Item carried in the mouth, rendered in the "ground" context like vanilla (HoldingEntityRenderState).
* Fox (FoxHeldItemLayer, awake): head pivot · [baby: S(0.75)] · head rotation · T(0.06, 0.27 [baby 0.26],
* -0.5) · rotX(90) — the head frame carries exactly that pivot + rotation. Dolphin (DolphinCarryingItemLayer)
* at the model root · T(0, 1, -1) (its xRot-dependent shift is 0: PixelPics dolphins aren't pitched).
* Fox (FoxHeldItemLayer): head PIVOT (the layer turns by the head-look angles, not the posed head part:
* PixelPics has no head look) · [baby: S(0.75)] · T(0.06, 0.27 [baby 0.26], -0.5) · rotX(90); asleep
* T(0.46, 0.26, 0.22) [baby (0.4, 0.26, 0.15)] · rotX(90) · rotZ(90). Dolphin (DolphinCarryingItemLayer) at
* the model root · T(0, 1 − a·0.5, −1 + a·0.5) looking up / T(0, 1 + a·0.8, −1 + a·0.2) looking down,
* a = |pitch| / 60.
*/
private void addMouthItem(EntityState s, Map<String, Affine> frames, List<EntityCube> out) {
ItemSpec item = s.mouthItem();
if (item == null) return;
boolean baby = s.baby();
Affine x = null;
if (s.typeKey().equals("fox") && frames.containsKey("head")) {
x = frames.get("head").mul(CemGeometry.JAVA_LOCAL);
String headPivot = CemGeometry.PIVOT + "head";
if (s.typeKey().equals("fox") && frames.containsKey(headPivot)) {
boolean asleep = s.hasPose("sleeping");
x = frames.get(headPivot).mul(CemGeometry.JAVA_LOCAL);
if (baby) x = x.mul(Affine.scale(0.75));
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
if (asleep) {
x = x.mul(baby ? Affine.translation(0.4, 0.26, 0.15) : Affine.translation(0.46, 0.26, 0.22))
.mul(Affine.rotX(Math.PI / 2))
.mul(Affine.rotZ(Math.PI / 2));
} else {
x = x.mul(Affine.translation(0.06, baby ? 0.26 : 0.27, -0.5)).mul(Affine.rotX(Math.PI / 2));
}
} else if (s.typeKey().equals("dolphin") && frames.containsKey("root")) {
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(Affine.translation(0, 1, -1));
double a = Math.abs(s.pitch()) / 60.0;
Affine off = s.pitch() < 0
? Affine.translation(0, 1 - a * 0.5, -1 + a * 0.5)
: Affine.translation(0, 1 + a * 0.8, -1 + a * 0.2);
x = frames.get("root").mul(CemGeometry.JAVA_LOCAL).mul(off);
}
if (x != null) items.bake(item, "ground", false, x, out);
}
/**
* 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) {
if (t0 == null || (s.shoulderLeft() == null && s.shoulderRight() == null)) return;
if (ch == null || !ch.base.startsWith("player")) return;
CemModelLoader.CemModel pm = models.vanilla("parrot");
CemModelLoader.EntityInfo pi = models.entityInfo("parrot");
if (pm == null || pi == null) return;
Map<String, CemModelLoader.PartPose> parts = pi.poses().getOrDefault("on_shoulder", Map.of());
double y = s.hasPose("crouching") ? -1.3 : -1.5;
for (int side = 0; side < 2; side++) {
String variant = side == 0 ? s.shoulderLeft() : s.shoulderRight();
if (variant == null) continue;
int[][] tex = null;
for (ResourceLocation rl : EntityModels.textureCandidates("parrot", variant)) {
tex = texture(rl.path());
if (tex != null) break;
}
if (tex == null && pi.texture() != null) tex = texture(pi.texture());
if (tex == null) continue;
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));
}
}
private static boolean isIllager(CemModelLoader.EntityInfo info) {
return info != null && info.poses().keySet().stream().anyMatch(k -> k.startsWith("arms_"));
}
@@ -81,6 +81,9 @@ final class CemGeometry {
/** Key prefix in the frames map marking parts that were actually drawn (visible). */
static final String DRAWN = "drawn:";
/** Key prefix in the frames map for a part's pivot frame {@code parent·T(P)} (without its own rotation). */
static final String PIVOT = "pivot:";
private record Ctx(
Set<String> hidden,
int texW,
@@ -107,31 +110,35 @@ final class CemGeometry {
private static void bakeAbsolute(CemModelLoader.CemPart part, Affine parentWorld, Ctx ctx, boolean draw) {
if (ctx.hidden.contains(part.name())) return;
CemModelLoader.PartPose ov = ctx.overrides.get(part.name());
boolean invisible = ov != null ? !ov.visible() : part.invisible();
boolean invisible = ov != null && ov.visible() != null ? !ov.visible() : part.invisible();
if (invisible) {
if (ctx.frames == null) return;
draw = false;
}
if (ov != null && ov.pivot() == null) ov = null; // visibility-only override: applied above
double[] o = ov != null ? ov.pivot() : part.pivot();
double[] rot = ov != null ? ov.rotate() : part.rotate();
Affine world = parentWorld
.mul(Affine.translation(o[0], o[1], o[2]))
double[] scale = ov != null && ov.scale() != null ? ov.scale() : part.scale();
double[] add = ov == null ? null : ov.rotateAdd();
boolean moved = ov != null && ov.pivot() != null; // else visibility/scale/extra-rotation only
double[] o = moved ? ov.pivot() : part.pivot();
double[] rot = moved ? ov.rotate() : part.rotate();
if (add != null) rot = new double[] {rot[0] + add[0], rot[1] + add[1], rot[2] + add[2]};
Affine pivotFrame = parentWorld.mul(Affine.translation(o[0], o[1], o[2]));
Affine world = pivotFrame
.mul(Affine.rotZ(Math.toRadians(rot[2])))
.mul(Affine.rotY(Math.toRadians(rot[1])))
.mul(Affine.rotX(Math.toRadians(rot[0])));
if (part.scale() != null) {
if (scale != null) {
// A collapsed (zero) scale is vanilla's way to hide a part; it would also be non-invertible.
for (double v : part.scale()) if (Math.abs(v) < 1e-6) return;
world = world.mul(Affine.scale(part.scale()[0], part.scale()[1], part.scale()[2]));
for (double v : scale) if (Math.abs(v) < 1e-6) return;
world = world.mul(Affine.scale(scale[0], scale[1], scale[2]));
}
if (ctx.frames != null) {
ctx.frames.putIfAbsent(part.name(), world);
ctx.frames.putIfAbsent(PIVOT + part.name(), pivotFrame);
if (draw) ctx.frames.putIfAbsent(DRAWN + part.name(), world);
}
world = world.mul(Affine.translation(-o[0], -o[1], -o[2]));
Affine boxWorld = world;
if (ov != null) {
if (moved) {
// Boxes are stored relative to the idle pivot: move them along with the overridden pivot
// (children carry their own absolute pivots, so they use the un-shifted transform).
double[] p0 = part.pivot();
@@ -66,19 +66,54 @@ public final class CemModelLoader {
double[] pitchAxis, // world-frame axis the pitch rotates about; null = pitch ignored
Map<String, Map<String, PartPose>> poses, // pose ("passenger+item_r", "arms", "chest", …) -> overrides
Map<String, Boolean> layers, // render layer class -> drawn for the default (idle, unequipped) state
boolean humanoidArmor) { // the renderer has a HumanoidArmorLayer (wears humanoid armor)
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
/** Whether the vanilla renderer draws this layer for an entity in its default state. */
public boolean layerDrawnByDefault(String layerClass) {
return layers.getOrDefault(layerClass, false);
}
/** The poses of a body model: its own set when the dump has one, else the main model's. */
public PoseSet poseSet(String subModel) {
PoseSet own = subModel == null ? null : modelPoses.get(subModel);
return own != null ? own : new PoseSet(poses, poseTransforms);
}
}
/** Pose overrides + renderer transforms of one body model (see {@link EntityInfo#poseSet}). */
public record PoseSet(Map<String, Map<String, PartPose>> poses, Map<String, PoseTransform> transforms) {}
/**
* The renderer transform of a pose (like {@link EntityInfo#transform}/{@code yawSign}/{@code pitchAxis}):
* e.g. sleeping turns the body by the bed direction (yawSign +1), swimming pitches it.
*/
public record PoseTransform(double[] transform, int yawSign, double[] pitchAxis) {}
/**
* A continuous state input bound linearly to part rotations: {@code part rotate += coeff · (value − def)},
* {@code coeff} row-major 3×3 (row = CEM rotate axis, column = input component). Armor-stand part poses,
* body pitch.
*/
public record RotationBinding(double[] def, Map<String, double[]> parts) {}
/**
* A posed part override: absolute CEM pivot and CEM rotation (deg) — both null for a pure visibility
* toggle (the part keeps the pivot of whichever model it is in) — and visibility.
* toggle (the part keeps the pivot of whichever model it is in) — visibility (null = the part's own), scale
* (null = the part's own) and an extra rotation (deg, added to whichever rotation applies; null = none).
*/
public record PartPose(double[] pivot, double[] rotate, boolean visible) {}
public record PartPose(double[] pivot, double[] rotate, Boolean visible, double[] scale, double[] rotateAdd) {
/** This override plus an extra rotation (rotation bindings). */
public PartPose plusRotation(double[] add) {
double[] r = rotateAdd == null
? add
: new double[] {rotateAdd[0] + add[0], rotateAdd[1] + add[1], rotateAdd[2] + add[2]};
return new PartPose(pivot, rotate, visible, scale, r);
}
}
/**
* Client assets of a data-driven mob variant ({@code data/<ns>/<type>_variant/<key>.json}): the texture
@@ -190,9 +225,12 @@ public final class CemModelLoader {
doubles(o, "transform"),
o.has("yawSign") ? o.get("yawSign").getAsInt() : -1,
doubles(o, "pitchAxis"),
poses(o),
poses(o.getAsJsonObject("poses")),
layers(o),
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean()));
o.has("humanoidArmor") && o.get("humanoidArmor").getAsBoolean(),
poseTransforms(o.getAsJsonObject("poseTransforms")),
modelPoses(o.getAsJsonObject("modelPoses")),
rotationBindings(o.getAsJsonObject("rotationBindings"))));
}
if (root.has("variants")) {
for (Map.Entry<String, JsonElement> t :
@@ -315,10 +353,10 @@ public final class CemModelLoader {
return o.has("mirrorTexture") && o.get("mirrorTexture").getAsString().contains("u");
}
private static Map<String, Map<String, PartPose>> poses(JsonObject o) {
private static Map<String, Map<String, PartPose>> poses(JsonObject poses) {
Map<String, Map<String, PartPose>> out = new HashMap<>();
if (!o.has("poses")) return out;
for (Map.Entry<String, JsonElement> v : o.getAsJsonObject("poses").entrySet()) {
if (poses == null) return out;
for (Map.Entry<String, JsonElement> v : poses.entrySet()) {
Map<String, PartPose> parts = new HashMap<>();
for (Map.Entry<String, JsonElement> pe :
v.getValue().getAsJsonObject().entrySet()) {
@@ -328,13 +366,60 @@ public final class CemModelLoader {
new PartPose(
po.has("pivot") ? arr3(po, "pivot") : null,
po.has("rotate") ? arr3(po, "rotate") : null,
!po.has("visible") || po.get("visible").getAsBoolean()));
!po.has("visible") || po.get("visible").getAsBoolean(),
po.has("scale") ? arr3(po, "scale") : null,
null));
}
out.put(v.getKey(), parts);
}
return out;
}
private static Map<String, PoseTransform> poseTransforms(JsonObject o) {
Map<String, PoseTransform> out = new HashMap<>();
if (o == null) return out;
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
JsonObject t = e.getValue().getAsJsonObject();
out.put(
e.getKey(),
new PoseTransform(
doubles(t, "transform"),
t.has("yawSign") ? t.get("yawSign").getAsInt() : -1,
doubles(t, "pitchAxis")));
}
return out;
}
private static Map<String, PoseSet> modelPoses(JsonObject o) {
Map<String, PoseSet> out = new HashMap<>();
if (o == null) return out;
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
JsonObject m = e.getValue().getAsJsonObject();
out.put(
e.getKey(),
new PoseSet(
poses(m.getAsJsonObject("poses")), poseTransforms(m.getAsJsonObject("poseTransforms"))));
}
return out;
}
private static Map<String, RotationBinding> rotationBindings(JsonObject o) {
Map<String, RotationBinding> out = new HashMap<>();
if (o == null) return out;
for (Map.Entry<String, JsonElement> e : o.entrySet()) {
JsonObject b = e.getValue().getAsJsonObject();
Map<String, double[]> parts = new HashMap<>();
for (Map.Entry<String, JsonElement> p : b.getAsJsonObject("parts").entrySet()) {
JsonArray a = p.getValue().getAsJsonArray();
double[] c = new double[9];
for (int i = 0; i < 9 && i < a.size(); i++) c[i] = a.get(i).getAsDouble();
parts.put(p.getKey(), c);
}
out.put(e.getKey(), new RotationBinding(doubles(b, "default"), parts));
}
return out;
}
private static Map<String, Boolean> layers(JsonObject o) {
Map<String, Boolean> out = new HashMap<>();
if (o.has("layers")) {
@@ -26,6 +26,7 @@ import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.Ageable;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Axolotl;
import org.bukkit.entity.Bat;
import org.bukkit.entity.Cat;
import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.Creeper;
@@ -44,7 +45,9 @@ import org.bukkit.entity.PufferFish;
import org.bukkit.entity.Rabbit;
import org.bukkit.entity.Sheep;
import org.bukkit.entity.Shulker;
import org.bukkit.entity.Sittable;
import org.bukkit.entity.Slime;
import org.bukkit.entity.Sniffer;
import org.bukkit.entity.Snowman;
import org.bukkit.entity.TraderLlama;
import org.bukkit.entity.Trident;
@@ -275,11 +278,35 @@ public final class EntitySnapshotBuilder {
// (vanilla dump), so the wearer list follows the game version.
EntityState.Equipment equipment = e instanceof LivingEntity wearer ? captureEquipment(wearer) : null;
String pose = poseFlags(e);
double x = loc.getX(), z = loc.getZ();
if (pose != null && pose.contains("sleeping") && e instanceof LivingEntity sleeper && !(e instanceof Fox)) {
// LivingEntityRenderer: a sleeper in a bed lies along the bed (yaw from the bed direction instead of
// the body yaw) and is shifted by its standing eye height - 0.1 towards the foot end.
org.bukkit.block.BlockFace bed = bedFacing(e);
if (bed != null) {
bodyYaw = switch (bed) {
case SOUTH -> 90;
case NORTH -> 270;
case EAST -> 180;
default -> 0;};
double off = standingEyeHeight(sleeper) - 0.1;
x -= bed.getModX() * off;
z -= bed.getModZ() * off;
}
}
java.util.Map<String, float[]> partPoses = e instanceof ArmorStand as ? armorStandPoses(as) : null;
String shoulderLeft = null, shoulderRight = null;
if (e instanceof HumanEntity he) {
shoulderLeft = shoulderParrot(he, true);
shoulderRight = shoulderParrot(he, false);
}
return new EntityState(
type,
loc.getX(),
x,
loc.getY(),
loc.getZ(),
z,
bodyYaw,
baby,
width,
@@ -308,7 +335,133 @@ public final class EntitySnapshotBuilder {
headItem,
collar,
powered,
mouthItem);
mouthItem,
pose,
partPoses,
shoulderLeft,
shoulderRight);
}
/**
* The entity's state-pose flags (names of the vanilla dump's STATE_POSES, sorted, "+"-joined; null = idle):
* the generic {@link Entity#getPose()} (sleeping, crouching, swimming/crawling, fall_flying, sitting,
* digging) plus the per-mob flags vanilla's renderers read from the entity (sitting pets/pandas/camels,
* fox sleeping/crouching/leaping/faceplanted, cat lying/head up/on a sleeping player, panda on its back,
* resting bat, standing/dancing parrot) and {@code in_water}. The baker ignores flags a type doesn't know.
*/
private static String poseFlags(Entity e) {
java.util.TreeSet<String> f = new java.util.TreeSet<>();
try {
switch (e.getPose()) {
case SLEEPING -> f.add("sleeping");
case SNEAKING -> f.add("crouching");
case SWIMMING -> f.add(e.isInWater() ? "swimming" : "crawling");
case FALL_FLYING -> f.add("fall_flying");
case SITTING -> f.add("sitting");
case DIGGING -> f.add("digging");
default -> {}
}
if (e instanceof LivingEntity le && le.isGliding()) f.add("fall_flying");
if (e.isInWater()) f.add("in_water");
if (e instanceof Sittable st && st.isSitting()) f.add("sitting");
if (e instanceof Fox fox) {
if (fox.isSleeping()) f.add("sleeping");
if (fox.isCrouching()) f.add("crouching");
if (fox.isLeaping()) f.add("pouncing");
if (fox.isFaceplanted()) f.add("faceplanted");
} else if (e instanceof Cat cat) {
if (cat.isLyingDown()) {
f.add("lying");
if (sleepingPlayerNear(cat)) f.add("on_player");
}
if (cat.isHeadUp()) f.add("head_up");
} else if (e instanceof Panda pa) {
if (pa.isOnBack()) f.add("on_back");
} else if (e instanceof Bat bat) {
if (!bat.isAwake()) f.add("resting");
} else if (e instanceof Parrot pa) {
// ParrotModel.getPose: party > sitting > flying (= not on the ground) > standing.
if (pa.isDancing()) f.add("party");
else if (!pa.isSitting() && pa.isOnGround()) f.add("standing");
} else if (e instanceof Sniffer sn && sn.getState() == Sniffer.State.DIGGING) {
f.add("digging");
}
} catch (Throwable ignored) {
// API missing on this server version: render the flags found so far
}
return f.isEmpty() ? null : String.join("+", f);
}
/** Cat.isLyingOnTopOfSleepingPlayer: a sleeping player within the cat's block box inflated by 2. */
private static boolean sleepingPlayerNear(Entity cat) {
Location l = cat.getLocation();
org.bukkit.util.BoundingBox box = new org.bukkit.util.BoundingBox(
l.getBlockX() - 2,
l.getBlockY() - 2,
l.getBlockZ() - 2,
l.getBlockX() + 3,
l.getBlockY() + 3,
l.getBlockZ() + 3);
for (Entity p : cat.getWorld().getNearbyEntities(box, en -> en instanceof Player)) {
if (((Player) p).isSleeping()) return true;
}
return false;
}
/**
* Eye height in the STANDING pose (vanilla state.eyeHeight of a sleeper): {@code getEyeHeight(true)} returns the
* sleeping pose's 0.2 on Paper 26.x, so ask the server entity ({@code getEyeHeight(Pose.STANDING)}).
*/
private static double standingEyeHeight(LivingEntity le) {
try {
Object handle = le.getClass().getMethod("getHandle").invoke(le);
for (java.lang.reflect.Method m : handle.getClass().getMethods()) {
if (!m.getName().equals("getEyeHeight") || m.getParameterCount() != 1) continue;
Class<?> pose = m.getParameterTypes()[0];
if (!pose.isEnum()) continue;
for (Object c : pose.getEnumConstants()) {
if (((Enum<?>) c).name().equals("STANDING")) return ((Number) m.invoke(handle, c)).doubleValue();
}
}
} catch (ReflectiveOperationException | RuntimeException ignored) {
// fall through
}
return le.getEyeHeight(true);
}
/** Facing of the bed the entity sleeps in (LivingEntity.getBedOrientation = the bed block's FACING), or null. */
private static org.bukkit.block.BlockFace bedFacing(Entity e) {
org.bukkit.block.data.BlockData bd = e.getLocation().getBlock().getBlockData();
return bd instanceof org.bukkit.block.data.type.Bed bed ? bed.getFacing() : null;
}
/** Armor-stand part poses in degrees, keyed by the vanilla render-state field (dump rotation bindings). */
private static java.util.Map<String, float[]> armorStandPoses(ArmorStand as) {
java.util.Map<String, float[]> out = new java.util.HashMap<>();
out.put("headPose", deg(as.getHeadPose()));
out.put("bodyPose", deg(as.getBodyPose()));
out.put("leftArmPose", deg(as.getLeftArmPose()));
out.put("rightArmPose", deg(as.getRightArmPose()));
out.put("leftLegPose", deg(as.getLeftLegPose()));
out.put("rightLegPose", deg(as.getRightLegPose()));
return out;
}
private static float[] deg(org.bukkit.util.EulerAngle a) {
return new float[] {
(float) Math.toDegrees(a.getX()), (float) Math.toDegrees(a.getY()), (float) Math.toDegrees(a.getZ())
};
}
/** Variant key of a parrot sitting on the player's left/right shoulder, or null. */
@SuppressWarnings("deprecation")
private static String shoulderParrot(HumanEntity he, boolean left) {
try {
Entity p = left ? he.getShoulderEntityLeft() : he.getShoulderEntityRight();
return p instanceof Parrot parrot ? keyOf(parrot.getVariant()) : null;
} catch (Throwable t) {
return null;
}
}
/** Worn armor (4 slots) from a humanoid wearer; null when nothing is equipped. */
@@ -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]}]}]}]}},"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
+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");