Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
3209f9a
Add dirty flag for all animated properties and check in transform
oss-haertl Oct 24, 2025
53b038b
Add KHR_implicit_shapes
oss-haertl Oct 31, 2025
c13fb18
Add parsing of KHR_physics_rigid_bodies
oss-haertl Oct 31, 2025
df242b4
Add read-only properties
oss-haertl Nov 3, 2025
ffe26b2
Create PhysX actors
oss-haertl Nov 12, 2025
fc24c71
Apply simulation
oss-haertl Nov 19, 2025
3c2da8e
Use physic transform if provided
oss-haertl Nov 19, 2025
19f7fab
First running WIP version
oss-haertl Nov 20, 2025
c16bd55
Add collider debug view
oss-haertl Nov 21, 2025
b3ae6b7
Fix wrong colliders
oss-haertl Nov 25, 2025
cfb99f8
Fix recursive collider function
oss-haertl Nov 26, 2025
55c57c1
WIP non-uniform scaling
oss-haertl Nov 27, 2025
4cbfe22
WIP handle non-uniform scaling for simple shapes
oss-haertl Dec 1, 2025
7ed3fec
Add joint limits
oss-haertl Dec 1, 2025
13d569c
Merge branch 'feature/KHR_interactivity' into feature/Physics
oss-haertl Dec 1, 2025
5d9cdb7
Add joint drives and fix limits
oss-haertl Dec 2, 2025
ad9ec00
Fix angular limit
oss-haertl Dec 3, 2025
335a931
Always set limits
oss-haertl Dec 3, 2025
1aebd5f
Apply velocity to kinematic actors
oss-haertl Dec 3, 2025
b8be89f
Fix bug
oss-haertl Dec 3, 2025
08e0866
Apply custom gravity
oss-haertl Dec 3, 2025
c1097b6
Improve mesh collider detection
oss-haertl Dec 4, 2025
803966f
Calculate skinned collider
oss-haertl Dec 4, 2025
a67d277
Calculate morphed colliders
oss-haertl Dec 4, 2025
7bdc44c
Add scene reset and cleanup
oss-haertl Dec 5, 2025
f9b7b3b
Fix pause
oss-haertl Dec 5, 2025
4ca47ee
Merge branch 'feature/KHR_interactivity' into feature/Physics
oss-haertl Dec 8, 2025
256c933
Handle translation and scale correctly for referenced nodes
oss-haertl Dec 9, 2025
8037ed3
Move scale calculation to own function
oss-haertl Dec 9, 2025
1d38183
Merge branch 'performance/dirtyTransform' into feature/Physics
oss-haertl Dec 10, 2025
27856d1
Adjust dirty flag
oss-haertl Dec 10, 2025
c1cddb1
Add drityScale
oss-haertl Dec 10, 2025
6fa9063
Move functions to utils
oss-haertl Dec 10, 2025
8b9f1be
Fix function name
oss-haertl Dec 10, 2025
e579056
Refactor function
oss-haertl Dec 10, 2025
518f098
WIP animate colliders
oss-haertl Dec 10, 2025
5093680
Animated colliders mostly done
oss-haertl Dec 11, 2025
af01779
Simplify collider functions
oss-haertl Dec 12, 2025
e419eb9
WIP animate materials and motions
oss-haertl Dec 12, 2025
d2410d1
Fix some update issues and deactivate animations for now
oss-haertl Dec 17, 2025
e9790f1
Correctly apply inertiaOrientation
oss-haertl Dec 17, 2025
d38b3f3
Fix combine mode
oss-haertl Dec 18, 2025
d63c309
Fix collision filters
oss-haertl Dec 18, 2025
4851700
Add simple debug views
oss-haertl Dec 18, 2025
a34596d
Fix geometry casting
oss-haertl Jan 15, 2026
d9e9830
Update motion velocities
oss-haertl Jan 15, 2026
0ea5114
Animate actor transforms
oss-haertl Jan 15, 2026
c47f9ae
Fix typo
oss-haertl Jan 16, 2026
2b97275
Reset all dirty flags
oss-haertl Jan 16, 2026
61b37be
Change geometry.node to geometry.mesh
oss-haertl Jan 22, 2026
1484453
Add empty functions for joint animations
oss-haertl Jan 22, 2026
a879334
Add ray cast node
oss-haertl Jan 27, 2026
b1dfb72
WIP add triggers
oss-haertl Jan 27, 2026
56245e4
Create trigger actors and shapes
oss-haertl Jan 29, 2026
df3d930
Add implementation for applyImpulse
oss-haertl Jan 29, 2026
dcd09b1
WIP fix trigger callback
oss-haertl Jan 30, 2026
2d8b7fb
WIP add composite triggers
oss-haertl Feb 4, 2026
28a1688
Fix compound triggers
oss-haertl Feb 5, 2026
e7be5a2
use ref counting for composed triggers
oss-haertl Feb 6, 2026
aa4194a
Handle triangle strips and triangle fans
oss-haertl Feb 10, 2026
f3b05fe
Update physx
oss-haertl Feb 10, 2026
040bfa5
Resolve remaining TODOs
oss-haertl Feb 10, 2026
2f265af
Fix cleanup
oss-haertl Feb 10, 2026
4d789a1
Enable CCD
oss-haertl Feb 10, 2026
12879d6
Fix kinematic actors with velocity
oss-haertl Feb 11, 2026
e419896
Fix joint space calculation
oss-haertl Feb 11, 2026
4aa5ba6
Fix custom gravity
oss-haertl Feb 11, 2026
1bd3bb3
Add physX substepping
oss-haertl Feb 12, 2026
8e5fc57
Improve error handling for inertia
oss-haertl Feb 12, 2026
1d93811
Handle velocities for animated kinematic flag
oss-haertl Feb 13, 2026
2e0b230
Fix warning and substepping
oss-haertl Feb 13, 2026
c81d0e5
Move reset to gltf
oss-haertl Feb 13, 2026
947d4bf
Add undefined check
oss-haertl Feb 13, 2026
8ff3c06
Fix setGlobalPose
oss-haertl Feb 13, 2026
c0b89be
Do not use physics transform for animated dynamic bodies
oss-haertl Feb 13, 2026
087c636
Attach triggers to already existing actors
oss-haertl Feb 13, 2026
a17afd7
Fix interactivity nodes
oss-haertl Feb 13, 2026
6c0125c
Fix box collider update
oss-haertl Feb 16, 2026
5c5773e
Remove unneeded code
oss-haertl Feb 17, 2026
94aedbf
Make simulation more stable
oss-haertl Feb 17, 2026
95490ad
Handle dirty flag reset for stepping
oss-haertl Feb 23, 2026
9345d7d
Fix scale update
oss-haertl Feb 24, 2026
dd32b5b
Fix colliding triggers
UX3D-labode Feb 24, 2026
48ec788
Fix parameters
oss-haertl Feb 24, 2026
ed995c3
Fix child colliders broken rotation when scaled
UX3D-labode Feb 25, 2026
1697e47
Disable debug by default
oss-haertl Feb 25, 2026
db48fec
Fix kinematic velocities
oss-haertl Feb 27, 2026
4ec4428
Remove TODOs
oss-haertl Feb 27, 2026
0f64c76
Fix worldquaternion not resetting
oss-haertl Mar 17, 2026
4c614e9
Implement joints after WASM update
oss-haertl Mar 20, 2026
42397ee
Create multiple simplifiedJoints per Joint
oss-haertl Mar 24, 2026
b73cba3
Handle animated joints
oss-haertl Mar 25, 2026
ae5c9b6
Restructure files
oss-haertl Mar 25, 2026
e06c1b3
Restructure/Comments
oss-haertl Mar 25, 2026
af0979a
Add more documentation
oss-haertl Mar 25, 2026
1f254cf
Update documentation
oss-haertl Mar 26, 2026
d0c8819
Add support for animated joint spaces
oss-haertl Mar 27, 2026
f0c3b50
Fix connected node
oss-haertl May 12, 2026
386c9ed
fixes #639
jim-ec May 27, 2026
69cdb6e
Revert "fixes #639"
jim-ec Jun 2, 2026
521ba21
compute normal matrix in renderer
jim-ec Jun 2, 2026
09e8a9a
remove normal matrix class member from node
jim-ec Jun 2, 2026
2d067d0
Merge pull request #46 from KhronosGroup/fix/physics-material-bug
oss-haertl Jun 8, 2026
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
155 changes: 155 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ that are then used to display the loaded data with GltfView</p>
<dt><a href="#GraphController">GraphController</a></dt>
<dd><p>A controller for managing KHR_interactivity graphs in a glTF scene.</p>
</dd>
<dt><a href="#PhysicsController">PhysicsController</a></dt>
<dd><p>Controller for managing the physics simulation of a glTF scene.</p>
</dd>
</dl>

