Table of Contents
Meshes and Models
Overview
Nearly all of the non-geographic visual elements in TR (as well as a few parts of the landscape) consist of as meshes. A mesh is simply a list of vertices and how they’re arranged. The mesh structure includes a list of vertices as relative coordinates (which allows meshes to easily be placed anywhere in the world geometry), a list of normals (to indicate which side of each face is visible), and lists of Rectangles and Triangles, both textured and coloured. The elements of each [tr_face4] or [tr_face3] (or same version-specific) structure (Rectangles and Triangles) contain an offset into the Vertices[]
array for the mesh. Other arrays (Entities[]
, StaticMeshes[]
) do not reference the array Meshes[]
directly, but instead reference the array MeshPointers[]
, which points to locations inside of Meshes[]
, inside of which the meshes are stored in packed fashion.
While it may be not obvious, but every time you see mesh look is changed, it means that meshswap happened. There was never any other way to modify mesh looks in classic TRs.
Meshes
The sign of the number of normals specifies which sort of lighting to use. If the sign is positive, then external vertex lighting is used, with the lighting calculated from the room’s ambient and point-source lighting values. The latter appears to use a simple Lambert law for directionality: intensity is proportional to $\max(\langle \vec n, \vec o - \vec p \rangle, 0)$, with $\vec n$ being the normal, $\vec o$ the lit position, and $\vec p$ the light's position. If the sign is negative, then internal vertex lighting is used, using the data included with the mesh.
NumXXX
elements that precede them.
virtual struct tr_mesh // (variable length) { tr_vertex Centre; int16_t CollRadius; uint16_t Flags; // 0x01 for Flat Shaded, i.e. Normals should not affect lighting int16_t NumVertices; // Number of vertices in this mesh tr_vertex Vertices[NumVertices]; // List of vertices (relative coordinates) int16_t NumNormals; if(NumNormals > 0) tr_vertex Normals[NumNormals]; else int16_t Lights[abs(NumNormals)]; int16_t NumTexturedRectangles; // number of textured rectangles in this mesh tr_face4 TexturedRectangles[NumTexturedRectangles]; // list of textured rectangles int16_t NumTexturedTriangles; // number of textured triangles in this mesh tr_face3 TexturedTriangles[NumTexturedTriangles]; // list of textured triangles int16_t NumColouredRectangles; // number of coloured rectangles in this mesh tr_face4 ColouredRectangles[NumColouredRectangles]; // list of coloured rectangles int16_t NumColouredTriangles; // number of coloured triangles in this mesh tr_face3 ColouredTriangles[NumColouredTriangles]; // list of coloured triangles };
Centre
is usually close to the mesh’s centroid, and is the center of a sphere used for certain kinds of collision testing.
CollRadius
is the radius of that aforementioned collisional sphere.
NumNormals
: If positive, it is a number of normals in this mesh. If negative, it is a number of vertex lighting elements (abs
value).
Depending on a value of NumNormals
, next data block is interpreted either as Normals[]
array (in [tr_vertex] format) or Lights
array (just standard int16_t
values).
NumTexturedTriangles
and NumTexturedRectangles
are respectively the number of triangular and rectangular faces in this mesh. Corresponding TexturedTriangles
and TexturedRectangles
array contain textured triangles and rectangles themselves.
NumColoredTriangles
and NumColoredRectangles
are respectively the number of triangular and rectangular faces in this mesh. Corresponding ColoredTriangles
and ColoredRectangles
array contain colored triangles and rectangles themselves.
As coloured faces feature was removed since TR4, [tr_mesh] structure was changed, and contain no data for coloured faces anymore:
virtual struct tr4_mesh // (variable length) { tr_vertex Centre; int32_t CollRadius; int16_t NumVertices; // Number of vertices in this mesh tr_vertex Vertices[NumVertices]; // List of vertices (relative coordinates) int16_t NumNormals; if(NumNormals > 0) tr_vertex Normals[NumNormals]; else int16_t Lights[abs(NumNormals)]; int16_t NumTexturedRectangles; // number of textured rectangles in this mesh tr_face4 TexturedRectangles[NumTexturedRectangles]; // list of textured rectangles int16_t NumTexturedTriangles; // number of textured triangles in this mesh tr_face3 TexturedTriangles[NumTexturedTriangles]; // list of textured triangles };
Static Meshes
As the name tells, static meshes are meshes that don’t move (e.g. skeletons lying on the floor, spiderwebs, trees, statues, etc.) Usually it implies that static mesh is completely non-interactive, i.e. all it does is sitting there in place serving as an ornament.
StaticMeshes have two bounding boxes. First one serves as visibililty box, and other is the collisional box. The former is being used for visibility testing, and the latter is used for collision testing.
struct tr_staticmesh // 32 bytes { uint32_t ID; // Static Mesh Identifier uint16_t Mesh; // Mesh (offset into MeshPointers[]) tr_bounding_box VisibilityBox; tr_bounding_box CollisionBox; uint16_t Flags; };
struct tr_bounding_box // 12 bytes { int16_t MinX, MaxX, MinY, MaxY, MinZ, MaxZ; };
VisibilityBox
and CollisionBox
boundaries is always stay axis aligned even after applying tr_room_staticmesh::Rotation (always have 90 degrees step). Additionally, the test whether to rotate the box or not relies on the mesh’s rotation being an exact multiple of 0x4000 (aka 90 degrees). If this is not the case, the box is not rotated, which results in wrong collision checks.
Flags
:
- Bit 0 (
0x0001
): no collision - Bit 1 (
0x0002
): is visible
Models
This defines a list of contiguous meshes that comprise one object, which is called a model. This structure also points to the hierarchy and offsets of the meshes (MeshTree
), and also to the animations used (Animation
); these will be described in detail below. If the Animation index is -1, that means that there are no predefined animations, and entity’s movement is all generated by the engine; an example is Lara’s ponytail or rolling balls from TR4 and TR5.
Some entities are really stationary, such as locks and the skybox, and some are not rendered at all, such as “camera target” points to aim the camera at, flame emitters, AI objects and other service entities. Such invisible models are frequently called nullmeshes, because usually they have null mesh index specified for them, and never actually use it.
Sometimes, model may refer to sprite or sprite sequence to draw itself (for example, pick-up items and flame emitters). In this case, model is replaced with sprite in run-time. This behaviour is hardcoded for specific model IDs.
Sometimes, model may have two different versions defined in level files — one is normal, and another is low-detailed one, with the latter used when camera position gets too far from them. These are called MIP models, and mostly exist for NPCs (enemies). Usually, their type IDs are one off their normal counterparts (for example, skeleton type ID in TR4 is 35, and its MIP variation is 36).
struct tr_model // 18 bytes { uint32_t ID; // Type Identifier (matched in Entities[]) uint16_t NumMeshes; // Number of meshes in this object uint16_t StartingMesh; // Stating mesh (offset into MeshPointers[]) uint32_t MeshTree; // Offset into MeshTree[] uint32_t FrameOffset; // Byte offset into Frames[] (divide by 2 for Frames[i]) uint16_t Animation; // Offset into Animations[] };
There is an extra uint16_t
at the end of [tr_model] structure, which is always 0xFFEF
and used for alignment. Consider it while parsing.
Entities
Entities are the actual instances of entity types, consisting either of models or sprites (with the latter existing in TR1-2 only). For an entity to appear in a level, it must be referenced in the Models[]
array. Multiple instances of the same model are possible (e.g. two identical tigers in different rooms are represented using two entries in Entities[]
, one for each).
Entity structure has gone through different variations across game versions, so we’ll list them all.
TR1 Entity Structure
struct tr_entity // 22 bytes { int16_t TypeID; // Entity type ID (matched in Models[]) int16_t Room; int32_t x; // Item position in world coordinates int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; uint16_t Flags; };
TR2-3 Entity Structure
struct tr2_entity // 24 bytes { int16_t TypeID; int16_t Room; int32_t x; int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; int16_t Intensity2; // Like Intensity1, and almost always with the same value. uint16_t Flags; };
TR4-5 Entity Structure
struct tr4_entity // 24 bytes { int16_t TypeID; int16_t Room; int32_t x; int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; int16_t OCB; // Replaces Intensity2, see further for explanations. uint16_t Flags; };
TypeID
is used to assign appropriate action for this entity and/or locate the appropriate sprite sequence or model to draw. If TypeID
is zero, it means it’s playable character (i.e. Lara).
Room
is a room ID to which this particular entity belongs to. If room value was modified incorrectly, entity will glitch and, most likely, won’t appear in engine. That is, you can’t change entity position without complementary edit or Room
field.
Angle
is an Euler Yaw angle (i.e. “horizontal” rotation) stored in a special manner. To convert it to ordinary degrees, use this formula:
$\angle^\circ = \frac{Angle}{16384} \cdot -90$
Intensity2
field is missing in this game version, so the structure size is 2 bytes less.
Intensity1
: If not -1, it is a value of constant lighting. -1 means “use mesh lighting”.
Flags
value contain packed list of several parameters:
- Bit 7 (
0x0080
) — Clear Body flag. It is used together with Clear Bodies trigger action to remove the body of dead enemy from the level to conserve resources. - Bit 8 (
0x0100
) — Invisible flag. If entity has this flag set, it will be invisible on start-up. However, it only works for specific types of entities. It is primarily used with pick-ups or other entities which should appear at certain point only after activation, but are visible by default. - Bits 9..13 (
0x3E00
) — Activation Mask for this entity. As you already learned in Trigger Actions chapter, entity is only activated when activation mask is all set (i.e. all 5 bits are set, and value is0x1F
). However, activation mask doesn’t strictly required to be set by trigger — level editor allows to pre-define activation mask, so entity will bear specific activation mask layout on level start-up.
If activation mask was pre-set to 0x1F
(all set), entity will activate immediately after level loading, and engine will also reset activation mask to zero and mark entity as inactive, effectively swapping “inactive” state with “active”. That is, when player will activate such pre-activated entity with a trigger, it will actually “deactivate”, et cetera. Most prominent example of this behaviour is pre-opened grated door in Tomb of Qualopec.
Object Code Bit
In TR4 and TR5, Intensity2
field was replaced with completely new one, called Object Code Bit (or OCB
). OCB allows to alter entity behaviour based on its value, thus providing very basic “script-like” functionality. For example, flame emitter entities have a case switch for OCB value, and each valid OCB value produces different result — flame emttier acts either as a static flame, as a directional flame, as a lightning, and so on.
More detailed description of OCB is provided in this section.
Sprites
These are “billboard” objects that are always rendered perpendicular to the view direction. These are used for text and explosion effects and similar things; they are also used for some scenery objects and pickup items, though this use gets less as one goes from TR1 to TR3. The various “Sides” below are the positions of the sprite sides relative to the sprite’s overall position, measured in TR’s world-coordinate units.
struct tr_sprite_texture // 16 bytes { uint16_t Atlas; uint8_t x; uint8_t y; uint16_t Width; // (ActualWidth * 256) + 255 uint16_t Height; // (ActualHeight * 256) + 255 int16_t LeftSide; int16_t TopSide; int16_t RightSide; int16_t BottomSide; };
x
and y
values are not used in this version. Additionally, formula for Width
and Height
is changed: now it’s $(\text{ActualWidth} - 1) \cdot 256$ and $(\text{ActualHeight} - 1) \cdot 256$ respectively.
Sprite Sequences
These are collections of sprites that are referred to as a group. The members of this group can be cycled through (animated sprites such as flames, blood splats or explosions) or selected in other ways (text). Some sequences have only one member; this is done so as to access all the sprites in the same way.
struct tr_sprite_sequence // 8 bytes { int32_t SpriteID; // Sprite identifier int16_t NegativeLength; // Negative of ``how many sprites are in this sequence'' int16_t Offset; // Where (in sprite texture list) this sequence starts };