DKRDS Track (File Format): Difference between revisions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
'''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 277: | Line 276: | ||
{ | { | ||
int cell = Math.Max(0, Math.Min(nrCells - 1, (int)MathF.Floor(faceMin))); | int cell = Math.Max(0, Math.Min(nrCells - 1, (int)MathF.Floor(faceMin))); | ||
bool atExact | bool atExact = MathF.Abs(faceMin - MathF.Round(faceMin)) < Epsilon; | ||
bool atMaxEdge = cell == nrCells - 1; | bool atMaxEdge = cell == nrCells - 1; | ||
| Line 359: | Line 358: | ||
== Culling BSP Tree == | == Culling BSP Tree == | ||
'' | 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 low bits'''. | |||
|- | |||
| 0x04 || UInt16 || '''Split value high 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 in world units is reconstructed by the game as a 32-bit signed result: | |||
<pre> | |||
split = (split_low << 10) | split_high | |||
</pre> | |||
To encode a world-coordinate integer back into the two stored fields: | |||
<pre> | |||
(Int16)split_low = split >> 10 | |||
(UInt16)split_high = split & 0xFFFF | |||
</pre> | |||
The split values are in the same coordinate scale as the [[#Track Section AABB|section AABBs]], divided by 64. | |||
The stored ''split_high'' in original game files often differs from the canonical <code>split & 0xFFFF</code> decomposition. The extra bits in positions 10-15 of ''split_high'' are ORed with bits already set by the sign extension of ''split_low'' and therefore have no effect on the reconstructed split. Their origin is unknown. | |||
=== 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 in world units, observed as <code>0xF00</code>. | |||
* '''out_list''': output structure that receives the active section list. | |||
At each interior node: | |||
<pre> | |||
pos = position[axis] | |||
split = (split_low << 10) | split_high | |||
</pre> | |||
If <code>split - pos > tolerance</code>, visit left child only; else if <code>pos - split ≥ tolerance</code>, visit right child only; otherwise, visit both cildren. | |||
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 ± tolerance) * (Z ± 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 == | ||
Revision as of 22:03, 12 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 | Collision Effects offset. |
| 0x0C | Byte[4] | Unknown. Always 0xCCCCCCCC. |
| 0x10 | Int32 | Culling Groups offset. |
| 0x14 | Int32 | Culling Groups data type:
|
| 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.
| 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.
| Offset | Type | Description |
|---|---|---|
| 0x00 | Byte | Unknown. |
| 0x01 | Byte | Unknown. Always 5? |
| 0x02 | UInt16 | Unknown. |
| 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:
| ||||||||||||||||||||
| 0x05 | Byte | Secondary collision attribute:
| ||||||||||||||||||||
| 0x06 | UInt16 | Tertiary collision attribute. Split in bits.
| ||||||||||||||||||||
| 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:
|
| 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. |
Collision Effects
Each texture is linked to 2 specific collision 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:
|
| 0x02 | Int16 | Split value low bits. |
| 0x04 | UInt16 | Split value high 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 in world units is reconstructed by the game as a 32-bit signed result:
split = (split_low << 10) | split_high
To encode a world-coordinate integer back into the two stored fields:
(Int16)split_low = split >> 10 (UInt16)split_high = split & 0xFFFF
The split values are in the same coordinate scale as the section AABBs, divided by 64.
The stored split_high in original game files often differs from the canonical split & 0xFFFF decomposition. The extra bits in positions 10-15 of split_high are ORed with bits already set by the sign extension of split_low and therefore have no effect on the reconstructed split. Their origin is unknown.
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 in world units, observed as
0xF00. - out_list: output structure that receives the active section list.
At each interior node:
pos = position[axis] split = (split_low << 10) | split_high
If split - pos > tolerance, visit left child only; else if pos - split ≥ tolerance, visit right child only; otherwise, visit both cildren.
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. TBD
Tools
The following tools can handle DKRDS Track:
- (none)