Tutorial: The packing (and non-packing) of bodygroups and skin groups in GoldSource models Last edited 1 month ago2024-10-26 15:15:27 UTC

A short explainer on the why and how of packing and non-packing of body and skin groups in GoldSource models.
This page will use a lot of QC syntax used by the studiomodel compiler (studiomdl.exe). Please check out The303's QC reference page for more information.

Body and bodygroups

A GoldSrc model consists, in one part, of meshes. As a modeller, you can have one or several meshes included in the final model. You can also set up "body groups" in which only one mesh in the group is rendered at any one time.
$body studio "<reference_smd>"
The above sets the main reference mesh. This mesh is always rendered.
$bodygroup <groupname>
{
    studio "<part1_smd>"
    studio "<part2_smd>"
    blank // optional
}
The above sets a body group. Only one of the meshes in the group is rendered at any one time. If you put blank as one of the lines in the group you can choose to render nothing for the group.
$bodygroup <groupname>
{
    studio "<onlypart_smd>"
}
This group above, with only a single member, will always be rendered. This is often used when your reference mesh is too big and need to be split up into several meshes (i.e. smd files), so you reference each part in its own single-item bodygroup.

Packing for body value

Bodygroups are packed into the body keyvalue first by fitting them into lists that are powers of 2 long e.g. 2,4,8,16... and then bit-shifting the subsequent groups the appropriate places to the left.

