DKRDS Track (File Format): Difference between revisions

From Wexos's Wiki
Jump to navigationJump to search
 
(10 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{under-construction}}
'''DKRDS Track''' is the nameless file format used in ''[https://en.wikipedia.org/wiki/Diddy_Kong_Racing_DS Diddy Kong Racing DS]'' that stores the tracks' section models, collision and textures. Its type identifier in [[assets.bin]] is '''0x9A'''.
'''DKRDS Track''' is the nameless file format used in ''[https://en.wikipedia.org/wiki/Diddy_Kong_Racing_DS Diddy Kong Racing DS]'' that stores the tracks' section models, collision and textures. Its type identifier in [[assets.bin]] is '''0x9A'''.


Line 15: Line 14:
| 0x04 || Int32 || '''[[#Track Section Group|Track Section Group]]''' offset.
| 0x04 || Int32 || '''[[#Track Section Group|Track Section Group]]''' offset.
|-
|-
| 0x08 || Int32 || '''[[#Unknown Section 1|Unknown Section 1]]''' offset.
| 0x08 || Int32 || '''[[#Terrain Effects|Terrain Effects]]''' offset.
|-
|-
| 0x0C || Byte[4] || {{unknown-left|'''Unknown'''. Always 0xCCCCCCCC.}}
| 0x0C || Byte[4] || {{unknown-left|'''Unknown'''. Always 0xCCCCCCCC.}}
Line 25: Line 24:
* 2 = 8-byte per '''N''' group.
* 2 = 8-byte per '''N''' group.
|-
|-
| 0x18 || Int32 || '''Number of textures'''.
| 0x18 || Int32 || '''Number of textures''' ('''X''').
|-
|-
| 0x1C || Int32 || '''[[#Texture Group|Texture Group]]''' offset.
| 0x1C || Int32 || '''[[#Texture Group|Texture Group]]''' offset.
|-
|-
| 0x20 || Int32 || '''[[#Unknown Section 2|Unknown Section 2]]''' offset.
| 0x20 || Int32 || '''[[#Culling BSP Tree|Culling BSP Tree]]''' offset.
|-
|-
| 0x24 || Int32 || '''[[#Wish Race Unknown Section|Wish Race Unknown Section]]''' entry count.
| 0x24 || Int32 || '''[[#Wish Race Unknown Section|Wish Race Unknown Section]]''' entry count.
Line 55: Line 54:


=== Track Section Entry ===
=== Track Section Entry ===
Every section entry defines model and collision data. The entry starts with a 32-byte header.
Every section entry defines model and collision data. The entry starts with a 32-byte header. All offsets are relative to the start of this entry.


{|class=wikitable
{|class=wikitable
Line 61: Line 60:
! Offset !! Type !! Description
! Offset !! Type !! Description
|-
|-
| 0x00 || Byte || {{unknown-left|'''Unknown'''.}}
| 0x00 || Byte || '''Main collision attribute mask'''. This is an OR of all of the main collision attribute values for all polygon groups in this section.
|-
|-
| 0x01 || Byte || {{unknown-left|'''Unknown'''. Always 5?}}
| 0x01 || Byte || '''Secondary collision attribute mask'''. This is an OR of all of the secondary collision attribute values for all polygon groups in this section.
|-
|-
| 0x02 || UInt16 || {{unknown-left|'''Unknown'''.}}
| 0x02 || UInt16 || '''Tertiary collision attribute mask'''. This is an OR of all of the tertiary collision attribute values for all polygon groups in this section.
|-
|-
| 0x04 || UInt16 || '''Number of polygon groups''' ('''P''').
| 0x04 || UInt16 || '''Number of polygon groups''' ('''P''').
Line 71: Line 70:
| 0x06 || UInt16 || '''Number of vertices''' in [[#Track Section Vertex Data|Track Section Vertex Data]] ('''V''').
| 0x06 || UInt16 || '''Number of vertices''' in [[#Track Section Vertex Data|Track Section Vertex Data]] ('''V''').
|-
|-
| 0x08 || UInt16 || '''Number of UVs''' in [[#Track Section UV|Track Section UV Data]] ('''U''').
| 0x08 || UInt16 || '''Number of UVs''' in [[#Track Section UV Data|Track Section UV Data]] ('''U''').
|-
|-
| 0x0A || UInt16 || '''Number of colors''' in [[#Track Section Color Data|Track Section Color Data]] ('''C''').
| 0x0A || UInt16 || '''Number of colors''' in [[#Track Section Color Data|Track Section Color Data]] ('''C''').
Line 143: Line 142:
| 0x01 || Road and effects.
| 0x01 || Road and effects.
|-
|-
| 0x05 || Wall.
| 0x05 || Wall (sparks on collision).
|-
|-
| 0x0C || Invisible wall.
| 0x08 || Special effects (turns off visual model).
|-
| 0x0C || Invisible wall (no sparks on collision, turns off visual model).
|}
|}
|-
|-
Line 158: Line 159:
|-
|-
| 0x04 || Road.
| 0x04 || Road.
|}
* '''Main flag = 0x08''':
{| class="wikitable"
! ID !! Description
|-
| 0x00 || Invisible wall (no sparks on collision).
|-
| 0x01 || Water.
|}
|}
|-
|-
Line 172: Line 181:
|-
|-
| 0x20 || {{unknown-left|'''Unknown'''. Used in non-collideable stuff.}}
| 0x20 || {{unknown-left|'''Unknown'''. Used in non-collideable stuff.}}
|-
| 0x60 || {{unknown-left|'''Unknown'''. Used in non-collideable stuff.}}
|}
* '''Main flag = 0x08''':
{| class="wikitable"
! ID !! Description
|-
| 0x30 || {{unknown-left|'''Unknown'''. Used in non-collideable stuff.}}
|}
|}
* '''Main flag = 0x0C''':
* '''Main flag = 0x0C''':
Line 180: Line 197:
|}
|}
|-
|-
| 0x08 || Byte[4] || {{unknown-left|'''Unknown'''.}}
| 0x08 || Byte[4] || '''Collision grid mask'''. This is an OR of all of the [[#Track Section Collision Data|collision grids]] for all triangles in this group.
|}
|}


Line 202: Line 219:
| 0x00 || UInt16 || '''Attribute flags'''. '''AAAA AAAA AAAA AAAB''':
| 0x00 || UInt16 || '''Attribute flags'''. '''AAAA AAAA AAAA AAAB''':
* '''A''': '''Texture ID'''.
* '''A''': '''Texture ID'''.
* '''B''': '''Is collidable'''.
* '''B''': '''Dominant normal direction sign''' used for collision detection. Calculated in the following way:
<pre>
bool ComputeNormalSign(Vector3 v0, Vector3 v1, Vector3 v2)
{
    Vector3 n = Vector3.Cross(v1 - v0, v2 - v0);
 
    int ax = (int)Math.Abs(n.X);
    int ay = (int)Math.Abs(n.Y);
    int az = (int)Math.Abs(n.Z);
 
    if (ax >= ay && ax >= az)
        return n.X > 0;
 
    if (az >= ay && az >= ax)
        return n.Z > 0;
 
    return n.Y < 0;
}
</pre>
|-
|-
| 0x02 || UInt16[3] || '''Face XYZ normals'''. Values need to be divided by 4096.
| 0x02 || UInt16[3] || '''Face XYZ normals'''. Values need to be divided by 4096.
Line 214: Line 249:


===== Track Section Collision Data =====
===== Track Section Collision Data =====
4-byte data per triangle (?). ''TBD''
Each triangle corresponds to a 4-byte set of values to help with collision detection. These values encode local X, Y and Z coordinates that form a grid of cells inside the section's AABB.
 
{|class=wikitable
! Offset !! Type !! Description
|-
| 0x00 || Byte || '''Y occupancy'''. Values correspond to 8 cells of the AABB (AABB's Y extent / 8).
|-
| 0x01 || Byte || '''Z occupancy'''. Values correspond to 8 cells of the AABB (AABB's Z extent / 8).
|-
| 0x02 || UInt16 || '''X occupancy'''. Values correspond to 16 cells of the AABB (AABB's X extent / 16).
|}
 
The following ARM assembly instructions (@01FF95D0 from the game's code) demonstrate how the vehicle's current grid bitmask is compared to the current testing triangle's collision:
<pre>
AND R0, R0, R5 // R0 = triangle's collision value AND vehicle's bitmask
TST R0, #FF // test byte 0 (Y)
TSTNE R0, #FF00 // if nonzero, test byte 1 (Z)
MOVNES R0, R0, LSR #10 // if still nonzero, test upper 16 bits (X)
MOVNE R0, #1 // collidable only if ALL 3 are nonzero
</pre>
 
The following functions in C# compute a triangle's collision grid from its minimum and maximum positions and its containing section's AABB:
<pre>
uint ComputeCollision(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, float aabbXMin, float aabbXExt, float aabbYMin, float aabbYExt, float aabbZMin, float aabbZExt)
{
    if (aabbXExt <= 0 || aabbYExt <= 0 || aabbZExt <= 0)
        return 0xFFFFFFFFu;
 
    uint xBits = ComputeCellBits(xMin - aabbXMin, xMax - aabbXMin, aabbXExt / 16f, 16);
    uint zBits = ComputeCellBits(zMin - aabbZMin, zMax - aabbZMin, aabbZExt / 8f, 8);
    uint yBits = ComputeCellBits(yMin - aabbYMin, yMax - aabbYMin, aabbYExt / 8f, 8);
 
    return (xBits << 16) | (zBits << 8) | yBits;
}
 
uint ComputeCellBits(float relMin, float relMax, float cellSize, int nrCells)
{
    const float Epsilon = 1e-4f;
    float faceMin = relMin / cellSize;
    float faceMax = relMax / cellSize;
 
    // Special case for degenerate faces (flat faces with zero extent in this axis, e.g. a wall parallel to XZ)
    if (MathF.Abs(faceMax - faceMin) < Epsilon)
    {
        int cell = Math.Max(0, Math.Min(nrCells - 1, (int)MathF.Floor(faceMin)));
        bool atExact = MathF.Abs(faceMin - MathF.Round(faceMin)) < Epsilon;
        bool atMaxEdge = cell == nrCells - 1;
 
        // Include the preceding cell only at an interior exact boundary
        if (atExact && cell > 0 && !atMaxEdge)
            return (1u << cell) | (1u << (cell - 1));
 
        return 1u << cell;
    }
 
    int cellMin = (int)MathF.Floor(faceMin);
    int cellMax = (int)MathF.Floor(faceMax - Epsilon); // Exact upper boundary stays in current cell
 
    // Exact lower boundary stays in the preceding cell
    bool atExactLow = MathF.Abs(faceMin - MathF.Round(faceMin)) < Epsilon;
    bool atMaxLow = cellMin == nrCells - 1;
    if (atExactLow && cellMin > 0 && !atMaxLow)
        cellMin--;
 
    cellMin = Math.Max(0, cellMin);
    cellMax = Math.Min(nrCells - 1, cellMax);
 
    uint bits = 0;
 
    for (int c = cellMin; c <= cellMax; c++)
        bits |= 1u << c;
 
    return bits;
}
</pre>


===== Track Section Vertex Data =====
===== Track Section Vertex Data =====
Line 243: Line 352:
|}
|}


== Unknown Section 1 ==
== Terrain Effects ==
''TBD''
Each texture is linked to 2 specific terrain effects that affect the vehicle's speed, particles and lighting. This section contains 2 blocks of '''X''' UInt16s.
 
{|class=wikitable
! Offset !! Type !! Description
|-
| 0x00 || UInt16['''X'''] || '''Effects''', split in bits.
|-
| '''X''' * 2 || UInt16['''X'''] || {{unknown-left|'''Unknown'''. This seems to be a copy of the previous block?}}
|}


== Culling Groups ==
== Culling Groups ==
Line 258: Line 375:
See '''[[DKRDS Texture Group]]'''.
See '''[[DKRDS Texture Group]]'''.


== Unknown Section 2 ==
== Culling BSP Tree ==
''TBD''
This section defines a serialized axis-aligned binary space partitioning tree used for culling sections along with the [[#Culling Groups|Culling Groups]] section. The game traverses it to determine which sections are candidates for rendering given the current player's position, and then performs a secondary [[#Track Section AABB|AABB]] overlap test on each candidate before adding it to the active render list. The section consists of a sequence of variable-length nodes and leaves stored one after another.
 
=== Interior Node ===
An interior node is always 8 bytes long and can be identified apart from a leaf node if the first Int16 is not negative.
 
{|class=wikitable
! Offset !! Type !! Description
|-
| 0x00 || Int16 || '''Axis index''' into the player position array:
* '''0''' = X axis.
* '''1''' = Y axis. Unused?
* '''2''' = Z axis.
|-
| 0x02 || Int16 || '''Split value high bits'''.
|-
| 0x04 || UInt16 || '''Split value low bits'''.
|-
| 0x06 || Int16 || '''Skip count'''. The right child begins at <code>(node_offset + 8) + skip * 2</code> bytes from the start of this section. The left child begins immediately at <code>node_offset + 8</code>.
|}
 
The split coordinate is reconstructed by the game as a 32-bit signed result, then divided by 64 to obtain the coordinates in world units (the same scale as the [[#Track Section AABB|section AABBs]]):
<pre>
split = ((split_high << 16) | (split_low & 0xFFFF)) / 64
</pre>
 
To encode a world-coordinate integer back into the two stored fields:
<pre>
split_raw = round(split * 64)  
(Int16)split_high = (split_raw >> 16) & 0xFFFF
(UInt16)split_low = split_raw & 0xFFFF
</pre>
 
=== Leaf Node ===
Leaf nodes are variable-length.
{|class=wikitable
! Offset !! Type !! Description
|-
| 0x00 || Int16 || '''Negative count''' ('''-L'''). The value is always negative. Its negation '''L''' gives the number of section IDs that follow.
|-
| 0x02 || Int16['''L'''] || '''Section IDs'''. Zero-based indices into the [[#Track Section Group|Track Section Group]]. A section may appear in more than one leaf when its AABB occupies a split plane.
|}
=== Traversal Algorithm ===
The game calls the function <code>@02006CC4(bsp_ptr, track_sg, position, tolerance, out_list)</code>:
* '''bsp_ptr''': pointer to the start of this section in RAM.
* '''track_sg''': pointer to the [[#Track Section Group|Track Section Group]].
* '''position''': player XYZ position.
* '''tolerance''': a proximity radius, observed as <code>0xF00</code> (60 in world units).
* '''out_list''': output structure that receives the active section list.
At each interior node, in world-unit scale:
<pre>
pos = position[axis]
split_raw = (split_high << 16) | (split_low & 0xFFFF)
split = split_raw / 64
</pre>
 
If <code>split - pos > tolerance</code>, visit the left child only; else if <code>pos - split &ge; tolerance</code>, visit the right child only; otherwise, visit both children.
 
The left child corresponds to sections whose AABB satisfies <code>min[axis] &le; split</code>; the right child corresponds to sections whose AABB satisfies <code>max[axis] &ge; split</code>. Sections satisfying both (i.e. whose AABB traverses the split plane) appear in leaves on both sides.
 
At each leaf node, for every listed section ID the section's data pointer is first looked up via the [[#Track Section Group|Track Section Group]]. Then, the player's bounding box is tested (<code>(X &plusmn; tolerance) * (Z &plusmn; tolerance)</code>) against the section's [[#Track Section AABB|AABB]]. If the AABB test passes, add the section to the active render list.


== Wish Race Unknown Section ==
== Wish Race Unknown Section ==
This section is only used for the Wish Race tracks. ''TBD''
This section is only used for the Wish Race tracks. Each entry is 0x18 bytes long. ''TBD''


= Tools =
= Tools =

Latest revision as of 18:39, 18 June 2026

DKRDS Track is the nameless file format used in Diddy Kong Racing DS that stores the tracks' section models, collision and textures. Its type identifier in assets.bin is 0x9A.

File Format

The file byte order is always little endian.

Header

The file starts with a header that is 56 bytes long.

Offset Type Description
0x00 Int32 Number of sections (N).
0x04 Int32 Track Section Group offset.
0x08 Int32 Terrain Effects offset.
0x0C Byte[4] Unknown. Always 0xCCCCCCCC.
0x10 Int32 Culling Groups offset.
0x14 Int32 Culling Groups data type:
  • 1 = 4-byte per N group.
  • 2 = 8-byte per N group.
0x18 Int32 Number of textures (X).
0x1C Int32 Texture Group offset.
0x20 Int32 Culling BSP Tree offset.
0x24 Int32 Wish Race Unknown Section entry count.
0x28 Int32 Wish Race Unknown Section offset.
0x2C Byte[4] Unknown. Seems to be some kind of flags.
0x30 Int32 Unknown.
0x34 Int32 File size.
0x38 End of header

Track Section Group

This group starts with a list of N offsets.

Track Section Header
Offset Type Description
0x00 Int32[N] Track Section Entry offsets. Relative to the start of this group.

Track Section Entry

Every section entry defines model and collision data. The entry starts with a 32-byte header. All offsets are relative to the start of this entry.

Track Section Entry Header
Offset Type Description
0x00 Byte Main collision attribute mask. This is an OR of all of the main collision attribute values for all polygon groups in this section.
0x01 Byte Secondary collision attribute mask. This is an OR of all of the secondary collision attribute values for all polygon groups in this section.
0x02 UInt16 Tertiary collision attribute mask. This is an OR of all of the tertiary collision attribute values for all polygon groups in this section.
0x04 UInt16 Number of polygon groups (P).
0x06 UInt16 Number of vertices in Track Section Vertex Data (V).
0x08 UInt16 Number of UVs in Track Section UV Data (U).
0x0A UInt16 Number of colors in Track Section Color Data (C).
0x0C Int32 Track Section Triangle Data offset.
0x10 Int32 Track Section Collision Data offset.
0x14 Int32 Track Section Vertex Data offset.
0x18 Int32 Track Section UV Data offset.
0x1C Int32 Track Section Color Data offset.
0x20 End of header, start of Track Section Data

Track Section Data

This data follows directly after the header of the section entry and starts with a 76-byte header.

Offset Type Description
0x00 Int32 Unknown. Seems to be always 0xFFFFFFFF and set to an address when loaded in memory.
0x04 Int32[3] Collision XYZ offset.
0x10 Int32[3] Visual model XYZ offset. The units are not the same as the collision offset.
0x1C Track Section AABB[2] AABB areas of the entire section used for collision detection. The second AABB always seems to be a duplicate of the first.
0x4C End of header, start of Track Section Polygon Data
Track Section AABB

Track sections contain 2 identical AABB areas each. It is unknown why there's a duplicate, perhaps used during runtime after object space transform. All values need to be divided by 64 in order to obtain the absolute position data and match with the visual models.

Offset Type Description
0x00 Int32[3] AABB minimum XYZ position.
0x0C Int32[3] AABB XYZ extent.
Track Section Polygon Data

This section seems to store P polygon groups' attributes.

Offset Type Description
0x00 Polygon Group Entry[P] Polygon group entries.
Polygon Group Entry

Each polygon group entry is 12 bytes long.

Offset Type Description
0x00 UInt16 First triangle ID.
0x02 UInt16 Number of triangles.
0x04 Byte Main collision attribute. AAAA BBBB:
  • A: Shadow level. The higher the value, the darker the character gets.
  • B: Collision flag:
ID Description
0x01 Road and effects.
0x05 Wall (sparks on collision).
0x08 Special effects (turns off visual model).
0x0C Invisible wall (no sparks on collision, turns off visual model).
0x05 Byte Secondary collision attribute:
  • Main flag = 0x01:
ID Description
0x00 Special effect? Used for water.
0x01 Pass through/no effect.
0x04 Road.
  • Main flag = 0x08:
ID Description
0x00 Invisible wall (no sparks on collision).
0x01 Water.
0x06 UInt16 Tertiary collision attribute. Split in bits.
  • Main flag = 0x01:
ID Description
0x01 Unknown. Used in non-collideable stuff.
0x02 Water, inflates tires in car.
0x10 Unknown. Used in non-collideable stuff.
0x20 Unknown. Used in non-collideable stuff.
0x60 Unknown. Used in non-collideable stuff.
  • Main flag = 0x08:
ID Description
0x30 Unknown. Used in non-collideable stuff.
  • Main flag = 0x0C:
ID Description
0x04 Ignore when using plane.
0x08 Byte[4] Collision grid mask. This is an OR of all of the collision grids for all triangles in this group.

The number of triangles T can be calculated by adding the first triangle ID to the number of triangles of the last polygon group entry.

Track Section Triangle Data

This section stores T triangle entries.

Offset Type Description
0x00 Triangle Entry[T] Triangle entries.
Triangle Entry

Each triangle entry is 20 bytes long.

Offset Type Description
0x00 UInt16 Attribute flags. AAAA AAAA AAAA AAAB:
  • A: Texture ID.
  • B: Dominant normal direction sign used for collision detection. Calculated in the following way:
bool ComputeNormalSign(Vector3 v0, Vector3 v1, Vector3 v2)
{
    Vector3 n = Vector3.Cross(v1 - v0, v2 - v0);

    int ax = (int)Math.Abs(n.X);
    int ay = (int)Math.Abs(n.Y);
    int az = (int)Math.Abs(n.Z);

    if (ax >= ay && ax >= az)
        return n.X > 0;

    if (az >= ay && az >= ax)
        return n.Z > 0;

    return n.Y < 0;
}
0x02 UInt16[3] Face XYZ normals. Values need to be divided by 4096.
0x08 Byte[4] Vertex position indices. The index for each of the 3 vertices is 10 bits long. 2 last bits are unknown.
0x0C Byte[4] UV indices. The index for each of the 3 vertices is 10 bits long. 2 last bits are unknown.
0x10 Byte[4] Color indices. The index for each of the 3 vertices is 10 bits long. 2 last bits are unknown.
Track Section Collision Data

Each triangle corresponds to a 4-byte set of values to help with collision detection. These values encode local X, Y and Z coordinates that form a grid of cells inside the section's AABB.

Offset Type Description
0x00 Byte Y occupancy. Values correspond to 8 cells of the AABB (AABB's Y extent / 8).
0x01 Byte Z occupancy. Values correspond to 8 cells of the AABB (AABB's Z extent / 8).
0x02 UInt16 X occupancy. Values correspond to 16 cells of the AABB (AABB's X extent / 16).

The following ARM assembly instructions (@01FF95D0 from the game's code) demonstrate how the vehicle's current grid bitmask is compared to the current testing triangle's collision:

AND R0, R0, R5 // R0 = triangle's collision value AND vehicle's bitmask
TST R0, #FF // test byte 0 (Y)
TSTNE R0, #FF00 // if nonzero, test byte 1 (Z)
MOVNES R0, R0, LSR #10 // if still nonzero, test upper 16 bits (X)
MOVNE R0, #1 // collidable only if ALL 3 are nonzero

The following functions in C# compute a triangle's collision grid from its minimum and maximum positions and its containing section's AABB:

uint ComputeCollision(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, float aabbXMin, float aabbXExt, float aabbYMin, float aabbYExt, float aabbZMin, float aabbZExt)
{
    if (aabbXExt <= 0 || aabbYExt <= 0 || aabbZExt <= 0)
        return 0xFFFFFFFFu;

    uint xBits = ComputeCellBits(xMin - aabbXMin, xMax - aabbXMin, aabbXExt / 16f, 16);
    uint zBits = ComputeCellBits(zMin - aabbZMin, zMax - aabbZMin, aabbZExt / 8f, 8);
    uint yBits = ComputeCellBits(yMin - aabbYMin, yMax - aabbYMin, aabbYExt / 8f, 8);

    return (xBits << 16) | (zBits << 8) | yBits;
}

uint ComputeCellBits(float relMin, float relMax, float cellSize, int nrCells)
{
    const float Epsilon = 1e-4f;
    float faceMin = relMin / cellSize;
    float faceMax = relMax / cellSize;

    // Special case for degenerate faces (flat faces with zero extent in this axis, e.g. a wall parallel to XZ)
    if (MathF.Abs(faceMax - faceMin) < Epsilon)
    {
        int cell = Math.Max(0, Math.Min(nrCells - 1, (int)MathF.Floor(faceMin)));
        bool atExact = MathF.Abs(faceMin - MathF.Round(faceMin)) < Epsilon;
        bool atMaxEdge = cell == nrCells - 1;

        // Include the preceding cell only at an interior exact boundary
        if (atExact && cell > 0 && !atMaxEdge)
            return (1u << cell) | (1u << (cell - 1));

        return 1u << cell;
    }

    int cellMin = (int)MathF.Floor(faceMin);
    int cellMax = (int)MathF.Floor(faceMax - Epsilon); // Exact upper boundary stays in current cell

    // Exact lower boundary stays in the preceding cell
    bool atExactLow = MathF.Abs(faceMin - MathF.Round(faceMin)) < Epsilon;
    bool atMaxLow = cellMin == nrCells - 1;
    if (atExactLow && cellMin > 0 && !atMaxLow)
        cellMin--;

    cellMin = Math.Max(0, cellMin);
    cellMax = Math.Min(nrCells - 1, cellMax);

    uint bits = 0;

    for (int c = cellMin; c <= cellMax; c++)
        bits |= 1u << c;

    return bits;
}
Track Section Vertex Data

Vertex data is stored as 6-byte groups per entry V.

Offset Type Description
0x00 Int16[3][V] Vertex XYZ position.
Track Section UV Data

UV data is stored as 4-byte groups per entry U.

Offset Type Description
0x00 Int16[2][U] Vertex UV position.
Track Section Color Data

Color data is stored as 2-byte groups per entry C.

Offset Type Description
0x00 UInt16[C] RGBA5551 color.

Terrain Effects

Each texture is linked to 2 specific terrain effects that affect the vehicle's speed, particles and lighting. This section contains 2 blocks of X UInt16s.

Offset Type Description
0x00 UInt16[X] Effects, split in bits.
X * 2 UInt16[X] Unknown. This seems to be a copy of the previous block?

Culling Groups

This section includes a 4-byte or 8-byte per track section values that determine which section models are loaded when entering a specific section. Each section is represented as 1 bit.

Offset Type Description
0x00 UInt16[N]/UInt32[N] Culled model sections, represented as N bits, 1 per section, in order from the least to the most significant bit. 0 if culled, 1 if loaded.

Texture Group

See DKRDS Texture Group.

Culling BSP Tree

This section defines a serialized axis-aligned binary space partitioning tree used for culling sections along with the Culling Groups section. The game traverses it to determine which sections are candidates for rendering given the current player's position, and then performs a secondary AABB overlap test on each candidate before adding it to the active render list. The section consists of a sequence of variable-length nodes and leaves stored one after another.

Interior Node

An interior node is always 8 bytes long and can be identified apart from a leaf node if the first Int16 is not negative.

Offset Type Description
0x00 Int16 Axis index into the player position array:
  • 0 = X axis.
  • 1 = Y axis. Unused?
  • 2 = Z axis.
0x02 Int16 Split value high bits.
0x04 UInt16 Split value low bits.
0x06 Int16 Skip count. The right child begins at (node_offset + 8) + skip * 2 bytes from the start of this section. The left child begins immediately at node_offset + 8.

The split coordinate is reconstructed by the game as a 32-bit signed result, then divided by 64 to obtain the coordinates in world units (the same scale as the section AABBs):

split = ((split_high << 16) | (split_low & 0xFFFF)) / 64

To encode a world-coordinate integer back into the two stored fields:

split_raw = round(split * 64)							   
(Int16)split_high = (split_raw >> 16) & 0xFFFF
(UInt16)split_low = split_raw & 0xFFFF

Leaf Node

Leaf nodes are variable-length.

Offset Type Description
0x00 Int16 Negative count (-L). The value is always negative. Its negation L gives the number of section IDs that follow.
0x02 Int16[L] Section IDs. Zero-based indices into the Track Section Group. A section may appear in more than one leaf when its AABB occupies a split plane.

Traversal Algorithm

The game calls the function @02006CC4(bsp_ptr, track_sg, position, tolerance, out_list):

  • bsp_ptr: pointer to the start of this section in RAM.
  • track_sg: pointer to the Track Section Group.
  • position: player XYZ position.
  • tolerance: a proximity radius, observed as 0xF00 (60 in world units).
  • out_list: output structure that receives the active section list.

At each interior node, in world-unit scale:

pos = position[axis]
split_raw = (split_high << 16) | (split_low & 0xFFFF)
split = split_raw / 64

If split - pos > tolerance, visit the left child only; else if pos - split ≥ tolerance, visit the right child only; otherwise, visit both children.

The left child corresponds to sections whose AABB satisfies min[axis] ≤ split; the right child corresponds to sections whose AABB satisfies max[axis] ≥ split. Sections satisfying both (i.e. whose AABB traverses the split plane) appear in leaves on both sides.

At each leaf node, for every listed section ID the section's data pointer is first looked up via the Track Section Group. Then, the player's bounding box is tested ((X ± tolerance) * (Z ± tolerance)) against the section's AABB. If the AABB test passes, add the section to the active render list.

Wish Race Unknown Section

This section is only used for the Wish Race tracks. Each entry is 0x18 bytes long. TBD

Tools

The following tools can handle DKRDS Track:

  • (none)