Table of Contents

Room Geometry

Overview

A room in TR2 is simply a rectangular three-dimensional area. A room may be “indoors” or “outdoors,” may or may not be enclosed, may be accessible or inaccessible to Lara, may or may not contain doors or objects.

All rooms have “portals,” called “doors” in some documentation, which are pathways to adjacent rooms. There are two kinds of portals — visibility portals and collisional portals. Visibility portals are for determining how much of a room (if any) is visible from another room, while collisional portals are for enabling an object to travel from one room to another.

The visibility portals are most likely for doing “portal rendering”, which is a visibility-calculation scheme that goes as follows: the viewpoint is a member of some room, which is then listed as visible from it. This room’s portals are checked for visibility from that viewpoint, and visible portals have their opposite-side rooms marked as visible. These rooms are then checked for portals that are visible from the viewpoint through the viewpoint’s room’s portals, and visible ones have their opposite-side rooms marked as visible. This operation is repeated, with viewing through intermediate portals, until all visible portals have been found. The result is a tree of rooms, starting from the viewpoint’s room; only those rooms and their contents need to be rendered.

It is clear that both visibility and collision calculations require that objects have room memberships given for them, and indeed we shall find that most map objects have room memberships.

Rooms may overlap; as we shall see, this is involved in how horizontal collisional portals are implemented. However, different rooms may overlap without either being directly accessible from the other; there are several inadvertent examples of such “5D space” in the Tomb Raider series. The only possibly deliberate example I know of is the flying saucer in “Area 51” in TR3, whose interior is bigger than its exterior.

A room can have an “alternate room” specified for it; that means that that room can be replaced by that alternate as the game is running. This trick is used to produce such tricks as empty rooms vs. rooms full of water, scenery rearrangements (for example, the dynamited house in “Bartoli’s Hideout” in TR2), and so forth. An empty room is first created, and then a full room is created at its location from a copy of it. The empty room then has that full room set as its alternate, and when that room is made to alternate, one sees a full room rather than an empty one.

The rooms are stored sequentially in an array, and “Room Numbers” are simply indices into this array (e.g. “Room Number 5” is simply Rooms[5]; the first room is Rooms[0]).

Rooms are divided into sectors (or squares), which are 1024×1024 unit squares that form a grid on the $X-Z$ plane. Sectors are the defining area for floor/ceiling heights and triggers (e.g. a tiger appears and attacks when Lara steps on a given square); the various attributes of each sector are stored in the Sector Data (described in this section) and the [FloorData]. As an aside, Sectors correspond to the “squares,” easily visible in all of the Tomb Raider games, that experienced players count when gauging jumps; they also account for some of the game’s less-appealing graphic artifacts. Careful tiling and texture construction can make these “squares” almost invisible.

Each room has two types of surface geometry — rendered and collisional. The former are what is seen, while the latter control how objects collide and interact with the world. Furthermore, these two types are specified separately in the room data — each type is completely independent of other, i. e. collisional geometry shouldn’t exactly match visible room geometry.

While this distinctive feature was never used in originals (collisional room “meshes” fully resembled visible room “meshes”), it is now extensively used by level editing community with the help of a program called meta2tr. This utility allows level builder to replace visible geometry generated by TRLE with any custom geometry, usually modelled in Metasequoia 3D editor (hence the name of meta2tr utility).

Rooms are defined with a complex structure, which is described below “inside-out,” meaning that the smaller component structures are described first, followed by the larger structures that are built using the smaller structures.

Room Structures

Room header

$X / Z$ indicate the base position of the room mesh in world coordinates ($Y$ is always zero-relative)

struct tr_room_info    // 16 bytes
{
    int32_t x;             // X-offset of room (world coordinates)
    int32_t z;             // Z-offset of room (world coordinates)
    int32_t yBottom;
    int32_t yTop;
};

yBottom is actually largest value, but indicates lowest point in the room. yTop is actually smallest value, but indicates highest point in the room.

TR5 uses an extended version of this structure:

struct tr5_room_info    // 20 bytes
{
    int32_t x;             // X-offset of room (world coordinates)
    int32_t y;             // Y-offset of room (world coordinates) - only in TR5
    int32_t z;             // Z-offset of room (world coordinates)
    int32_t yBottom;
    int32_t yTop;
};

The additional y value is usually 0.

Portal Structure

These portals, sometimes called “doors”, define the view from a room into another room. This can be through a “real” door, a window, or even some open area that makes the rooms look like one big room. Note that “rooms” here are really just areas; they aren’t necessarily enclosed. The portal structure below defines only visibility portals, not an actual door model, texture, or action (if any). And if the portal is not properly oriented, the camera cannot “see” through it.

