Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 121 additions & 16 deletions docs/api-reference/gltf/gltf-animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ console.log(scenegraphs.animator.getAnimations().map(clip => clip.name));

`parseGLTFAnimations()` owns glTF accessor decoding and pointer interpretation. The returned
channels are converted to shared `AnimationTrack`, `AnimationClip`, and `AnimationMixer` objects
by `GLTFAnimator`.
by `GLTFAnimator`. `scenegraphs.animations` retains the parsed source channels;
`scenegraphs.animator` owns their shared runtime actions; `scenegraphs.skins` owns reusable,
automatically updated source skin palettes.

## `GLTFAnimator`

Expand All @@ -42,7 +44,36 @@ requestAnimationFrame(renderFrame);
```

`GLTFAnimator.setTime(timeMilliseconds)` accepts an **absolute timestamp in milliseconds**,
matching `requestAnimationFrame()`. It evaluates all active clips in one shared mixer pass.
matching `requestAnimationFrame()`. It evaluates all active clips in one shared mixer pass, then
updates dependent skin palettes once.

Applications that already maintain an animation delta can use seconds directly:

```ts
scenegraphs.animator.update(deltaSeconds);
```

Unlike calling the underlying `mixer.update()` manually, `animator.update()` also refreshes the
automatically managed glTF skin bindings after every animation frame.

### Select and crossfade clips

```ts
const animator = scenegraphs.animator;

animator.selectClip('Walk');
console.log(animator.activeClip); // 'Walk'

animator.selectClip('Run', {
crossFadeDuration: 0.35
});

console.log(animator.activeClip); // 'Run'
```

`selectClip()` stops unrelated actions, activates the selected source clip, and optionally
crossfades from the previously active clip. Durations are measured in **seconds**. Unknown clip
names are rejected without changing the active selection.

Individual `GLTFAnimationClip` instances expose their format-independent `clip`, their shared
`mixer`, and their playback `action`:
Expand All @@ -56,9 +87,9 @@ walk.action.crossFadeTo(run.action, 0.35);
```

`AnimationAction` and `AnimationMixer` measure clip time, fade duration, and update deltas in
**seconds**. If an application takes direct control of `animator.mixer.update(deltaSeconds)`, use
that mixer as the animation clock instead of simultaneously advancing the same actions through
`animator.setTime()`.
**seconds**. Do not simultaneously advance the same actions through `animator.setTime()` and
`animator.update()`. If an application directly controls `animator.mixer.update(deltaSeconds)`, it
must also update dependent source skin palettes explicitly with `scenegraphs.skins.update()`.

See the [engine animation guide](/docs/api-guide/engine/animation) and
[AnimationMixer API reference](/docs/api-reference/engine/animation/animation-mixer) for pause,
Expand All @@ -70,17 +101,62 @@ seek, reverse playback, once/repeat/ping-pong loops, weighted blending, and cros
| --- | --- |
| Node `translation`, `rotation`, and `scale` | The corresponding retained `GroupNode` transform. |
| Node `weights` | Node-local mesh morph-target weights and existing GPU vertex buffers. |
| `KHR_node_visibility.visible` | Recursive scenegraph visibility and an in-place punctual-light refresh. |
| Supported material-factor pointers | Shared canonical PBR material uniforms. |
| Supported texture-transform pointers | Per-slot UV offset, rotation, or scale. |
| Supported texture-transform pointers | Per-slot UV offset, rotation, or scale across all 17 map slots. |
| Perspective or orthographic camera pointers | Independent runtime projection definitions. |
| Punctual-light pointers | Authored linear color, intensity, range, and spotlight cone angles. |

`STEP`, `LINEAR`, and `CUBICSPLINE` interpolation are supported. Quaternion rotation tracks use
shortest-path interpolation, and cubic quaternion results are normalized. Morph channels unpack
all source target weights, including cubic spline tangent/value/tangent groups.

`KHR_animation_pointer` supports the node transforms and morph weights above, selected
scalar/vector PBR factors, and `KHR_texture_transform` offset/rotation/scale on all 17 supported
texture slots. Camera pointers, extras, structural material switches such as `alphaMode`,
`doubleSided`, or `unlit`, animated `texCoord`, and `TEXCOORD_2+` are not supported.
### Typed `KHR_animation_pointer` targets

