Skip to content

PMX and glTF Ragdoll Configuration

Interface, configuration, and call reference. For the concepts see mmd-ragdoll.md.

Physics definitions, IK, and diamond closure now all hang under Skeleton.getDynamics(). For extending the Reader/Builder and for the Caliko integration see skeleton-dynamics.md. SkeletonRagdoll keeps its historical API name; at runtime it only reads the generic skeleton definitions.

SkeletonRagdoll has two construction paths:

  • enablePhysics() uses every rigid body in the PMX file, so it also includes secondary-motion rigid bodies such as skirts and hair.
  • enablePhysics(profile) works for PMX or glTF and uses the profile's bodies to generate stable humanoid capsules and semantic masses; a PMX profile can keep unregistered author-made rigid body chains such as skirts, hair, and accessories through include_secondary_bodies as well. Inertia is generated by Box3D from the shape and the mass. glTF is not allowed to guess a human structure automatically without a profile.

Physics solving is done by the vendored Box3D C17 engine. RigidBodyWorld, SimBody, GenericRigidBody, and BallJoint in uml/dynamic/physics/core only handle Java object mapping, fixed steps, lifecycle, and state synchronization; SkeletonRagdoll is the PMX/skeleton adapter. Any code can create a world directly:

Large-coordinate precision

When SkeletonRagdoll is enabled, the model root's world translation is moved out of the float bone matrices and stored as a separate double worldOrigin. Bone evaluation, Box3D rigid bodies, joints, and the cached block collisions all use local coordinates near that origin; at render time worldOrigin - cameraPosition is computed in double first, and only then written into the float pose matrix.

Body.position() and RayHit.point() are simulation-local coordinates; use ragdoll.worldPosition(body) when you need a world position. Double world coordinates such as a camera's or an entity's should enter the physics world through raycastWorld(...), beginDragWorld(...), updateDragTargetWorld(...), addStaticBoxColliderWorld(...), or worldToSimulation(...).

The MinecraftRagdollDeployments.Request constructor that takes a Vector3d worldOrigin preserves the fractional part of the deployment position; the old constructor is still compatible, but it can only inherit the float precision already present in the original Transform.

var crate = GenericRigidBody.box(new Vector3f(0.5f), 8f).at(2f, 5f, 1f);
var world = new RigidBodyWorld(List.of(crate), List.of(),
        RigidBodyWorld.DEFAULT_SUBSTEP_COUNT);
world.addGroundPlane(0f, 0.8f, 0f);
world.step(dt, RigidBodyWorld.KinematicDriver.none());

Official implementation: physics blocks

The client-side service MinecraftBlockPhysics turns a Minecraft block's real VoxelShape into a compound Box3D rigid body (MinecraftBlockRigidBody): mass comes from block hardness and friction from slipperiness, it is drawn visually with the vanilla block renderer, terrain collision reuses the ragdoll's incremental block environment, and it interacts with the local player through a kinematic AABB proxy. For the full API and the client/server boundary see physics.md. Command entry points:

  • /kasuga_physics block [1-32]: spawns a physics block in front of the view.
  • /kasuga_physics status / clear: show or clear every physics block.

The cap is MAX_PROPS = 256; blocks that fall out of the bottom of the world are reclaimed automatically, and a dimension change clears everything.

Debug commands

  • /kasuga_ragdoll deploy: deploys a ragdoll with the built-in demo model on the ground in front of the player.
  • /kasuga_ragdoll deploy <model resource location>: reads the model manifest and deploys any published glTF that declares a ragdoll.
  • /kasuga_ragdoll list, remove all: list and undeploy deployments.

Lifecycle: a dimension change or leaving the world cleans up every deployment and releases mouse dragging; block-collision-environment invalidation notices are forwarded to nearby environments in real time by core's ClientBlockUpdateHooks (the ClientLevel#sendBlockUpdated mixin), and the periodic refresh is only a fallback.

Native Box3D backend

  • Box3D is pinned to the official main commit 30c67b5e6d0a3a66f0f506c69ce9e9e0587e3b7c, with the source in modules/modelling/src/main/native/box3d; for the license see modules/modelling/THIRD_PARTY_NOTICES.md.
  • CMake statically links Box3D into the kasuga_box3d JNI library; Gradle puts the current platform's artifacts in native/<os>-<arch>/, and the runtime extracts and loads them from the jar. CI builds and tests on Linux x86-64, Windows x86-64, and macOS ARM64 separately, then merges them into the kasuga-modelling-universal universal jar.
  • Box3D owns integration, broad phase, narrow phase, contact, CCD, sleeping, and joint solving; Java contains no fallback solver.
  • PMX spheres, boxes, and capsules map directly to Box3D shapes. PMX six-axis joints map to spherical joints; the rotation range is approximated as cone/twist, and the three-axis translation range is currently tightened to a fixed anchor.
  • Minecraft block collision boxes map to Box3D static hulls created and destroyed incrementally. An infinite plane is approximated by a large static thin box placed along its normal.
  • Box3D has no fluid volume/buoyancy interface, so the old in-house fluid force model has been removed; a Minecraft fluid change still triggers a refresh of the adjacent collision cache, but applies no buoyancy or flow drag.

