Textured Geometry Pipeline#

This page documents how dk64_lib turns DK64 geometry display lists into OBJ, MTL, glTF, GLB, DAE, and PNG files. It is intended to be a human-readable map of the current implementation rather than a replacement for the API reference.

The relevant implementation lives primarily in:

  • dk64_lib.data_types.geometry.GeometryData

  • dk64_lib.f3dex2.texture_export.TexturedObjExporter

  • dk64_lib.f3dex2.texture_export.TexturedGltfExporter

  • dk64_lib.f3dex2.texture_export.TexturedDaeExporter

  • dk64_lib.f3dex2.texture_export.decode_texture

  • dk64_lib.f3dex2.commands

  • dk64_lib.f3dex2.vertex.Vertex

High-Level Export Flow#

Textured geometry export starts at one of the high-level helpers:

  • Rom.export_all()

  • Rom.export_geometries()

  • GeometryData.save_to_obj()

  • GeometryData.save_to_textured_obj()

  • GeometryData.create_textured_obj()

  • GeometryData.save_to_gltf()

  • GeometryData.save_to_glb()

  • GeometryData.create_textured_gltf()

  • GeometryData.create_textured_glb()

  • GeometryData.save_to_dae()

  • GeometryData.create_textured_dae()

Geometry exports include textures by default. Rom.export_geometries() and Rom.export_all() write GLB files unless the caller selects another geometry_format. The explicit GeometryData.save_to_obj(), save_to_gltf(), save_to_glb(), and save_to_dae() helpers also keep include_textures=True as their default.

The export flow is:

  1. GeometryData parses a geometry table entry into display lists, vertex data, display-list chunk metadata, and expansion display lists.

  2. The matching GeometryData.create_textured_* helper fetches the geometry texture table through rom.get_geometry_texture_data().

  3. TexturedObjExporter, TexturedGltfExporter, or TexturedDaeExporter walks the geometry display lists, tracks the active F3DEX2 texture state, and groups triangles by the texture that was active when those triangles were emitted.

  4. The exporter writes OBJ text plus MTL text, glTF JSON plus binary data, a GLB binary, or an in-memory DAE document, and the PNG files needed by the materials.

  5. save_textured_obj_export() writes the OBJ, MTL, and PNG files to disk. save_textured_gltf_export() writes the glTF, binary buffer, and PNG files. save_textured_glb_export() writes the GLB file. save_textured_dae_export() writes the DAE and PNG files.

The production geometry exporters do not write packed mipmap base/reference images. Those base images are only written by test_mipmap_export(), which is a temporary visual debugging helper.

Display-List Texture State#

F3DEX2 display lists are stateful. A triangle does not directly say which image it uses. Instead, the exporter watches texture-related commands and reconstructs the texture state that is active when a triangle command is encountered.

The current state tracker pays attention to these commands:

Command

How export uses it

G_SETTIMG

Records the pending image index plus the image fmt and size. In DK64 geometry exports this index refers into the geometry texture table, not a filesystem path.

G_LOADBLOCK and G_LOADTILE

Associate the pending image with the command’s tile number. The most recently loaded tile is also remembered as a fallback.

G_LOADTLUT

Treats the pending image as a palette image. Color-indexed textures use this palette when decoded.

G_SETTILE

Records a tile descriptor: fmt, size, line stride, TMEM address, palette number, clamp/mirror bits, masks, and shifts. The exporter currently uses the format, size, tile, palette, and clamp information.

G_SETTILESIZE

Records the tile dimensions. F3DEX2 tile coordinates are quarter-texel values, so export computes width = abs(lrs - uls) / 4 + 1 and the equivalent value for height.

G_TEXTURE

Selects the active tile when texturing is on. If texturing is disabled, subsequent faces are exported without texture coordinates or a material.

G_DL

Recurses into a branch display list with a cloned texture state, so branch-local changes do not unexpectedly mutate the parent traversal.

When the exporter has an active tile, a tile descriptor, tile dimensions, and a loaded image source, it can build a texture key:

tex_<image_index>_pal_<palette_index|none>_f<fmt>_s<size>_<width>x<height>

For example:

tex_158_pal_159_f2_s1_32x32

That material name is used consistently in the OBJ, MTL, and texture filenames.

Mesh Grouping#

OBJ files are easier to import when faces that share a material are grouped together. TexturedObjExporter therefore creates internal mesh groups while walking each display list.

Groups are split when:

  • a new G_VTX command loads a different vertex buffer;

  • triangle output switches to a different active texture;

  • a branched display list yields its own groups.

