Physics Syncing: The Animation-Physics Link
Bridging Two Worlds: The Animation-Physics Conflict
In modern game engines, a character’s movement is almost always a tug-of-war between two fundamentally different systems: the Animation System and the Physics Engine. To understand why we need to "sync" these systems, we first have to recognize how they view the world in completely different ways.
The Animation System is essentially a playback device. It takes pre-recorded or calculated "poses" (bone positions and rotations) and applies them to a visual mesh. It doesn’t care if a character’s arm passes through a stone wall or if their feet are hovering six inches off the ground. Its only goal is to make the character look like they are performing a specific action, like walking, swinging a sword, or reacting to a hit.
The Physics Engine, on the other hand, is a mathematical simulation of the physical laws of our world. It calculates how objects move based on forces like gravity, friction, and collisions. It doesn’t know about "animations" or "skeletons." It only knows about Rigid Bodies—mathematical shapes like boxes, spheres, and capsules that have mass and momentum. Its goal is to ensure that objects don’t pass through each other and that they react realistically when they collide.
The challenge we face is that these two systems often disagree on where a character should be. If an animation says a character’s hand should be inside a wall, the physics engine says "No, it must stop at the surface." If the physics engine says a character should be falling, the animation system might still be playing a "stand idle" clip.
To resolve this, we need a Bi-directional Link: a bridge that allows us to pass transformation data back and forth between our visual scene graph and the physics simulation.
Understanding the Players: Kinematic vs. Dynamic
To implement this bridge, we must understand the two primary modes that a physics body can operate in. These modes define which system is currently the "Master" and which is the "Slave."
1. Kinematic: Animation in Control
A Kinematic body is a special type of rigid body that is "immune" to the laws of physics simulation. It has infinite mass, it isn’t affected by gravity, and it cannot be pushed by other objects. Instead, its position and rotation are set directly by the CPU (in our case, by the animation system).
Think of a kinematic body as a "God Mode" object. It moves exactly where the animation tells it to go, and it will push any dynamic objects (like boxes or other characters) out of its way with unstoppable force. This is how we represent a character’s limbs during normal gameplay. The arm moves because the animation says so, and the physics engine uses the kinematic collider to check if that arm has hit anything else in the world.
2. Dynamic: Physics in Control
A Dynamic body (often called a "Ragdoll" body in character contexts) is a standard rigid body that is fully simulated by the physics engine. It reacts to gravity, it can be pushed by other objects, and it follows the laws of momentum and friction.
In this mode, the animation system is effectively turned off for that specific bone. The physics engine becomes the "Source of Truth," and we pull the resulting transformation data back into our scene graph so the visual mesh follows the physical simulation. This is the classic "ragdoll" effect you see when a character is knocked unconscious or killed.
The Synchronization Pipeline
The synchronization between our Scene Graph and the Physics Engine is a carefully orchestrated loop that occurs every single frame. If we sync at the wrong time, we risk One-Frame Lag, where the visual character is always one step behind their physics representation, leading to "ghosting" or clipping artifacts where the character appears to pass through objects they should be colliding with.
The standard engine update loop follows this specific sequence:
-
Animation Update: The engine calculates new local poses for all bones based on time, blending, and animation clips.
-
Scene Graph Update: We propagate these local poses down the hierarchy to calculate the
world_matrixfor every node (using our Dirty Flag system). -
Kinematic Sync (Animation → Physics): For every node that is in "Kinematic" mode, we push its newly calculated
world_matrixinto the physics engine. This "positions" the colliders for the upcoming simulation step. -
Physics Step: The physics engine simulates one tick of time (e.g., 1/60th of a second). It calculates collisions and resolves forces for all dynamic bodies.
-
Dynamic Sync (Physics → Animation): For every node that is in "Dynamic" mode, we pull its new world position from the physics engine and convert it back into our Scene Graph’s local-space coordinates.
-
Final Cleanup: We mark any modified nodes as "Dirty" so that the renderer and other systems (like ray-tracing acceleration structure updates) use the correct, physics-driven positions.
Implementation: Kinematic Sync (Animation Driving Physics)
When our character is performing a standard animation, the Scene Graph is the primary source of truth. We must "teleport" the physics engine’s bone proxies (colliders) to match the visual nodes.
Because physics engines usually expect separate position and rotation data rather than a raw 4x4 matrix, we must decompose our node’s world_matrix.
void sync_kinematic_to_physics(const Node& node, JPH::BodyID body_id,
PhysicsWorld& physics_world) {
// Optimization: Only update physics if the visual node has actually moved
if (node.status & TransformStatus::WorldDirty) {
// 1. Extract the Translation
// The 4th column of a standard 4x4 matrix contains the world-space position.
PhysicsPose pose;
pose.position = glm::vec3(node.world_matrix[3]);
// 2. Extract the Rotation
// This gives us the world-space orientation without any scaling factors.
pose.orientation = node.get_world_rotation();
// 3. Update the Physics Representation
// move_kinematic teleports the kinematic body to the new pose.
physics_world.move_kinematic(body_id, pose);
}
}
Note on Scaling: Most physics engines assume that rigid bodies have a scale of 1.0. If your artist has scaled a bone in Blender, the world_matrix will contain that scale. By decomposing the matrix into a position and a quaternion, we intentionally strip away the scale, providing the physics engine with exactly what it needs for a stable simulation.
Implementation: Dynamic Sync (The Ragdoll Handoff)
The most complex part of the bi-directional link is Dynamic Syncing, where physics takes control. To visualize a ragdoll, we must map the physics body’s world-space transform back into our hierarchical Scene Graph nodes.
The math here is a common trap. Because our scene graph is hierarchical, we cannot simply overwrite the world_matrix. If we did, the next time update_transforms() is called, our parent-child calculation (ParentWorld * LocalMatrix) would overwrite the physics position with the (now incorrect) animation position.
Instead, we must calculate what the local transform needs to be, relative to the parent, to result in the specific world-space position provided by the physics engine.
void sync_physics_to_dynamic(Node& node, JPH::BodyID body_id,
const PhysicsWorld& physics_world,
std::vector<Node>& nodes) {
// 1. Get the new world transform from the physics simulation
PhysicsPose pose = physics_world.get_body_pose(body_id);
glm::mat4 new_world_matrix = pose.to_matrix();
// 2. Convert world matrix to local transform relative to parent
// The fundamental formula for our hierarchy is: World = ParentWorld * Local
// To solve for Local, we multiply both sides by the inverse of ParentWorld:
// Local = Inverse(ParentWorld) * World
glm::mat4 local;
if (node.parent_index != INVALID_NODE_INDEX) {
const Node& parent = nodes[node.parent_index];
local = glm::inverse(parent.world_matrix) * new_world_matrix;
} else {
// Root nodes have no parent, so world space is identical to local space
local = new_world_matrix;
}
// Decompose the local matrix into the SRT components stored on the node
node.translation = glm::vec3(local[3]);
node.scale = glm::vec3(glm::length(glm::vec3(local[0])),
glm::length(glm::vec3(local[1])),
glm::length(glm::vec3(local[2])));
glm::mat3 rot_only;
rot_only[0] = glm::normalize(glm::vec3(local[0]));
rot_only[1] = glm::normalize(glm::vec3(local[1]));
rot_only[2] = glm::normalize(glm::vec3(local[2]));
node.local_rotation = glm::quat_cast(rot_only);
// 3. Mark the node dirty so its children and the renderer update correctly
node.mark_dirty();
}
By converting the physics world-space coordinate into a local-space coordinate, we maintain the integrity of our hierarchical scene graph. This ensures that if a character is ragdolled while standing on a moving platform (like an elevator), the ragdoll will correctly follow the parent platform’s movement while still being physically simulated.
Managing the Simulation: Collision Filtering
One final, critical aspect of character physics is Self-Collision. In a ragdoll, every limb is a separate rigid body. If the arm’s capsule collider is allowed to collide with the torso’s collider, the ragdoll will "explode" or jitter violently as the limbs fight for space at the joints.
To prevent this, Jolt Physics uses Object Layers. A globally defined layer-pair table specifies which layers can interact. We assign each body to a layer using set_object_layer().
// Object layer constants — defined once at physics initialization.
constexpr uint16_t LAYER_ENVIRONMENT = 0; // Ground, walls, buildings
constexpr uint16_t LAYER_CHARACTER = 1; // The character's own limbs
constexpr uint16_t LAYER_PLAYER = 2; // Other players or NPCs
// Example: Assign a bone collider to the character layer.
// The layer-pair table (set up during PhysicsWorld::global_init()) prevents
// LAYER_CHARACTER bodies from colliding with each other, avoiding self-collision.
void setup_bone_physics(JPH::BodyID body_id, PhysicsWorld& physics_world) {
physics_world.set_object_layer(body_id, LAYER_CHARACTER);
}
This "Social Distancing" for colliders ensures that a character’s arms can pass through their own chest without generating phantom forces, while still being able to hit the ground or be struck by a player’s weapon.
Summary
By implementing bi-directional syncing, we’ve bridged the gap between the aesthetic world of animation and the mathematical world of physics. Our scene graph nodes now act as a unified interface that can be driven by either system, allowing for the complex transitions between scripted animation and emergent physical simulation that define modern character action.
In the next section, we’ll see how to automate the setup of these colliders. Instead of manually configuring capsules for every bone in code, we will leverage glTF "extras" and custom metadata to build our physics proxies directly from the artist’s source file.