Skip to content

max2gltf_plan

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

title: Max → glTF/Blend Conversion and Assimp-Route Parity — Plan description: Whole-max-file conversion to glTF (and .blend via Blender import), 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 is a derivative, not a sibling: .blend is not writable externally in practice, so the .blend deliverable is a headless blender --background --python pass that imports our glTF (optionally interpreting nel_* extras into Blender-native constructs) and saves. Build glTF once, get both targets.
  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) stays direct-route (optionally opaque extras blob + tessellated viewing proxy later) out of scope for parity
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.
  3. Animations: sampled tier + nel_* track blob; anim corpus gate on the blob path, tolerance tier on the sampled path.
  4. .lmscene emission from the assimp route — closes lightmapper producer (c) (§11-lm).
  5. Blender post-pass (max2blend): headless import + save, plus optional bpy interpretation of extras; visual spot-checks, not gated.
  6. Later/optional: PatchMesh extras blob + viewing proxy; particle systems and the other assimp_shape.cpp TODO classes as needed.

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