@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
291 lines (290 loc) • 15.7 kB
JavaScript
;
/**
* VanillaGeometryTransforms
*
* This module provides transforms for vanilla Minecraft geometry files that need
* manual corrections because Minecraft's rendering code has hardcoded optimizations
* that affect how certain models appear.
*
* The Minecraft game engine contains special rendering logic for certain entities
* that isn't represented in the geometry files themselves. This transform system
* allows MCTools to replicate those visual adjustments so models render correctly.
*
* Last Updated: December 2025
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findGeometryTransform = findGeometryTransform;
exports.applyGeometryTransforms = applyGeometryTransforms;
exports.getRegisteredTransformPatterns = getRegisteredTransformPatterns;
const Log_1 = __importDefault(require("../core/Log"));
/**
* Registry of all vanilla geometry transforms.
*
* When adding new transforms:
* 1. Document why Minecraft renders this model differently
* 2. Specify the exact geometry patterns that need correction
* 3. Apply minimal transforms to achieve correct appearance
*
* IMPORTANT: Most vanilla entities now use v2/v3 geometry formats that have correct
* cube-level rotations and don't need bone-level transforms. Only add transforms for
* specific legacy geometry IDs that are still actively used and have issues.
*
* Geometry format evolution:
* - Legacy (v1.8 format): Uses bind_pose_rotation on bones, which affects child bones
* and requires Minecraft's renderer to apply hardcoded compensations
* - Modern (v2/v3 format): Uses per-cube rotation, which doesn't affect bone hierarchy
* and renders correctly without special handling
*
* Entity geometry usage (as of Jan 2026):
* - cow.entity.json uses geometry.cow.v2 (modern format, no transform needed)
* - pig.entity.json uses geometry.pig.v3 (modern format, no transform needed)
* - sheep.entity.json uses geometry.sheep.v1.8 (legacy format, but has bind_pose_rotation)
* - cat.entity.json uses geometry.cat (legacy v1.21.0 format — body cube is modeled
* vertically [4,16,6] with NO rotation. Minecraft applies a hardcoded 90° X rotation.)
* - wolf.entity.json uses geometry.wolf (legacy flat hierarchy — body and upperBody
* cubes modeled vertically, all bones root-level with no parent chain.)
* - fox.entity.json uses geometry.fox (hierarchical — body cube [6,11,6] modeled
* vertically with children parented to body. Needs 90° X on body.)
* - ocelot.entity.json uses geometry.ocelot.v1.8 (identical structure to geometry.cat.)
* - chicken.entity.json uses geometry.chicken.v1.12 (modern, has per-cube rotation.)
*/
const VANILLA_GEOMETRY_TRANSFORMS = [
// ── Cat / Ocelot ──────────────────────────────────────────────────────
// geometry.cat and geometry.ocelot.v1.8 model the body cube as a tall
// vertical column [4,16,6] but Minecraft's engine hardcodes a 90° X
// rotation on the body cube only (matching cow.v2's per-cube approach).
//
// Comparison with geometry.ocelot v1.0 (which has bind_pose_rotation):
// v1.0: body pivot [0,12,-10], cube [-2,-7,-18], bind_pose_rotation [90,0,0]
// v1.8: body pivot [0,7,1], cube [-2,-1,-2], NO rotation
//
// All child bones (head, legs, tail) already have correct world-space
// positions — only the body cube needs rotation. Tail cubes are vertical
// at rest; the curve comes from animation, not static geometry.
{
geometryPatterns: ["geometry.cat", "geometry.ocelot.v1.8"],
reason: "Body cube modeled vertically [4,16,6] — Minecraft hardcodes per-cube 90° X rotation (matches cow.v2 convention)",
boneTransforms: [
{ boneName: "body", setCubeRotation: [90, 0, 0] },
],
},
// ── Sheep ─────────────────────────────────────────────────────────────
// Sheep geometry has bind_pose_rotation [90,0,0] on the body bone, with
// legs as children. The sheep's leg cube positions are in WORLD coordinates
// (Y=0 to Y=12, ground to hip). Applying the body's 90° rotation to these
// world-space legs scatters them.
//
// Fix: detach the legs from the body so they render at their world-space
// positions. The body's bind_pose_rotation correctly rotates its own cubes;
// the head is already a root bone (no parent).
{
geometryPatterns: ["geometry.sheep.sheared.v1.8", "geometry.sheep.v1.8*"],
reason: "Sheep legs are in world coordinates — detach from rotated body bone",
boneTransforms: [
{ boneName: "leg0", removeParent: true },
{ boneName: "leg1", removeParent: true },
{ boneName: "leg2", removeParent: true },
{ boneName: "leg3", removeParent: true },
],
},
// ── Turtle ────────────────────────────────────────────────────────────
// The turtle body has bind_pose_rotation [90,0,0] but ALL child bone cubes
// (head, flippers) are in world coordinates, not the body's rotated local
// space. The body rotation correctly orients the shell, but applying it to
// children pushes the head up through the shell and scatters the flippers.
//
// Fix: detach head and flippers so they render at world coordinates.
// The head at Y=1-6, Z=-13 to -7 naturally sits in front of the shell.
// Back flippers need Z-correction because the shell is rendered through
// the rotation node while detached bones use Babylon's Z-negation.
{
geometryPatterns: ["geometry.turtle", "geometry.turtle.*"],
reason: "Turtle head/flippers are in world coordinates — detach from rotated body",
boneTransforms: [
{ boneName: "head", removeParent: true },
{ boneName: "leg0", removeParent: true, addCubeOriginOffset: [0, 0, -27] },
{ boneName: "leg1", removeParent: true, addCubeOriginOffset: [0, 0, -27] },
{ boneName: "leg2", removeParent: true, addCubeOriginOffset: [0, 0, -23] },
{ boneName: "leg3", removeParent: true, addCubeOriginOffset: [0, 0, -23] },
],
},
// ── Enderman ──────────────────────────────────────────────────────────
// The enderman geometry defines bones at "animation-ready" positions that
// Minecraft corrects via `animation.enderman.base_pose` (always-on loop).
// Without the base_pose offsets, the head is inside the body, the hat
// floats detached above, arms overlap the body, and legs clip underground.
//
// base_pose offsets (from animation.enderman.base_pose):
// body: position [0, +11, 0] — raises entire model
// head: position [0, 0, 0] — moves with body (+11) via bone hierarchy
// hat: position [0, 0, 0] — moves with body via head (+11) via hierarchy
// rightArm: position [-4, 0, 0] — spread outward, moves with body (+11)
// leftArm: position [+4, 0, 0] — spread outward, moves with body (+11)
// rightLeg: position [0, -5, 0] — moves with body (+11), then -5 = net +6
// leftLeg: position [0, -5, 0] — moves with body (+11), then -5 = net +6
//
// Our renderer positions cubes at world coordinates (not through bone hierarchy),
// so the head doesn't automatically follow the body's offset. Additionally, the
// head pivot (Y=24) is 14 units below the body pivot (Y=38). In Minecraft's bone
// hierarchy rendering, the head ends up inside the body — it's the look_at_target
// animation that tilts the head up to sit on the shoulders. For our static render,
// we move the head to sit on top of the body (body top = Y=38 +11 = Y=49).
//
// Head total offset: +11 (body base_pose) +14 (lift to body top) = +25
// Hat stays at +11 because its geometry origin (Y=37.5) is already designed to
// surround the head at the body-top position (Y=49→57, hat Y=48.5→56.5).
{
geometryPatterns: ["geometry.enderman*"],
reason: "Replicates animation.enderman.base_pose offsets + head-to-top lift",
boneTransforms: [
{ boneName: "body", addCubeOriginOffset: [0, 11, 0] },
{ boneName: "head", addCubeOriginOffset: [0, 25, 0] },
{ boneName: "hat", addCubeOriginOffset: [0, 11, 0] },
{ boneName: "rightArm", addCubeOriginOffset: [-4, 11, 0] },
{ boneName: "leftArm", addCubeOriginOffset: [4, 11, 0] },
{ boneName: "rightLeg", addCubeOriginOffset: [0, 6, 0] },
{ boneName: "leftLeg", addCubeOriginOffset: [0, 6, 0] },
],
},
];
/**
* Checks if a geometry identifier matches a pattern.
* Supports simple wildcards: "*" matches any characters
*/
function matchesPattern(geometryId, pattern) {
// Normalize both to lowercase for comparison
const normalizedId = geometryId.toLowerCase();
const normalizedPattern = pattern.toLowerCase();
// Simple wildcard matching
if (normalizedPattern.includes("*")) {
// Convert pattern to regex: escape special regex characters, then replace * with .*
// Must escape backslash first, then other special characters
const regexPattern = normalizedPattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*");
const regex = new RegExp("^" + regexPattern + "$");
return regex.test(normalizedId);
}
return normalizedId === normalizedPattern;
}
/**
* Finds the transform configuration for a given geometry identifier
*/
function findGeometryTransform(geometryId) {
for (const transform of VANILLA_GEOMETRY_TRANSFORMS) {
for (const pattern of transform.geometryPatterns) {
if (matchesPattern(geometryId, pattern)) {
Log_1.default.verbose(`VanillaGeometryTransforms: Found transform for ${geometryId} (pattern: ${pattern})`);
return transform;
}
}
}
return undefined;
}
/**
* Applies transforms to a geometry definition.
* Returns a deep copy with transforms applied - does not modify the original.
*
* @param geometry The original geometry definition
* @param geometryId The geometry identifier (e.g., "geometry.cow.v1.8")
* @returns A transformed copy of the geometry, or the original if no transforms apply
*/
function applyGeometryTransforms(geometry, geometryId) {
const transformConfig = findGeometryTransform(geometryId);
if (!transformConfig) {
return geometry;
}
Log_1.default.verbose(`VanillaGeometryTransforms: Applying transforms to ${geometryId}: ${transformConfig.reason}`);
// Deep clone the geometry to avoid modifying the original
const transformed = JSON.parse(JSON.stringify(geometry));
if (!transformed.bones) {
return transformed;
}
// Apply each bone transform
for (const boneTransform of transformConfig.boneTransforms) {
for (const bone of transformed.bones) {
// Check if this transform applies to this bone
const matches = boneTransform.boneName === "*" || bone.name === boneTransform.boneName;
if (!matches) {
continue;
}
Log_1.default.verbose(`VanillaGeometryTransforms: Transforming bone "${bone.name}"`);
// Apply bind_pose_rotation modifications
if (boneTransform.setBindPoseRotation !== undefined) {
bone.bind_pose_rotation = [...boneTransform.setBindPoseRotation];
Log_1.default.verbose(` - Set bind_pose_rotation to [${bone.bind_pose_rotation.join(", ")}]`);
}
if (boneTransform.addBindPoseRotation !== undefined) {
const current = bone.bind_pose_rotation || [0, 0, 0];
bone.bind_pose_rotation = [
current[0] + boneTransform.addBindPoseRotation[0],
current[1] + boneTransform.addBindPoseRotation[1],
current[2] + boneTransform.addBindPoseRotation[2],
];
Log_1.default.verbose(` - Added to bind_pose_rotation, now [${bone.bind_pose_rotation.join(", ")}]`);
}
// Apply pivot modifications
if (boneTransform.setPivot !== undefined) {
bone.pivot = [...boneTransform.setPivot];
Log_1.default.verbose(` - Set pivot to [${bone.pivot.join(", ")}]`);
}
if (boneTransform.addPivotOffset !== undefined) {
const current = bone.pivot || [0, 0, 0];
bone.pivot = [
current[0] + boneTransform.addPivotOffset[0],
current[1] + boneTransform.addPivotOffset[1],
current[2] + boneTransform.addPivotOffset[2],
];
Log_1.default.verbose(` - Added to pivot, now [${bone.pivot.join(", ")}]`);
}
// Apply cube origin offsets
if (boneTransform.addCubeOriginOffset !== undefined && bone.cubes) {
for (const cube of bone.cubes) {
if (cube.origin) {
cube.origin = [
cube.origin[0] + boneTransform.addCubeOriginOffset[0],
cube.origin[1] + boneTransform.addCubeOriginOffset[1],
cube.origin[2] + boneTransform.addCubeOriginOffset[2],
];
}
}
Log_1.default.verbose(` - Added offset to ${bone.cubes.length} cube origins`);
}
// Apply per-cube rotation (rotates cube geometry only, not the bone TransformNode)
if (boneTransform.setCubeRotation !== undefined && bone.cubes) {
const bonePivot = bone.pivot || [0, 0, 0];
for (const cube of bone.cubes) {
cube.rotation = [...boneTransform.setCubeRotation];
cube.pivot = [...bonePivot];
}
Log_1.default.verbose(` - Set per-cube rotation [${boneTransform.setCubeRotation.join(", ")}] on ${bone.cubes.length} cubes (pivot: [${bonePivot.join(", ")}])`);
}
// Apply parent modifications
if (boneTransform.removeParent) {
delete bone.parent;
Log_1.default.verbose(` - Removed parent`);
}
if (boneTransform.setParent !== undefined) {
if (boneTransform.setParent === null) {
delete bone.parent;
}
else {
bone.parent = boneTransform.setParent;
}
Log_1.default.verbose(` - Set parent to "${bone.parent || "(none)"}"`);
}
}
}
return transformed;
}
/**
* Returns a list of all registered geometry patterns that have transforms
*/
function getRegisteredTransformPatterns() {
const patterns = [];
for (const transform of VANILLA_GEOMETRY_TRANSFORMS) {
patterns.push(...transform.geometryPatterns);
}
return patterns;
}