The current triangle path handles G_TRI1 and G_TRI2. G_TRI2 expands to two triangles. Faces emitted while no texture is active are still exported, but they do not receive vt coordinates or a usemtl statement.

Vertex Output and Vertex Colors#

DK64 vertices are 16 bytes:

Byte range

Field

Export use

0..1

x

OBJ vertex position.

2..3

y

OBJ vertex position.

4..5

z

OBJ vertex position.

6..7

unk

Parsed and preserved in Vertex but not written to OBJ.

8..9

texture_cord_u

Used to compute OBJ vt u.

10..11

texture_cord_v

Used to compute OBJ vt v.

12

xr

Red vertex color channel.

13

yg

Green vertex color channel.

14

zb

Blue vertex color channel.

15

alpha

Parsed and preserved. OBJ export currently omits alpha. glTF, GLB, and DAE export write it in the vertex color source.

OBJ vertex color is written using the common extended OBJ form:

v <x> <y> <z> <red> <green> <blue>

The color channels are normalized from 0..255 to 0.000000..1.000000. For example, a vertex with red 255, green 0, and blue 128 exports as:

v 0 0 0 1.000000 0.000000 0.501961

This is useful in tools such as Blender, which can read vertex colors from OBJ files even though color support is not part of the oldest Wavefront OBJ specification. Alpha is available in the parsed Vertex object and in glTF, GLB, and DAE export, but OBJ export writes RGB only.

glTF and GLB export write COLOR_0 accessors as RGBA vertex colors. RGB channels are converted from DK64’s byte values into linear floats before export, because glTF vertex colors are material multipliers rather than sRGB texture samples. Alpha is normalized directly from 0..255 to 0.0..1.0. This prevents mid-range vertex colors from rendering too bright in glTF viewers such as Blender while preserving vertex alpha for blend effects.

DAE export currently writes a COLOR source with normalized RGBA values. Textured materials still use the decoded texture as the diffuse map; the vertex color stream is exported as geometry data rather than baked into the PNGs.

UV Mapping#

F3DEX2 stores per-vertex texture coordinates as signed 16-bit fixed-point values. The current exporter interprets them with 5 fractional bits, so one texel is 32 units.

The OBJ and DAE UV conversion is:

u = signed_16(texture_cord_u) / 32 / texture_width
v = 1 - (signed_16(texture_cord_v) / 32 / texture_height)

The glTF and GLB UV conversion uses the same u value, but writes the source v value without the OBJ/DAE vertical-axis flip:

u = signed_16(texture_cord_u) / 32 / texture_width
v = signed_16(texture_cord_v) / 32 / texture_height

Important details:

  • Coordinates are clamped to 0..1 only when the active G_SETTILE state has the N64 clamp bit set for that axis. Otherwise values outside 0..1 are preserved so normal texture wrapping/repeating can still work in import tools.

  • OBJ and DAE invert the vertical texture axis to match those importer conventions. glTF and GLB keep the source vertical axis because Blender’s glTF importer applies the expected image orientation for that format.

  • Texture width and height come from the active G_SETTILESIZE command, not from the raw byte count.

  • G_TEXTURE scale values are parsed by the command class but are not applied by OBJ, glTF, GLB, or DAE export today.

If a vertex has texture_cord_u = texture_width * 32 then it maps to u = 1.0. If it has texture_cord_v = texture_height * 32 then it maps to v = 0.0 after the exported vertical-axis flip.

OBJ and MTL Structure#

Every textured OBJ starts with an MTL reference:

mtllib geometry.mtl

Each mesh group writes:

  • a comment identifying the mesh group and display-list offset;

  • v lines for all vertices, including RGB vertex colors;

  • vt lines when the group has an active texture;

  • usemtl before textured faces;

  • f lines using vertex_index/texture_index pairs for textured faces;

  • plain f lines for untextured faces.

The MTL file creates one material per unique texture key:

newmtl tex_158_pal_159_f2_s1_32x32
Ka 1.000000 1.000000 1.000000
Kd 1.000000 1.000000 1.000000
Ks 0.000000 0.000000 0.000000
d 1.000000
illum 1
map_Kd textures/tex_158_pal_159_f2_s1_32x32.png

If the decoded highest-resolution texture contains any transparent pixels, the exporter also writes a dedicated alpha-mask PNG and references it with map_d. The mask stores opacity in both RGB and alpha channels: black means transparent, white means opaque. This is more compatible than pointing map_d at the color PNG, because some OBJ importers read opacity from image luminance rather than from the PNG alpha channel.

