Skip to content

VXL and HVA models

Formats

Draws an object from a voxel model whose layers are placed by a companion motion file.

Format
Kind
Binary format
Extension
.VXL, .HVA
Role
Model

A voxel model is a .VXL and a .HVA under the same base name, loaded as a pair. The .VXL holds the model itself, divided into layers; the .HVA holds one transform per layer per frame, and it is what puts each layer where it belongs. One pair is one piece of an object, so a turreted vehicle draws from three of them.

An ObjectType marked Voxel=yes fetches its whole set at once, from base names built out of its Image ID. The table sets each base name against the condition under which it is looked for at all. The column to read is the second one: what a type asks for is settled by the kind of type it is and whether it carries a turret, never by what its artwork happens to contain, so anything that is not a UnitType is asked for a turret and a barrel whether it could use them or not.

Base nameLooked for when
<Image ID>Always; this is the body
<Image ID>TURThe type is not a UnitType, or is a UnitType with a turret
<Image ID>BARLThe same condition as the turret
<Image ID>WThe type is a UnitType without a turret whose ID is exactly APC

Only the .VXL decides whether a piece is looked for at all. Once one is present its .HVA is opened whether or not it exists. Within this set, a model with no motion file beside it counts as a piece that failed rather than as a piece without motion; the shape-drawn case below is fetched by a separate routine that records no failure.

A UnitType that is drawn from shapes rather than voxels still picks up <Image ID>TUR and <Image ID>BARL when it has a turret and its shape artwork loaded. A building takes its voxel turret and barrel from a base name of its own instead of from its Image ID; TurretAnimIsVoxel and VoxelBarrelFile cover how that name is arrived at. A voxel animation type loads a .VXL alone and no motion file with it, unless it is set to borrow another type’s body, turret or barrel — ShareSource covers that — in which case it takes that pair whole. One further pair, DPOD.VXL and DPOD.HVA, is loaded once at startup under those fixed names.

Both files are found through the ordinary game file layer, so either can sit in a MIX archive or loose in the game directory.

If any piece fails — the body model missing, or any model or motion file that is present refusing to load — the body and turret pieces are released and the type is left with no voxel artwork, whatever else loaded successfully. A barrel that loaded is not released with them.

Where the whole set loads, the largest X, Y or Z size among the body model’s layers, or 8 where that is larger, becomes the size the type reports for its artwork.

Voxel artwork is not stored in a save game; save games covers what a restored type is given in its place.

The loader reads a .VXL in this order:

  • a header, carrying an internal name, the number of palettes, the number of layers, the number of layer info records and the size of the voxel body;
  • one palette per the header’s count, each a pair of remap bounds followed by a red, a green and a blue byte for each of 256 colors;
  • one header per layer, carrying a name and the index of the first info record belonging to that layer;
  • the voxel body, run length encoded down each column, with a color index and a normal index for every voxel in a run;
  • one record per layer info, carrying the offsets within the body of its two span tables and of its voxel data, the scale the layer was built at, a placement transform, two opposite corners of its bounding box, its X, Y and Z sizes in voxels, and which normal table its voxels index.

A layer names its first info record and may own several that follow it, so the layer count and the info count need not agree.

Several of those parts never reach the routines that draw the model. The palettes are stepped over rather than loaded — no caller asks for the file’s own colors, so a model is always drawn from VOXELS.VPL, which startup reads separately; the palette count still matters, because it is what sizes the step. The internal names in the header and in each layer header are never consulted: files are found by filename, and a layer is paired with its motion by position. The placement transform is converted into the engine’s own matrix layout as it is read and then left alone, so a layer’s position comes entirely from the motion file.

A normal table selector of zero marks a layer as carrying no usable normals, and it is drawn without lighting. Values 1 through 4 select one of the four normal tables the engine holds. Nothing rejects a larger one: the selector is an unvalidated byte from the file, the drawing pass tests only whether it is zero, and the routine that builds the lighting lookup indexes both its table of normal sets and its table of set sizes with it, so a value above four reads past the end of each.

The loader reads a .HVA as a header carrying an internal name, the number of frames and the number of layers; then a table of layer names, sixteen bytes each and one per layer, which is stepped over; then, frame by frame, one transform of twelve floating point values for each layer. As the pair is loaded, the translation of every one of those transforms is multiplied by the scale recorded in the model’s first layer info, which brings the animation into the model’s scale.

Frames are asked for modulo the frame count, so an animation loops rather than running off its end. A file that declares no frames at all still loads, and the modulus against a count of zero is what stops the game the first time the model is drawn.

Source files

Linked at revision 59fae722af8c.