Ragdoll Solver¶
The acron.ragdoll module is a small, fast simulation of a limp body:
the ragdoll that a character becomes when it dies, tumbles down the stairs and
comes to rest. It is a port of the ragdoll solver of the Tesseract engine
(the Cube 2 family), a Verlet particle system: the body is a cloud of weighted
points held together by distance limits, with triangles of points that give
the bones their orientation and rotation limits that keep joints in range.
There is no rigid-body engine behind it, which is the point – a body costs
microseconds per step, so a battlefield’s worth of them fit in a frame.
It is not a general physics engine. The world is a flat floor, collision is with that floor only, and the body does not push anything. For characters that interact with the scene while alive, use Using Bullet with Acron and its constraints; for the moment they stop being alive, use this.
Two classes¶
RagdollSkelThe shared, immutable definition of a kind of body: its vertices with their radii, the triangles that carry bones, the distance limits and the rotation limits between triangles. Built once per character type and shared by every body of that type.
RagdollDataOne body: seeded from the pose at the moment of death, stepped once per frame, read back into the character’s joints, and frozen once it has come to rest.
Defining a skeleton¶
A skeleton is built with the add_* methods, in the terms Tesseract’s
ragdoll.cfg files use, so an existing definition transcribes line by line:
from acron.ragdoll import RagdollSkel
skel = RagdollSkel()
# verts: model-space position and radius
for (x, y, z), radius in verts:
skel.add_vert(x, y, z, radius)
# tris: three vert indices; each tri's orientation drives one bone
for v0, v1, v2 in tris:
skel.add_tri(v0, v1, v2)
# distance limits between two verts
for v0, v1, min_dist, max_dist in distlimits:
skel.add_distlimit(v0, v1, min_dist, max_dist)
# rotation limits between two tris: a maximum angle and trace, and the
# "middle" orientation as three column vectors a, b, c
for t0, t1, max_angle, max_trace, (a, b, c) in rotlimits:
skel.add_rotlimit(t0, t1, max_angle, max_trace,
a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2])
# pairs of tris that damp each other's rotation
for t0, t1 in rotfrictions:
skel.add_rotfriction(t0, t1)
The rotation limit’s middle matrix is given as column vectors, the
convention of Tesseract’s matrix3, so that a definition compares directly
with the original.
Driving a body¶
A RagdollData is created from the skeleton and the scale the model is
rendered at. It simulates in world space, in double precision, so a body
settles identically however the engine’s float type is built.
Set the floor:
RagdollData.set_floor_z()with the floor height under the body, probed from the scene by the caller.Seed every vertex with
RagdollData.seed_vert()– its world position in the pose at death and its velocity – and callRagdollData.finish_seed(). The animated pose gives the positions; the character’s velocity, plus the hit that killed it, the velocities.Call
RagdollData.step()with the frame time, every frame. The step pays the elapsed time down in fixed substeps betweenget_timestep_min()andget_timestep_max(), at mostget_max_substeps()of them per call, so a stalled frame cannot buy a burst of work that stalls the next.Read the vertices back with
RagdollData.get_vert()and pose the character’s joints from them: each triangle’s three points give the orientation of the bone it carries, the same way Tesseract does it.Once the body has rested for
get_expire_offset()seconds of simulated time,RagdollData.frozenbecomesTrue, the pose is final andstepis a no-op from then on. Bodies that are frozen can be stopped and, when the corpse is removed, dropped.
from acron.ragdoll import RagdollData
body = RagdollData(skel, scale)
body.set_floor_z(floor_height)
for i, (position, velocity) in enumerate(pose):
body.seed_vert(i, *position, *velocity)
body.finish_seed()
def update(task):
if not body.frozen:
body.step(globalClock.dt)
for i in range(body.num_verts):
positions[i] = body.get_vert(i)
pose_joints(positions)
return task.cont
RagdollData.time is the simulated time so far and
RagdollData.get_collisions() the number of floor contacts, both useful
for sound and dust effects.
Tuning¶
The constants – gravity, the substep bounds, the constraint iteration count,
the rotation, body, ground and air friction, and the settle time – default to
Tesseract’s, and each has a setter and getter on RagdollData
(set_gravity, set_constraint_iters, set_ground_fric, …). Set
them right after construction, before seeding, and keep the values in one place
in your program rather than in every body.
Why it exists¶
The same solver in Python costs about half a millisecond per substep, enough for a handful of falling bodies; in the engine it costs microseconds. The Arena sample, which switched to this module, went from an unplayable frame rate with thirty simultaneous ragdolls to sixty frames per second, and keeps the Python solver only as the reference its parity test compares against.