That admittedly sounds like gibberish... Instead, let's clarify the concept by visualizing things in binary. First, a representation of all the binary places for 32 bits (the size of body):
xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
Here's the $body and $bodygroup of models/hgrunt_opfor.mdl (from Opposing Force). We will comment our enumeration of the values in decimal and binary.
$body studio "grunt_fatigues_reference"
$bodygroup heads
{                                               // DEC  BIN
    studio "grunt_head_mask_reference"          // 0    000
    studio "grunt_head_commander_reference"     // 1    001
    studio "grunt_head_shotgun_reference"       // 2    010
    studio "grunt_head_saw_wht_reference"       // 3    011
    studio "grunt_head_saw_blk_reference"       // 4    100
    studio "grunt_head_MP_reference"            // 5    101
    studio "grunt_head_major_reference"         // 6    110
    studio "grunt_head_commander_blk_reference" // 7    111
}
$bodygroup torso
{
    studio "grunt_reg_torso_reference"          // 0    000
    studio "grunt_saw_gunner_torso_reference"   // 1    001
    studio "grunt_noback_torso_reference"       // 2    010
    studio "grunt_shotgun_torso_reference"      // 3    011
}
$bodygroup weapons
{
    studio "MP5_reference"                      // 0    000
    studio "shotgun_reference"                  // 1    001
    studio "SAW_reference"                      // 2    010
    blank                                       // 3    011
}
As you can see, the heads group takes up three binary places to enumerate everything. The other 2 groups on the other hand only goes up to 2 binary places. This means the heads group occupy 3 bits, and torso and weapons groups take up 2 bits. Let's map that onto our binary representation:
0000 0000 0000 0000 0000 0000 0CCB BAAA
The first group (A) goes to the right-most place on the binary number representation. The second group (B) is to the left of (A) shifted left by three places (A's width), and the third (C) by 5 (width of A+B). The other places are unused, so we fill it with zeroes.

Now let's say we want a combination of the following groups: Let's fill up our binary representation with our values:
//                             CCB BAAA
0000 0000 0000 0000 0000 0000 0001 0110
And that translates into decimal as 22. We can then take this value for body in entities like cycler or monster_generic.
You can double check with HLAM by selecting the same combo in its Body Parts widget, and checking the body value it it outputs at the bottom of the widget:
User posted image
As another example, here's the $body and $bodygroup part of a QC file for a complex model that had to have its meshes split into 4 smd files:
$body studio "space_shuttle.1_p1"
$bodygroup pt2 {
    studio "space_shuttle.1_p2"
}
$bodygroup pt3 {
    studio "space_shuttle.1_p3"
}
$bodygroup pt4 {
    studio "space_shuttle.1_p4"
}
None of the groups have more than one entry, and this is reflected in HLAM where there's only 1 selection for all groups, and the resulting body value is always 0:
User posted image

Texture groups

Texture groups provide a way to switch out any texture used by the meshes into one or more alternative sets.
$texturegroup <groupname>
{
    { "tex1_base.bmp" "tex2_base.bmp" ⋯ "texN_base.bmp" }
    { "tex1_alt1.bmp" "tex2_alt1.bmp" ⋯ "texN_alt1.bmp" }
    { "tex1_alt2.bmp" "tex2_alt2.bmp" ⋯ "texN_alt2.bmp" }
            ⋮               ⋮          ⋱       ⋮
    { "tex1_altN.bmp" "tex2_altN.bmp" ⋯ "texN_altN.bmp" }
}
In the first row, you list all the textures from the base model (i.e. the textures you sculpt the models with) that you want to have different skins for. In the subsequent rows, you list the alternative textures to replace the base textures for that selection, according to column position. In the above example, selecting the third row means "tex1_base.bmp" is replaced with "tex1_alt2.bmp", "tex2_base.bmp" with "tex2_alt2.bmp", and so on...
Reminders
  • All texture files referenced need to be in indexed 8-bit mode, otherwise studiomdl will fail to compile the model.
  • In QC files, the curly brackets must be surrounded on both sides by whitespace. Writing a $texturegroup texture entry row with an item directly touching any curly brackets e.g. {"alt1.bmp"} will crash studiomdl.
This works well for one dimension of skin changes e.g. changing the skin colour of NPCs or the paint colour of cars. If you want to add a second dimension e.g. clean vs blood-soaked vs torn clothing, or different decal patterns on cars, you just have a second $texturegroup, right?

Nope.

Unfortunately, unlike meshes, GoldSource doesn't support multiple texture groups. Even studiomdl (the model compiler) will only compile the first one found in the QC file, and ignore all subsequent $texturegroups.

Therefore, we need to pack all possible texture combinations into a single $texturegroup. Combining 2 sets is just a matter of creating unique pairs from the elements of the 2 sets. An example of set A of size 2 and set B of size 3 would be:
  A = ⎡X⎤
      ⎣Y⎦
  B = [ 1  2  3]
A×B = ⎡X1 X2 X3⎤
      ⎣Y1 Y2 Y3⎦
Then we roll the elements of A×B into a list, and expand the elements to their constituent texture(s). The following is an example of a model with 2 head and 3 fatigue variations:
Hypothetical texture groups
// elements of A
$texturegroup headvars {
    { "helmet.BMP"  "beret_black.BMP" } // X
    { "helmet2.BMP" "beret_red.BMP"   } // Y
}

// elements of B
$texturegroup fatigues {
    { "fatigues.BMP"      "boots.BMP"      } // 1
    { "fatiguesDIRT1.BMP" "bootsDIRT1.BMP" } // 2
    { "fatiguesDIRT2.BMP" "bootsDIRT2.BMP" } // 3
}
Actual texture group used
// combination of A and B
$texturegroup variations
{
//  [--------ITEMS FROM A---------] [-----------ITEMS FROM B-----------]
  { "helmet.BMP"  "beret_black.BMP" "fatigues.BMP"      "boots.BMP"      } // X1
  { "helmet2.BMP" "beret_red.BMP"   "fatigues.BMP"      "boots.BMP"      } // Y1
  { "helmet.BMP"  "beret_black.BMP" "fatiguesDIRT1.BMP" "bootsDIRT1.BMP" } // X2
  { "helmet2.BMP" "beret_red.BMP"   "fatiguesDIRT1.BMP" "bootsDIRT1.BMP" } // Y2
  { "helmet.BMP"  "beret_black.BMP" "fatiguesDIRT2.BMP" "bootsDIRT2.BMP" } // X3
  { "helmet2.BMP" "beret_red.BMP"   "fatiguesDIRT2.BMP" "bootsDIRT2.BMP" } // Y3
}
skin values 0 through 5skin values 0 through 5
Since vanilla Half-Life doesn't come with multiple texture groups (nor do the studiomdl compiler supports them), obviously the vanilla SDK entities don't intelligently set the model skin using offsets, instead only using hardcoded values e.g. 0 or 1. You need to edit the code for the relevant monster's Spawn function (and any other relevant functions) to access both groups with offsets and math. We won't be getting into that in detail here, though. Sorry.
A third dimension is possible still, but now we're stretching the limits of both the model format and human comprehension. It's best to keep the third dimension as separate models.
AB = ⎡X1⎤
     ⎢Y1⎥
     ⎢X2⎥
     ⎢Y2⎥
     ⎢X3⎥
     ⎣Y3⎦
C = [  p   q   r]
AB×C = ⎡X1p X1q X1r⎤
       ⎢Y1p Y1q Y1r⎥
       ⎢X2p X2q X2r⎥
       ⎢Y2p Y2q Y2r⎥
       ⎢X3p X3q X3r⎥
       ⎣Y3p Y3q Y3r⎦
Brain ouchie... I wouldn't impose such torment to anyone... Seriously don't do this.

Summary

1 Comment

Commented 3 months ago2024-09-18 05:20:19 UTC Comment #106405
Well done. About time we got a tutorial going into detail about the body property packing. 🙂

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