MTL can describe opacity maps, but it cannot describe Blender’s Render Method setting. When a textured OBJ export contains transparent materials, dk64_lib therefore writes a companion Blender script named after the MTL file, for example 034_DK_Isles_Overworld.blender.py. After importing the OBJ into Blender, run that script from Blender’s Text Editor to switch only the transparent DK64 materials to Blended. The script sets surface_render_method = "BLENDED" on Blender 4.2 and newer and falls back to blend_method = "BLEND" for older Blender versions.

newmtl tex_0_pal_none_f0_s2_2x2
Ka 1.000000 1.000000 1.000000
Kd 1.000000 1.000000 1.000000
Ks 0.000000 0.000000 0.000000
d 1.000000
illum 4
map_Kd textures/tex_0_pal_none_f0_s2_2x2.png
map_d textures/tex_0_pal_none_f0_s2_2x2_alpha.png

If either texture axis is clamped by G_SETTILE, the material name is suffixed with _clamp_s, _clamp_t, or _clamp_st. The MTL texture map statements also include the standard Wavefront -clamp on option as a hint for importers that honor it:

newmtl tex_0_pal_none_f0_s2_2x2_clamp_st
map_Kd -clamp on textures/tex_0_pal_none_f0_s2_2x2_clamp_st.png

When a packed mipmap texture is detected, the exporter writes all decoded mip levels as PNG files, but map_Kd points at the highest-resolution level. For example, the files may include:

textures/tex_158_pal_159_f2_s1_32x32.png
textures/tex_158_pal_159_f2_s1_32x32_mip1_16x16.png
textures/tex_158_pal_159_f2_s1_32x32_mip2_8x8.png
textures/tex_158_pal_159_f2_s1_32x32_mip3_4x4.png

Only the highest-resolution color PNG is referenced by map_Kd. If the texture has transparent pixels, the generated *_alpha.png mask is referenced by map_d.

glTF and GLB Structure#

GeometryData.save_to_glb() writes single-file binary glTF. This is the preferred Blender-focused export path because the geometry, material description, binary buffers, and referenced texture PNGs are packaged into one .glb file.

GeometryData.save_to_gltf() writes separate glTF assets. It returns the paths written with the .gltf file first, the sidecar .bin buffer second, and texture PNG files after that. This form is useful when the JSON and texture files need to be inspected directly.

The glTF/GLB exporter uses the same display-list traversal, mesh grouping, texture decoding, UV conversion, clamp handling, and packed mipmap detection as the OBJ exporter. The file structure is:

  • each mesh group becomes one glTF mesh and one scene node;

  • each mesh primitive contains POSITION, COLOR_0, and, when textured, TEXCOORD_0 attributes;

  • indices are written as unsigned integer accessors into the binary buffer;

  • each unique texture key becomes one glTF texture, image, and sampler, plus one or more materials when opaque and vertex-alpha groups need different blend modes;

  • materials use the KHR_materials_unlit extension because DK64 map textures are closer to unlit game materials than to authored PBR materials;

  • the material base color texture references the highest-resolution decoded PNG;

  • transparent textures keep their alpha in the color PNG and set alphaMode to BLEND;

  • mesh groups with vertex alpha below full opacity also set alphaMode to BLEND so COLOR_0 alpha can create DK64-style foliage and terrain fades even when the texture image itself is opaque;

  • when the same opaque texture is used by both opaque and vertex-alpha mesh groups, the exporter writes a separate *_vertex_alpha material variant for the blended groups;

  • clamped DK64 tile axes are written as glTF sampler wrapS and wrapT values, using CLAMP_TO_EDGE for clamped axes and REPEAT otherwise.

Separate .gltf export writes decoded packed mipmap levels beside the highest-resolution texture PNG, but only the highest-resolution image is referenced by the glTF material. .glb embeds the highest-resolution PNG needed by each material and does not add unreferenced mip images to the binary container.

For batch exports, Rom.export_geometries(..., geometry_format="glb") writes .glb files, and geometry_format="gltf" writes separate glTF assets. Rom.export_all() accepts the same geometry_format option and passes it through to geometry export.

DAE Structure#

GeometryData.save_to_dae() writes textured DAE output by default. It returns the paths written, with the DAE file first and the companion PNG texture files after it. Pass include_textures=False to keep the legacy geometry-only DAE path.

