-
Notifications
You must be signed in to change notification settings - Fork 113
max2gltf_plan
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:
-
max2gltf: whole-
.max-file conversion to glTF 2.0 withnel_*extras, as a newpipeline_max_export_gltftool.max2blendDROPPED 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. -
Assimp-route parity: expand
mesh_export/mesh_utilsuntil, for every asset class the direct route exports,max → glTF → assimp route → artifactmatchesmax → direct route → artifactunder the existing corpus comparators.
- 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) (
.lmscenefrom assimp, §11-lm) falls out of parity work for free.
-
Branch at the pre-build model, never re-decode.
pipeline_max_export_gltfshares the same T1 parse andpipeline_max_export_commonextraction as the direct exporters: glTF geometry buffers are written from the sameCMeshBuild-level dataexport_shapeconsumes, tracks from the same decoded dataexport_animconsumes. 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.lmsceneintermediate, in an interoperable container.) -
NeL semantics ride in
extras, per the established convention (NeL-Specific glTF Extras): per-key scalars,%.9gforced-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 --gltf→mesh_utilsexportSkels(), bit-exact TRS vianel_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).
| 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).
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.
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.
-
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). -
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 (ctestpipeline_max_gltf_skins); skinned characters are now poseable in any glTF DCC/viewer. -
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_animcarries the exactCAnimationstream; 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 (ctestpipeline_max_gltf_anim_channels). Non-TRS tracks (materials, morph factors) not sampled yet. -
.lmsceneemission from the assimp route — closes lightmapper producer (c) (§11-lm). -
Blender post-pass (— DROPPED 2026-07-14 (user imports the .gltf directly; visual spot-checks stay a good idea, just not a tool).max2blend) - 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 asnel_zonesblob + tessellatednel_proxyviewing 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). —.lmscenefrom the glTF route DONE 2026-07-15 asasset.extras.nel_lmscene: the shape exporter's whole per-file flow compiled in withPMB_SHAPE_NO_MAINruns in lightmap-scene-only mode (receiver nodes take the full build path, everything else skips), bytes byte-gated against the direct--lm-scenewrite on the singles tier. Every artifact class the .max pipeline produces now rides the container.
-
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_materialconversion — 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--materialsflag. — Loader DONE 2026-07-14 (mesh_utils/material_sidecar.cpp,mesh_export --materials, repeatable with later-overrides-earlier stacking; behaviors gated by ctestpipeline_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).
- 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
-
NeL export properties (the Max appdata) for artist-provided scenes ride a glTF sidecar
too: empty dummy nodes in the sidecar's
nodesarray, matched to the scene's nodes by name, carrying the same per-nodenel_*extras vocabulary the max2gltf writer emits (scene-level properties on the sidecar'sasset.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/CSceneMetasidecar. Design details in NeL-Specific glTF Extras § Node-properties sidecar. This is the last piece the assimp-route.lmsceneproducer (item 4) needs designed: geometry from the artist file, lights fromKHR_lights_punctual+nel_*light extras, per-node lightmap appdata from this sidecar.
- 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.