struct tr_room_portal  // 32 bytes
{
    uint16_t  AdjoiningRoom; // Which room this portal leads to
    tr_vertex Normal;
    tr_vertex Vertices[4];
};

Normal field tells which way the portal faces (the normal points away from the adjacent room; to be seen through, it must point toward the viewpoint).

Vertices are the corners of this portal (the right-hand rule applies with respect to the normal). If the right-hand-rule is not followed, the portal will contain visual artifacts instead of a viewport to AdjoiningRoom.

The original portal testing algorithm performs a breadth-first visibility check of the bounding boxes of the screen-projected portal vertices, and also compares the vector from the camera to the first portal vertex to its normal. This works pretty fine, unless there are denormalized bounding boxes, which the original TR solves by expanding the portal bounding box to its maximum if a projected edge crosses the screen plane or its boundaries.

Room Sector Structure

Core: FLOOR_INFO

All the geometry specified here is collisional geometry.

struct tr_room_sector // 8 bytes
{
    uint16_t FDindex;    // Index into FloorData[]
    uint16_t BoxIndex;   // Index into Boxes[] (-1 if none)
    uint8_t  RoomBelow;  // 255 is none
    int8_t   Floor;      // Absolute height of floor
    uint8_t  RoomAbove;  // 255 if none
    int8_t   Ceiling;    // Absolute height of ceiling
};

Floor and Ceiling are signed numbers of 256 units of height (relative to 0) — e.g. Floor 0x04 corresponds to $Y = 1024$ in world coordinates. Therefore, 256 units is a minimum vertical stride of collisional geometry. However, this rule could be broken by specific entities, which Lara can stand on. But horizontal sector dimensions, which, as mentioned earlier, are 1024 x 1024 (in world coordinates), could not. Therefore, minimal horizontal platform dimensions, on which Lara can stand and grab, are 1024 x 1024 as well.

This implies that, while $X$ and $Z$ can be quite large, $Y$ is constrained to -32768..32512.

Floor and Ceiling value of 0x81 is a magic number used to indicate impenetrable walls around the sector. Floor values are used by the game engine to determine what objects Lara can traverse and how. Relative steps of 1 (-256) can be walked up; steps of 2..7 (-512..-1792) can/must be jumped up; steps larger than 7 (-2048..-32768) cannot be jumped up (too tall).

RoomAbove and RoomBelow values indicate what neighboring rooms are in these directions — the number of the room below this one and the number of the room above this one. If RoomAbove is not none, then the ceiling is a collisional portal to that room, while if RoomBelow is not none, then the floor is a collisional portal to that room.

Also, RoomBelow value is extensively used by engine to determine actual sector data and triggers in so-called stacked room setups, when one room is placed above another through collisional portal. The thing is, engine uses sector data and triggers only for the lowest sector of the stacked room setup, so it recursively scans for a lowest room to determine which sector to use.

FDindex is a pointer to specific entry in [FloorData] array, which keeps all the information about sector flags, triggers and other parameters. While it is implied that one FDindex entry may be shared between several sectors, it is usually not the case with original Tomb Raider levels built with TRLE. However, Dxtre3d takes advantage of this feature and may optimize similar sectors to share same FDindex pointer.

BoxIndex is a pointer to special [Boxes] array entry, which is basically a subset of sectors with same height configuration. It is primarily used for AI pathfinding (see the Non-player character behaviour chapter for more details).

In these games, BoxIndex field is more complicated:

struct tr3_room_sector // 8 bytes
{
    uint16_t FDindex;    // Index into FloorData[]
    uint16_t Material : 4; // Material index, used
    uint16_t Box : 11; // Actual box index
    uint16_t Stopper : 1;
    uint8_t  RoomBelow;  // 255 is none
    int8_t   Floor;      // Absolute height of floor
    uint8_t  RoomAbove;  // 255 if none
    int8_t   Ceiling;    // Absolute height of ceiling
};
Hex value 0x8000 0x7FF0 0x000F
Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Field Stopper Box Material

Material is the material index, which is used to produce specific footstep sound, when Lara is walking or running in this sector. On PlayStation game versions, this index was also used to determine if footprint textures should be applied to this particular place. Both procedures are invoked via FOOTPRINT_FX flipeffect, which is described in corresponding section.

Majority of material index values are the same across game versions, but some of them exist only in particular game. Here is the description:

Mud, snow, sand, grass and maybe some other materials produce footprints in PlayStation version.

Furthermore, in TR3-5, actual box index may contain special value 2047 (full 1s), which is most likely indicates that this sector is a slope on which Lara can slide (and, therefore, possibly impassable by most NPCs).

Room Light Structure

