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.
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
.
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.
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:
- 0 — Mud
- 1 — Snow (TR3 and TR5 only)
- 2 — Sand
- 3 — Gravel
- 4 — Ice (TR3 and TR5 only)
- 5 — Water (unused, as water footstep is only activated in water rooms)
- 6 — Stone (unused, as it is default footstep sound)
- 7 — Wood
- 8 — Metal
- 9 — Marble (TR4 only)
- 10 — Grass (same sound effect as sand)
- 11 — Concrete (same sound effect as stone, hence unused)
- 12 — Old wood (same sound effect as wood)
- 13 — Old metal (same sound effect as metal)
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
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.
- 0 — Sun
- 1 — Light
- 2 — Spot
- 3 — Shadow
- 4 — Fog bulb
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,223 | 21 = 0,30,16 |
||||
1 = 245,200,60 | 8 = 0,128,0 | 15 = 244,216,152 | 22 = 250,222,167 |
||||
2 = 120,196,112 | 9 = 150,172,157 | 16 = 248,192,60 | 23 = 218,175,117 |
||||
3 = 202,204,230 | 10 = 128,128,128 | 17 = 252,0,0 | 24 = 225,191,78 |
||||
4 = 128,64,0 | 11 = 204,163,123 | 18 = 198,95,87 | 25 = 77,140,141 |
||||
5 = 64,64,64 | 12 = 177,162,140 | 19 = 226,151,118 | 26 = 4,181,154 |
||||
6 = 243,232,236 | 13 = 0,223,191 | 20 = 248,235,206 | 27 = 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:
- if both
x
andy
$\text{LightDirectionVectorX} = \cos(X) \cdot \sin(Y)$ - $\text{LightDirectionVectorY} = \sin(X)$
- $\text{LightDirectionVectorZ} = \cos(X) \cdot \cos(Y)$
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:
- Bits 0..4 are only used together in combination with the
LightMode
field of the [tr_room] structure. See below. - Bit 15: When used in room filled with water, don’t move the vertices of the room when viewed from above (normally, when viewed from above, the vertices of a room filled with water moves to simulate the refraction of lights in water). Note that when viewed from inside the room filled with water, the vertices of the other rooms outside still moves.
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:
- Bit 13: Water / quicksand surface “wave” movement. Brightness is also shifting, if this flag is set (but it’s not the same type as with Bit 14, it’s much less noticeable).
- Bit 14: Simulates caustics by constantly shifting vertex colour brightness. Used mainly in underwater rooms, but can be used in rooms without water. In TR2, there was a similar effect, but it was assigned for all vertices in any water room.
- Bit 15: Broken in these game versions. Instead, same “wave” effect is produced as with Bit 13, but with slightly less glow.
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:
- Red:
((Colour & 0x7C00) >> 10)
- Green:
((Colour & 0x03E0) >> 5)
- Blue:
(Colour & 0x001F)
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.
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:
- Red:
((Colour & 0x7C00) >> 10)
- Green:
((Colour & 0x03E0) >> 5)
- Blue:
(Colour & 0x001F)
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
:
- 0 — Normal lighting mode, no special effects.
- 1 — Produces flickering effect, with
effect_value
acting the same way — as intensity multiplier. - 2 — If
effect_value
is in 1-15 range, then vertex lighting is cyclically fading to more bright value. The lower the value is, the deeper the fade to full vertex lighting is. Ifeffect_value
is in 17-30 range (not 31!), then vertex lighting is cyclically fading to more dark value. The higher the value is, the deeper the fade to black is. Ifeffect_value
is 16 or 0, no effect is produced. So practically,effect_value
serves as a multiplier to overall effect brightness. - 3 — If
sunset
gameflow script opcode is present, rooms with this light type will gradually dim all lights for 20 minutes. This happens in Bartoli's Hideout. Sunset state isn't saved in savegames and will be reset on reload.
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.
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:
- Bit 0 — Room is filled with water. Sound effects played without internal flag
SFX_WATER
(0x01
) will not play. - Bit 3 — Set if the skybox can be seen from this room. Used to speed things up: if no rendered room has this bit set, then the sky can never been seen, so it is not rendered. Else, if at least one visible room has this bit set, then the sky must be drawn because it is (could be) visible.
- Bit 5 — Lara’s ponytail gets blown by the wind. Beginning with TR3, some particle types are also be blown, if they end up in such room (particle type is specified by certain particle flag).
- Bit 6 — Room is inside. Used in official levels. No apparent effects.
- Bit 7 — Different meaning in TR3 and TR4/5. In TR3, it means that room is filled with quicksand, while in TR4/5 it presumably blocks global lens flare from appearing in that room (in TRLE, checkbox which sets this flag is named NL).
- Bit 8 — Skybox-like room. These must be rendered before other rooms with depth testing disabled, as these skybox rooms generally overlap each other and depth testing generall leads to z-fightig flickering.
- Bit 8 — Creates caustics effect similar to that used in water rooms. TRLE sets this bit when the M option is used (in the same time, the degree of fading intensity typed by the user is put in the
water_scheme
byte). - Bit 9 — The room has some water reflectivity. TRLE sets this bit when the R (reflectivity) option is used (in the same time, the amount of reflectivity typed by the user + 5 is put in the
water_scheme
byte). When the flag is set for normal room and there is water room below it, game engine creates “reflection effect” above the water surface — effectively it means that all the vertices at the bottom of the room receive caustics effect described well above. - Bit 10 — unused. Was re-used in NGLE as a flag specifying room with snow.
- Bit 11 — Not found in any original TR levels, but when the D flag is set in the TRLE, this bit is set. Was re-used in NGLE as a flag specifying room with rain.
- Bit 12 — Not found in any original TR levels, but when the P flag is set in the TRLE, this bit is set. Was also re-used in NGLE as a flag specifying cold room (a room which produce damage on Lara).
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:
- 0 — Outside. No (or barely heard) reverberation.
- 1 — Small room. Little reverberation.
- 2 — Medium room.
- 3 — Large room.
- 4 — Pipe. Highest reverberation level. Almost never used.