Skip to content
zbirow edited this page May 24, 2026 · 5 revisions

Texture

  • Texture information is contained in the Texture Header.

  • The first two bytes correspond to the texture format.

Texture Format

Two First Bytes Texture Format
0xA1 0xBC CMPR - Wii
0xE9 0x78 Unknown - Wii
0xD3 0x3A DXT5 - PC
0xF9 0x3D DXT1 - PC
0x9F 0x5B R8G8B8A8 - PC

Width/Height

  • Two bytes
Game Width Adress HeightAdress Endian Example bytes Out
PC 0x0C 0x0E Little Endian 0x00 0x02 512
Wii 0x0C 0x0E Big Endian 0x02 0x00 512

Scooby Doo Games

Texture Format

PC

Adress 0x34:0x40 Texture Format
DXT5 DXT5 - PC
DXT1 DXT1 - PC
0x15 R8G8B8A8 - PC

Wii

Adress 0x5:0x9 Texture Format
0x01 0x00 0x00 0x24 CMPR - Wii
0x01 0x00 0x00 0x28 CMPR - Wii
0x01 0x00 0x00 0x20 CMPR - Wii
0x01 0x00 0x00 0x2C CMPR - Wii
0x01 0x00 0x00 0x30 CMPR - Wii

Width/Height

  • Two bytes
Game Width Adress Height Adress Endian Example bytes Out Format
PC 0x30 0x32 Little Endian 0x00 0x02 512 All
Wii 0x58 0x5A Big Endian 0x00 0x02 512 0x01 0x00 0x00 0x24
Wii 0x5C 0x5E Big Endian 0x00 0x02 512 0x01 0x00 0x00 0x28
Wii 0x54 0x56 Big Endian 0x00 0x02 512 0x01 0x00 0x00 0x20
Wii 0x60 0x62 Big Endian 0x00 0x02 512 0x01 0x00 0x00 0x2C
Wii 0x64 0x66 Big Endian 0x00 0x02 512 0x01 0x00 0x00 0x30

Audio Format

PC WII
PCM

3D Model

PC

How the 3D mesh is reconstructed

The model is rebuilt from two main streams:

  • a vertex buffer
  • an index buffer

The vertex buffer contains packed vertex entries.
The index buffer contains 16-bit indices that describe triangles.

The mesh is not treated as one continuous object. It is reconstructed in blocks, and each block can contain multiple submeshes.

Vertex buffer layout

Each vertex entry begins with the position:

0x00  float32  X
0x04  float32  Y
0x08  float32  Z

The vertex stride is detected automatically. The decoder searches the vertex buffer for the marker:

FF FF FF FF

The distance between repeated markers is used as the vertex size. If no reliable size is found, the decoder falls back to 64 bytes per vertex.

UV coordinates are read from the same vertex entry.

For a 64-byte vertex, the default UV offset is:

0x2C

So UV is read as:

0x2C  float32  U
0x30  float32  V

If the FF FF FF FF marker is found inside a vertex entry, UVs are read immediately after that marker instead.

The V coordinate is flipped during export:

exportedV = 1.0 - sourceV

Index buffer layout

The index buffer is read as an array of 16-bit little-endian integers:

uint16 index0
uint16 index1
uint16 index2
...

Every three indices form one triangle:

index0, index1, index2 = triangle
index3, index4, index5 = triangle
...

Mesh blocks

A model can have multiple geometry blocks. Each block has its own vertex buffer and index buffer.

For every block:

Block N:
  vertex buffer N
  index buffer N

The decoder appends all vertices from the block into one global vertex list.

To make indices work after appending multiple blocks, every block gets a global vertex offset:

globalIndex = localIndex + globalVertexCounter

globalVertexCounter is increased after each block by the number of vertices in that block.

Submesh detection

Submeshes are detected from the index buffer.

The important pattern is:

0, 1

When the decoder sees 0, 1 after the beginning of the index stream, it treats this as the start of a new submesh.

So the index buffer is split like this:

[ indices before 0,1 ] = submesh 0
[ 0,1 ... next 0,1 ] = submesh 1
[ 0,1 ... end ]      = submesh 2