<a name="GltfView"></a>
Expand Down Expand Up @@ -1361,3 +1364,155 @@ Khronos test assets use test/onStart, test/onFail and test/onSuccess.
Clears all custom event listeners from the decorator.

**Kind**: instance method of [<code>GraphController</code>](#GraphController)
<a name="PhysicsController"></a>

## PhysicsController
Controller for managing the physics simulation of a glTF scene.

**Kind**: global class

* [PhysicsController](#PhysicsController)
* [.initializeEngine(engine)](#PhysicsController+initializeEngine)
* [.loadScene(state, sceneIndex)](#PhysicsController+loadScene)
* [.resetScene(gltf)](#PhysicsController+resetScene)
* [.resumeSimulation()](#PhysicsController+resumeSimulation)
* [.pauseSimulation()](#PhysicsController+pauseSimulation)
* [.simulateStep(state, deltaTime)](#PhysicsController+simulateStep)
* [.enableDebugColliders(enable)](#PhysicsController+enableDebugColliders)
* [.enableDebugJoints(enable)](#PhysicsController+enableDebugJoints)
* [.applyImpulse(nodeIndex, linearImpulse, angularImpulse)](#PhysicsController+applyImpulse)
* [.applyPointImpulse(nodeIndex, impulse, position)](#PhysicsController+applyPointImpulse)
* [.rayCast(rayStart, rayEnd)](#PhysicsController+rayCast) ⇒ <code>Object</code>

<a name="PhysicsController+initializeEngine"></a>

### physicsController.initializeEngine(engine)
Initializes the physics engine. This must be called before loading any scenes.
Currently, only "NvidiaPhysX" is supported.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| engine | <code>string</code> |

<a name="PhysicsController+loadScene"></a>

### physicsController.loadScene(state, sceneIndex)
Resets the current physics state and loads the physics data for a given scene and initializes the physics simulation.
The first two frames of the simulation are skipped to allow the physics engine to initialize before applying any physics updates.
Resets all dirty flags.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| state | [<code>GltfState</code>](#GltfState) |
| sceneIndex | <code>number</code> |

<a name="PhysicsController+resetScene"></a>

### physicsController.resetScene(gltf)
Resets the current physics state.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| gltf | <code>glTF</code> |

<a name="PhysicsController+resumeSimulation"></a>

### physicsController.resumeSimulation()
Resumes the physics simulation if it was paused. If the simulation is not paused, this function does nothing.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)
<a name="PhysicsController+pauseSimulation"></a>

### physicsController.pauseSimulation()
Pauses the physics simulation. If the simulation is already paused, this function does nothing.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)
<a name="PhysicsController+simulateStep"></a>

### physicsController.simulateStep(state, deltaTime)
Simulates a single step of the physics simulation,
if the initial loading is done.
A step will only be simulated if enough time has passed since the last simulated step,
based on the configured simulation step time.
Can also be used to manually advance the simulation when it is paused.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| state | [<code>GltfState</code>](#GltfState) |
| deltaTime | <code>number</code> |

<a name="PhysicsController+enableDebugColliders"></a>

### physicsController.enableDebugColliders(enable)
Enable debug visualization of physics colliders.
The exact visualization depends on the physics engine implementation.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| enable | <code>boolean</code> |

<a name="PhysicsController+enableDebugJoints"></a>

### physicsController.enableDebugJoints(enable)
Enable debug visualization of physics joints.
The exact visualization depends on the physics engine implementation.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type |
| --- | --- |
| enable | <code>boolean</code> |

<a name="PhysicsController+applyImpulse"></a>

### physicsController.applyImpulse(nodeIndex, linearImpulse, angularImpulse)
Applies a linear and/or angular impulse to the actor associated with the given node.
An impulse causes an instantaneous change in the actor's velocity proportional to its mass.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type | Description |
| --- | --- | --- |
| nodeIndex | <code>number</code> | glTF node index of the target dynamic actor. |
| linearImpulse | <code>vec3</code> | Impulse vector applied to the center of mass, in world space (kg⋅m/s). |
| angularImpulse | <code>vec3</code> | Angular impulse vector applied around the center of mass, in world space (kg⋅m²/s). |

<a name="PhysicsController+applyPointImpulse"></a>

### physicsController.applyPointImpulse(nodeIndex, impulse, position)
Applies a linear impulse to the actor associated with the given node at a specific world-space position.
Applying the impulse off-center will also induce a torque on the actor.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)

| Param | Type | Description |
| --- | --- | --- |
| nodeIndex | <code>number</code> | glTF node index of the target dynamic actor. |
| impulse | <code>vec3</code> | Impulse vector to apply, in world space (kg⋅m/s). |
| position | <code>vec3</code> | World-space position at which the impulse is applied. |

<a name="PhysicsController+rayCast"></a>

### physicsController.rayCast(rayStart, rayEnd) ⇒ <code>Object</code>
Performs a ray-cast between two world-space points and returns information
about the first shape hit.

**Kind**: instance method of [<code>PhysicsController</code>](#PhysicsController)
**Returns**: <code>Object</code> - An object containing the index of the hit node (`-1` on miss), the normalised
hit fraction along the ray, and the surface normal at the hit point.

| Param | Type | Description |
| --- | --- | --- |
| rayStart | <code>Array.&lt;number&gt;</code> | World-space ray origin as `[x, y, z]`. |
| rayEnd | <code>Array.&lt;number&gt;</code> | World-space ray terminus as `[x, y, z]`. |

36 changes: 36 additions & 0 deletions PhysicsEngines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Supported Physics Engines and Limitations

Currently, only NVIDIA PhysX is supported.

## NVIDIA PhysX

The PhysX engine is loaded as a WebAssembly module via [physx-js-webidl](https://github.com/fabmax/physx-js-webidl).

### Limitations

#### Mesh Colliders

PhysX does not support triangle meshes as collision shapes for dynamic (non-kinematic) actors and triggers. When building a dynamic actor or trigger that references a mesh collider without the `convexHull` flag, the mesh is cooked as a convex hull anyway.

#### Shape Approximations (`KHR_implicit_shapes`)

PhysX does not have native types for every shape defined in the glTF spec. The following shapes are approximated:

- **Cylinder** — PhysX has no native cylinder type. Cylinders are always represented as convex meshes built from generated vertex data.
- **Capsule** — PhysX's native capsule type is limited to equal top/bottom radii and requires X-axis alignment. All glTF capsules are represented as convex meshes to support arbitrary radii and scaling.
- **Sphere with non-uniform scale** — Falls back to a convex mesh approximation. A uniform-scale sphere uses the native `PxSphereGeometry`.
- **Box with non-uniform scale and a non-identity scale-axis quaternion** — Falls back to a convex mesh approximation. Uniformly or simply scaled boxes use the native `PxBoxGeometry`.

#### Joint Simplification

`KHR_physics_rigid_bodies` allows arbitrary per-axis combinations of linear and angular limits and drives. PhysX only exposes D6 joints with a fixed twist/swing constraint model. The implementation resolves this mismatch by:

1. Decomposing a glTF joint into one or more `simplifiedPhysicsJoint` objects, splitting whenever multiple limits affect the same axis.
2. Remapping glTF axis indices to PhysX's expected twist axis (X) via a computed local rotation quaternion.
3. Mapping cone/ellipse limits (`angularAxes` spanning multiple axes) to PhysX swing limits.

This simplification is a best-effort approximation. Complex joint definitions might result in unexpected behavior such as shaking, jittering and instability.

#### Collision Filters

The filter system uses a 32-bit bitmask internally. A maximum of **31 user-defined** collision filters are supported per scene. If a glTF asset defines more than 31 collision filters, filters beyond the limit are ignored and a warning is emitted.
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Try out the [glTF Sample Viewer](https://github.khronos.org/glTF-Sample-Viewer-R
- [GltfState](#gltfstate)
- [GraphController](#graphcontroller)
- [AnimationTimer](#animationtimer)
- [PhysicsController](#physicscontroller)
- [Dirty flags](#dirty-flags)
- [`AnimatableProperty` dirty flags](#animatableproperty-dirty-flags)
- [Node transform dirty flags](#node-transform-dirty-flags)
- [Resetting dirty flags](#resetting-dirty-flags)
- [ResourceLoader](#resourceloader)
- [Render Fidelity Tools](#render-fidelity-tools)
- [Development](#development)
Expand All @@ -40,6 +45,7 @@ For KHR_interactivity, the behavior engine of the [glTF-InteractivityGraph-Autho
- [ ] Skins not supported since WebGL2 only supports 32 bit
- [x] [KHR_animation_pointer](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer)
- [x] [KHR_draco_mesh_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression)
- [x] [KHR_implicit_shapes](https://github.com/eoineoineoin/glTF_Physics/blob/master/extensions/2.0/Khronos/KHR_implicit_shapes/README.md)
- [x] [KHR_interactivity](https://github.com/KhronosGroup/glTF/pull/2293)
- [x] [KHR_lights_punctual](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_lights_punctual)
- [x] [KHR_materials_anisotropy](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_anisotropy)
Expand All @@ -63,6 +69,9 @@ For KHR_interactivity, the behavior engine of the [glTF-InteractivityGraph-Autho
- [x] [KHR_node_hoverability](https://github.com/KhronosGroup/glTF/pull/2426)
- [x] [KHR_node_selectability](https://github.com/KhronosGroup/glTF/pull/2422)
- [x] [KHR_node_visibility](https://github.com/KhronosGroup/glTF/pull/2410)
- [x] [KHR_physics_rigid_bodies](https://github.com/eoineoineoin/glTF_Physics/blob/master/extensions/2.0/Khronos/KHR_physics_rigid_bodies/README.md)\
Supported Engines:
- NVIDIA PhysX ([limitations](PhysicsEngines.md))
- [x] [KHR_texture_basisu](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu)
- [x] [KHR_texture_transform](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform)
- [x] [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld)
Expand Down Expand Up @@ -95,6 +104,14 @@ const update = () => {
window.requestAnimationFrame(update);
```

The GltfView handles the order of execution for animations, interactivity and physics:
1. Animations are applied
2. Any playing interactivity graph is executed
3. The transform hierarchy is computed
4. Any playing physics engine is updated and applied
5. The scene is rendered
6. If physics is used, all dirty flags are reset

### GltfState

The GltfState encapsulates the state of the content of a GltfView. *As currently some WebGL resources are stored directly in the Gltf objects, the state cannot be shared between views.*
Expand Down Expand Up @@ -123,6 +140,83 @@ To make sure that `KHR_interactivity` always behaves correctly together with `KH
The GltfState contains an instance of the AnimationTimer, which is used to play, pause and reset animations. It needs to be started to enable animations.
The `KHR_interactivity` extension controls animations if present. Therefore, the GraphController uses the time of the AnimationTimer to control animations. The GraphController is paused and resumed independently from the AnimationTimer, thus if an interactivity graph is paused, currently playing animations will continue playing if the AnimationTimer is not paused as well.

#### PhysicsController

The GltfState contains an instance of the `PhysicsController`, which manages rigid-body physics simulation for glTF scenes that use the `KHR_physics_rigid_bodies` and `KHR_implicit_shapes` extensions. The controller is available on the state as `state.physicsController`.

Before loading any scene, the physics engine must be initialized. Currently only `"NvidiaPhysX"` is supported:

```js
await state.physicsController.initializeEngine("NvidiaPhysX");
```

After a glTF has been loaded, call `loadScene` to build the physics actors for the active scene:

```js
state.physicsController.loadScene(state, state.sceneIndex);
```

The simulation is advanced by calling `simulateStep` each frame inside `GltfView`. The controller uses a fixed-step accumulator and only advances the simulation when enough time (`simulationStepTime`, default `1/60` s) has elapsed.

Playback can be paused and resumed independently of other state:

```js
state.physicsController.pauseSimulation();
state.physicsController.resumeSimulation();
```

The `playing` property reflects whether the simulation is currently running, and `enabled` indicates whether the loaded scene contains any physics data.

Debug visualization of colliders and joints can be toggled at runtime:

```js
state.physicsController.enableDebugColliders(true);
state.physicsController.enableDebugJoints(true);
```

The following runtime physics operations are available, and are also called internally by the `KHR_interactivity` engine:

```js
// Apply a linear and/or angular impulse to a node
state.physicsController.applyImpulse(nodeIndex, linearImpulse, angularImpulse);

// Apply an impulse at a specific world-space position on a node
state.physicsController.applyPointImpulse(nodeIndex, impulse, position);

// Cast a ray and return the first hit
const hit = state.physicsController.rayCast(rayStart, rayEnd);
```

#### Dirty flags

Dirty flags are per-property boolean markers used to propagate change information through the scene graph without re-evaluating the entire hierarchy every frame. They are the primary mechanism by which animations, `KHR_interactivity`, and the physics simulation communicate what has changed.

##### `AnimatableProperty` dirty flags

Every animatable property defined by the glTF Object Model (e.g. `translation`, `rotation`, `scale`, `mass`, `linearVelocity`) is backed by an `AnimatableProperty` instance that carries a `dirty` boolean. A property is marked dirty whenever its value is written — either by the animation system or by the interactivity graph. It does not matter, if the written value is the same as the old value, since one can animate an e.g. a node transform to stay still. If the dirty flag would not be set in this case, the physics engine might apply e.g. gravitational forces to the an animated body.

##### Node transform dirty flags

Each `gltfNode` carries two additional boolean flags that are computed during `scene.applyTransformHierarchy()`:

| Flag | Set when |
|---|---|
| `node.dirtyTransform` | This node's local transform or any ancestor's transform changed since the last reset. |
| `node.dirtyScale` | This node's scale or any ancestor's scale changed since the last reset. |

##### Resetting dirty flags

`glTF.resetAllDirtyFlags()` performs a full reset in one call.

`GltfView.renderFrame()` calls this automatically at the end of each frame — but **only** when the physics simulation is either disabled or actively playing. When the simulation is paused, dirty flags are intentionally **not** cleared so that any changes made while paused (e.g. via `KHR_interactivity`) are still visible to the physics engine once playback resumes.

If you manually advance the simulation (e.g. via a single-step button) you must reset dirty flags yourself afterwards:

```js
state.physicsController.simulateStep(state, 1 / 60);
state.gltf.resetAllDirtyFlags();
```

### ResourceLoader

The ResourceLoader can be used to load external resources and make them available to the renderer.
Expand Down
Loading