-
Notifications
You must be signed in to change notification settings - Fork 0
Home
-
Texture information is contained in the Texture Header.
-
The first two bytes correspond to the 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 |
- 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 |
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 |
- 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 |
| PC | WII |
|---|---|
| PCM |
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.
Each vertex entry begins with the position:
0x00 float32 X
0x04 float32 Y
0x08 float32 ZThe vertex stride is detected automatically. The decoder searches the vertex buffer for the marker:
FF FF FF FFThe 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:
0x2CSo UV is read as:
0x2C float32 U
0x30 float32 VIf 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 - sourceVThe 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
...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 NThe 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 + globalVertexCounterglobalVertexCounter is increased after each block by the number of vertices in that block.
Submeshes are detected from the index buffer.
The important pattern is:
0, 1When 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 2In 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 currentSubmeshThis means submeshes are not currently read from a separate material table. They are inferred from index buffer resets.
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 + globalVertexCounterAfter a submesh is processed, the offset is advanced by:
localBlockOffset += maxIndexInSubmesh + 1So if submesh 0 uses indices:
0..120then submesh 1 starts after those vertices:
localBlockOffset = 121This suggests that the vertex buffer stores submesh vertex ranges sequentially, while each submesh index list is local to its own range.
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 + globalVertexCounterThe triangle is only accepted if all three final indices point to existing vertices.
if a, b, c are valid:
add triangle(a, b, c)The same geometry can be exported in two ways.
All submeshes inside one block are grouped together:
Block_0
Block_1
Block_2Each detected submesh is exported separately:
Block_0_Part_0
Block_0_Part_1
Block_0_Part_2The 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 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.
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 dataThe 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 is stored separately from positions.
UV coordinates are stored as 4-byte pairs:
0x00 uint16 BE U
0x02 uint16 BE VThe values are normalized by dividing by 1024:
u = rawU / 1024
v = rawV / 1024For OBJ export, V is flipped:
exportedV = 1.0 - vImportant 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 14The first two pairs are invalid, but the last two pairs are valid UVs.
The index/display-list buffer contains GX primitive commands.
Each command starts with:
uint8 primitive opcode
uint16 vertex count, big-endianAfter 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 texCoordIndexThe decoder tests both formats per display-list segment and keeps the one that parses correctly.
The current decoder supports:
0x90 Triangles
0x98 Triangle Strip
0xA0 Triangle Fan
0x80 QuadsTriangle strips are rebuilt with alternating winding order.
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:
- finds the next valid position range
- finds the UV table after that position range
- rebuilds faces from GX primitive commands
GX references position and UV independently:
positionIndex != texCoordIndexOBJ 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.
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.
The sprite block starts with a small header. At offset 0x10 there is a pointer to the first sprite entry:
0x10 uint32 LE firstSpritePointerBefore the actual sprite entries, there is a pointer table.
The number of sprites is calculated from the first pointer:
spriteCount = (firstSpritePointer - 0x10) / 4So if the first sprite entry starts at 0x30, then:
(0x30 - 0x10) / 4 = 8 spritesThis 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.
Each sprite entry is currently treated as a fixed 64-byte structure.
spriteEntrySize = 64 bytesThe fields currently used are:
0x00 4 bytes sprite hash / id
0x10 float32 U1
0x14 float32 V1
0x18 float32 U2
0x1C float32 V2The 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.0So a sprite does not store pixel coordinates directly.
It stores a rectangle in UV space.
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..textureHeightThe sprite size is then:
spriteWidth = right - left
spriteHeight = bottom - topA minimum size of 1x1 is enforced.
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 imageThe result is a standalone RGBA image.
For preview/export, this RGBA buffer is encoded as PNG.
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
V2When exporting all sprites, each sprite rectangle is cropped from the matching texture and written as a separate PNG file.
RenderSprite does not contain the texture pixels itself.
It only contains:
pointer table
sprite entries
UV rectangles
hash/id valuesThe 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