On the Minecraft side, MinecraftRagdollConfig.load(resourceManager, location) reads a configuration from the resource pack, after which you call config.attach(instance, levelSupplier). The built-in example is at assets/kasuga_lib/ragdolls/test_mmd.json, and a resource pack can override it or provide a new resource location. The built-in rendering demo now uses TDA Bunny Miku 2.0.pmx from test3.mmd.zip, whose body rigid body mapping is at assets/kasuga_lib/ragdolls/tda_bunny_miku.json. Rigid body indices are not reusable across different PMX files.

Model size is controlled by the .mmd.json next to the archive. For example test2.mmd.zip corresponds to test2.mmd.json:

{
  "encoding": "gbk",
  "model_scale": 0.1
}

model_scale can be a uniform scale number or [x, y, z]. When it is omitted, the legacy-compatible 1/12 is still used. The scale is applied uniformly to vertices, bones, rigid body shapes, and joint positions, so render and collision sizes stay consistent.

glTF uses the adjacent .gltf.json model manifest. For example character.glb corresponds to character.gltf.json:

{
  "model_scale": 1.3,
  "ragdoll": "example:ragdolls/character.json"
}

The manifest is the only automatic association entry point: the loader takes the model scale from it and the deployer takes the ragdoll profile from it; Java code never hardcodes selection logic on maribel, renko, or any bone name. A glTF without a ragdoll field still renders and plays animations normally, but a generic ragdoll deployment fails explicitly.

Fields

  • bodies: the body rigid body topology. In PMX, rigid_body and parent are PMX rigid body indices; in a glTF profile they are glTF node indices of the model that owns the manifest; the root node omits parent. The MMD adapter first walks the real bone tree to find the nearest registered physics ancestor; only when it finds no physics ancestor does it use parent to bridge separated PMX branches (for example the "upper body" and "lower body" branches that both sit under "waist").
  • include_secondary_bodies: PMX only, default false. When enabled, author-made rigid bodies and their joints that are not replaced by bodies and do not conflict with the main body bones enter the physics world as well, which suits the common bone-driven skirts and hair. It is not the same as PMX 2.1 per-vertex soft-body. Under a hybrid profile, secondary-motion bones keep the animated translation and write back only the physics rotation, so that hair strands and skirt bones are not stretched by the different reference frame of the main body capsules. An attachment point on the main body drives the secondary-motion chains one-way through a kinematic proxy that moves with the main body, using a separate 16-layer collision namespace, which stops a large number of secondary-motion rigid bodies from dragging the main body backward or bouncing off the generated capsules. The built-in TDA example uses 8 Box3D substeps; per-vertex soft-body currently only reads the format and is not yet connected to Box3D solving or vertex writeback.
  • role: pelvis, spine, chest, neck, head, shoulder, upper_arm, lower_arm, hand, upper_leg, lower_leg, foot, or toe.
  • Top-level limits: swing/twist limits can be declared per role instead of repeating them on every body; fields on the body itself take precedence.
  • The humanoid profile uses swing/twist limits by default: max_swing_degrees controls the cone swing, and min_twist_degrees / max_twist_degrees control the twist along the bone axis. limit_stiffness is a legacy profile compatibility field; Box3D manages the limit correction strength itself. The three angle fields must be provided together. When they are omitted, the humanoid defaults for the role apply.
  • rotation_min_degrees / rotation_max_degrees: the legacy three-axis Euler box limit compatibility path; they must appear together and cannot be mixed with the swing/twist fields.
  • simulation.hertz: the fixed world step frequency; a variable frame length is never used to solve directly.
  • simulation.update_mode: render_frame is advanced by an independent runtime and no longer depends on the model actually being drawn by the backend; manual is advanced by the caller through MinecraftRagdollRuntime.step(instance, dt) or instance.simulatePhysics(dt).
  • simulation.substeps: the number of solver substeps per fixed world step. For long multi-segment secondary-motion chains such as long hair and skirts, 8 is recommended; the built-in TDA configuration uses that value.
  • simulation.solver_iterations: legacy configuration compatibility field; Box3D does not expose this iteration count, so it is currently only validated and does not change the solve.
  • simulation.constraint_hertz / constraint_damping_ratio: joint constraint softening parameters.
  • simulation.max_linear_speed: mapped to the Box3D world maximum linear speed. max_angular_speed is a legacy configuration compatibility field; the current Box3D API has no corresponding world-level setting.
  • simulation.max_fixed_steps_per_update: the maximum number of catch-up fixed world steps in a single tick/frame update, default 12; backlog beyond the budget is dropped, which stops one stall from causing a sustained physics spiral.
  • sleeping.enabled: whether the whole group of rigid bodies sleeps. It is recommended to turn this off when debugging collisions or running sustained airborne simulation.
  • sleeping.linear_speed / angular_speed: scaled by each shape's size into a per-rigid-body farthest-point speed threshold for Box3D. delay_seconds is a legacy configuration compatibility field; the actual sleep delay is managed by Box3D itself.
  • collision.self_collision: whether the inside of one ragdoll collides with itself. false uses a Box3D negative group index; turning it off is recommended for a humanoid main body. When it is false, Box3D filters shape pairs inside the whole ragdoll, not just adjacent joints.
  • collision.continuous: whether to enable swept collision, to avoid penetration at high speed.
  • environment: the Minecraft block collision cache refresh frequency, boundary expansion, friction, and restitution.
  • initial_state: the optional overall initial offset and root rigid body velocity, mainly for demos or spawn effects.
  • dragging.enabled: register this instance for crosshair dragging.
  • dragging.mouse_button / max_distance: the mouse button (0 is the left button) and the maximum picking distance.
  • dragging.bias_rate: mapped to the spring hertz of the Box3D drag spherical joint. The remaining legacy drag tuning fields are still read for resource pack compatibility, but no extra Java stabilizer runs.

