Skip to content

max_geometry_formats

Jan Boon edited this page Jul 6, 2026 · 4 revisions

title: Max Geometry Formats description: published: true date: 2026-07-06T12:44:41.048Z tags: editor: markdown dateCreated: 2026-07-06T12:44:41.047Z

.MAX Geometry & Material Chunk Formats — Implementation Specification

Scope: the custom payloads stored by the following scene entities inside a 3ds Max scene file: the Ryzom/NeL patch object (RPO) and Edit Patch modifier; Editable Poly object; Poly Select modifier; Edit Poly modifier; Editable Mesh object; Edit Normals modifier; shared paint-selection payloads; and eight material/texture types. The outer .max compound-file container, the scene reference graph, ParamBlock/ParamBlock2 encoding, and Mesh, PatchMesh, MNMesh, BitArray, Matrix3, Spline3D, named-set-list, and wide-string encodings are external formats referenced but not defined here.


0. Container conventions

0.1 Chunk streams

All payloads live in a nestable tagged-chunk stream. Each chunk records a 16-bit ID and a byte length (framing is provided by the container; a reader can always determine a chunk's payload size and skip it). Chunks may nest. Chunk IDs are scoped to their owning object type and nesting level — identical numeric IDs recur with unrelated meanings; a reader must dispatch on (owner class, nesting context, ID), never on ID alone.

Two parser disciplines occur, and each stream below is labeled with one:

  • ORDERED (peek-based): chunks must appear in exactly the documented order; each is individually optional, but an unexpected ID terminates the current phase. Extending such a stream is only safe by appending trailing chunks.
  • DISPATCH (switch-loop): sibling chunks may appear in any order; unknown IDs are skipped silently. Safe to extend anywhere at that level.

0.2 Primitive encodings

All scalars are native little-endian x86/x64 Windows encodings, written raw with no alignment or endianness conversion:

Type Size Notes
int32 / BOOL / DWORD / uint32 / TimeValue 4 BOOL stores 0/1
uint16 2
uint8 / bool8 1 C++ bool
float32 4 IEEE-754
Point3 12 three consecutive float32 (x,y,z)
wide string var SDK wide-string chunk encoding (external)

int and DWORD remain 4 bytes on 64-bit builds, so 32- and 64-bit writers agree. Some blocks are raw struct dumps and additionally depend on MSVC struct padding — flagged individually.


PART A — Ryzom/NeL Patch Object ("RPO")

This is the format most relevant to exporting Ryzom Core assets.

A.1 Top-level object stream (ORDERED)

Chunk 0x08FD (2301) "validity":
    uint32  version                  ← format version of the following blob
    <RPatchMesh blob>                ← §A.2, raw sequential, inline

<PatchMesh>                          ← PatchMesh stream (external)
<Mesh>                               ← Mesh stream (external);
                                       cached triangulation of the patch

Reader rule: if version is not a recognized value, skip the remainder of chunk 2301 (chunk framing bounds it) and treat the RPatchMesh state as defaults; the PatchMesh and Mesh streams still follow unconditionally.

A.2 RPatchMesh blob (raw sequential, versioned 1–9)

Writers always emit the newest layout (v9). Readers must accept 1–9 and reject anything else.

v9 layout:

uint32  version

int32   patchCount
patchCount × PatchRecord:
    int32   log2TilesU               ← tile columns = 1 << log2TilesU
    int32   log2TilesV               ← tile rows    = 1 << log2TilesV
    int32   tileCount                ← = (1<<log2TilesU) * (1<<log2TilesV)
    tileCount × TileRecord:
        uint16  tileNum
        uint16  tileFlags
        uint8   displaceNoise        ← v9 only; see version table
        3 × LayerRecord:
            bool8   reserved         ← always written 0; ignore on read
            int32   tileId
            int32   rotation
    int32   colorCount               ← = ((1<<log2TilesU)+1) * ((1<<log2TilesV)+1)
    colorCount × uint32 vertexColor  ← 0x00RRGGBB
    4 × uint32 edgeFlags             ← per patch edge (v7+; see table)

int32   vertexCount
vertexCount × BindRecord:
    bool8   isBound
    int32   bindType                 ← enum, see below
    uint32  edgeIndex                ← 0..3, edge of target patch
    uint32  patchIndex               ← target patch
    uint32  before, before2          ← rebuildable cache indices
    uint32  after,  after2           ←   "
    uint32  tangent                  ←   "
    int32   bindType2                ← duplicate of bindType (written twice;
                                       readers should read & discard)
    uint32  primaryVertex            ← primary vertex of the bind group

int32   tileTessLevel
bool8?  modeTile                     ← width = declared member size; see A.3
bool8?  keepMapping                  ←   "
int32?  transitionType               ← WARNING: see A.4
int32?  selLevel

Bind type enum: four values corresponding to bind fractions {0.25, 0.75, 0.50, single}; the enum ordering observed in the interpolation table is 25%, 75%, 50%, SINGLE = 0..3, but this must be confirmed against a sample file before relying on it. Width is that of a C enum (4 bytes).

Cache fields (before/after/tangent) are persisted but fully reconstructable from topology; an exporter may ignore them.

A.3 Version differences (reader requirements)

Field group v1–v2 v3–v4 v5–v6 v7–v8 v9
Tile record two int32 per tile, read and discarded; tiles empty uint16 num + 3 layers; flags forced 0; noise randomized on load + uint16 flags + uint16 flags + uint8 noise
Vertex colors present, overwritten to white (≤v5) same v5 white / v6 kept kept kept
Edge flags absent absent absent present (4×uint32) present
Bind records identical in all versions
Trailer tessLevel, modeTile, keepMapping + transitionType (v4 only — see below) ? ? + selLevel

Loads of pre-v9 files are non-deterministic in the original tool (tile noise is randomized at load); an independent reader should just set noise = 0 or any fixed value.

A.4 Known trailer inconsistency (must verify empirically)

The original reader consumes transitionType only when version == 4, while the writer emits it for every version. If the current write version is really 9, then in files as written the trailer bytes after keepMapping are: transitionType, selLevel — but a version-9-aware reader following the original logic would mis-assign them. Before finalizing your parser, hex-inspect a known v9 file: locate keepMapping and check whether one or two 4-byte fields precede end-of-chunk. Treat the trailer layout for v≥5 as empirically determined, and use the chunk length of the enclosing 0x08FD chunk to bound/validate the read.


PART B — Edit Patch modifier (NeL variant)

B.1 Modifier stream (DISPATCH), after standard modifier base data

ID Payload Semantics
0x1100 empty presence ⇒ named-selection sets need no fixup (newer files); absence ⇒ legacy fixup
0x1001 int32 subobject level (object/vertex/edge/patch/tile)
0x1010 BOOL display lattice
0x1020 BOOL display surface
0x1040 int32 meshSteps; BOOL adaptive (always 0, reserved) viewport tessellation
0x10E0 int32 render steps; readers clamp to 0..100 else use 5
0x10F0 BOOL show interior edges
0x1090 / 0x10A0 / 0x10B0 TessApprox (external SDK format) view / production / displacement tessellation
0x2000 tileMode; tileLevel; keepMapping (member-sized raw fields) Ryzom tiling globals
0x2002 includeMeshes Ryzom
0x2001 transitionType Ryzom
0x10C0 BOOL viewTessNormals; BOOL prodTessNormals
0x10D0 BOOL viewTessWeld; BOOL prodTessWeld
0x1050 / 0x1060 / 0x1070 container named-set names for vertex / edge / patch level; contains repeated 0x1080 wide-string subchunks, one per set name
0x1000 int16 legacy read-only: old selection level; 1 → patch, 3 → vertex
0x1030 legacy read-only marker; obsolete-file indicator

Note the 0x2000-range IDs here are unrelated to the 0x4000-range IDs in §B.2 despite identical meaning.

B.2 Per-node local data

All wrapped in a single chunk 0x1000; its children (DISPATCH):

ID Payload
0x1015 DWORD flags
0x1020 int32 meshSteps; BOOL adaptive (0)
0x1150 int32 renderSteps (clamped as above)
0x1160 BOOL showInterior
0x1070 / 0x1080 / 0x1090 TessApprox view / prod / disp (external)
0x1030 BOOL displaySurface; BOOL displayLattice
0x1110 BOOL viewTessNormals; BOOL prodTessNormals
0x1120 BOOL viewTessWeld; BOOL prodTessWeld
0x4000 tileMode; tileLevel; keepMapping
0x4003 includeMeshes
0x4002 transitionType
0x1040 / 0x1050 / 0x1060 named-set lists (vertex/edge/patch), external list format; only if non-empty
0x1130 vertex mapper — §B.3
0x1140 PatchMesh (external) — the fully edited result patch
0x4001 RPatchMesh blob — the complete §A.2 format, nested here

Legacy pre-R3 files instead contain an edit-history record list (per-operation records replayed at load). The record chunk IDs/payloads are not covered here; for asset extraction you can treat any file lacking 0x1140/0x4001 inside the local data as requiring the legacy path, which is out of scope. For exporting geometry, chunks 0x1140 + 0x4001 are the ones you want: they hold the final patch and its Ryzom tile/bind data.

B.3 Vertex mapper (chunk 0x1130 contents)

One child chunk 0x1000:

int32       vertCount
vertCount × MapVertRecord      ← raw struct dump
int32       vecCount
vecCount  × MapVertRecord      ← raw struct dump

MapVertRecord is, by usage: int32 currentIndex (−1 = unmapped), Point3 originalPos, Point3 delta, BOOL originalStored — nominally 32 bytes with no padding, but the on-disk size is compiler-defined and unversioned. Validate against chunk length: record size = (chunkLen − 8) / (vertCount + vecCount). This block only matters if you need to re-apply deltas against a changed base object; for straight export of the stored final patch it can be skipped.

B.4 Point-delta tables (undo-support structure)

A delta wrapper chunk 0x1000 contains three sibling chunks:

ID Payload
0x1000 int32 n; n × Point3 — additive per-vertex position deltas
0x1020 int32 n; n × int32 — per-vertex flag masks, applied by XOR (coplanar bit)
0x1010 int32 n; n × Point3 — additive per-tangent-handle deltas

Counts are independent and normally equal the patch's vertex/vertex/vector counts. This structure serves the legacy record system and is not needed for exporting current-format files.


PART C — Editable Poly object (ORDERED)

Stream layout, exact order required:

[0x4039]  paint-handler payload (§H.1)             always present in current files
[0x403A]  paint-host payload (§H.2)                always present in current files
0–3 pairs, one per non-empty named-set level j ∈ {0=vertex,1=edge,2=face}:
    [0x3003]  int32 j
    [0x3004]  named-set list (external format)     must immediately follow its 0x3003
[0x3002]  int32 vertexControllerCount              only if any animated vertices
[0x4038]  DWORD subobjectLevel
<PolyObject base data>                             external; includes the MNMesh

Rules:

  • Every chunk is optional (older files omit leading ones); order is mandatory. A 0x3003 not followed by 0x3004 ends named-set parsing.
  • 0x3002 carries only a count; vertex controllers persist through the scene reference system, not this stream.
  • Subobject level values: 0 object, 1 vertex, 2 edge, 3 border, 4 face, 5 element (inferred; the four base values are confirmed via the fallback mapping below, border/element by parallel with §D — verify against a sample).
  • If 0x4038 is absent, derive level from the mesh's own stored level (object/vertex/edge/face; border and element are unrecoverable). If present, it is authoritative (border ⇒ mesh edge level, element ⇒ mesh face level).
  • Current selection bit-arrays are not stored; rebuild them from per-component selection flags inside the mesh.
  • The parameter block (soft selection, subdivision, cage colors, etc.) and point controllers are not in this stream.

PART D — Poly Select modifier

Selection-level values (explicit): 0 object, 1 vertex, 2 edge, 3 border, 4 face, 5 element. Named-set levels: 0 vertex, 1 edge, 2 face; border shares edge sets, element shares face sets.

D.1 Modifier stream (DISPATCH), after modifier base data

ID Payload
0x0100 DWORD selectionLevel (0–5)
0x0120 DWORD flags; bit 0x0001 = "show end result" toggle
0x2805 container: vertex-level set names
0x2807 container: edge-level set names
0x2806 container: face-level set names
0x2848 paint-handler payload (§H.1)

⚠ Note the numeric inversion: face container (0x2806) < edge container (0x2807). Same inversion in §D.2.

Inside each name container (ORDERED pairing within a DISPATCH loop): repeated per set —

ID Payload
0x2809 wide string: set name; on read also assigns a provisional sequential ID (0,1,2,…)
0x2810 DWORD set ID; overrides the provisional ID of the most recently read name

A name chunk must precede its ID chunk; the ID chunk is per-set optional (legacy files keep sequential IDs).

D.2 Per-node local data (DISPATCH)

ID Payload
0x0200 BitArray (external): vertex selection — always present
0x0210 BitArray: face selection — always present
0x0220 BitArray: edge selection — always present
0x2845 named-set list, vertex level (only if non-empty)
0x2847 named-set list, edge level (only if non-empty)
0x2846 named-set list, face level (only if non-empty)
0x2849 paint-host payload (§H.2) — always present

Bit-array sizes are not stored against a mesh size; consumers resize to the incoming mesh preserving bits.

Name/ID split: set names live in the modifier stream, set bit-arrays live per node, joined by the DWORD ID — never by index or name. The two lists may drift (counts and order can differ); a reader must treat the union as truth with ID as the join key.


PART E — Edit Poly modifier (operation-history variant)

E.1 Modifier-level chunks (DISPATCH)

ID Payload
0x0080 modifier base data (external; includes the parameter block: animation, current-operation selector, slice plane, master point controller, per-vertex controllers)
0x0100 raw subobject level (same 6-level enum family as Part C)
0x0110 preserve-UVs per-map-channel bit set (external map-bit-array format)
0x0120 paint-handler state (external/§H family; format owned by the paint system)
0x0138 int32 n — count of per-vertex point-controller range boundaries
0x0130 n × int32 — the boundaries (must follow 0x0138)
0x0148 int32 n — count of node-name entries
0x0140 one wide string per chunk, repeated; associated to slots purely by encounter order after 0x0148

The last four are present only when animated vertex transforms exist.

E.2 Per-node chunks (DISPATCH except where noted)

ID Payload
0x0200 / 0x0210 / 0x0220 BitArray: vertex / edge / face selection
0x0224 / 0x0228 BitArray: vertex hide / face hide
0x0240 paint-host + named-set state (external paint format)
0x0230 int32 operation ID — starts an operation-history triple
0x0234 operation payload (§E.3); belongs to the immediately preceding 0x0230
0x0238 operation local data (§E.4); optional; belongs to the same triple
0x0260 int32 — ID of the operation currently being edited
0x0268 local data for that current operation

Ordering rule: the triple 0x0230 → 0x0234 → [0x0238] must be adjacent and in that order, repeated once per committed operation in application order. 0x0260 must precede 0x0268. Unknown operation IDs are fatal to the original loader (no fallback); an independent exporter should record and skip unknown ops gracefully instead.

E.3 Operation payload (inside 0x0234 or a current-op equivalent)

One wrapper chunk 0x0600 ("basics"), plus optional per-type sibling chunks. Inside 0x0600 (DISPATCH):

ID Payload
0x0400 BitArray — captured subobject selection
0x0410 int32 paramID + raw parameter bytes; repeated once per parameter. Byte length is parameter-defined: 4 for int/float/world/angle, 12 for Point3, 64 for a raw 4×4×4-float matrix.
0x0420 Matrix3 (external) — transform (only if the op has one)
0x0430 Matrix3 — mod-context TM
0x0440 map-bit-array — preserve-map settings

Unknown parameter IDs abort the load in the original, except five reserved IDs that are skipped. An independent reader should skip-with-warning using the chunk length.

Per-type sibling chunks next to 0x0600:

Op type ID Payload
detach vertex / detach face 0x0601 bool8 — detach-as-clone
remove edge 0x1601 bool8 — ctrl-key (also remove verts)
extrude along spline 0x0644 Matrix3 — spline node TM at capture
extrude along spline 0x0640 Spline3D (external) — captured spline

Extrude-along-spline also carries a world-to-object matrix as a parameter (0x0410 with 64-byte raw payload).

E.4 Operation local-data payloads (inside 0x0238 / 0x0268)

Each op type has its own child-chunk vocabulary. "count+array" = int32 n then n elements in the same chunk.

Op ID Payload
create 0x0600 count+array of Point3 — new vertex positions
0x0608 int32 — total new-face count
0x0610 repeated per face: int32 faceIndex, int32 degree, degree × int32 vertex indices
attach 0x0E04 int32 n — number of attach slots
0x0E08 int32 which — slot selector for the next mesh chunk
0x0E00 MNMesh (external) — attached mesh, into slot which
0x0E0C n × int32 — per-attach material-ID modulus
cut 0x0840 int32 n; n×int32 startLevel; n×int32 startIndex; n×Point3 start; n×Point3 end; n×Point3 normal
target weld (vert & edge) 0x06C0 count+array int32 — source indices
0x06C4 count+array int32 — target indices
create edge 0x0640 count+array int32 — flattened (v1,v2) pairs
edit triangulation 0x0680 count+array int32 — flattened diagonal pairs
turn edge 0x06F0 count+array int32 — diagonal indices
0x06F2 count+array int32 — parallel face indices
insert vertex (edge) 0x1600 int32 n; n×int32 edge index; n×float32 parametric position
insert vertex (face) 0x1608 int32 n — allocation count
0x1610 repeated: int32 which, int32 face, int32 numBary, numBary×float32
hinge from edge 0x32F0 int32 — hinge edge index
transform 0x12C3 int32 n
0x12C4 n × Point3 — per-vertex offsets (0x12C3 must precede)
0x12C5 wide string — node name

ID collisions across ops (e.g. 0x0600, 0x0640) are safe because each payload is parsed only in its own op context.


PART F — Editable Mesh object (ORDERED)

Write order (each chunk optional on read; sequence fixed):

optional legacy block:
    [0x3001] container — pre-R3 named sets:
        [0x2845] vertex sets  \  each contains repeated pairs:
        [0x2846] face sets     >   [0x2849] BitArray (external)
        [0x2847] edge sets    /    [0x2850] wide string name
      (inner containers optional, any order; set/name pairing is by arrival order;
       IDs are synthesized sequentially on migration: set i → ID i)

0–3 modern named-set pairs:
    [0x3003] int32 levelIndex (0..2)
    [0x3004] named-set list (external)
[0x3002] int32 vertexControllerCount
[0x4020] BOOL   affectRegion
[0x4024] float32 falloff            (written under a 4-byte int size; payload is float bits)
[0x4025] float32 pinch              (−10..10)
[0x4026] float32 bubble             (−10..10)
[0x402C] BOOL   useEdgeDistance
[0x402D] int32  edgeIterations      (1..999)
[0x4030] BOOL   ignoreBackfacing    (inverted sense vs. UI "affect backfacing")
[0x4034] DWORD  objectFlags         (bit meanings mostly external; one bit = show-end-result)
[0x4038] DWORD  subobjectLevel      (0 obj, 1 vert, 2 edge, 3 face, 4 polygon, 5 element)
[0x403B] int32  referenceLayoutVersion (always 1 in current files)
<TriObject base mesh data>          external

Reader notes:

  • A file contains either the legacy 0x3001 block or modern pairs, not both.
  • If 0x4038 is absent, derive the level from the mesh's own level, mapping mesh-face-level to polygon (4), not face (3). If present, it is authoritative; face/polygon/element all map back to mesh face level.
  • Reference layout: version 0 (chunk absent) means per-vertex controllers start at reference 0 and everything shifts up by one to make room for a master controller; version 1 means reference 0 is the master controller and vertices follow. Only relevant if you consume animated vertices via the reference system.
  • Edge named-set bit-arrays are sized numFaces × 3, indexed face*3 + side. Vertex sets size numVerts, face sets numFaces.
  • The modern-pair loop tolerates a dangling 0x3003; the level index is not bounds-checked in the original (valid files use only 0–2).

PART G — Edit Normals modifier

Identity: Class ID (0x4AA52AE3, 0x35CA1CDE), object-space modifier. One reference (the parameter block; external encoding). Pblock parameters for completeness: select-by mode (int 0–3), ignore-backfacing (BOOL), show-handles (BOOL), display-length (float, default 10).

G.1 Modifier stream (DISPATCH), after modifier base data

ID Payload
0x0100 int32 — selection level (object / normal / vertex / edge / face; numeric values external, verify)
0x0140 container — named-set names; children (positional pairing): repeated { 0x0144 wide-string name → 0x0148 DWORD id }. Name must precede its ID; ID per-set optional (sequential provisional IDs otherwise).

G.2 Per-node local data (DISPATCH)

ID Payload
0x0200 named-set list (external) — per-set bit-arrays keyed by DWORD ID
0x0240 poly-mesh normal spec (external) — only for poly meshes
0x0250 tri-mesh normal spec (external) — only for tri meshes; mutually exclusive with 0x0240
0x0260 DWORD flags — only bit 0x08 ("tri-mesh mode") is durable; bits 0x01/0x02/0x04 are transient undo markers that may appear and should be ignored

Cached meshes, validity intervals, and in-progress buffers are never persisted.


PART H — Shared paint payloads

These payloads appear inside different container IDs per owner (Editable Poly: 0x4039/0x403A; Poly Select: 0x2848/0x2849) but have identical contents. Their internal chunk IDs are 0x0000 and 0x0001 (sequential-from-zero enums), safe only because of nesting.

H.1 Paint-handler payload (DISPATCH)

ID Payload
0x0000 BOOL — painted soft-selection active
0x0001 BOOL — paint deformation active

Both optional; default false.

H.2 Paint-host payload (DISPATCH)

ID Payload
0x0000 float32[N] — soft-selection weights, one per vertex in vertex order; N = chunkLength / 4 (no explicit count)
0x0001 Point3[M] — deformation offsets (object-space deltas); M = chunkLength / 12

Each written only if its count > 0. Weights nominally 0..1. Writers emit exactly the logical count, which may be smaller than a runtime buffer.


PART I — Material and texture formats

Universal rule: chunk 0x4000 in every material/texmap wraps the shared material-base payload (name, G-buffer channel, base flags — external format). All loaders here are DISPATCH; unknown IDs skip silently. Three payload kinds recur:

  • data — carries bytes;
  • presence-only — empty; existence is the boolean;
  • marker — empty; selects a load path / migration.

I.1 Bitmap texture

Current files write only 0x4000; everything else lives in a ParamBlock2 (external). Legacy read-only chunks:

ID Kind Meaning
0x5002 data int32 filter type
0x5003 data int32 alpha source; clamp to {0 file, 2 rgb, 3 none}, fallback 0. Note: the ParamBlock2 layer re-encodes these as 0/1/2.
0x5004 data wstring legacy bitmap filename
0x5010 data bitmap-info block (external); start/end frame forced to "undefined" after read
0x5011 data TimeValue start time
0x5012 data float32 playback rate
0x5013 presence alpha-as-mono = true
0x5014 data int32 end condition
0x5016 presence alpha-as-RGB = true
0x5021 presence very-old-file marker
0x5030 presence premultiplied alpha = false
0x5040 presence apply crop = true
0x5050 presence place-image mode = true
0x5060 presence jitter placement = true
0x5001, 0x5022 defined but never handled; dead

Any 0x50xx chunk ⇒ legacy file ⇒ values migrate into the param block after load.

I.2 Standard material (original)

ID Kind Payload
0x4000 data material base
0x5000 data ULONG flags
0x5004 data int32 shading model; clamp, fallback Phong
0x5300 data int32 version (current 9)
0x5002 data ULONG per-map enable bitmask (bit i = map i on)
0x5400 presence dim-reflect = true
0x5100–0x510A data float32 per-map amount (one chunk per map slot; written only when ≠ 1.0)

Files with version < 9 require migration (map-amount blending, soften level) — for asset extraction, reading the values as-is is usually sufficient.

I.3 Standard material 2

Save: 0x4000 base; 0x5300 int32 version (current 13); 0x5600 exposure-control payload (external). Load additionally accepts all §I.2 legacy chunks (0x5000/0x5004/0x5002/0x5400). Reserved IDs: 0x5500, 0x5510, 0x5511. Versions ≤ 10 stored parameters in an old flat block redistributed at load; 11–12 had a bump-scale fixup. Reference-slot order (for scene-graph remapping): old-pblock, texmaps, shader, shader-pblock, extended-pblock, sampling-pblock, maps-pblock, dynamics-pblock, sampler.

Texmap sub-container vocabulary (owned by the maps container, referenced by both variants): 0x5002 presence ⇒ obsolete file; 0x5003 data ULONG enable bitmask; 0x5100–0x510A per-map amount.

I.4 Composite map

New namespace: 0x1000 = header (material base), 0x1001 = int32 fileVersion. Compatibility namespace: 0x4000 = old header, 0x0010 = int32 legacy sub-tex count. File versions: 0 = 2008-era, 1 = beta, 2 = 2009+. Collision hazard: new header 0x1000 equals the old map-offset base, and old header 0x4000 equals the universal base-header ID; disambiguation is stateful (first header chunk seen wins, and old/new headers are mutually exclusive). Old versions store maps/masks as direct references restructured into a param block on load; opacity was once integer, now float ×1.

I.5 Composite material

0x4000 base; 0x1010 int32 version (current 2). Version 1 files carry one extra leading slot in each of three tab parameters (type/enable/amount) that must be dropped (delete index 0, shrinking from max-slots to max-slots−1). ID 0x1000 reserved (map-off range).

I.6 Multi/sub-object material

ID Kind Payload
0x4000 data base
0x4005 marker "new sparse array" layout; implies 0x4003 semantics too
0x4002 data int32 sub-material count; sizes name/enable tables
0x4001 marker obsolete layout; implies 0x4002 follows semantically (old writer fall-through)
0x4010 data name table (external)
0x4003 marker "ParamBlock2 present"
0x1000–0x1FFF presence range id − 0x1000 = sub-material index whose enable flag is FALSE

Discriminators: no 0x4003 ⇒ oldest layout; no 0x4005 ⇒ old dense-array layout; both trigger table rebuild/reindex at load.

I.7 Double-sided material

0x4000 base; 0x1010 marker "ParamBlock2 present". Legacy: 0x1000/0x1001 presence ⇒ facing/back material disabled; old files also rescale translucency ×100 into the param block.

I.8 Vertex-color map

0x4000 base. 0x5000 (presence: use-UVW) is written by old files but ignored on read in current code — state lives in the param block.

I.9 Cross-class ID collision table (why global dispatch is impossible)

ID Distinct meanings
0x1000 multi map-off base / double-sided material-off base / composite-map new header / composite-material map-off / patch local-data wrapper / vertex-mapper data / point-delta table …
0x1010 double-sided marker / composite-material version
0x4000 universal material header and composite-map legacy header
0x5000 std-material flags / vertex-color use-UVW
0x5002 std-material map bitmask / texmap-container obsolete marker

Clone this wiki locally