Entity Programming - Introduction to Entities with Animated Models Last edited 8 months ago2023-08-16 19:53:25 UTC

Half-Life Programming

Opening words

Half-Life has a neat animation system, at least for 1998. It's got skeletal animation, which is a big step up from Quake's vertex animation. There is a lot more to it though. It's possible, for instance, to rotate certain bones from code, which can be very useful for entities like security cameras, or robot arms. It is also possible to attach entities onto bones, and many more.

We'll analyse it in 3 aspects, gradually linking one to another: We'll also touch a couple of concepts unrelated to animation: bodygroups and skins.

Concepts of animation in Half-Life

Obviously, you already know what animation is, and perhaps what a skeleton is. Half-Life models consist of bones that can change position and orientation depending on the current animation keyframe. In other words, they can be animated.
barney.mdl with bonesbarney.mdl with bones
Animations in general are defined by name, length in keyframes and a framerate. Note that they're also called "sequences" in Half-Life.

Half-Life models also consist of the following: There are a couple of other concepts, such as animation blending and gait sequences. Gait sequences are essentially a way to play multiple animations on the same model. In Half-Life, these are specific to players and one animation can affect the torso, while another can affect the legs. This is how player models in HLDM can play running and shooting animations simultaneously.
Blends are basically special types of sequences that combine two subsequences with a blending mode, and their blending is controlled by code.

Low-level animation control

Now, the question is: how are these concepts concretely implemented? StudioModelRenderer.cpp on the clientside contains a lot of code about bone transformation, blending and controllers. It involves a lot of interpolation and quaternion maths, so much that it deserves its very own article. As such, it won't be covered here.

In the context of entity programming, what we actually do is control the parameters which will be interpreted by the engine and the studio model renderer. These parameters are located in entvars_t:
int     sequence;       // animation sequence
int     gaitsequence;   // movement animation sequence for player (0 for none)
float   frame;          // % playback position in animation sequences (0..255)
float   animtime;       // world time when frame was set
float   framerate;      // animation playback rate (-8x to 8x)
byte    controller[4];  // bone controller setting (0..255)
byte    blending[2];    // blending amount between sub-sequences (0..255)
sequence is the animation ID. In a modern engine, you'd typically set animations by name, but here, you set animations by number. It is possible to write a utility function to set animations by name though. You will see how later.
gaitsequence is player-specific, and it controls the leg animation.
frame is the current animation frame. In brushes and sprites, it controls which texture frame to display. In models, it controls the percentage of the animation progress, so for example, if it's 127, it'll be roughly 50% complete.
animtime is the server time when frame was set. This way, the client will have a reference as to when the animation started playing, and as such, will be able to interpolate it correctly.
framerate is quite self-explanatory, except it doesn't exactly control the animation framerate directly. Instead, it is a multiplier, so a value of 1.0 will mean normal playback speed.
controller is an array of 4 bone controller channels. If you have a controller that goes between -180 and +180 degrees, then setting its value to 127 will effectively set its angle to 0. Setting its value to 0 will mean an angle of -180°.
blending only works if sequence or gaitsequence are special blend sequences, and it basically blends between their subsequences.

We also have these:
int    skin;
int    body; // sub-model selection for studiomodels
skin simply selects the current texture. Let's say we have the following texture groups in some model:
$texturegroup arms
{
  { "newarm.bmp" "handback.bmp" "helmet.bmp" }
  { "newarm(dark).bmp" "handback(dark).bmp" "helmet(dark).bmp" }
}
skin would basically control which row here is selected.

The way body works is by simply packing multiple integers into itself.
For example, let's say we have a model with 3 skin groups, where group A has 4 variations, group B has 7 and group C has 13.
Group A would need only 2 bits, B would need 3, and C would need 4:
Binary numbers! Revise themBinary numbers! Revise them
You do not have to perform the packing/unpacking manually, though. More about that in the high-level section.

Studio model headers

We can also obtain information about the model itself and its animations, by obtaining the studio model header:
// Get a model pointer for this entity
void* pmodel = GET_MODEL_PTR( pEntity->edict() );
// Interpret that as a studio model header
studiohdr_t* pStudioHeader = (studiohdr_t*)pmodel;
From here onward, you can read all kinds of information about the model. The name, the bounding boxes, flags, number of bones, animations and many more. For example, obtaining information about a group of bodyparts would go like this:
int groupNumber = 2;
mstudiobodyparts_t* pBodyGroup = (mstudiobodyparts_t*)((byte*)pStudioHeader + pStudioHeader->bodypartindex) + groupNumber;

ALERT( at_console, "Bodygroup %i has %i bodyparts\n", groupNumber, pBodyGroup->nummodels );
As you can see, the general pattern for obtaining model data is this:
data_t* pData = (data_t*)((byte*)pStudioHeader + pStudioHeader->dataindex);
This way, we may write a utility function that sets an entity's animation by name. Speaking of which...

High-level animation system

Valve programmers already wrote such a utility ages ago, that is LookupSequence.
You may find it in animation.cpp, and this is essentially how it works:
mstudioseqdesc_t    *pseqdesc = (mstudioseqdesc_t *)((byte *)pstudiohdr + pstudiohdr->seqindex);

for (int i = 0; i < pstudiohdr->numseq; i++)
{
    if (stricmp( pseqdesc[i].label, label ) == 0)
        return i;
}

return -1;
Not just that, they wrote an entire base class to handle animated model entities.

CBaseAnimating

It inherits from CBaseDelay, with a lot of animation utilities added on top: If you look further, this turns out to be a wrapper for the global functions in animation.cpp.
Either way, if you write an entity that is intended to use animated models, you will definitely want it to inherit from CBaseAnimating. An example animating entity will be shown on the next page.

Comments

You must log in to post a comment. You can login or register a new account.