import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.io.FileWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import net.minecraft.SharedConstants; import net.minecraft.client.model.Model; import net.minecraft.client.model.geom.EntityModelSet; import net.minecraft.client.model.geom.LayerDefinitions; import net.minecraft.client.model.geom.ModelLayerLocation; import net.minecraft.client.model.geom.ModelPart; import net.minecraft.client.model.geom.builders.CubeDefinition; import net.minecraft.client.model.geom.builders.LayerDefinition; import net.minecraft.client.model.geom.builders.PartDefinition; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.core.Direction; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.Bootstrap; import net.minecraft.world.entity.EntityType; /** * Dumps the vanilla Java entity models straight from an (unobfuscated, 26.x+) Minecraft CLIENT jar into * the CEM-template JSON layout PixelPics loads ({@code {"models": {name: {"model": ""}}}}). * *

Unlike the third-party CEM template set (rest geometry only), every entity model is instantiated * through its real renderer and posed by its real {@code setupAnim} with a default (idle, adult, not * aggressive) render state — so zombie arms are raised, spider legs splayed, alternate-state parts * (rolled-up armadillo, crossed/straight illager arms, chests) carry their vanilla visibility, and new * mobs of the jar's version are included automatically. * *

Output per entity type key: {@code } (main model) plus {@code /} for the * renderer's additional models and render-layer models (outer layers, armor sets, saddles, …). * Part coordinates are ABSOLUTE in the CEM frame (x negated, y up with the ground at 24, z kept); * every part carries its absolute pivot ({@code "pivot"}), so the jem is flagged {@code "absolute": true}. * *

Run (classpath = client jar + its libraries from the version json): * {@code java -cp tools/VanillaModelDump.java } */ public class VanillaModelDump { static final Map BAKED = new IdentityHashMap<>(); static Map roots; public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); roots = LayerDefinitions.createRoots(); EntityModelSet set = new EntityModelSet(roots) { @Override public ModelPart bakeLayer(ModelLayerLocation loc) { ModelPart p = super.bakeLayer(loc); BAKED.put(p, loc); return p; } }; EntityRendererProvider.Context ctx = new EntityRendererProvider.Context(null, null, null, null, null, set, null, null, null, null, null); Field provField = Class.forName("net.minecraft.client.renderer.entity.EntityRenderers") .getDeclaredField("PROVIDERS"); provField.setAccessible(true); @SuppressWarnings("unchecked") Map, EntityRendererProvider> providers = (Map, EntityRendererProvider>) provField.get(null); Map out = new TreeMap<>(); Map entities = new TreeMap<>(); List failed = new ArrayList<>(); for (Map.Entry, EntityRendererProvider> e : providers.entrySet()) { String key = BuiltInRegistries.ENTITY_TYPE.getKey(e.getKey()).getPath(); EntityRenderer r; try { r = e.getValue().create(ctx); } catch (Throwable t) { failed.add(key + " (renderer: " + t + ")"); continue; } Object state; try { state = r.createRenderState(); } catch (Throwable t) { state = null; } idleTweaks(state); Map> found = new LinkedHashMap<>(); Map textures = new LinkedHashMap<>(); // The renderer's current model is the main one, whatever field else also holds it (bigModel, …). if (fieldValue(r, "model") instanceof Model current) found.put("model", current); collectModels(r, "", found, 0); Object layers = fieldValue(r, "layers"); if (layers instanceof List ls) { for (Object layer : ls) collectLayer(layer, found, textures); } 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); entities.put(key, info); emit(key, found, textures, state, out, failed); } // Players & mannequins use the avatar renderers (one per skin model type: wide/slim). Map avatars = (Map) Class.forName("net.minecraft.client.renderer.entity.EntityRenderers") .getMethod("createAvatarRenderers", EntityRendererProvider.Context.class) .invoke(null, ctx); for (Map.Entry a : avatars.entrySet()) { EntityRenderer r = (EntityRenderer) a.getValue(); String key = String.valueOf(a.getKey()).toLowerCase().equals("slim") ? "player_slim" : "player"; Map> found = new LinkedHashMap<>(); Map textures = new LinkedHashMap<>(); collectModels(r, "", found, 0); Object layers = fieldValue(r, "layers"); if (layers instanceof List ls) { for (Object layer : ls) collectLayer(layer, found, textures); } JsonObject info = new JsonObject(); addLayerInfo(info, layers, r); addTransform(info, r); Model main = found.get("model"); if (main != null) addPoseVariants(info, main, r); entities.put(key, info); emit(key, found, textures, r.createRenderState(), out, failed); } // Every layer definition in its rest pose as "layer/#" (special item models such as the // shield, block-entity models, …) — same absolute CEM frame as the entity models. for (Map.Entry le : roots.entrySet()) { String name = "layer/" + le.getKey().model().getPath() + "#" + le.getKey().layer(); try { ModelPart baked = le.getValue().bakeRoot(); PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition) fieldValue(le.getValue(), "mesh")) .getRoot(); Object mat = fieldValue(le.getValue(), "material"); JsonObject jem = new JsonObject(); JsonArray ts = new JsonArray(); ts.add((int) fieldValue(mat, "xTexSize")); ts.add((int) fieldValue(mat, "yTexSize")); jem.add("textureSize", ts); jem.addProperty("absolute", true); JsonArray parts = new JsonArray(); parts.add(part("root", rootDef, baked, 0, 0, 0)); jem.add("models", parts); JsonObject entry = new JsonObject(); entry.addProperty("model", new Gson().toJson(jem)); out.putIfAbsent(name, entry); } catch (Throwable t) { failed.add(name + " (layer: " + t + ")"); } } // Block-entity models posed by their own setupAnim with the static state the vanilla // BlockEntityRenderer uses ("posed/"): skulls at animation 0 (dragon jaw, piglin ears), banner // flags at wave phase 0 (item/static pose), the lectern's open book (LecternRenderer.BOOK_STATE). // Rest-pose-only models (chest, bell, shulker box, pot, conduit) use "layer/" directly. posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.DRAGON_SKULL, m -> new net.minecraft.client.model.object.skull.DragonHeadModel(m), new net.minecraft.client.model.object.skull.SkullModelBase.State()); posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.PIGLIN_HEAD, m -> new net.minecraft.client.model.object.skull.PiglinHeadModel(m), new net.minecraft.client.model.object.skull.SkullModelBase.State()); posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.STANDING_BANNER_FLAG, m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F); posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.WALL_BANNER_FLAG, m -> new net.minecraft.client.model.object.banner.BannerFlagModel(m), 0.0F); try { Field bookState = Class.forName("net.minecraft.client.renderer.blockentity.LecternRenderer") .getDeclaredField("BOOK_STATE"); bookState.setAccessible(true); posedBlockEntity(out, failed, set, net.minecraft.client.model.geom.ModelLayers.BOOK, m -> new net.minecraft.client.model.object.book.BookModel(m), bookState.get(null)); } catch (ReflectiveOperationException e) { failed.add("posed/book (" + e + ")"); } JsonObject root = new JsonObject(); root.addProperty("version", SharedConstants.getCurrentVersion().name()); JsonObject models = new JsonObject(); out.forEach(models::add); root.add("models", models); JsonObject ents = new JsonObject(); entities.forEach(ents::add); root.add("entities", ents); root.add("variants", registryVariants()); try (FileWriter w = new FileWriter(args.length > 0 ? args[0] : "vanilla_entity_models.json")) { new GsonBuilder().create().toJson(root, w); } System.out.println("models: " + out.size()); for (String f : failed) System.out.println("FAILED " + f); } /** * The renderer's layers: {@code layers: {LayerClass: drawnByDefault}} — whether the layer submits anything * for the default (idle, adult, untamed, unequipped, …) render state, measured by running its real * {@code submit} against a recording collector — plus {@code humanoidArmor: true} when one of them is a * HumanoidArmorLayer (the entity wears humanoid armor). */ static void addLayerInfo(JsonObject info, Object layers, EntityRenderer r) { if (!(layers instanceof List ls) || ls.isEmpty()) return; JsonObject out = new JsonObject(); for (Object layer : ls) { Class lc = layer.getClass(); while (lc.getSimpleName().isEmpty()) lc = lc.getSuperclass(); // anonymous layer -> its base class String name = lc.getSimpleName(); if (layer instanceof net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer) { info.addProperty("humanoidArmor", true); } Boolean drawn = layerDrawsByDefault(layer, r); if (drawn == null) { System.out.println("layer " + name + " (" + r.getClass().getSimpleName() + "): submit failed"); drawn = false; } if (!out.has(name) || drawn) out.addProperty(name, drawn); } info.add("layers", out); } /** True/false = the layer submits / doesn't submit geometry for the default state; null = submit threw. */ static Boolean layerDrawsByDefault(Object layer, EntityRenderer r) { Object state; try { state = r.createRenderState(); } catch (Throwable t) { return null; } idleTweaks(state); boolean[] submitted = {false}; Object proxy = recordingCollector(m -> { if (m.getName().startsWith("submit")) submitted[0] = true; }); Method submit = null; for (Method m : layer.getClass().getMethods()) { if (m.getName().equals("submit") && m.getParameterCount() == 6 && !m.isBridge()) submit = m; } if (submit == null) return null; try { submit.invoke(layer, new com.mojang.blaze3d.vertex.PoseStack(), proxy, 0xF000F0, state, 0f, 0f); } catch (Throwable t) { return submitted[0] ? Boolean.TRUE : null; } return submitted[0]; } /** A SubmitNodeCollector proxy that reports every call and otherwise does nothing. */ static Object recordingCollector(java.util.function.Consumer onCall) { Class collector; try { collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector"); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } return java.lang.reflect.Proxy.newProxyInstance( VanillaModelDump.class.getClassLoader(), new Class[] {collector}, (px, m, a) -> { if (m.getName().equals("order")) return px; onCall.accept(m); Class rt = m.getReturnType(); if (rt == boolean.class) return false; if (rt.isPrimitive() && rt != void.class) return 0; return null; }); } /** * The built-in data-driven mob variants ({@code data/minecraft/_variant/.json}, cat/wolf/frog/ * cow/pig/chicken/zombie_nautilus …) with their client assets: {@code variants: {: {: {asset_id, * baby_asset_id, assets{wild,tame,angry}, baby_assets{…}, model}}}} (spawn conditions dropped). */ static JsonObject registryVariants() throws java.io.IOException { JsonObject out = new JsonObject(); java.net.URL jar = SharedConstants.class.getProtectionDomain().getCodeSource().getLocation(); java.util.regex.Pattern p = java.util.regex.Pattern.compile("data/minecraft/([a-z0-9_]+)_variant/([a-z0-9_/.-]+)\\.json"); try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(new java.io.File(jar.getPath()))) { List entries = zf.stream().sorted(java.util.Comparator.comparing(java.util.zip.ZipEntry::getName)).toList(); for (java.util.zip.ZipEntry ze : entries) { java.util.regex.Matcher m = p.matcher(ze.getName()); if (!m.matches()) continue; String type = m.group(1); if (type.endsWith("_sound") || type.equals("painting")) continue; // not mob textures JsonObject v; try (java.io.Reader rd = new java.io.InputStreamReader(zf.getInputStream(ze))) { v = com.google.gson.JsonParser.parseReader(rd).getAsJsonObject(); } JsonObject keep = new JsonObject(); for (String k : new String[] {"asset_id", "baby_asset_id", "assets", "baby_assets", "model"}) { if (v.has(k)) keep.add(k, v.get(k)); } if (keep.isEmpty()) continue; if (!out.has(type)) out.add(type, new JsonObject()); out.getAsJsonObject(type).add(m.group(2), keep); } } return out; } /** One block-entity model posed with {@code state} (see main), stored as {@code posed/#}. */ static void posedBlockEntity( Map out, List failed, EntityModelSet set, ModelLayerLocation loc, java.util.function.Function> factory, Object state) { String name = "posed/" + loc.model().getPath() + "#" + loc.layer(); try { JsonObject jem = dump(factory.apply(set.bakeLayer(loc)), state, name, failed); if (jem == null) return; JsonObject entry = new JsonObject(); entry.addProperty("model", new Gson().toJson(jem)); out.put(name, entry); } catch (Throwable t) { failed.add(name + " (" + t + ")"); } } /** * Small, name-based tweaks so the default render state shows the typical idle look: illagers stand * with crossed arms (their peaceful pose) instead of the empty-handed "neutral" pose. */ static void idleTweaks(Object state) { if (state == null) return; for (String sizeField : new String[] {"size"}) { try { Field f = state.getClass().getField(sizeField); if (f.getType() == int.class && f.getInt(state) == 0) f.setInt(state, 1); if (f.getType() == float.class && f.getFloat(state) == 0) f.setFloat(state, 1); } catch (Throwable ignored) { } } try { Field f = state.getClass().getField("armPose"); if (f.getType().isEnum()) { for (Object c : f.getType().getEnumConstants()) if (((Enum) c).name().equals("CROSSED")) f.set(state, c); } } catch (Throwable ignored) { } } /** Writes every collected model of one entity type; adult/normal/plain "model" fields become the main model. */ static void emit( String key, Map> found, Map textures, Object state, Map out, List failed) { for (Map.Entry> m : found.entrySet()) { String f = m.getKey(); String name = switch (f) { case "model", "adultModel", "normalModel" -> key; case "babyModel" -> key + "/baby"; default -> key + "/" + f; }; if (out.containsKey(name)) continue; JsonObject jem = dump(m.getValue(), state, name, failed); if (jem != null) { String tex = textures.get(f); if (tex != null) jem.addProperty("texture", tex); JsonObject entry = new JsonObject(); entry.addProperty("model", new Gson().toJson(jem)); out.put(name, entry); } } } /** * A render layer's models (prefixed by the layer class; equipment layers by their equipment layer type, * e.g. {@code equip.horse_saddle}) plus the texture the layer draws them with, when it holds one. */ static void collectLayer(Object layer, Map> found, Map textures) { String prefix = layer.getClass().getSimpleName() + "."; Object type = fieldValue(layer, "layer"); if (type instanceof Enum en) prefix = "equip." + en.name().toLowerCase() + "."; Map> mine = new LinkedHashMap<>(); collectModels(layer, prefix, mine, 0); // Texture identifiers held by the layer (static constants or instance fields): baby vs adult. String adult = null, baby = null; for (Class c = layer.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field fl : c.getDeclaredFields()) { if (!fl.getType().getSimpleName().equals("Identifier")) continue; try { fl.setAccessible(true); Object v = fl.get(Modifier.isStatic(fl.getModifiers()) ? null : layer); if (v == null) continue; String path = v.toString().replaceFirst("^minecraft:", "") .replaceFirst("^textures/", "") .replaceFirst("\\.png$", ""); if (fl.getName().toLowerCase().contains("baby")) { if (baby == null) baby = path; } else if (adult == null) adult = path; } catch (Throwable ignored) { } } } for (Map.Entry> m : mine.entrySet()) { if (found.containsValue(m.getValue())) continue; found.put(m.getKey(), m.getValue()); String tex = m.getKey().toLowerCase().contains("baby") ? baby : adult; if (tex != null) textures.put(m.getKey(), tex); } } /** * Runs the renderer's real {@code submit} (yaw 0, pitch 0) against a recording collector and returns the * pose matrix (column-major 4x4, Java model units -> entity-local blocks) of its first model submission: * the renderer's own translate/rotate/scale (end-crystal float, minecart offsets, wither x2, …), so no * per-entity placement table is needed. */ /** * Stores the renderer transform at yaw=pitch=0 ({@code transform}, column-major 4x4) plus how yaw and * pitch enter it, measured by re-rendering at yaw 90 / pitch 90: {@code yawSign} (+1/-1, 0 = ignores yaw) * and {@code pitchAxis} (world-frame axis for +pitch; absent = ignores pitch). */ static void addTransform(JsonObject info, EntityRenderer r) { float[] t0 = captureTransform(r, 0, 0); if (t0 == null) return; JsonArray ta = new JsonArray(); for (float v : t0) ta.add(Math.round(v * 100000f) / 100000f); info.add("transform", ta); org.joml.Matrix3f inv0 = new org.joml.Matrix4f().set(t0).get3x3(new org.joml.Matrix3f()).invert(); float[] ty = captureTransform(r, 90, 0); if (ty != null) { org.joml.Matrix3f rel = new org.joml.Matrix4f().set(ty).get3x3(new org.joml.Matrix3f()).mul(inv0); // rel ≈ RotY(±90): m20 (column 0, row 2) = -sin(angle) float sinA = -rel.m02; info.addProperty("yawSign", Math.abs(sinA) < 0.5f ? 0 : (sinA > 0 ? 1 : -1)); } float[] tp = captureTransform(r, 0, 90); if (tp != null) { org.joml.Matrix3f rel = new org.joml.Matrix4f().set(tp).get3x3(new org.joml.Matrix3f()).mul(inv0); // axis of a 90° rotation: the antisymmetric part (R - R^T)/2 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) info.add("pitchAxis", arr(ax / len, ay / len, az / len)); } } /** * 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). */ @SuppressWarnings({"unchecked", "rawtypes"}) static void addPoseVariants(JsonObject info, Model model, EntityRenderer r) { Object probe; try { probe = r.createRenderState(); } catch (Throwable t) { return; } // Toggle groups: {name, field, value} entries sharing a name are applied together. List> toggles = new ArrayList<>(); List passenger = new ArrayList<>(); 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"})); // 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])) toggles.add(java.util.Collections.singletonList(new String[] {f[0], f[1], "true"})); } List> combos = new ArrayList<>(); for (int mask = 1; mask < (1 << toggles.size()); mask++) { List c = new ArrayList<>(); for (int i = 0; i < toggles.size(); i++) if ((mask & (1 << i)) != 0) c.addAll(toggles.get(i)); combos.add(c); } // Illagers: the arm pose enum replaces the crossed arms (crossbow hold, attacking, …). if (hasField(probe, "armPose")) { for (String v : new String[] {"CROSSBOW_HOLD", "ATTACKING", "NEUTRAL", "BOW_AND_ARROW"}) { List c = new ArrayList<>(); c.add(new String[] {"arms_" + v.toLowerCase(), "armPose", v}); combos.add(c); if (!passenger.isEmpty()) { List cr = new ArrayList<>(c); cr.addAll(passenger); combos.add(cr); } } } if (combos.isEmpty()) return; Map base = poseOf(model, freshState(r, List.of())); if (base == null) return; JsonObject poses = new JsonObject(); for (List c : combos) { Map v = poseOf(model, freshState(r, c)); if (v == null) continue; JsonObject diff = new JsonObject(); for (Map.Entry 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); } 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())); } /** Whether the state field is an enum (HumanoidModel.ArmPose), not e.g. an armor stand's Rotations. */ static boolean probeEnum(Object o, String f) { try { return o.getClass().getField(f).getType().isEnum(); } catch (NoSuchFieldException e) { return false; } } static boolean hasField(Object o, String f) { try { o.getClass().getField(f); return true; } catch (NoSuchFieldException e) { return false; } } @SuppressWarnings({"unchecked", "rawtypes"}) static Object freshState(EntityRenderer r, List sets) { 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])); } return st; } catch (Throwable t) { return null; } } /** setupAnim with the state, then every part's absolute CEM pivot, CEM rotation and visibility, by name. */ @SuppressWarnings({"unchecked", "rawtypes"}) static Map poseOf(Model model, Object state) { if (state == null) return null; try { ((Model) model).setupAnim(state); } catch (Throwable t) { return null; } Map out = new LinkedHashMap<>(); collectPose(model.root(), "root", 0, 0, 0, out); return out; } @SuppressWarnings("unchecked") static void collectPose(ModelPart p, String name, float px, float py, float pz, Map out) { float ax = px + p.x, ay = py + p.y, az = pz + p.z; 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 }); Map children = (Map) fieldValue(p, "children"); if (children != null) children.forEach((n, c) -> collectPose(c, n, ax, ay, az, out)); } static void setFloat(Object o, String field, float v) { try { Field f = o.getClass().getField(field); if (f.getType() == float.class) f.setFloat(o, v); } catch (Throwable ignored) { } } static float[] captureTransform(EntityRenderer r, float yaw, float pitch) { Object state; try { state = r.createRenderState(); } catch (Throwable t) { return null; } idleTweaks(state); setFloat(state, "bodyRot", yaw); setFloat(state, "yRot", yaw); setFloat(state, "xRot", pitch); float[][] first = new float[1][]; Class collector; try { collector = Class.forName("net.minecraft.client.renderer.SubmitNodeCollector"); } catch (ClassNotFoundException e) { return null; } Object proxy = java.lang.reflect.Proxy.newProxyInstance( VanillaModelDump.class.getClassLoader(), new Class[] {collector}, (px, m, a) -> { if (m.getName().equals("order")) return px; if (a != null && first[0] == null && m.getName().startsWith("submitModel")) { for (Object arg : a) { if (arg instanceof com.mojang.blaze3d.vertex.PoseStack ps) { first[0] = ps.last().pose().get(new float[16]); break; } } } Class rt = m.getReturnType(); if (rt == boolean.class) return false; if (rt.isPrimitive() && rt != void.class) return 0; return null; }); try { Method submit = null; for (Method m : r.getClass().getMethods()) { if (m.getName().equals("submit") && m.getParameterCount() == 4 && !m.isBridge()) submit = m; } if (submit == null) return null; submit.invoke(r, state, new com.mojang.blaze3d.vertex.PoseStack(), proxy, null); } catch (Throwable ignored) { // partial submissions still count (layers may fail on the null context) } return first[0]; } /** Render scale (the renderer's own {@code scale(state, poseStack)}) and default adult/baby textures. */ static JsonObject entityInfo(EntityRenderer r, Object state) { JsonObject o = new JsonObject(); if (state == null) return o; try { Method m = null; for (Class c = r.getClass(); c != null && m == null; c = c.getSuperclass()) { for (Method dm : c.getDeclaredMethods()) { if (dm.getName().equals("scale") && dm.getParameterCount() == 2 && dm.getParameterTypes()[1].getSimpleName().equals("PoseStack") && !dm.isBridge()) { m = dm; break; } } } if (m != null) { m.setAccessible(true); com.mojang.blaze3d.vertex.PoseStack ps = new com.mojang.blaze3d.vertex.PoseStack(); m.invoke(r, state, ps); float sc = ps.last().pose().getScale(new org.joml.Vector3f()).y; if (sc > 0.05f && Math.abs(sc - 1) > 1e-3) o.addProperty("scale", Math.round(sc * 1000f) / 1000f); } } catch (Throwable ignored) { } for (boolean baby : new boolean[] {false, true}) { try { Object st = r.createRenderState(); try { st.getClass().getField("isBaby").setBoolean(st, baby); } catch (NoSuchFieldException e) { if (baby) continue; } Method gt = null; for (Class c = r.getClass(); c != null && gt == null; c = c.getSuperclass()) { for (Method dm : c.getDeclaredMethods()) if (dm.getName().equals("getTextureLocation") && dm.getParameterCount() == 1 && !dm.isBridge()) gt = dm; } if (gt == null) break; gt.setAccessible(true); Object id = gt.invoke(r, st); if (id != null) { String path = id.toString().replaceFirst("^minecraft:", "") .replaceFirst("^textures/", "") .replaceFirst("\\.png$", ""); o.addProperty(baby ? "babyTexture" : "texture", path); } } catch (Throwable ignored) { } } return o; } /** Finds Model instances in the object's fields (also inside records / maps / ArmorModelSet-like holders). */ static void collectModels(Object o, String prefix, Map> found, int depth) { if (o == null || depth > 2) return; for (Class c = o.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; Object v; try { f.setAccessible(true); v = f.get(o); } catch (Throwable t) { continue; } if (v == null) continue; String n = prefix + f.getName(); if (v instanceof Model m) { if (!found.containsValue(m)) found.put(n, m); } else if (v instanceof Map map) { for (Map.Entry me : map.entrySet()) { String mk = n + "." + String.valueOf(me.getKey()).toLowerCase(); if (me.getValue() instanceof Model m) { if (!found.containsValue(m)) found.put(mk, m); } else if (me.getValue() != null) { collectModels(me.getValue(), mk + ".", found, depth + 1); // e.g. adult/baby pairs } } } else if (v.getClass().isRecord() || v.getClass().getSimpleName().endsWith("ModelSet")) { collectModels(v, n + ".", found, depth + 1); } } } } /** Path of child names from {@code from} down to the identical part {@code target}, or null. */ @SuppressWarnings("unchecked") static List findPath(ModelPart from, ModelPart target, List path) { if (from == target) return path; Map children = (Map) fieldValue(from, "children"); if (children == null) return null; for (Map.Entry c : children.entrySet()) { List p = new ArrayList<>(path); p.add(c.getKey()); List r = findPath(c.getValue(), target, p); if (r != null) return r; } return null; } static Object fieldValue(Object o, String name) { for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { try { Field f = c.getDeclaredField(name); f.setAccessible(true); return f.get(o); } catch (NoSuchFieldException ignored) { } catch (Throwable t) { return null; } } return null; } @SuppressWarnings({"unchecked", "rawtypes"}) static JsonObject dump(Model model, Object state, String name, List failed) { ModelPart root = model.root(); ModelLayerLocation loc = BAKED.get(root); List subPath = List.of(); if (loc == null) { // Some models keep a CHILD of the baked layer root as their root (e.g. allay, vex, frog). for (Map.Entry b : BAKED.entrySet()) { List path = findPath((ModelPart) b.getKey(), root, new ArrayList<>()); if (path != null) { loc = b.getValue(); subPath = path; break; } } } if (loc == null) { failed.add(name + " (no layer)"); return null; } // Pose with the renderer's default state; fall back to a fresh state of the model's own type. boolean posed = false; if (state != null) { try { ((Model) model).setupAnim(state); posed = true; } catch (Throwable t) { // layer model expecting another state type } } if (!posed) { for (Method m : model.getClass().getMethods()) { if (!m.getName().equals("setupAnim") || m.getParameterCount() != 1) continue; try { Object st = m.getParameterTypes()[0].getDeclaredConstructor().newInstance(); m.invoke(model, st); posed = true; break; } catch (Throwable ignored) { } } } if (!posed) failed.add(name + " (rest pose: setupAnim failed)"); LayerDefinition def = roots.get(loc); PartDefinition rootDef = ((net.minecraft.client.model.geom.builders.MeshDefinition) fieldValue(def, "mesh")) .getRoot(); for (String seg : subPath) rootDef = rootDef.getChild(seg); Object mat = fieldValue(def, "material"); int tw = (int) fieldValue(mat, "xTexSize"), th = (int) fieldValue(mat, "yTexSize"); JsonObject jem = new JsonObject(); JsonArray ts = new JsonArray(); ts.add(tw); ts.add(th); jem.add("textureSize", ts); jem.addProperty("absolute", true); jem.addProperty("layer", loc.model().getPath() + "#" + loc.layer()); JsonArray parts = new JsonArray(); parts.add(part(subPath.isEmpty() ? "root" : subPath.getLast(), rootDef, root, 0, 0, 0)); jem.add("models", parts); return jem; } /** One part: absolute pivot = parent pivot + this part's (posed) offset; boxes absolute. */ @SuppressWarnings("unchecked") static JsonObject part(String name, PartDefinition def, ModelPart p, float px, float py, float pz) { float ax = px + p.x, ay = py + p.y, az = pz + p.z; JsonObject o = new JsonObject(); o.addProperty("part", name); o.add("pivot", arr(-ax, 24 - ay, az)); // CEM frame = Java rotated 180° about Z: x/y rotations flip sign, z keeps it. o.add("rotate", arr( (float) Math.toDegrees(-p.xRot), (float) Math.toDegrees(-p.yRot), (float) Math.toDegrees(p.zRot))); if (p.xScale != 1 || p.yScale != 1 || p.zScale != 1) o.add("scale", arr(p.xScale, p.yScale, p.zScale)); if (!p.visible) o.addProperty("invisible", true); if (p.skipDraw) o.addProperty("skipDraw", true); JsonArray boxes = new JsonArray(); for (CubeDefinition c : (List) fieldValue(def, "cubes")) { org.joml.Vector3fc org = (org.joml.Vector3fc) fieldValue(c, "origin"); org.joml.Vector3fc dim = (org.joml.Vector3fc) fieldValue(c, "dimensions"); Object grow = fieldValue(c, "grow"); float gx = (float) fieldValue(grow, "growX"), gy = (float) fieldValue(grow, "growY"), gz = (float) fieldValue(grow, "growZ"); Object tex = fieldValue(c, "texCoord"), texScale = fieldValue(c, "texScale"); boolean mirror = (boolean) fieldValue(c, "mirror"); Set faces = (Set) fieldValue(c, "visibleFaces"); float x0 = ax + org.x(), y0 = ay + org.y(), z0 = az + org.z(); JsonObject b = new JsonObject(); b.add("coordinates", arr(-(x0 + dim.x()), 24 - (y0 + dim.y()), z0, dim.x(), dim.y(), dim.z())); b.add("textureOffset", arr((float) fieldValue(tex, "u"), (float) fieldValue(tex, "v"))); float su = (float) fieldValue(texScale, "u"), sv = (float) fieldValue(texScale, "v"); if (su != 1 || sv != 1) b.add("textureScale", arr(su, sv)); if (gx == gy && gy == gz) { if (gx != 0) b.addProperty("sizeAdd", gx); } else { b.add("sizeAddXYZ", arr(gx, gy, gz)); } if (mirror) b.addProperty("mirrorTexture", "u"); if (faces != null && faces.size() < 6) { JsonArray hide = new JsonArray(); // Named in the CEM frame (x and y flipped): Java east is CEM west, Java up is CEM down. for (Direction d : Direction.values()) if (!faces.contains(d)) hide.add( switch (d) { case EAST -> "west"; case WEST -> "east"; case UP -> "down"; case DOWN -> "up"; default -> d.getName(); }); b.add("hiddenFaces", hide); } boxes.add(b); } if (!boxes.isEmpty()) o.add("boxes", boxes); JsonArray subs = new JsonArray(); for (Map.Entry ce : def.getChildren()) { ModelPart child = p.getChild(ce.getKey()); subs.add(part(ce.getKey(), ce.getValue(), child, ax, ay, az)); } if (!subs.isEmpty()) o.add("submodels", subs); return o; } static JsonArray arr(float... v) { JsonArray a = new JsonArray(); for (float f : v) a.add(Math.round(f * 10000f) / 10000f); return a; } }