In simplified pseudocode:

currentSubmesh = []

for each index in indexBuffer:
  if current position is not near the start
     and current index == 0
     and next index == 1:
       finish currentSubmesh
       start new currentSubmesh

  add index to currentSubmesh

This means submeshes are not currently read from a separate material table. They are inferred from index buffer resets.

Local vertex ranges inside submeshes

Each submesh appears to restart its indices from zero or from a small local range.

Because of that, the decoder tracks a localBlockOffset.

For each submesh:

finalVertexIndex = index + localBlockOffset + globalVertexCounter

After a submesh is processed, the offset is advanced by:

localBlockOffset += maxIndexInSubmesh + 1

So if submesh 0 uses indices:

0..120

then submesh 1 starts after those vertices:

localBlockOffset = 121

This suggests that the vertex buffer stores submesh vertex ranges sequentially, while each submesh index list is local to its own range.

Triangle construction

For each submesh batch, triangles are created by reading indices in groups of three:

a = index[i + 0] + localBlockOffset + globalVertexCounter
b = index[i + 1] + localBlockOffset + globalVertexCounter
c = index[i + 2] + localBlockOffset + globalVertexCounter

The triangle is only accepted if all three final indices point to existing vertices.

if a, b, c are valid:
  add triangle(a, b, c)

Export grouping

The same geometry can be exported in two ways.

Normal block export

All submeshes inside one block are grouped together:

Block_0
Block_1
Block_2

Submesh export

Each detected submesh is exported separately:

Block_0_Part_0
Block_0_Part_1
Block_0_Part_2

Important note

The current submesh detection is heuristic.

It assumes that 0, 1 inside the index buffer means “new submesh starts here”, and that every submesh has its own local vertex index range. This works for the currently tested models, but the real format may also contain a table that describes submesh offsets, materials, or draw calls.

WII

How the Wii 3D mesh is reconstructed

Wii models are reconstructed from two main data streams:

  • a geometry data buffer
  • a GX-style display list buffer

Unlike the PC model format, Wii models do not use a simple linear triangle index buffer.
The mesh is built from GameCube/Wii GX primitive commands.

Geometry data layout

The geometry data buffer contains several attribute ranges.

Position data is stored as 16-byte entries:

0x00  float32 BE  X
0x04  float32 BE  Y
0x08  float32 BE  Z
0x0C  unused / packed data

The position ranges are separated by padding or non-position data.

Empty position entries such as:

00 00 00 00 00 00 00 00 00 00 00 00 ...

are not treated as real vertices.

Invalid padding entries such as:

FF FF FF FF ...

are also skipped.

UV data layout

UV data is stored separately from positions.

UV coordinates are stored as 4-byte pairs:

0x00  uint16 BE  U
0x02  uint16 BE  V

The values are normalized by dividing by 1024:

u = rawU / 1024
v = rawV / 1024

For OBJ export, V is flipped:

exportedV = 1.0 - v

Important detail: UV tables are not always aligned to 16-byte entries.
A UV table can begin in the middle of a 16-byte block, after one or more FF FF pairs.
Because of that, UV tables must be scanned as 4-byte pairs, not as full 16-byte entries.

Example:

FF FF FF FF FF FF FF FF 00 29 00 3F 00 04 00 14

The first two pairs are invalid, but the last two pairs are valid UVs.

Display list layout

The index/display-list buffer contains GX primitive commands.

Each command starts with:

uint8   primitive opcode
uint16  vertex count, big-endian

After that, each vertex reference contains attribute indices.

The decoder supports two reference formats:

u16 format:
  uint16 positionIndex
  uint16 normalIndex
  uint16 colorIndex
  uint16 texCoordIndex

u8 format:
  uint8 positionIndex
  uint8 normalIndex
  uint8 colorIndex
  uint8 texCoordIndex

The decoder tests both formats per display-list segment and keeps the one that parses correctly.

Supported GX primitive types

The current decoder supports:

0x90  Triangles
0x98  Triangle Strip
0xA0  Triangle Fan
0x80  Quads

