Secondary motion as core engine functionality: soft parts of a character
(breasts, glutes, belly, thighs, hair, tails) lag, overshoot and settle
when the body moves, and whole soft objects (jelly, slime) dent and wobble.
Everything is in engine/include/kke/JigglePhysics.h, pure CPU, no GPU
types, tested in tests/test_jiggle.cpp. The demo is games/jiggle_demo.
| Piece | What it does | Cost |
|---|---|---|
kke::JiggleRig |
Bone chains become verlet points that chase the animated pose; the bones are turned (and, with stretch, lengthened, squashed) to follow them. Runs after the Animator, on its Pose. |
8 points for a full body: ~20-40 µs per character per frame, including the pose rebuild |
kke::addJiggleBone |
Adds a soft-tissue bone to a rig that has none (most game rigs, Synty's included) and moves the nearby skin onto it. Nothing changes at rest. | load time |
kke::inflateSkin |
Reshapes the body: pushes the skin near a point outward (volume in metres). | load time |
kke::addHumanoidSoftTissue |
All of the above for any humanoid in one call: finds chest, pelvis and thighs by bone name and the skin around them, reshapes, adds breast and glute bones, returns rig chains and skin zones. | load time |
kke::JiggleSkin |
Jiggle on the mesh without bones: a few verlet points ride on bones and displace the skin near them (ModelModule::setSkinJiggle, applied after skinning). Belly, thighs. |
a few µs |
kke::JellyBody |
A whole soft body: a particle lattice kept in shape by shape matching over every 2x2x2 cell, a rounded render surface embedded in it, two-way collision with balls. | 320 particles, 3 iterations at 120 Hz: ~0.8 ms per frame |
kke::ModelData body = kke::loadModel(path, options);
kke::HumanoidSoftTissue tissue;
tissue.bust = 0.06f; // metres of extra volume, 0 = the artist's shape
tissue.glutes = 0.05f;
kke::HumanoidJiggleSetup setup = kke::addHumanoidSoftTissue(body, tissue);
// ... retarget clips onto `body` now (the new bones stay at rest in them)
auto id = models.add(std::move(body), "my-character");
kke::JiggleRig rig(rigData, setup.chains);
kke::JiggleSkin skin(rigData, setup.zones, setup.zonePositions);
// every frame, after the Animator:
kke::Pose pose = animator.pose();
rig.apply(rigData, pose, instanceTransform, dt);
skin.apply(rigData, pose, instanceTransform, dt);
models.setSkinJiggle(instance, skin.offsets());
kke::poseToLocals(pose, *models.boneLocals(instance));Hair, tails, antennae: JiggleRig::Chain{ firstHairBone } on the rig's
own bones; the last bone gets a virtual tip continuing its parent's
direction. JiggleCollider spheres and capsules keep points out of the
body.
stiffness: pull back to the animated pose each step.soften: how much weaker that pull is near rest (soft wobble for small motions, still held for big ones).stretch: 0 keeps bone length, 1 is a free spring (squash and stretch; a one-bone chain fromaddJiggleBonescales along its axis, keeping volume).drag: damping relative to the parent (the tissue's own wobble).airDrag: damping in the world (what makes a ponytail trail).gravity: multiple of world gravity (sag).blend: 0 = animation only.angleLimit(degrees),maxStretch(ratio),radius(metres, for colliders).
Techniques from naelstrof's JigglePhysics for Unity, rewritten for this engine rather than copied, plus fixes for what makes jiggle look bad:
- Driven by the difference from animation. Points chase where the pose puts them, so the authored pose always wins and nothing drifts. A character standing still has no energy to bounce with: no bouncing on its own at idle.
- Fixed internal step (90 Hz) with interpolation of the offset from the pose, not of the positions. Frame-rate independent, and no lag or jitter against the body at any frame rate (interpolating positions would trail the body by one step).
- Two drags. Relative drag damps the tissue's own wobble, air drag damps world motion. Separate, so riding in a car or an elevator doesn't set everything wobbling forever.
- Soften near rest, angle limits, hard length clamps, colliders: it stays anatomically sane whatever the sliders say.
- Every step is a blend or a clamp, never a force. It can't explode.
- Sleep. A rig whose points are still and whose pose isn't moving
skips its solve. Teleports (more than
teleportDistancein one frame) reset instead of whipping across the map. - Hitches run at most 4 steps and drop the rest.
- Flat-shaded art.
inflateSkinpushes by position, never by vertex normal: low-poly meshes split every corner per face, and a normal-based push would tear them open.
The jelly uses lattice shape matching (Müller et al. 2005, "Meshless deformations based on shape matching"; per-cell regions as in Rivers & James 2007, "FastLSM"). Each cell's rotation comes from Müller et al. 2016, "A robust method to extract the rotational part of deformations", warm-started from the last step, so three iterations are enough. Balls collide with the particles and with the top as a height field, so a fast ball can dent the jelly but never end up inside it.
FEMFX (kke::PhysicsModule) solves real volumetric elasticity and
fracture; that's the right tool for breakable hero objects. Jiggle is
secondary motion that has to run on every character every frame: FEMFX
costs about 0.2 ms per body per step on its own and needs a tet mesh; the
rig above costs tens of microseconds for a whole character and needs
nothing but the skeleton. For a jelly you want to break, FEMFX is still
the tool (see the physics demo's rubber ball); for a jelly that wobbles,
JellyBody is ~0.8 ms and works on builds without FEMFX (the default).
cd build/bin && ./jiggle_demo. Tab switches scenes.
-
Jelly: balls rain onto a jelly with fruit set in it.
Spacetoggles the rain,Bdrops a big ball,Psquishes it,Rresets; sliders for firmness, iterations, damping, and the look: flavour (strawberry, lime, blue raspberry, orange, panna cotta, clear gelatin), density, milkiness.KKE_JELLY_LOOK=0..5picks the flavour.The jelly is drawn with
DynamicMeshRenderer::drawTranslucent, a reusable translucent material (shaders/translucent*.{glsl,frag}): one pass multiplies what's behind by the transmittance (Beer-Lambert through a thickness that grows toward the silhouette, so edges look richer, as real jelly does), a second adds Fresnel reflection, light scattered inside, light shining through from behind and a glossy highlight. No sorting or extra render targets; draw it after the opaque scene. -
Body: a realistic woman (
female_body, a Character Creator 4 OBJ with no skeleton, rigged at load bykke::autoRigHumanoid, AUTO_RIG.md) with soft-tissue bones fromaddHumanoidSoftTissueand strand hair (HAIR.md); without her, a Synty character reshaped withaddHumanoidSoftTissue. She runs a circle on UAL clips: stand, jog, jump, sprint, stop dead, jump, walk (5, the tour), or1-4for one gait,Spaceto jump. The camera follows from the side. "Twin without jiggle" adds the same body without jiggle half a lap behind; "Show points" draws the simulated points (pink) against their animated targets (blue). Breast and glute sliders.
Environment: KKE_JIGGLE_SCENE=body, KKE_JIGGLE_MOVE=0..4,
KKE_JIGGLE_CHARACTER=SK_..., KKE_JIGGLE_BODY=<file>,
KKE_JIGGLE_HAIR=<style>, KKE_JIGGLE_TWIN=1,
KKE_JIGGLE_VIEW=yaw,pitch,distance (screenshots), KKE_JIGGLE_TRACE=1
(logs swing and stretch every frame). The body scene needs female_body
(or a Synty character pack) and assets/animations/UAL1_Standard.fbx;
see SCENES.md for exactly which.