-
Notifications
You must be signed in to change notification settings - Fork 113
max_geometry_formats
title: Max Geometry Formats description: Chunk-level payload formats of the RPO/NeL patch, editable-object, modifier, and material scene classes inside .max files — original-plugin serializer semantics, cross-referenced against the pipeline_max implementation rules published: true date: 2026-07-06T12:44:41.048Z tags: editor: markdown dateCreated: 2026-07-06T12:44:41.047Z
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.
Where this comes from. Parts A–B describe the serializers of the in-tree NeL plugin sources (nel/tools/3d/plugin_max/nel_patch_lib/rpo.cpp + nel_patch_mesh.cpp, nel_patch_edit/np_epm_file.cpp + np_edit_patch_data.cpp + np_ep_vert_mapper.cpp) and were re-verified against them line-by-line (2026-07-06); the corrections from that pass are folded in below and the affected statements are marked SOURCE-VERIFIED. Parts C–I describe the corresponding 3ds Max editable-object and material serializers of the same era; that code is not in this repository, so those parts carry the confidence of a careful source reading but no in-tree re-verification and no corpus validation yet — treat their unhedged claims as "per the original serializer source".
What this page is for. It catalogues the semantic payloads: what the original plugins wrote and how their loaders consumed it. It is the reference map for the day these chunks get typed decoders in the headless pipeline (geometry for shape export; materials for milestone §12.5 (g) of Pipeline Max Design). It is not an implementation directive for how pipeline_max readers behave.
The implementation rules live in Pipeline Max Design, and they override the reader guidance on this page wherever the two differ. Where this page says the original loader "skips unknown IDs silently", "clamps", "defaults", "randomizes on load", or "rejects":
-
pipeline_maxnever skips or drops anything. Unknown chunks fall through toCStorageRaw/CStorageContainerand are re-emitted verbatim; a typed class that cannot make sense of its data callsdisown()and degrades to raw pass-through for that one object (design doc §5) — it does not reject the file. - A typed reader must consume every byte and reproduce it exactly on write (the T2 corpus gate) — including values the original loader discards, clamps, or overwrites: the v9 noise byte, the bind-record cache fields, the duplicated
bindType, always-zero reserved bytes, out-of-range values. Where this page says an exporter "may ignore" a field, that means ignore semantically; the bytes still roundtrip. - A chunk id in this catalogue gets a typed
pipeline_maxmapping only once the typed serializer reproduces every corpus instance byte-exactly (design doc §12.2, "type only what you can prove"). This page shortcuts the format archaeology, not the corpus gate. - The ORDERED/DISPATCH labels (§0.1) describe the original loaders' parse disciplines. They are evidence about which chunk orders and extensions can occur in real files and where the original writer's emission order is fixed;
pipeline_maxitself preserves whatever order the file actually has, always.
Where these streams sit in the pipeline_max object model today:
| This page | .max chunk ids as seen by pipeline_max
|
Current handling |
|---|---|---|
| Part A (RPO object) | 0x08FD + PatchMesh/Mesh chunk streams on the RPO scene object | untyped — CSceneClassUnknown pass-through (plugin object class, not registered) |
| Part B (Edit Patch modifier) | modifier + per-node local data | untyped pass-through |
| Part C (Editable Poly stream) | 0x4039, 0x403A, {0x3003+0x3004}×n, 0x3002, 0x4038 | exactly the known-unknown sequence EPOLY::CEditablePoly::parse drains and re-emits verbatim (epoly/editable_poly.cpp) |
Part C <PolyObject base data> (MNMesh) |
PolyObject ids 0x0906–0x090C (noted, unclaimed) + GeomObject 0x0900 and 0x08FE GeomBuffers (poly buffers: 0x0100 vertices / 0x010A edges / 0x011A faces) | typed serializers exist for the 0x08FE buffers but are compiled out (PMBS_GEOM_BUFFERS_PARSE=0); rides raw |
| Part F (Editable Mesh stream) | 0x3001 (+0x2845/6/7), {0x3003+0x3004}×n, 0x3002, 0x4020–0x403B | the known-unknown sequence UPDATE1::CEditableMesh::parse drains (update1/editable_mesh.cpp) |
Part F <TriObject base mesh data> (Mesh) |
TriObject ids 0x0901–0x0904 (noted, unclaimed) + 0x0900/0x08FE (tri buffers: 0x0914 vertices / 0x0912 faces / 0x0916+0x0918, 0x0938+0x0942) | same as above |
| Parts D, E, G, H (Poly Select / Edit Poly / Edit Normals / paint) | modifier scene classes | untyped pass-through (registered unknown superclasses) |
| Part I (materials/texmaps) | material scene classes | untyped pass-through — the builtin material stubs are empty and unregistered; forward reference for design-doc milestone §12.5 (g) |
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.
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.
This is the format most relevant to exporting Ryzom Core assets.
Chunk 0x08FD (2301) "validity":
uint32 rpoVersion ← RPO_SERIALIZE_VERSION; always 0 (SOURCE-VERIFIED)
<RPatchMesh blob> ← §A.2, raw sequential, inline;
carries its OWN leading version field (1–9)
<PatchMesh> ← PatchMesh stream (external)
<Mesh> ← Mesh stream (external);
cached triangulation of the patch
There are two version fields: the outer rpoVersion (a container/validity version, RPO_SERIALIZE_VERSION = 0 — the only value ever written) and the blob's internal format version (1–9, §A.2). Reader rule (SOURCE-VERIFIED, RPO::Load): the original loader parses the blob only when rpoVersion == 0; for any other value it skips the remainder of chunk 2301 (chunk framing bounds it) and leaves the RPatchMesh state at defaults. The PatchMesh and Mesh streams follow unconditionally either way.
Writers always emit the newest layout (v9). The original loader accepts 1–9 and returns IO_ERROR on anything else (SOURCE-VERIFIED, RPatchMesh::Load). For pipeline_max the equivalent of "reject" is per-object disown() to raw pass-through, per §0a.
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 ← rTess.TileTesselLevel (int)
bool8 modeTile ← rTess.ModeTile (bool, 1 byte)
bool8 keepMapping ← rTess.KeepMapping (bool, 1 byte)
int32 transitionType ← always written; original loader consumes it
only when version == 4 — see A.4
int32 selLevel ← always written; see A.4
(Trailer field widths SOURCE-VERIFIED from the member declarations in nel_patch_mesh.h and the raw sizeof-based writes.)
Bind type enum (SOURCE-VERIFIED, nel_patch_mesh.h): enum typeBind { BIND_25=0, BIND_75=1, BIND_50=2, BIND_SINGLE=3, BIND_COUNT, BIND_ALIGN=0xffffffff }, i.e. bind fractions in order 25%, 75%, 50%, single. Width is that of a C enum (4 bytes; the BIND_ALIGN member forces it).
Cache fields (before/after/tangent) are persisted but fully reconstructable from topology; an exporter may ignore them.
| 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
|
(unchanged) | + 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 (as consumed by the original reader) | tessLevel, modeTile, keepMapping, selLevel | same, + transitionType inserted before selLevel when version == 4 only | same as v1–v2 | same | same |
(SOURCE-VERIFIED: selLevel is read unconditionally for every version — it is not a v9 addition; transitionType is consumed only on the version == 4 path. See A.4 for why the reader's trailer does not match the writer's.)
Loads of pre-v9 files are non-deterministic in the original tool (tile noise is randomized at load); for semantic use a reader can pick any fixed noise, but a pipeline_max typed class preserves the stored byte (v9) exactly (§0a).
The original reader consumes transitionType only when version == 4, while the writer emits it for every version (SOURCE-VERIFIED: RPatchMesh::Save writes tessLevel, modeTile, keepMapping, transitionType, selLevel unconditionally; RPatchMesh::Load reads transitionType only inside if (nVersion == RPATCHMESH_SERIALIZE_VERSION_4)). Consequences:
-
Files as written by the current (v9) writer carry both fields after
keepMapping, in the ordertransitionType,selLevel. This is the layout to parse for byte-exact roundtrip. -
The original loader mis-reads its own v9 files: it assigns
selLevel ← transitionType's bytes and leaves the final 4 bytes (the realselLevel) unconsumed — absorbed silently by chunk framing atCloseChunk. Both fields are UI state, so the bug is cosmetic in the original tool, but it means the original loader is not the authority on the trailer layout; the writer is. - Files written by genuinely old plugin builds (pre-v9 writers) may have shorter trailers than the current writer emits; the enclosing 0x08FD chunk length bounds the read either way, and a reader should validate against it rather than assume.
Chunk ids and payloads in this part are SOURCE-VERIFIED against nel_patch_edit/np_epm_file.cpp (B.1), np_edit_patch_data.cpp (B.2, B.4), and np_ep_vert_mapper.cpp (B.3).
| 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 display-verts chunk (DISP_VERTS_CHUNK); the original loader ignores the payload and only calls SetObsolete() — obsolete-file indicator |
Note the 0x2000-range IDs here are unrelated to the 0x4000-range IDs in §B.2 despite identical meaning.
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.
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 (SOURCE-VERIFIED: written as a raw sizeof(EPMapVert) * count memory dump, both tables). 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.
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.
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.
pipeline_max correspondence: this stream — same ids, same order — is exactly the known-unknown sequence EPOLY::CEditablePoly::parse (epoly/editable_poly.cpp) drains and re-emits verbatim, which corpus-corroborates the layout above. The <PolyObject base data> that follows appears in the same scene-object container as the noted-but-unclaimed PolyObject ids 0x0906–0x090C plus GeomObject 0x0900 and 0x08FE GeomBuffers, whose poly buffers (0x0100 vertices / 0x010A edges / 0x011A faces incl. material, smoothing, triangulation cuts) are the MNMesh payload this page defers to — typed serializers exist in builtin/storage/geom_buffers.cpp but are compiled out pending the corpus gate (Pipeline Max Design §7).
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.
| 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).
| 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.
| 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.
| 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.
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).
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.
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.
-
Nesting of the legacy set containers is UNRESOLVED: the description above places 0x2845/0x2846/0x2847 inside the 0x3001 container;
UPDATE1::CEditableMesh::parse(update1/editable_mesh.cpp), written against real files, instead drains them as siblings immediately following 0x3001 at the same level. Both readings survive the roundtrip harness (unknown chunks pass through verbatim either way, nested or not), so the corpus gate cannot discriminate — hex-inspect an actual legacy-block file before typing this structure. - 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, indexedface*3 + side. Vertex sets sizenumVerts, face setsnumFaces. - The modern-pair loop tolerates a dangling 0x3003; the level index is not bounds-checked in the original (valid files use only 0–2).
pipeline_max correspondence: the sequence above — same ids, same order, including every 0x40xx trailer chunk through 0x403B — is exactly the known-unknown sequence UPDATE1::CEditableMesh::parse drains and re-emits verbatim, corpus-corroborating the layout. The <TriObject base mesh data> corresponds to the noted-but-unclaimed TriObject ids 0x0901–0x0904 plus GeomObject 0x0900 and 0x08FE GeomBuffers (tri buffers: 0x0914 vertices / 0x0912 CGeomTriIndexInfo faces incl. smoothing groups / 0x0916+0x0918 and 0x0938+0x0942 secondary pairs) — see Pipeline Max Design §7.
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).
| 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). |
| 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.
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.
| ID | Payload |
|---|---|
| 0x0000 |
BOOL — painted soft-selection active |
| 0x0001 |
BOOL — paint deformation active |
Both optional; default false.
| 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.
Status in pipeline_max: all material and texmap classes are currently untyped pass-through (the builtin material stubs — CMtlBase, CMtl, CStdMat, CStdMat2, CTexmap, CBitmapTex, … — are empty and unregistered). This part is the forward reference for Pipeline Max Design §12.5 milestone (g); every mapping below still has to pass the §12.2 corpus gate before it counts as established. Note also that for NeL export most material properties of interest come from the NeL AppData entries (design doc §8) and the standard-material ParamBlock2s, not from these legacy chunks.
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 (in the original loaders — see §0a for the pipeline_max discipline). Three payload kinds recur:
- data — carries bytes;
- presence-only — empty; existence is the boolean;
- marker — empty; selects a load path / migration.
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.
| 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.
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.
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.
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).
| 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.
0x4000 base; 0x1010 marker "ParamBlock2 present". Legacy: 0x1000/0x1001 presence ⇒ facing/back material disabled; old files also rescale translucency ×100 into the param block.
0x4000 base. 0x5000 (presence: use-UVW) is written by old files but ignored on read in current code — state lives in the param block.
| 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 |