nel_gltf_extras - ryzom/ryzomcore GitHub Wiki
title: NeL-Specific glTF Extras (nel_* keys) description: Catalogue of custom keys the NeL toolchain writes into and reads from glTF nodes' "extras" objects โ used to preserve information the standard glTF properties can't carry losslessly (float32 bit-exact TRS, NeL-specific flags) published: true date: 2026-07-05T00:00:00.000Z tags: editor: markdown dateCreated: 2026-07-05T00:00:00.000Z
The NeL pipeline uses glTF 2.0 as a lossless intermediate between authoring tools and the runtime asset formats (.skel, .shape, .anim, โฆ). Standard glTF properties (translation, rotation, scale, matrix) get lossily transformed by import/export layers โ assimp, in particular, combines per-node TRS into a single aiNode::mTransformation on read and requires the consumer to Decompose() it back, introducing precision loss on near-identity rotations and non-uniform scales.
To make the intermediate byte-lossless where it matters, tools that generate glTF for the NeL runtime write duplicate values in extras, and NeL's assimp-based readers prefer the extras when present, falling back to standard glTF properties otherwise. This preserves interop โ Blender, three.js, and every other glTF consumer sees the standard fields and works correctly โ while our roundtrip through the pipeline stays bit-exact.
The extras are scoped: per-node (aiNode::mMetaData after assimp import), per-mesh, etc. Extras on other structures are not currently used; if a future asset class needs them (e.g. per-primitive material flags), add a new section here and note the type mapping.
The extras represent the source truth as the writer saw it, not a suggestion. A reader that respects the extras must not renormalise, quantise, or re-decompose the values behind them โ that's the whole point. If a reader wants to normalise (e.g., a runtime that requires unit quaternions), it should either normalise the standard rotation field or normalise a copy, keeping the extras semantics unchanged for downstream tools.
Aimed at aiNode. Assimp's glTF loader surfaces JSON extras as aiMetadata entries keyed by the JSON property name. Type mapping (assimp 5.x observed):
| JSON value form | aiMetadataType |
Notes |
|---|---|---|
true / false
|
AI_BOOL |
Direct |
123 (integer literal, no . or e) |
AI_UINT64 (or AI_INT32) |
Writer must force decimal notation for floats |
1.5 (with . or e) |
AI_DOUBLE |
9 decimal digits round-trip float32 exactly |
"foo" |
AI_AISTRING |
|
[a, b, c] |
version-dependent | Not used in NeL extras โ separate scalar keys instead |
{ ... } |
AI_AIMETADATA (nested) |
Not used |
Written by pipeline_max_export_skel --gltf โฆ; read by mesh_utils's exportSkels() when the source is a glTF file. If any of the T/R/S triples is incomplete, the reader falls back to decomposing mTransformation โ so an artist-authored glTF without our extras still works.
| Key | Type | Format | Meaning |
|---|---|---|---|
nel_tx, nel_ty, nel_tz
|
float | %.9g with forced decimal | Local translation. Byte-identical to what the writer had before conversion. |
nel_rx, nel_ry, nel_rz, nel_rw
|
float | %.9g with forced decimal | Local rotation quaternion. Component order is XYZW to match glTF standard. Not pre-normalised โ writer must preserve the source's exact bits, including tiny deviations from unit length. |
nel_sx, nel_sy, nel_sz
|
float | %.9g with forced decimal | Local scale (per-axis). |
Rationale for individual scalar keys instead of nel_translation: [x, y, z]: assimp's glTF-to-metadata mapping for JSON arrays has varied across versions. Scalar numbers map cleanly to typed aiMetadataEntry in every 5.x release; arrays are inconsistent. Ten extra keys per node is a small cost compared to a fragile array roundtrip.
Rationale for decimal-forced encoding: assimp's JSON parser picks the smallest integer type when the token has no . or e, so "nel_lodDisableDistance": 0 came through as AI_UINT64. The writer post-processes %.9g output and appends .0 if neither . nor e[E] appears, guaranteeing AI_DOUBLE.
Rationale for %.9g not %.17g: the source values are float32. 9 decimal digits are sufficient to round-trip float32 exactly through a double parser and back; 17 digits (needed for double bit-exactness) would just add noise-looking trailing digits with no information gain.
What about the standard translation/rotation/scale fields? The writer emits both. The standard fields make the glTF viewable in Blender, three.js, glTF-Validator etc. โ where slight precision loss doesn't matter. The extras make our internal roundtrip bit-exact.
| Key | Type | Default when absent | Meaning |
|---|---|---|---|
nel_unheritScale |
bool | false |
NeL bone's UnheritScale flag (see CBoneBase). true means the bone's scale is applied without inheriting from the parent โ matches the Max exporter's read of `INHERIT_SCL_X |
nel_lodDisableDistance |
float | 0.0 |
NeL bone's LodDisableDistance (from NEL3D_APPDATA_BONE_LOD_DISTANCE). Bone is disabled from skeleton evaluation when distance to camera exceeds this. 0 = never disable. |
Standard glTF has no concept of either โ they're NeL-runtime-specific and would be silently lost by a round-trip through a stock glTF pipeline. Preserving them in extras is what makes the mesh_export roundtrip carry a full skeleton.
Consumer code should read extras and prefer them over the standard fields when both are present and the extras form a complete set:
float tx, ty, tz, rx, ry, rz, rw, sx, sy, sz;
bool haveTrans = getNelFloat(node, "nel_tx", tx) & getNelFloat(node, "nel_ty", ty) & getNelFloat(node, "nel_tz", tz);
bool haveRot = getNelFloat(node, "nel_rx", rx) & /* ...ry, rz, rw */;
bool haveScl = /* nel_sx, sy, sz */;
if (haveTrans && haveRot && haveScl) {
// use nel_* extras verbatim
} else {
// fall back to assimp's Decompose(mTransformation)
}All-or-nothing per triple: if a writer emits only two of three translation components, the reader treats the whole triple as missing rather than mixing extras with decomposed values. Cleaner failure mode, avoids partial-noise cases.
mesh_export exposes a --no-nel-extras flag that makes the reader ignore every nel_* per-node key and reconstruct transforms via Decompose(mTransformation) even when the extras are present. This is the same code path an artist-authored glTF without our extras hits, so with the flag we can:
- confirm the fallback is functionally correct (semantic content preserved end-to-end),
- measure the fallback's byte-accuracy floor against our extras-based path,
- catch regressions in the fallback that would otherwise be hidden by the extras always winning.
On the 9-file non-biped skeleton corpus, A vs B parity is:
| with extras (default) | with --no-nel-extras
|
|
|---|---|---|
| A-vs-B match | 100.0% on every file | 90.7% โ 97.2% |
The residual delta with the flag on is float noise in InvBindPos rotation cells from Decompose's row-magnitude renormalisation of the 3ร3 โ same class of noise Max's own arithmetic produces in the reference outputs (T3-epsilon per pipeline_max_design ยง9). Semantic correctness โ bone hierarchy, names, in-engine animation appearance โ is unaffected.
Use this flag in CI as a regression gate on the fallback path: it must always produce a valid .skel, and the byte-parity against the extras-based .skel should stay โฅ90% on the corpus.
The current writer is nel/tools/3d/pipeline_max_export_skel/main.cpp::writeGltf(). The current reader is nel/tools/3d/mesh_utils/assimp_skel.cpp::walkSkelNode(). Both track this spec; a change to either without updating the other is a bug.
When adding a new extras key:
- Update this page (add a row in the appropriate table).
- Update both writer and reader in the same commit if both sides need to change.
- Add a compile-time or CI check that reader knows about all keys writer emits (nice-to-have, not currently enforced).
Under consideration but not implemented:
-
Mesh/shape extras for
nel_material_flags,nel_lightmap_group, etc. Would move NeL-specific material data out ofscene_metasidecar files and into the glTF itself. -
Animation extras for
nel_track_default,nel_loop_modeetc. -
Scene extras for
nel_scene_version,nel_tag_timeโ for the tag-file roundtrip protocol frombuild_gamedata. -
Deprecation path if the standard glTF
KHR_*extensions ever cover our needs (e.g., a glTF extension for non-uniform bone scale semantics). Extras are cheap to add and remove; extensions are conservative.
See also: Pipeline Max Design for the .max-side context, and mesh_utils/scene_meta.h for the JSON sidecar format currently used for non-transform NeL flags.