The textured DAE exporter uses the same display-list traversal, mesh grouping, texture decoding, UV conversion, clamp handling, and packed mipmap detection as the OBJ exporter. The file structure is different:

  • each mesh group becomes one library_geometries entry;

  • each geometry contains aligned VERTEX, COLOR, and, when textured, TEXCOORD sources;

  • each textured triangle set binds its material symbol to the geometry TEXCOORD source through bind_vertex_input;

  • each unique texture key becomes one DAE material and effect;

  • each material’s diffuse channel references the highest-resolution decoded PNG, matching OBJ’s map_Kd behavior;

  • transparent textures add a separate alpha-mask image and reference it from the DAE transparent channel;

  • clamped DK64 tile axes are written as wrap_s and wrap_t sampler hints with CLAMP or WRAP values.

Packed mipmap levels are still exported as PNG files beside the highest resolution image, but the DAE material references the highest-resolution PNG. That mirrors the OBJ path and avoids relying on importer-specific mipmap-chain extensions.

For batch exports, Rom.export_geometries(..., geometry_format="dae") writes .dae files instead of .obj files. Rom.export_all() accepts the same geometry_format option and passes it through to geometry export.

Animated DAE Texture Atlases#

Some DK64 materials are expected to animate by swapping through a run of texture frames. The current library does not yet parse a first-class texture-animation table, so DAE animation export is intentionally opt-in: the caller supplies the frame indices that belong to a material’s first frame.

For a texture whose geometry display list references texture index 31, and whose animation frames live at texture indices 31 through 38, export a DAE preview like this:

rom.geometry_tables[1].save_to_dae(
    "funkys_store.dae",
    "dk64_export/geometries",
    animated_texture_frames={31: range(31, 39)},
    animation_frame_duration=4,
)

The same option is available on ROM-level DAE export:

rom.export_geometries(
    "dk64_export/geometries",
    geometry_format="dae",
    animated_texture_frames={31: range(31, 39)},
    animation_frame_duration=4,
)

Each mapping key is the base texture index used by the geometry. Each value is the ordered list of frame texture indices. When a color-indexed animation needs different palettes per frame, use (texture_index, palette_index) tuples in the frame list. Plain integer frame entries reuse the palette from the material referenced by the display list.

For each animated material, the DAE exporter:

  • decodes each supplied frame with the material’s fmt, size, width, and height;

  • stitches the frames horizontally into one atlas named <material_name>_anim_<count>frames.png;

  • writes an *_alpha.png atlas mask when any frame contains transparent pixels;

  • points the DAE material at the stitched atlas instead of the standalone frame PNG;

  • scales the DAE TEXCOORD S values by 1 / frame_count so the static DAE import displays frame 0 correctly;

  • writes animated_textures.blender.py beside the DAE export.

Run animated_textures.blender.py after importing the DAE into Blender. The script rebuilds the affected materials around the stitched atlas and keyframes a constant-stepped UV offset. This is Blender-specific support code because normal DAE material import does not reliably preserve or evaluate animated texture sampler state.

Texture Decoding#

decode_texture() converts raw texture bytes into RGBA pixels before the PNG writer runs. The decoder currently supports these N64 format/size combinations:

fmt

size

Meaning

0

2

RGBA16.

0

3

RGBA32.

2

0

CI4, using a palette decoded as RGBA16 entries.

2

1

CI8, using a palette decoded as RGBA16 entries.

3

0

IA4.

3

1

IA8.

3

2 or higher

IA16-style intensity/alpha pairs.

4

0

I4.

4

1

I8.

4

2 or higher

16-bit storage where the high byte is used as intensity.

Unsupported or incomplete texture data decodes to a black and magenta checkerboard placeholder. That keeps full ROM export robust: one unsupported texture does not abort every geometry export.

Palette Handling#

Color-indexed textures use G_LOADTLUT to identify the palette source. The palette is decoded as up to 256 RGBA16 entries. CI4 textures use the high nibble then the low nibble from each byte. CI8 textures use each byte as one palette index.

The material name includes the palette index because the same texture bytes can produce different colors with a different palette:

tex_896_pal_897_f2_s0_64x64.png

Packed Mipmap Detection#

Some DK64 texture table entries contain several mipmap levels packed into one raw texture blob. The display list usually describes the highest-resolution tile, but the raw byte count is larger than that tile alone. The exporter uses that extra storage to decide whether a known packed layout is present.

