max2gltf_plan - ryzom/ryzomcore GitHub Wiki


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 --gltf โ†’ mesh_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.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