The animated mesh objects in the Tomb Raider series are sets of meshes that are moved relative to each other, as defined by Entities[]
entries. Each entry describes which meshes to be used (a contiguous set of them referred to in MeshPointers[]
), what hierarchy and relative offsets they have (contents of MeshTree[]
pointed to), and what animations are to be used (contents of Animations[]
pointed to).
The hierarchy used is a branching one, with the meshes being at the nodes, and with the first mesh being the root node. The MeshTree[]
values are applied to each of the child meshes in sequence; they are sets of four int32_t
s, the first being a hierarchy operator, and the remaining three being the coordinates in the parent mesh’s system. A hierarchy example is that for the Lara meshes:
Top-down hierarchy of Lara’s MeshTree. Hips is a root mesh. Ponytail is not listed, as it is a separate object. |
This is implemented by using a stack of meshes and “push” and “pop” operations in MeshTree[]
. Normally, each mesh’s parent is the previous mesh in series. But such meshes can be “remembered” by adding them to a stack of meshes with a “push” operation. This remembered mesh can then be used as the parent mesh with a “pop” operation. It is not clear what the maximum stack depth is; most TR mesh stacks do not extend beyond 2 or 3 meshes.
The animations for each mesh object are selected with some ingenious techniques. Which animations to use are not hardcoded; instead, each entity has some states it can be in, and these states are used to select which animation. For example, locks have only one state (they just sit there), doors have two states (open and closed), and Lara has numerous states, such as standing, walking, running, jumping, falling, being hurt, dying, etc. Each animation has a state ID, which can be used to select it; however, state transitions might seem to require a large number of intermediate states (opening, closing, starting to jump, landing, etc.). The alternative used in the Tomb Raider engine is for each animation to have bridge animations to other states' animations, which are selected using the ID of which state to change to. These bridge animations then lead to the animation with the appropriate state. Thus, a closed door will run a looped closed-door animation as long as its state stays “closed”, but when its state becomes “open”, it will change to an opening-door bridge animation, which will end in a looped open-door animation. Likewise, closing a door will make it use a closing-door bridge animation. Some bridge animations are chosen with a finer grain of selectivity, however, such as using one for left foot forward and one for right foot forward.
Thus, each animation references a set of StateChange
structures, each one of which references an AnimDispatch
structure (called a “range” in some documentation). Each StateChange
structure contains a new state and which AnimDispatch
structures to use. When an entity goes into a new state, the StateChange
structures are scanned for that state’s ID, and if one matches, then that StateChange
's AnimDispatch
es are then scanned for a range of frames that contains the ID of the current frame. If such an AnimDispatch
is found, the animation and the frame are changed to those listed in it.
The ultimate unit of animation is, of course, the frame, and each frame consists of a bounding box, the offset of the root mesh, and rotation angles for all the meshes with respect to their parent meshes. The root mesh is also rotated, but relative to the object’s overall coordinates. All rotations are performed around the meshes' origins, and are in order Y, X, Z (yaw, pitch, roll). The reason for the root mesh’s displacement is because entities traveling on solid surfaces are likely tracked by having their locations be at ground level, and Lara’s hips, for example, are well above the ground. Finally, some of the angles are not specified explicitly, when they are not, they are zero.
Frames are referenced in two ways, either by an offset into the Frames[]
array that contains them, or by frame index. The values of the latter appear to be unique to each kind of entity, but not between entities; the first frame for each kind is numbered 0. This is likely a convenience when constructing the animations, since the list of animation frames for each entity can be constructed separately. However, using these indices is fairly simple. Each Animation structure has a first-frame index; this index is subtracted from the index of the desired frame in order to find out its index relative to the animation’s first frame.
There are also some special AnimCommands for doing various additional things. Some of them are for moving entities in absolute coordinates, for example to position Lara at climb location, or specifying jump momentum. Some others define actions per frame, like playing sounds, emitting bubbles, and so forth.
Finally, some entities appear to have incomplete set of animations; their complete animations are “borrowed” from similar entities. Such setup is mostly used in TR2’s Venice levels — some of Venice goons them have a full set of animations, while some others have only the standing animation. The ones with only the standing animation borrow their other animations from the fully-animated ones.
struct tr_meshtree_node // 4 bytes { uint32_t Flags; int32_t Offset_X; int32_t Offset_Y; int32_t Offset_Z; };
MeshTree[]
array consists of meshtree nodes.
In Flags
field, two bits are used:
0x0001
) indicates “take the top mesh off of the mesh stack and use as the parent mesh” when set, otherwise “use the previous mesh as the parent mesh”.0x0002
) indicates “put the parent mesh on the mesh stack”.When both bits are set, the Bit 0 operation is always done before the Bit 1 operation. In effect, read the stack but do not change it.
Offset_X
, Offset_Y
and Offset_Z
are offsets of the mesh’s origin from the parent mesh’s origin.
This describes each individual animation. These may be looped by specifying the next animation to be itself. In TR2 and TR3, one must be careful when parsing frames using the FrameSize value as the size of each frame, since an animation’s frame range may extend into the next animation’s frame range, and that may have a different FrameSize value.
struct tr_animation // 32 bytes { uint32_t FrameOffset; // Byte offset into Frames[] (divide by 2 for Frames[i]) uint8_t FrameRate; // Engine ticks per frame uint8_t FrameSize; // Number of int16_t's in Frames[] used by this animation uint16_t State_ID; fixed Speed; fixed Accel; uint16_t FrameStart; // First frame in this animation uint16_t FrameEnd; // Last frame in this animation uint16_t NextAnimation; uint16_t NextFrame; uint16_t NumStateChanges; uint16_t StateChangeOffset; // Offset into StateChanges[] uint16_t NumAnimCommands; // How many of them to use. uint16_t AnimCommand; // Offset into AnimCommand[] };
FrameOffset
is a byte offset into Frames[] (divide by 2 for Frames[i]).
FrameRate
is a multiplier value which defines how many game frames will be spent for each actual animation frame. For example, if value is 1, then each animation frame belongs to single game frame. If value is 2, then each animation frame belongs to two game frames, and so on. In latter case, animation frames will be interpolated between game frames using slerp function.
State_ID
identifies current state type to be used with this animation. Engine uses current State_ID
not only to solve state changes, but also to define current Lara behaviour — like collisional routines to be used, controls to be checked, health/air/sprint points to be drained, and so on.
Speed
and Accel
values are used to set a specific momentum to a given entity. That is, entity will be accelerated with Accel
value, until Speed
value is reached. If Accel
is negative, speed will be decreased to fit Speed
value. The direction in which entity is moved using speed value is hardcoded, and mostly is forward.
NextAnimation
defines which animation should be played after current one is finished. When current animation ends, engine will switch it to NextAnimation
, not regarding current State_ID
value. If NextAnimation
value is the same as animation number itself, it means animation will be looped until loop is broken by state change.
NextFrame
specifies the frame number to be used when switching to next animation. That is, if NextFrame
is 5 and NextAnimation
is 20, it basically means that at the end of current animation engine will switch right to frame 5 of animation 20. If animation is looped, NextFrame
defines to which frame animation should be rewound. It allows to “eat up” certain start-up frames of some animations and re-use them as looped.
Core: ANIM_STRUCT
For TR4 and TR5, extended version of [tr_animation] is used:
struct tr4_animation // 40 bytes { uint32_t FrameOffset; uint8_t FrameRate; uint8_t FrameSize; uint16_t State_ID; fixed Speed; fixed Accel; fixed SpeedLateral; // New field fixed AccelLateral; // New field uint16_t FrameStart; uint16_t FrameEnd; uint16_t NextAnimation; uint16_t NextFrame; uint16_t NumStateChanges; uint16_t StateChangeOffset; uint16_t NumAnimCommands; uint16_t AnimCommand; };
In addition to Speed
and Accel
values, TR4 introduced LateralSpeed
and LateralAccel
values, which are used to move entity to the sides, rather than forward or backward. However, these values are only used for any entity but Lara — engine ignores them in such case.
Lateral speed and acceleration primarily used for “start-up” animations of NPCs — for example, armed baddies in TR4 can roll or jump aside.
Core: CHANGE_STRUCT
Each state change entry contains the state to change to and which animation dispatches to use; there may be more than one, with each separate one covering a different range of frames.
struct tr_state_change // 6 bytes { uint16_t StateID; uint16_t NumAnimDispatches; // number of ranges (seems to always be 1..5) uint16_t AnimDispatch; // Offset into AnimDispatches[] };
Core: RANGE_STRUCT
This specifies the next animation and frame to use; these are associated with some range of frames. This makes possible such specificity as one animation for left foot forward and another animation for right foot forward.
struct tr_anim_dispatch // 8 bytes { int16_t Low; // Lowest frame that uses this range int16_t High; // Highest frame that uses this range int16_t NextAnimation; // Animation to dispatch to int16_t NextFrame; // Frame offset to dispatch to };
These are various commands associated with each animation. They are varying numbers of int16_t
s packed into an array. As the FloorData, AnimCommands must be parsed sequentially, one by one.
The first AnimCommand entry is the type, which also determines how many int16_t
arguments (operands) follow it (i.e. how many int16_t
values must be parsed after current one without switching to next AnimCommand). For a given animation, AnimCommands are parsed until NumAnimCommands
value is reached.
Some of commands refer to the whole animation (jump speed, position change, kill and empty hands commands), while others of them are associated with specific frames (sound, bubbles, etc.). When command refers whole animation, it means that actual command execution will occur on animation transition, i.e. last animation frame.
struct tr_anim_command // 2 bytes { int16_t Value; };
Here are all the AnimCommand
types and their arguments.
0x4000
): play this sound when on dry land (example: footsteps)0x8000
): play this sound when in water (example: running through shallow water)FOOTPRINT_FX
flipeffect (ID 32) is used, second argument also may contain one of two “packed” bit flags, similar to Play Sound animcommand. However, meaning is different:0x4000
): apply FOOTPRINT_FX
in relation to left foot0x8000
): apply FOOTPRINT_FX
in relation to right footFrames indicate how composite meshes are positioned and rotated. They work in conjunction with Animations[] and MeshTree[]. A given frame has the following format:
struct tr_anim_frame // Variable size { tr_bounding_box box; // Bounding box int16_t OffsetX, OffsetY, OffsetZ; // Starting offset for this model int16_t NumValues; uint16_t AngleSets[]; // Variable size }
NumValues
: Number of angle sets to follow; these start with the first mesh, and meshes without angles get zero angles. NumValues is implicitly NumMeshes (from model).
AngleSets
are sets of rotation angles for all the meshes with respect to their parent meshes. In TR2/3, an angle set can specify either one or three axes of rotation.
If either of the high two bits (0xC000
) of the first angle uint16_t
are set, it’s one axis: only one uint16_t
, low 10 bits (0x03FF
), scale is 0x0100
— 90 degrees; the high two bits are interpreted as follows: 0x4000
— X only, 0x8000
— Y only, 0xC000
— Z only.
If neither of the high bits are set, it’s a three-axis rotation. The next 10 bits (0x3FF0
) are the X rotation, the next 10 (including the following uint16_t
) (0x000F
, 0xFC00
) are the Y rotation, the next 10 (0x03FF
) are the Z rotation, same scale as before (0x0100
— 90 degrees).
Rotations are performed in Y, X, Z order.
All angle sets are two words and interpreted like the two-word sets in TR2/3, except that the word order is reversed.