Detection happens before normal texture decoding. A packed decoder is used only when:

  • the active texture’s fmt, size, width, and height match a known layout;

  • the raw texture has enough texels for that layout;

  • any required palette data can be fetched, or placeholder palette entries can be tolerated.

If those checks fail, export falls back to a single decoded PNG.

The production exporter writes decoded mip levels, not the raw packed base image. The raw base image is useful for reverse engineering and is available through test_mipmap_export().

Packed Mipmap Layouts#

The packed mipmap code works in decoded RGBA pixels, but the storage math is based on source texels. The descriptions below use “pixel” to mean one decoded texel after CI/RGBA conversion.

Half-swap means that a fixed-width group is split into two equal halves and the halves are swapped. For example, a 16-pixel group:

00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

becomes:

08 09 10 11 12 13 14 15 00 01 02 03 04 05 06 07

Smaller groups use the same rule. An 8-pixel group swaps two 4-pixel halves. A 4-pixel group swaps two 2-pixel halves.

RGBA16 32x32#

Example texture: tex_2_pal_none_f0_s2_32x32.

Raw packed dimensions are normally interpreted as 32x43 pixels. Exported levels are:

32x32
16x16
8x8
4x4

Layout:

  • Level 0 is the first 32 * 32 pixels. Odd rows are half-swapped in 4-pixel groups.

  • Level 1 starts after level 0. Each 32-pixel storage row contains two 16-pixel output rows. The second row is half-swapped in 4-pixel groups.

  • Level 2 stores four 8-pixel output rows in each 32-pixel storage row. Odd output rows are half-swapped in 4-pixel groups.

  • Level 3 uses four 4-pixel rows per 16-pixel storage group. Odd output rows are half-swapped in 4-pixel groups.

CI4 32x64#

Example texture: tex_0_pal_1_f2_s0_32x64.

Raw packed dimensions are normally interpreted as 32x92 pixels. Exported levels are:

32x64
16x32
8x16
4x8

Layout:

  • Level 0 is the first 32 * 64 pixels. Odd rows are half-swapped in 16-pixel groups.

  • Level 1 starts after level 0 and is stored as a flat 16x32 image. Odd rows are half-swapped in 16-pixel groups.

  • Level 2 starts after level 1. Each 32-pixel storage row contributes two 8-pixel output rows: the first 8 pixels, then skip 16 pixels, then the next 8 pixels.

  • Level 3 starts after level 2. Each 32-pixel storage row contributes two 4-pixel output rows: the first 4 pixels, then skip 20 pixels, then the next 4 pixels.

CI4 64x32#

Example texture: tex_208_pal_209_f2_s0_64x32.

Raw packed dimensions are normally interpreted as 64x43 pixels. Exported levels are:

64x32
32x16
16x8
8x4

Layout:

  • Level 0 is the first 64 * 32 pixels. Odd rows are half-swapped in 16-pixel groups.

  • Level 1 starts after level 0. Each 64-pixel storage row contains two 32-pixel output rows. The second row is half-swapped in 16-pixel groups.

  • Level 2 stores four 16-pixel output rows in each 64-pixel storage row. Rows 1 and 3 are each half-swapped as one 16-pixel group.

  • Level 3 uses row offsets 0, 24, 32, and 56 inside each 64-pixel storage row. This means it reads 8 pixels, skips 16, reads 8, reads 8, skips 16, and reads 8.

CI4 32x32#

Example texture: tex_1272_pal_1273_f2_s0_32x32.

Raw packed dimensions are normally interpreted as 32x46 pixels. Exported levels are:

32x32
16x16
8x8
4x4

Layout:

  • Level 0 is the first 32 * 32 pixels. Odd rows are half-swapped in 16-pixel groups.

  • Level 1 starts after level 0 and is stored as a flat 16x16 image with no additional row swapping.

  • Level 2 starts after level 1. Each 32-pixel storage row contributes two 8-pixel output rows: read 8, skip 16, read 8.

  • Level 3 starts after level 2. Each 32-pixel storage row contributes two 4-pixel output rows: read 4, skip 20, read 4.

CI8 32x32#

Example texture: tex_158_pal_159_f2_s1_32x32.

Raw packed dimensions are normally interpreted as 32x43 pixels. Exported levels are:

32x32
16x16
8x8
4x4