Source pointers preserve their original JSON paths while being represented as typed node,
material, texture-transform, camera, or light channels:

```ts
for (const animation of scenegraphs.animations) {
for (const channel of animation.channels) {
switch (channel.type) {
case 'node':
console.log(channel.targetNodeId, channel.path);
break;
case 'material':
console.log(channel.targetMaterialIndex, channel.property);
break;
case 'textureTransform':
console.log(channel.textureSlot, channel.path);
break;
case 'camera':
console.log(channel.targetCameraIndex, channel.projection, channel.property);
break;
case 'light':
console.log(channel.targetLightIndex, channel.property, channel.component);
break;
}
}
}
```

Supported source pointers include:

- `/nodes/1/extensions/KHR_node_visibility/visible` with `STEP` interpolation.
- `/cameras/0/perspective/yfov` and `/cameras/0/orthographic/xmag`.
- `/extensions/KHR_lights_punctual/lights/0/intensity` and individual `color/0` components.
- `/extensions/KHR_lights_punctual/lights/0/spot/innerConeAngle` and `outerConeAngle`.
- `/materials/0/extensions/KHR_materials_dispersion/dispersion` and supported physical factors.
- `KHR_texture_transform` offset, rotation, and scale across all 17 supported material map slots.

Camera channels update `scenegraphs.cameras`, which contains independent copies of source
projection definitions. Light and visibility channels refresh the existing `scenegraphs.lights`
array in place. Original postprocessed camera and light source data remains unchanged.

Extras, structural material switches such as `alphaMode`, `doubleSided`, or `unlit`, animated
`texCoord`, and `TEXCOORD_2+` are not supported. See
[native glTF extensions](/docs/api-reference/gltf/gltf-native-extensions) for the full target
matrix and strict extension diagnostics.

## Skeletal animation and skinning

Expand All @@ -89,15 +165,44 @@ integer joint weights retain their intended normalized interpretation at the con
The existing shared `skin` shader module in `@luma.gl/shadertools` applies joint palettes; its
current uniform-array capacity is 64 joints.

The module accepts either its existing glTF scenegraph-based inputs or a format-independent
`jointMatrices` palette. The shared
`createScenegraphsFromGLTF()` automatically builds a source-aware `GLTFSkinController`. Each
binding maps one authored mesh node to its source skin, animated joints, optional inverse bind
matrices, reusable mesh-local joint palette, and existing primitive models:

```ts
for (const binding of scenegraphs.skins.bindings) {
console.log({
sourceNode: binding.nodeIndex,
sourceSkin: binding.skinIndex,
jointCount: binding.joints.length,
palette: binding.jointMatrices
});
}

const skinBinding = scenegraphs.skins.getBinding(2);
console.log(skinBinding?.models.length);
```

`animator.setTime()` and `animator.update()` refresh all bindings once after their animation
channels evaluate. Multiple independent source skins, authored inverse-bind transforms, mesh-local
motion, and shared source nodes reuse the existing GPU models and skin shader instead of creating a
parallel skeletal runtime.

If application code changes a joint manually outside the animation controller, refresh the existing
palettes explicitly:

```ts
scenegraphs.skins.update();
```

The shared
[experimental SceneRenderer](/docs/api-reference/experimental/scene-renderer) consumes the
format-independent palette through its surface skin descriptor.

The optional ANARI integration can import source joint attributes and render an explicitly
provided surface joint palette, but its showcase importer does not automatically create or update
that palette from glTF skins. Imported skeletal playback through ANARI therefore still requires
application-provided skin-palette integration.
The optional ANARI glTF integration also maps retained source skin bindings to the same generic
joint-palette helper and updates its palettes after each animation frame. It remains an optional
`@luma.gl/anari/gltf` adapter: the ANARI core does not own a loader, animation mixer, or skinning
shader.

## Morph targets

Expand Down
118 changes: 105 additions & 13 deletions docs/api-reference/gltf/gltf-extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ default loader-to-scenegraph path.
Each extension name links to its official Khronos-managed specification page in
the glTF extension registry repository.