Triangle strips are rebuilt with alternating winding order.

Submesh detection

The display-list buffer is split into segments by zero padding.

Each segment is treated as one submesh:

Segment 0 = Submesh 0
Segment 1 = Submesh 1
Segment 2 = Submesh 2
...

Each segment uses local position indices starting from zero.

For every segment, the decoder:

  1. finds the next valid position range
  2. finds the UV table after that position range
  3. rebuilds faces from GX primitive commands

Position and UV indexing

GX references position and UV independently:

positionIndex != texCoordIndex

OBJ files normally use matching vertex and UV indices, so the exporter creates one output vertex for every unique pair:

(positionIndex, texCoordIndex)

This preserves UV seams correctly.

Render Sprites

A RenderSprite block describes multiple rectangular sprite regions inside a texture atlas.

The sprite data is not stored as image pixels.
It only stores references to rectangular UV regions. The actual pixels are taken from a matching texture.

Data structure

The sprite block starts with a small header. At offset 0x10 there is a pointer to the first sprite entry:

0x10  uint32 LE  firstSpritePointer

Before the actual sprite entries, there is a pointer table.

The number of sprites is calculated from the first pointer:

spriteCount = (firstSpritePointer - 0x10) / 4

So if the first sprite entry starts at 0x30, then:

(0x30 - 0x10) / 4 = 8 sprites

This means the layout is:

0x00  header / unknown data
0x10  pointer to sprite 0
0x14  pointer to sprite 1
0x18  pointer to sprite 2
0x1C  pointer to sprite 3
...

Each pointer is a 32-bit little-endian offset inside the same RenderSprite data block.

Sprite entry

Each sprite entry is currently treated as a fixed 64-byte structure.

spriteEntrySize = 64 bytes

The fields currently used are:

0x00  4 bytes   sprite hash / id
0x10  float32   U1
0x14  float32   V1
0x18  float32   U2
0x1C  float32   V2

The first 4 bytes are shown as a hex hash:

hash = bytes[0x00..0x03]

The UV values are stored as normalized texture coordinates:

U = 0.0 to 1.0
V = 0.0 to 1.0

So a sprite does not store pixel coordinates directly.
It stores a rectangle in UV space.

Converting UV to pixels

To preview or export a sprite, the tool first finds the matching texture atlas.

The texture is matched by name:

sprite asset name
sprite asset name + "0"

Then the whole texture is decoded to RGBA pixels.

The sprite UV rectangle is converted into pixel coordinates:

x1 = round(u1 * textureWidth)
y1 = round(v1 * textureHeight)

x2 = round(u2 * textureWidth)
y2 = round(v2 * textureHeight)

The final rectangle is normalized so it works even if the coordinates are reversed:

left   = min(x1, x2)
right  = max(x1, x2)
top    = min(y1, y2)
bottom = max(y1, y2)

The values are clamped to the texture bounds:

left/right  = 0..textureWidth
top/bottom  = 0..textureHeight

The sprite size is then:

spriteWidth  = right - left
spriteHeight = bottom - top

A minimum size of 1x1 is enforced.

Cropping

After the rectangle is calculated, the tool copies pixels row by row from the decoded texture atlas:

for each row in spriteHeight:
    copy RGBA pixels from texture row into output sprite image

The result is a standalone RGBA image.

For preview/export, this RGBA buffer is encoded as PNG.

Multiple sprites

A single RenderSprite asset can contain many sprite entries.

The UI allows selecting a sprite by index:

Sprite 1 / N
Sprite 2 / N
Sprite 3 / N
...

The list shows:

ID
Hash
U1
V1
U2
V2

When exporting all sprites, each sprite rectangle is cropped from the matching texture and written as a separate PNG file.

Important notes

RenderSprite does not contain the texture pixels itself.

It only contains:

pointer table
sprite entries
UV rectangles
hash/id values

The actual image data comes from a separate texture atlas.

The current parser assumes:

pointer table starts at 0x10
each pointer is uint32 little-endian
each sprite entry is 64 bytes
UV values are float32 little-endian