Layout:

  • Level 0 is the first 32 * 32 pixels. Odd rows are half-swapped in 8-pixel groups. In practice that means each odd row is split into four 8-pixel groups, and each group swaps its 4-pixel halves.

  • Level 1 starts after level 0 and is stored as a flat 16x16 image. Odd rows are half-swapped in 8-pixel groups.

  • Level 2 starts after level 1. Each 32-pixel storage row contains four 8-pixel output rows. Rows 0 and 2 are read as-is. Rows 1 and 3 are half-swapped in 8-pixel groups.

  • Level 3 starts after level 2. Each 32-pixel storage row uses row offsets 0, 12, 16, and 28. This corresponds to: read 4 pixels, skip 8, read 4, read 4, skip 8, read 4. There is no swapping at this level.

Test Mipmap Export Helper#

test_mipmap_export() is a visual reverse-engineering helper. It is not used by normal OBJ, glTF, GLB, or DAE export.

It writes:

  • a raw base/reference PNG named *_base_<width>x<height>.png;

  • the decoded highest-resolution PNG;

  • decoded mip1, mip2, and mip3 PNGs for known layouts;

  • optional reference textures that have been useful while researching DK64 mipmap packing.

This helper intentionally remains in the library for now because the mipmap decoding work is still being validated against real assets. The long-term plan is to remove the ad hoc helper once the remaining reverse-engineering cases are covered by normal unit tests and production code.

Full ROM Export#

Rom.export_all() writes all currently supported outputs into sibling folders:

exports/
  geometries/
  textures/
  text/
  cutscenes/
  assets/

Geometry exports use textured GLB output by default. Pass geometry_format="obj", geometry_format="gltf", or geometry_format="dae" to Rom.export_geometries() or Rom.export_all() to write another geometry format instead. Rom.export_textures() separately writes PNG files for geometry textures referenced by display lists. It uses the same texture-state reconstruction described on this page, so those PNGs have reliable dimensions from display-list format, size, palette, width, and height. For table 7, table 14, and unreferenced table 25 entries, Rom.export_textures() also writes best-effort RGBA5551 PNGs when the decompressed byte length matches a known size guess. Guessed outputs include guess in their filenames. Use Rom.export_assets() or Rom.export_raw_tables() when you need raw bytes from texture tables.

Testing Coverage#

The texture, OBJ, glTF/GLB, and DAE behavior is covered in tests/test_texture_export.py. Important coverage includes:

  • textured OBJ material and PNG output;

  • textured glTF material, sampler, alpha, binary buffer, and PNG output;

  • GLB chunking and embedded texture output;

  • textured DAE material, sampler, alpha-mask, and PNG output;

  • OBJ RGB vertex colors and glTF/GLB/DAE RGBA vertex colors;

  • UV generation from vertex texture coordinates;

  • material grouping by active texture state;

  • palette-based CI4 and CI8 decoding;

  • RGBA, IA, and I texture decoding;

  • packed mipmap exports for RGBA16 32x32, CI4 32x64, CI4 64x32, CI4 32x32, and CI8 32x32;

  • ensuring production geometry export does not write raw *_base_* mipmap images;

  • ensuring test_mipmap_export() does write base/reference images.

Run the focused texture tests with:

python -m pytest tests/test_texture_export.py -q

Run the whole suite with:

python -m pytest -q

Known Limitations#

The current exporter is intentionally conservative:

  • It supports the packed mipmap layouts that have been identified so far, not every possible N64 layout.

  • It does not write or reference mipmap chains in the MTL, glTF, GLB, or DAE material. The material references the highest-resolution decoded PNG because that is what normal importers expect.

  • It parses G_TEXTURE scale fields but does not currently apply them to OBJ, glTF, GLB, or DAE UVs.

  • It does not auto-detect DK64 animated texture frame ranges. Animated DAE atlas export requires an explicit animated_texture_frames mapping.

  • It writes RGB vertex colors to OBJ but does not write vertex alpha.

  • Texture clamp fields are translated to clamped OBJ UVs and -clamp on MTL texture map hints, to glTF/GLB wrapS/wrapT sampler values, and to DAE wrap_s/wrap_t sampler hints. Mirror, mask, and shift fields are parsed in G_SETTILE but are not currently translated into exporter behavior.

  • OBJ/MTL importer support for -clamp on varies. The exporter emits it because it is part of the Wavefront material syntax, but clamped UVs are the compatibility fallback.

  • OBJ/MTL cannot express Blender’s Blended render method. The generated *.blender.py script is the Blender-specific workaround.

  • Unsupported formats fall back to a placeholder image instead of failing the export.

These limitations are good candidates for future refactors, especially before the eventual Rust rewrite. The current tests should be treated as behavioral documentation for the layouts and export decisions described on this page.