In game, aim the crosshair at the model's main body and hold the left button to drag it; after release the rigid bodies keep the velocity they had at the moment of release. Profile humanoid picking uses the capsules generated from the bones; the profile-less compatibility mode still uses PMX spheres, boxes, and capsules. What is grabbed is a local point on the hit surface; the whole skeleton is not teleported. The whole group of rigid bodies stays awake for as long as the mouse constraint exists, so gravity, joints, and collisions do not suddenly freeze mid-drag. Dragging applies a soft constraint only at the grab point of the hit rigid body and no longer applies extra linear or angular damping to the whole group. After the mouse is released, the rigid bodies only sleep normally once they touch the static environment and meet the low-speed threshold.

Runtime deployment and removal

The client main thread can create instances dynamically through MinecraftRagdollDeployments. The model package must already have been scanned and published by models/model_proxy.json; while it is not yet published, deploy returns empty and you can retry after resource loading completes:

var request = new MinecraftRagdollDeployments.Request(
        ResourceLocation.fromNamespaceAndPath("kasuga_lib", "models/pmx/test3.mmd.zip"),
        "TDA Bunny Miku 2.0.pmx",
        ResourceLocation.fromNamespaceAndPath("example", "ragdoll/1"),
        ResourceLocation.fromNamespaceAndPath("kasuga_lib", "ragdolls/tda_bunny_miku.json"),
        new Transform().translate(x, y, z),
        true
);
RagdollDeployment deployment = MinecraftRagdollDeployments.deploy(request).orElseThrow();

For glTF, prefer a manifest request rather than passing a hardcoded configuration path:

var request = new MinecraftRagdollDeployments.Request(
        ResourceLocation.fromNamespaceAndPath("example", "models/character.glb"),
        ResourceLocation.fromNamespaceAndPath("example", "ragdoll/1"),
        new Transform().translate(x, y, z),
        true
);
RagdollDeployment deployment = MinecraftRagdollDeployments.deploy(request).orElseThrow();

The handle exposes both instance() and ragdoll() for applying velocity or changing parameters at runtime. The two removal forms below are equivalent and can be called repeatedly:

deployment.remove();
MinecraftRagdollDeployments.remove(ResourceLocation.fromNamespaceAndPath("example", "ragdoll/1"));

ragdoll() exposes a stable runtime control interface: query rigid bodies with body(PMX rigid body index) and body(bone name), and call the impulse, force, torque, gravity scale, and drag APIs. lastFixedStepCount(), droppedSimulationTime(), selfContactCount(), staticContactBodyCount(), and sleepTime() are available for performance panels and collision/sleep diagnostics. The deployment handle also supports entity mounting: anchorTo(entity) hangs the nearest dynamic rigid body at the entity's eye position through a soft constraint, and detachAnchor() releases it; mouse dragging takes over the anchor automatically.

Removal takes the model out of the render backend and the pipeline instance table, unregisters mouse dragging, shuts down the block collision environment, and restores/releases the physics pose. removeAll() can be used for a level exit or for a caller that unloads everything at once.

Plain PMX and profile humanoids both use the four substeps Box3D recommends. The profile still handles bone capsules, semantic masses, physics bone writeback, and bind-pose follower tracking, but all physics constraints and the drag anchor are solved by Box3D.