|
| 1 | +# EGF animation overview — `shared/animating` |
| 2 | + |
| 3 | +`animatingLib` animates the skeleton of an EGF model: sequences from motion packages (`.mop`), |
| 4 | +activities, blending and transitions, pose controllers, animation events and an experimental IK |
| 5 | +solver. The result is an array of bone matrices used to draw the model with skinning (see |
| 6 | +[rendering_egf_overview.md](rendering_egf_overview.md)). File formats: |
| 7 | +[public_egf_overview.md](public_egf_overview.md). |
| 8 | + |
| 9 | +| File | Contents | |
| 10 | +|---|---| |
| 11 | +| `Animating.h/.cpp` | `CAnimatingEGF` — the animation component | |
| 12 | +| `BoneSetup.h/.cpp` | Runtime structures: `AnimSequence`, `AnimSequenceTimer`, `AnimPoseController`, `AnimIkChain`, `AnimFrame` | |
| 13 | +| `anim_activity.h/.cpp` | `Activity` enum, `GetActivityByName`, `GetActivityName` | |
| 14 | +| `anim_events.h/.cpp` | `AnimationEvent` enum, `GetEventByName`, `GetEventName` | |
| 15 | + |
| 16 | +## Usage |
| 17 | + |
| 18 | +`CAnimatingEGF` is meant to be a base class (or member) of a game object that has a model: |
| 19 | + |
| 20 | +```cpp |
| 21 | +class CPedestrian : public CAnimatingEGF |
| 22 | +{ |
| 23 | +protected: |
| 24 | + Activity TranslateActivity(Activity act, int slot) const override; |
| 25 | + void HandleAnimatingEvent(AnimationEvent event, const char* options) override; |
| 26 | +}; |
| 27 | + |
| 28 | +// the model must be fully loaded (see rendering_egf_overview.md: loading) |
| 29 | +EGF_LOADING_CRITICAL_SECTION(geom); // or init from geom->GetLoadingFuture() callback |
| 30 | +ped->InitAnimating(geom); // skeleton, IK chains, motion packages of the model |
| 31 | +ped->InitMotionData("models/peds/shared_anims"); // optional extra package |
| 32 | +ped->SetActivity(ACT_IDLE); |
| 33 | + |
| 34 | +// every frame |
| 35 | +ped->AdvanceFrame(dt); // timers, pose controllers, events |
| 36 | +ped->RecalcBoneTransforms(); // recomputes bones if anything changed |
| 37 | +ped->UpdateIK(dt, worldMatrix); // optional |
| 38 | + |
| 39 | +// drawing |
| 40 | +RenderBoneTransform bones[128]; |
| 41 | +const int numBones = geom->ConvertBoneMatricesToQuaternions(ped->GetBoneMatrices(), bones); |
| 42 | +... |
| 43 | +``` |
| 44 | +
|
| 45 | +- `InitAnimating(geom)` — binds the model: copies the bind pose to the bone matrices, creates IK |
| 46 | + chains and adds all motion packages referenced by the model. The model must be in |
| 47 | + `MODEL_LOAD_OK` state (asserted): skeleton and motion packages are filled by the loading jobs. `DestroyAnimating()` releases it; |
| 48 | + `IsAnimatingInitialized()`. |
| 49 | +- `InitMotionData(name)` — adds one more motion package (by file name, through `g_studioCache`). |
| 50 | + Packages are identified by file name without path and extension. |
| 51 | +- `GetBoneMatrices()` — model-space bone matrices (`Matrix4x4[numBones]`), valid after |
| 52 | + `RecalcBoneTransforms`. |
| 53 | +- `FindBone(name)`, `GetLocalBoneOrigin(bone)`, `GetLocalBoneDirection(bone)`. |
| 54 | +- `GetLocalStudioTransformMatrix(idx)` — model-space matrix of an EGF transform (attachment), |
| 55 | + following its bone if it is attached to one. |
| 56 | +
|
| 57 | +## Sequences and activities |
| 58 | +
|
| 59 | +A **sequence** (from the `.mop`, compiled by `animca`) is what gets played: one or more |
| 60 | +animations, framerate, flags (`SEQFLAG_LOOP`, `SEQFLAG_SLOTBLEND`), transition time, an optional |
| 61 | +pose controller, layered sequences and events. An **activity** is a game-level name for "what the |
| 62 | +object is doing" (`ACT_IDLE`, `ACT_WALK`, `ACT_VEHICLE_STEER`, ...); sequences declare the activity |
| 63 | +and slot they implement. |
| 64 | +
|
| 65 | +```cpp |
| 66 | +SetActivity(ACT_RUN); // sequence by activity (+ slot) |
| 67 | +SetSequenceByName("pedestrian.wave", 1); // "package.sequence" |
| 68 | +
|
| 69 | +const int seqId = FindSequence("pedestrian.wave"); // -1 if not found |
| 70 | +SetSequence(seqId, 1); |
| 71 | +ResetSequenceTime(1); |
| 72 | +PlaySequence(1); |
| 73 | +``` |
| 74 | + |
| 75 | +- `SetActivity(act, slot)` calls `TranslateActivity` (override to pick a variant, e.g. a different |
| 76 | + run for a wounded character), finds the sequence with `FindSequenceByActivity`, resets the time |
| 77 | + and plays it. |
| 78 | +- `SetSequenceByName(name, slot)` / `FindSequence(name)` take `"<package>.<sequence>"`. |
| 79 | +- `SetSequence(seqId, slot)` only assigns the sequence (no reset, no play). |
| 80 | +- Playback: `PlaySequence`, `StopSequence` (pause), `IsSequencePlaying`, `ResetSequenceTime`, |
| 81 | + `SetSequenceTime(frame)`, `SetPlaybackSpeedScale`. |
| 82 | +- Queries: `GetCurrentActivity`, `GetSequenceById`, `GetSequenceTimer`, |
| 83 | + `GetCurrentAnimationFrameCount`, `GetCurrentAnimationDuration` / `Time` / |
| 84 | + `RemainingDuration` (seconds). |
| 85 | +- Non-looping sequences stop on the last frame; looping ones wrap and restart their events. |
| 86 | +- `ACT_SCRIPTED_SEQUENCE` sequences are not registered by activity — play them by name. |
| 87 | +- To add an activity, extend `Activity` in `anim_activity.h` and the name table in |
| 88 | + `anim_activity.cpp` (sequences refer to activities by name). |
| 89 | + |
| 90 | +### Slots, blending and transitions |
| 91 | + |
| 92 | +There are `MAX_SEQUENCE_TIMERS` (5) slots; each plays its own sequence: |
| 93 | + |
| 94 | +- Slot 0 is the base pose (weight always 1). Other slots are mixed in with |
| 95 | + `SetSequenceBlending(weight, slot)`: sequences with `SEQFLAG_SLOTBLEND` are added on top |
| 96 | + (scaled by the weight; e.g. upper-body gestures), others are interpolated towards by the weight. |
| 97 | +- When a slot switches to another sequence, the previous one keeps playing in a transition timer |
| 98 | + and fades out over the new sequence's `transitiontime` (default `SEQ_DEFAULT_TRANSITION_TIME` = |
| 99 | + 0.15 s). `IsTransitionCompleted(slot)`. |
| 100 | +- `SwapSequenceTimers(a, b)` swaps two slots. |
| 101 | + |
| 102 | +### Pose controllers |
| 103 | + |
| 104 | +A pose controller is a named value that blends between the animations of a sequence (e.g. aim |
| 105 | +direction, steering angle): the value is mapped over the controller's blend range onto the |
| 106 | +sequence's animations, and the two nearest animations are interpolated. |
| 107 | + |
| 108 | +```cpp |
| 109 | +const int steerCtrl = FindPoseController("steer"); |
| 110 | +SetPoseControllerValue(steerCtrl, steerAngle); |
| 111 | +``` |
| 112 | +
|
| 113 | +- `GetPoseControllerValue`, `GetPoseControllerInterpValue` (the value used for blending; it |
| 114 | + follows the set value smoothly in `AdvanceFrame`), `GetPoseControllerRange`. |
| 115 | +- Controllers are shared by name between all motion packages of the object; their initial value is |
| 116 | + the middle of the range. |
| 117 | +
|
| 118 | +## Animation events |
| 119 | +
|
| 120 | +Sequences carry events at frames (`command` + `parameter` strings in the `.asc` script). When the |
| 121 | +timer passes the event frame, `HandleAnimatingEvent(event, parameter)` is called with the |
| 122 | +`AnimationEvent` resolved by name (`EV_SOUND`, `EV_MUZZLEFLASH`, `EV_IK_*`) or, if the name is |
| 123 | +unknown, by number (`atoi(command)`). Override it in the game object, e.g. to play a sound: |
| 124 | +
|
| 125 | +```cpp |
| 126 | +void CPedestrian::HandleAnimatingEvent(AnimationEvent event, const char* options) |
| 127 | +{ |
| 128 | + if (event == EV_SOUND) |
| 129 | + { |
| 130 | + EmitParams ep(options, GetOrigin()); |
| 131 | + EmitSound(CSoundingObject::ID_RANDOM, &ep); // object is also a CSoundingObject |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +To add an event type, extend `AnimationEvent` and `anim_event_register` in `anim_events.cpp`. |
| 137 | + |
| 138 | +## Inverse kinematics |
| 139 | + |
| 140 | +IK chains come from the EGF (`studioIkChain_t`, defined in the `egfca` script): |
| 141 | + |
| 142 | +```cpp |
| 143 | +const int chain = FindIKChain("left_leg"); |
| 144 | +SetIKChainEnabled(chain, true); |
| 145 | +SetIKWorldTarget(chain, footTargetWorld, worldMatrix); // or SetIKLocalTarget(chain, localPos) |
| 146 | +... |
| 147 | +UpdateIK(dt, worldMatrix); // after RecalcBoneTransforms |
| 148 | +``` |
| 149 | +
|
| 150 | +Disabled chains follow the animated pose. The solver iterates `r_ikIterations` times per update. |
| 151 | +The IK code is marked as broken in the source (`UpdateIkChain` FIXME) — treat it as experimental. |
| 152 | +
|
| 153 | +## Debugging |
| 154 | +
|
| 155 | +- `DebugRender(worldMatrix)` with cheat ConVars `r_debugSkeleton` (bones), `r_debugShowBone <idx>` |
| 156 | + and `r_debugIK` (IK targets). |
| 157 | +- Lua: `AnimatingEGF` is bound in `shared/sys/scripting/sys_esl_animating.cpp` (see |
| 158 | + [scripting_esl_overview.md](scripting_esl_overview.md)). |
| 159 | +- `tools/egfMan2` (`CAnimatedModel`, derived from `CAnimatingEGF`) plays sequences, edits pose |
| 160 | + controllers and shows bones; use it to check motion packages. |
0 commit comments