ExcaliBug

Why PhysicalBone3D ragdolls shrink scaled characters, and the fix

3 min readgodotphysicsanimation

The characters in my game are scaled to 1.3. The first time one got knocked out with a PhysicalBone3D ragdoll, it shrank as it fell: the moment physics took over, every bone came out smaller by exactly the character's scale. Put the same character at scale 1.0 and the ragdoll is fine.

Why it happens

Godot's physics engine doesn't keep scale. Scale a RigidBody3D in the editor and Godot warns you that the physics engine will override it, and the transforms the physics server hands back have no scale in them. PhysicalBone3D lives inside the skeleton, which is inside your scaled character, so its pose goes through physics and comes back with the scale stripped out. The bones it drives get posed from that, and the character's scale is lost along the way.

The usual workarounds only half work:

  • Scale the mesh instead of the skeleton. Fine when you control the import, but not when the scale comes from gameplay (a growth potion, a giant variant, a scaled parent scene).
  • Bake the scale into the model on import. Fine for one fixed size, useless for characters that change size at runtime.

The fix: keep the scale away from physics

Build the ragdoll yourself, outside the scaled part of the tree, and pose the bones yourself.

  1. **One RigidBody3D per body part**, as children of an unscaled node in world space, not under the skeleton. Size each collision shape in world units, at the character's real size: a 1.3 character's thigh capsule is simply 1.3 times longer.
  2. Joints between them: ConeTwistJoint3D for hips, shoulders, spine and neck, HingeJoint3D for elbows and knees.
  3. **Write the bone poses back from a SkeletonModifier3D**, putting the scale back in on the way.

Step 3 is the important one. Here's the core of it, simplified from what Drop-In Ragdoll does every frame while the body is limp:

func _process_modification() -> void:
	var sk := get_skeleton()
	var to_skeleton := sk.global_transform.affine_inverse()
	var s := sk.global_transform.basis.get_scale()  # the character's scale, e.g. 1.3
	for part in bodies:
		var t: Transform3D = bodies[part].global_transform  # full size, no scale
		var r := t.basis.orthonormalized()
		# Put the character's scale back BEFORE going into skeleton space. Otherwise the inverse
		# divides by it, and every offset below comes out 1/s too short: the shrinking bug again.
		var body := to_skeleton * Transform3D(Basis(r.x * s.x, r.y * s.y, r.z * s.z), t.origin)
		var pose: Transform3D = body * offsets[part]  # the bone relative to its body, in skeleton space
		sk.set_bone_global_pose(bones[part], Transform3D(pose.basis.orthonormalized(), pose.origin))

offsets[part] is measured once, when the body goes limp: the bone's global pose relative to its body, both in skeleton space. Because the bodies never sit under anything scaled, nothing about the character's size ever reaches the physics engine, and the character can be any size at all.

Two joint details that cost me time

  • Joint axes. ConeTwistJoint3D twists around its local X axis, and HingeJoint3D turns around its local Z axis. Line those axes up with the bone, or your elbow limits end up limiting the wrong motion.
  • Run the modifier after your animation layers. Put it after IK, look-at and procedural motion and before cloth, so a cape or skirt falls with the body instead of fighting it.

If you'd rather not build all of this yourself, Drop-In Ragdoll is this approach, packaged. It finds the bones on its own, fits the bodies to your mesh, and gets the character back up afterwards.