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