For a complete application walkthrough, including source JSON, public TypeScript APIs, real
instanced draws, material selection, recursive mesh/light visibility, typed camera and light
animation, capability diagnostics, and official sample coverage, see
[native glTF extensions](/docs/api-reference/gltf/gltf-native-extensions).

Status meanings:

- `✅`: works end-to-end in the default luma.gl glTF pipeline.
Expand Down Expand Up @@ -132,8 +137,10 @@ Status meanings:
<SupportRow name="KHR_mesh_quantization" support="✅ *️⃣">
Quantized accessors are unpacked during load before geometry creation.
</SupportRow>
<SupportRow name="EXT_mesh_gpu_instancing" support="❌">
GPU instancing data is not yet converted into luma.gl instanced draw setup.
<SupportRow name="EXT_mesh_gpu_instancing" support="✅">
Accessor-backed translation, rotation, and scale become one real instanced draw
per source primitive on WebGL and WebGPU. Aggregate bounds include every
instance, and application-defined <code>_NAME</code> attributes remain available.
</SupportRow>
<SupportRow name="KHR_lights_punctual" support="✅">
Directional, point, and spot lights preserve authored intensity, range,
Expand Down Expand Up @@ -170,6 +177,7 @@ Status meanings:
<SupportRow name="KHR_materials_dispersion" support="✅ 🚧" fromVersion="9.3">
Authored dispersion is parsed into the canonical PBR material. Shared experimental and ANARI
rendering separate transmitted RGB wavelengths using the ratified wavelength-dependent IOR.
A supported material animation pointer can update the same physical dispersion uniform.
</SupportRow>
<SupportRow name="KHR_materials_volume_scatter" support="❌">
Volume scattering is not implemented in the stock PBR shader.
Expand All @@ -194,9 +202,10 @@ Status meanings:
loaders.gl can preserve the extension data, but <code>@luma.gl/gltf</code> does
not translate it into the default metallic-roughness shader path.
</SupportRow>
<SupportRow name="KHR_materials_variants" support="App">
Variant metadata can be loaded, but applications must choose and apply variants
themselves.
<SupportRow name="KHR_materials_variants" support="✅">
<code>scenegraphs.variants.selectVariant(name)</code> switches authored primitive
materials without replacing scenegraph nodes; <code>resetVariant()</code>
restores source defaults and pipeline state.
</SupportRow>
<SupportRow name="KHR_texture_basisu" support="✅ *️⃣">
BasisU / KTX2 textures are passed through as compressed textures when supported
Expand All @@ -215,9 +224,14 @@ Status meanings:
and authored <code>TEXCOORD_0</code> / <code>TEXCOORD_1</code> selection.
</SupportRow>
<SupportRow name="KHR_animation_pointer" support="✅ 🚧" fromVersion="9.3">
Node TRS and morph-weight pointers, selected material factor pointers, and
animated <code>KHR_texture_transform</code> offset/rotation/scale pointers across
all 17 supported slots are wired to runtime updates. Still unsupported: cameras, extras,
Node TRS, morph-weight, and recursive boolean visibility pointers; selected
material factor pointers; and animated <code>KHR_texture_transform</code>
offset/rotation/scale pointers across all 17 supported slots are wired to
runtime updates. Perspective/orthographic camera properties and typed punctual-light
color, intensity, range, spotlight angles, and physical chromatic-dispersion factors are also
supported. Boolean
visibility uses authored <code>STEP</code> interpolation and refreshes punctual
lights. Still unsupported: extras,
structural material switches such as <code>alphaMode</code> /
<code>doubleSided</code> / <code>unlit</code>, animated
<code>KHR_texture_transform.texCoord</code>, and texture slots that resolve to
Expand All @@ -226,9 +240,10 @@ Status meanings:
the shared experimental renderer and ANARI can refract captured scene color,
while the standalone glTF scenegraph path still uses its alpha approximation.
</SupportRow>
<SupportRow name="KHR_node_visibility" support="❌">
Node-visibility animations and toggles are not mapped onto runtime scenegraph
state.
<SupportRow name="KHR_node_visibility" support="✅">
Source-authored visibility recursively hides mesh descendants and punctual
lights. Visibility-pointer animation updates existing scenegraph nodes and
preserves the identity of the exported light array.
</SupportRow>
<SupportRow name="KHR_xmp" support="❌">
Metadata payloads remain in the loaded glTF, but luma.gl does not interpret
Expand All @@ -252,8 +267,83 @@ Status meanings:
</tbody>
</table>