TR engines always used static room lights only for processing lighting on entities (such as Lara, enemies, doors, and others). This is called external lighting. For room meshes, they used so-called internal, or pre-baked lighting, which is done on level building stage: lights are calculated and applied to room faces via vertex colours. There is no way to change room lighting when the level is compiled — meaning, any changes in light positions, intensities and colour won’t affect room faces.

There are four different types of room light structures. First one is used in TR1-2, second is used in TR3, third is used in TR4, and fourth is used in TR5. Here is the description of each:

TR1 Room Lighting

struct tr_room_light   // 18 bytes
{
     int32_t x, y, z;       // Position of light, in world coordinates
    uint16_t Intensity1;    // Light intensity
    uint32_t Fade1;         // Falloff value
};

X/Y/Z are in world coordinates. Intensity1/Intensity2 are almost always equal. This lighting only affects externally-lit objects. Tomb Raider 1 has only the first of the paired Intensity and Fade values.

Intensity1 ranges from 0 (dark) to 0x1FFF (bright). However, some rooms occasionally have some lights with intensity greater than 0x1FFF (for example, look at room #9, 2nd light in level1.phd). Fade1 is the maximum distance the light shines on, and ranges from 0 to 0x7FFF.

TR2 Room Lighting

TR2 uses an extended version of TR1 light structure:

struct tr2_room_light   // 24 bytes
{
     int32_t x, y, z;       // Position of light, in world coordinates
    uint16_t Intensity1;    // Light intensity
    uint16_t Intensity2;    // Only in TR2
    uint32_t Fade1;         // Falloff value
    uint32_t Fade2;         // Only in TR2
};

Intensity2 and Fade2 values are seemingly not used. Intensity1 can go very well beyond 0x1FFF, right to 0x7FFF (ultra bright light). Above 0x7FFF, it is always black, so the number is pseudo-signed (negative values are always treated as zero).

TR3 Room Lighting

TR3 introduced a rudimentary “light type” concept. Although only possible types are sun (type 0) and point light (type 1). It is not clear how sun affetcts room lighting, but somehow it interpolates with all other light objects in a given room. Both light types are kept using the same structure, with two last 8 bytes used as union.

struct tr3_room_light   // 24 bytes
{
        int32_t x, y, z;       // Position of light, in world coordinates
      tr_colour Colour;        // Colour of the light
        uint8_t LightType;     // Only 2 types - sun and point lights
 
  if (LightType == 0) // Sun
  {
        int16_t nx, ny, nz; // Most likely a normal
        int16_t Unused;
  }
  else if (LightType == 1) // Point
  {
        int32_t Intensity;
        int32_t Fade;          // Falloff value
  }
};

Intensity is the power of the light and ranges mainly from 0 (low power) to 0x1FFF (high power). Fade is the distance max the light can shine on. Range is mainly from 0 to 0x7FFF.

TR4 Room Lighting

struct tr4_room_light   // 46 bytes
{
        int32_t x, y, z;       // Position of light, in world coordinates
      tr_colour Colour;        // Colour of the light
 
        uint8_t LightType;
        uint8_t Unknown;       // Always 0xFF?
        uint8_t Intensity;
 
          float In;            // Also called hotspot in TRLE manual
          float Out;           // Also called falloff in TRLE manual
          float Length;
          float CutOff;
 
          float dx, dy, dz;    // Direction - used only by sun and spot lights
};

LightType was extended and is now somewhat similar to D3D light type, but there are some differences.

Fog bulb is a special case of room light, which actually don’t work as usual light. It serves as a point in space, where a kind of volumetric fog effect is generated. It works only if user has enabled corresponding option in game setup.

Fog bulbs don’t use Colour field to define its colour. However, Red field of a Colour structure is used to define fog density. Colour itself can be only changed with special flipeffect #28 trigger function, which takes a value from the timer field of the trigger to index into hardcoded RGB table of pre-defined colours. The table consists of 28 RGB values:

0 = 0,0,0 7 = 0,64,192 14 = 111,255,22321 = 0,30,16
1 = 245,200,60 8 = 0,128,0 15 = 244,216,15222 = 250,222,167
2 = 120,196,112 9 = 150,172,15716 = 248,192,6023 = 218,175,117
3 = 202,204,23010 = 128,128,12817 = 252,0,0 24 = 225,191,78
4 = 128,64,0 11 = 204,163,12318 = 198,95,87 25 = 77,140,141
5 = 64,64,64 12 = 177,162,14019 = 226,151,11826 = 4,181,154
6 = 243,232,23613 = 0,223,191 20 = 248,235,20627 = 255,174,0

TR5 Room Lighting

struct tr5_room_light   // 88 bytes
{
        float x, y, z;       // Position of light, in world coordinates
        float r, g, b;       // Colour of the light
 
       uint32_t shadow;    // Dummy value = 0xCDCDCDCD
 
        float In;            // Cosine of the IN value for light / size of IN value
        float Out;           // Cosine of the OUT value for light / size of OUT value
        float RadIn;         // (IN radians) * 2
        float RadOut;        // (OUT radians) * 2
        float Range;         // Range of light
 
        float dx, dy, dz;    // Direction - used only by sun and spot lights
      int32_t x2, y2, z2;    // Same as position, only in integer.
      int32_t dx2, dy2, dz2; // Same as direction, only in integer.
 
      uint8_t LightType;
 
      uint8_t Filler[3];     // Dummy values = 3 x 0xCD
};
struct tr5_fog_bulb   // 36 bytes
{
        float x, y, z;       // Position of light, in world coordinates
        float rad, sqrad;
        float den;
        float r, g, b;
};

x,y,z values shouldn’t be used by sun type light, but sun seems to have a large x value (9 million, give or take), a zero y value, and a small z value (4..20) in the original TR5 levels.

In and Out values aren’t used by sun type. For the spot type, these are the hotspot and falloff angle cosines. For the light and shadow types, these are the TR units for the hotspot / falloff (1024 = 1 sector).

RadIn, RadOut and Range are only used by the spot light type.

dx, dy and dz values are used only by the sun and spot type lights. They describe the directional vector of the light. This can be obtained by:

x2, y2, z2, dx2, dy2 and dz2 values repeat previous corresponding information in long data types instead of floats.

Room Vertex Structure

This defines the vertices within a room. As mentioned above, room lighting is internal vertex lighting, except for necessarily external sources like flares, flame emitters and gunflashes. Room ambient lights and point sources are ignored.

As TR3 introduced colored lighting, room vertex structure drastically changed. It changed once again in TR5, when floating-point numbers were introduced. So we’ll define vertex structure for TR1-2, TR3-4 and TR5 independently.

TR1-2 Room Vertex Structure

struct tr_room_vertex  // 8 bytes
{
     tr_vertex Vertex;
       int16_t Lighting;
};

Vertex is the coordinates of the vertex, relative to [tr_room_info] x and z values.

Lighting ranges from 0 (bright) to 0x1FFF (dark). This value is ignored by TR2, and Lighting2 is used instead with the same brightness range.

TR2 uses an extended version of the structure:

struct tr2_room_vertex  // 12 bytes
{
     tr_vertex Vertex;
       int16_t Lighting;
      uint16_t Attributes; // A set of flags for special rendering effects
       int16_t Lighting2;  // Almost always equal to Lighting1
};

Attributes field is a set of flags, and their meaning is:

Lighting field is ignored by TR2, and Lighting2 is used instead with the same brightness range — from 0 (bright) to 0x1FFF (dark).

TR3-4 Room Vertex Structure

struct tr3_room_vertex  // 12 bytes
{
     tr_vertex Vertex;
       int16_t Lighting;   // Value is ignored!
      uint16_t Attributes; // A set of flags for special rendering effects
      uint16_t Colour;     // 15-bit colour
};

Lighting value is ignored by the engine, as now each vertex has its own defined 15-bit colour (see below).

Attributes bit flags were extended. Also, old bits 0..4 aren't working properly anymore, because effect lighting was broken in TR3. Here is the list of new flags:

Colour value specifies vertex colour in 15-bit format (each colour occupies 5 bits). Therefore, each colour value’s maximum is 31. You can use this code to get each colour:

TR5 Room Vertex Structure

In TR5, room vertex structure was almost completely changed. Coordinates were converted to floats, and normal was added:

struct tr5_room_vertex  // 28 bytes
{
    tr5_vertex Vertex;     // Vertex is now floating-point
    tr5_vertex Normal;
      uint32_t Colour;     // 32-bit colour
};

There is no more Attributes field in room vertex structure for TR5.

Room Sprite Structure

struct tr_room_sprite  // 4 bytes
{
    int16_t Vertex;       // Offset into vertex list
    int16_t Texture;      // Offset into sprite texture list
};

Vertex indicates an index into room vertex list (Room.Vertices[room_sprite.Vertex]), which acts as a point in space where to display a sprite.

Texture is an index into the sprite texture list.

Room Data Structure

This is the whole geometry of the “room,” including walls, floors, ceilings, and other embedded landscape. It does not include objects that Lara can interact with (keyholes, moveable blocks, moveable doors, etc.), neither does it include static meshes (mentioned below in the next section).

The surfaces specified here are rendered surfaces.

This is not a “real” C/C++ structure, in that the arrays are sized by the NumXXX elements that precede them. Also [tr_room_vertex] could be replaced by any other version-specific room vertex type ([tr3_room_vertex], etc.).

virtual struct tr_room_data    // (variable length)
{
    int16_t NumVertices;                   // Number of vertices in the following list
    tr2_room_vertex Vertices[NumVertices]; // List of vertices (relative coordinates)
 
    int16_t NumRectangles;                 // Number of textured rectangles
    tr_face4 Rectangles[NumRectangles];    // List of textured rectangles
 
    int16_t NumTriangles;                  // Number of textured triangles
    tr_face3 Triangles[NumTriangles];      // List of textured triangles
 
    int16_t NumSprites;                    // Number of sprites
    tr_room_sprite Sprites[NumSprites];    // List of sprites
};

Room Static Mesh Structure

Core: MESH_INFO

Positions and IDs of static meshes (e.g. skeletons, spiderwebs, furniture, trees). This is comparable to the [tr_entity] structure, except that static meshes have no animations and are confined to a single room.

TR1 Room Static Mesh Structure

struct tr_room_staticmesh  // 18 bytes
{
    uint32_t x, y, z;    // Absolute position in world coordinates
    uint16_t Rotation;
    uint16_t Intensity1;
    uint16_t MeshID;     // Which StaticMesh item to draw
};

Intensity1 ranges from 0 (bright) to 0x1FFF (dark).

In Rotation field, high two bits (0xC000) indicate steps of 90 degrees (e.g. (Rotation >> 14) * 90). However, when parsing this value, no extra bitshifting is needed, as you can simply interpret it using this formula:

$RealRotation = \frac{Rotation}{16384} \cdot -90$

TR2 Room Static Mesh Structure

TR2 again uses an extended version:

struct tr2_room_staticmesh  // 20 bytes
{
    uint32_t x, y, z;    // Absolute position in world coordinates
    uint16_t Rotation;
    uint16_t Intensity1;
    uint16_t Intensity2; // Absent in TR1
    uint16_t MeshID;     // Which StaticMesh item to draw
};

Intensity2 is seemingly not used, as changing this value does nothing.

TR3-5 Room Static Mesh Structure

virtual struct tr3_room_staticmesh  // 20 bytes
{
    uint32_t x, y, z;    // Absolute position in world coordinates
    uint16_t Rotation;
    uint16_t Colour;     // 15-bit colour
    uint16_t Unused;     // Not used!
    uint16_t MeshID;     // Which StaticMesh item to draw
};

Colour value specifies vertex colour in 15-bit format (each colour occupies 5 bits): 0b0RRRRRGGGGGBBBBB. Therefore, each colour value’s maximum is 31. You can use this code to get each colour:

TR5 Room Structure Changes

In TR5 the room format was drastically changed. The room itself is made up of sections. These sections encompass a 3×3 sector grid (actually 3069×3069 pixels). Historically, these sections are referred as layers, however, more proper name for them is volumes. Layers are organized in a quadtree-like structure, and their purpose was presumably optimizing rendering by some kind of space partitioning and culling invisible volumes.

Another thing to note is that some rooms in TR5 do not actually contain visible mesh data. If concerned, we will refer to these rooms as null rooms.

TR5 Room Layer Structure

struct tr5_room_layer   // 56 bytes
{
    uint16_t NumLayerVertices;   // Number of vertices in this layer (2 bytes)
    uint16_t NumLayerVerticesWater; // Number of underwater vertices in this layer (2 bytes)
    uint16_t NumLayerVerticesShore;
    uint16_t NumLayerRectangles; // Number of rectangles in this layer (2 bytes)
    uint16_t NumLayerTriangles;  // Number of triangles in this layer (2 bytes)
    uint16_t NumLayerRectanglesWater; // Number of rectangles containing water vertices
    uint16_t NumLayerTrianglesWater; // Number of trianglescontaining water vertices
 
    uint16_t Filler;             // Always 0
 
    // The following 6 floats define the bounding box for the layer
 
    float    LayerBoundingBoxX1;
    float    LayerBoundingBoxY1;
    float    LayerBoundingBoxZ1;
    float    LayerBoundingBoxX2;
    float    LayerBoundingBoxY2;
    float    LayerBoundingBoxZ2;
 
    uint32_t Filler3;     // Always 0 (4 bytes), internal pointer
    uint32_t VerticesOffset;   // Those fields are overwritten at level loading 
    uint32_t PolyOffset;       // by the ones present in the tr5_room struct + an offset
    uint32_t PrelightOffset;      // i.e. the values are not read, the fields are there for storage purposes
}

NumLayerVerticesShore appears to be the number of double sided textures in this layer, however is sometimes 1 off (2 bytes).

The Whole Room Structure

Here’s where all the room data come together.

Room structure differs drastically across different game versions (especially in TR5). For this reason, we will define each version of Room structure independently, to avoid confusion. Also, version-specific fields will be described in each version’s section in a “backwards-compatible” manner, while common fields with version-specific variations, such as Flags, will be described afterwards in separate section.

These are not “real” C/C++ structures, in that the arrays are sized by the NumXXX elements that precede them.

TR1 Room Structure

As it’s stored in the file, the [tr_room_info] structure comes first, followed by a uint32_t NumDataWords, which specifies the number of 16-bit words to follow. Those data words must be parsed in order to interpret and construct the variable-length arrays of vertices, meshes, doors, and sectors. Such setup is also applicable to all variations of room structures, except [tr5_room], which will be described independently.

virtual struct tr_room  // (variable length)
{
    tr_room_info info;           // Where the room exists, in world coordinates
 
    uint32_t NumDataWords;       // Number of data words (uint16_t's)
    uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived
 
    tr_room_data RoomData;       // The room mesh
 
    uint16_t NumPortals;                 // Number of visibility portals to other rooms
    tr_room_portal Portals[NumPortals];  // List of visibility portals
 
    uint16_t NumZsectors;                                  // ``Width'' of sector list
    uint16_t NumXsectors;                                  // ``Height'' of sector list
    tr_room_sector SectorList[NumXsectors * NumZsectors];  // List of sectors in this room
 
    int16_t AmbientIntensity;
 
    uint16_t NumLights;                 // Number of lights in this room
    tr_room_light Lights[NumLights];    // List of lights
 
    uint16_t NumStaticMeshes;                            // Number of static meshes
    tr_room_staticmesh StaticMeshes[NumStaticMeshes];   // List of static meshes
 
    int16_t AlternateRoom;
    int16_t Flags;
};

AmbientIntensity is a brightness value which affects only externally-lit objects. It ranges from 0 (bright) to 0x1FFF (dark).

AlternateRoom (or, as it is called in TRLE terms, flipped room) is the number of the room that this room can flip with. In the terms of the gameplay, flipped room is a state change of the same room — for example, empty or flooded with water, filled with sand or debris. Alternate room usually has the same boundaries as original room, but altered geometry and/or texturing. Detailed description of alternate rooms will be provided in a separate section.

TR2 Room Structure

virtual struct tr2_room  // (variable length)
{
    tr_room_info info;           // Where the room exists, in world coordinates
 
    uint32_t NumDataWords;       // Number of data words (uint16_t's)
    uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived
 
    tr_room_data RoomData;       // The room mesh
 
    uint16_t NumPortals;                 // Number of visibility portals to other rooms
    tr_room_portal Portals[NumPortals];  // List of visibility portals
 
    uint16_t NumZsectors;                                  // "Width" of sector list
    uint16_t NumXsectors;                                  // "Height" of sector list
    tr_room_sector SectorList[NumXsectors * NumZsectors];  // List of sectors in this room
 
    int16_t AmbientIntensity;
    int16_t AmbientIntensity2;  // Usually the same as AmbientIntensity
    int16_t LightMode;
 
    uint16_t NumLights;                 // Number of point lights in this room
    tr_room_light Lights[NumLights];    // List of point lights
 
    uint16_t NumStaticMeshes;                            // Number of static meshes
    tr2_room_staticmesh StaticMeshes[NumStaticMeshes];   // List of static meshes
 
    int16_t AlternateRoom;
    int16_t Flags;
};

AmbientIntensity2 value is usually equal to AmbientIntensity value. Seems it’s not used.

LightMode specifies lighting mode special effect, which is applied to all room vertices in conjunction with 5 lowest bits of Attributes field belonging to [tr_room_vertex] structure. Here we will refer these 5 bits value to as effect_value:

TR3 Room Structure

virtual struct tr3_room  // (variable length)
{
    tr_room_info info;           // Where the room exists, in world coordinates
 
    uint32_t NumDataWords;       // Number of data words (uint16_t's)
    uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived
 
    tr_room_data RoomData;       // The room mesh
 
    uint16_t NumPortals;                 // Number of visibility portals to other rooms
    tr_room_portal Portals[NumPortals];  // List of visibility portals
 
    uint16_t NumZsectors;                                  // ``Width'' of sector list
    uint16_t NumXsectors;                                  // ``Height'' of sector list
    tr3_room_sector SectorList[NumXsectors * NumZsectors];  // List of sectors in this room
 
    int16_t AmbientIntensity;  // Affects externally-lit objects
    int16_t LightMode;  // Broken in this game version
 
    uint16_t NumLights;                 // Number of point lights in this room
    tr3_room_light Lights[NumLights];   // List of point lights
 
    uint16_t NumStaticMeshes;                            // Number of static meshes
    tr3_room_staticmesh StaticMeshes[NumStaticMeshes];    // List of static meshes
 
    int16_t AlternateRoom;
    int16_t Flags;
 
    uint8_t WaterScheme;
    uint8_t ReverbInfo;
 
    uint8_t Filler;  // Unused.
};

LightMode types are broken and produce aburpt “bumpy ceiling” effect. It happens because internally same data field was reused for vertex waving effects (seen in quicksand and water rooms). Type 3 (sunset) works the same as in TR2.

TR4 Room Structure

virtual struct tr4_room  // (variable length)
{
    tr_room_info info;           // Where the room exists, in world coordinates
 
    uint32_t NumDataWords;       // Number of data words (uint16_t's)
    uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived
 
    tr_room_data RoomData;       // The room mesh
 
    uint16_t NumPortals;                 // Number of visibility portals to other rooms
    tr_room_portal Portals[NumPortals];  // List of visibility portals
 
    uint16_t NumZsectors;                                  // ``Width'' of sector list
    uint16_t NumXsectors;                                  // ``Height'' of sector list
    tr3_room_sector SectorList[NumXsectors * NumZsectors]; // List of sectors in this room
 
    uint32_t RoomColour;        // In ARGB format!
 
    uint16_t NumLights;                 // Number of point lights in this room
    tr4_room_light Lights[NumLights];   // List of point lights
 
    uint16_t NumStaticMeshes;                           // Number of static meshes
    tr3_room_staticmesh StaticMeshes[NumStaticMeshes];   // List of static meshes
 
    int16_t AlternateRoom;
    int16_t Flags;
 
    uint8_t WaterScheme;
    uint8_t ReverbInfo;
 
    uint8_t AlternateGroup;  // Replaces Filler from TR3
};

RoomColour replaces AmbientIntensity and AmbientIntensity2 values from [tr2_room] structure. Note it’s not in [tr_colour4] format, because colour order is reversed. It should be treated as ARGB, where A is unused.

AlternateGroup was introduced in TR4 to solve long-existing engine limitation, which flipped all alternate rooms at once (see flipmap trigger action description in Trigger actions section). Since TR4, engine only flips rooms which have similar index in room’s AlternateGroup field and trigger operand.

TR5 Room Structure

As it was mentioned before, TR5 room structure was almost completely changed, when compared to previous versions. For example, TR5 shuffles numerous values and structures in almost chaotic manner, and introduces a bunch of completely new parameters (mostly to deal with layers). Also, there is vast amount of fillers and separators, which contain no specific data.

The one possible reason for such ridiculous structure change is an attempt to crypt file format, so it won’t be accessed by unofficial level editing tools, which received major development by that time. Another possible reason is whole TR5 development process was rushed, as the team developed Tomb Raider: Angel of Darkness at the very same time.
There are multiple pointer fields in the structure, they are relative to the end of the tr5_room structure, i.e. to the beginning of the tr5_room_data structure.

struct tr5_room // 216 bytes
{
    char XELA[4];           // So-called "XELA landmark"
 
    uint32_t RoomDataSize; // size of following fields (208 bytes) + tr5_room_data (dynamic)
 
    char* RoomData; // is filled by the game at run time. Always 0xCDCDCDCD in level files (4 bytes),
 
    uint16_t* NumPortals; // points to tr5_room_data.NumPortals
    tr3_room_sector* SectorList; // points to tr5_room_data.SectorList
 
    tr5_room_light* Lights;     // points to tr5_room_data.Lights
 
    tr3_room_staticmesh* StaticMeshes; // points to tr5_room_data.StaticMeshes
 
    tr5_room_info info;
 
    uint16_t NumZSectors;
    uint16_t NumXSectors;
 
    uint32_t RoomColour;   // In ARGB format!
 
    uint16_t NumLights;
    uint16_t NumStaticMeshes;
 
    uint8_t  ReverbInfo;
    uint8_t  AlternateGroup;
 
    int8_t MeshEffect;
    int8_t bound_active;
 
    int16_t left;         // always 0x7FFF
    int16_t right;        // always 0
    int16_t top;          // always 0x7FFF
    int16_t bottom;       // always 0
 
    int16_t test_left;    // always 0xCDCD
    int16_t test_right;   // always 0xCDCD
    int16_t test_top;     // always 0xCDCD
    int16_t test_bottom;  // always 0xCDCD
 
    int16_t item_number;  // always 0xFFFF (-1)
    int16_t fx_number;    // always 0xFFFF (-1)
 
    uint16_t AlternateRoom;
    uint16_t Flags;
 
    uint32_t nVerts;
    uint32_t nWaterVerts;     // Always 0
    uint32_t nShoreVerts;     // Always 0
 
    uint32_t Separator;    // 0xCDCDCDCD, internally a pointer
 
    uint32_t Separator; // internally a pointer to the face data
 
    float RoomX;
    float RoomY;
    float RoomZ;
 
    uint32_t vnormals; // internal pointer
    uint32_t fnormals; // internal pointer
    uint32_t prelight; // internal pointer
    uint32_t prelightwater; // internal pointer
    uint32_t watercalc; // internal pointer, 0 for normal rooms and 0xCDCDCDCD for null rooms
    uint32_t verts; // internal pointer
 
    uint32_t NumRoomTriangles;
    uint32_t NumRoomRectangles;
 
    tr5_room_light* RoomLights; // points to tr5_room_data.RoomLights
    tr5_fog_bulb* FogBulbs; // points to tr5_room_data.FogBulbs
 
    uint32_t NumLights2;    // Always same as NumLights
 
    uint32_t NumFogBulbs;
 
    int32_t RoomYTop;
    int32_t RoomYBottom;
 
    uint32_t NumLayers;
 
    tr5_room_layer* Layers; // points to tr5_room_data.Layers
    tr5_room_vertex* Vertices; // points to tr5_room_data.Vertices
    void* PolyOffset; // points to tr5_room_data.Faces
    void* PrelightOffset; // points to tr5_room_data.Faces, internal?
 
    uint32_t NumVertices;
 
    // Always 0xCDCDCDCD
    float fLeft;
    float fRight;
    float fTop;
    float fBottom;
}
// immediately after
virtual struct tr5_room_data
{
    tr5_room_light Lights[NumLights];    // Data for the lights (88 bytes * NumRoomLights)
    tr5_fog_bulb FogBulbs[NumFogBulbs];      // Data for the fog bulbs (36 bytes * NumFogBulbs)
    tr3_room_sector SectorList[NumXSectors * NumZSectors]; // List of sectors in this room
 
    uint16_t NumPortals;                 // Number of visibility portals to other rooms
    tr_room_portal Portals[NumPortals];  // List of visibility portals
 
    uint16_t Separator;  // Always 0xCDCD
 
    tr3_room_staticmesh StaticMeshes[NumStaticMeshes];   // List of static meshes
 
    tr5_room_layer Layers[NumLayers]; // Data for the room layers (volumes) (56 bytes * NumLayers)
 
    uint8_t Faces[(NumRoomRectangles * sizeof(tr_face4) + NumRoomTriangles * sizeof(tr_face3)];
 
    tr5_room_vertex Vertices[NumVertices];
}

XELA landmark seemingly serves as a header for room structure. It is clear that XELA is a reversed ALEX, which is most likely the name of TR5 programmer, Alex Davis. It probably indicates that Alex Davis is responsible for changes in room structures.

RoomDataSize is a handy value determining the size of the following data. You can use this value to quickly parse thru to the next room.

RoomX, RoomY and RoomZ values are positions of room in world coordinates. NOTE: If room is null room, then each of these values will be 0xCDCDCDCD.

NumRoomTriangles and NumRoomRectangles are respectively the numbers of triangular and rectangular faces in a given room. NOTE: If room is null room, each of these values will be 0xCDCDCDCD.

LightDataSize is the size of the light data in bytes (not in [tr5_room_light] units).

RoomYTop and RoomYBottom are equal to yTop and yBottom values in [tr_room_info] structure. If room is a null room, both of these values are 0xCDCDCDCD.

NumLayers is a number of layers (volumes) in this room.

VerticesSize is the size of vertex data block in bytes. Therefore, it must be a multiple of [tr5_room_vertex] size, else it means the block size is wrong.

Faces is a sequential data array for the room polygons (both [tr_face4] and [tr_face3]),

Faces array is strictly linked with NumLayers value. The data is sequentially structured for each layer — at first it lists first layer’s rectangles then triangles, followed by the second layer’s rectangles and triangles, and so on, until all layers are done.

Common Fields of a Room Structure

Flags is an array of various flag bits, which meaning is as follows:

WaterScheme is used for different purposes. If room is a water room, then it specifies underwater caustics patterns. If it is set for normal room placed above the water room, then it controls wave strength effect applied to the faces adjoining water room. Maximum value in both cases is 15.

ReverbInfo defines room reverberation type. It affects sound postprocessing, if listener position belongs to that room. This feature was present only in PlayStation versions of the game, but not on PC. Nevertheless, the info is preserved in PC level files. Here are the types of reverberation: