Skip to content

max2gltf_plan

Jan Boon edited this page Jul 15, 2026 · 13 revisions

title: Max → glTF/Blend Conversion and Assimp-Route Parity — Plan description: Whole-max-file conversion to glTF, plus expanding the assimp exporter to match the direct-from-max route's coverage and fidelity — differential-gated against the existing corpus published: true date: 2026-07-14T00:00:00.000Z tags: editor: markdown dateCreated: 2026-07-14T00:00:00.000Z

Plan agreed 2026-07-14 (Kaetemi). Two deliverables, one architecture:

  1. max2gltf: whole-.max-file conversion to glTF 2.0 with nel_* extras, as a new pipeline_max_export_gltf tool. max2blend DROPPED 2026-07-14: the .gltf is the deliverable — importing it into Blender (extras arrive as custom properties automatically) is easy enough for the user; no headless post-pass tooling needed.
  2. Assimp-route parity: expand mesh_export/mesh_utils until, for every asset class the direct route exports, max → glTF → assimp route → artifact matches max → direct route → artifact under the existing corpus comparators.

Why

  • Asset liberation: the recovered .max corpus (8634 files) becomes editable in living tools for the first time since the Nevrax artist PCs were sold. Max-under-Wine leaves the loop.
  • Role inversion: the direct-from-max route (corpus-gated, byte/floateq tiers) becomes the frozen reference validator; Blender/glTF becomes the living authoring path. New content and edits flow through the assimp route; the legacy corpus proves that route faithful.
  • Downstream alignment: the tutorial/WebGL work (tutorial_roadmap) authors in Blender/glTF; the lightmapper's missing producer (c) (.lmscene from assimp, §11-lm) falls out of parity work for free.

Architecture rules

  • Branch at the pre-build model, never re-decode. pipeline_max_export_gltf shares the same T1 parse and pipeline_max_export_common extraction as the direct exporters: glTF geometry buffers are written from the same CMeshBuild-level data export_shape consumes, tracks from the same decoded data export_anim consumes. glTF buffers are raw IEEE float32, so values survive byte-exact. Fidelity testing then measures format loss only, not decode drift. (Same move as the .lmscene intermediate, in an interoperable container.)
  • NeL semantics ride in extras, per the established convention (NeL-Specific glTF Extras): per-key scalars, %.9g forced-decimal floats, readers prefer extras and fall back to standard glTF fields. Extras (not a vendor extension) because Blender maps extras to custom properties and round-trips them — artists see and can edit NeL appdata (LOD settings, collision/SWT flags, lightmap params) as plain custom props. The skeleton pipeline already does exactly this (pipeline_max_export_skel --gltfmesh_utils exportSkels(), bit-exact TRS via nel_t*/r*/s* keys) — it is the beachhead and the pattern to copy.
  • Assimp handles geometry/skins/nodes only; NeL extras are parsed from the glTF JSON directly. Assimp's aiMetadata plumbing is version-fragile; we control both ends of the file, so a small JSON pass for nel_* keys is more robust. Pin the assimp version; keep aiProcess post-processing at zero (no tangent generation, no node collapsing, no unit scaling — every one is a fidelity leak).

Lossless vs baked, per subsystem (explicit decisions)

Subsystem glTF carrier Fidelity class
Meshes (pos/normal/UV/vcolor), hierarchy standard glTF buffers lossless (float32 verbatim)
Materials incl. UV-scroll (waterfalls) PBR + KHR_texture_transform + nel_* extras for NeL-specific params lossless via extras
Skinning glTF skins (NeL's 4-weight limit fits) lossless
Skeletons done today (extras TRS) lossless (proven)
Animations sampled curves for interop plus exact NeL track data as an extras blob dual: sampled view lossy, blob lossless; Blender edits invalidate the blob (documented, detectable)
PatchMesh (zones, patch props) ligo outputs as nel_zones blob + tessellated nel_proxy viewing meshes (decision 2026-07-14; zone BUILD pipeline unchanged) blob lossless, proxy baked
Appdata (LOD, collision, SWT, lightmap params) nel_* extras per node/material lossless

Animation is the honest problem case: glTF has no Bezier/TCB keys, and the tangent semantics were hard-won (anim-export corpus). Sampled curves serve Blender authoring; the extras blob serves corpus round-tripping. Zones stay native — with the improvised ligo heightmap the zone sources are a direct-route concern regardless (see §10z-ring).

The differential gate (definition of "parity")

For every corpus file and every asset class: export via the direct route AND via max → glTF → mesh_export, compare artifacts with the existing field-level comparators (shape floateq tiers, skel, anim, ig). New ctest pipeline_max_gltf_corpus in the *_corpus.py shape, self-skipping without the corpus, budgeted like the other gates. This is the strongest available correctness story and it is cheap — every tier already exists.

Current assimp-route baseline (2026-07-14)

mesh_utils today: static meshes (position/normal/UV, materials via assimp_material, scene_meta) and bit-exact skeletons. Declared TODOs in assimp_shape.cpp: multi-LOD/MRM builds, water shapes, particle systems, remanence, flare, pacs prim. No animations, no IG, no .lmscene.

Sequencing

  1. max2gltf: static meshes + materials + node hierarchy + IG placement, differential-gated against the shape/ig corpora. (Largest asset volume, no open questions.) — DONE 2026-07-14 (design doc §13-gltf): full corpus differential green, every co-produced .shape and .ig byte-identical; ctest pipeline_max_gltf_corpus. Multilod assembly landed same day (+418 shapes → 2649 shapes + 623 igs byte-identical; direct-only = skinned/water/remanence/flare only). Remaining staged import skip: MRM morph targets (zero corpus hits outside skinned).
  2. Skinned meshes (skeletons already done) — skel/shape gates. — DONE 2026-07-14 (design doc §13-gltf): Physique + MRM morph targets carried (nel_skin_*, nel_bs_*); 3301 shapes + 623 igs byte-identical; direct-only = water/remanence/flare only. — glTF-skins interop view DONE 2026-07-14: JOINTS_0/WEIGHTS_0 + skin objects + IBMs, with the biped rig's figure-mode rest pose decoded onto the glTF nodes (was identity — biped TM controllers aren't PRS); rest-pose skinning identity verified to ~3e-7 m (ctest pipeline_max_gltf_skins); skinned characters are now poseable in any glTF DCC/viewer.
  3. Animations: sampled tier + nel_* track blob; anim corpus gate on the blob path, tolerance tier on the sampled path. — BLOB TIER DONE 2026-07-14 (design doc §13-gltf): asset.extras.nel_anim carries the exact CAnimation stream; 5045 anims byte-identical over the unioned 4524-file anim corpus; caught + fixed an NL3D uninitialized-range serialization bug that made even the direct exporter byte-nondeterministic. — SAMPLED CHANNELS DONE 2026-07-14: TRS tracks from the blob LINEAR-sampled at 30 fps onto the rig nodes (rest pose from the skins tier); clips play in any glTF viewer/DCC; blob stays authoritative (ctest pipeline_max_gltf_anim_channels). Non-TRS tracks (materials, morph factors) not sampled yet.
  4. .lmscene emission from the assimp route — closes lightmapper producer (c) (§11-lm).
  5. Blender post-pass (max2blend)DROPPED 2026-07-14 (user imports the .gltf directly; visual spot-checks stay a good idea, just not a tool).
  6. Remaining classes and processes — full parity at the glTF-with-nel-extras level is the goal (Kaetemi, 2026-07-14): nothing stays "as needed"; every artifact the .max pipeline produces must be producible from the .gltf.water/remanence/flare DONE 2026-07-14 as exact per-node nel_shape_blob; structural extras QUEUED (bar: reader reconstructs from extras, byte-gate against direct — same proof as materials; blob drops once green). — Ligo zones DONE 2026-07-14 as nel_zones blob + tessellated nel_proxy viewing meshes: final sweep 29910 shapes + 625 igs + 5045 anims + 2640 zone files byte-identical, 0 direct-only / 0 via-only — total co-production. — ALL .max processes DONE 2026-07-15 as gated blobs (swt, pacs_prim, veget, clod, cmb, skel — same guarded-main + corpus-gate pattern as igs/anims/zones; structural extras for water/remanence/flare also landed, writer self-checked). — .lmscene from the glTF route DONE 2026-07-15 as asset.extras.nel_lmscene: the shape exporter's whole per-file flow compiled in with PMB_SHAPE_NO_MAIN runs in lightmap-scene-only mode (receiver nodes take the full build path, everything else skips), bytes byte-gated against the direct --lm-scene write on the singles tier. Every artifact class the .max pipeline produces now rides the container.

Decisions 2026-07-14 (Kaetemi): zones + materials sidecar

  • Zones ride the glTF after all — as blob + baked proxy (supersedes the "stays direct-route" non-goal below for the container; the zone build pipeline itself is unchanged). The goal is the option to swap every .max asset to a .gltf asset with zero fidelity loss: nel_* custom data carries authoring truth, baked tessellated proxies give any glTF consumer something to render. Accepted limit: importing a zone proxy through the no-nel-extras emulation route (the "new artist in an unknown tool" case) yields only a plain mesh — patch landscape is not expressible in foreign tools, and materials on that route are likewise visualizable-but-broken.
  • Materials for the assimp route use a glTF SIDECAR, not a new metadata format: a materials-only .gltf (valid glTF — no nodes needed) carrying the same nel_* material extras
    • PBR interop view, edited by the future material editor, resolved by material NAME against whatever model file the artist provided (aiProcess zero preserves source names; unnamed materials warn + fall back to assimp_material conversion — never positional binding). Same codec as the whole-file route (flag-dword verified), previewable in any glTF viewer; support both per-asset and shared material-library sidecars via an explicit --materials flag. — Loader DONE 2026-07-14 (mesh_utils/material_sidecar.cpp, mesh_export --materials, repeatable with later-overrides-earlier stacking; behaviors gated by ctest pipeline_max_materials_sidecar — bind-all/fallback/partial/tamper-fails/stacking/ exact-route-ignores; schema section in NeL-Specific glTF Extras). The writer side stays with the future material editor (any max2gltf .gltf's materials array already IS a valid sidecar).

Decision 2026-07-15 (Kaetemi): node-properties sidecar for the assimp route

  • NeL export properties (the Max appdata) for artist-provided scenes ride a glTF sidecar too: empty dummy nodes in the sidecar's nodes array, matched to the scene's nodes by name, carrying the same per-node nel_* extras vocabulary the max2gltf writer emits (scene-level properties on the sidecar's asset.extras). Authored by a future dedicated NeL scene-properties editor that loads the artist file visually through assimp and saves only the sidecar — a separate tool from the material editor, same file format. Supersedes the binary .nelmeta/CSceneMeta sidecar. Design details in NeL-Specific glTF Extras § Node-properties sidecar. This is the last piece the assimp-route .lmscene producer (item 4) needs designed: geometry from the artist file, lights from KHR_lights_punctual + nel_* light extras, per-node lightmap appdata from this sidecar.

Non-goals

  • No zone/landscape production through glTF.
  • No "improving" exported values in the converter — it emits what the direct route's extraction produced, verbatim.
  • No reliance on assimp preserving extras — extras always read from the glTF JSON directly.

See also: Pipeline Max — Design and Coherency Contract (§11-lm lightmapper architecture, §9 shape corpus tiers), NeL-Specific glTF Extras, Tutorial Tech Tree.

Clone this wiki locally