## Inspect runtime capabilities

Source extension declarations can be inspected before scenegraph creation:

```ts
import {
assertSupportedGLTFExtensions,
createScenegraphsFromGLTF,
getGLTFExtensionSupport,
getUnsupportedRequiredGLTFExtensions
} from '@luma.gl/gltf';

for (const capability of getGLTFExtensionSupport(gltf).values()) {
console.log(capability.extensionName, {
required: capability.required,
supported: capability.supported,
level: capability.supportLevel,
explanation: capability.comment
});
}

const unsupportedRequired = getUnsupportedRequiredGLTFExtensions(gltf);

if (unsupportedRequired.length > 0) {
assertSupportedGLTFExtensions(gltf);
}

const scenegraphs = createScenegraphsFromGLTF(device, gltf, {
strictExtensions: true
});
```

`built-in` and `parsed-and-wired` capabilities satisfy required-extension checks. `loader-only`
capabilities, including browser-dependent WebP and AVIF image decoding, do not promise complete
portable runtime support. Unknown or `none` capabilities also fail when required. Optional
unsupported extensions remain visible in the report without preventing scene creation.

The returned `scenegraphs.extensionSupport` preserves the document-specific capability report.
Strict checks run before model creation, so an unsupported required feature does not leave a
partially constructed GPU scene behind.

## Use authored runtime extensions

```ts
import {getGLTFNodeInstancing} from '@luma.gl/gltf';

scenegraphs.variants.selectVariant('Midnight');
scenegraphs.variants.resetVariant();

const instancing = getGLTFNodeInstancing(gltf, gltf.nodes[0]);
console.log(instancing?.matrices.length);

scenegraphs.animator.selectClip('Night lighting', {
crossFadeDuration: 0.3
});
scenegraphs.animator.setTime(1000);

console.log(scenegraphs.cameras[0]);
console.log(scenegraphs.lights);
```

Variant selection preserves existing scenegraph node/model identities and restores unmapped
primitives to their authored default materials. Instancing submits one GPU draw per source
primitive on both WebGL and WebGPU. Visibility and punctual-light pointers update stable retained
scene objects through the existing shared animation mixer. Automatic source skin palettes and morph
targets are evaluated in the same frame.

## Notes

- `getGLTFExtensionSupport(gltf)` distinguishes optional and required extensions.
Use `getUnsupportedRequiredGLTFExtensions(gltf)`,
`assertSupportedGLTFExtensions(gltf)`, or
`createScenegraphsFromGLTF(device, gltf, {strictExtensions: true})` to reject
unsupported required features instead of silently degrading them.
- `getGLTFNodeInstancing(gltf, node)` exposes resolved source matrices and all
authored instance-accessor semantics, including application-defined `_NAME`
attributes.
- The built-in material extension rows reuse the shared `pbrMaterial` shader.
The experimental `SceneRenderer` and ANARI capture opaque scene color for
screen-space transmission/refraction; the standalone `createScenegraphsFromGLTF()`
Expand All @@ -266,5 +356,7 @@ Status meanings:
- `@luma.gl/gltf` relies on `@loaders.gl/gltf` for low-level extension decoding,
decompression, and glTF post-processing.
- Core glTF skin attributes and animated morph targets are supported independently
of these extension rows. See [glTF animation and deformation](/docs/api-reference/gltf/gltf-animation)
and [glTF materials and textures](/docs/api-reference/gltf/gltf-materials).
of these extension rows; source skin palettes are updated automatically once per animation
frame. See [glTF animation and deformation](/docs/api-reference/gltf/gltf-animation),
[native glTF extensions](/docs/api-reference/gltf/gltf-native-extensions), and
[glTF materials and textures](/docs/api-reference/gltf/gltf-materials).
Loading
Loading