From 3209f9aa62f171fb9afe3fdf53c5cf01be66cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 24 Oct 2025 15:33:19 +0200 Subject: [PATCH 01/93] Add dirty flag for all animated properties and check in transform --- source/gltf/animatable_property.js | 29 ++++++++++++++++++++++- source/gltf/node.js | 15 ++++++++++++ source/gltf/scene.js | 38 ++++++++++++++---------------- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/source/gltf/animatable_property.js b/source/gltf/animatable_property.js index 0bed6d45..93bcd43e 100644 --- a/source/gltf/animatable_property.js +++ b/source/gltf/animatable_property.js @@ -2,18 +2,28 @@ class AnimatableProperty { constructor(value) { this.restValue = value; this.animatedValue = null; + this.dirty = true; } restAt(value) { + if (!this.dirty && !this._equals(value, this.restValue)) { + this.dirty = true; + } this.restValue = value; } animate(value) { + if (!this.dirty && !this._equals(value, this.animatedValue)) { + this.dirty = true; + } this.animatedValue = value; } rest() { - this.animatedValue = null; + if (this.animatedValue !== null) { + this.dirty = true; + this.animatedValue = null; + } } value() { @@ -23,6 +33,23 @@ class AnimatableProperty { isDefined() { return this.restValue !== undefined; } + + _equals(first, second) { + if (typeof first !== typeof second) { + return false; + } + // We do not have animatable objects and arrays always have the same length + if (Array.isArray(first) && Array.isArray(second)) { + for (let i = 0; i < first.length; i++) { + if (!this._equals(first[i], second[i])) { + return false; + } + } + return true; + } else { + return first === second; + } + } } const makeAnimatable = (object, json, properties) => { diff --git a/source/gltf/node.js b/source/gltf/node.js index 7d984300..57051ee2 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -157,6 +157,21 @@ class gltfNode extends GltfObject { this.scale ); } + + isTransformDirty() { + for (const prop of ["rotation", "scale", "translation"]) { + if (this.animatedPropertyObjects[prop].dirty) { + return true; + } + } + return false; + } + + clearTransformDirty() { + for (const prop of ["rotation", "scale", "translation"]) { + this.animatedPropertyObjects[prop].dirty = false; + } + } } class KHR_node_visibility extends GltfObject { diff --git a/source/gltf/scene.js b/source/gltf/scene.js index 4966be10..f9fc4c41 100644 --- a/source/gltf/scene.js +++ b/source/gltf/scene.js @@ -15,12 +15,17 @@ class gltfScene extends GltfObject { } applyTransformHierarchy(gltf, rootTransform = mat4.create()) { - function applyTransform(gltf, node, parentTransform) { - mat4.multiply(node.worldTransform, parentTransform, node.getLocalTransform()); - mat4.invert(node.inverseWorldTransform, node.worldTransform); - mat4.transpose(node.normalMatrix, node.inverseWorldTransform); + function applyTransform(gltf, node, parentTransform, parentRotation, parentDirty) { + const nodeDirty = parentDirty || node.isTransformDirty(); + if (nodeDirty) { + mat4.multiply(node.worldTransform, parentTransform, node.getLocalTransform()); + mat4.invert(node.inverseWorldTransform, node.worldTransform); + mat4.transpose(node.normalMatrix, node.inverseWorldTransform); + quat.multiply(node.worldQuaternion, parentRotation, node.rotation); + node.clearTransformDirty(); + } - if (node.instanceMatrices) { + if (nodeDirty && node.instanceMatrices) { node.instanceWorldTransforms = []; for (let i = 0; i < node.instanceMatrices.length; i++) { const instanceTransform = node.instanceMatrices[i]; @@ -31,24 +36,17 @@ class gltfScene extends GltfObject { } for (const child of node.children) { - applyTransform(gltf, gltf.nodes[child], node.worldTransform); + applyTransform( + gltf, + gltf.nodes[child], + node.worldTransform, + node.worldQuaternion, + nodeDirty + ); } } for (const node of this.nodes) { - applyTransform(gltf, gltf.nodes[node], rootTransform); - } - - function applyWorldRotation(gltf, node, parentRotation) { - quat.multiply(node.worldQuaternion, parentRotation, node.rotation); - - // Recurse into children - for (const child of node.children) { - applyWorldRotation(gltf, gltf.nodes[child], node.worldQuaternion); - } - } - - for (const node of this.nodes) { - applyWorldRotation(gltf, gltf.nodes[node], quat.create()); + applyTransform(gltf, gltf.nodes[node], rootTransform, quat.create(), false); } } From 53b038b52f698a0cdf37626936b6d6f1965ffbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 31 Oct 2025 12:48:04 +0100 Subject: [PATCH 02/93] Add KHR_implicit_shapes --- README.md | 1 + source/gltf/gltf.js | 10 ++++ source/gltf/implicit_shape.js | 95 +++++++++++++++++++++++++++++++++++ source/gltf/interactivity.js | 14 ++++++ 4 files changed, 120 insertions(+) create mode 100644 source/gltf/implicit_shape.js diff --git a/README.md b/README.md index c6c6c457..66cc8e96 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,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) diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index dee7008b..1f7572fc 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -4,6 +4,7 @@ import { gltfBufferView } from "./buffer_view.js"; import { gltfCamera } from "./camera.js"; import { gltfImage } from "./image.js"; import { gltfLight } from "./light.js"; +import { gltfImplicitShape } from "./implicit_shape.js"; import { gltfMaterial } from "./material.js"; import { gltfMesh } from "./mesh.js"; import { gltfNode } from "./node.js"; @@ -22,6 +23,7 @@ const allowedExtensions = [ "KHR_accessor_float64", "KHR_animation_pointer", "KHR_draco_mesh_compression", + "KHR_implicit_shapes", "KHR_interactivity", "KHR_lights_image_based", "KHR_lights_punctual", @@ -141,6 +143,14 @@ class glTF extends GltfObject { this.extensions.KHR_interactivity.graph = json.extensions.KHR_interactivity?.graph ?? 0; } + if (json.extensions?.KHR_implicit_shapes !== undefined) { + this.extensions.KHR_implicit_shapes = new GltfObject([]); + this.extensions.KHR_implicit_shapes.shapes = objectsFromJsons( + json.extensions.KHR_implicit_shapes.shapes, + gltfImplicitShape + ); + } + this.materials.push(gltfMaterial.createDefault()); this.samplers.push(gltfSampler.createDefault()); diff --git a/source/gltf/implicit_shape.js b/source/gltf/implicit_shape.js new file mode 100644 index 00000000..104f6185 --- /dev/null +++ b/source/gltf/implicit_shape.js @@ -0,0 +1,95 @@ +import { GltfObject } from "./gltf_object"; + +class gltfImplicitShape extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.name = undefined; + this.type = undefined; + this.plane = undefined; + this.box = undefined; + this.capsule = undefined; + this.cylinder = undefined; + this.sphere = undefined; + } + + fromJson(json) { + super.fromJson(json); + + if (json.plane !== undefined) { + this.plane = new gltfShapePlane(); + this.plane.fromJson(json.plane); + } + if (json.box !== undefined) { + this.box = new gltfShapeBox(); + this.box.fromJson(json.box); + } + if (json.capsule !== undefined) { + this.capsule = new gltfShapeCapsule(); + this.capsule.fromJson(json.capsule); + } + if (json.cylinder !== undefined) { + this.cylinder = new gltfShapeCylinder(); + this.cylinder.fromJson(json.cylinder); + } + if (json.sphere !== undefined) { + this.sphere = new gltfShapeSphere(); + this.sphere.fromJson(json.sphere); + } + } +} + +class gltfShapeBox extends GltfObject { + static animatedProperties = ["size"]; + constructor() { + super(); + this.size = [1, 1, 1]; + } +} + +class gltfShapeCapsule extends GltfObject { + static animatedProperties = ["radiusBottom", "height", "radiusTop"]; + constructor() { + super(); + this.radiusBottom = 0.25; + this.height = 0.5; + this.radiusTop = 0.25; + } +} + +class gltfShapeCylinder extends GltfObject { + static animatedProperties = ["radiusBottom", "height", "radiusTop"]; + constructor() { + super(); + this.radiusBottom = 0.25; + this.height = 0.5; + this.radiusTop = 0.25; + } +} + +class gltfShapePlane extends GltfObject { + static animatedProperties = ["doubleSided", "sizeX", "sizeZ"]; + constructor() { + super(); + this.doubleSided = false; + this.sizeX = undefined; + this.sizeZ = undefined; + } +} + +class gltfShapeSphere extends GltfObject { + static animatedProperties = ["radius"]; + constructor() { + super(); + this.radius = 0.5; + } +} + +export { + gltfImplicitShape, + gltfShapeBox, + gltfShapeCapsule, + gltfShapeCylinder, + gltfShapePlane, + gltfShapeSphere +}; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index a56db75a..c85bffdb 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -581,6 +581,20 @@ class SampleViewerDecorator extends interactivity.ADecorator { true ); + this.registerJsonPointer( + `/extensions/KHR_implicit_shapes/shapes.length`, + (_path) => { + const shapes = this.world.gltf.extensions?.KHR_implicit_shapes?.shapes; + if (shapes === undefined) { + return [0]; + } + return [shapes.length]; + }, + (_path, _value) => {}, + "int", + true + ); + this.registerJsonPointer( `/materials.length`, (_path) => { From c13fb180a686f90ea9ccedae37fc856f837d7d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 31 Oct 2025 14:59:08 +0100 Subject: [PATCH 03/93] Add parsing of KHR_physics_rigid_bodies --- source/gltf/gltf.js | 8 +++ source/gltf/node.js | 108 ++++++++++++++++++++++++++++++++++++ source/gltf/rigid_bodies.js | 98 ++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 source/gltf/rigid_bodies.js diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index 1f7572fc..bf9307b8 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -18,6 +18,7 @@ import { gltfAnimation } from "./animation.js"; import { gltfSkin } from "./skin.js"; import { gltfVariant } from "./variant.js"; import { gltfGraph } from "./interactivity.js"; +import { KHR_physics_rigid_bodies } from "./rigid_bodies.js"; const allowedExtensions = [ "KHR_accessor_float64", @@ -151,6 +152,13 @@ class glTF extends GltfObject { ); } + if (json.extensions?.KHR_physics_rigid_bodies !== undefined) { + this.extensions.KHR_physics_rigid_bodies = new KHR_physics_rigid_bodies(); + this.extensions.KHR_physics_rigid_bodies.fromJson( + json.extensions.KHR_physics_rigid_bodies + ); + } + this.materials.push(gltfMaterial.createDefault()); this.samplers.push(gltfSampler.createDefault()); diff --git a/source/gltf/node.js b/source/gltf/node.js index 7d984300..c7adb7a9 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -121,6 +121,12 @@ class gltfNode extends GltfObject { jsonNode.extensions.KHR_node_hoverability ); } + if (jsonNode.extensions?.KHR_physics_rigid_bodies !== undefined) { + this.extensions.KHR_physics_rigid_bodies = new KHR_physics_rigid_bodies_node(); + this.extensions.KHR_physics_rigid_bodies.fromJson( + jsonNode.extensions.KHR_physics_rigid_bodies + ); + } } getWeights(gltf) { @@ -183,4 +189,106 @@ class KHR_node_hoverability extends GltfObject { } } +class KHR_physics_rigid_bodies_node extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.motion = undefined; + this.collider = undefined; + this.trigger = undefined; + this.joint = undefined; + } + fromJson(json) { + super.fromJson(json); + + if (json.motion !== undefined) { + this.motion = new KHR_physics_rigid_bodies_motion(); + this.motion.fromJson(json.motion); + } + if (json.collider !== undefined) { + this.collider = new KHR_physics_rigid_bodies_collider(); + this.collider.fromJson(json.collider); + } + if (json.trigger !== undefined) { + this.trigger = new KHR_physics_rigid_bodies_trigger(); + this.trigger.fromJson(json.trigger); + } + if (json.joint !== undefined) { + this.joint = new KHR_physics_rigid_bodies_joint(); + this.joint.fromJson(json.joint); + } + } +} + +class KHR_physics_rigid_bodies_trigger extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.geometry = undefined; + this.nodes = []; + this.collisionFilter = undefined; + } +} + +class KHR_physics_rigid_bodies_collider extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.geometry = undefined; + this.physicsMaterial = undefined; + this.collisionFilter = undefined; + } + fromJson(json) { + super.fromJson(json); + if (json.geometry !== undefined) { + this.geometry = new KHR_physics_rigid_bodies_geometry(); + this.geometry.fromJson(json.geometry); + } + } +} + +class KHR_physics_rigid_bodies_geometry extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.convexHull = false; + this.shape = undefined; + this.node = undefined; + } +} + +class KHR_physics_rigid_bodies_motion extends GltfObject { + static animatedProperties = [ + "isKinematic", + "mass", + "centerOfMass", + "inertialDiagonal", + "inertialOrientation", + "linearVelocity", + "angularVelocity", + "gravityFactor" + ]; + constructor() { + super(); + this.isKinematic = false; + this.mass = undefined; + this.centerOfMass = undefined; + this.inertialDiagonal = undefined; + this.inertialOrientation = undefined; + this.linearVelocity = [0, 0, 0]; + this.angularVelocity = [0, 0, 0]; + this.gravityFactor = 1; + } +} + +class KHR_physics_rigid_bodies_joint extends GltfObject { + static animatedProperties = ["enableCollision"]; + constructor() { + super(); + this.connectedNode = undefined; + this.joint = undefined; + this.enableCollision = false; + } +} + export { gltfNode }; diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js new file mode 100644 index 00000000..891f60eb --- /dev/null +++ b/source/gltf/rigid_bodies.js @@ -0,0 +1,98 @@ +import { GltfObject } from "./gltf_object"; +import { objectsFromJsons } from "./utils"; + +class KHR_physics_rigid_bodies extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.physicsMaterials = []; + this.collisionFilters = []; + this.physicsJoints = []; + } + fromJson(json) { + super.fromJson(json); + this.physicsMaterials = objectsFromJsons(json.physicsMaterials, gltfPhysicsMaterial); + this.collisionFilters = objectsFromJsons(json.collisionFilters, gltfCollisionFilter); + this.physicsJoints = objectsFromJsons(json.physicsJoints, gltfPhysicsJoint); + } +} + +class gltfPhysicsMaterial extends GltfObject { + static animatedProperties = ["staticFriction", "dynamicFriction", "restitution"]; + constructor() { + super(); + this.staticFriction = 0.6; + this.dynamicFriction = 0.6; + this.restitution = 0; + this.frictionCombine = undefined; + this.restitutionCombine = undefined; + } +} + +class gltfCollisionFilter extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.collisionSystems = []; + this.collideWithSystems = []; + this.notCollideWithSystems = []; + } +} + +class gltfPhysicsJoint extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.limits = []; + this.drives = []; + } + + fromJson(json) { + super.fromJson(json); + this.limits = objectsFromJsons(json.limits, gltfPhysicsJointLimit); + this.drives = objectsFromJsons(json.drives, gltfPhysicsJointDrive); + } +} + +class gltfPhysicsJointLimit extends GltfObject { + static animatedProperties = ["min", "max", "stiffness", "damping"]; + constructor() { + super(); + this.min = undefined; + this.max = undefined; + this.stiffness = Infinity; + this.damping = 0; + this.linearAxes = undefined; + this.angularAxes = undefined; + } +} + +class gltfPhysicsJointDrive extends GltfObject { + static animatedProperties = [ + "maxForce", + "positionTarget", + "velocityTarget", + "stiffness", + "damping" + ]; + constructor() { + super(); + this.type = undefined; + this.mode = undefined; + this.axis = undefined; + this.maxForce = Infinity; + this.positionTarget = undefined; + this.velocityTarget = undefined; + this.stiffness = 0; + this.damping = 0; + } +} + +export { + KHR_physics_rigid_bodies, + gltfPhysicsMaterial, + gltfCollisionFilter, + gltfPhysicsJoint, + gltfPhysicsJointLimit, + gltfPhysicsJointDrive +}; From df242b4b844eb691c8ef66f481150ccbcadcd76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 3 Nov 2025 12:34:33 +0100 Subject: [PATCH 04/93] Add read-only properties --- source/gltf/rigid_bodies.js | 1 + 1 file changed, 1 insertion(+) diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js index 891f60eb..eef90d6e 100644 --- a/source/gltf/rigid_bodies.js +++ b/source/gltf/rigid_bodies.js @@ -3,6 +3,7 @@ import { objectsFromJsons } from "./utils"; class KHR_physics_rigid_bodies extends GltfObject { static animatedProperties = []; + static readonlyAnimatedProperties = ["physicsMaterials", "collisionFilters", "physicsJoints"]; constructor() { super(); this.physicsMaterials = []; From ffe26b27837ef6199f7e124a1e7df64bec484bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 12 Nov 2025 18:07:34 +0100 Subject: [PATCH 05/93] Create PhysX actors --- package-lock.json | 13 +- package.json | 5 +- source/GltfState/phyiscs_controller.js | 831 +++++++++++++++++++++++++ source/geometry_generator.js | 162 +++++ source/gltf/gltf_utils.js | 118 +++- 5 files changed, 1125 insertions(+), 4 deletions(-) create mode 100644 source/GltfState/phyiscs_controller.js create mode 100644 source/geometry_generator.js diff --git a/package-lock.json b/package-lock.json index f3c77c40..5724b8da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "gl-matrix": "^3.2.1", "globals": "^15.5.0", "jpeg-js": "^0.4.3", - "json-ptr": "^3.1.0" + "json-ptr": "^3.1.0", + "physx-js-webidl": "^2.7.0" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -3386,6 +3387,11 @@ "node": ">=8" } }, + "node_modules/physx-js-webidl": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.0.tgz", + "integrity": "sha512-BSE0a3Ti0qAAbzkFEWGByzcbvfYtOipzy8S54uFjgK03E2Rojda14QRixWEs8Wl6tEkgjq2bnZQCUYeF/dHBBA==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -7136,6 +7142,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "physx-js-webidl": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.0.tgz", + "integrity": "sha512-BSE0a3Ti0qAAbzkFEWGByzcbvfYtOipzy8S54uFjgK03E2Rojda14QRixWEs8Wl6tEkgjq2bnZQCUYeF/dHBBA==" + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", diff --git a/package.json b/package.json index bf8f40c3..6a1df974 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "gl-matrix": "^3.2.1", "globals": "^15.5.0", "jpeg-js": "^0.4.3", - "json-ptr": "^3.1.0" + "json-ptr": "^3.1.0", + "physx-js-webidl": "^2.7.0" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -58,4 +59,4 @@ "url": "https://github.com/KhronosGroup/glTF-Sample-Renderer/issues" }, "homepage": "https://github.com/KhronosGroup/glTF-Sample-Renderer/#readme" -} \ No newline at end of file +} diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js new file mode 100644 index 00000000..8df20096 --- /dev/null +++ b/source/GltfState/phyiscs_controller.js @@ -0,0 +1,831 @@ +import { filter } from "rxjs/operators"; +import { getAnimatedIndices, getMorphedNodeIndices } from "../gltf/gltf_utils"; +import PhysX from "physx-js-webidl"; +import { gltfPhysicsMaterial } from "../gltf/rigid_bodies"; +import { createCapsuleVertexData, createCylinderVertexData } from "../geometry_generator"; +import { vec3, mat4, quat } from "gl-matrix"; + +class PhysicsController { + constructor() { + this.engine = undefined; + this.state = undefined; + this.staticActors = []; + this.kinematicActors = []; + this.dynamicActors = []; + this.morphedColliders = []; + this.skinnedColliders = []; + this.hasRuntimeAnimationTargets = false; + this.morphWeights = new Map(); + + //TODO different scaled primitive colliders might need to be uniquely created + //TODO PxShape needs to be recreated if collisionFilter differs + //TODO Cache geometries for faster computation + // PxShape has localTransform which applies to all actors using the shape + // setGlobalPos can move static actors in a non physically accurate way and dynamic actors in a physically accurate way + // otherwise use kinematic actors for physically accurate movement of static actors + + // MORPH: Call PxShape::setGeometry on each shape which references the mesh, to ensure that internal data structures are updated to reflect the new geometry. + + // Which scale affects the collider geometry? + + // Different primitive modes? + } + + calculateMorphColliders(gltf) { + for (const node of this.morphedColliders) { + const mesh = gltf.meshes[node.mesh]; + let morphWeights = node.weights ?? mesh.weights; + if (morphWeights === undefined) { + continue; + } + morphWeights = morphWeights.slice(); + const oldMorphWeights = this.morphWeights.get(node.gltfObjectIndex); + + // Check if morph weights have changed + if ( + oldMorphWeights !== undefined && + oldMorphWeights.length === morphWeights.length && + oldMorphWeights.every((value, index) => value === morphWeights[index]) + ) { + continue; + } + + this.morphWeights.set(node.gltfObjectIndex, morphWeights); + + const vertices = new Float32Array(); + + for (const primitive of mesh.primitives) { + const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; + const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + const morphData = []; + for (let i = 0; i < morphWeights.length; i++) { + const morphAccessor = gltf.accessors[primitive.targets[i].POSITION]; + morphData.push(morphAccessor.getNormalizedDeinterlacedView(gltf)); + } + + // Calculate morphed vertex positions on CPU + for (let i = 0; i < positionData.length; i++) { + let position = positionData[i]; + for (let j = 0; j < morphWeights.length; j++) { + const morphValue = morphData[j]; + position += morphValue[i] * morphWeights[j]; + } + vertices.push(position); + } + } + + this.engine.updateMorphedColliderGeometry(node, vertices); + } + } + + calculateSkinnedColliders(gltf) { + for (const node of this.skinnedColliders) { + const mesh = gltf.meshes[node.mesh]; + const skin = gltf.skins[node.skin]; + const inverseBindMatricesAccessor = gltf.accessors[skin.inverseBindMatrices]; + const inverseBindMatrices = + inverseBindMatricesAccessor.getNormalizedDeinterlacedView(gltf); + const jointNodes = skin.joints.map((jointIndex) => gltf.nodes[jointIndex]); + } + } + + async initializeEngine(engine) { + if (engine === "NvidiaPhysX") { + this.engine = new NvidiaPhysicsInterface(); + await this.engine.initializeEngine(); + } + } + + loadScene(state, sceneIndex) { + const morphedNodeIndices = getMorphedNodeIndices(state.gltf); + const result = getAnimatedIndices(state.gltf, "/nodes/", [ + "translation", + "rotation", + "scale" + ]); + const animatedNodeIndices = result.animatedIndices; + this.hasRuntimeAnimationTargets = result.runtimeChanges; + const gatherRigidBodies = (nodes, currentRigidBody) => { + for (const node of nodes) { + const rigidBody = node.extensions?.KHR_physics?.rigidBody; + if (rigidBody) { + if (rigidBody.motion) { + if (rigidBody.motion.isKinematic) { + this.kinematicActors.push(node); + } else { + this.dynamicActors.push(node); + } + currentRigidBody = node; + } else if (currentRigidBody === undefined) { + if (animatedNodeIndices.has(node.gltfObjectIndex)) { + this.kinematicActors.push(node); + } else { + this.staticActors.push(node); + } + } + if (rigidBody.collider?.geometry?.node !== undefined) { + const colliderNodeIndex = rigidBody.collider.geometry.node; + const colliderNode = state.gltf.nodes[colliderNodeIndex]; + if (colliderNode.skin !== undefined) { + this.skinnedColliders.push(colliderNode); + } + if (morphedNodeIndices.has(colliderNodeIndex)) { + this.morphedColliders.push(colliderNode); + } + } + } + gatherRigidBodies(node.children, currentRigidBody); + } + }; + gatherRigidBodies(state.gltf.scenes[sceneIndex].nodes, undefined); + if (!this.engine) { + return; + } + this.engine.initializeSimulation( + state.gltf, + this.staticActors, + this.kinematicActors, + this.dynamicActors, + this.hasRuntimeAnimationTargets + ); + } + + resetScene() { + if (this.engine) { + this.engine.resetSimulation(); + } + } + + stopSimulation() { + if (this.engine) { + this.engine.stopSimulation(); + } + } + + resumeSimulation() { + if (this.engine) { + this.engine.resumeSimulation(); + } + } + + pauseSimulation() { + if (this.engine) { + this.engine.pauseSimulation(); + } + } +} + +class PhysicsInterface { + constructor() { + this.simpleShapes = []; + } + + async initializeEngine() {} + initializeSimulation(state, staticActors, kinematicActors, dynamicActors) {} + pauseSimulation() {} + resumeSimulation() {} + resetSimulation() {} + stopSimulation() {} + + generateBox(x, y, z, scale, scaleAxis) {} + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) {} + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) {} + generateSphere(radius, scale, scaleAxis) {} + generatePlane(width, height, doubleSided, scale, scaleAxis) {} + + //TODO Handle non-uniform scale properly (also for parent nodes) + generateSimpleShape(shape, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + switch (shape.type) { + case "box": + this.simpleShapes.push( + this.generateBox( + shape.box.size[0], + shape.box.size[1], + shape.box.size[2], + scale, + scaleAxis + ) + ); + break; + case "capsule": + this.simpleShapes.push( + this.generateCapsule( + shape.capsule.height, + shape.capsule.radiusTop, + shape.capsule.radiusBottom, + scale, + scaleAxis + ) + ); + break; + case "cylinder": + this.simpleShapes.push( + this.generateCylinder( + shape.cylinder.height, + shape.cylinder.radiusTop, + shape.cylinder.radiusBottom, + scale, + scaleAxis + ) + ); + break; + case "sphere": + this.simpleShapes.push(this.generateSphere(shape.sphere.radius, scale, scaleAxis)); + break; + case "plane": + this.simpleShapes.push( + this.generatePlane( + shape.plane.width, + shape.plane.height, + shape.plane.doubleSided, + scale, + scaleAxis + ) + ); + break; + } + } + + generateSimpleShapes(gltf) { + this.simpleShapes = []; + for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { + this.generateSimpleShape(shape); + } + } +} + +class NvidiaPhysicsInterface extends PhysicsInterface { + constructor() { + super(); + this.PhysX = undefined; + this.physics = undefined; + this.scene = undefined; + this.staticActors = new Map(); + this.kinematicActors = new Map(); + this.dynamicActors = new Map(); + this.defaultMaterial = new gltfPhysicsMaterial(); + this.tolerances = undefined; + this.filterData = []; + } + + async initializeEngine() { + this.PhysX = await PhysX(); + const version = this.PhysX.PHYSICS_VERSION; + console.log( + "PhysX loaded! Version: " + + ((version >> 24) & 0xff) + + "." + + ((version >> 16) & 0xff) + + "." + + ((version >> 8) & 0xff) + ); + + const allocator = new this.PhysX.PxDefaultAllocator(); + const errorCb = new this.PhysX.PxDefaultErrorCallback(); + const foundation = this.PhysX.CreateFoundation(version, allocator, errorCb); + console.log("Created PxFoundation"); + + this.tolerances = new this.PhysX.PxTolerancesScale(); + this.physics = this.PhysX.CreatePhysics(version, foundation, this.tolerances); + console.log("Created PxPhysics"); + return this.PhysX; + } + + createCollider(colliderNode) {} + + mapCombineMode(mode) { + switch (mode) { + case "average": + return this.PhysX.PxCombineModeEnum.eAVERAGE; + case "minimum": + return this.PhysX.PxCombineModeEnum.eMIN; + case "maximum": + return this.PhysX.PxCombineModeEnum.eMAX; + case "multiply": + return this.PhysX.PxCombineModeEnum.eMULTIPLY; + } + } + + generateBox(x, y, z, scale, scaleAxis) { + if (quat.equals(scaleAxis, quat.create()) === false) { + //TODO scale with rotation + } + const geometry = new this.PhysX.PxBoxGeometry( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] + ); + return geometry; + } + + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) { + if ( + quat.equals(scaleAxis, quat.create()) === false || + radiusTop !== radiusBottom || + scale[0] !== scale[2] + ) { + //TODO scale with rotation + const data = createCapsuleVertexData(radiusTop, radiusBottom, height); + return this.createConvexMesh(data.vertices, data.indices); + } + height *= scale[1]; + radiusTop *= scale[0]; + radiusBottom *= scale[0]; + + return new this.PhysX.PxCapsuleGeometry(radiusTop, height / 2); + } + + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) { + if ( + quat.equals(scaleAxis, quat.create()) === false || + radiusTop !== radiusBottom || + scale[0] !== scale[2] + ) { + //TODO scale with rotation + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexMesh(data.vertices, data.indices); + } + height *= scale[1]; + radiusTop *= scale[0]; + radiusBottom *= scale[0]; + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexMesh(data.vertices, data.indices); + } + + generateSphere(radius, scale, scaleAxis) { + if ( + scale.every((value) => value === scale[0]) === false || + quat.equals(scaleAxis, quat.create()) === false + ) { + //TODO + } else { + radius *= scale[0]; + } + const geometry = new this.PhysX.PxSphereGeometry(radius); + return geometry; + } + + generatePlane(width, height, doubleSided, scale, scaleAxis) { + const geometry = new this.PhysX.PxPlaneGeometry(); + return geometry; + } + + createConvexMesh( + vertices, + indices, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxConvexMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); + let flag = 0; + flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eCOMPUTE_CONVEX(); + flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eQUANTIZE_INPUT(); + flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eDISABLE_MESH_VALIDATION(); + const pxflags = new this.PhysX.PxConvexFlags(flag); + des.flags = pxflags; + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + const tri = this.PhysX.CreateConvexMesh(cookingParams, des); + + const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); + const PxQuat = new this.PhysX.PxQuat(...scaleAxis); + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxConvexMeshGeometryFlags(); + const geometry = new this.PhysX.PxConvexMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(pxflags); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } + + createTriangleMesh( + vertices, + indices, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + const geometry = new this.PhysX.PxTriangleMeshGeometry(vertices, indices, scale, scaleAxis); + return geometry; + } + + collectVerticesAndIndicesFromNode(gltf, node) { + // TODO Handle different primitive modes + const mesh = gltf.meshes[node.mesh]; + let positionDataArray = []; + let positionCount = 0; + let indexDataArray = []; + let indexCount = 0; + for (const primitive of mesh.primitives) { + const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; + positionDataArray.push(positionAccessor.getNormalizedDeinterlacedView(gltf)); + positionCount += positionAccessor.count; + if (primitive.indices !== undefined) { + const indexAccessor = gltf.accessors[primitive.indices]; + indexDataArray.push(indexAccessor.getNormalizedDeinterlacedView(gltf)); + indexCount += indexAccessor.count; + } else { + const array = Array.from(Array(positionAccessor.count).keys()); + indexDataArray.push(new Uint32Array(array)); + indexCount += positionAccessor.count; + } + } + + const positionData = new Float32Array(positionCount * 3); + const indexData = new Uint32Array(indexCount); + let offset = 0; + for (const positionChunk of positionDataArray) { + positionData.set(positionChunk, offset); + offset += positionChunk.length; + } + offset = 0; + for (const indexChunk of indexDataArray) { + indexData.set(indexChunk, offset); + offset += indexChunk.length; + } + return { positionData, indexData }; + } + + createConvexMeshFromNode( + gltf, + node, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + const { positionData, indexData } = this.collectVerticesAndIndicesFromNode(gltf, node); + return this.createConvexMesh(positionData, indexData, scale, scaleAxis); + } + + createMeshFromNode(gltf, node, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const { vertices, indices } = this.collectVerticesAndIndicesFromNode(gltf, node); + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxTriangleMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); + + des.triangles.stride = indices.BYTES_PER_ELEMENT * 3; + des.triangles.count = indices.length / 3; + des.triangles.data = malloc(indices); + + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); + + const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); + const PxQuat = new this.PhysX.PxQuat(...scaleAxis); + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxMeshGeometryFlags(); + const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } + + collidesWith(filterA, filterB) { + if (filterA.collideWithSystems.length > 0) { + for (const system of filterA.collideWithSystems) { + if (filterB.collisionSystems.includes(system)) { + return true; + } + } + return false; + } else if (filterA.notCollideWithSystems.length > 0) { + for (const system of filterA.notCollideWithSystems) { + if (filterB.collisionSystems.includes(system)) { + return false; + } + return true; + } + } + return true; + } + + computeFilterData(gltf) { + // Default filter is sign bit + const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; + this.filterData = new Array(32).fill(0); + this.filterData[31] = Math.pow(2, 32) - 1; // Default filter with all bits set + let filterCount = filters?.length ?? 0; + if (filterCount > 31) { + filterCount = 31; + console.warn( + "PhysX supports a maximum of 31 collision filters. Additional filters will be ignored." + ); + } + + for (let i = 0; i < filterCount; i++) { + let bitMask = 0; + for (let j = 0; j < filterCount; j++) { + if (this.collidesWith(filters[i], filters[j])) { + bitMask |= 1 << j; + } + } + this.filterData[i] = bitMask; + } + } + + createShape( + gltf, + collider, + shapeFlags, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + let geometry = null; + if (collider?.geometry?.shape !== undefined) { + if ( + scale[0] !== 1 || + scale[1] !== 1 || + scale[2] !== 1 || + quat.equals(scaleAxis, quat.create()) === false + ) { + const simpleShape = + gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; + geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); + } else { + geometry = this.simpleShapes[collider.geometry.shape]; + } + } else if (collider?.geometry?.node !== undefined) { + if (collider.geometry.convexHull === true) { + geometry = this.createConvexMeshFromNode( + gltf, + collider.geometry.node, + scale, + scaleAxis + ); + } else { + geometry = this.createMeshFromNode(gltf, collider.geometry.node, scale, scaleAxis); + } + } + + const gltfMaterial = collider.physicsMaterial + ? gltf.extensions.KHR_physics_rigid_bodies.physicsMaterials[collider.physicsMaterial] + : this.defaultMaterial; + + const physxMaterial = this.physics.createMaterial( + gltfMaterial.staticFriction, + gltfMaterial.dynamicFriction, + gltfMaterial.restitution + ); + if (gltfMaterial.frictionCombine !== undefined) { + physxMaterial.setFrictionCombine(this.mapCombineMode(gltfMaterial.frictionCombine)); + } + if (gltfMaterial.restitutionCombine !== undefined) { + physxMaterial.setRestitutionCombine( + this.mapCombineMode(gltfMaterial.restitutionCombine) + ); + } + + const shape = this.physics.createShape(geometry, physxMaterial, true, shapeFlags); + + let word0 = null; + let word1 = null; + if ( + collider?.collisionFilter !== undefined && + collider.collisionFilter < this.filterData.length - 1 + ) { + word0 = 1 << collider.collisionFilter; + word1 = this.filterData[collider.collisionFilter]; + } else { + // Default filter id is signed bit and all bits set to collide with everything + word0 = Math.pow(2, 31); + word1 = Math.pow(2, 32) - 1; + } + + const additionalFlags = 0; + const filterData = new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); + + shape.setSimulationFilterData(filterData); + + return shape; + } + + createActor(gltf, node, shapeFlags, type) { + const worldTransform = node.worldTransform; + const translation = vec3.create(); + mat4.getTranslation(translation, worldTransform); + const pos = new this.PhysX.PxVec3(translation.x, translation.y, translation.z); + const rotation = new this.PhysX.PxQuat(...node.worldRotation); + const pose = new this.PhysX.PxTransform(pos, rotation); + let actor = null; + if (type === "static") { + actor = this.physics.createRigidStatic(pose); + } else { + actor = this.physics.createRigidDynamic(pose); + if (type === "kinematic") { + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); + } + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion) { + const gltfAngularVelocity = motion?.angularVelocity; + const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); + actor.setAngularVelocity(angularVelocity); + this.PhysX.destroy(angularVelocity); + + const gltfLinearVelocity = motion?.linearVelocity; + const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); + actor.setLinearVelocity(linearVelocity); + this.PhysX.destroy(linearVelocity); + + if (motion.mass !== undefined) { + actor.setMass(motion.mass); + } + if (motion.centerOfMass !== undefined) { + const com = new this.PhysX.PxVec3(...motion.centerOfMass); + const inertiaRotation = new this.PhysX.PxQuat( + this.PhysX.PxIDENTITYEnum.PxIdentity + ); + if (motion.inertiaOrientation !== undefined) { + inertiaRotation.x = motion.inertiaOrientation[0]; + inertiaRotation.y = motion.inertiaOrientation[1]; + inertiaRotation.z = motion.inertiaOrientation[2]; + inertiaRotation.w = motion.inertiaOrientation[3]; + } + const comTransform = new this.PhysX.PxTransform(com, inertiaRotation); + actor.setCMassLocalPose(comTransform); + this.PhysX.destroy(com); + this.PhysX.destroy(inertiaRotation); + this.PhysX.destroy(comTransform); + } + if (motion.inertiaDiagonal !== undefined) { + const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } + + // Let the engine compute mass and inertia if not all parameters are specified + if (motion.inertiaDiagonal === undefined) { + const pose = motion.centerOfMass + ? new this.PhysX.PxVec3(...motion.centerOfMass) + : new this.PhysX.PxVec3(0, 0, 0); + if (motion.mass === undefined) { + this.PhysX.PxRigidBodyExt.updateMassAndInertia(actor, 1.0, pose); + } else { + this.PhysX.PxRigidBodyExt.setMassAndUpdateInertia(actor, motion.mass, pose); + } + this.PhysX.destroy(pose); + } + + if (motion.gravityFactor !== 1.0) { + actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); + //TODO Apply custom gravity in simulation step + } + } + } + + const recurseShapes = ( + gltf, + node, + shapeFlags, + shapeTransform, + offsetTransform, + origin = false + ) => { + // Do not add other motion bodies' shapes to this actor + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined && !origin) { + return; + } + const scalingTransform = mat4.create(); + mat4.fromScaling(scalingTransform, node.scale); + mat4.multiply(scalingTransform, shapeTransform, scalingTransform); + + const computedOffset = mat4.create(); + mat4.multiply(computedOffset, offsetTransform, node.getLocalTransform()); + + if (node.extensions?.KHR_physics_rigid_bodies?.collider !== undefined) { + const shape = this.createShape( + gltf, + node.extensions.KHR_physics_rigid_bodies.collider, + shapeFlags + //scalingTransform + ); + + const translation = vec3.create(); + const rotation = quat.create(); + mat4.getTranslation(translation, offsetTransform); + mat4.getRotation(rotation, offsetTransform); + + const PxPos = new this.PhysX.PxVec3(translation.x, translation.y, translation.z); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + shape.setLocalPose(pose); + + actor.attachShape(shape); + this.PhysX.destroy(PxPos); + this.PhysX.destroy(PxRotation); + this.PhysX.destroy(pose); + + if ( + node.extensions.KHR_physics_rigid_bodies.collider?.geometry?.node !== undefined + ) { + const geometryNode = + gltf.nodes[node.extensions.KHR_physics_rigid_bodies.collider.geometry.node]; + recurseShapes(gltf, geometryNode, shapeFlags, scalingTransform, computedOffset); + } + } + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + recurseShapes(gltf, childNode, shapeFlags, scalingTransform, computedOffset); + } + }; + + recurseShapes( + gltf, + node.extensions.KHR_physics_rigid_bodies.collider, + shapeFlags, + worldTransform, + mat4.create(), + true + ); + + this.PhysX.destroy(pos); + this.PhysX.destroy(rotation); + this.PhysX.destroy(pose); + + this.scene.addActor(actor); + this.staticActors.set(node.gltfObjectIndex, actor); + } + + initializeSimulation( + state, + staticActors, + kinematicActors, + dynamicActors, + hasRuntimeAnimationTargets + ) { + if (!this.PhysX) { + return; + } + if (this.scene) { + this.stopSimulation(); + } + this.generateSimpleShapes(state.gltf); + this.computeFilterData(state.gltf); + + const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); + const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); + sceneDesc.set_gravity(tmpVec); + sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); + sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); + this.scene = this.physics.createScene(sceneDesc); + console.log("Created scene"); + const shapeFlags = new this.PhysX.PxShapeFlags( + this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | + this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE | + this.PhysX.PxShapeFlagEnum.eVISUALIZATION + ); + + for (const node of staticActors) { + this.createActor(state.gltf, node, shapeFlags, "static"); + } + for (const node of kinematicActors) { + this.createActor(state.gltf, node, shapeFlags, "kinematic"); + } + for (const node of dynamicActors) { + this.createActor(state.gltf, node, shapeFlags, "dynamic"); + } + + this.PhysX.destroy(tmpVec); + this.PhysX.destroy(sceneDesc); + this.PhysX.destroy(shapeFlags); + } + pauseSimulation() { + // Implementation specific to Nvidia physics engine + } + resumeSimulation() { + if (!this.scene) { + return; + } + this.scene.simulate(1 / 60); + this.scene.fetchResults(true); + } + resetSimulation() { + // Implementation specific to Nvidia physics engine + } + stopSimulation() { + // Implementation specific to Nvidia physics engine + } +} + +export { PhysicsController }; diff --git a/source/geometry_generator.js b/source/geometry_generator.js new file mode 100644 index 00000000..c222336a --- /dev/null +++ b/source/geometry_generator.js @@ -0,0 +1,162 @@ +/** + * Script based off of Babylon.js: https://github.com/BabylonJS/Babylon.js/blob/10428bb078689922ea643ec49c5240af38e05925/packages/dev/core/src/Meshes/Builders/capsuleBuilder.ts + */ + +import { vec3 } from "gl-matrix"; + +export function createCapsuleVertexData( + radiusTop = 0.25, + radiusBottom = 0.25, + height = 1, + subdivisions = 2, + tessellation = 16, + capSubdivisions = 6 +) { + const capDetail = capSubdivisions; + const radialSegments = tessellation; + const heightSegments = subdivisions; + + const heightMinusCaps = height - (radiusTop + radiusBottom); + + const thetaStart = 0.0; + const thetaLength = 2.0 * Math.PI; + + const capsTopSegments = capDetail; + const capsBottomSegments = capDetail; + + const alpha = Math.acos((radiusBottom - radiusTop) / height); + + let indices = []; + const vertices = []; + + let index = 0; + const indexArray = [], + halfHeight = heightMinusCaps * 0.5; + const pi2 = Math.PI * 0.5; + + let x, y; + + const cosAlpha = Math.cos(alpha); + const sinAlpha = Math.sin(alpha); + + for (y = 0; y <= capsTopSegments; y++) { + const indexRow = []; + + const a = pi2 - alpha * (y / capsTopSegments); + + const cosA = Math.cos(a); + const sinA = Math.sin(a); + + // calculate the radius of the current row + const _radius = cosA * radiusTop; + + for (x = 0; x <= radialSegments; x++) { + const u = x / radialSegments; + const theta = u * thetaLength + thetaStart; + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + // vertex + vertices.push(_radius * sinTheta); + vertices.push(halfHeight + sinA * radiusTop); + vertices.push(_radius * cosTheta); + + // save index of vertex in respective row + indexRow.push(index); + // increase index + index++; + } + // now save vertices of the row in our index array + indexArray.push(indexRow); + } + + const coneHeight = + height - radiusTop - radiusBottom + cosAlpha * radiusTop - cosAlpha * radiusBottom; + + for (y = 1; y <= heightSegments; y++) { + const indexRow = []; + // calculate the radius of the current row + const _radius = sinAlpha * ((y * (radiusBottom - radiusTop)) / heightSegments + radiusTop); + for (x = 0; x <= radialSegments; x++) { + const u = x / radialSegments; + const theta = u * thetaLength + thetaStart; + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + // vertex + vertices.push(_radius * sinTheta); + vertices.push(halfHeight + cosAlpha * radiusTop - (y * coneHeight) / heightSegments); + vertices.push(_radius * cosTheta); + + // save index of vertex in respective row + indexRow.push(index); + // increase index + index++; + } + // now save vertices of the row in our index array + indexArray.push(indexRow); + } + + for (y = 1; y <= capsBottomSegments; y++) { + const indexRow = []; + const a = pi2 - alpha - (Math.PI - alpha) * (y / capsBottomSegments); + const cosA = Math.cos(a); + const sinA = Math.sin(a); + // calculate the radius of the current row + const _radius = cosA * radiusBottom; + for (x = 0; x <= radialSegments; x++) { + const u = x / radialSegments; + const theta = u * thetaLength + thetaStart; + const sinTheta = Math.sin(theta); + const cosTheta = Math.cos(theta); + // vertex + vertices.push(_radius * sinTheta); + vertices.push(-halfHeight + sinA * radiusBottom); + vertices.push(_radius * cosTheta); + + // save index of vertex in respective row + indexRow.push(index); + // increase index + index++; + } + // now save vertices of the row in our index array + indexArray.push(indexRow); + } + // generate indices + for (x = 0; x < radialSegments; x++) { + for (y = 0; y < capsTopSegments + heightSegments + capsBottomSegments; y++) { + // we use the index array to access the correct indices + const i1 = indexArray[y][x]; + const i2 = indexArray[y + 1][x]; + const i3 = indexArray[y + 1][x + 1]; + const i4 = indexArray[y][x + 1]; + // face one + indices.push(i1); + indices.push(i2); + indices.push(i4); + // face two + indices.push(i2); + indices.push(i3); + indices.push(i4); + } + } + + indices = indices.reverse(); + + return { positions: new Float32Array(vertices), indices: new Uint8Array(indices) }; +} + +export function createCylinderVertexData(radiusTop, radiusBottom, height, numDivisions = 30) { + const positions = []; + for (let i = 0; i < numDivisions; i++) { + const c = Math.cos((2 * Math.PI * i) / (numDivisions - 1)); + const s = Math.sin((2 * Math.PI * i) / (numDivisions - 1)); + positions.push(c * radiusTop); + positions.push(0.5 * height); + positions.push(s * radiusTop); + + positions.push(c * radiusBottom); + positions.push(-0.5 * height); + positions.push(s * radiusBottom); + } + const indices = Array.from(positions.keys()); + return { positions: new Float32Array(positions), indices: new Uint8Array(indices) }; +} diff --git a/source/gltf/gltf_utils.js b/source/gltf/gltf_utils.js index e6335d1e..b3e6dfd6 100644 --- a/source/gltf/gltf_utils.js +++ b/source/gltf/gltf_utils.js @@ -96,4 +96,120 @@ function getExtentsFromAccessor(accessor, worldTransform, outMin, outMax) { } } -export { getSceneExtents }; +function getAnimatedIndices(gltf, prefix, properties) { + const checkNodePointer = (pointer, prefix, properties, graphNode = undefined) => { + if (!pointer.startsWith(prefix)) { + return undefined; + } + let match = undefined; + for (const property of properties) { + if (pointer.endsWith("/" + property)) { + match = property; + break; + } + } + if (!match) { + return undefined; + } + const indexPart = pointer.substring(prefix.length, pointer.length - ("/" + match).length); + if (indexPart.startsWith("{") && indexPart.endsWith("}")) { + // Check in interactivity graph + if (graphNode === undefined) { + return undefined; + } + const nodeId = indexPart.substring(1, indexPart.length - 1); + if (graphNode.values[nodeId] !== undefined) { + return parseInt(graphNode.values[nodeId]); + } + // Every node can be animated since it is determined at runtime + return Infinity; + } + return parseInt(indexPart); + }; + + const animatedIndices = new Set(); + let runtimeChanges = false; + + // Check animation channels + for (const animation of gltf.animations) { + for (const channel of animation.channels) { + const target = channel.target; + if ( + prefix === "/nodes/" && + target.node !== undefined && + properties.includes(target.path) + ) { + animatedIndices.add(target.node); + } + const pointer = target.extensions?.KHR_animation_pointer?.pointer; + if (pointer) { + const result = checkNodePointer(pointer, prefix, properties); + if (result !== undefined) { + animatedIndices.add(result); + } + } + } + } + + // Check interactivity graphs + if (gltf.extensions?.KHR_interactivity?.graphs !== undefined) { + for (const graph of gltf.extensions.KHR_interactivity.graphs) { + let pointerSetID = undefined; + for (const [index, declaration] of graph.declarations.entries()) { + if (declaration.op === "pointer/set") { + pointerSetID = index; + break; + } + } + if (pointerSetID === undefined) { + continue; + } + for (const node of graph.nodes) { + if (node.declaration !== pointerSetID) { + continue; + } + const pointer = node.configuration.pointer.value[0]; + const result = checkNodePointer(pointer, prefix, properties, node); + if (result === Infinity) { + runtimeChanges = true; + } else if (result !== undefined) { + animatedIndices.add(result); + } + } + } + } + return { animatedIndices: animatedIndices, runtimeChanges: runtimeChanges }; +} + +function getMorphedNodeIndices(gltf) { + const morphedNodes = new Set(); + const morphedMeshes = new Set(); + for (const mesh of gltf.meshes) { + if (mesh.primitives === undefined) { + continue; + } + for (const primitive of mesh.primitives) { + let isMorphed = false; + if (primitive.targets !== undefined && primitive.targets.length > 0) { + for (const target of primitive.targets) { + if (target.POSITION !== undefined) { + isMorphed = true; + break; + } + } + } + if (isMorphed) { + morphedMeshes.add(mesh.gltfObjectIndex); + break; + } + } + } + for (const node of gltf.nodes) { + if (node.mesh !== undefined && morphedMeshes.has(node.mesh)) { + morphedNodes.add(node.gltfObjectIndex); + } + } + return morphedNodes; +} + +export { getSceneExtents, getAnimatedIndices, getMorphedNodeIndices }; From fc24c71830061c5f41c010d504cefabab93fd0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 19 Nov 2025 19:16:22 +0100 Subject: [PATCH 06/93] Apply simulation --- source/GltfState/gltf_state.js | 4 + source/GltfState/phyiscs_controller.js | 133 +++++++++++++++++++++---- source/GltfView/gltf_view.js | 8 ++ source/geometry_generator.js | 4 +- 4 files changed, 127 insertions(+), 22 deletions(-) diff --git a/source/GltfState/gltf_state.js b/source/GltfState/gltf_state.js index 696ea300..8a90f893 100644 --- a/source/GltfState/gltf_state.js +++ b/source/GltfState/gltf_state.js @@ -1,6 +1,7 @@ import { GraphController } from "../gltf/interactivity.js"; import { UserCamera } from "../gltf/user_camera.js"; import { AnimationTimer } from "./animation_timer.js"; +import { PhysicsController } from "./phyiscs_controller.js"; /** * GltfState containing a state for visualization in GltfView @@ -56,6 +57,9 @@ class GltfState { /* Array of screen positions for hovering. Currently only one is supported. */ this.hoverPositions = [{ x: undefined, y: undefined }]; + /* the physics controller allows selecting and controlling different physics engines */ + this.physicsController = new PhysicsController(); + /** parameters used to configure the rendering */ this.renderingParameters = { /** morphing between vertices */ diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 8df20096..5c6f4e80 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1,6 +1,8 @@ -import { filter } from "rxjs/operators"; import { getAnimatedIndices, getMorphedNodeIndices } from "../gltf/gltf_utils"; import PhysX from "physx-js-webidl"; +// The import is needed for rollup to include the wasm file in the build output +// eslint-disable-next-line no-unused-vars +import PhysXBinaryFile from "physx-js-webidl/physx-js-webidl.wasm"; import { gltfPhysicsMaterial } from "../gltf/rigid_bodies"; import { createCapsuleVertexData, createCylinderVertexData } from "../geometry_generator"; import { vec3, mat4, quat } from "gl-matrix"; @@ -8,7 +10,6 @@ import { vec3, mat4, quat } from "gl-matrix"; class PhysicsController { constructor() { this.engine = undefined; - this.state = undefined; this.staticActors = []; this.kinematicActors = []; this.dynamicActors = []; @@ -16,6 +17,10 @@ class PhysicsController { this.skinnedColliders = []; this.hasRuntimeAnimationTargets = false; this.morphWeights = new Map(); + this.playing = false; + this.enabled = false; + this.simulationStepTime = 1 / 60; + this.timeAccumulator = 0; //TODO different scaled primitive colliders might need to be uniquely created //TODO PxShape needs to be recreated if collisionFilter differs @@ -86,6 +91,7 @@ class PhysicsController { const inverseBindMatrices = inverseBindMatricesAccessor.getNormalizedDeinterlacedView(gltf); const jointNodes = skin.joints.map((jointIndex) => gltf.nodes[jointIndex]); + //TODO: Implement skinned collider calculation } } @@ -97,6 +103,17 @@ class PhysicsController { } loadScene(state, sceneIndex) { + //TODO reset previous scene + if ( + state.gltf.extensionsUsed === undefined || + state.gltf.extensionsUsed.includes("KHR_physics_rigid_bodies") === false + ) { + return; + } + const scene = state.gltf.scenes[sceneIndex]; + if (!scene.nodes) { + return; + } const morphedNodeIndices = getMorphedNodeIndices(state.gltf); const result = getAnimatedIndices(state.gltf, "/nodes/", [ "translation", @@ -105,9 +122,10 @@ class PhysicsController { ]); const animatedNodeIndices = result.animatedIndices; this.hasRuntimeAnimationTargets = result.runtimeChanges; - const gatherRigidBodies = (nodes, currentRigidBody) => { - for (const node of nodes) { - const rigidBody = node.extensions?.KHR_physics?.rigidBody; + const gatherRigidBodies = (nodeIndices, currentRigidBody) => { + for (const nodeIndex of nodeIndices) { + const node = state.gltf.nodes[nodeIndex]; + const rigidBody = node.extensions?.KHR_physics_rigid_bodies; if (rigidBody) { if (rigidBody.motion) { if (rigidBody.motion.isKinematic) { @@ -137,12 +155,17 @@ class PhysicsController { gatherRigidBodies(node.children, currentRigidBody); } }; - gatherRigidBodies(state.gltf.scenes[sceneIndex].nodes, undefined); - if (!this.engine) { + gatherRigidBodies(scene.nodes, undefined); + if ( + !this.engine || + (this.staticActors.length === 0 && + this.kinematicActors.length === 0 && + this.dynamicActors.length === 0) + ) { return; } this.engine.initializeSimulation( - state.gltf, + state, this.staticActors, this.kinematicActors, this.dynamicActors, @@ -157,6 +180,8 @@ class PhysicsController { } stopSimulation() { + this.playing = false; + this.enabled = false; if (this.engine) { this.engine.stopSimulation(); } @@ -164,13 +189,44 @@ class PhysicsController { resumeSimulation() { if (this.engine) { - this.engine.resumeSimulation(); + this.enabled = true; + this.playing = true; } } pauseSimulation() { - if (this.engine) { - this.engine.pauseSimulation(); + this.enabled = true; + this.playing = false; + } + + simulateStep(state, deltaTime) { + if (state === undefined) { + return; + } + this.applyAnimations(state); + this.timeAccumulator += deltaTime; + if ( + this.playing && + this.engine && + state && + this.timeAccumulator >= this.simulationStepTime + ) { + this.engine.simulateStep(state, this.timeAccumulator); + this.timeAccumulator = 0; + } + } + + applyAnimations(state) { + for (const node of state.gltf.nodes) { + // TODO set worldTransformUpdated in node when transform changes from animations/interactivity + if (node.worldTransformUpdated) { + node.physicsTransform = node.worldTransform; + if (this.engine) { + this.engine.updateRigidBodyTransform(node); + } + } + // TODO check if morph target weights and skinning have changed + // TODO check if collider/physics properties have changed } } } @@ -248,6 +304,9 @@ class PhysicsInterface { generateSimpleShapes(gltf) { this.simpleShapes = []; + if (gltf?.extensions?.KHR_implicit_shapes === undefined) { + return; + } for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { this.generateSimpleShape(shape); } @@ -260,16 +319,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX = undefined; this.physics = undefined; this.scene = undefined; - this.staticActors = new Map(); - this.kinematicActors = new Map(); - this.dynamicActors = new Map(); + this.nodeToActor = new Map(); this.defaultMaterial = new gltfPhysicsMaterial(); this.tolerances = undefined; this.filterData = []; } async initializeEngine() { - this.PhysX = await PhysX(); + this.PhysX = await PhysX({ locateFile: () => "./libs/physx-js-webidl.wasm" }); const version = this.PhysX.PHYSICS_VERSION; console.log( "PhysX loaded! Version: " + @@ -764,7 +821,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(pose); this.scene.addActor(actor); - this.staticActors.set(node.gltfObjectIndex, actor); + this.nodeToActor.set(node.gltfObjectIndex, actor); } initializeSimulation( @@ -810,15 +867,51 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(sceneDesc); this.PhysX.destroy(shapeFlags); } - pauseSimulation() { - // Implementation specific to Nvidia physics engine + + applyTransformRecursively(gltf, node, parentTransform) { + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; + } + const localTransform = node.getLocalTransform(); + const globalTransform = mat4.create(); + mat4.multiply(globalTransform, parentTransform, localTransform); + node.physicsTransform = globalTransform; + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.applyTransformRecursively(gltf, childNode, globalTransform); + } } - resumeSimulation() { + + simulateStep(state, deltaTime) { if (!this.scene) { return; } - this.scene.simulate(1 / 60); + this.scene.simulate(deltaTime); this.scene.fetchResults(true); + + for (const [nodeIndex, actor] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + if (node.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { + const transform = actor.getGlobalPose(); + const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); + const rotation = quat.fromValues( + transform.q.x, + transform.q.y, + transform.q.z, + transform.q.w + ); + node.physicsTransform = mat4.fromRotationTranslation( + mat4.create(), + rotation, + position + ); + + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + this.applyTransformRecursively(state.gltf, childNode, node.physicsTransform); + } + } + } } resetSimulation() { // Implementation specific to Nvidia physics engine diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index 8990047b..fd9d8f3b 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -17,6 +17,7 @@ class GltfView { constructor(context) { this.context = context; this.renderer = new gltfRenderer(this.context); + this.lastFrameTime = undefined; } /** @@ -57,6 +58,10 @@ class GltfView { * @param {*} height of the viewport */ renderFrame(state, width, height) { + const lastFrameTime = + this.lastFrameTime === undefined ? performance.now() : this.lastFrameTime; + this.lastFrameTime = performance.now(); + const currentFrameTime = performance.now(); this.renderer.init(state); this._animate(state); @@ -75,6 +80,9 @@ class GltfView { } scene.applyTransformHierarchy(state.gltf); + if (state.physicsController.playing && state.physicsController.enabled) { + state.physicsController.simulateStep(state, currentFrameTime - lastFrameTime); + } this.renderer.drawScene(state, scene); } diff --git a/source/geometry_generator.js b/source/geometry_generator.js index c222336a..e8ed3718 100644 --- a/source/geometry_generator.js +++ b/source/geometry_generator.js @@ -141,7 +141,7 @@ export function createCapsuleVertexData( indices = indices.reverse(); - return { positions: new Float32Array(vertices), indices: new Uint8Array(indices) }; + return { vertices: new Float32Array(vertices), indices: new Uint8Array(indices) }; } export function createCylinderVertexData(radiusTop, radiusBottom, height, numDivisions = 30) { @@ -158,5 +158,5 @@ export function createCylinderVertexData(radiusTop, radiusBottom, height, numDiv positions.push(s * radiusBottom); } const indices = Array.from(positions.keys()); - return { positions: new Float32Array(positions), indices: new Uint8Array(indices) }; + return { vertices: new Float32Array(positions), indices: new Uint8Array(indices) }; } From 3c2da8e2e759f28607af4630912b85bff7f44089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 19 Nov 2025 19:16:57 +0100 Subject: [PATCH 07/93] Use physic transform if provided --- source/Renderer/renderer.js | 12 +++++++----- source/gltf/camera.js | 10 +++++++--- source/gltf/gltf_utils.js | 2 +- source/gltf/interactivity.js | 16 ++++++++++++++-- source/gltf/light.js | 2 +- source/gltf/node.js | 5 +++++ source/gltf/skin.js | 2 +- 7 files changed, 36 insertions(+), 13 deletions(-) diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index 4c41ccdf..33b09add 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -408,7 +408,7 @@ class gltfRenderer { let counter = 0; this.opaqueDrawables = Object.groupBy(this.opaqueDrawables, (a) => { - const winding = Math.sign(mat4.determinant(a.node.worldTransform)); + const winding = Math.sign(mat4.determinant(a.node.getRenderedWorldTransform())); const id = `${a.node.mesh}_${winding}_${a.primitiveIndex}`; // Disable instancing for skins, morph targets and if the GPU attributes limit is reached. // Additionally we define a new id for each instance of the EXT_mesh_gpu_instancing extension. @@ -532,7 +532,7 @@ class gltfRenderer { if (instance.length > 1) { instanceOffset = []; for (const iDrawable of instance) { - instanceOffset.push(iDrawable.node.worldTransform); + instanceOffset.push(iDrawable.node.getRenderedWorldTransform()); } } else if (instance[0].node.instanceMatrices !== undefined) { // Set instance matrices for EXT_mesh_gpu_instancing extension @@ -1092,9 +1092,11 @@ class gltfRenderer { this.webGl.context.uniform1i(this.shader.getUniformLocation("u_MaterialID"), renderpassConfiguration.drawID); } + const worldTransform = node.getRenderedWorldTransform(); + // update model dependant matrices once per node this.shader.updateUniform("u_ViewProjectionMatrix", viewProjectionMatrix); - this.shader.updateUniform("u_ModelMatrix", node.worldTransform); + this.shader.updateUniform("u_ModelMatrix", worldTransform); this.shader.updateUniform("u_NormalMatrix", node.normalMatrix, false); this.shader.updateUniform("u_Exposure", state.renderingParameters.exposure, false); this.shader.updateUniform("u_Camera", this.currentCameraPosition, false); @@ -1104,7 +1106,7 @@ class gltfRenderer { this.updateAnimationUniforms(state, node, primitive); - if (mat4.determinant(node.worldTransform) < 0.0) + if (mat4.determinant(worldTransform) < 0.0) { this.webGl.context.frontFace(GL.CW); } @@ -1342,7 +1344,7 @@ class gltfRenderer { this.webGl.context.uniform2i(this.shader.getUniformLocation("u_TransmissionFramebufferSize"), this.opaqueFramebufferWidth, this.opaqueFramebufferHeight); - this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ModelMatrix"),false, node.worldTransform); + this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ModelMatrix"),false, worldTransform); this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ViewMatrix"),false, this.viewMatrix); this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ProjectionMatrix"),false, this.projMatrix); } diff --git a/source/gltf/camera.js b/source/gltf/camera.js index e08389e6..16b9951b 100644 --- a/source/gltf/camera.js +++ b/source/gltf/camera.js @@ -33,7 +33,11 @@ class gltfCamera extends GltfObject { // Precompute the distances to avoid their computation during sorting. for (const drawable of drawables) { const modelView = mat4.create(); - mat4.multiply(modelView, this.getViewMatrix(gltf), drawable.node.worldTransform); + mat4.multiply( + modelView, + this.getViewMatrix(gltf), + drawable.node.getRenderedWorldTransform() + ); // Transform primitive centroid to find the primitive's depth. const pos = vec3.transformMat4( @@ -148,7 +152,7 @@ class gltfCamera extends GltfObject { getPosition(gltf) { const position = vec3.create(); const node = this.getNode(gltf); - mat4.getTranslation(position, node.worldTransform); + mat4.getTranslation(position, node.getRenderedWorldTransform()); return position; } @@ -195,7 +199,7 @@ class gltfCamera extends GltfObject { getTransformMatrix(gltf) { const node = this.getNode(gltf); - if (node === undefined || node.worldTransform === undefined) { + if (node === undefined || node.getRenderedWorldTransform() === undefined) { return mat4.create(); } diff --git a/source/gltf/gltf_utils.js b/source/gltf/gltf_utils.js index b3e6dfd6..6e8576c9 100644 --- a/source/gltf/gltf_utils.js +++ b/source/gltf/gltf_utils.js @@ -33,7 +33,7 @@ function getSceneExtents(gltf, sceneIndex, outMin, outMax) { const accessor = gltf.accessors[attribute.accessor]; const assetMin = vec3.create(); const assetMax = vec3.create(); - getExtentsFromAccessor(accessor, node.worldTransform, assetMin, assetMax); + getExtentsFromAccessor(accessor, node.getRenderedWorldTransform(), assetMin, assetMax); for (const i of [0, 1, 2]) { outMin[i] = Math.min(outMin[i], assetMin[i]); diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index c85bffdb..29bdc109 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -1,5 +1,6 @@ import { GltfObject } from "./gltf_object"; import * as interactivity from "@khronosgroup/gltf-interactivity-sample-engine"; +import { mat4 } from "gl-matrix"; class gltfGraph extends GltfObject { static animatedProperties = []; @@ -625,7 +626,7 @@ class SampleViewerDecorator extends interactivity.ADecorator { const nodeIndex = parseInt(pathParts[2]); const node = this.world.gltf.nodes[nodeIndex]; node.scene.applyTransformHierarchy(this.world.gltf); - return this.convertArrayToMatrix(node.worldTransform, 4); // gl-matrix uses column-major order + return this.convertArrayToMatrix(node.getRenderedWorldTransform(), 4); // gl-matrix uses column-major order }, (_path, _value) => {}, "float4x4", @@ -639,7 +640,18 @@ class SampleViewerDecorator extends interactivity.ADecorator { const pathParts = path.split("/"); const nodeIndex = parseInt(pathParts[2]); const node = this.world.gltf.nodes[nodeIndex]; - return this.convertArrayToMatrix(node.getLocalTransform(), 4); // gl-matrix uses column-major order + if (!node.physicsTransform) { + return this.convertArrayToMatrix(node.getLocalTransform(), 4); // gl-matrix uses column-major order + } + node.scene.applyTransformHierarchy(this.world.gltf); + const parentTransform = node.parentNode + ? node.parentNode.getRenderedWorldTransform() + : mat4.create(); + const parentInverse = mat4.create(); + mat4.invert(parentInverse, parentTransform); + const localTransform = mat4.create(); + mat4.multiply(localTransform, parentInverse, node.getRenderedWorldTransform()); + return this.convertArrayToMatrix(localTransform, 4); // gl-matrix uses column-major order }, (_path, _value) => {}, "float4x4", diff --git a/source/gltf/light.js b/source/gltf/light.js index 9e79091c..a3cc1787 100644 --- a/source/gltf/light.js +++ b/source/gltf/light.js @@ -26,7 +26,7 @@ class gltfLight extends GltfObject { } toUniform(node) { - const matrix = node?.worldTransform ?? mat4.identity; + const matrix = node?.getRenderedWorldTransform() ?? mat4.identity; // To extract a correct rotation, the scaling component must be eliminated. var scale = vec3.fromValues(1, 1, 1); diff --git a/source/gltf/node.js b/source/gltf/node.js index c7adb7a9..0038cb33 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -35,6 +35,7 @@ class gltfNode extends GltfObject { this.pickingColor = undefined; this.parentNode = undefined; this.scene = undefined; + this.physicsTransform = undefined; } // eslint-disable-next-line no-unused-vars @@ -163,6 +164,10 @@ class gltfNode extends GltfObject { this.scale ); } + + getRenderedWorldTransform() { + return this.physicsTransform ?? this.worldTransform; + } } class KHR_node_visibility extends GltfObject { diff --git a/source/gltf/skin.js b/source/gltf/skin.js index 4c044348..3e2a9a21 100644 --- a/source/gltf/skin.js +++ b/source/gltf/skin.js @@ -91,7 +91,7 @@ class gltfSkin extends GltfObject { for (const joint of this.joints) { const node = gltf.nodes[joint]; - let jointMatrix = mat4.clone(node.worldTransform); + let jointMatrix = mat4.clone(node.getRenderedWorldTransform()); if (ibmAccessorData !== null) { let ibm = jsToGlSlice(ibmAccessorData, i * 16, 16); From 19f7fab26aeb40de400a8cdb1894b27aadfab060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 20 Nov 2025 16:39:06 +0100 Subject: [PATCH 08/93] First running WIP version --- package-lock.json | 14 +++---- package.json | 4 +- source/GltfState/phyiscs_controller.js | 56 +++++++++++++------------- source/GltfView/gltf_view.js | 2 +- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5724b8da..ad8c7c3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "^2.7.0" + "physx-js-webidl": "2.6.2" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -3388,9 +3388,9 @@ } }, "node_modules/physx-js-webidl": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.0.tgz", - "integrity": "sha512-BSE0a3Ti0qAAbzkFEWGByzcbvfYtOipzy8S54uFjgK03E2Rojda14QRixWEs8Wl6tEkgjq2bnZQCUYeF/dHBBA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.6.2.tgz", + "integrity": "sha512-10JMdenjZCJgNVOvLsbIcNGDw1mIPu0sFU7G5KB6eQviu4T9Hua6o+MHWC5frva8Ey9BlYNBjQC42LhTnhYQug==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -7143,9 +7143,9 @@ "dev": true }, "physx-js-webidl": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.0.tgz", - "integrity": "sha512-BSE0a3Ti0qAAbzkFEWGByzcbvfYtOipzy8S54uFjgK03E2Rojda14QRixWEs8Wl6tEkgjq2bnZQCUYeF/dHBBA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.6.2.tgz", + "integrity": "sha512-10JMdenjZCJgNVOvLsbIcNGDw1mIPu0sFU7G5KB6eQviu4T9Hua6o+MHWC5frva8Ey9BlYNBjQC42LhTnhYQug==" }, "picomatch": { "version": "2.3.1", diff --git a/package.json b/package.json index 6a1df974..d8d56117 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "^2.7.0" + "physx-js-webidl": "2.6.2" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -59,4 +59,4 @@ "url": "https://github.com/KhronosGroup/glTF-Sample-Renderer/issues" }, "homepage": "https://github.com/KhronosGroup/glTF-Sample-Renderer/#readme" -} +} \ No newline at end of file diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 5c6f4e80..a21e42ce 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -21,6 +21,7 @@ class PhysicsController { this.enabled = false; this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; + this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize //TODO different scaled primitive colliders might need to be uniquely created //TODO PxShape needs to be recreated if collisionFilter differs @@ -114,6 +115,7 @@ class PhysicsController { if (!scene.nodes) { return; } + this.skipFrames = 2; const morphedNodeIndices = getMorphedNodeIndices(state.gltf); const result = getAnimatedIndices(state.gltf, "/nodes/", [ "translation", @@ -123,6 +125,7 @@ class PhysicsController { const animatedNodeIndices = result.animatedIndices; this.hasRuntimeAnimationTargets = result.runtimeChanges; const gatherRigidBodies = (nodeIndices, currentRigidBody) => { + let parentRigidBody = currentRigidBody; for (const nodeIndex of nodeIndices) { const node = state.gltf.nodes[nodeIndex]; const rigidBody = node.extensions?.KHR_physics_rigid_bodies; @@ -133,7 +136,7 @@ class PhysicsController { } else { this.dynamicActors.push(node); } - currentRigidBody = node; + parentRigidBody = node; } else if (currentRigidBody === undefined) { if (animatedNodeIndices.has(node.gltfObjectIndex)) { this.kinematicActors.push(node); @@ -152,7 +155,7 @@ class PhysicsController { } } } - gatherRigidBodies(node.children, currentRigidBody); + gatherRigidBodies(node.children, parentRigidBody); } }; gatherRigidBodies(scene.nodes, undefined); @@ -203,6 +206,10 @@ class PhysicsController { if (state === undefined) { return; } + if (this.skipFrames > 0) { + this.skipFrames -= 1; + return; + } this.applyAnimations(state); this.timeAccumulator += deltaTime; if ( @@ -511,7 +518,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { indexData.set(indexChunk, offset); offset += indexChunk.length; } - return { positionData, indexData }; + return { vertices: positionData, indices: indexData }; } createConvexMeshFromNode( @@ -520,8 +527,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create() ) { - const { positionData, indexData } = this.collectVerticesAndIndicesFromNode(gltf, node); - return this.createConvexMesh(positionData, indexData, scale, scaleAxis); + const { vertices, indices } = this.collectVerticesAndIndicesFromNode(gltf, node); + return this.createConvexMesh(vertices, indices, scale, scaleAxis); } createMeshFromNode(gltf, node, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { @@ -623,15 +630,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { geometry = this.simpleShapes[collider.geometry.shape]; } } else if (collider?.geometry?.node !== undefined) { + const node = gltf.nodes[collider.geometry.node]; if (collider.geometry.convexHull === true) { - geometry = this.createConvexMeshFromNode( - gltf, - collider.geometry.node, - scale, - scaleAxis - ); + geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); } else { - geometry = this.createMeshFromNode(gltf, collider.geometry.node, scale, scaleAxis); + geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); } } @@ -648,7 +651,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxMaterial.setFrictionCombine(this.mapCombineMode(gltfMaterial.frictionCombine)); } if (gltfMaterial.restitutionCombine !== undefined) { - physxMaterial.setRestitutionCombine( + physxMaterial.setRestitutionCombineMode( this.mapCombineMode(gltfMaterial.restitutionCombine) ); } @@ -681,8 +684,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const worldTransform = node.worldTransform; const translation = vec3.create(); mat4.getTranslation(translation, worldTransform); - const pos = new this.PhysX.PxVec3(translation.x, translation.y, translation.z); - const rotation = new this.PhysX.PxQuat(...node.worldRotation); + const pos = new this.PhysX.PxVec3(...translation); + const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); const pose = new this.PhysX.PxTransform(pos, rotation); let actor = null; if (type === "static") { @@ -736,9 +739,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ? new this.PhysX.PxVec3(...motion.centerOfMass) : new this.PhysX.PxVec3(0, 0, 0); if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.updateMassAndInertia(actor, 1.0, pose); + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pose); } else { - this.PhysX.PxRigidBodyExt.setMassAndUpdateInertia(actor, motion.mass, pose); + this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( + actor, + motion.mass, + pose + ); } this.PhysX.destroy(pose); } @@ -782,7 +789,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getTranslation(translation, offsetTransform); mat4.getRotation(rotation, offsetTransform); - const PxPos = new this.PhysX.PxVec3(translation.x, translation.y, translation.z); + + const PxPos = new this.PhysX.PxVec3(...translation); const PxRotation = new this.PhysX.PxQuat(...rotation); const pose = new this.PhysX.PxTransform(PxPos, PxRotation); shape.setLocalPose(pose); @@ -807,14 +815,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } }; - recurseShapes( - gltf, - node.extensions.KHR_physics_rigid_bodies.collider, - shapeFlags, - worldTransform, - mat4.create(), - true - ); + recurseShapes(gltf, node, shapeFlags, worldTransform, mat4.create(), true); this.PhysX.destroy(pos); this.PhysX.destroy(rotation); @@ -891,7 +892,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const [nodeIndex, actor] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; - if (node.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion && !motion.isKinematic) { const transform = actor.getGlobalPose(); const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); const rotation = quat.fromValues( diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index fd9d8f3b..871480cb 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -81,7 +81,7 @@ class GltfView { scene.applyTransformHierarchy(state.gltf); if (state.physicsController.playing && state.physicsController.enabled) { - state.physicsController.simulateStep(state, currentFrameTime - lastFrameTime); + state.physicsController.simulateStep(state, (currentFrameTime - lastFrameTime) / 1000); } this.renderer.drawScene(state, scene); From c16bd55f368e072bb069659d534b0a1be503875d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 21 Nov 2025 17:18:45 +0100 Subject: [PATCH 09/93] Add collider debug view --- source/GltfState/phyiscs_controller.js | 31 ++++++++++++++++++- source/Renderer/renderer.js | 42 ++++++++++++++++++++++++++ source/Renderer/shaders/simple.frag | 9 ++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 source/Renderer/shaders/simple.frag diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index a21e42ce..ad410e14 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -236,6 +236,13 @@ class PhysicsController { // TODO check if collider/physics properties have changed } } + + getDebugLineData() { + if (this.engine) { + return this.engine.getDebugLineData(); + } + return []; + } } class PhysicsInterface { @@ -789,7 +796,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getTranslation(translation, offsetTransform); mat4.getRotation(rotation, offsetTransform); - const PxPos = new this.PhysX.PxVec3(...translation); const PxRotation = new this.PhysX.PxQuat(...rotation); const pose = new this.PhysX.PxTransform(PxPos, PxRotation); @@ -867,6 +873,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(tmpVec); this.PhysX.destroy(sceneDesc); this.PhysX.destroy(shapeFlags); + this.scene.setVisualizationParameter(this.PhysX.eSCALE, 1); + this.scene.setVisualizationParameter(this.PhysX.eWORLD_AXES, 1); + this.scene.setVisualizationParameter(this.PhysX.eACTOR_AXES, 1); + this.scene.setVisualizationParameter(this.PhysX.eCOLLISION_SHAPES, 1); } applyTransformRecursively(gltf, node, parentTransform) { @@ -921,6 +931,25 @@ class NvidiaPhysicsInterface extends PhysicsInterface { stopSimulation() { // Implementation specific to Nvidia physics engine } + + getDebugLineData() { + if (!this.scene) { + return []; + } + const result = []; + const rb = this.scene.getRenderBuffer(); + for (let i = 0; i < rb.getNbLines(); i++) { + const line = this.PhysX.NativeArrayHelpers.prototype.getDebugLineAt(rb.getLines(), i); + + result.push(line.pos0.x); + result.push(line.pos0.y); + result.push(line.pos0.z); + result.push(line.pos1.x); + result.push(line.pos1.y); + result.push(line.pos1.z); + } + return result; + } } export { PhysicsController }; diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index 33b09add..ed1b0b68 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -20,6 +20,7 @@ import animationShader from "./shaders/animation.glsl"; import cubemapVertShader from "./shaders/cubemap.vert"; import cubemapFragShader from "./shaders/cubemap.frag"; import scatterShader from "./shaders/scatter.frag"; +import simpleFragShader from "./shaders/simple.frag"; import specularGlossinesShader from "./shaders/specular_glossiness.frag"; import { gltfLight } from "../gltf/light.js"; import { jsToGl } from "../gltf/utils.js"; @@ -70,6 +71,7 @@ class gltfRenderer { shaderSources.set("cubemap.vert", cubemapVertShader); shaderSources.set("cubemap.frag", cubemapFragShader); shaderSources.set("specular_glossiness.frag", specularGlossinesShader); + shaderSources.set("simple.frag", simpleFragShader); this.shaderCache = new ShaderCache(shaderSources, this.webGl); @@ -542,6 +544,7 @@ class gltfRenderer { } instanceWorldTransforms.push(instanceOffset); } + const scatterEnabled = this.scatterDrawables.length > 0 && state.renderingParameters.enabledExtensions.KHR_materials_volume_scatter && @@ -775,6 +778,45 @@ class gltfRenderer { fragDefines ); + // Physics debug view + if (state.physicsController.enabled && state.physicsController.playing) { + const lines = state.physicsController.getDebugLineData(); + if (lines.length !== 0) { + const vertexShader = "picking.vert"; + const fragmentShader = "simple.frag"; + const fragmentHash = this.shaderCache.selectShader(fragmentShader, []); + const vertexHash = this.shaderCache.selectShader(vertexShader, []); + this.shader = this.shaderCache.getShaderProgram(fragmentHash, vertexHash); + this.webGl.context.useProgram(this.shader.program); + this.shader.updateUniform("u_ViewProjectionMatrix", this.viewProjectionMatrix); + this.shader.updateUniform("u_ModelMatrix", mat4.create()); + this.shader.updateUniform("u_Color", vec4.fromValues(1.0, 0.0, 0.0, 1.0)); + const location = this.shader.getAttributeLocation("a_position"); + if (location !== null) { + const buffer = this.webGl.context.createBuffer(); + this.webGl.context.bindBuffer(this.webGl.context.ARRAY_BUFFER, buffer); + this.webGl.context.bufferData( + this.webGl.context.ARRAY_BUFFER, + new Float32Array(lines), + this.webGl.context.STATIC_DRAW + ); + this.webGl.context.vertexAttribPointer( + location, + 3, + this.webGl.context.FLOAT, + false, + 0, + 0 + ); + this.webGl.context.enableVertexAttribArray(location); + this.webGl.context.drawArrays(this.webGl.context.LINES, 0, lines.length / 3); + this.webGl.context.disableVertexAttribArray(location); + this.webGl.context.bindBuffer(this.webGl.context.ARRAY_BUFFER, null); + this.webGl.context.deleteBuffer(buffer); + } + } + } + let drawableCounter = 0; for (const instance of Object.values(this.opaqueDrawables)) { const drawable = instance[0]; diff --git a/source/Renderer/shaders/simple.frag b/source/Renderer/shaders/simple.frag new file mode 100644 index 00000000..a6750656 --- /dev/null +++ b/source/Renderer/shaders/simple.frag @@ -0,0 +1,9 @@ +precision highp float; + +out vec4 g_finalColor; + +uniform vec4 u_Color; + +void main() { + g_finalColor = vec4(u_Color); +} From b3ae6b7c7cbb57c5b5cdf8f67281caaf2b4c90ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 25 Nov 2025 18:22:52 +0100 Subject: [PATCH 10/93] Fix wrong colliders --- source/GltfState/phyiscs_controller.js | 44 ++++++++++---------------- source/geometry_generator.js | 7 ++-- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index ad410e14..77a1ec98 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -122,6 +122,7 @@ class PhysicsController { "rotation", "scale" ]); + let meshColliderCount = 0; const animatedNodeIndices = result.animatedIndices; this.hasRuntimeAnimationTargets = result.runtimeChanges; const gatherRigidBodies = (nodeIndices, currentRigidBody) => { @@ -145,6 +146,7 @@ class PhysicsController { } } if (rigidBody.collider?.geometry?.node !== undefined) { + meshColliderCount++; const colliderNodeIndex = rigidBody.collider.geometry.node; const colliderNode = state.gltf.nodes[colliderNodeIndex]; if (colliderNode.skin !== undefined) { @@ -172,7 +174,8 @@ class PhysicsController { this.staticActors, this.kinematicActors, this.dynamicActors, - this.hasRuntimeAnimationTargets + this.hasRuntimeAnimationTargets, + meshColliderCount ); } @@ -390,20 +393,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) { - if ( - quat.equals(scaleAxis, quat.create()) === false || - radiusTop !== radiusBottom || - scale[0] !== scale[2] - ) { - //TODO scale with rotation - const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices); - } - height *= scale[1]; - radiusTop *= scale[0]; - radiusBottom *= scale[0]; - - return new this.PhysX.PxCapsuleGeometry(radiusTop, height / 2); + //TODO scale with rotation + const data = createCapsuleVertexData(radiusTop, radiusBottom, height); + return this.createConvexMesh(data.vertices, data.indices); } generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) { @@ -458,6 +450,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { des.points.stride = vertices.BYTES_PER_ELEMENT * 3; des.points.count = vertices.length / 3; des.points.data = malloc(vertices); + let flag = 0; flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eCOMPUTE_CONVEX(); flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eQUANTIZE_INPUT(); @@ -619,6 +612,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { gltf, collider, shapeFlags, + noMeshShape = false, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create() ) { @@ -638,7 +632,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } else if (collider?.geometry?.node !== undefined) { const node = gltf.nodes[collider.geometry.node]; - if (collider.geometry.convexHull === true) { + if (collider.geometry.convexHull === true || noMeshShape === true) { geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); } else { geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); @@ -687,7 +681,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } - createActor(gltf, node, shapeFlags, type) { + createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { const worldTransform = node.worldTransform; const translation = vec3.create(); mat4.getTranslation(translation, worldTransform); @@ -787,7 +781,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const shape = this.createShape( gltf, node.extensions.KHR_physics_rigid_bodies.collider, - shapeFlags + shapeFlags, + noMeshShapes //scalingTransform ); @@ -805,14 +800,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(PxPos); this.PhysX.destroy(PxRotation); this.PhysX.destroy(pose); - - if ( - node.extensions.KHR_physics_rigid_bodies.collider?.geometry?.node !== undefined - ) { - const geometryNode = - gltf.nodes[node.extensions.KHR_physics_rigid_bodies.collider.geometry.node]; - recurseShapes(gltf, geometryNode, shapeFlags, scalingTransform, computedOffset); - } } for (const childIndex of node.children) { @@ -836,7 +823,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { staticActors, kinematicActors, dynamicActors, - hasRuntimeAnimationTargets + hasRuntimeAnimationTargets, + meshColliderCount ) { if (!this.PhysX) { return; @@ -867,7 +855,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.createActor(state.gltf, node, shapeFlags, "kinematic"); } for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, "dynamic"); + this.createActor(state.gltf, node, shapeFlags, "dynamic", meshColliderCount > 1); } this.PhysX.destroy(tmpVec); diff --git a/source/geometry_generator.js b/source/geometry_generator.js index e8ed3718..28718214 100644 --- a/source/geometry_generator.js +++ b/source/geometry_generator.js @@ -16,7 +16,8 @@ export function createCapsuleVertexData( const radialSegments = tessellation; const heightSegments = subdivisions; - const heightMinusCaps = height - (radiusTop + radiusBottom); + const totalHeight = height + radiusTop + radiusBottom; + const heightMinusCaps = height; const thetaStart = 0.0; const thetaLength = 2.0 * Math.PI; @@ -24,7 +25,7 @@ export function createCapsuleVertexData( const capsTopSegments = capDetail; const capsBottomSegments = capDetail; - const alpha = Math.acos((radiusBottom - radiusTop) / height); + const alpha = Math.acos((radiusBottom - radiusTop) / totalHeight); let indices = []; const vertices = []; @@ -70,7 +71,7 @@ export function createCapsuleVertexData( } const coneHeight = - height - radiusTop - radiusBottom + cosAlpha * radiusTop - cosAlpha * radiusBottom; + totalHeight - radiusTop - radiusBottom + cosAlpha * radiusTop - cosAlpha * radiusBottom; for (y = 1; y <= heightSegments; y++) { const indexRow = []; From cfb99f8098abb8df3357b2af80242e60a4b2c0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 26 Nov 2025 15:17:59 +0100 Subject: [PATCH 11/93] Fix recursive collider function --- source/GltfState/phyiscs_controller.js | 358 ++++++++++++++++++------- 1 file changed, 257 insertions(+), 101 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 77a1ec98..b8698e3d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -270,52 +270,39 @@ class PhysicsInterface { generateSimpleShape(shape, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { switch (shape.type) { case "box": - this.simpleShapes.push( - this.generateBox( - shape.box.size[0], - shape.box.size[1], - shape.box.size[2], - scale, - scaleAxis - ) + return this.generateBox( + shape.box.size[0], + shape.box.size[1], + shape.box.size[2], + scale, + scaleAxis ); - break; case "capsule": - this.simpleShapes.push( - this.generateCapsule( - shape.capsule.height, - shape.capsule.radiusTop, - shape.capsule.radiusBottom, - scale, - scaleAxis - ) + return this.generateCapsule( + shape.capsule.height, + shape.capsule.radiusTop, + shape.capsule.radiusBottom, + scale, + scaleAxis ); - break; case "cylinder": - this.simpleShapes.push( - this.generateCylinder( - shape.cylinder.height, - shape.cylinder.radiusTop, - shape.cylinder.radiusBottom, - scale, - scaleAxis - ) + return this.generateCylinder( + shape.cylinder.height, + shape.cylinder.radiusTop, + shape.cylinder.radiusBottom, + scale, + scaleAxis ); - break; case "sphere": - this.simpleShapes.push(this.generateSphere(shape.sphere.radius, scale, scaleAxis)); - break; + return this.generateSphere(shape.sphere.radius, scale, scaleAxis); case "plane": - this.simpleShapes.push( - this.generatePlane( - shape.plane.width, - shape.plane.height, - shape.plane.doubleSided, - scale, - scaleAxis - ) + return this.generatePlane( + shape.plane.width, + shape.plane.height, + shape.plane.doubleSided, + scale, + scaleAxis ); - break; } } @@ -325,7 +312,7 @@ class PhysicsInterface { return; } for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { - this.generateSimpleShape(shape); + this.simpleShapes.push(this.generateSimpleShape(shape)); } } } @@ -335,11 +322,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { super(); this.PhysX = undefined; this.physics = undefined; + this.defaultMaterial = undefined; + this.tolerances = undefined; + + // Needs to be reset for each scene this.scene = undefined; this.nodeToActor = new Map(); - this.defaultMaterial = new gltfPhysicsMaterial(); - this.tolerances = undefined; this.filterData = []; + this.physXFilterData = []; + this.physXMaterials = []; } async initializeEngine() { @@ -361,6 +352,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.tolerances = new this.PhysX.PxTolerancesScale(); this.physics = this.PhysX.CreatePhysics(version, foundation, this.tolerances); + this.defaultMaterial = this.createPhysXMaterial(new gltfPhysicsMaterial()); console.log("Created PxPhysics"); return this.PhysX; } @@ -608,75 +600,92 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + createPhysXMaterial(gltfPhysicsMaterial) { + if (gltfPhysicsMaterial === undefined) { + return this.defaultMaterial; + } + + const physxMaterial = this.physics.createMaterial( + gltfPhysicsMaterial.staticFriction, + gltfPhysicsMaterial.dynamicFriction, + gltfPhysicsMaterial.restitution + ); + if (gltfPhysicsMaterial.frictionCombine !== undefined) { + physxMaterial.setFrictionCombine( + this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) + ); + } + if (gltfPhysicsMaterial.restitutionCombine !== undefined) { + physxMaterial.setRestitutionCombineMode( + this.mapCombineMode(gltfPhysicsMaterial.restitutionCombine) + ); + } + return physxMaterial; + } + + createPhysXCollisionFilter(collisionFilter) { + let word0 = null; + let word1 = null; + if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { + word0 = 1 << collisionFilter; + word1 = this.filterData[collisionFilter]; + } else { + // Default filter id is signed bit and all bits set to collide with everything + word0 = Math.pow(2, 31); + word1 = Math.pow(2, 32) - 1; + } + + const additionalFlags = 0; + return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); + } + createShape( gltf, - collider, + node, shapeFlags, - noMeshShape = false, + physXMaterial, + physXFilterData, + convexHull, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create() ) { - let geometry = null; + const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + let geometry = undefined; if (collider?.geometry?.shape !== undefined) { - if ( - scale[0] !== 1 || - scale[1] !== 1 || - scale[2] !== 1 || - quat.equals(scaleAxis, quat.create()) === false - ) { + if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { const simpleShape = gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); } else { geometry = this.simpleShapes[collider.geometry.shape]; } - } else if (collider?.geometry?.node !== undefined) { - const node = gltf.nodes[collider.geometry.node]; - if (collider.geometry.convexHull === true || noMeshShape === true) { + } else if (node.mesh !== undefined) { + if (convexHull === true) { geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); } else { geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); } } - const gltfMaterial = collider.physicsMaterial - ? gltf.extensions.KHR_physics_rigid_bodies.physicsMaterials[collider.physicsMaterial] - : this.defaultMaterial; - - const physxMaterial = this.physics.createMaterial( - gltfMaterial.staticFriction, - gltfMaterial.dynamicFriction, - gltfMaterial.restitution - ); - if (gltfMaterial.frictionCombine !== undefined) { - physxMaterial.setFrictionCombine(this.mapCombineMode(gltfMaterial.frictionCombine)); - } - if (gltfMaterial.restitutionCombine !== undefined) { - physxMaterial.setRestitutionCombineMode( - this.mapCombineMode(gltfMaterial.restitutionCombine) - ); + if (geometry === undefined) { + return undefined; } - const shape = this.physics.createShape(geometry, physxMaterial, true, shapeFlags); - - let word0 = null; - let word1 = null; - if ( - collider?.collisionFilter !== undefined && - collider.collisionFilter < this.filterData.length - 1 - ) { - word0 = 1 << collider.collisionFilter; - word1 = this.filterData[collider.collisionFilter]; - } else { - // Default filter id is signed bit and all bits set to collide with everything - word0 = Math.pow(2, 31); - word1 = Math.pow(2, 32) - 1; + if (physXMaterial === undefined) { + if (collider?.physicsMaterial !== undefined) { + physXMaterial = this.physXMaterials[collider.physicsMaterial]; + } else { + physXMaterial = this.defaultMaterial; + } } + const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); - const additionalFlags = 0; - const filterData = new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); + if (physXFilterData === undefined) { + physXFilterData = + this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; + } - shape.setSimulationFilterData(filterData); + shape.setSimulationFilterData(physXFilterData); return shape; } @@ -762,12 +771,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { gltf, node, shapeFlags, + collider, shapeTransform, offsetTransform, - origin = false + isReferenceNode ) => { // Do not add other motion bodies' shapes to this actor - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined && !origin) { + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { return; } const scalingTransform = mat4.create(); @@ -777,19 +787,86 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const computedOffset = mat4.create(); mat4.multiply(computedOffset, offsetTransform, node.getLocalTransform()); - if (node.extensions?.KHR_physics_rigid_bodies?.collider !== undefined) { - const shape = this.createShape( - gltf, - node.extensions.KHR_physics_rigid_bodies.collider, - shapeFlags, - noMeshShapes - //scalingTransform - ); + const materialIndex = + node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? + collider?.physicsMaterial; + + const filterIndex = + node.extensions?.KHR_physics_rigid_bodies?.collider?.collisionFilter ?? + collider?.collisionFilter; + + const material = materialIndex + ? this.physXMaterials[materialIndex] + : this.defaultMaterial; + + const physXFilterData = filterIndex + ? this.physXFilterData[filterIndex] + : this.physXFilterData[this.physXFilterData.length - 1]; + + const isConvexHull = + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.convexHull ?? + collider?.geometry?.convexHull; + + const convexHull = noMeshShapes ? true : isConvexHull === true; + + if ( + isReferenceNode === false && + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined + ) { + if ( + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node !== + undefined + ) { + const colliderNodeIndex = + node.extensions.KHR_physics_rigid_bodies.collider.geometry.node; + const colliderNode = gltf.nodes[colliderNodeIndex]; + const referenceCollider = { + geometry: { convexHull: isConvexHull }, + physicsMaterial: materialIndex, + collisionFilter: filterIndex + }; + recurseShapes( + gltf, + colliderNode, + shapeFlags, + referenceCollider, + scalingTransform, + computedOffset, + true + ); + } + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + recurseShapes( + gltf, + childNode, + shapeFlags, + undefined, + scalingTransform, + computedOffset, + false + ); + } + return; + } + + const shape = this.createShape( + gltf, + node, + shapeFlags, + material, + physXFilterData, + convexHull, + vec3.fromValues(1, 1, 1), + quat.create() + ); + if (shape !== undefined) { const translation = vec3.create(); const rotation = quat.create(); - mat4.getTranslation(translation, offsetTransform); - mat4.getRotation(rotation, offsetTransform); + mat4.getTranslation(translation, computedOffset); + mat4.getRotation(rotation, computedOffset); const PxPos = new this.PhysX.PxVec3(...translation); const PxRotation = new this.PhysX.PxQuat(...rotation); @@ -804,11 +881,62 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; - recurseShapes(gltf, childNode, shapeFlags, scalingTransform, computedOffset); + recurseShapes( + gltf, + childNode, + shapeFlags, + collider, + scalingTransform, + computedOffset, + isReferenceNode + ); } }; - recurseShapes(gltf, node, shapeFlags, worldTransform, mat4.create(), true); + const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + const physxMaterial = collider?.physicsMaterial + ? this.physXMaterials[collider.physicsMaterial] + : this.defaultMaterial; + const physxFilterData = + this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; + + if (collider?.geometry?.node !== undefined) { + const colliderNode = gltf.nodes[collider.geometry.node]; + recurseShapes( + gltf, + colliderNode, + shapeFlags, + node.extensions?.KHR_physics_rigid_bodies?.collider, + worldTransform, + mat4.create(), + true + ); + } else if (collider?.geometry?.shape !== undefined) { + const shape = this.createShape( + gltf, + node, + shapeFlags, + physxMaterial, + physxFilterData, + true + ); + if (shape !== undefined) { + actor.attachShape(shape); + } + } + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + recurseShapes( + gltf, + childNode, + shapeFlags, + undefined, + worldTransform, + mat4.create(), + false + ); + } this.PhysX.destroy(pos); this.PhysX.destroy(rotation); @@ -832,8 +960,21 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (this.scene) { this.stopSimulation(); } + this.resetSimulation(); this.generateSimpleShapes(state.gltf); this.computeFilterData(state.gltf); + for (let i = 0; i < this.filterData.length; i++) { + const physXFilterData = this.createPhysXCollisionFilter(i); + this.physXFilterData.push(physXFilterData); + } + + const materials = state.gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials !== undefined) { + for (const gltfMaterial of materials) { + const physxMaterial = this.createPhysXMaterial(gltfMaterial); + this.physXMaterials.push(physxMaterial); + } + } const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); @@ -914,7 +1055,22 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } resetSimulation() { - // Implementation specific to Nvidia physics engine + this.filterData = []; + for (const physXFilterData of this.physXFilterData) { + this.PhysX.destroy(physXFilterData); + } + this.physXFilterData = []; + + for (const material of this.physXMaterials) { + material.release(); + } + this.physXMaterials = []; + + this.nodeToActor.clear(); + if (this.scene) { + this.scene.release(); + this.scene = undefined; + } } stopSimulation() { // Implementation specific to Nvidia physics engine From 55c57c1b64629ba11fd1a38ea9817700b96325cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 27 Nov 2025 18:09:01 +0100 Subject: [PATCH 12/93] WIP non-uniform scaling --- source/GltfState/phyiscs_controller.js | 224 +++++++++++++++++++++---- source/geometry_generator.js | 135 +++++++++++++-- 2 files changed, 312 insertions(+), 47 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index b8698e3d..ad15d7ab 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -4,8 +4,12 @@ import PhysX from "physx-js-webidl"; // eslint-disable-next-line no-unused-vars import PhysXBinaryFile from "physx-js-webidl/physx-js-webidl.wasm"; import { gltfPhysicsMaterial } from "../gltf/rigid_bodies"; -import { createCapsuleVertexData, createCylinderVertexData } from "../geometry_generator"; -import { vec3, mat4, quat } from "gl-matrix"; +import { + createBoxVertexData, + createCapsuleVertexData, + createCylinderVertexData +} from "../geometry_generator"; +import { vec3, mat4, quat, mat3 } from "gl-matrix"; class PhysicsController { constructor() { @@ -373,8 +377,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } generateBox(x, y, z, scale, scaleAxis) { - if (quat.equals(scaleAxis, quat.create()) === false) { - //TODO scale with rotation + if ( + scale.every((value) => value === scale[0]) === false && + quat.equals(scaleAxis, quat.create()) === false + ) { + const data = createBoxVertexData(x, y, z); + return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } const geometry = new this.PhysX.PxBoxGeometry( (x / 2) * scale[0], @@ -387,18 +395,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) { //TODO scale with rotation const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices); + return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) { if ( - quat.equals(scaleAxis, quat.create()) === false || + (quat.equals(scaleAxis, quat.create()) === false && + scale.every((value) => value === scale[0]) === false) || radiusTop !== radiusBottom || scale[0] !== scale[2] ) { //TODO scale with rotation const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices); + return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } height *= scale[1]; radiusTop *= scale[0]; @@ -408,11 +417,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } generateSphere(radius, scale, scaleAxis) { - if ( - scale.every((value) => value === scale[0]) === false || - quat.equals(scaleAxis, quat.create()) === false - ) { + if (scale.every((value) => value === scale[0]) === false) { //TODO + const data = createCapsuleVertexData(radius, radius, 0, scale, scaleAxis); + return this.createConvexMesh(data.vertices, data.indices); } else { radius *= scale[0]; } @@ -466,16 +474,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } - createTriangleMesh( - vertices, - indices, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create() - ) { - const geometry = new this.PhysX.PxTriangleMeshGeometry(vertices, indices, scale, scaleAxis); - return geometry; - } - collectVerticesAndIndicesFromNode(gltf, node) { // TODO Handle different primitive modes const mesh = gltf.meshes[node.mesh]; @@ -691,6 +689,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { + let parentNode = node; + while (parentNode.parentNode !== undefined) { + parentNode = parentNode.parentNode; + } const worldTransform = node.worldTransform; const translation = vec3.create(); mat4.getTranslation(translation, worldTransform); @@ -781,8 +783,17 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return; } const scalingTransform = mat4.create(); + console.log( + "Node scale for physics shape:", + node.gltfObjectIndex, + node.scale, + shapeTransform + ); mat4.fromScaling(scalingTransform, node.scale); + const rotMat = mat4.create(); + mat4.fromQuat(rotMat, node.rotation); mat4.multiply(scalingTransform, shapeTransform, scalingTransform); + mat4.multiply(scalingTransform, scalingTransform, rotMat); const computedOffset = mat4.create(); mat4.multiply(computedOffset, offsetTransform, node.getLocalTransform()); @@ -851,6 +862,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return; } + const localScale = vec3.create(); + mat4.getScaling(localScale, scalingTransform); + + const localScaleQuat = quat.create(); + mat4.getRotation(localScaleQuat, scalingTransform); + const shape = this.createShape( gltf, node, @@ -858,8 +875,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { material, physXFilterData, convexHull, - vec3.fromValues(1, 1, 1), - quat.create() + localScale, + localScaleQuat ); if (shape !== undefined) { @@ -873,6 +890,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const pose = new this.PhysX.PxTransform(PxPos, PxRotation); shape.setLocalPose(pose); + console.log( + "Attaching shape to actor", + node.gltfObjectIndex, + translation, + rotation, + localScale + ); + actor.attachShape(shape); this.PhysX.destroy(PxPos); this.PhysX.destroy(PxRotation); @@ -900,6 +925,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const physxFilterData = this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; + const worldScale = vec3.create(); + mat4.getScaling(worldScale, worldTransform); + + const scalingTransform = mat4.create(); + mat4.fromScaling(scalingTransform, worldScale); + if (collider?.geometry?.node !== undefined) { const colliderNode = gltf.nodes[collider.geometry.node]; recurseShapes( @@ -907,20 +938,71 @@ class NvidiaPhysicsInterface extends PhysicsInterface { colliderNode, shapeFlags, node.extensions?.KHR_physics_rigid_bodies?.collider, - worldTransform, + scalingTransform, mat4.create(), true ); } else if (collider?.geometry?.shape !== undefined) { + console.warn("Create shape with", worldScale, parentNode.rotation); + + const childchildRotation = quat.create(); + const childNodeRot = node.rotation; + const parentScale = vec3.fromValues(2, 1, 1); + const childScale = vec3.fromValues(2, 1, 1); + + const childNodeInvertedRot = quat.create(); + quat.invert(childNodeInvertedRot, childNodeRot); + + const childScaleMat = mat4.create(); + mat4.fromScaling(childScaleMat, childScale); + const parentScaleMat = mat4.create(); + mat4.fromScaling(parentScaleMat, parentScale); + + const invRotMat = mat4.create(); + mat4.fromQuat(invRotMat, childNodeInvertedRot); + + const invRotMatChildChild = mat4.create(); + mat4.fromQuat(invRotMatChildChild, childchildRotation); + + const transformedMat = mat4.create(); + mat4.multiply(transformedMat, invRotMatChildChild, childScaleMat); + mat4.multiply(transformedMat, transformedMat, parentScaleMat); + + const searchedScaleRotation = quat.create(); + quat.multiply(searchedScaleRotation, childNodeRot, childchildRotation); + + const searchedScale = vec3.create(); + mat4.getScaling(searchedScale, transformedMat); + + const searchedScaleRotationInverted = quat.create(); + quat.invert(searchedScaleRotationInverted, searchedScaleRotation); + + const expectedScale = vec3.create(); + mat4.getScaling(expectedScale, node.worldTransform); + const expectedRot = quat.create(); + mat4.getRotation(expectedRot, node.worldTransform); + + //vec3.transformQuat(expectedScale, expectedScale, invRot); + //vec3.multiply(expectedScale, expectedScale, childScale); + console.warn("Expected result:", expectedScale, node.rotation); + const shape = this.createShape( gltf, node, shapeFlags, physxMaterial, physxFilterData, - true + true, + node.scale ); if (shape !== undefined) { + const PxPos = new this.PhysX.PxVec3(0, 0, 0); + const PxRotation = new this.PhysX.PxQuat(...searchedScaleRotationInverted); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + //shape.setLocalPose(pose); + this.PhysX.destroy(PxPos); + this.PhysX.destroy(PxRotation); + this.PhysX.destroy(pose); actor.attachShape(shape); } } @@ -932,7 +1014,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { childNode, shapeFlags, undefined, - worldTransform, + scalingTransform, mat4.create(), false ); @@ -1041,12 +1123,94 @@ class NvidiaPhysicsInterface extends PhysicsInterface { transform.q.z, transform.q.w ); - node.physicsTransform = mat4.fromRotationTranslation( - mat4.create(), - rotation, - position + + /*const absoluteScale = vec3.create(); + mat4.getScaling(absoluteScale, node.worldTransform); + + const parentTransform = node.parentNode + ? node.parentNode.worldTransform + : mat4.create(); + const localTransform = mat4.create(); + + const parentInverse = mat4.create(); + mat4.invert(parentInverse, parentTransform); + + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, rotation, position); + console.log("Physics world transform:", physicsTransform, position, rotation); + + mat4.multiply(localTransform, parentInverse, physicsTransform); + mat4.scale(localTransform, localTransform, absoluteScale); + + const localRotation = quat.create(); + const localPosition = vec3.create(); + mat4.getRotation(localRotation, localTransform); + mat4.getTranslation(localPosition, localTransform); + + mat4.fromScaling(localTransform, node.scale); + const rotMat = mat4.create(); + mat4.fromQuat(rotMat, localRotation); + mat4.multiply(localTransform, rotMat, localTransform); + localTransform[12] = localPosition[0]; + localTransform[13] = localPosition[1]; + localTransform[14] = localPosition[2]; + + mat4.fromRotationTranslationScale( + localTransform, + localRotation, + localPosition, + node.scale ); + mat4.multiply(physicsTransform, parentTransform, localTransform); + + console.log( + "Compare local transforms:", + localTransform, + node.getLocalTransform(), + mat4.equals(localTransform, node.getLocalTransform()) + ); + console.log("Compare world rotation:", rotation, node.worldQuaternion); + + const rotTest = quat.create(); + mat4.getRotation(rotTest, physicsTransform); + console.log("Extracted local rotation:", rotTest, rotation);*/ + + const rotationBetween = quat.create(); + + let parentNode = node; + while (parentNode.parentNode !== undefined) { + parentNode = parentNode.parentNode; + } + + quat.invert(rotationBetween, node.worldQuaternion); + quat.multiply(rotationBetween, rotation, rotationBetween); + console.log("Rotation difference:", rotationBetween); + + const rotMat = mat3.create(); + mat3.fromQuat(rotMat, rotationBetween); + + const scaleRot = mat3.create(); + mat3.fromMat4(scaleRot, node.worldTransform); + + mat3.multiply(scaleRot, rotMat, scaleRot); + + const physicsTransform = mat4.create(); + physicsTransform[0] = scaleRot[0]; + physicsTransform[1] = scaleRot[1]; + physicsTransform[2] = scaleRot[2]; + physicsTransform[4] = scaleRot[3]; + physicsTransform[5] = scaleRot[4]; + physicsTransform[6] = scaleRot[5]; + physicsTransform[8] = scaleRot[6]; + physicsTransform[9] = scaleRot[7]; + physicsTransform[10] = scaleRot[8]; + physicsTransform[12] = position[0]; + physicsTransform[13] = position[1]; + physicsTransform[14] = position[2]; + + node.physicsTransform = physicsTransform; + for (const childIndex of node.children) { const childNode = state.gltf.nodes[childIndex]; this.applyTransformRecursively(state.gltf, childNode, node.physicsTransform); diff --git a/source/geometry_generator.js b/source/geometry_generator.js index 28718214..8a30a30a 100644 --- a/source/geometry_generator.js +++ b/source/geometry_generator.js @@ -2,12 +2,14 @@ * Script based off of Babylon.js: https://github.com/BabylonJS/Babylon.js/blob/10428bb078689922ea643ec49c5240af38e05925/packages/dev/core/src/Meshes/Builders/capsuleBuilder.ts */ -import { vec3 } from "gl-matrix"; +import { vec3, quat } from "gl-matrix"; export function createCapsuleVertexData( radiusTop = 0.25, radiusBottom = 0.25, height = 1, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create(), subdivisions = 2, tessellation = 16, capSubdivisions = 6 @@ -40,6 +42,8 @@ export function createCapsuleVertexData( const cosAlpha = Math.cos(alpha); const sinAlpha = Math.sin(alpha); + const tmpVec = vec3.create(); + for (y = 0; y <= capsTopSegments; y++) { const indexRow = []; @@ -57,9 +61,18 @@ export function createCapsuleVertexData( const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); // vertex - vertices.push(_radius * sinTheta); - vertices.push(halfHeight + sinA * radiusTop); - vertices.push(_radius * cosTheta); + vec3.transformQuat( + tmpVec, + vec3.fromValues( + _radius * sinTheta, + halfHeight + sinA * radiusTop, + _radius * cosTheta + ), + scaleAxis + ); + vertices.push(tmpVec[0] * scale[0]); + vertices.push(tmpVec[1] * scale[1]); + vertices.push(tmpVec[2] * scale[2]); // save index of vertex in respective row indexRow.push(index); @@ -83,9 +96,19 @@ export function createCapsuleVertexData( const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); // vertex - vertices.push(_radius * sinTheta); - vertices.push(halfHeight + cosAlpha * radiusTop - (y * coneHeight) / heightSegments); - vertices.push(_radius * cosTheta); + + vec3.transformQuat( + tmpVec, + vec3.fromValues( + _radius * sinTheta, + halfHeight + cosAlpha * radiusTop - (y * coneHeight) / heightSegments, + _radius * cosTheta + ), + scaleAxis + ); + vertices.push(tmpVec[0] * scale[0]); + vertices.push(tmpVec[1] * scale[1]); + vertices.push(tmpVec[2] * scale[2]); // save index of vertex in respective row indexRow.push(index); @@ -109,9 +132,19 @@ export function createCapsuleVertexData( const sinTheta = Math.sin(theta); const cosTheta = Math.cos(theta); // vertex - vertices.push(_radius * sinTheta); - vertices.push(-halfHeight + sinA * radiusBottom); - vertices.push(_radius * cosTheta); + + vec3.transformQuat( + tmpVec, + vec3.fromValues( + _radius * sinTheta, + -halfHeight + sinA * radiusBottom, + _radius * cosTheta + ), + scaleAxis + ); + vertices.push(tmpVec[0] * scale[0]); + vertices.push(tmpVec[1] * scale[1]); + vertices.push(tmpVec[2] * scale[2]); // save index of vertex in respective row indexRow.push(index); @@ -145,19 +178,87 @@ export function createCapsuleVertexData( return { vertices: new Float32Array(vertices), indices: new Uint8Array(indices) }; } -export function createCylinderVertexData(radiusTop, radiusBottom, height, numDivisions = 30) { +export function createCylinderVertexData( + radiusTop, + radiusBottom, + height, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create(), + numDivisions = 30 +) { const positions = []; + const tempVec = vec3.create(); for (let i = 0; i < numDivisions; i++) { const c = Math.cos((2 * Math.PI * i) / (numDivisions - 1)); const s = Math.sin((2 * Math.PI * i) / (numDivisions - 1)); - positions.push(c * radiusTop); - positions.push(0.5 * height); - positions.push(s * radiusTop); + vec3.transformQuat( + tempVec, + vec3.fromValues(c * radiusTop, 0.5 * height, s * radiusTop), + scaleAxis + ); + positions.push(tempVec[0] * scale[0]); + positions.push(tempVec[1] * scale[1]); + positions.push(tempVec[2] * scale[2]); - positions.push(c * radiusBottom); - positions.push(-0.5 * height); - positions.push(s * radiusBottom); + vec3.transformQuat( + tempVec, + vec3.fromValues(c * radiusBottom, -0.5 * height, s * radiusBottom), + scaleAxis + ); + positions.push(tempVec[0] * scale[0]); + positions.push(tempVec[1] * scale[1]); + positions.push(tempVec[2] * scale[2]); } const indices = Array.from(positions.keys()); return { vertices: new Float32Array(positions), indices: new Uint8Array(indices) }; } + +export function createBoxVertexData( + width, + height, + depth, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() +) { + const hw = width / 2; + const hh = height / 2; + const hd = depth / 2; + + const positions = []; + const tempVec = vec3.create(); + + // prettier-ignore + const boxVertices = [ + -hw, -hh, hd, + hw, -hh, hd, + hw, hh, hd, + -hw, hh, hd, + -hw, -hh, -hd, + -hw, hh, -hd, + hw, hh, -hd, + hw, -hh, -hd, + ]; + + for (let i = 0; i < boxVertices.length; i += 3) { + vec3.transformQuat( + tempVec, + vec3.fromValues(boxVertices[i], boxVertices[i + 1], boxVertices[i + 2]), + scaleAxis + ); + positions.push(tempVec[0] * scale[0]); + positions.push(tempVec[1] * scale[1]); + positions.push(tempVec[2] * scale[2]); + } + + // prettier-ignore + const indices = [ + 0, 1, 2, 0, 2, 3, // front + 4, 6, 5, 4, 7, 6, // back + 4, 5, 1, 4, 1, 0, // bottom + 3, 2, 6, 3, 6, 7, // top + 1, 5, 6, 1, 6, 2, // right + 4, 0, 3, 4, 3, 7, // left + ]; + + return { vertices: new Float32Array(positions), indices: new Uint8Array(indices) }; +} From 4cbfe22bc7fb26f2e05001549ffa50ffea8db864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 1 Dec 2025 12:28:35 +0100 Subject: [PATCH 13/93] WIP handle non-uniform scaling for simple shapes --- source/GltfState/phyiscs_controller.js | 121 ++++--------------------- 1 file changed, 19 insertions(+), 102 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index ad15d7ab..b4d3029e 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -943,48 +943,23 @@ class NvidiaPhysicsInterface extends PhysicsInterface { true ); } else if (collider?.geometry?.shape !== undefined) { - console.warn("Create shape with", worldScale, parentNode.rotation); - - const childchildRotation = quat.create(); - const childNodeRot = node.rotation; - const parentScale = vec3.fromValues(2, 1, 1); - const childScale = vec3.fromValues(2, 1, 1); - - const childNodeInvertedRot = quat.create(); - quat.invert(childNodeInvertedRot, childNodeRot); - - const childScaleMat = mat4.create(); - mat4.fromScaling(childScaleMat, childScale); - const parentScaleMat = mat4.create(); - mat4.fromScaling(parentScaleMat, parentScale); - - const invRotMat = mat4.create(); - mat4.fromQuat(invRotMat, childNodeInvertedRot); - - const invRotMatChildChild = mat4.create(); - mat4.fromQuat(invRotMatChildChild, childchildRotation); - - const transformedMat = mat4.create(); - mat4.multiply(transformedMat, invRotMatChildChild, childScaleMat); - mat4.multiply(transformedMat, transformedMat, parentScaleMat); - - const searchedScaleRotation = quat.create(); - quat.multiply(searchedScaleRotation, childNodeRot, childchildRotation); - - const searchedScale = vec3.create(); - mat4.getScaling(searchedScale, transformedMat); - - const searchedScaleRotationInverted = quat.create(); - quat.invert(searchedScaleRotationInverted, searchedScaleRotation); - - const expectedScale = vec3.create(); - mat4.getScaling(expectedScale, node.worldTransform); - const expectedRot = quat.create(); - mat4.getRotation(expectedRot, node.worldTransform); - - //vec3.transformQuat(expectedScale, expectedScale, invRot); - //vec3.multiply(expectedScale, expectedScale, childScale); - console.warn("Expected result:", expectedScale, node.rotation); + const scaleFactor = vec3.clone(node.scale); + let scaleRotation = quat.create(); + + let currentNode = node.parentNode; + const currentRotation = quat.clone(node.rotation); + + while (currentNode !== undefined) { + if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { + const localScale = currentNode.scale; + vec3.transformQuat(localScale, currentNode.scale, scaleRotation); + vec3.multiply(scaleFactor, scaleFactor, localScale); + scaleRotation = quat.clone(currentRotation); + } + const nextRotation = quat.clone(currentNode.rotation); + quat.multiply(currentRotation, currentRotation, nextRotation); + currentNode = currentNode.parentNode; + } const shape = this.createShape( gltf, @@ -993,16 +968,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxMaterial, physxFilterData, true, - node.scale + scaleFactor, + scaleRotation ); if (shape !== undefined) { - const PxPos = new this.PhysX.PxVec3(0, 0, 0); - const PxRotation = new this.PhysX.PxQuat(...searchedScaleRotationInverted); - const pose = new this.PhysX.PxTransform(PxPos, PxRotation); - //shape.setLocalPose(pose); - this.PhysX.destroy(PxPos); - this.PhysX.destroy(PxRotation); - this.PhysX.destroy(pose); actor.attachShape(shape); } } @@ -1124,58 +1093,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { transform.q.w ); - /*const absoluteScale = vec3.create(); - mat4.getScaling(absoluteScale, node.worldTransform); - - const parentTransform = node.parentNode - ? node.parentNode.worldTransform - : mat4.create(); - const localTransform = mat4.create(); - - const parentInverse = mat4.create(); - mat4.invert(parentInverse, parentTransform); - - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, rotation, position); - console.log("Physics world transform:", physicsTransform, position, rotation); - - mat4.multiply(localTransform, parentInverse, physicsTransform); - mat4.scale(localTransform, localTransform, absoluteScale); - - const localRotation = quat.create(); - const localPosition = vec3.create(); - mat4.getRotation(localRotation, localTransform); - mat4.getTranslation(localPosition, localTransform); - - mat4.fromScaling(localTransform, node.scale); - const rotMat = mat4.create(); - mat4.fromQuat(rotMat, localRotation); - mat4.multiply(localTransform, rotMat, localTransform); - localTransform[12] = localPosition[0]; - localTransform[13] = localPosition[1]; - localTransform[14] = localPosition[2]; - - mat4.fromRotationTranslationScale( - localTransform, - localRotation, - localPosition, - node.scale - ); - - mat4.multiply(physicsTransform, parentTransform, localTransform); - - console.log( - "Compare local transforms:", - localTransform, - node.getLocalTransform(), - mat4.equals(localTransform, node.getLocalTransform()) - ); - console.log("Compare world rotation:", rotation, node.worldQuaternion); - - const rotTest = quat.create(); - mat4.getRotation(rotTest, physicsTransform); - console.log("Extracted local rotation:", rotTest, rotation);*/ - const rotationBetween = quat.create(); let parentNode = node; From 7ed3fecc2b41cd47fd12b1a654d1d3c2c3fbf270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 1 Dec 2025 18:24:32 +0100 Subject: [PATCH 14/93] Add joint limits --- source/GltfState/phyiscs_controller.js | 190 ++++++++++++++++++++++++- source/gltf/rigid_bodies.js | 4 +- 2 files changed, 191 insertions(+), 3 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index b4d3029e..d9c3b8e1 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -17,6 +17,7 @@ class PhysicsController { this.staticActors = []; this.kinematicActors = []; this.dynamicActors = []; + this.jointNodes = []; this.morphedColliders = []; this.skinnedColliders = []; this.hasRuntimeAnimationTargets = false; @@ -160,6 +161,9 @@ class PhysicsController { this.morphedColliders.push(colliderNode); } } + if (rigidBody.joint !== undefined) { + this.jointNodes.push(node); + } } gatherRigidBodies(node.children, parentRigidBody); } @@ -178,6 +182,7 @@ class PhysicsController { this.staticActors, this.kinematicActors, this.dynamicActors, + this.jointNodes, this.hasRuntimeAnimationTargets, meshColliderCount ); @@ -258,7 +263,15 @@ class PhysicsInterface { } async initializeEngine() {} - initializeSimulation(state, staticActors, kinematicActors, dynamicActors) {} + initializeSimulation( + state, + staticActors, + kinematicActors, + dynamicActors, + jointNodes, + hasRuntimeAnimationTargets, + meshColliderCount + ) {} pauseSimulation() {} resumeSimulation() {} resetSimulation() {} @@ -332,9 +345,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // Needs to be reset for each scene this.scene = undefined; this.nodeToActor = new Map(); + this.nodeToJoint = new Map(); this.filterData = []; this.physXFilterData = []; this.physXMaterials = []; + + this.MAX_FLOAT = 3.4028234663852885981170418348452e38; } async initializeEngine() { @@ -997,11 +1013,178 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToActor.set(node.gltfObjectIndex, actor); } + computeJointOffsetAndActor(node) { + let currentNode = node; + while (currentNode !== undefined) { + if (currentNode.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + break; + } + currentNode = currentNode.parentNode; + } + if (currentNode === undefined) { + return { actor: undefined, offset: mat4.clone(node.worldTransform) }; + } + const actor = this.nodeToActor.get(currentNode.gltfObjectIndex); + const inverseActorTransform = mat4.create(); + mat4.invert(inverseActorTransform, currentNode.worldTransform); + const offset = mat4.create(); + mat4.multiply(offset, inverseActorTransform, node.worldTransform); + return { actor: actor, offset: offset }; + } + + createJoint(gltf, node) { + const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; + const referencedJoint = gltf.extensions?.KHR_physics_rigid_bodies?.joints[joint.joint]; + + if (referencedJoint === undefined) { + console.error("Referenced joint not found:", joint.joint); + return; + } + + const { actorA, offsetA } = this.computeJointOffsetAndActor(node); + const { actorB, offsetB } = this.computeJointOffsetAndActor( + gltf.nodes[joint.connectedNode] + ); + const positionA = vec3.create(); + mat4.getTranslation(positionA, offsetA); + const rotationA = quat.create(); + mat4.getRotation(rotationA, offsetA); + + const pos = new this.PhysX.PxVec3(...positionA); + const rot = new this.PhysX.PxQuat(...rotationA); + const poseA = new this.PhysX.PxTransform(pos, rot); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + + const positionB = vec3.create(); + mat4.getTranslation(positionB, offsetB); + const rotationB = quat.create(); + mat4.getRotation(rotationB, offsetB); + + const posB = new this.PhysX.PxVec3(...positionB); + const rotB = new this.PhysX.PxQuat(...rotationB); + const poseB = new this.PhysX.PxTransform(posB, rotB); + this.PhysX.destroy(posB); + this.PhysX.destroy(rotB); + + const physxJoint = this.PhysX.PxTopLevelFunctions.D6JointCreate( + this.physics, + actorA, + poseA, + actorB, + poseB + ); + this.PhysX.destroy(poseA); + this.PhysX.destroy(poseB); + + physxJoint.setConstraintFlag(this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, true); + + this.nodeToJoint.set(node.gltfObjectIndex, physxJoint); + + // Do not restict any axis by default + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); + + for (const limit of referencedJoint.limits) { + const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); + if (limit.linearAxes.length > 0) { + const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( + limit.min ?? -this.MAX_FLOAT, + limit.max ?? this.MAX_FLOAT, + spring + ); + if (limit.linearAxes.includes(1)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eX, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); + } + if (limit.linearAxes.includes(2)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eY, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); + } + if (limit.linearAxes.includes(3)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eZ, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); + } + this.PhysX.destroy(linearLimitPair); + } + if (limit.angularAxes.length > 0) { + const angularLimitPair = new this.PhysX.PxJointAngularLimitPair( + limit.min ?? -Math.PI / 2, + limit.max ?? Math.PI / 2, + spring + ); + if (limit.min && limit.min === limit.max) { + if (limit.angularAxes.includes(1)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eTWIST, + this.PhysX.PxD6MotionEnum.eLOCKED + ); + } + if (limit.angularAxes.includes(2)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING1, + this.PhysX.PxD6MotionEnum.eLOCKED + ); + } + if (limit.angularAxes.includes(3)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING2, + this.PhysX.PxD6MotionEnum.eLOCKED + ); + } + } else if (limit.angularAxes.includes(1)) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eTWIST, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setTwistLimit(angularLimitPair); + } else if (limit.angularAxes.includes(2) && limit.angularAxes.includes(3)) { + const jointLimitCone = new this.PhysX.PxJointLimitPyramid( + limit.min ?? -Math.PI / 2, + limit.max ?? Math.PI / 2, + limit.min ?? -Math.PI / 2, + limit.max ?? Math.PI / 2, + spring + ); + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING1, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING2, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setPyramidSwingLimit(jointLimitCone); + } + this.PhysX.destroy(angularLimitPair); + } + this.PhysX.destroy(spring); + } + + for (const drive of referencedJoint.drives) { + //TODO + } + } + initializeSimulation( state, staticActors, kinematicActors, dynamicActors, + jointNodes, hasRuntimeAnimationTargets, meshColliderCount ) { @@ -1049,6 +1232,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const node of dynamicActors) { this.createActor(state.gltf, node, shapeFlags, "dynamic", meshColliderCount > 1); } + for (const node of jointNodes) { + this.createJoint(state.gltf, node); + } this.PhysX.destroy(tmpVec); this.PhysX.destroy(sceneDesc); @@ -1057,6 +1243,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.scene.setVisualizationParameter(this.PhysX.eWORLD_AXES, 1); this.scene.setVisualizationParameter(this.PhysX.eACTOR_AXES, 1); this.scene.setVisualizationParameter(this.PhysX.eCOLLISION_SHAPES, 1); + this.scene.setVisualizationParameter(this.PhysX.eJOINT_LOCAL_FRAMES, 1); + this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, 1); } applyTransformRecursively(gltf, node, parentTransform) { diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js index eef90d6e..8101e9f3 100644 --- a/source/gltf/rigid_bodies.js +++ b/source/gltf/rigid_bodies.js @@ -61,7 +61,7 @@ class gltfPhysicsJointLimit extends GltfObject { super(); this.min = undefined; this.max = undefined; - this.stiffness = Infinity; + this.stiffness = undefined; this.damping = 0; this.linearAxes = undefined; this.angularAxes = undefined; @@ -81,7 +81,7 @@ class gltfPhysicsJointDrive extends GltfObject { this.type = undefined; this.mode = undefined; this.axis = undefined; - this.maxForce = Infinity; + this.maxForce = undefined; this.positionTarget = undefined; this.velocityTarget = undefined; this.stiffness = 0; From 5d9cdb788563732d67978b7b6c3bdd03155699dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 2 Dec 2025 18:03:07 +0100 Subject: [PATCH 15/93] Add joint drives and fix limits --- package-lock.json | 502 +++---------------------- package.json | 2 +- source/GltfState/phyiscs_controller.js | 200 +++++++--- 3 files changed, 210 insertions(+), 494 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad8c7c3f..1bf026e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.0", - "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-wasm": "^6.2.2", "@types/node": "^24.7.2", @@ -280,50 +280,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -381,16 +337,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@playwright/test": { "version": "1.56.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", @@ -407,17 +353,18 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "26.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz", - "integrity": "sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", + "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^10.4.1", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { "node": ">=16.0.0 || 14 >= 14.17" @@ -431,6 +378,35 @@ } } }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", @@ -2046,22 +2022,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -2141,26 +2101,6 @@ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2173,30 +2113,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "15.9.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", @@ -2719,21 +2635,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -2973,12 +2874,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -3118,15 +3013,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -3282,12 +3168,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, "node_modules/package-name-regex": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/package-name-regex/-/package-name-regex-2.0.6.tgz", @@ -3356,22 +3236,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", @@ -3962,18 +3826,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4134,27 +3986,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4194,19 +4025,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -4513,44 +4331,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -4837,37 +4617,6 @@ "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -4917,13 +4666,6 @@ "fastq": "^1.6.0" } }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, "@playwright/test": { "version": "1.56.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", @@ -4934,17 +4676,33 @@ } }, "@rollup/plugin-commonjs": { - "version": "26.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz", - "integrity": "sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", + "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", "dev": true, "requires": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^10.4.1", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } } }, "@rollup/plugin-node-resolve": { @@ -6088,16 +5846,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - } - }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -6155,40 +5903,6 @@ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, - "glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6626,16 +6340,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, "jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -6828,12 +6532,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, "magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -6945,12 +6643,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true - }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7064,12 +6756,6 @@ "p-limit": "^3.0.2" } }, - "package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, "package-name-regex": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/package-name-regex/-/package-name-regex-2.0.6.tgz", @@ -7120,16 +6806,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "requires": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - } - }, "path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", @@ -7553,12 +7229,6 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7712,25 +7382,6 @@ } } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - } - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7740,15 +7391,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -7996,36 +7638,6 @@ } } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index d8d56117..a081fdff 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.0", - "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-wasm": "^6.2.2", "@types/node": "^24.7.2", diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index d9c3b8e1..7ee54be6 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -727,12 +727,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (motion) { const gltfAngularVelocity = motion?.angularVelocity; const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); - actor.setAngularVelocity(angularVelocity); + actor.setAngularVelocity(angularVelocity, true); this.PhysX.destroy(angularVelocity); const gltfLinearVelocity = motion?.linearVelocity; const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); - actor.setLinearVelocity(linearVelocity); + actor.setLinearVelocity(linearVelocity, true); this.PhysX.destroy(linearVelocity); if (motion.mass !== undefined) { @@ -1016,7 +1016,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { computeJointOffsetAndActor(node) { let currentNode = node; while (currentNode !== undefined) { - if (currentNode.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { break; } currentNode = currentNode.parentNode; @@ -1025,62 +1025,89 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return { actor: undefined, offset: mat4.clone(node.worldTransform) }; } const actor = this.nodeToActor.get(currentNode.gltfObjectIndex); - const inverseActorTransform = mat4.create(); - mat4.invert(inverseActorTransform, currentNode.worldTransform); - const offset = mat4.create(); - mat4.multiply(offset, inverseActorTransform, node.worldTransform); - return { actor: actor, offset: offset }; + const inverseActorRotation = quat.create(); + quat.invert(inverseActorRotation, currentNode.worldQuaternion); + const offsetRotation = quat.create(); + quat.multiply(offsetRotation, inverseActorRotation, node.worldQuaternion); + + const actorPosition = vec3.create(); + mat4.getTranslation(actorPosition, currentNode.worldTransform); + const nodePosition = vec3.create(); + mat4.getTranslation(nodePosition, node.worldTransform); + const offsetPosition = vec3.create(); + vec3.subtract(offsetPosition, nodePosition, actorPosition); + + return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; + } + + convertAxisIndexToEnum(axisIndex, type) { + if (type === "linear") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eX; + case 1: + return this.PhysX.PxD6AxisEnum.eY; + case 2: + return this.PhysX.PxD6AxisEnum.eZ; + } + } else if (type === "angular") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eTWIST; + case 1: + return this.PhysX.PxD6AxisEnum.eSWING1; + case 2: + return this.PhysX.PxD6AxisEnum.eSWING2; + } + } + return null; } createJoint(gltf, node) { const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; - const referencedJoint = gltf.extensions?.KHR_physics_rigid_bodies?.joints[joint.joint]; + const referencedJoint = + gltf.extensions?.KHR_physics_rigid_bodies?.physicsJoints[joint.joint]; if (referencedJoint === undefined) { console.error("Referenced joint not found:", joint.joint); return; } - const { actorA, offsetA } = this.computeJointOffsetAndActor(node); - const { actorB, offsetB } = this.computeJointOffsetAndActor( - gltf.nodes[joint.connectedNode] - ); - const positionA = vec3.create(); - mat4.getTranslation(positionA, offsetA); - const rotationA = quat.create(); - mat4.getRotation(rotationA, offsetA); + const resultA = this.computeJointOffsetAndActor(node); + const resultB = this.computeJointOffsetAndActor(gltf.nodes[joint.connectedNode]); - const pos = new this.PhysX.PxVec3(...positionA); - const rot = new this.PhysX.PxQuat(...rotationA); + const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); + const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); const poseA = new this.PhysX.PxTransform(pos, rot); this.PhysX.destroy(pos); this.PhysX.destroy(rot); - const positionB = vec3.create(); - mat4.getTranslation(positionB, offsetB); - const rotationB = quat.create(); - mat4.getRotation(rotationB, offsetB); - - const posB = new this.PhysX.PxVec3(...positionB); - const rotB = new this.PhysX.PxQuat(...rotationB); + const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); + const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); const poseB = new this.PhysX.PxTransform(posB, rotB); this.PhysX.destroy(posB); this.PhysX.destroy(rotB); - const physxJoint = this.PhysX.PxTopLevelFunctions.D6JointCreate( + const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( this.physics, - actorA, + resultA.actor, poseA, - actorB, + resultB.actor, poseB ); this.PhysX.destroy(poseA); this.PhysX.destroy(poseB); + //TODO toogle debug view physxJoint.setConstraintFlag(this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, true); this.nodeToJoint.set(node.gltfObjectIndex, physxJoint); + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + joint.enableCollision + ); + // Do not restict any axis by default physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); @@ -1090,68 +1117,81 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); for (const limit of referencedJoint.limits) { + const lock = limit.min === 0 && limit.max === 0; const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); - if (limit.linearAxes.length > 0) { + if (limit.linearAxes && limit.linearAxes.length > 0) { const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( limit.min ?? -this.MAX_FLOAT, limit.max ?? this.MAX_FLOAT, spring ); - if (limit.linearAxes.includes(1)) { + if (limit.linearAxes.includes(0)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eX, - this.PhysX.PxD6MotionEnum.eLIMITED + lock + ? this.PhysX.PxD6MotionEnum.eLOCKED + : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); + if (!lock) { + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); + } } - if (limit.linearAxes.includes(2)) { + if (limit.linearAxes.includes(1)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eY, - this.PhysX.PxD6MotionEnum.eLIMITED + lock + ? this.PhysX.PxD6MotionEnum.eLOCKED + : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); + if (!lock) { + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); + } } - if (limit.linearAxes.includes(3)) { + if (limit.linearAxes.includes(2)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eZ, - this.PhysX.PxD6MotionEnum.eLIMITED + lock + ? this.PhysX.PxD6MotionEnum.eLOCKED + : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); + if (!lock) { + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); + } } this.PhysX.destroy(linearLimitPair); } - if (limit.angularAxes.length > 0) { + if (limit.angularAxes && limit.angularAxes.length > 0) { const angularLimitPair = new this.PhysX.PxJointAngularLimitPair( limit.min ?? -Math.PI / 2, limit.max ?? Math.PI / 2, spring ); - if (limit.min && limit.min === limit.max) { - if (limit.angularAxes.includes(1)) { + if (lock) { + if (limit.angularAxes.includes(0)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eLOCKED ); } - if (limit.angularAxes.includes(2)) { + if (limit.angularAxes.includes(1)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eLOCKED ); } - if (limit.angularAxes.includes(3)) { + if (limit.angularAxes.includes(2)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eLOCKED ); } - } else if (limit.angularAxes.includes(1)) { + } else if (limit.angularAxes.includes(0)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eLIMITED ); physxJoint.setTwistLimit(angularLimitPair); - } else if (limit.angularAxes.includes(2) && limit.angularAxes.includes(3)) { + } else if (limit.angularAxes.includes(1) && limit.angularAxes.includes(2)) { const jointLimitCone = new this.PhysX.PxJointLimitPyramid( limit.min ?? -Math.PI / 2, limit.max ?? Math.PI / 2, @@ -1174,9 +1214,71 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(spring); } + const positionTarget = vec3.fromValues(0, 0, 0); + const angleTarget = quat.create(); + const linearVelocityTarget = vec3.fromValues(0, 0, 0); + const angularVelocityTarget = vec3.fromValues(0, 0, 0); + for (const drive of referencedJoint.drives) { - //TODO + const physxDrive = new this.PhysX.PxD6JointDrive( + drive.stiffness, + drive.damping, + drive.maxForce ?? this.MAX_FLOAT, + drive.mode === "acceleration" + ); + if (drive.type === "linear") { + const axis = this.convertAxisIndexToEnum(drive.axis, "linear"); + physxJoint.setDrive(axis, physxDrive); + if (drive.positionTarget !== undefined) { + positionTarget[drive.axis] = drive.positionTarget; + } + if (drive.velocityTarget !== undefined) { + linearVelocityTarget[drive.axis] = drive.velocityTarget; + } + } else if (drive.type === "angular") { + if (drive.positionTarget !== undefined) { + switch (drive.axis) { + case 0: { + quat.rotateX(angleTarget, angleTarget, drive.positionTarget); + break; + } + case 1: { + quat.rotateY(angleTarget, angleTarget, drive.positionTarget); + break; + } + case 2: { + quat.rotateZ(angleTarget, angleTarget, drive.positionTarget); + break; + } + } + } + + if (drive.velocityTarget !== undefined) { + angularVelocityTarget[drive.axis] = drive.velocityTarget; + } + + const axis = this.convertAxisIndexToEnum(drive.axis, "angular"); + physxJoint.setDrive(axis, physxDrive); + } + this.PhysX.destroy(physxDrive); } + + const posTarget = new this.PhysX.PxVec3(...positionTarget); + const rotTarget = new this.PhysX.PxQuat(...angleTarget); + const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); + physxJoint.setDrivePosition(targetTransform); + + const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); + const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); + physxJoint.setDriveVelocity(linVel, angVel); + + this.PhysX.destroy(posTarget); + this.PhysX.destroy(rotTarget); + this.PhysX.destroy(linVel); + this.PhysX.destroy(angVel); + this.PhysX.destroy(targetTransform); + + return physxJoint; } initializeSimulation( @@ -1265,6 +1367,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (!this.scene) { return; } + + //TODO set custom gravity and kinematic forces before simulating + this.scene.simulate(deltaTime); this.scene.fetchResults(true); @@ -1290,7 +1395,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { quat.invert(rotationBetween, node.worldQuaternion); quat.multiply(rotationBetween, rotation, rotationBetween); - console.log("Rotation difference:", rotationBetween); const rotMat = mat3.create(); mat3.fromQuat(rotMat, rotationBetween); From ad9ec00cf87cf6a7e14d3f6dbe683069046136d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 3 Dec 2025 17:21:54 +0100 Subject: [PATCH 16/93] Fix angular limit --- source/GltfState/phyiscs_controller.js | 103 +++++++++++++++++++++---- 1 file changed, 90 insertions(+), 13 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 7ee54be6..eb83ca89 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1116,6 +1116,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); + let angularYLimit = undefined; + let angularZLimit = undefined; + for (const limit of referencedJoint.limits) { const lock = limit.min === 0 && limit.max === 0; const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); @@ -1178,12 +1181,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eLOCKED ); + angularYLimit = limit; } if (limit.angularAxes.includes(2)) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eLOCKED ); + angularZLimit = limit; } } else if (limit.angularAxes.includes(0)) { physxJoint.setMotion( @@ -1191,27 +1196,98 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxD6MotionEnum.eLIMITED ); physxJoint.setTwistLimit(angularLimitPair); - } else if (limit.angularAxes.includes(1) && limit.angularAxes.includes(2)) { - const jointLimitCone = new this.PhysX.PxJointLimitPyramid( - limit.min ?? -Math.PI / 2, - limit.max ?? Math.PI / 2, - limit.min ?? -Math.PI / 2, - limit.max ?? Math.PI / 2, - spring + } else if (limit.angularAxes.includes(1)) { + angularYLimit = limit; + } else if (limit.angularAxes.includes(2)) { + angularZLimit = limit; + } + this.PhysX.destroy(angularLimitPair); + } + this.PhysX.destroy(spring); + } + + if (angularYLimit !== undefined && angularZLimit !== undefined) { + if ( + angularYLimit.stiffness !== angularZLimit.stiffness || + angularYLimit.damping !== angularZLimit.damping + ) { + console.warn( + "PhysX does not support different stiffness/damping for swing limits." + ); + } else { + const spring = new this.PhysX.PxSpring( + angularYLimit.stiffness ?? 0, + angularYLimit.damping + ); + let yMin = -Math.PI / 2; + let yMax = Math.PI / 2; + let zMin = -Math.PI / 2; + let zMax = Math.PI / 2; + if (angularYLimit.min !== undefined) { + yMin = angularYLimit.min; + } + if (angularYLimit.max !== undefined) { + yMax = angularYLimit.max; + } + if (angularZLimit.min !== undefined) { + zMin = angularZLimit.min; + } + if (angularZLimit.max !== undefined) { + zMax = angularZLimit.max; + } + const jointLimitCone = new this.PhysX.PxJointLimitPyramid( + yMin, + yMax, + zMin, + zMax, + spring + ); + physxJoint.setPyramidSwingLimit(jointLimitCone); + this.PhysX.destroy(spring); + + if (yMin !== yMax) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING1, + this.PhysX.PxD6MotionEnum.eLIMITED ); + } + if (zMin !== zMax) { + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eSWING2, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + } + } + } else if (angularYLimit !== undefined || angularZLimit !== undefined) { + const singleLimit = angularYLimit ?? angularZLimit; + if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { + console.warn( + "PhysX requires symmetric limits for swing limits in single axis mode." + ); + } else { + const spring = new this.PhysX.PxSpring( + singleLimit.stiffness ?? 0, + singleLimit.damping + ); + const maxY = angularYLimit?.max ?? Math.PI / 2; + const maxZ = angularZLimit?.max ?? Math.PI / 2; + const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); + if (angularYLimit !== undefined) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eLIMITED ); + } + if (angularZLimit !== undefined) { physxJoint.setMotion( this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setPyramidSwingLimit(jointLimitCone); } - this.PhysX.destroy(angularLimitPair); + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(spring); + this.PhysX.destroy(jointLimitCone); } - this.PhysX.destroy(spring); } const positionTarget = vec3.fromValues(0, 0, 0); @@ -1237,17 +1313,18 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } else if (drive.type === "angular") { if (drive.positionTarget !== undefined) { + // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise switch (drive.axis) { case 0: { - quat.rotateX(angleTarget, angleTarget, drive.positionTarget); + quat.rotateX(angleTarget, angleTarget, -drive.positionTarget); break; } case 1: { - quat.rotateY(angleTarget, angleTarget, drive.positionTarget); + quat.rotateY(angleTarget, angleTarget, -drive.positionTarget); break; } case 2: { - quat.rotateZ(angleTarget, angleTarget, drive.positionTarget); + quat.rotateZ(angleTarget, angleTarget, -drive.positionTarget); break; } } From 335a93188ee93a9a63f012cd96f0f65b3df44a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 3 Dec 2025 17:25:39 +0100 Subject: [PATCH 17/93] Always set limits --- source/GltfState/phyiscs_controller.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index eb83ca89..45eb5b94 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1135,9 +1135,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - if (!lock) { - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); - } + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); } if (limit.linearAxes.includes(1)) { physxJoint.setMotion( @@ -1146,9 +1144,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - if (!lock) { - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); - } + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); } if (limit.linearAxes.includes(2)) { physxJoint.setMotion( @@ -1157,9 +1153,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - if (!lock) { - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); - } + physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); } this.PhysX.destroy(linearLimitPair); } @@ -1175,6 +1169,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eLOCKED ); + physxJoint.setTwistLimit(angularLimitPair); } if (limit.angularAxes.includes(1)) { physxJoint.setMotion( From 1aebd5f28ea493371fbdf11261bddbf16a6afd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 3 Dec 2025 17:26:30 +0100 Subject: [PATCH 18/93] Apply velocity to kinematic actors --- source/GltfState/phyiscs_controller.js | 113 ++++++++++++++++++++----- source/Renderer/renderer.js | 3 +- source/gltf/interactivity.js | 4 +- source/gltf/node.js | 4 +- source/gltf/scene.js | 1 + 5 files changed, 100 insertions(+), 25 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 45eb5b94..b235dfd1 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -23,7 +23,7 @@ class PhysicsController { this.hasRuntimeAnimationTargets = false; this.morphWeights = new Map(); this.playing = false; - this.enabled = false; + this.enabled = true; this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize @@ -225,7 +225,7 @@ class PhysicsController { this.applyAnimations(state); this.timeAccumulator += deltaTime; if ( - this.playing && + this.enabled && this.engine && state && this.timeAccumulator >= this.simulationStepTime @@ -238,8 +238,9 @@ class PhysicsController { applyAnimations(state) { for (const node of state.gltf.nodes) { // TODO set worldTransformUpdated in node when transform changes from animations/interactivity + // Find a good way to specify that the node is animated. Either with a flag or by setting physicsTransform to undefined if (node.worldTransformUpdated) { - node.physicsTransform = node.worldTransform; + node.scaledPhysicsTransform = undefined; if (this.engine) { this.engine.updateRigidBodyTransform(node); } @@ -1428,7 +1429,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const localTransform = node.getLocalTransform(); const globalTransform = mat4.create(); mat4.multiply(globalTransform, parentTransform, localTransform); - node.physicsTransform = globalTransform; + node.scaledPhysicsTransform = globalTransform; for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; this.applyTransformRecursively(gltf, childNode, globalTransform); @@ -1441,6 +1442,68 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } //TODO set custom gravity and kinematic forces before simulating + for (const [nodeIndex, actor] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + // TODO ignore if animated + if (motion && motion.isKinematic) { + const worldTransform = node.physicsTransform ?? node.worldTransform; + const targetPosition = vec3.create(); + const targetRotation = quat.create(); + if (motion.linearVelocity !== undefined) { + const linearVelocity = vec3.create(); + vec3.scale(linearVelocity, motion.linearVelocity, deltaTime); + targetPosition[0] = worldTransform[12] + linearVelocity[0]; + targetPosition[1] = worldTransform[13] + linearVelocity[1]; + targetPosition[2] = worldTransform[14] + linearVelocity[2]; + } + if (motion.angularVelocity !== undefined) { + // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise + const angularVelocity = quat.create(); + quat.rotateX( + angularVelocity, + angularVelocity, + -motion.angularVelocity[0] * deltaTime + ); + quat.rotateY( + angularVelocity, + angularVelocity, + -motion.angularVelocity[1] * deltaTime + ); + quat.rotateZ( + angularVelocity, + angularVelocity, + -motion.angularVelocity[2] * deltaTime + ); + let currentRotation = quat.create(); + if (node.physicsTransform !== undefined) { + mat4.getRotation(currentRotation, worldTransform); + } else { + currentRotation = node.worldQuaternion; + } + quat.multiply(targetRotation, angularVelocity, currentRotation); + } + const pos = new this.PhysX.PxVec3(...targetPosition); + const rot = new this.PhysX.PxQuat(...targetRotation); + const transform = new this.PhysX.PxTransform(pos, rot); + + actor.setKinematicTarget(transform); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(transform); + + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, targetRotation, targetPosition); + + const scaledPhysicsTransform = mat4.create(); + mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); + + node.physicsTransform = physicsTransform; + node.scaledPhysicsTransform = scaledPhysicsTransform; + } else if (motion && motion.gravityFactor !== 1.0) { + //TODO apply custom gravity + } + } this.scene.simulate(deltaTime); this.scene.fetchResults(true); @@ -1458,6 +1521,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { transform.q.w ); + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, rotation, position); + + node.physicsTransform = physicsTransform; + const rotationBetween = quat.create(); let parentNode = node; @@ -1476,25 +1544,28 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat3.multiply(scaleRot, rotMat, scaleRot); - const physicsTransform = mat4.create(); - physicsTransform[0] = scaleRot[0]; - physicsTransform[1] = scaleRot[1]; - physicsTransform[2] = scaleRot[2]; - physicsTransform[4] = scaleRot[3]; - physicsTransform[5] = scaleRot[4]; - physicsTransform[6] = scaleRot[5]; - physicsTransform[8] = scaleRot[6]; - physicsTransform[9] = scaleRot[7]; - physicsTransform[10] = scaleRot[8]; - physicsTransform[12] = position[0]; - physicsTransform[13] = position[1]; - physicsTransform[14] = position[2]; - - node.physicsTransform = physicsTransform; - + const scaledPhysicsTransform = mat4.create(); + scaledPhysicsTransform[0] = scaleRot[0]; + scaledPhysicsTransform[1] = scaleRot[1]; + scaledPhysicsTransform[2] = scaleRot[2]; + scaledPhysicsTransform[4] = scaleRot[3]; + scaledPhysicsTransform[5] = scaleRot[4]; + scaledPhysicsTransform[6] = scaleRot[5]; + scaledPhysicsTransform[8] = scaleRot[6]; + scaledPhysicsTransform[9] = scaleRot[7]; + scaledPhysicsTransform[10] = scaleRot[8]; + scaledPhysicsTransform[12] = position[0]; + scaledPhysicsTransform[13] = position[1]; + scaledPhysicsTransform[14] = position[2]; + + node.scaledPhysicsTransform = scaledPhysicsTransform; for (const childIndex of node.children) { const childNode = state.gltf.nodes[childIndex]; - this.applyTransformRecursively(state.gltf, childNode, node.physicsTransform); + this.applyTransformRecursively( + state.gltf, + childNode, + node.scaledPhysicsTransform + ); } } } diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index ed1b0b68..3726189a 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -779,7 +779,8 @@ class gltfRenderer { ); // Physics debug view - if (state.physicsController.enabled && state.physicsController.playing) { + //TODO make optional + if (state.physicsController.enabled) { const lines = state.physicsController.getDebugLineData(); if (lines.length !== 0) { const vertexShader = "picking.vert"; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index ac50fe42..677929a7 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -639,8 +639,8 @@ class SampleViewerDecorator extends interactivity.ADecorator { const pathParts = path.split("/"); const nodeIndex = parseInt(pathParts[2]); const node = this.world.gltf.nodes[nodeIndex]; - if (!node.physicsTransform) { - return this.convertArrayToMatrix(node.getLocalTransform(), 4); // gl-matrix uses column-major order + if (!node.scaledPhysicsTransform) { + return node.getLocalTransform(); } node.scene.applyTransformHierarchy(this.world.gltf); const parentTransform = node.parentNode diff --git a/source/gltf/node.js b/source/gltf/node.js index 0038cb33..73aa3126 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -27,6 +27,7 @@ class gltfNode extends GltfObject { // non gltf this.worldTransform = mat4.create(); this.worldQuaternion = quat.create(); + this.worldScale = vec3.create(); this.inverseWorldTransform = mat4.create(); this.normalMatrix = mat4.create(); this.light = undefined; @@ -36,6 +37,7 @@ class gltfNode extends GltfObject { this.parentNode = undefined; this.scene = undefined; this.physicsTransform = undefined; + this.scaledPhysicsTransform = undefined; } // eslint-disable-next-line no-unused-vars @@ -166,7 +168,7 @@ class gltfNode extends GltfObject { } getRenderedWorldTransform() { - return this.physicsTransform ?? this.worldTransform; + return this.scaledPhysicsTransform ?? this.worldTransform; } } diff --git a/source/gltf/scene.js b/source/gltf/scene.js index 4966be10..b9f1e605 100644 --- a/source/gltf/scene.js +++ b/source/gltf/scene.js @@ -19,6 +19,7 @@ class gltfScene extends GltfObject { mat4.multiply(node.worldTransform, parentTransform, node.getLocalTransform()); mat4.invert(node.inverseWorldTransform, node.worldTransform); mat4.transpose(node.normalMatrix, node.inverseWorldTransform); + mat4.getScaling(node.worldScale, node.worldTransform); if (node.instanceMatrices) { node.instanceWorldTransforms = []; From b8be89f484fb6dfbbfc0ce3e5f84f8b0ed50094a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 3 Dec 2025 18:28:27 +0100 Subject: [PATCH 19/93] Fix bug --- source/GltfState/phyiscs_controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index b235dfd1..16b07bd1 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1023,7 +1023,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { currentNode = currentNode.parentNode; } if (currentNode === undefined) { - return { actor: undefined, offset: mat4.clone(node.worldTransform) }; + const pos = vec3.create(); + mat4.getTranslation(pos, node.worldTransform); + return { actor: undefined, offsetPosition: pos, offsetRotation: node.worldQuaternion }; } const actor = this.nodeToActor.get(currentNode.gltfObjectIndex); const inverseActorRotation = quat.create(); From 08e086690f8751d3b549fea216b0a592eda1b9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 3 Dec 2025 18:28:44 +0100 Subject: [PATCH 20/93] Apply custom gravity --- source/GltfState/phyiscs_controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 16b07bd1..97df8985 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1503,7 +1503,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node.physicsTransform = physicsTransform; node.scaledPhysicsTransform = scaledPhysicsTransform; } else if (motion && motion.gravityFactor !== 1.0) { - //TODO apply custom gravity + const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); + actor.addForce(force); + this.PhysX.destroy(force); } } From c1097b69403f5abf3acd416987543851ecfefa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 4 Dec 2025 12:59:48 +0100 Subject: [PATCH 21/93] Improve mesh collider detection --- source/GltfState/phyiscs_controller.js | 95 ++++++++++++++++---------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 97df8985..65d6608d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -127,48 +127,66 @@ class PhysicsController { "rotation", "scale" ]); - let meshColliderCount = 0; + let dynamicMeshColliderCount = 0; + let staticMeshColliderCount = 0; const animatedNodeIndices = result.animatedIndices; this.hasRuntimeAnimationTargets = result.runtimeChanges; - const gatherRigidBodies = (nodeIndices, currentRigidBody) => { + const gatherRigidBodies = (nodeIndex, currentRigidBody) => { let parentRigidBody = currentRigidBody; - for (const nodeIndex of nodeIndices) { - const node = state.gltf.nodes[nodeIndex]; - const rigidBody = node.extensions?.KHR_physics_rigid_bodies; - if (rigidBody) { - if (rigidBody.motion) { - if (rigidBody.motion.isKinematic) { - this.kinematicActors.push(node); - } else { - this.dynamicActors.push(node); - } - parentRigidBody = node; - } else if (currentRigidBody === undefined) { - if (animatedNodeIndices.has(node.gltfObjectIndex)) { - this.kinematicActors.push(node); + const node = state.gltf.nodes[nodeIndex]; + const rigidBody = node.extensions?.KHR_physics_rigid_bodies; + if (rigidBody) { + if (rigidBody.motion) { + if (rigidBody.motion.isKinematic) { + this.kinematicActors.push(node); + } else { + this.dynamicActors.push(node); + } + parentRigidBody = node; + } else if (currentRigidBody === undefined) { + if (animatedNodeIndices.has(node.gltfObjectIndex)) { + this.kinematicActors.push(node); + } else { + this.staticActors.push(node); + } + } + if (rigidBody.collider?.geometry?.node !== undefined) { + if (!rigidBody.collider.geometry.convexHull) { + if ( + parentRigidBody === undefined || + parentRigidBody.extensions.KHR_physics_rigid_bodies.motion.isKinematic + ) { + staticMeshColliderCount++; } else { - this.staticActors.push(node); + if ( + currentRigidBody?.gltfObjectIndex !== + parentRigidBody.gltfObjectIndex + ) { + dynamicMeshColliderCount++; + } } } - if (rigidBody.collider?.geometry?.node !== undefined) { - meshColliderCount++; - const colliderNodeIndex = rigidBody.collider.geometry.node; - const colliderNode = state.gltf.nodes[colliderNodeIndex]; - if (colliderNode.skin !== undefined) { - this.skinnedColliders.push(colliderNode); - } - if (morphedNodeIndices.has(colliderNodeIndex)) { - this.morphedColliders.push(colliderNode); - } + const colliderNodeIndex = rigidBody.collider.geometry.node; + const colliderNode = state.gltf.nodes[colliderNodeIndex]; + if (colliderNode.skin !== undefined) { + this.skinnedColliders.push(colliderNode); } - if (rigidBody.joint !== undefined) { - this.jointNodes.push(node); + if (morphedNodeIndices.has(colliderNodeIndex)) { + this.morphedColliders.push(colliderNode); } } - gatherRigidBodies(node.children, parentRigidBody); + if (rigidBody.joint !== undefined) { + this.jointNodes.push(node); + } + } + for (const childIndex of node.children) { + gatherRigidBodies(childIndex, parentRigidBody); } }; - gatherRigidBodies(scene.nodes, undefined); + + for (const nodeIndex of scene.nodes) { + gatherRigidBodies(nodeIndex, undefined); + } if ( !this.engine || (this.staticActors.length === 0 && @@ -184,7 +202,8 @@ class PhysicsController { this.dynamicActors, this.jointNodes, this.hasRuntimeAnimationTargets, - meshColliderCount + staticMeshColliderCount, + dynamicMeshColliderCount ); } @@ -271,7 +290,8 @@ class PhysicsInterface { dynamicActors, jointNodes, hasRuntimeAnimationTargets, - meshColliderCount + staticMeshColliderCount, + dynamicMeshColliderCount ) {} pauseSimulation() {} resumeSimulation() {} @@ -1363,7 +1383,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { dynamicActors, jointNodes, hasRuntimeAnimationTargets, - meshColliderCount + staticMeshColliderCount, + dynamicMeshColliderCount ) { if (!this.PhysX) { return; @@ -1400,6 +1421,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxShapeFlagEnum.eVISUALIZATION ); + const alwaysConvexMeshes = + dynamicMeshColliderCount > 1 || + (staticMeshColliderCount > 0 && dynamicMeshColliderCount > 0); + for (const node of staticActors) { this.createActor(state.gltf, node, shapeFlags, "static"); } @@ -1407,7 +1432,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.createActor(state.gltf, node, shapeFlags, "kinematic"); } for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, "dynamic", meshColliderCount > 1); + this.createActor(state.gltf, node, shapeFlags, "dynamic", alwaysConvexMeshes); } for (const node of jointNodes) { this.createJoint(state.gltf, node); From 803966fda8325887da8ebb613bd26590ee6c7f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 4 Dec 2025 17:27:42 +0100 Subject: [PATCH 22/93] Calculate skinned collider --- source/GltfState/phyiscs_controller.js | 99 +++++++++++++++++++++++--- source/Renderer/renderer.js | 2 +- source/gltf/skin.js | 3 + 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 65d6608d..c5d19644 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -518,9 +518,79 @@ class NvidiaPhysicsInterface extends PhysicsInterface { let positionCount = 0; let indexDataArray = []; let indexCount = 0; + let skinData = undefined; + + if (node.skin !== undefined) { + const skin = gltf.skins[node.skin]; + if (skin.jointTextureData === undefined) { + skin.computeJoints(gltf); + } + skinData = skin.jointTextureData; + } + for (const primitive of mesh.primitives) { const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; - positionDataArray.push(positionAccessor.getNormalizedDeinterlacedView(gltf)); + const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + if (skinData !== undefined) { + // Apply skinning on CPU + const joints0Accessor = gltf.accessors[primitive.attributes.JOINTS_0]; + const weights0Accessor = gltf.accessors[primitive.attributes.WEIGHTS_0]; + const joints0Data = joints0Accessor.getDeinterlacedView(gltf); + const weights0Data = weights0Accessor.getNormalizedDeinterlacedView(gltf); + let joints1Data = undefined; + let weights1Data = undefined; + if ( + primitive.attributes.JOINTS_1 !== undefined && + primitive.attributes.WEIGHTS_1 !== undefined + ) { + const joints1Accessor = gltf.accessors[primitive.attributes.JOINTS_1]; + const weights1Accessor = gltf.accessors[primitive.attributes.WEIGHTS_1]; + joints1Data = joints1Accessor.getDeinterlacedView(gltf); + weights1Data = weights1Accessor.getNormalizedDeinterlacedView(gltf); + } + + for (let i = 0; i < positionData.length / 3; i++) { + let skinnedPosition = vec3.create(); + const originalPosition = vec3.fromValues( + positionData[i * 3], + positionData[i * 3 + 1], + positionData[i * 3 + 2] + ); + const skinningMatrix = mat4.create(); + for (let j = 0; j < 4; j++) { + const jointIndex = joints0Data[i * 4 + j]; + const weight = weights0Data[i * 4 + j]; + const jointMatrix = mat4.create(); + jointMatrix.set(skinData.slice(jointIndex * 32, jointIndex * 32 + 16)); + mat4.multiplyScalarAndAdd( + skinningMatrix, + skinningMatrix, + jointMatrix, + weight + ); + if (joints1Data !== undefined && weights1Data !== undefined && j >= 4) { + const joint1Index = joints1Data[i * 4 + (j - 4)]; + const weight1 = weights1Data[i * 4 + (j - 4)]; + const jointMatrix = mat4.create(); + jointMatrix.set( + skinData.slice(joint1Index * 32, joint1Index * 32 + 16) + ); + mat4.multiplyScalarAndAdd( + skinningMatrix, + skinningMatrix, + jointMatrix, + weight1 + ); + } + } + vec3.transformMat4(skinnedPosition, originalPosition, skinningMatrix); + positionData[i * 3] = skinnedPosition[0]; + positionData[i * 3 + 1] = skinnedPosition[1]; + positionData[i * 3 + 2] = skinnedPosition[2]; + } + } + + positionDataArray.push(positionData); positionCount += positionAccessor.count; if (primitive.indices !== undefined) { const indexAccessor = gltf.accessors[primitive.indices]; @@ -579,8 +649,18 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); - const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); - const PxQuat = new this.PhysX.PxQuat(...scaleAxis); + const PxScale = new this.PhysX.PxVec3(1, 1, 1); + const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); + // Skins ignore the the transforms of the nodes they are attached to + if (node.skin === undefined) { + PxScale.x = scale[0]; + PxScale.y = scale[1]; + PxScale.z = scale[2]; + PxQuat.x = scaleAxis[0]; + PxQuat.y = scaleAxis[1]; + PxQuat.z = scaleAxis[2]; + PxQuat.w = scaleAxis[3]; + } const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); const f = new this.PhysX.PxMeshGeometryFlags(); const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); @@ -694,11 +774,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } else { geometry = this.simpleShapes[collider.geometry.shape]; } - } else if (node.mesh !== undefined) { - if (convexHull === true) { - geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); - } else { - geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); + } else { + if (node.mesh !== undefined) { + if (convexHull === true && node.skin === undefined) { + geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); + } else { + geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); + } } } @@ -857,6 +939,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const convexHull = noMeshShapes ? true : isConvexHull === true; + // If current node is not a reference to a collider search this node and its children to find colliders if ( isReferenceNode === false && node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index 3726189a..5f49d37c 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -522,7 +522,7 @@ class gltfRenderer { mat4.multiply(this.viewProjectionMatrix, this.projMatrix, this.viewMatrix); // Update skins. - for (const node of this.nodes) { + for (const node of state.gltf.nodes) { if (node.mesh !== undefined && node.skin !== undefined) { this.updateSkin(state, node); } diff --git a/source/gltf/skin.js b/source/gltf/skin.js index 3e2a9a21..fa08894f 100644 --- a/source/gltf/skin.js +++ b/source/gltf/skin.js @@ -22,6 +22,7 @@ class gltfSkin extends GltfObject { // not gltf this.jointTextureInfo = undefined; this.jointWebGlTexture = undefined; + this.jointTextureData = undefined; } initGl(gltf, webGlContext) { @@ -107,6 +108,8 @@ class gltfSkin extends GltfObject { ++i; } + this.jointTextureData = textureData; + webGlContext.bindTexture(webGlContext.TEXTURE_2D, this.jointWebGlTexture); // Set texture format and upload data. let internalFormat = webGlContext.RGBA32F; From a67d2772fd10e543447171abcc21a0988e58e006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 4 Dec 2025 17:45:55 +0100 Subject: [PATCH 23/93] Calculate morphed colliders --- source/GltfState/phyiscs_controller.js | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index c5d19644..8922edbc 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -531,6 +531,36 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const primitive of mesh.primitives) { const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + + if (primitive.targets !== undefined) { + let morphWeights = node.weights ?? mesh.weights; + if (morphWeights !== undefined) { + // Calculate morphed vertex positions on CPU + const morphPositionData = []; + for (const target of primitive.targets) { + if (target.POSITION !== undefined) { + const morphAccessor = gltf.accessors[target.POSITION]; + morphPositionData.push( + morphAccessor.getNormalizedDeinterlacedView(gltf) + ); + } else { + morphPositionData.push(undefined); + } + } + for (let i = 0; i < positionData.length / 3; i++) { + for (let j = 0; j < morphWeights.length; j++) { + const morphData = morphPositionData[j]; + if (morphWeights[j] === 0 || morphData === undefined) { + continue; + } + positionData[i * 3] += morphData[i * 3] * morphWeights[j]; + positionData[i * 3 + 1] += morphData[i * 3 + 1] * morphWeights[j]; + positionData[i * 3 + 2] += morphData[i * 3 + 2] * morphWeights[j]; + } + } + } + } + if (skinData !== undefined) { // Apply skinning on CPU const joints0Accessor = gltf.accessors[primitive.attributes.JOINTS_0]; From 7bdc44ce0c38d58af182542545654b60568b5bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 5 Dec 2025 11:37:21 +0100 Subject: [PATCH 24/93] Add scene reset and cleanup --- source/GltfState/phyiscs_controller.js | 83 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 8922edbc..7bbf1f08 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -22,13 +22,13 @@ class PhysicsController { this.skinnedColliders = []; this.hasRuntimeAnimationTargets = false; this.morphWeights = new Map(); + this.playing = false; this.enabled = true; this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize - //TODO different scaled primitive colliders might need to be uniquely created //TODO PxShape needs to be recreated if collisionFilter differs //TODO Cache geometries for faster computation // PxShape has localTransform which applies to all actors using the shape @@ -109,7 +109,7 @@ class PhysicsController { } loadScene(state, sceneIndex) { - //TODO reset previous scene + this.resetScene(); if ( state.gltf.extensionsUsed === undefined || state.gltf.extensionsUsed.includes("KHR_physics_rigid_bodies") === false @@ -208,6 +208,15 @@ class PhysicsController { } resetScene() { + this.staticActors = []; + this.kinematicActors = []; + this.dynamicActors = []; + this.jointNodes = []; + this.morphedColliders = []; + this.skinnedColliders = []; + this.hasRuntimeAnimationTargets = false; + this.morphWeights.clear(); + this.timeAccumulator = 0; if (this.engine) { this.engine.resetSimulation(); } @@ -363,6 +372,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.defaultMaterial = undefined; this.tolerances = undefined; + this.reset = false; + // Needs to be reset for each scene this.scene = undefined; this.nodeToActor = new Map(); @@ -371,6 +382,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.physXFilterData = []; this.physXMaterials = []; + // Need for memory management + this.convexMeshes = []; + this.triangleMeshes = []; + this.MAX_FLOAT = 3.4028234663852885981170418348452e38; } @@ -392,6 +407,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { console.log("Created PxFoundation"); this.tolerances = new this.PhysX.PxTolerancesScale(); + this.tolerances.speed = 9.81; this.physics = this.PhysX.CreatePhysics(version, foundation, this.tolerances); this.defaultMaterial = this.createPhysXMaterial(new gltfPhysicsMaterial()); console.log("Created PxPhysics"); @@ -430,7 +446,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) { - //TODO scale with rotation const data = createCapsuleVertexData(radiusTop, radiusBottom, height); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } @@ -442,7 +457,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { radiusTop !== radiusBottom || scale[0] !== scale[2] ) { - //TODO scale with rotation const data = createCylinderVertexData(radiusTop, radiusBottom, height); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } @@ -455,9 +469,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { generateSphere(radius, scale, scaleAxis) { if (scale.every((value) => value === scale[0]) === false) { - //TODO - const data = createCapsuleVertexData(radius, radius, 0, scale, scaleAxis); - return this.createConvexMesh(data.vertices, data.indices); + const data = createCapsuleVertexData(radius, radius, 0); + return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } else { radius *= scale[0]; } @@ -496,6 +509,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { des.flags = pxflags; const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); const tri = this.PhysX.CreateConvexMesh(cookingParams, des); + this.convexMeshes.push(tri); const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); const PxQuat = new this.PhysX.PxQuat(...scaleAxis); @@ -678,6 +692,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); + this.triangleMeshes.push(tri); const PxScale = new this.PhysX.PxVec3(1, 1, 1); const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); @@ -913,7 +928,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (motion.gravityFactor !== 1.0) { actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); - //TODO Apply custom gravity in simulation step } } } @@ -1502,10 +1516,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (!this.PhysX) { return; } - if (this.scene) { - this.stopSimulation(); - } - this.resetSimulation(); this.generateSimpleShapes(state.gltf); this.computeFilterData(state.gltf); for (let i = 0; i < this.filterData.length; i++) { @@ -1545,7 +1555,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.createActor(state.gltf, node, shapeFlags, "kinematic"); } for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, "dynamic", alwaysConvexMeshes); + this.createActor(state.gltf, node, shapeFlags, "dynamic", true); } for (const node of jointNodes) { this.createJoint(state.gltf, node); @@ -1578,10 +1588,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { simulateStep(state, deltaTime) { if (!this.scene) { + this.reset = false; + return; + } + if (this.reset === true) { + this._resetSimulation(); + this.reset = false; return; } - //TODO set custom gravity and kinematic forces before simulating for (const [nodeIndex, actor] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; @@ -1712,7 +1727,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } } + resetSimulation() { + this.reset = true; + this.simulateStep({}, 0); + } + + _resetSimulation() { + const scenePointer = this.scene; + this.scene = undefined; this.filterData = []; for (const physXFilterData of this.physXFilterData) { this.PhysX.destroy(physXFilterData); @@ -1724,15 +1747,35 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } this.physXMaterials = []; + for (const shape of this.simpleShapes) { + shape.destroy?.(); + } + this.simpleShapes = []; + + for (const convexMesh of this.convexMeshes) { + convexMesh.release(); + } + this.convexMeshes = []; + + for (const triangleMesh of this.triangleMeshes) { + triangleMesh.release(); + } + this.triangleMeshes = []; + + for (const joint of this.nodeToJoint.values()) { + joint.release(); + } + this.nodeToJoint.clear(); + + for (const actor of this.nodeToActor.values()) { + actor.release(); + } + this.nodeToActor.clear(); - if (this.scene) { - this.scene.release(); - this.scene = undefined; + if (scenePointer) { + scenePointer.release(); } } - stopSimulation() { - // Implementation specific to Nvidia physics engine - } getDebugLineData() { if (!this.scene) { From f9b7b3b7a5f5ff618091274e1526bc9fd4b9458b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 5 Dec 2025 14:58:08 +0100 Subject: [PATCH 25/93] Fix pause --- source/GltfState/phyiscs_controller.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 7bbf1f08..9b7edc85 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -27,6 +27,7 @@ class PhysicsController { this.enabled = true; this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; + this.pauseTime = undefined; this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize //TODO PxShape needs to be recreated if collisionFilter differs @@ -238,6 +239,7 @@ class PhysicsController { } pauseSimulation() { + this.pauseTime = performance.now(); this.enabled = true; this.playing = false; } @@ -252,6 +254,12 @@ class PhysicsController { } this.applyAnimations(state); this.timeAccumulator += deltaTime; + if (this.pauseTime !== undefined) { + this.timeAccumulator = this.simulationStepTime; + if (this.playing) { + this.pauseTime = undefined; + } + } if ( this.enabled && this.engine && From 256c933f323f8eddae8383ad28f791961d57c3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 9 Dec 2025 16:28:54 +0100 Subject: [PATCH 26/93] Handle translation and scale correctly for referenced nodes --- source/GltfState/phyiscs_controller.js | 95 +++++++++++--------------- 1 file changed, 41 insertions(+), 54 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 9b7edc85..5d4887a5 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -945,29 +945,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node, shapeFlags, collider, - shapeTransform, - offsetTransform, + actorNode, + worldTransform, isReferenceNode ) => { // Do not add other motion bodies' shapes to this actor if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { return; } - const scalingTransform = mat4.create(); + console.log( "Node scale for physics shape:", node.gltfObjectIndex, node.scale, - shapeTransform + actorNode ); - mat4.fromScaling(scalingTransform, node.scale); - const rotMat = mat4.create(); - mat4.fromQuat(rotMat, node.rotation); - mat4.multiply(scalingTransform, shapeTransform, scalingTransform); - mat4.multiply(scalingTransform, scalingTransform, rotMat); - const computedOffset = mat4.create(); - mat4.multiply(computedOffset, offsetTransform, node.getLocalTransform()); + const computedWorldTransform = mat4.create(); + mat4.multiply(computedWorldTransform, worldTransform, node.getLocalTransform()); const materialIndex = node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? @@ -1013,8 +1008,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { colliderNode, shapeFlags, referenceCollider, - scalingTransform, - computedOffset, + actorNode, + computedWorldTransform, true ); } @@ -1026,19 +1021,38 @@ class NvidiaPhysicsInterface extends PhysicsInterface { childNode, shapeFlags, undefined, - scalingTransform, - computedOffset, + actorNode, + computedWorldTransform, false ); } return; } - const localScale = vec3.create(); - mat4.getScaling(localScale, scalingTransform); - - const localScaleQuat = quat.create(); - mat4.getRotation(localScaleQuat, scalingTransform); + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, computedWorldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + const offsetTransform = mat4.create(); + const inverseShapeTransform = mat4.create(); + mat4.invert(inverseShapeTransform, actorNode.worldTransform); + mat4.multiply(offsetTransform, inverseShapeTransform, computedWorldTransform); + mat4.getRotation(rotation, offsetTransform); + + // Calculate scale and scaleAxis + const scaleRotation = quat.create(); + //mat4.getRotation(scaleRotation, computedWorldTransform); + const scale = vec3.create(); + //mat4.getScaling(scale, computedWorldTransform); const shape = this.createShape( gltf, @@ -1047,29 +1061,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { material, physXFilterData, convexHull, - localScale, - localScaleQuat + scale, + scaleRotation ); if (shape !== undefined) { - const translation = vec3.create(); - const rotation = quat.create(); - mat4.getTranslation(translation, computedOffset); - mat4.getRotation(rotation, computedOffset); - const PxPos = new this.PhysX.PxVec3(...translation); const PxRotation = new this.PhysX.PxQuat(...rotation); const pose = new this.PhysX.PxTransform(PxPos, PxRotation); shape.setLocalPose(pose); - console.log( - "Attaching shape to actor", - node.gltfObjectIndex, - translation, - rotation, - localScale - ); - actor.attachShape(shape); this.PhysX.destroy(PxPos); this.PhysX.destroy(PxRotation); @@ -1083,8 +1084,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { childNode, shapeFlags, collider, - scalingTransform, - computedOffset, + actorNode, + computedWorldTransform, isReferenceNode ); } @@ -1097,12 +1098,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const physxFilterData = this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; - const worldScale = vec3.create(); - mat4.getScaling(worldScale, worldTransform); - - const scalingTransform = mat4.create(); - mat4.fromScaling(scalingTransform, worldScale); - if (collider?.geometry?.node !== undefined) { const colliderNode = gltf.nodes[collider.geometry.node]; recurseShapes( @@ -1110,8 +1105,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { colliderNode, shapeFlags, node.extensions?.KHR_physics_rigid_bodies?.collider, - scalingTransform, - mat4.create(), + node, + worldTransform, true ); } else if (collider?.geometry?.shape !== undefined) { @@ -1150,15 +1145,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; - recurseShapes( - gltf, - childNode, - shapeFlags, - undefined, - scalingTransform, - mat4.create(), - false - ); + recurseShapes(gltf, childNode, shapeFlags, undefined, node, worldTransform, false); } this.PhysX.destroy(pos); From 8037ed336d78b7b17888034862429947f5dd00da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 9 Dec 2025 18:03:33 +0100 Subject: [PATCH 27/93] Move scale calculation to own function --- source/GltfState/phyiscs_controller.js | 70 ++++++++++++++------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 5d4887a5..05d3254e 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -860,6 +860,33 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } + calculateScaleAndAxis(node, referencingNode = undefined) { + const referencedNodeIndex = + referencingNode?.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node; + const scaleFactor = vec3.clone(node.scale); + let scaleRotation = quat.create(); + + let currentNode = + node.gltfObjectIndex === referencedNodeIndex ? referencingNode : node.parentNode; + const currentRotation = quat.clone(node.rotation); + + while (currentNode !== undefined) { + if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { + const localScale = currentNode.scale; + vec3.transformQuat(localScale, currentNode.scale, scaleRotation); + vec3.multiply(scaleFactor, scaleFactor, localScale); + scaleRotation = quat.clone(currentRotation); + } + const nextRotation = quat.clone(currentNode.rotation); + quat.multiply(currentRotation, currentRotation, nextRotation); + currentNode = + currentNode.gltfObjectIndex === referencedNodeIndex + ? referencingNode + : currentNode.parentNode; + } + return { scale: scaleFactor, scaleAxis: scaleRotation }; + } + createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { let parentNode = node; while (parentNode.parentNode !== undefined) { @@ -947,7 +974,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { collider, actorNode, worldTransform, - isReferenceNode + referencingNode ) => { // Do not add other motion bodies' shapes to this actor if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { @@ -988,7 +1015,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // If current node is not a reference to a collider search this node and its children to find colliders if ( - isReferenceNode === false && + referencingNode === undefined && node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined ) { if ( @@ -1010,7 +1037,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { referenceCollider, actorNode, computedWorldTransform, - true + node ); } @@ -1023,7 +1050,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined, actorNode, computedWorldTransform, - false + undefined ); } return; @@ -1049,10 +1076,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getRotation(rotation, offsetTransform); // Calculate scale and scaleAxis - const scaleRotation = quat.create(); - //mat4.getRotation(scaleRotation, computedWorldTransform); - const scale = vec3.create(); - //mat4.getScaling(scale, computedWorldTransform); + const { scale, scaleAxis } = this.calculateScaleAndAxis(node, referencingNode); const shape = this.createShape( gltf, @@ -1062,7 +1086,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physXFilterData, convexHull, scale, - scaleRotation + scaleAxis ); if (shape !== undefined) { @@ -1086,7 +1110,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { collider, actorNode, computedWorldTransform, - isReferenceNode + referencingNode ); } }; @@ -1107,26 +1131,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node.extensions?.KHR_physics_rigid_bodies?.collider, node, worldTransform, - true + node ); } else if (collider?.geometry?.shape !== undefined) { - const scaleFactor = vec3.clone(node.scale); - let scaleRotation = quat.create(); - - let currentNode = node.parentNode; - const currentRotation = quat.clone(node.rotation); - - while (currentNode !== undefined) { - if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { - const localScale = currentNode.scale; - vec3.transformQuat(localScale, currentNode.scale, scaleRotation); - vec3.multiply(scaleFactor, scaleFactor, localScale); - scaleRotation = quat.clone(currentRotation); - } - const nextRotation = quat.clone(currentNode.rotation); - quat.multiply(currentRotation, currentRotation, nextRotation); - currentNode = currentNode.parentNode; - } + const { scale, scaleAxis } = this.calculateScaleAndAxis(node); const shape = this.createShape( gltf, @@ -1135,8 +1143,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxMaterial, physxFilterData, true, - scaleFactor, - scaleRotation + scale, + scaleAxis ); if (shape !== undefined) { actor.attachShape(shape); @@ -1145,7 +1153,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; - recurseShapes(gltf, childNode, shapeFlags, undefined, node, worldTransform, false); + recurseShapes(gltf, childNode, shapeFlags, undefined, node, worldTransform, undefined); } this.PhysX.destroy(pos); From 27856d1b27e9734e6b1fbf99ba58901d106ca4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 11:29:52 +0100 Subject: [PATCH 28/93] Adjust dirty flag --- source/GltfView/gltf_view.js | 1 + source/gltf/animatable_property.js | 25 ++----------------------- source/gltf/gltf_object.js | 6 ++++++ source/gltf/node.js | 7 ++++++- source/gltf/scene.js | 7 +++++++ 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index 926eb831..b7e60984 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -89,6 +89,7 @@ class GltfView { } this.renderer.drawScene(state, scene); + scene.resetHierarchyDirtyFlags(state.gltf); } /** diff --git a/source/gltf/animatable_property.js b/source/gltf/animatable_property.js index 93bcd43e..b159ad0e 100644 --- a/source/gltf/animatable_property.js +++ b/source/gltf/animatable_property.js @@ -6,16 +6,12 @@ class AnimatableProperty { } restAt(value) { - if (!this.dirty && !this._equals(value, this.restValue)) { - this.dirty = true; - } + this.dirty = true; this.restValue = value; } animate(value) { - if (!this.dirty && !this._equals(value, this.animatedValue)) { - this.dirty = true; - } + this.dirty = true; this.animatedValue = value; } @@ -33,23 +29,6 @@ class AnimatableProperty { isDefined() { return this.restValue !== undefined; } - - _equals(first, second) { - if (typeof first !== typeof second) { - return false; - } - // We do not have animatable objects and arrays always have the same length - if (Array.isArray(first) && Array.isArray(second)) { - for (let i = 0; i < first.length; i++) { - if (!this._equals(first[i], second[i])) { - return false; - } - } - return true; - } else { - return first === second; - } - } } const makeAnimatable = (object, json, properties) => { diff --git a/source/gltf/gltf_object.js b/source/gltf/gltf_object.js index e16dfcf6..4fddc9fc 100644 --- a/source/gltf/gltf_object.js +++ b/source/gltf/gltf_object.js @@ -37,6 +37,12 @@ class GltfObject { initGl(gltf, webGlContext) { initGlForMembers(this, gltf, webGlContext); } + + resetDirtyFlags() { + for (const prop in this.animatedPropertyObjects) { + this.animatedPropertyObjects[prop].dirty = false; + } + } } export { GltfObject }; diff --git a/source/gltf/node.js b/source/gltf/node.js index 5d48a4da..07a7ef50 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -38,6 +38,7 @@ class gltfNode extends GltfObject { this.scene = undefined; this.physicsTransform = undefined; this.scaledPhysicsTransform = undefined; + this.dirtyTransform = true; } // eslint-disable-next-line no-unused-vars @@ -170,8 +171,11 @@ class gltfNode extends GltfObject { getRenderedWorldTransform() { return this.scaledPhysicsTransform ?? this.worldTransform; } - + isTransformDirty() { + if (this.dirtyTransform) { + return true; + } for (const prop of ["rotation", "scale", "translation"]) { if (this.animatedPropertyObjects[prop].dirty) { return true; @@ -184,6 +188,7 @@ class gltfNode extends GltfObject { for (const prop of ["rotation", "scale", "translation"]) { this.animatedPropertyObjects[prop].dirty = false; } + this.dirtyTransform = false; } } diff --git a/source/gltf/scene.js b/source/gltf/scene.js index 41fd9b96..bd25848b 100644 --- a/source/gltf/scene.js +++ b/source/gltf/scene.js @@ -51,6 +51,13 @@ class gltfScene extends GltfObject { } } + resetHierarchyDirtyFlags(gltf) { + for (const nodeIndex of this.nodes) { + const node = gltf.nodes[nodeIndex]; + node.clearTransformDirty(); + } + } + gatherNodes(gltf, enabledExtensions) { const nodes = []; const selectableNodes = []; From c1cddb1008b63f5ee5671b4b2290f025c499a3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 17:08:02 +0100 Subject: [PATCH 29/93] Add drityScale --- source/gltf/node.js | 2 ++ source/gltf/scene.js | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/source/gltf/node.js b/source/gltf/node.js index 07a7ef50..8c836241 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -39,6 +39,7 @@ class gltfNode extends GltfObject { this.physicsTransform = undefined; this.scaledPhysicsTransform = undefined; this.dirtyTransform = true; + this.dirtyScale = true; } // eslint-disable-next-line no-unused-vars @@ -189,6 +190,7 @@ class gltfNode extends GltfObject { this.animatedPropertyObjects[prop].dirty = false; } this.dirtyTransform = false; + this.dirtyScale = false; } } diff --git a/source/gltf/scene.js b/source/gltf/scene.js index bd25848b..75fccfc7 100644 --- a/source/gltf/scene.js +++ b/source/gltf/scene.js @@ -15,15 +15,26 @@ class gltfScene extends GltfObject { } applyTransformHierarchy(gltf, rootTransform = mat4.create()) { - function applyTransform(gltf, node, parentTransform, parentRotation, parentDirty) { - const nodeDirty = parentDirty || node.isTransformDirty(); + function applyTransform( + gltf, + node, + parentTransform, + parentRotation, + parentDirty, + parentScaleDirty + ) { + const nodeDirty = parentDirty || node.isLocalTransformDirty(); + node.dirtyTransform = nodeDirty; + node.dirtyScale = false; if (nodeDirty) { mat4.multiply(node.worldTransform, parentTransform, node.getLocalTransform()); mat4.invert(node.inverseWorldTransform, node.worldTransform); mat4.transpose(node.normalMatrix, node.inverseWorldTransform); quat.multiply(node.worldQuaternion, parentRotation, node.rotation); mat4.getScaling(node.worldScale, node.worldTransform); - node.clearTransformDirty(); + if (parentScaleDirty || node.animatedPropertyObjects["scale"].dirty) { + node.dirtyScale = true; + } } if (nodeDirty && node.instanceMatrices) { @@ -42,12 +53,13 @@ class gltfScene extends GltfObject { gltf.nodes[child], node.worldTransform, node.worldQuaternion, - nodeDirty + nodeDirty, + node.dirtyScale ); } } for (const node of this.nodes) { - applyTransform(gltf, gltf.nodes[node], rootTransform, quat.create(), false); + applyTransform(gltf, gltf.nodes[node], rootTransform, quat.create(), false, false); } } From 6fa9063cff5e0756a6c23b56c69c8fd56a12bd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 17:08:55 +0100 Subject: [PATCH 30/93] Move functions to utils --- source/GltfState/phyiscs_controller.js | 333 +++++++++++++------------ 1 file changed, 179 insertions(+), 154 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 05d3254e..e86a1769 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -11,6 +11,163 @@ import { } from "../geometry_generator"; import { vec3, mat4, quat, mat3 } from "gl-matrix"; +class PhysicsUtils { + static calculateScaleAndAxis(node, referencingNode = undefined) { + const referencedNodeIndex = + referencingNode?.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node; + const scaleFactor = vec3.clone(node.scale); + let scaleRotation = quat.create(); + + let currentNode = + node.gltfObjectIndex === referencedNodeIndex ? referencingNode : node.parentNode; + const currentRotation = quat.clone(node.rotation); + + while (currentNode !== undefined) { + if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { + const localScale = currentNode.scale; + vec3.transformQuat(localScale, currentNode.scale, scaleRotation); + vec3.multiply(scaleFactor, scaleFactor, localScale); + scaleRotation = quat.clone(currentRotation); + } + const nextRotation = quat.clone(currentNode.rotation); + quat.multiply(currentRotation, currentRotation, nextRotation); + currentNode = + currentNode.gltfObjectIndex === referencedNodeIndex + ? referencingNode + : currentNode.parentNode; + } + return { scale: scaleFactor, scaleAxis: scaleRotation }; + } + + static recurseCollider( + gltf, + node, + collider, + actorNode, + worldTransform, + referencingNode, + customFunction, + args = [] + ) { + // Do not add other motion bodies' shapes to this actor + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; + } + + const computedWorldTransform = mat4.create(); + mat4.multiply(computedWorldTransform, worldTransform, node.getLocalTransform()); + + const materialIndex = + node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? + collider?.physicsMaterial; + + const filterIndex = + node.extensions?.KHR_physics_rigid_bodies?.collider?.collisionFilter ?? + collider?.collisionFilter; + + const material = materialIndex ? this.physXMaterials[materialIndex] : this.defaultMaterial; + + const physXFilterData = filterIndex + ? this.physXFilterData[filterIndex] + : this.physXFilterData[this.physXFilterData.length - 1]; + + const isConvexHull = + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.convexHull ?? + collider?.geometry?.convexHull; + + // If current node is not a reference to a collider search this node and its children to find colliders + if ( + referencingNode === undefined && + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined + ) { + if (node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node !== undefined) { + const colliderNodeIndex = + node.extensions.KHR_physics_rigid_bodies.collider.geometry.node; + const colliderNode = gltf.nodes[colliderNodeIndex]; + const referenceCollider = { + geometry: { convexHull: isConvexHull }, + physicsMaterial: materialIndex, + collisionFilter: filterIndex + }; + this.recurseCollider( + gltf, + colliderNode, + referenceCollider, + actorNode, + computedWorldTransform, + node, + customFunction, + args + ); + } + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.recurseCollider( + gltf, + childNode, + undefined, + actorNode, + computedWorldTransform, + undefined, + customFunction, + args + ); + } + return; + } + + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, computedWorldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + const offsetTransform = mat4.create(); + const inverseShapeTransform = mat4.create(); + mat4.invert(inverseShapeTransform, actorNode.worldTransform); + mat4.multiply(offsetTransform, inverseShapeTransform, computedWorldTransform); + mat4.getRotation(rotation, offsetTransform); + + // Calculate scale and scaleAxis + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + + customFunction( + gltf, + node, + material, + physXFilterData, + isConvexHull, + translation, + rotation, + scale, + scaleAxis, + ...args + ); + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.recurseCollider( + gltf, + childNode, + collider, + actorNode, + computedWorldTransform, + referencingNode, + customFunction, + args + ); + } + } +} + class PhysicsController { constructor() { this.engine = undefined; @@ -860,33 +1017,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } - calculateScaleAndAxis(node, referencingNode = undefined) { - const referencedNodeIndex = - referencingNode?.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node; - const scaleFactor = vec3.clone(node.scale); - let scaleRotation = quat.create(); - - let currentNode = - node.gltfObjectIndex === referencedNodeIndex ? referencingNode : node.parentNode; - const currentRotation = quat.clone(node.rotation); - - while (currentNode !== undefined) { - if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { - const localScale = currentNode.scale; - vec3.transformQuat(localScale, currentNode.scale, scaleRotation); - vec3.multiply(scaleFactor, scaleFactor, localScale); - scaleRotation = quat.clone(currentRotation); - } - const nextRotation = quat.clone(currentNode.rotation); - quat.multiply(currentRotation, currentRotation, nextRotation); - currentNode = - currentNode.gltfObjectIndex === referencedNodeIndex - ? referencingNode - : currentNode.parentNode; - } - return { scale: scaleFactor, scaleAxis: scaleRotation }; - } - createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { let parentNode = node; while (parentNode.parentNode !== undefined) { @@ -967,124 +1097,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - const recurseShapes = ( + const createAndAddShape = ( gltf, node, - shapeFlags, - collider, - actorNode, - worldTransform, - referencingNode + material, + physXFilterData, + isConvexHull, + translation, + rotation, + scale, + scaleAxis ) => { - // Do not add other motion bodies' shapes to this actor - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { - return; - } - - console.log( - "Node scale for physics shape:", - node.gltfObjectIndex, - node.scale, - actorNode - ); - - const computedWorldTransform = mat4.create(); - mat4.multiply(computedWorldTransform, worldTransform, node.getLocalTransform()); - - const materialIndex = - node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? - collider?.physicsMaterial; - - const filterIndex = - node.extensions?.KHR_physics_rigid_bodies?.collider?.collisionFilter ?? - collider?.collisionFilter; - - const material = materialIndex - ? this.physXMaterials[materialIndex] - : this.defaultMaterial; - - const physXFilterData = filterIndex - ? this.physXFilterData[filterIndex] - : this.physXFilterData[this.physXFilterData.length - 1]; - - const isConvexHull = - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.convexHull ?? - collider?.geometry?.convexHull; - - const convexHull = noMeshShapes ? true : isConvexHull === true; - - // If current node is not a reference to a collider search this node and its children to find colliders - if ( - referencingNode === undefined && - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined - ) { - if ( - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node !== - undefined - ) { - const colliderNodeIndex = - node.extensions.KHR_physics_rigid_bodies.collider.geometry.node; - const colliderNode = gltf.nodes[colliderNodeIndex]; - const referenceCollider = { - geometry: { convexHull: isConvexHull }, - physicsMaterial: materialIndex, - collisionFilter: filterIndex - }; - recurseShapes( - gltf, - colliderNode, - shapeFlags, - referenceCollider, - actorNode, - computedWorldTransform, - node - ); - } - - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - recurseShapes( - gltf, - childNode, - shapeFlags, - undefined, - actorNode, - computedWorldTransform, - undefined - ); - } - return; - } - - // Calculate offset position - const translation = vec3.create(); - const shapePosition = vec3.create(); - mat4.getTranslation(shapePosition, actorNode.worldTransform); - const invertedActorRotation = quat.create(); - quat.invert(invertedActorRotation, actorNode.worldQuaternion); - const offsetPosition = vec3.create(); - mat4.getTranslation(offsetPosition, computedWorldTransform); - vec3.subtract(translation, offsetPosition, shapePosition); - vec3.transformQuat(translation, translation, invertedActorRotation); - - // Calculate offset rotation - const rotation = quat.create(); - const offsetTransform = mat4.create(); - const inverseShapeTransform = mat4.create(); - mat4.invert(inverseShapeTransform, actorNode.worldTransform); - mat4.multiply(offsetTransform, inverseShapeTransform, computedWorldTransform); - mat4.getRotation(rotation, offsetTransform); - - // Calculate scale and scaleAxis - const { scale, scaleAxis } = this.calculateScaleAndAxis(node, referencingNode); - const shape = this.createShape( gltf, node, shapeFlags, material, physXFilterData, - convexHull, + noMeshShapes || isConvexHull, scale, scaleAxis ); @@ -1100,19 +1130,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(PxRotation); this.PhysX.destroy(pose); } - - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - recurseShapes( - gltf, - childNode, - shapeFlags, - collider, - actorNode, - computedWorldTransform, - referencingNode - ); - } }; const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; @@ -1124,17 +1141,17 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (collider?.geometry?.node !== undefined) { const colliderNode = gltf.nodes[collider.geometry.node]; - recurseShapes( + PhysicsUtils.recurseCollider( gltf, colliderNode, - shapeFlags, node.extensions?.KHR_physics_rigid_bodies?.collider, node, worldTransform, - node + node, + createAndAddShape ); } else if (collider?.geometry?.shape !== undefined) { - const { scale, scaleAxis } = this.calculateScaleAndAxis(node); + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); const shape = this.createShape( gltf, @@ -1153,7 +1170,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; - recurseShapes(gltf, childNode, shapeFlags, undefined, node, worldTransform, undefined); + PhysicsUtils.recurseCollider( + gltf, + childNode, + undefined, + node, + worldTransform, + undefined, + createAndAddShape + ); } this.PhysX.destroy(pos); From 8b9f1bed5367bc9aec44d1f009b600277cccbdc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 17:21:03 +0100 Subject: [PATCH 31/93] Fix function name --- source/gltf/node.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/gltf/node.js b/source/gltf/node.js index 8c836241..4d4abc56 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -173,10 +173,7 @@ class gltfNode extends GltfObject { return this.scaledPhysicsTransform ?? this.worldTransform; } - isTransformDirty() { - if (this.dirtyTransform) { - return true; - } + isLocalTransformDirty() { for (const prop of ["rotation", "scale", "translation"]) { if (this.animatedPropertyObjects[prop].dirty) { return true; From e5790566ca316d009f1207c8290bd51fc2731002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 17:29:38 +0100 Subject: [PATCH 32/93] Refactor function --- source/GltfState/phyiscs_controller.js | 94 +++++++++++++------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index e86a1769..0432d980 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -65,16 +65,16 @@ class PhysicsUtils { node.extensions?.KHR_physics_rigid_bodies?.collider?.collisionFilter ?? collider?.collisionFilter; - const material = materialIndex ? this.physXMaterials[materialIndex] : this.defaultMaterial; - - const physXFilterData = filterIndex - ? this.physXFilterData[filterIndex] - : this.physXFilterData[this.physXFilterData.length - 1]; - const isConvexHull = node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.convexHull ?? collider?.geometry?.convexHull; + const referenceCollider = { + geometry: { convexHull: isConvexHull }, + physicsMaterial: materialIndex, + collisionFilter: filterIndex + }; + // If current node is not a reference to a collider search this node and its children to find colliders if ( referencingNode === undefined && @@ -84,11 +84,6 @@ class PhysicsUtils { const colliderNodeIndex = node.extensions.KHR_physics_rigid_bodies.collider.geometry.node; const colliderNode = gltf.nodes[colliderNodeIndex]; - const referenceCollider = { - geometry: { convexHull: isConvexHull }, - physicsMaterial: materialIndex, - collisionFilter: filterIndex - }; this.recurseCollider( gltf, colliderNode, @@ -117,38 +112,13 @@ class PhysicsUtils { return; } - // Calculate offset position - const translation = vec3.create(); - const shapePosition = vec3.create(); - mat4.getTranslation(shapePosition, actorNode.worldTransform); - const invertedActorRotation = quat.create(); - quat.invert(invertedActorRotation, actorNode.worldQuaternion); - const offsetPosition = vec3.create(); - mat4.getTranslation(offsetPosition, computedWorldTransform); - vec3.subtract(translation, offsetPosition, shapePosition); - vec3.transformQuat(translation, translation, invertedActorRotation); - - // Calculate offset rotation - const rotation = quat.create(); - const offsetTransform = mat4.create(); - const inverseShapeTransform = mat4.create(); - mat4.invert(inverseShapeTransform, actorNode.worldTransform); - mat4.multiply(offsetTransform, inverseShapeTransform, computedWorldTransform); - mat4.getRotation(rotation, offsetTransform); - - // Calculate scale and scaleAxis - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); - customFunction( gltf, node, - material, - physXFilterData, - isConvexHull, - translation, - rotation, - scale, - scaleAxis, + referenceCollider, + actorNode, + computedWorldTransform, + referencingNode, ...args ); @@ -1100,21 +1070,49 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const createAndAddShape = ( gltf, node, - material, - physXFilterData, - isConvexHull, - translation, - rotation, - scale, - scaleAxis + collider, + actorNode, + worldTransform, + referencingNode ) => { + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, worldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + const offsetTransform = mat4.create(); + const inverseShapeTransform = mat4.create(); + mat4.invert(inverseShapeTransform, actorNode.worldTransform); + mat4.multiply(offsetTransform, inverseShapeTransform, worldTransform); + mat4.getRotation(rotation, offsetTransform); + + // Calculate scale and scaleAxis + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + + const materialIndex = collider?.physicsMaterial; + const material = materialIndex + ? this.physXMaterials[materialIndex] + : this.defaultMaterial; + + const physXFilterData = collider?.collisionFilter + ? this.physXFilterData[collider.collisionFilter] + : this.physXFilterData[this.physXFilterData.length - 1]; + const shape = this.createShape( gltf, node, shapeFlags, material, physXFilterData, - noMeshShapes || isConvexHull, + noMeshShapes || collider?.geometry?.convexHull === true, scale, scaleAxis ); From 518f09897c2dc0d4a39b672b88920a07431b5d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 10 Dec 2025 17:53:18 +0100 Subject: [PATCH 33/93] WIP animate colliders --- source/GltfState/phyiscs_controller.js | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 0432d980..9f59b0c7 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -46,6 +46,8 @@ class PhysicsUtils { actorNode, worldTransform, referencingNode, + offsetChanged, + scaleChanged, customFunction, args = [] ) { @@ -56,6 +58,12 @@ class PhysicsUtils { const computedWorldTransform = mat4.create(); mat4.multiply(computedWorldTransform, worldTransform, node.getLocalTransform()); + if (node.animatedPropertyObjects.scale.dirty) { + scaleChanged = true; + } + if (node.isLocalTransformDirty()) { + offsetChanged = true; + } const materialIndex = node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? @@ -91,6 +99,8 @@ class PhysicsUtils { actorNode, computedWorldTransform, node, + offsetChanged, + scaleChanged, customFunction, args ); @@ -105,6 +115,8 @@ class PhysicsUtils { actorNode, computedWorldTransform, undefined, + offsetChanged, + scaleChanged, customFunction, args ); @@ -119,6 +131,8 @@ class PhysicsUtils { actorNode, computedWorldTransform, referencingNode, + offsetChanged, + scaleChanged, ...args ); @@ -131,6 +145,8 @@ class PhysicsUtils { actorNode, computedWorldTransform, referencingNode, + offsetChanged, + scaleChanged, customFunction, args ); @@ -399,6 +415,54 @@ class PhysicsController { } applyAnimations(state) { + const updateColliders = () => { + //TODO + }; + + for (const node of this.staticActors) { + const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + if (node.dirtyTransform) { + this.engine.updateRigidBodyTransform(node); + } + + if (collider?.geometry?.node !== undefined) { + const colliderNode = state.gltf.nodes[collider.geometry.node]; + PhysicsUtils.recurseCollider( + state.gltf, + colliderNode, + node.extensions?.KHR_physics_rigid_bodies?.collider, + node, + node.worldTransform, + node, + node.dirtyScale, + node.dirtyScale, + updateColliders + ); + } else if (collider?.geometry?.shape !== undefined) { + if (node.dirtyScale) { + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); + + //TODO update simple shape collider scale + } + //TODO update shapes properties + } + + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + PhysicsUtils.recurseCollider( + state.gltf, + childNode, + undefined, + node, + node.worldTransform, + undefined, + node.dirtyScale, + node.dirtyScale, + updateColliders + ); + } + } + for (const node of state.gltf.nodes) { // TODO set worldTransformUpdated in node when transform changes from animations/interactivity // Find a good way to specify that the node is animated. Either with a flag or by setting physicsTransform to undefined @@ -1146,6 +1210,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node, worldTransform, node, + node.dirtyScale, + node.dirtyScale, createAndAddShape ); } else if (collider?.geometry?.shape !== undefined) { @@ -1175,6 +1241,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node, worldTransform, undefined, + node.dirtyScale, + node.dirtyScale, createAndAddShape ); } From 50936805eee2d2c8727efac7790304ad14e0b069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 11 Dec 2025 18:10:27 +0100 Subject: [PATCH 34/93] Animated colliders mostly done --- source/GltfState/phyiscs_controller.js | 286 +++++++++++++++++++------ source/gltf/gltf_object.js | 9 + 2 files changed, 234 insertions(+), 61 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 9f59b0c7..2f5e759e 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -415,14 +415,14 @@ class PhysicsController { } applyAnimations(state) { - const updateColliders = () => { - //TODO - }; + this.engine.updateSimpleShapes(state.gltf); + const actors = [].concat(this.staticActors, this.kinematicActors, this.dynamicActors); - for (const node of this.staticActors) { + for (let i = 0; i < actors.length; i++) { + const node = actors[i]; const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; if (node.dirtyTransform) { - this.engine.updateRigidBodyTransform(node); + this.engine.updateRigidBodyTransform(node); //TODO } if (collider?.geometry?.node !== undefined) { @@ -436,7 +436,7 @@ class PhysicsController { node, node.dirtyScale, node.dirtyScale, - updateColliders + this.engine.updateCollider ); } else if (collider?.geometry?.shape !== undefined) { if (node.dirtyScale) { @@ -458,23 +458,10 @@ class PhysicsController { undefined, node.dirtyScale, node.dirtyScale, - updateColliders + this.engine.updateCollider ); } } - - for (const node of state.gltf.nodes) { - // TODO set worldTransformUpdated in node when transform changes from animations/interactivity - // Find a good way to specify that the node is animated. Either with a flag or by setting physicsTransform to undefined - if (node.worldTransformUpdated) { - node.scaledPhysicsTransform = undefined; - if (this.engine) { - this.engine.updateRigidBodyTransform(node); - } - } - // TODO check if morph target weights and skinning have changed - // TODO check if collider/physics properties have changed - } } getDebugLineData() { @@ -506,14 +493,18 @@ class PhysicsInterface { resetSimulation() {} stopSimulation() {} - generateBox(x, y, z, scale, scaleAxis) {} - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) {} - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) {} - generateSphere(radius, scale, scaleAxis) {} - generatePlane(width, height, doubleSided, scale, scaleAxis) {} - + generateBox(x, y, z, scale, scaleAxis, reference) {} + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateSphere(radius, scale, scaleAxis, reference) {} + generatePlane(width, height, doubleSided, scale, scaleAxis, reference) {} //TODO Handle non-uniform scale properly (also for parent nodes) - generateSimpleShape(shape, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + generateSimpleShape( + shape, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create(), + reference = undefined + ) { switch (shape.type) { case "box": return this.generateBox( @@ -521,7 +512,8 @@ class PhysicsInterface { shape.box.size[1], shape.box.size[2], scale, - scaleAxis + scaleAxis, + reference ); case "capsule": return this.generateCapsule( @@ -529,7 +521,8 @@ class PhysicsInterface { shape.capsule.radiusTop, shape.capsule.radiusBottom, scale, - scaleAxis + scaleAxis, + reference ); case "cylinder": return this.generateCylinder( @@ -537,17 +530,19 @@ class PhysicsInterface { shape.cylinder.radiusTop, shape.cylinder.radiusBottom, scale, - scaleAxis + scaleAxis, + reference ); case "sphere": - return this.generateSphere(shape.sphere.radius, scale, scaleAxis); + return this.generateSphere(shape.sphere.radius, scale, scaleAxis, reference); case "plane": return this.generatePlane( shape.plane.width, shape.plane.height, shape.plane.doubleSided, scale, - scaleAxis + scaleAxis, + reference ); } } @@ -561,6 +556,27 @@ class PhysicsInterface { this.simpleShapes.push(this.generateSimpleShape(shape)); } } + + updateSimpleShapes(gltf) { + if (gltf?.extensions?.KHR_implicit_shapes === undefined) { + return; + } + for (let i = 0; i < gltf.extensions.KHR_implicit_shapes.shapes.length; i++) { + const shape = gltf.extensions.KHR_implicit_shapes.shapes[i]; + if (shape.isDirty()) { + const newGeometry = this.generateSimpleShape( + shape, + vec3.fromValues(1, 1, 1), + quat.create(), + this.simpleShapes[i] + ); + if (newGeometry !== undefined) { + this.simpleShapes[i].release?.(); + this.simpleShapes[i] = newGeometry; + } + } + } + } } class NvidiaPhysicsInterface extends PhysicsInterface { @@ -613,7 +629,117 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.PhysX; } - createCollider(colliderNode) {} + updateCollider( + gltf, + node, + collider, + actorNode, + worldTransform, + referencingNode, + offsetChanged, + scaleChanged + ) { + const glTFCollider = node.extensions?.KHR_physics_rigid_bodies?.collider; + const { actor, pxShapeMap } = this.nodeToActor.get(actorNode.gltfObjectIndex); + const currentShape = pxShapeMap.get(node.gltfObjectIndex); + let currentGeometry = currentShape.getGeometry(); + const currentColliderType = currentShape.getType(); + const actorType = actor.getType(); + const shapeIndex = glTFCollider?.shape; + let scale = vec3.fromValues(1, 1, 1); + let scaleAxis = quat.create(); + if (shapeIndex !== undefined) { + // Simple shapes need to be recreated if scale changed + // If properties changed we also need to recreate the mesh colliders + const dirty = this.simpleShapes[shapeIndex].isDirty(); + if (scaleChanged && !dirty && currentColliderType === "eCONVEXMESH") { + // Update convex mesh scale + const result = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + scale = result.scale; + scaleAxis = result.scaleAxis; + currentGeometry.scale.scale = scale; + currentGeometry.scale.rotation = scaleAxis; + } else if (!scaleChanged && dirty && currentColliderType !== "eCONVEXMESH") { + // Use geometry from array, if this is a reference we do not need to do anything here since we already updated the array + } else { + // Recreate simple shape collider + const newGeometry = this.generateSimpleShape( + gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], + scale, + scaleAxis + ); + if (newGeometry.getType() !== currentColliderType) { + // We need to recreate the shape + this.PhysX.destroy(currentShape); + const shape = this.createShapeFromGeometry( + newGeometry, + undefined, + undefined, + undefined /*TODO*/, + glTFCollider + ); + pxShapeMap.set(node.gltfObjectIndex, shape); + actor.detachShape(currentShape); + actor.attachShape(shape); + currentGeometry = newGeometry; + } else { + currentShape.setGeometry(newGeometry); + } + } + } else if (node.mesh !== undefined) { + const weights = + node.animatedPropertyObjects.weights ?? + gltf.meshes[node.mesh].animatedPropertyObjects.weights; + if (weights !== undefined && weights.dirty) { + if (actorType === "dynamic") { + //recreate convex hull from morphed mesh + currentGeometry = this.createConvexMeshFromNode(gltf, node); + currentShape.setGeometry(currentGeometry); + } else { + // apply morphed vertices to collider + //TODO: not sure how if getVerticesForModification returns correct values + currentGeometry.triangleMesh.getVerticesForModification(); + currentGeometry.triangleMesh.refitBVH(); + this.scene.resetFiltering(actor); + } + } + if (scaleChanged) { + // apply scale + const result = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + scale = result.scale; + scaleAxis = result.scaleAxis; + currentGeometry.scale.scale = scale; + currentGeometry.scale.rotation = scaleAxis; + } + } else if (node.skin !== undefined) { + //TODO handle skinned colliders (can also include morphing) + } + if (offsetChanged) { + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, worldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + const offsetTransform = mat4.create(); + const inverseShapeTransform = mat4.create(); + mat4.invert(inverseShapeTransform, actorNode.worldTransform); + mat4.multiply(offsetTransform, inverseShapeTransform, worldTransform); + mat4.getRotation(rotation, offsetTransform); + + const PxPos = new this.PhysX.PxVec3(...translation); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + currentShape.setLocalPose(pose); + } + } mapCombineMode(mode) { switch (mode) { @@ -628,7 +754,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - generateBox(x, y, z, scale, scaleAxis) { + // Either create a box or update an existing one. Returns only newly created geometry + generateBox(x, y, z, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); + } if ( scale.every((value) => value === scale[0]) === false && quat.equals(scaleAxis, quat.create()) === false @@ -636,20 +767,27 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const data = createBoxVertexData(x, y, z); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } - const geometry = new this.PhysX.PxBoxGeometry( + const halfExtents = new this.PhysX.PxVec3( (x / 2) * scale[0], (y / 2) * scale[1], (z / 2) * scale[2] ); + let geometry = undefined; + if (referenceType === "eBOX") { + reference.halfExtents = halfExtents; + } else { + geometry = new this.PhysX.PxBoxGeometry(halfExtents); + } + this.PhysX.destroy(halfExtents); return geometry; } - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis) { + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { const data = createCapsuleVertexData(radiusTop, radiusBottom, height); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis) { + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { if ( (quat.equals(scaleAxis, quat.create()) === false && scale.every((value) => value === scale[0]) === false) || @@ -666,18 +804,30 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.createConvexMesh(data.vertices, data.indices); } - generateSphere(radius, scale, scaleAxis) { + generateSphere(radius, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); + } if (scale.every((value) => value === scale[0]) === false) { const data = createCapsuleVertexData(radius, radius, 0); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } else { radius *= scale[0]; } + if (referenceType === "eSPHERE") { + reference.radius = radius; + return undefined; + } const geometry = new this.PhysX.PxSphereGeometry(radius); return geometry; } - generatePlane(width, height, doubleSided, scale, scaleAxis) { + generatePlane(width, height, doubleSided, scale, scaleAxis, reference) { + if (reference !== undefined) { + //TODO handle update + return undefined; + } const geometry = new this.PhysX.PxPlaneGeometry(); return geometry; } @@ -998,6 +1148,28 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); } + createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { + if (physXMaterial === undefined) { + if (glTFCollider?.physicsMaterial !== undefined) { + physXMaterial = this.physXMaterials[glTFCollider.physicsMaterial]; + } else { + physXMaterial = this.defaultMaterial; + } + } + const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); + + if (physXFilterData === undefined) { + physXFilterData = + this.physXFilterData[ + glTFCollider?.collisionFilter ?? this.physXFilterData.length - 1 + ]; + } + + shape.setSimulationFilterData(physXFilterData); + + return shape; + } + createShape( gltf, node, @@ -1032,23 +1204,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return undefined; } - if (physXMaterial === undefined) { - if (collider?.physicsMaterial !== undefined) { - physXMaterial = this.physXMaterials[collider.physicsMaterial]; - } else { - physXMaterial = this.defaultMaterial; - } - } - const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); - - if (physXFilterData === undefined) { - physXFilterData = - this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; - } - - shape.setSimulationFilterData(physXFilterData); - - return shape; + return this.createShapeFromGeometry( + geometry, + physXMaterial, + physXFilterData, + shapeFlags, + collider + ); } createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { @@ -1063,6 +1225,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); const pose = new this.PhysX.PxTransform(pos, rotation); let actor = null; + const pxShapeMap = new Map(); if (type === "static") { actor = this.physics.createRigidStatic(pose); } else { @@ -1188,6 +1351,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { shape.setLocalPose(pose); actor.attachShape(shape); + pxShapeMap.set(node.gltfObjectIndex, shape); this.PhysX.destroy(PxPos); this.PhysX.destroy(PxRotation); this.PhysX.destroy(pose); @@ -1252,7 +1416,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(pose); this.scene.addActor(actor); - this.nodeToActor.set(node.gltfObjectIndex, actor); + this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } computeJointOffsetAndActor(node) { @@ -1268,7 +1432,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getTranslation(pos, node.worldTransform); return { actor: undefined, offsetPosition: pos, offsetRotation: node.worldQuaternion }; } - const actor = this.nodeToActor.get(currentNode.gltfObjectIndex); + const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; const inverseActorRotation = quat.create(); quat.invert(inverseActorRotation, currentNode.worldQuaternion); const offsetRotation = quat.create(); @@ -1691,7 +1855,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return; } - for (const [nodeIndex, actor] of this.nodeToActor.entries()) { + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; // TODO ignore if animated @@ -1759,7 +1923,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.scene.simulate(deltaTime); this.scene.fetchResults(true); - for (const [nodeIndex, actor] of this.nodeToActor.entries()) { + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; if (motion && !motion.isKinematic) { @@ -1862,7 +2026,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToJoint.clear(); for (const actor of this.nodeToActor.values()) { - actor.release(); + actor.actor.release(); } this.nodeToActor.clear(); diff --git a/source/gltf/gltf_object.js b/source/gltf/gltf_object.js index 4fddc9fc..ba7c9037 100644 --- a/source/gltf/gltf_object.js +++ b/source/gltf/gltf_object.js @@ -38,6 +38,15 @@ class GltfObject { initGlForMembers(this, gltf, webGlContext); } + isDirty() { + for (const prop in this.animatedPropertyObjects) { + if (this.animatedPropertyObjects[prop].dirty) { + return true; + } + } + return false; + } + resetDirtyFlags() { for (const prop in this.animatedPropertyObjects) { this.animatedPropertyObjects[prop].dirty = false; From af0177909e208edf286d7e507ec70c0b26b01486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 12 Dec 2025 11:39:05 +0100 Subject: [PATCH 35/93] Simplify collider functions --- source/GltfState/phyiscs_controller.js | 37 ++++++++------------------ 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 2f5e759e..bc275989 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -439,12 +439,16 @@ class PhysicsController { this.engine.updateCollider ); } else if (collider?.geometry?.shape !== undefined) { - if (node.dirtyScale) { - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); - - //TODO update simple shape collider scale - } - //TODO update shapes properties + this.engine.updateCollider( + state.gltf, + node, + node.extensions?.KHR_physics_rigid_bodies?.collider, + node, + node.worldTransform, + undefined, + false, + node.dirtyScale + ); } for (const childIndex of node.children) { @@ -1359,11 +1363,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { }; const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - const physxMaterial = collider?.physicsMaterial - ? this.physXMaterials[collider.physicsMaterial] - : this.defaultMaterial; - const physxFilterData = - this.physXFilterData[collider?.collisionFilter ?? this.physXFilterData.length - 1]; if (collider?.geometry?.node !== undefined) { const colliderNode = gltf.nodes[collider.geometry.node]; @@ -1379,21 +1378,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { createAndAddShape ); } else if (collider?.geometry?.shape !== undefined) { - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); - - const shape = this.createShape( - gltf, - node, - shapeFlags, - physxMaterial, - physxFilterData, - true, - scale, - scaleAxis - ); - if (shape !== undefined) { - actor.attachShape(shape); - } + createAndAddShape(gltf, node, collider, node, worldTransform, undefined); } for (const childIndex of node.children) { From e419eb9028bad42e50b00875803ef34132f414b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 12 Dec 2025 17:26:19 +0100 Subject: [PATCH 36/93] WIP animate materials and motions --- source/GltfState/phyiscs_controller.js | 225 +++++++++++++++++-------- 1 file changed, 155 insertions(+), 70 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index bc275989..01a18427 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -414,57 +414,72 @@ class PhysicsController { } } + updateColliders(state, node) { + this.engine.animateActorTransform(node); //TODO + const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + + if (collider?.geometry?.node !== undefined) { + const colliderNode = state.gltf.nodes[collider.geometry.node]; + PhysicsUtils.recurseCollider( + state.gltf, + colliderNode, + node.extensions?.KHR_physics_rigid_bodies?.collider, + node, + node.worldTransform, + node, + node.dirtyScale, + node.dirtyScale, + this.engine.updateCollider + ); + } else if (collider?.geometry?.shape !== undefined) { + this.engine.updateCollider( + state.gltf, + node, + node.extensions?.KHR_physics_rigid_bodies?.collider, + node, + node.worldTransform, + undefined, + false, + node.dirtyScale + ); + } + + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + PhysicsUtils.recurseCollider( + state.gltf, + childNode, + undefined, + node, + node.worldTransform, + undefined, + node.dirtyScale, + node.dirtyScale, + this.engine.updateCollider + ); + } + } + applyAnimations(state) { this.engine.updateSimpleShapes(state.gltf); - const actors = [].concat(this.staticActors, this.kinematicActors, this.dynamicActors); + this.engine.updatePhysicMaterials(state.gltf); - for (let i = 0; i < actors.length; i++) { - const node = actors[i]; - const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - if (node.dirtyTransform) { - this.engine.updateRigidBodyTransform(node); //TODO - } + for (const actorNode of this.staticActors) { + this.updateColliders(state, actorNode); + } - if (collider?.geometry?.node !== undefined) { - const colliderNode = state.gltf.nodes[collider.geometry.node]; - PhysicsUtils.recurseCollider( - state.gltf, - colliderNode, - node.extensions?.KHR_physics_rigid_bodies?.collider, - node, - node.worldTransform, - node, - node.dirtyScale, - node.dirtyScale, - this.engine.updateCollider - ); - } else if (collider?.geometry?.shape !== undefined) { - this.engine.updateCollider( - state.gltf, - node, - node.extensions?.KHR_physics_rigid_bodies?.collider, - node, - node.worldTransform, - undefined, - false, - node.dirtyScale - ); - } + for (const actorNode of this.kinematicActors) { + this.engine.updateMotion(actorNode); //TODO + this.updateColliders(state, actorNode); + } - for (const childIndex of node.children) { - const childNode = state.gltf.nodes[childIndex]; - PhysicsUtils.recurseCollider( - state.gltf, - childNode, - undefined, - node, - node.worldTransform, - undefined, - node.dirtyScale, - node.dirtyScale, - this.engine.updateCollider - ); - } + for (const actorNode of this.dynamicActors) { + this.engine.updateMotion(actorNode); + this.updateColliders(state, actorNode); + } + + for (const jointNode of this.jointNodes) { + this.engine.updatePhysicsJoint(state, jointNode); //TODO } } @@ -633,6 +648,76 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.PhysX; } + updatePhysicMaterials(gltf) { + const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials === undefined) { + return; + } + for (let i = 0; i < materials.length; i++) { + const material = materials[i]; + if (material.isDirty()) { + const physXMaterial = this.physXMaterials[i]; + physXMaterial.setStaticFriction(material.staticFriction); + physXMaterial.setDynamicFriction(material.dynamicFriction); + physXMaterial.setRestitution(material.restitution); + } + } + } + + updateMotion(actorNode) { + const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; + const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; + if (motion.animatedPropertyObjects.isKinematic.dirty) { + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlag.eKINEMATIC, motion.isKinematic); + } + if (motion.animatedPropertyObjects.mass.dirty) { + actor.setMass(motion.mass); + } + if ( + motion.animatedPropertyObjects.centerOfMass.dirty || + motion.animatedPropertyObjects.inertiaOrientation.dirty || + motion.animatedPropertyObjects.inertiaDiagonal.dirty + ) { + const pos = new this.PhysX.PxVec3(0, 0, 0); + if (motion.centerOfMass !== undefined) { + pos.x = motion.centerOfMass[0]; + pos.y = motion.centerOfMass[1]; + pos.z = motion.centerOfMass[2]; + } + const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); + if (motion.inertiaDiagonal !== undefined) { + if ( + motion.inertiaOrientation !== undefined && + !quat.exactEquals(motion.inertiaOrientation, quat.create()) + ) { + //TODO diagonalize inertia tensor with given rotation + } + const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } else { + if (motion.mass === undefined) { + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); + } else { + this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( + actor, + motion.mass, + pos + ); + } + } + + const pose = new this.PhysX.PxTransform(pos, rot); + actor.setCMassLocalPose(pose); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(pose); + } + if (motion.animatedPropertyObjects.isKinematic.dirty) { + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlag.eKINEMATIC, motion.isKinematic); + } + } + updateCollider( gltf, node, @@ -1252,45 +1337,45 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (motion.mass !== undefined) { actor.setMass(motion.mass); } + + const com = new this.PhysX.PxVec3(0, 0, 0); + const inertiaRotation = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); if (motion.centerOfMass !== undefined) { - const com = new this.PhysX.PxVec3(...motion.centerOfMass); - const inertiaRotation = new this.PhysX.PxQuat( - this.PhysX.PxIDENTITYEnum.PxIdentity - ); - if (motion.inertiaOrientation !== undefined) { - inertiaRotation.x = motion.inertiaOrientation[0]; - inertiaRotation.y = motion.inertiaOrientation[1]; - inertiaRotation.z = motion.inertiaOrientation[2]; - inertiaRotation.w = motion.inertiaOrientation[3]; - } - const comTransform = new this.PhysX.PxTransform(com, inertiaRotation); - actor.setCMassLocalPose(comTransform); - this.PhysX.destroy(com); - this.PhysX.destroy(inertiaRotation); - this.PhysX.destroy(comTransform); + com.x = motion.centerOfMass[0]; + com.y = motion.centerOfMass[1]; + com.z = motion.centerOfMass[2]; } if (motion.inertiaDiagonal !== undefined) { - const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); + if ( + motion.inertiaOrientation !== undefined && + !quat.exactEquals(motion.inertiaOrientation, quat.create()) + ) { + // TODO diagonalize inertia tensor + } else { + const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } } + const comTransform = new this.PhysX.PxTransform(com, inertiaRotation); + actor.setCMassLocalPose(comTransform); + // Let the engine compute mass and inertia if not all parameters are specified if (motion.inertiaDiagonal === undefined) { - const pose = motion.centerOfMass - ? new this.PhysX.PxVec3(...motion.centerOfMass) - : new this.PhysX.PxVec3(0, 0, 0); if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pose); + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, com); } else { this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( actor, motion.mass, - pose + com ); } - this.PhysX.destroy(pose); } + this.PhysX.destroy(com); + this.PhysX.destroy(inertiaRotation); + this.PhysX.destroy(comTransform); if (motion.gravityFactor !== 1.0) { actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); From d2410d1e25872bb00cfd4ff57d2380a180c28957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 17 Dec 2025 18:29:49 +0100 Subject: [PATCH 37/93] Fix some update issues and deactivate animations for now --- source/GltfState/phyiscs_controller.js | 45 +++++++++++++++----------- source/gltf/gltf.js | 1 + 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 01a18427..3bde5b34 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -429,7 +429,7 @@ class PhysicsController { node, node.dirtyScale, node.dirtyScale, - this.engine.updateCollider + this.engine.updateCollider.bind(this.engine) ); } else if (collider?.geometry?.shape !== undefined) { this.engine.updateCollider( @@ -455,12 +455,13 @@ class PhysicsController { undefined, node.dirtyScale, node.dirtyScale, - this.engine.updateCollider + this.engine.updateCollider.bind(this.engine) ); } } applyAnimations(state) { + /* this.engine.updateSimpleShapes(state.gltf); this.engine.updatePhysicMaterials(state.gltf); @@ -480,7 +481,7 @@ class PhysicsController { for (const jointNode of this.jointNodes) { this.engine.updatePhysicsJoint(state, jointNode); //TODO - } + }*/ } getDebugLineData() { @@ -668,7 +669,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; if (motion.animatedPropertyObjects.isKinematic.dirty) { - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlag.eKINEMATIC, motion.isKinematic); + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); } if (motion.animatedPropertyObjects.mass.dirty) { actor.setMass(motion.mass); @@ -713,8 +714,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(rot); this.PhysX.destroy(pose); } - if (motion.animatedPropertyObjects.isKinematic.dirty) { - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlag.eKINEMATIC, motion.isKinematic); + if (motion.animatedPropertyObjects.gravityFactor.dirty) { + actor.setActorFlag( + this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, + motion.gravityFactor !== 1.0 + ); } } @@ -732,7 +736,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const { actor, pxShapeMap } = this.nodeToActor.get(actorNode.gltfObjectIndex); const currentShape = pxShapeMap.get(node.gltfObjectIndex); let currentGeometry = currentShape.getGeometry(); - const currentColliderType = currentShape.getType(); + const currentColliderType = currentGeometry.getType(); const actorType = actor.getType(); const shapeIndex = glTFCollider?.shape; let scale = vec3.fromValues(1, 1, 1); @@ -776,10 +780,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } } else if (node.mesh !== undefined) { - const weights = - node.animatedPropertyObjects.weights ?? - gltf.meshes[node.mesh].animatedPropertyObjects.weights; - if (weights !== undefined && weights.dirty) { + const weights = node.animatedPropertyObjects.weights.value() + ? node.animatedPropertyObjects.weights + : gltf.meshes[node.mesh].animatedPropertyObjects.weights; + if (weights.value() !== undefined && weights.dirty) { if (actorType === "dynamic") { //recreate convex hull from morphed mesh currentGeometry = this.createConvexMeshFromNode(gltf, node); @@ -856,18 +860,23 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const data = createBoxVertexData(x, y, z); return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); } - const halfExtents = new this.PhysX.PxVec3( - (x / 2) * scale[0], - (y / 2) * scale[1], - (z / 2) * scale[2] - ); let geometry = undefined; if (referenceType === "eBOX") { + const halfExtents = new this.PhysX.PxVec3( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] + ); reference.halfExtents = halfExtents; + this.PhysX.destroy(halfExtents); } else { - geometry = new this.PhysX.PxBoxGeometry(halfExtents); + geometry = new this.PhysX.PxBoxGeometry( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] + ); } - this.PhysX.destroy(halfExtents); + return geometry; } diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index bf9307b8..f8186b2c 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -47,6 +47,7 @@ const allowedExtensions = [ "KHR_node_hoverability", "KHR_node_selectability", "KHR_node_visibility", + "KHR_physics_rigid_bodies", "KHR_texture_basisu", "KHR_texture_transform", "KHR_xmp_json_ld", From e9790f16cf62a63b5d3dc524dca54326f54c75a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 17 Dec 2025 18:31:23 +0100 Subject: [PATCH 38/93] Correctly apply inertiaOrientation --- source/GltfState/phyiscs_controller.js | 147 +++++++++++++------------ 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 3bde5b34..b3c8461e 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -665,6 +665,79 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + calculateMassAndInertia(motion, actor) { + const pos = new this.PhysX.PxVec3(0, 0, 0); + if (motion.centerOfMass !== undefined) { + pos.x = motion.centerOfMass[0]; + pos.y = motion.centerOfMass[1]; + pos.z = motion.centerOfMass[2]; + } + const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); + if (motion.inertiaDiagonal !== undefined) { + let inertia = undefined; + if ( + motion.inertiaOrientation !== undefined && + !quat.exactEquals(motion.inertiaOrientation, quat.create()) + ) { + const intertiaRotMat = mat3.create(); + mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); + + const inertiaDiagonalMat = mat3.create(); + inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; + inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; + inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; + + const inertiaTensor = mat3.create(); + mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); + + const col0 = new this.PhysX.PxVec3( + inertiaTensor[0], + inertiaTensor[1], + inertiaTensor[2] + ); + const col1 = new this.PhysX.PxVec3( + inertiaTensor[3], + inertiaTensor[4], + inertiaTensor[5] + ); + const col2 = new this.PhysX.PxVec3( + inertiaTensor[6], + inertiaTensor[7], + inertiaTensor[8] + ); + const pxInertiaTensor = new this.PhysX.PxMat33(col0, col1, col2); + inertia = this.PhysX.PxMassProperties.prototype.getMassSpaceInertia( + pxInertiaTensor, + rot + ); + this.PhysX.destroy(col0); + this.PhysX.destroy(col1); + this.PhysX.destroy(col2); + this.PhysX.destroy(pxInertiaTensor); + } else { + inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + } + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } else { + if (motion.mass === undefined) { + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); + } else { + this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( + actor, + motion.mass, + pos + ); + } + } + + const pose = new this.PhysX.PxTransform(pos, rot); + actor.setCMassLocalPose(pose); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(pose); + } + updateMotion(actorNode) { const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; @@ -679,40 +752,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { motion.animatedPropertyObjects.inertiaOrientation.dirty || motion.animatedPropertyObjects.inertiaDiagonal.dirty ) { - const pos = new this.PhysX.PxVec3(0, 0, 0); - if (motion.centerOfMass !== undefined) { - pos.x = motion.centerOfMass[0]; - pos.y = motion.centerOfMass[1]; - pos.z = motion.centerOfMass[2]; - } - const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); - if (motion.inertiaDiagonal !== undefined) { - if ( - motion.inertiaOrientation !== undefined && - !quat.exactEquals(motion.inertiaOrientation, quat.create()) - ) { - //TODO diagonalize inertia tensor with given rotation - } - const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); - } else { - if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); - } else { - this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( - actor, - motion.mass, - pos - ); - } - } - - const pose = new this.PhysX.PxTransform(pos, rot); - actor.setCMassLocalPose(pose); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(pose); + this.calculateMassAndInertia(motion, actor); } if (motion.animatedPropertyObjects.gravityFactor.dirty) { actor.setActorFlag( @@ -1347,44 +1387,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { actor.setMass(motion.mass); } - const com = new this.PhysX.PxVec3(0, 0, 0); - const inertiaRotation = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); - if (motion.centerOfMass !== undefined) { - com.x = motion.centerOfMass[0]; - com.y = motion.centerOfMass[1]; - com.z = motion.centerOfMass[2]; - } - if (motion.inertiaDiagonal !== undefined) { - if ( - motion.inertiaOrientation !== undefined && - !quat.exactEquals(motion.inertiaOrientation, quat.create()) - ) { - // TODO diagonalize inertia tensor - } else { - const inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); - } - } - - const comTransform = new this.PhysX.PxTransform(com, inertiaRotation); - actor.setCMassLocalPose(comTransform); - - // Let the engine compute mass and inertia if not all parameters are specified - if (motion.inertiaDiagonal === undefined) { - if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, com); - } else { - this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( - actor, - motion.mass, - com - ); - } - } - this.PhysX.destroy(com); - this.PhysX.destroy(inertiaRotation); - this.PhysX.destroy(comTransform); + this.calculateMassAndInertia(motion, actor); if (motion.gravityFactor !== 1.0) { actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); From d38b3f336203ea9605880e86cb36775efe71fd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 18 Dec 2025 11:54:43 +0100 Subject: [PATCH 39/93] Fix combine mode --- source/GltfState/phyiscs_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index b3c8461e..06feb999 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1258,7 +1258,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { gltfPhysicsMaterial.restitution ); if (gltfPhysicsMaterial.frictionCombine !== undefined) { - physxMaterial.setFrictionCombine( + physxMaterial.setFrictionCombineMode( this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) ); } From d63c309bdd812582e990a4fab8ecdef34fbe271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 18 Dec 2025 12:58:38 +0100 Subject: [PATCH 40/93] Fix collision filters --- source/GltfState/phyiscs_controller.js | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 06feb999..744d5ea9 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1205,20 +1205,20 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } collidesWith(filterA, filterB) { - if (filterA.collideWithSystems.length > 0) { - for (const system of filterA.collideWithSystems) { - if (filterB.collisionSystems.includes(system)) { + if (filterB.collideWithSystems.length > 0) { + for (const system of filterB.collideWithSystems) { + if (filterA.collisionSystems.includes(system)) { return true; } } return false; - } else if (filterA.notCollideWithSystems.length > 0) { - for (const system of filterA.notCollideWithSystems) { - if (filterB.collisionSystems.includes(system)) { + } else if (filterB.notCollideWithSystems.length > 0) { + for (const system of filterB.notCollideWithSystems) { + if (filterA.collisionSystems.includes(system)) { return false; } - return true; } + return true; } return true; } @@ -1239,7 +1239,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (let i = 0; i < filterCount; i++) { let bitMask = 0; for (let j = 0; j < filterCount; j++) { - if (this.collidesWith(filters[i], filters[j])) { + if ( + this.collidesWith(filters[i], filters[j]) && + this.collidesWith(filters[j], filters[i]) + ) { bitMask |= 1 << j; } } @@ -1426,13 +1429,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); const materialIndex = collider?.physicsMaterial; - const material = materialIndex - ? this.physXMaterials[materialIndex] - : this.defaultMaterial; - - const physXFilterData = collider?.collisionFilter - ? this.physXFilterData[collider.collisionFilter] - : this.physXFilterData[this.physXFilterData.length - 1]; + const material = + materialIndex !== undefined + ? this.physXMaterials[materialIndex] + : this.defaultMaterial; + + const physXFilterData = + collider?.collisionFilter !== undefined + ? this.physXFilterData[collider.collisionFilter] + : this.physXFilterData[this.physXFilterData.length - 1]; const shape = this.createShape( gltf, From 4851700928352783cac2bc1ce99651383dce583d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 18 Dec 2025 18:33:14 +0100 Subject: [PATCH 41/93] Add simple debug views --- source/GltfState/phyiscs_controller.js | 66 +++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 744d5ea9..30dd8505 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -171,7 +171,7 @@ class PhysicsController { this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; this.pauseTime = undefined; - this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize + this.skipFrames = 0; // Skip the first two simulation frames to allow engine to initialize //TODO PxShape needs to be recreated if collisionFilter differs //TODO Cache geometries for faster computation @@ -484,6 +484,14 @@ class PhysicsController { }*/ } + enableDebugColliders(enable) { + this.engine.enableDebugColliders(enable); + } + + enableDebugJoints(enable) { + this.engine.enableDebugJoints(enable); + } + getDebugLineData() { if (this.engine) { return this.engine.getDebugLineData(); @@ -512,6 +520,8 @@ class PhysicsInterface { resumeSimulation() {} resetSimulation() {} stopSimulation() {} + enableDebugColliders(enable) {} + enableDebugJoints(enable) {} generateBox(x, y, z, scale, scaleAxis, reference) {} generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} @@ -621,6 +631,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.convexMeshes = []; this.triangleMeshes = []; + // Debug + this.debugColliders = true; + this.debugJoints = true; + this.debugStateChanged = true; + this.MAX_FLOAT = 3.4028234663852885981170418348452e38; } @@ -1848,6 +1863,34 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return physxJoint; } + changeDebugVisualization() { + if (!this.scene || !this.debugStateChanged) { + return; + } + this.debugStateChanged = false; + this.scene.setVisualizationParameter( + this.PhysX.eSCALE, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eWORLD_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eACTOR_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eCOLLISION_SHAPES, + this.debugColliders ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eJOINT_LOCAL_FRAMES, + this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); + } + initializeSimulation( state, staticActors, @@ -1909,12 +1952,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(tmpVec); this.PhysX.destroy(sceneDesc); this.PhysX.destroy(shapeFlags); - this.scene.setVisualizationParameter(this.PhysX.eSCALE, 1); - this.scene.setVisualizationParameter(this.PhysX.eWORLD_AXES, 1); - this.scene.setVisualizationParameter(this.PhysX.eACTOR_AXES, 1); - this.scene.setVisualizationParameter(this.PhysX.eCOLLISION_SHAPES, 1); - this.scene.setVisualizationParameter(this.PhysX.eJOINT_LOCAL_FRAMES, 1); - this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, 1); + + this.debugStateChanged = true; + this.changeDebugVisualization(); + } + + enableDebugColliders(enable) { + this.debugColliders = enable; + this.debugStateChanged = true; + } + + enableDebugJoints(enable) { + this.debugJoints = enable; + this.debugStateChanged = true; } applyTransformRecursively(gltf, node, parentTransform) { @@ -1942,6 +1992,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return; } + this.changeDebugVisualization(); + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; From a34596d9a52a81a9fdbf8d9f8e650af1e2ebb14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 15 Jan 2026 15:20:24 +0100 Subject: [PATCH 42/93] Fix geometry casting --- source/GltfState/phyiscs_controller.js | 27 ++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 30dd8505..9faf1fdb 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -800,14 +800,26 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // Simple shapes need to be recreated if scale changed // If properties changed we also need to recreate the mesh colliders const dirty = this.simpleShapes[shapeIndex].isDirty(); - if (scaleChanged && !dirty && currentColliderType === "eCONVEXMESH") { + if ( + scaleChanged && + !dirty && + currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH + ) { // Update convex mesh scale + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); const result = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); scale = result.scale; scaleAxis = result.scaleAxis; currentGeometry.scale.scale = scale; currentGeometry.scale.rotation = scaleAxis; - } else if (!scaleChanged && dirty && currentColliderType !== "eCONVEXMESH") { + } else if ( + !scaleChanged && + dirty && + currentColliderType !== this.PhysX.PxGeometryTypeEnum.eCONVEXMESH + ) { // Use geometry from array, if this is a reference we do not need to do anything here since we already updated the array } else { // Recreate simple shape collider @@ -838,6 +850,17 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const weights = node.animatedPropertyObjects.weights.value() ? node.animatedPropertyObjects.weights : gltf.meshes[node.mesh].animatedPropertyObjects.weights; + if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxTriangleMeshGeometry + ); + } if (weights.value() !== undefined && weights.dirty) { if (actorType === "dynamic") { //recreate convex hull from morphed mesh From d9e9830d7154e1a031d797065d7c80bccf5d0f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 15 Jan 2026 15:30:17 +0100 Subject: [PATCH 43/93] Update motion velocities --- source/GltfState/phyiscs_controller.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 9faf1fdb..4390b7b1 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -775,6 +775,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { motion.gravityFactor !== 1.0 ); } + if (motion.animatedPropertyObjects.linearVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); + actor.setLinearVelocity(pxVelocity); + } + if (motion.animatedPropertyObjects.angularVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); + actor.setAngularVelocity(pxVelocity); + } } updateCollider( From 0ea5114b48970d0ed16ca5bf63d0d402134c6304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 15 Jan 2026 17:58:27 +0100 Subject: [PATCH 44/93] Animate actor transforms --- source/GltfState/phyiscs_controller.js | 33 ++++++++++++++++++++++++-- source/gltf/node.js | 2 ++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 4390b7b1..19fa68e5 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -415,7 +415,7 @@ class PhysicsController { } updateColliders(state, node) { - this.engine.animateActorTransform(node); //TODO + this.engine.updateActorTransform(node); const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; if (collider?.geometry?.node !== undefined) { @@ -607,6 +607,8 @@ class PhysicsInterface { } } } + + updateActorTransform(node) {} } class NvidiaPhysicsInterface extends PhysicsInterface { @@ -680,6 +682,30 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + updateActorTransform(node) { + if (node.dirtyTransform) { + const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; + if (actor === undefined) { + return; + } + const pxPos = new this.PhysX.PxVec3( + node.worldTransform[12], + node.worldTransform[13], + node.worldTransform[14] + ); + const pxRot = new this.PhysX.PxQuat(...node.worldQuaternion); + const pxTransform = new this.PhysX.PxTransform(pxPos, pxRot); + if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { + actor.setKinematicTarget(pxTransform); + } else { + actor.setGlobalPos(pxTransform); + } + this.PhysX.destroy(pxPos); + this.PhysX.destroy(pxRot); + this.PhysX.destroy(pxTransform); + } + } + calculateMassAndInertia(motion, actor) { const pos = new this.PhysX.PxVec3(0, 0, 0); if (motion.centerOfMass !== undefined) { @@ -2027,8 +2053,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; + if (node.dirtyTransform) { + // Node transform is currently animated + continue; + } const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - // TODO ignore if animated if (motion && motion.isKinematic) { const worldTransform = node.physicsTransform ?? node.worldTransform; const targetPosition = vec3.create(); diff --git a/source/gltf/node.js b/source/gltf/node.js index 4d4abc56..fd3c9243 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -38,6 +38,8 @@ class gltfNode extends GltfObject { this.scene = undefined; this.physicsTransform = undefined; this.scaledPhysicsTransform = undefined; + + // These are set if this or any parent transform changed this.dirtyTransform = true; this.dirtyScale = true; } From c47f9aec704953bd2798174fdb3f1f022e4bd4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 16 Jan 2026 13:16:47 +0100 Subject: [PATCH 45/93] Fix typo --- source/gltf/node.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/gltf/node.js b/source/gltf/node.js index fd3c9243..06010c0b 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -290,8 +290,8 @@ class KHR_physics_rigid_bodies_motion extends GltfObject { "isKinematic", "mass", "centerOfMass", - "inertialDiagonal", - "inertialOrientation", + "inertiaDiagonal", + "inertiaOrientation", "linearVelocity", "angularVelocity", "gravityFactor" @@ -301,8 +301,8 @@ class KHR_physics_rigid_bodies_motion extends GltfObject { this.isKinematic = false; this.mass = undefined; this.centerOfMass = undefined; - this.inertialDiagonal = undefined; - this.inertialOrientation = undefined; + this.inertiaDiagonal = undefined; + this.inertiaOrientation = undefined; this.linearVelocity = [0, 0, 0]; this.angularVelocity = [0, 0, 0]; this.gravityFactor = 1; From 2b97275d5b7d049d3772b76956d28eadda7a48ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 16 Jan 2026 13:27:25 +0100 Subject: [PATCH 46/93] Reset all dirty flags --- source/GltfView/gltf_view.js | 3 ++- source/gltf/animatable_property.js | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index b7e60984..ab2d644e 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -1,3 +1,4 @@ +import { AnimatableProperty } from "../gltf/animatable_property.js"; import { GltfState } from "../GltfState/gltf_state.js"; import { gltfRenderer } from "../Renderer/renderer.js"; import { GL } from "../Renderer/webgl.js"; @@ -89,7 +90,7 @@ class GltfView { } this.renderer.drawScene(state, scene); - scene.resetHierarchyDirtyFlags(state.gltf); + AnimatableProperty.resetAllDirtyFlags(); } /** diff --git a/source/gltf/animatable_property.js b/source/gltf/animatable_property.js index b159ad0e..9c5e7622 100644 --- a/source/gltf/animatable_property.js +++ b/source/gltf/animatable_property.js @@ -1,23 +1,41 @@ class AnimatableProperty { + static dirtyFlagList = []; // Collect all animatable properties with dirty flags set to true + static resetAllDirtyFlags() { + for (const prop of this.dirtyFlagList) { + prop.dirty = false; + } + this.dirtyFlagList = []; + } + constructor(value) { this.restValue = value; this.animatedValue = null; this.dirty = true; + AnimatableProperty.dirtyFlagList.push(this); } restAt(value) { - this.dirty = true; + if (!this.dirty) { + this.dirty = true; + AnimatableProperty.dirtyFlagList.push(this); + } this.restValue = value; } animate(value) { - this.dirty = true; + if (!this.dirty) { + this.dirty = true; + AnimatableProperty.dirtyFlagList.push(this); + } this.animatedValue = value; } rest() { if (this.animatedValue !== null) { - this.dirty = true; + if (!this.dirty) { + this.dirty = true; + AnimatableProperty.dirtyFlagList.push(this); + } this.animatedValue = null; } } From 61b37be99a0e7f20675675cd463ca269b215b5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 22 Jan 2026 15:32:26 +0100 Subject: [PATCH 47/93] Change geometry.node to geometry.mesh --- source/GltfState/phyiscs_controller.js | 324 +++++-------------------- source/gltf/node.js | 2 +- 2 files changed, 60 insertions(+), 266 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 19fa68e5..67da87bb 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -12,14 +12,11 @@ import { import { vec3, mat4, quat, mat3 } from "gl-matrix"; class PhysicsUtils { - static calculateScaleAndAxis(node, referencingNode = undefined) { - const referencedNodeIndex = - referencingNode?.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node; + static calculateScaleAndAxis(node) { const scaleFactor = vec3.clone(node.scale); let scaleRotation = quat.create(); - let currentNode = - node.gltfObjectIndex === referencedNodeIndex ? referencingNode : node.parentNode; + let currentNode = node.parentNode; const currentRotation = quat.clone(node.rotation); while (currentNode !== undefined) { @@ -31,10 +28,7 @@ class PhysicsUtils { } const nextRotation = quat.clone(currentNode.rotation); quat.multiply(currentRotation, currentRotation, nextRotation); - currentNode = - currentNode.gltfObjectIndex === referencedNodeIndex - ? referencingNode - : currentNode.parentNode; + currentNode = currentNode.parentNode; } return { scale: scaleFactor, scaleAxis: scaleRotation }; } @@ -44,8 +38,6 @@ class PhysicsUtils { node, collider, actorNode, - worldTransform, - referencingNode, offsetChanged, scaleChanged, customFunction, @@ -56,8 +48,7 @@ class PhysicsUtils { return; } - const computedWorldTransform = mat4.create(); - mat4.multiply(computedWorldTransform, worldTransform, node.getLocalTransform()); + const computedWorldTransform = node.worldTransform; if (node.animatedPropertyObjects.scale.dirty) { scaleChanged = true; } @@ -65,77 +56,23 @@ class PhysicsUtils { offsetChanged = true; } - const materialIndex = - node.extensions?.KHR_physics_rigid_bodies?.collider?.physicsMaterial ?? - collider?.physicsMaterial; - - const filterIndex = - node.extensions?.KHR_physics_rigid_bodies?.collider?.collisionFilter ?? - collider?.collisionFilter; - - const isConvexHull = - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.convexHull ?? - collider?.geometry?.convexHull; - - const referenceCollider = { - geometry: { convexHull: isConvexHull }, - physicsMaterial: materialIndex, - collisionFilter: filterIndex - }; - - // If current node is not a reference to a collider search this node and its children to find colliders + // Found a collider geometry if ( - referencingNode === undefined && - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape === undefined + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.mesh !== undefined || + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape !== undefined ) { - if (node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.node !== undefined) { - const colliderNodeIndex = - node.extensions.KHR_physics_rigid_bodies.collider.geometry.node; - const colliderNode = gltf.nodes[colliderNodeIndex]; - this.recurseCollider( - gltf, - colliderNode, - referenceCollider, - actorNode, - computedWorldTransform, - node, - offsetChanged, - scaleChanged, - customFunction, - args - ); - } - - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - this.recurseCollider( - gltf, - childNode, - undefined, - actorNode, - computedWorldTransform, - undefined, - offsetChanged, - scaleChanged, - customFunction, - args - ); - } - return; + customFunction( + gltf, + node, + node.extensions.KHR_physics_rigid_bodies.collider, + actorNode, + computedWorldTransform, + offsetChanged, + scaleChanged, + ...args + ); } - customFunction( - gltf, - node, - referenceCollider, - actorNode, - computedWorldTransform, - referencingNode, - offsetChanged, - scaleChanged, - ...args - ); - for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; this.recurseCollider( @@ -143,8 +80,6 @@ class PhysicsUtils { childNode, collider, actorNode, - computedWorldTransform, - referencingNode, offsetChanged, scaleChanged, customFunction, @@ -418,20 +353,7 @@ class PhysicsController { this.engine.updateActorTransform(node); const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - if (collider?.geometry?.node !== undefined) { - const colliderNode = state.gltf.nodes[collider.geometry.node]; - PhysicsUtils.recurseCollider( - state.gltf, - colliderNode, - node.extensions?.KHR_physics_rigid_bodies?.collider, - node, - node.worldTransform, - node, - node.dirtyScale, - node.dirtyScale, - this.engine.updateCollider.bind(this.engine) - ); - } else if (collider?.geometry?.shape !== undefined) { + if (collider?.geometry?.shape !== undefined || collider?.geometry?.mesh !== undefined) { this.engine.updateCollider( state.gltf, node, @@ -451,8 +373,6 @@ class PhysicsController { childNode, undefined, node, - node.worldTransform, - undefined, node.dirtyScale, node.dirtyScale, this.engine.updateCollider.bind(this.engine) @@ -811,22 +731,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - updateCollider( - gltf, - node, - collider, - actorNode, - worldTransform, - referencingNode, - offsetChanged, - scaleChanged - ) { + updateCollider(gltf, node, collider, actorNode, worldTransform, offsetChanged, scaleChanged) { const glTFCollider = node.extensions?.KHR_physics_rigid_bodies?.collider; const { actor, pxShapeMap } = this.nodeToActor.get(actorNode.gltfObjectIndex); const currentShape = pxShapeMap.get(node.gltfObjectIndex); let currentGeometry = currentShape.getGeometry(); const currentColliderType = currentGeometry.getType(); - const actorType = actor.getType(); const shapeIndex = glTFCollider?.shape; let scale = vec3.fromValues(1, 1, 1); let scaleAxis = quat.create(); @@ -844,7 +754,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { currentGeometry, this.PhysX.PxConvexMeshGeometry ); - const result = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + const result = PhysicsUtils.calculateScaleAndAxis(node); scale = result.scale; scaleAxis = result.scaleAxis; currentGeometry.scale.scale = scale; @@ -880,44 +790,26 @@ class NvidiaPhysicsInterface extends PhysicsInterface { currentShape.setGeometry(newGeometry); } } - } else if (node.mesh !== undefined) { - const weights = node.animatedPropertyObjects.weights.value() - ? node.animatedPropertyObjects.weights - : gltf.meshes[node.mesh].animatedPropertyObjects.weights; - if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxConvexMeshGeometry - ); - } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxTriangleMeshGeometry - ); - } - if (weights.value() !== undefined && weights.dirty) { - if (actorType === "dynamic") { - //recreate convex hull from morphed mesh - currentGeometry = this.createConvexMeshFromNode(gltf, node); - currentShape.setGeometry(currentGeometry); - } else { - // apply morphed vertices to collider - //TODO: not sure how if getVerticesForModification returns correct values - currentGeometry.triangleMesh.getVerticesForModification(); - currentGeometry.triangleMesh.refitBVH(); - this.scene.resetFiltering(actor); - } - } + } else if (collider.mesh !== undefined) { if (scaleChanged) { + if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxTriangleMeshGeometry + ); + } // apply scale - const result = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + const result = PhysicsUtils.calculateScaleAndAxis(node); scale = result.scale; scaleAxis = result.scaleAxis; currentGeometry.scale.scale = scale; currentGeometry.scale.rotation = scaleAxis; } - } else if (node.skin !== undefined) { - //TODO handle skinned colliders (can also include morphing) } if (offsetChanged) { // Calculate offset position @@ -970,7 +862,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { quat.equals(scaleAxis, quat.create()) === false ) { const data = createBoxVertexData(x, y, z); - return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); } let geometry = undefined; if (referenceType === "eBOX") { @@ -994,7 +886,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); } generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { @@ -1005,13 +897,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scale[0] !== scale[2] ) { const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); } height *= scale[1]; radiusTop *= scale[0]; radiusBottom *= scale[0]; const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexMesh(data.vertices, data.indices); + return this.createConvexPxMesh(data.vertices, data.indices); } generateSphere(radius, scale, scaleAxis, reference) { @@ -1021,7 +913,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } if (scale.every((value) => value === scale[0]) === false) { const data = createCapsuleVertexData(radius, radius, 0); - return this.createConvexMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); } else { radius *= scale[0]; } @@ -1042,7 +934,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } - createConvexMesh( + createConvexPxMesh( vertices, indices, scale = vec3.fromValues(1, 1, 1), @@ -1084,29 +976,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } - collectVerticesAndIndicesFromNode(gltf, node) { + collectVerticesAndIndicesFromMesh(gltf, mesh) { // TODO Handle different primitive modes - const mesh = gltf.meshes[node.mesh]; let positionDataArray = []; let positionCount = 0; let indexDataArray = []; let indexCount = 0; - let skinData = undefined; - - if (node.skin !== undefined) { - const skin = gltf.skins[node.skin]; - if (skin.jointTextureData === undefined) { - skin.computeJoints(gltf); - } - skinData = skin.jointTextureData; - } for (const primitive of mesh.primitives) { const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); if (primitive.targets !== undefined) { - let morphWeights = node.weights ?? mesh.weights; + let morphWeights = mesh.weights; if (morphWeights !== undefined) { // Calculate morphed vertex positions on CPU const morphPositionData = []; @@ -1134,65 +1016,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - if (skinData !== undefined) { - // Apply skinning on CPU - const joints0Accessor = gltf.accessors[primitive.attributes.JOINTS_0]; - const weights0Accessor = gltf.accessors[primitive.attributes.WEIGHTS_0]; - const joints0Data = joints0Accessor.getDeinterlacedView(gltf); - const weights0Data = weights0Accessor.getNormalizedDeinterlacedView(gltf); - let joints1Data = undefined; - let weights1Data = undefined; - if ( - primitive.attributes.JOINTS_1 !== undefined && - primitive.attributes.WEIGHTS_1 !== undefined - ) { - const joints1Accessor = gltf.accessors[primitive.attributes.JOINTS_1]; - const weights1Accessor = gltf.accessors[primitive.attributes.WEIGHTS_1]; - joints1Data = joints1Accessor.getDeinterlacedView(gltf); - weights1Data = weights1Accessor.getNormalizedDeinterlacedView(gltf); - } - - for (let i = 0; i < positionData.length / 3; i++) { - let skinnedPosition = vec3.create(); - const originalPosition = vec3.fromValues( - positionData[i * 3], - positionData[i * 3 + 1], - positionData[i * 3 + 2] - ); - const skinningMatrix = mat4.create(); - for (let j = 0; j < 4; j++) { - const jointIndex = joints0Data[i * 4 + j]; - const weight = weights0Data[i * 4 + j]; - const jointMatrix = mat4.create(); - jointMatrix.set(skinData.slice(jointIndex * 32, jointIndex * 32 + 16)); - mat4.multiplyScalarAndAdd( - skinningMatrix, - skinningMatrix, - jointMatrix, - weight - ); - if (joints1Data !== undefined && weights1Data !== undefined && j >= 4) { - const joint1Index = joints1Data[i * 4 + (j - 4)]; - const weight1 = weights1Data[i * 4 + (j - 4)]; - const jointMatrix = mat4.create(); - jointMatrix.set( - skinData.slice(joint1Index * 32, joint1Index * 32 + 16) - ); - mat4.multiplyScalarAndAdd( - skinningMatrix, - skinningMatrix, - jointMatrix, - weight1 - ); - } - } - vec3.transformMat4(skinnedPosition, originalPosition, skinningMatrix); - positionData[i * 3] = skinnedPosition[0]; - positionData[i * 3 + 1] = skinnedPosition[1]; - positionData[i * 3 + 2] = skinnedPosition[2]; - } - } - positionDataArray.push(positionData); positionCount += positionAccessor.count; if (primitive.indices !== undefined) { @@ -1221,18 +1044,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return { vertices: positionData, indices: indexData }; } - createConvexMeshFromNode( - gltf, - node, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create() - ) { - const { vertices, indices } = this.collectVerticesAndIndicesFromNode(gltf, node); - return this.createConvexMesh(vertices, indices, scale, scaleAxis); + createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh); + return this.createConvexPxMesh(vertices, indices, scale, scaleAxis); } - createMeshFromNode(gltf, node, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const { vertices, indices } = this.collectVerticesAndIndicesFromNode(gltf, node); + createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh); const malloc = (f, q) => { const nDataBytes = f.length * f.BYTES_PER_ELEMENT; if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); @@ -1255,11 +1073,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const PxScale = new this.PhysX.PxVec3(1, 1, 1); const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); - // Skins ignore the the transforms of the nodes they are attached to - if (node.skin === undefined) { + + if (scale !== undefined) { PxScale.x = scale[0]; PxScale.y = scale[1]; PxScale.z = scale[2]; + } + if (scaleAxis !== undefined) { PxQuat.x = scaleAxis[0]; PxQuat.y = scaleAxis[1]; PxQuat.z = scaleAxis[2]; @@ -1403,13 +1223,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } else { geometry = this.simpleShapes[collider.geometry.shape]; } - } else { - if (node.mesh !== undefined) { - if (convexHull === true && node.skin === undefined) { - geometry = this.createConvexMeshFromNode(gltf, node, scale, scaleAxis); - } else { - geometry = this.createMeshFromNode(gltf, node, scale, scaleAxis); - } + } else if (collider?.geometry?.mesh !== undefined) { + const mesh = gltf.meshes[collider.geometry.mesh]; + if (convexHull === true) { + geometry = this.createConvexMesh(gltf, mesh, scale, scaleAxis); + } else { + geometry = this.createPxMesh(gltf, mesh, scale, scaleAxis); } } @@ -1470,14 +1289,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - const createAndAddShape = ( - gltf, - node, - collider, - actorNode, - worldTransform, - referencingNode - ) => { + const createAndAddShape = (gltf, node, collider, actorNode, worldTransform) => { // Calculate offset position const translation = vec3.create(); const shapePosition = vec3.create(); @@ -1498,7 +1310,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getRotation(rotation, offsetTransform); // Calculate scale and scaleAxis - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node, referencingNode); + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); const materialIndex = collider?.physicsMaterial; const material = @@ -1537,23 +1349,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { }; const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - - if (collider?.geometry?.node !== undefined) { - const colliderNode = gltf.nodes[collider.geometry.node]; - PhysicsUtils.recurseCollider( - gltf, - colliderNode, - node.extensions?.KHR_physics_rigid_bodies?.collider, - node, - worldTransform, - node, - node.dirtyScale, - node.dirtyScale, - createAndAddShape - ); - } else if (collider?.geometry?.shape !== undefined) { - createAndAddShape(gltf, node, collider, node, worldTransform, undefined); - } + createAndAddShape(gltf, node, collider, node, worldTransform); for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; @@ -1562,8 +1358,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { childNode, undefined, node, - worldTransform, - undefined, node.dirtyScale, node.dirtyScale, createAndAddShape diff --git a/source/gltf/node.js b/source/gltf/node.js index 06010c0b..c15b2069 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -281,7 +281,7 @@ class KHR_physics_rigid_bodies_geometry extends GltfObject { super(); this.convexHull = false; this.shape = undefined; - this.node = undefined; + this.mesh = undefined; } } From 148445367aca4ded6eaab9b4ee2ca31a36e8bce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 22 Jan 2026 15:33:28 +0100 Subject: [PATCH 48/93] Add empty functions for joint animations --- source/GltfState/phyiscs_controller.js | 47 ++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 67da87bb..a46b3627 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -381,7 +381,6 @@ class PhysicsController { } applyAnimations(state) { - /* this.engine.updateSimpleShapes(state.gltf); this.engine.updatePhysicMaterials(state.gltf); @@ -390,7 +389,7 @@ class PhysicsController { } for (const actorNode of this.kinematicActors) { - this.engine.updateMotion(actorNode); //TODO + this.engine.updateMotion(actorNode); this.updateColliders(state, actorNode); } @@ -401,7 +400,7 @@ class PhysicsController { for (const jointNode of this.jointNodes) { this.engine.updatePhysicsJoint(state, jointNode); //TODO - }*/ + } } enableDebugColliders(enable) { @@ -529,6 +528,7 @@ class PhysicsInterface { } updateActorTransform(node) {} + updatePhysicsJoint(state, jointNode) {} } class NvidiaPhysicsInterface extends PhysicsInterface { @@ -838,6 +838,47 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + updatePhysicsJoint(state, jointNode) { + const pxJoint = this.nodeToJoint.get(jointNode.gltfObjectIndex); + if (pxJoint === undefined) { + return; + } + const jointIndex = jointNode.extensions?.KHR_physics_rigid_bodies?.joint?.joint; + const gltfJoint = state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[jointIndex]; + if ( + jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects + .enableCollision.dirty + ) { + pxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision + ); + } + for (const limit of gltfJoint.limits) { + if (limit.animatedPropertyObjects.min.dirty) { + } + if (limit.animatedPropertyObjects.max.dirty) { + } + if (limit.animatedPropertyObjects.stiffness.dirty) { + } + if (limit.animatedPropertyObjects.damping.dirty) { + } + } + + for (const drive of gltfJoint.drives) { + if (drive.animatedPropertyObjects.stiffness.dirty) { + } + if (drive.animatedPropertyObjects.damping.dirty) { + } + if (drive.animatedPropertyObjects.maxForce.dirty) { + } + if (drive.animatedPropertyObjects.positionTarget.dirty) { + } + if (drive.animatedPropertyObjects.velocityTarget.dirty) { + } + } + } + mapCombineMode(mode) { switch (mode) { case "average": From a879334361b3845c2a0b77fbc646951d437b41aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 27 Jan 2026 17:14:28 +0100 Subject: [PATCH 49/93] Add ray cast node --- source/GltfState/phyiscs_controller.js | 89 +++++++++++++++++++++++++- source/gltf/interactivity.js | 13 ++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index a46b3627..5b4cba9d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -417,6 +417,18 @@ class PhysicsController { } return []; } + + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { + this.engine.applyImpulse(nodeIndex, linearImpulse, angularImpulse); + } + + applyPointImpulse(nodeIndex, impulse, position) { + this.engine.applyPointImpulse(nodeIndex, impulse, position); + } + + rayCast(rayStart, rayEnd) { + this.engine.rayCast(rayStart, rayEnd); + } } class PhysicsInterface { @@ -442,6 +454,10 @@ class PhysicsInterface { enableDebugColliders(enable) {} enableDebugJoints(enable) {} + applyImpulse(nodeIndex, linearImpulse, angularImpulse) {} + applyPointImpulse(nodeIndex, impulse, position) {} + rayCast(rayStart, rayEnd) {} + generateBox(x, y, z, scale, scaleAxis, reference) {} generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} @@ -545,6 +561,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.scene = undefined; this.nodeToActor = new Map(); this.nodeToJoint = new Map(); + this.shapeToNode = new Map(); this.filterData = []; this.physXFilterData = []; this.physXMaterials = []; @@ -1277,13 +1294,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return undefined; } - return this.createShapeFromGeometry( + const shape = this.createShapeFromGeometry( geometry, physXMaterial, physXFilterData, shapeFlags, collider ); + + this.shapeToNode.set(shape.ptr, node.gltfObjectIndex); + return shape; } createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { @@ -2067,6 +2087,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (scenePointer) { scenePointer.release(); } + + this.shapeToNode.clear(); } getDebugLineData() { @@ -2087,6 +2109,71 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } return result; } + + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { + this.engine.applyImpulse(nodeIndex, linearImpulse, angularImpulse); + } + + applyPointImpulse(nodeIndex, impulse, position) { + this.engine.applyPointImpulse(nodeIndex, impulse, position); + } + + rayCast(rayStart, rayEnd) { + const result = {}; + result.hitNodeIndex = -1; + if (!this.scene) { + return result; + } + const origin = new this.PhysX.PxVec3(...rayStart); + const directionVec = vec3.create(); + vec3.subtract(directionVec, rayEnd, rayStart); + vec3.normalize(directionVec, directionVec); + const direction = new this.PhysX.PxVec3(...directionVec); + const maxDistance = vec3.distance(rayStart, rayEnd); + + const hitBuffer = new this.PhysX.PxRaycastBuffer10(); + const hitFlags = new this.PhysX.PxHitFlags(this.PhysX.PxHitFlagEnum.eDEFAULT); + + const queryFilterData = new this.PhysX.PxQueryFilterData(); + queryFilterData.set_flags( + this.PhysX.PxQueryFlagEnum.eSTATIC | this.PhysX.PxQueryFlagEnum.eDYNAMIC + ); + + const hasHit = this.scene.raycast( + origin, + direction, + maxDistance, + hitBuffer, + hitFlags, + queryFilterData + ); + + this.PhysX.destroy(origin); + this.PhysX.destroy(direction); + this.PhysX.destroy(hitFlags); + this.PhysX.destroy(queryFilterData); + + if (hasHit) { + const hitCount = hit.getNbAnyHits(); + if (hitCount > 1) { + console.warn("Raycast hit multiple objects, only the first hit is returned."); + } + const hit = hitBuffer.getAnyHit(0); + const fraction = hit.distance / maxDistance; + const hitNormal = vec3.fromValues(hit.normal.x, hit.normal.y, hit.normal.z); + const hitNodeIndex = this.shapeToNode.get(hit.shape.ptr); + if (hitNodeIndex === undefined) { + return result; + } + return { + hitNodeIndex: hitNodeIndex, + hitFraction: fraction, + hitNormal: hitNormal + }; + } else { + return result; + } + } } export { PhysicsController }; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index 19c97473..3db18304 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -214,6 +214,7 @@ class SampleViewerDecorator extends interactivity.ADecorator { this.registerBehaveEngineNode("event/onSelect", interactivity.OnSelect); this.registerBehaveEngineNode("event/onHoverIn", interactivity.OnHoverIn); this.registerBehaveEngineNode("event/onHoverOut", interactivity.OnHoverOut); + this.registerRigidBodyNodes(); } dispatchCustomEvent(eventName, data) { @@ -873,6 +874,18 @@ class SampleViewerDecorator extends interactivity.ADecorator { animation.endCallback = callback; animation.createdTimestamp = this.world.animationTimer.elapsedSec(); } + + rayCastRigidBodies(rayStart, rayEnd) { + return this.world.physicsController.rayCast(rayStart, rayEnd); + } + + applyImpulseToRigidBody(nodeIndex, linearImpulse, angularImpulse) { + this.world.physicsController.applyImpulse(nodeIndex, linearImpulse, angularImpulse); + } + + applyPointImpulseToRigidBody(nodeIndex, impulse, position) { + this.world.physicsController.applyPointImpulse(nodeIndex, impulse, position); + } } export { gltfGraph, GraphController }; From b1dfb722fdb6a34e2382504aca9b43e70ef56ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 27 Jan 2026 18:39:09 +0100 Subject: [PATCH 50/93] WIP add triggers --- source/GltfState/phyiscs_controller.js | 48 +++++++++++++++++++++----- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 5b4cba9d..aa1fe0a8 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -95,6 +95,8 @@ class PhysicsController { this.staticActors = []; this.kinematicActors = []; this.dynamicActors = []; + this.triggerNodes = []; + this.compoundTriggerNodes = []; this.jointNodes = []; this.morphedColliders = []; this.skinnedColliders = []; @@ -257,6 +259,13 @@ class PhysicsController { if (rigidBody.joint !== undefined) { this.jointNodes.push(node); } + if (rigidBody.trigger !== undefined) { + if (rigidBody.trigger.nodes !== undefined) { + this.compoundTriggerNodes.push(node); + } else { + this.triggerNodes.push(node); + } + } } for (const childIndex of node.children) { gatherRigidBodies(childIndex, parentRigidBody); @@ -270,7 +279,8 @@ class PhysicsController { !this.engine || (this.staticActors.length === 0 && this.kinematicActors.length === 0 && - this.dynamicActors.length === 0) + this.dynamicActors.length === 0 && + this.triggerNodes.length === 0) ) { return; } @@ -280,6 +290,7 @@ class PhysicsController { this.kinematicActors, this.dynamicActors, this.jointNodes, + this.triggerNodes, this.hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -291,6 +302,8 @@ class PhysicsController { this.kinematicActors = []; this.dynamicActors = []; this.jointNodes = []; + this.triggerNodes = []; + this.compoundTriggerNodes = []; this.morphedColliders = []; this.skinnedColliders = []; this.hasRuntimeAnimationTargets = false; @@ -398,6 +411,10 @@ class PhysicsController { this.updateColliders(state, actorNode); } + for (const node of this.triggerNodes) { + this.updateColliders(state, node); + } + for (const jointNode of this.jointNodes) { this.engine.updatePhysicsJoint(state, jointNode); //TODO } @@ -443,6 +460,7 @@ class PhysicsInterface { kinematicActors, dynamicActors, jointNodes, + triggerNodes, hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -1264,6 +1282,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { createShape( gltf, node, + collider, shapeFlags, physXMaterial, physXFilterData, @@ -1271,7 +1290,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create() ) { - const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; let geometry = undefined; if (collider?.geometry?.shape !== undefined) { if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { @@ -1321,6 +1339,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const pxShapeMap = new Map(); if (type === "static") { actor = this.physics.createRigidStatic(pose); + } else if (type === "trigger") { + actor = this.physics.createRigidStatic(pose); + actor.setActorFlag(this.PhysX.PxActorFlagEnum.eTRIGGER_ENABLE, true); } else { actor = this.physics.createRigidDynamic(pose); if (type === "kinematic") { @@ -1387,6 +1408,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const shape = this.createShape( gltf, node, + collider, shapeFlags, material, physXFilterData, @@ -1409,7 +1431,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } }; - const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + let collider = undefined; + if (type === "trigger") { + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + } else { + collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + } + createAndAddShape(gltf, node, collider, node, worldTransform); for (const childIndex of node.children) { @@ -1809,6 +1837,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { kinematicActors, dynamicActors, jointNodes, + triggerNodes, hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -1844,6 +1873,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxShapeFlagEnum.eVISUALIZATION ); + const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); + const alwaysConvexMeshes = dynamicMeshColliderCount > 1 || (staticMeshColliderCount > 0 && dynamicMeshColliderCount > 0); @@ -1857,6 +1888,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const node of dynamicActors) { this.createActor(state.gltf, node, shapeFlags, "dynamic", true); } + for (const node of triggerNodes) { + this.createActor(state.gltf, node, triggerFlags, "trigger"); + } for (const node of jointNodes) { this.createJoint(state.gltf, node); } @@ -2110,13 +2144,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return result; } - applyImpulse(nodeIndex, linearImpulse, angularImpulse) { - this.engine.applyImpulse(nodeIndex, linearImpulse, angularImpulse); - } + applyImpulse(nodeIndex, linearImpulse, angularImpulse) {} - applyPointImpulse(nodeIndex, impulse, position) { - this.engine.applyPointImpulse(nodeIndex, impulse, position); - } + applyPointImpulse(nodeIndex, impulse, position) {} rayCast(rayStart, rayEnd) { const result = {}; From 56245e4563ec7fe798781c3d45dadaa6881c2824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 29 Jan 2026 14:44:22 +0100 Subject: [PATCH 51/93] Create trigger actors and shapes --- source/GltfState/phyiscs_controller.js | 98 +++++++++++++++++--------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index aa1fe0a8..0d089c2d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -97,6 +97,7 @@ class PhysicsController { this.dynamicActors = []; this.triggerNodes = []; this.compoundTriggerNodes = []; + this.nodeToMotion = new Map(); this.jointNodes = []; this.morphedColliders = []; this.skinnedColliders = []; @@ -267,6 +268,10 @@ class PhysicsController { } } } + + if (parentRigidBody !== undefined) { + this.nodeToMotion.set(node.gltfObjectIndex, parentRigidBody.gltfObjectIndex); + } for (const childIndex of node.children) { gatherRigidBodies(childIndex, parentRigidBody); } @@ -291,6 +296,7 @@ class PhysicsController { this.dynamicActors, this.jointNodes, this.triggerNodes, + this.nodeToMotion, this.hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -303,6 +309,7 @@ class PhysicsController { this.dynamicActors = []; this.jointNodes = []; this.triggerNodes = []; + this.nodeToMotion.clear(); this.compoundTriggerNodes = []; this.morphedColliders = []; this.skinnedColliders = []; @@ -362,15 +369,21 @@ class PhysicsController { } } - updateColliders(state, node) { + updateColliders(state, node, isTrigger = false) { this.engine.updateActorTransform(node); - const collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + + let collider = undefined; + if (isTrigger) { + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + } else { + collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + } if (collider?.geometry?.shape !== undefined || collider?.geometry?.mesh !== undefined) { this.engine.updateCollider( state.gltf, node, - node.extensions?.KHR_physics_rigid_bodies?.collider, + collider, node, node.worldTransform, undefined, @@ -379,17 +392,24 @@ class PhysicsController { ); } - for (const childIndex of node.children) { - const childNode = state.gltf.nodes[childIndex]; - PhysicsUtils.recurseCollider( - state.gltf, - childNode, - undefined, - node, - node.dirtyScale, - node.dirtyScale, - this.engine.updateCollider.bind(this.engine) - ); + if (!isTrigger) { + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + if (isTrigger) { + collider = childNode.extensions?.KHR_physics_rigid_bodies?.trigger; + } else { + collider = childNode.extensions?.KHR_physics_rigid_bodies?.collider; + } + PhysicsUtils.recurseCollider( + state.gltf, + childNode, + collider, + node, + node.dirtyScale, + node.dirtyScale, + this.engine.updateCollider.bind(this.engine) + ); + } } } @@ -412,7 +432,7 @@ class PhysicsController { } for (const node of this.triggerNodes) { - this.updateColliders(state, node); + this.updateColliders(state, node, true); } for (const jointNode of this.jointNodes) { @@ -461,6 +481,7 @@ class PhysicsInterface { dynamicActors, jointNodes, triggerNodes, + nodeToMotion, hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -578,7 +599,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // Needs to be reset for each scene this.scene = undefined; this.nodeToActor = new Map(); + this.nodeToMotion = new Map(); this.nodeToJoint = new Map(); + this.nodeToTrigger = new Map(); this.shapeToNode = new Map(); this.filterData = []; this.physXFilterData = []; @@ -767,12 +790,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } updateCollider(gltf, node, collider, actorNode, worldTransform, offsetChanged, scaleChanged) { - const glTFCollider = node.extensions?.KHR_physics_rigid_bodies?.collider; const { actor, pxShapeMap } = this.nodeToActor.get(actorNode.gltfObjectIndex); const currentShape = pxShapeMap.get(node.gltfObjectIndex); let currentGeometry = currentShape.getGeometry(); const currentColliderType = currentGeometry.getType(); - const shapeIndex = glTFCollider?.shape; + const shapeIndex = collider?.shape; let scale = vec3.fromValues(1, 1, 1); let scaleAxis = quat.create(); if (shapeIndex !== undefined) { @@ -815,7 +837,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined, undefined, undefined /*TODO*/, - glTFCollider + collider ); pxShapeMap.set(node.gltfObjectIndex, shape); actor.detachShape(currentShape); @@ -1341,7 +1363,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { actor = this.physics.createRigidStatic(pose); } else if (type === "trigger") { actor = this.physics.createRigidStatic(pose); - actor.setActorFlag(this.PhysX.PxActorFlagEnum.eTRIGGER_ENABLE, true); + actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_SIMULATION, true); } else { actor = this.physics.createRigidDynamic(pose); if (type === "kinematic") { @@ -1440,17 +1462,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { createAndAddShape(gltf, node, collider, node, worldTransform); - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - PhysicsUtils.recurseCollider( - gltf, - childNode, - undefined, - node, - node.dirtyScale, - node.dirtyScale, - createAndAddShape - ); + if (type !== "trigger") { + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + PhysicsUtils.recurseCollider( + gltf, + childNode, + undefined, + node, + node.dirtyScale, + node.dirtyScale, + createAndAddShape + ); + } } this.PhysX.destroy(pos); @@ -1458,7 +1482,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(pose); this.scene.addActor(actor); - this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); + if (type === "trigger") { + this.nodeToTrigger.set(node.gltfObjectIndex, actor); + } else { + this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); + } } computeJointOffsetAndActor(node) { @@ -1838,6 +1866,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { dynamicActors, jointNodes, triggerNodes, + nodeToMotion, hasRuntimeAnimationTargets, staticMeshColliderCount, dynamicMeshColliderCount @@ -1845,6 +1874,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (!this.PhysX) { return; } + this.nodeToMotion = nodeToMotion; this.generateSimpleShapes(state.gltf); this.computeFilterData(state.gltf); for (let i = 0; i < this.filterData.length; i++) { @@ -1889,7 +1919,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.createActor(state.gltf, node, shapeFlags, "dynamic", true); } for (const node of triggerNodes) { - this.createActor(state.gltf, node, triggerFlags, "trigger"); + this.createActor(state.gltf, node, triggerFlags, "trigger", true); } for (const node of jointNodes) { this.createJoint(state.gltf, node); @@ -2117,6 +2147,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { actor.actor.release(); } + for (const trigger of this.nodeToTrigger.values()) { + trigger.release(); + } + this.nodeToActor.clear(); if (scenePointer) { scenePointer.release(); From df3d93020e0970456cb554d4f0aa5862494ad7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 29 Jan 2026 14:44:37 +0100 Subject: [PATCH 52/93] Add implementation for applyImpulse --- source/GltfState/phyiscs_controller.js | 48 ++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 0d089c2d..01c1d1e6 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2178,9 +2178,53 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return result; } - applyImpulse(nodeIndex, linearImpulse, angularImpulse) {} + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { + if (!this.scene) { + return; + } + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; + } + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; - applyPointImpulse(nodeIndex, impulse, position) {} + const linImpulse = new this.PhysX.PxVec3(...linearImpulse); + const angImpulse = new this.PhysX.PxVec3(...angularImpulse); + actor.addForce(linImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + actor.addTorque(angImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + this.PhysX.destroy(linImpulse); + this.PhysX.destroy(angImpulse); + } + + applyPointImpulse(nodeIndex, impulse, position) { + if (!this.scene) { + return; + } + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; + } + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; + + const pxImpulse = new this.PhysX.PxVec3(...impulse); + const pxPosition = new this.PhysX.PxVec3(...position); + this.PhysX.PxRigidBodyExt.prototype.addForceAtPos( + actor, + pxImpulse, + pxPosition, + this.PhysX.PxForceModeEnum.eIMPULSE + ); + this.PhysX.destroy(pxImpulse); + this.PhysX.destroy(pxPosition); + } rayCast(rayStart, rayEnd) { const result = {}; From dcd09b1bcec5d5396f22cd683a1490a17ef824df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 30 Jan 2026 15:51:19 +0100 Subject: [PATCH 53/93] WIP fix trigger callback --- source/GltfState/phyiscs_controller.js | 78 +++++++++++++++++++------- source/gltf/node.js | 2 +- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 01c1d1e6..dc0e9908 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -225,14 +225,14 @@ class PhysicsController { this.dynamicActors.push(node); } parentRigidBody = node; - } else if (currentRigidBody === undefined) { + } else if (currentRigidBody === undefined && rigidBody.collider !== undefined) { if (animatedNodeIndices.has(node.gltfObjectIndex)) { this.kinematicActors.push(node); } else { this.staticActors.push(node); } } - if (rigidBody.collider?.geometry?.node !== undefined) { + if (rigidBody.collider?.geometry?.mesh !== undefined) { if (!rigidBody.collider.geometry.convexHull) { if ( parentRigidBody === undefined || @@ -248,14 +248,6 @@ class PhysicsController { } } } - const colliderNodeIndex = rigidBody.collider.geometry.node; - const colliderNode = state.gltf.nodes[colliderNodeIndex]; - if (colliderNode.skin !== undefined) { - this.skinnedColliders.push(colliderNode); - } - if (morphedNodeIndices.has(colliderNodeIndex)) { - this.morphedColliders.push(colliderNode); - } } if (rigidBody.joint !== undefined) { this.jointNodes.push(node); @@ -370,7 +362,7 @@ class PhysicsController { } updateColliders(state, node, isTrigger = false) { - this.engine.updateActorTransform(node); + this.engine.updateActorTransform(node, isTrigger); let collider = undefined; if (isTrigger) { @@ -386,9 +378,9 @@ class PhysicsController { collider, node, node.worldTransform, - undefined, false, - node.dirtyScale + node.dirtyScale, + isTrigger ); } @@ -660,9 +652,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - updateActorTransform(node) { + updateActorTransform(node, isTrigger = false) { if (node.dirtyTransform) { - const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; + const actor = isTrigger + ? this.nodeToTrigger.get(node.gltfObjectIndex)?.actor + : this.nodeToActor.get(node.gltfObjectIndex)?.actor; if (actor === undefined) { return; } @@ -789,9 +783,22 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - updateCollider(gltf, node, collider, actorNode, worldTransform, offsetChanged, scaleChanged) { - const { actor, pxShapeMap } = this.nodeToActor.get(actorNode.gltfObjectIndex); - const currentShape = pxShapeMap.get(node.gltfObjectIndex); + updateCollider( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger = false + ) { + const lookup = isTrigger ? this.nodeToTrigger : this.nodeToActor; + + const result = lookup.get(actorNode.gltfObjectIndex); + const actor = result?.actor; + const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); + let currentGeometry = currentShape.getGeometry(); const currentColliderType = currentGeometry.getType(); const shapeIndex = collider?.shape; @@ -1483,7 +1490,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.scene.addActor(actor); if (type === "trigger") { - this.nodeToTrigger.set(node.gltfObjectIndex, actor); + this.nodeToTrigger.set(node.gltfObjectIndex, { + actor, + pxShapeMap: pxShapeMap + }); } else { this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } @@ -1896,6 +1906,34 @@ class NvidiaPhysicsInterface extends PhysicsInterface { sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); this.scene = this.physics.createScene(sceneDesc); + + if (triggerNodes.length > 0) { + console.log("Enabling trigger report callback"); + const triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); + triggerCallback.onTrigger = (pairs, count) => { + for (let i = 0; i < count; i++) { + const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); + const triggerShape = pair.triggerShape; + const otherShape = pair.otherShape; + const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); + const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); + const triggerNode = state.gltf.nodes[triggerNodeIndex]; + const otherNode = state.gltf.nodes[otherNodeIndex]; + console.log( + "Trigger event:", + pair.status, + "Trigger:", + triggerNodeIndex, + "Other:", + otherNodeIndex, + "Flags:", + pair.flags + ); + } + }; + this.scene.setSimulationEventCallback(triggerCallback); + } + console.log("Created scene"); const shapeFlags = new this.PhysX.PxShapeFlags( this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | @@ -2148,7 +2186,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } for (const trigger of this.nodeToTrigger.values()) { - trigger.release(); + trigger.actor.release(); } this.nodeToActor.clear(); diff --git a/source/gltf/node.js b/source/gltf/node.js index c15b2069..53531869 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -253,7 +253,7 @@ class KHR_physics_rigid_bodies_trigger extends GltfObject { constructor() { super(); this.geometry = undefined; - this.nodes = []; + this.nodes = undefined; this.collisionFilter = undefined; } } From 2d8b7fbcb381a375c21ef33fffd018cad2acbad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 4 Feb 2026 17:52:08 +0100 Subject: [PATCH 54/93] WIP add composite triggers --- source/GltfState/phyiscs_controller.js | 118 +++++++++++++++++++------ source/gltf/interactivity.js | 22 +++++ 2 files changed, 115 insertions(+), 25 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index dc0e9908..cc93fbf3 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -96,7 +96,8 @@ class PhysicsController { this.kinematicActors = []; this.dynamicActors = []; this.triggerNodes = []; - this.compoundTriggerNodes = []; + this.compoundTriggerNodes = new Map(); // Map of compound trigger node index to set of included colliders + this.triggerToCompound = new Map(); // Map of trigger node index to compound trigger node index this.nodeToMotion = new Map(); this.jointNodes = []; this.morphedColliders = []; @@ -254,7 +255,14 @@ class PhysicsController { } if (rigidBody.trigger !== undefined) { if (rigidBody.trigger.nodes !== undefined) { - this.compoundTriggerNodes.push(node); + this.compoundTriggerNodes.set(node.gltfObjectIndex, { + previous: new Set(), + added: new Set(), + removed: new Set() + }); + for (const triggerNodeIndex of rigidBody.trigger.nodes) { + this.triggerToCompound.set(triggerNodeIndex, node.gltfObjectIndex); + } } else { this.triggerNodes.push(node); } @@ -302,7 +310,8 @@ class PhysicsController { this.jointNodes = []; this.triggerNodes = []; this.nodeToMotion.clear(); - this.compoundTriggerNodes = []; + this.compoundTriggerNodes.clear(); + this.triggerToCompound.clear(); this.morphedColliders = []; this.skinnedColliders = []; this.hasRuntimeAnimationTargets = false; @@ -1270,7 +1279,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return physxMaterial; } - createPhysXCollisionFilter(collisionFilter) { + createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { let word0 = null; let word1 = null; if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { @@ -1282,7 +1291,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { word1 = Math.pow(2, 32) - 1; } - const additionalFlags = 0; return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); } @@ -1366,11 +1374,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const pose = new this.PhysX.PxTransform(pos, rotation); let actor = null; const pxShapeMap = new Map(); - if (type === "static") { - actor = this.physics.createRigidStatic(pose); - } else if (type === "trigger") { + if (type === "static" || type === "trigger") { actor = this.physics.createRigidStatic(pose); - actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_SIMULATION, true); } else { actor = this.physics.createRigidDynamic(pose); if (type === "kinematic") { @@ -1906,34 +1911,93 @@ class NvidiaPhysicsInterface extends PhysicsInterface { sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); this.scene = this.physics.createScene(sceneDesc); + let triggerCallback = undefined; if (triggerNodes.length > 0) { console.log("Enabling trigger report callback"); - const triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); + triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); triggerCallback.onTrigger = (pairs, count) => { + for (const compoundTrigger of state.physicsController.compoundTriggerNodes.values()) { + compoundTrigger.added.clear(); + compoundTrigger.removed.clear(); + } + console.log("Trigger callback called with", count, "pairs"); for (let i = 0; i < count; i++) { const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); const triggerShape = pair.triggerShape; const otherShape = pair.otherShape; const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); - const triggerNode = state.gltf.nodes[triggerNodeIndex]; - const otherNode = state.gltf.nodes[otherNodeIndex]; - console.log( - "Trigger event:", - pair.status, - "Trigger:", - triggerNodeIndex, - "Other:", - otherNodeIndex, - "Flags:", - pair.flags - ); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + state.graphController.rigidBodyTriggerEntered( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex), + true + ); + } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { + state.graphController.rigidBodyTriggerExited( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex), + false + ); + } + const compoundTriggers = + state.physicsController.triggerToCompoundTrigger.get(triggerNodeIndex); + if (compoundTriggers !== undefined) { + for (const compoundTriggerIndex of compoundTriggers) { + const compoundTriggerInfo = + this.compoundTriggerNodes.get(compoundTriggerIndex); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + compoundTriggerInfo.added.add(otherNodeIndex); + } else if ( + pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST + ) { + compoundTriggerInfo.removed.add(otherNodeIndex); + } + } + } + } + + for (const [ + idx, + compoundTrigger + ] of state.physicsController.compoundTriggerNodes.entries()) { + for (const addedNodeIndex of compoundTrigger.added) { + if (!compoundTrigger.previous.has(addedNodeIndex)) { + state.graphController.rigidBodyTriggerEntered( + idx, + addedNodeIndex, + nodeToMotion.get(addedNodeIndex), + true + ); + } + } + for (const removedNodeIndex of compoundTrigger.removed) { + if ( + compoundTrigger.previous.has(removedNodeIndex) && + !compoundTrigger.added.has(removedNodeIndex) + ) { + state.graphController.rigidBodyTriggerEntered( + idx, + removedNodeIndex, + nodeToMotion.get(removedNodeIndex), + false + ); + } + } } }; - this.scene.setSimulationEventCallback(triggerCallback); + triggerCallback.onConstraintBreak = (constraints, count) => {}; + triggerCallback.onWake = (actors, count) => {}; + triggerCallback.onSleep = (actors, count) => {}; + triggerCallback.onContact = (pairHeaders, pairs, count) => {}; + sceneDesc.simulationEventCallback = triggerCallback; } + this.scene = this.physics.createScene(sceneDesc); + console.log("Created scene"); const shapeFlags = new this.PhysX.PxShapeFlags( this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | @@ -1941,7 +2005,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.PxShapeFlagEnum.eVISUALIZATION ); - const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); + const triggerFlags = new this.PhysX.PxShapeFlags( + this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE | this.PhysX.PxShapeFlagEnum.eVISUALIZATION + ); const alwaysConvexMeshes = dynamicMeshColliderCount > 1 || @@ -2077,7 +2143,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } this.scene.simulate(deltaTime); - this.scene.fetchResults(true); + if (!this.scene.fetchResults(true)) { + console.warn("PhysX: fetchResults failed"); + } for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index 3db18304..47827429 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -65,6 +65,28 @@ class GraphController { } } + rigidBodyTriggerEntered(triggerNodeIndex, colliderNodeIndex, motionNodeIndex) { + if (this.graphIndex === undefined) { + return; + } + this.decorator.rigidBodyTriggerEntered( + triggerNodeIndex, + colliderNodeIndex, + motionNodeIndex ?? -1 + ); + } + + rigidBodyTriggerExited(triggerNodeIndex, colliderNodeIndex, motionNodeIndex) { + if (this.graphIndex === undefined) { + return; + } + this.decorator.rigidBodyTriggerExited( + triggerNodeIndex, + colliderNodeIndex, + motionNodeIndex ?? -1 + ); + } + /** * Initialize the graph controller with the given state. * This needs to be called every time a glTF assets is loaded. From 28a16883059acbf69a74fa60780c02ca499a59cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 5 Feb 2026 12:10:58 +0100 Subject: [PATCH 55/93] Fix compound triggers --- source/GltfState/phyiscs_controller.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index cc93fbf3..7bc1891b 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -261,7 +261,16 @@ class PhysicsController { removed: new Set() }); for (const triggerNodeIndex of rigidBody.trigger.nodes) { - this.triggerToCompound.set(triggerNodeIndex, node.gltfObjectIndex); + if (this.triggerToCompound.has(triggerNodeIndex)) { + this.triggerToCompound + .get(triggerNodeIndex) + .add(node.gltfObjectIndex); + } else { + this.triggerToCompound.set( + triggerNodeIndex, + new Set([node.gltfObjectIndex]) + ); + } } } else { this.triggerNodes.push(node); @@ -1944,11 +1953,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ); } const compoundTriggers = - state.physicsController.triggerToCompoundTrigger.get(triggerNodeIndex); + state.physicsController.triggerToCompound.get(triggerNodeIndex); if (compoundTriggers !== undefined) { for (const compoundTriggerIndex of compoundTriggers) { const compoundTriggerInfo = - this.compoundTriggerNodes.get(compoundTriggerIndex); + state.physicsController.compoundTriggerNodes.get( + compoundTriggerIndex + ); if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { compoundTriggerInfo.added.add(otherNodeIndex); } else if ( From e7be5a2074fe5e389d40ebc6a9965d299f9050fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 6 Feb 2026 18:13:41 +0100 Subject: [PATCH 56/93] use ref counting for composed triggers --- source/GltfState/phyiscs_controller.js | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 7bc1891b..50efb076 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -256,7 +256,7 @@ class PhysicsController { if (rigidBody.trigger !== undefined) { if (rigidBody.trigger.nodes !== undefined) { this.compoundTriggerNodes.set(node.gltfObjectIndex, { - previous: new Set(), + previous: new Map(), //ref counting added: new Set(), removed: new Set() }); @@ -1941,15 +1941,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { state.graphController.rigidBodyTriggerEntered( triggerNodeIndex, otherNodeIndex, - nodeToMotion.get(otherNodeIndex), - true + nodeToMotion.get(otherNodeIndex) ); } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { state.graphController.rigidBodyTriggerExited( triggerNodeIndex, otherNodeIndex, - nodeToMotion.get(otherNodeIndex), - false + nodeToMotion.get(otherNodeIndex) ); } const compoundTriggers = @@ -1977,24 +1975,27 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ] of state.physicsController.compoundTriggerNodes.entries()) { for (const addedNodeIndex of compoundTrigger.added) { if (!compoundTrigger.previous.has(addedNodeIndex)) { + compoundTrigger.previous.set(addedNodeIndex, 1); state.graphController.rigidBodyTriggerEntered( idx, addedNodeIndex, - nodeToMotion.get(addedNodeIndex), - true + nodeToMotion.get(addedNodeIndex) ); + } else { + const currentCount = compoundTrigger.previous.get(addedNodeIndex); + compoundTrigger.previous.set(addedNodeIndex, currentCount + 1); } } for (const removedNodeIndex of compoundTrigger.removed) { - if ( - compoundTrigger.previous.has(removedNodeIndex) && - !compoundTrigger.added.has(removedNodeIndex) - ) { - state.graphController.rigidBodyTriggerEntered( + const currentCount = compoundTrigger.previous.get(removedNodeIndex); + if (currentCount > 1) { + compoundTrigger.previous.set(removedNodeIndex, currentCount - 1); + } else { + compoundTrigger.previous.delete(removedNodeIndex); + state.graphController.rigidBodyTriggerExited( idx, removedNodeIndex, - nodeToMotion.get(removedNodeIndex), - false + nodeToMotion.get(removedNodeIndex) ); } } From aa4194af1694f7fe8ae7aec253ef10f6166ac359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 10 Feb 2026 12:22:26 +0100 Subject: [PATCH 57/93] Handle triangle strips and triangle fans --- source/GltfState/phyiscs_controller.js | 110 +++++++++++++++++++------ 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 50efb076..6616f602 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -33,6 +33,62 @@ class PhysicsUtils { return { scale: scaleFactor, scaleAxis: scaleRotation }; } + /** + * Converts triangle strip indices to triangle list indices + * @param {Uint32Array|Array} stripIndices - The triangle strip indices + * @returns {Uint32Array} - Triangle list indices + */ + static convertTriangleStripToTriangles(stripIndices) { + if (stripIndices.length < 3) { + return new Uint32Array(0); + } + + const triangleCount = stripIndices.length - 2; + const triangleIndices = new Uint32Array(triangleCount * 3); + let triangleIndex = 0; + + for (let i = 0; i < triangleCount; i++) { + if (i % 2 === 0) { + // Even triangle: maintain winding order + triangleIndices[triangleIndex++] = stripIndices[i]; + triangleIndices[triangleIndex++] = stripIndices[i + 1]; + triangleIndices[triangleIndex++] = stripIndices[i + 2]; + } else { + // Odd triangle: reverse winding order + triangleIndices[triangleIndex++] = stripIndices[i]; + triangleIndices[triangleIndex++] = stripIndices[i + 2]; + triangleIndices[triangleIndex++] = stripIndices[i + 1]; + } + } + + return triangleIndices; + } + + /** + * Converts triangle fan indices to triangle list indices + * @param {Uint32Array|Array} fanIndices - The triangle fan indices + * @returns {Uint32Array} - Triangle list indices + */ + static convertTriangleFanToTriangles(fanIndices) { + if (fanIndices.length < 3) { + return new Uint32Array(0); + } + + const triangleCount = fanIndices.length - 2; + const triangleIndices = new Uint32Array(triangleCount * 3); + let triangleIndex = 0; + + const centerVertex = fanIndices[0]; + + for (let i = 1; i < fanIndices.length - 1; i++) { + triangleIndices[triangleIndex++] = fanIndices[i]; + triangleIndices[triangleIndex++] = fanIndices[i + 1]; + triangleIndices[triangleIndex++] = centerVertex; + } + + return triangleIndices; + } + static recurseCollider( gltf, node, @@ -864,7 +920,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined /*TODO*/, collider ); - pxShapeMap.set(node.gltfObjectIndex, shape); + result?.pxShapeMap.set(node.gltfObjectIndex, shape); actor.detachShape(currentShape); actor.attachShape(shape); currentGeometry = newGeometry; @@ -1057,12 +1113,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } - createConvexPxMesh( - vertices, - indices, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create() - ) { + createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { const malloc = (f, q) => { const nDataBytes = f.length * f.BYTES_PER_ELEMENT; if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); @@ -1076,12 +1127,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { des.points.data = malloc(vertices); let flag = 0; - flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eCOMPUTE_CONVEX(); - flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eQUANTIZE_INPUT(); - flag |= this.PhysX._emscripten_enum_PxConvexFlagEnum_eDISABLE_MESH_VALIDATION(); + flag |= this.PhysX.PxConvexFlagEnum.eCOMPUTE_CONVEX; + flag |= this.PhysX.PxConvexFlagEnum.eSHIFT_VERTICES; + //flag |= this.PhysX.PxConvexFlagEnum.eDISABLE_MESH_VALIDATION; const pxflags = new this.PhysX.PxConvexFlags(flag); des.flags = pxflags; const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + cookingParams.planeTolerance = 0.0007; //Default const tri = this.PhysX.CreateConvexMesh(cookingParams, des); this.convexMeshes.push(tri); @@ -1099,8 +1151,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } - collectVerticesAndIndicesFromMesh(gltf, mesh) { - // TODO Handle different primitive modes + collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { let positionDataArray = []; let positionCount = 0; let indexDataArray = []; @@ -1141,14 +1192,27 @@ class NvidiaPhysicsInterface extends PhysicsInterface { positionDataArray.push(positionData); positionCount += positionAccessor.count; - if (primitive.indices !== undefined) { - const indexAccessor = gltf.accessors[primitive.indices]; - indexDataArray.push(indexAccessor.getNormalizedDeinterlacedView(gltf)); - indexCount += indexAccessor.count; - } else { - const array = Array.from(Array(positionAccessor.count).keys()); - indexDataArray.push(new Uint32Array(array)); - indexCount += positionAccessor.count; + if (computeIndices) { + let indexData = undefined; + if (primitive.indices !== undefined) { + const indexAccessor = gltf.accessors[primitive.indices]; + indexData = indexAccessor.getNormalizedDeinterlacedView(gltf); + } else { + const array = Array.from(Array(positionAccessor.count).keys()); + indexData = new Uint32Array(array); + } + if (primitive.mode === 5) { + indexData = PhysicsUtils.convertTriangleStripToTriangles(indexData); + } else if (primitive.mode === 6) { + indexData = PhysicsUtils.convertTriangleFanToTriangles(indexData); + } else if (primitive.mode !== undefined && primitive.mode !== 4) { + console.warn( + "Unsupported primitive mode for physics mesh collider creation: " + + primitive.mode + ); + } + indexDataArray.push(indexData); + indexCount += indexData.length; } } @@ -1168,12 +1232,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh); - return this.createConvexPxMesh(vertices, indices, scale, scaleAxis); + const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); + return this.createConvexPxMesh(result.vertices, scale, scaleAxis); } createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh); + const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); const malloc = (f, q) => { const nDataBytes = f.length * f.BYTES_PER_ELEMENT; if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); From f3b05fe48e9753ae3b893b7aa3e4ea95874717ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 10 Feb 2026 12:22:36 +0100 Subject: [PATCH 58/93] Update physx --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bf026e1..f3d52fe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "2.6.2" + "physx-js-webidl": "2.7.1" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -3252,9 +3252,9 @@ } }, "node_modules/physx-js-webidl": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.6.2.tgz", - "integrity": "sha512-10JMdenjZCJgNVOvLsbIcNGDw1mIPu0sFU7G5KB6eQviu4T9Hua6o+MHWC5frva8Ey9BlYNBjQC42LhTnhYQug==" + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.1.tgz", + "integrity": "sha512-D838Ng/1Qx419L6ySuTnEgFwtFAKWp0cb8voHt1HKjfrr5hMc/Ms/llYDG5HCH72+1EtIInWIwV/HcNeM/C4Gg==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6819,9 +6819,9 @@ "dev": true }, "physx-js-webidl": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.6.2.tgz", - "integrity": "sha512-10JMdenjZCJgNVOvLsbIcNGDw1mIPu0sFU7G5KB6eQviu4T9Hua6o+MHWC5frva8Ey9BlYNBjQC42LhTnhYQug==" + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.1.tgz", + "integrity": "sha512-D838Ng/1Qx419L6ySuTnEgFwtFAKWp0cb8voHt1HKjfrr5hMc/Ms/llYDG5HCH72+1EtIInWIwV/HcNeM/C4Gg==" }, "picomatch": { "version": "2.3.1", diff --git a/package.json b/package.json index a081fdff..b6b65fa6 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "2.6.2" + "physx-js-webidl": "2.7.1" }, "devDependencies": { "@playwright/test": "^1.56.0", From 040bfa5e18cc3effd2a9b9b6fbbcc77081a84897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 10 Feb 2026 12:53:03 +0100 Subject: [PATCH 59/93] Resolve remaining TODOs --- source/GltfState/phyiscs_controller.js | 58 ++++++++++++-------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 6616f602..854749a3 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -167,18 +167,6 @@ class PhysicsController { this.timeAccumulator = 0; this.pauseTime = undefined; this.skipFrames = 0; // Skip the first two simulation frames to allow engine to initialize - - //TODO PxShape needs to be recreated if collisionFilter differs - //TODO Cache geometries for faster computation - // PxShape has localTransform which applies to all actors using the shape - // setGlobalPos can move static actors in a non physically accurate way and dynamic actors in a physically accurate way - // otherwise use kinematic actors for physically accurate movement of static actors - - // MORPH: Call PxShape::setGeometry on each shape which references the mesh, to ensure that internal data structures are updated to reflect the new geometry. - - // Which scale affects the collider geometry? - - // Different primitive modes? } calculateMorphColliders(gltf) { @@ -228,18 +216,6 @@ class PhysicsController { } } - calculateSkinnedColliders(gltf) { - for (const node of this.skinnedColliders) { - const mesh = gltf.meshes[node.mesh]; - const skin = gltf.skins[node.skin]; - const inverseBindMatricesAccessor = gltf.accessors[skin.inverseBindMatrices]; - const inverseBindMatrices = - inverseBindMatricesAccessor.getNormalizedDeinterlacedView(gltf); - const jointNodes = skin.joints.map((jointIndex) => gltf.nodes[jointIndex]); - //TODO: Implement skinned collider calculation - } - } - async initializeEngine(engine) { if (engine === "NvidiaPhysX") { this.engine = new NvidiaPhysicsInterface(); @@ -913,11 +889,20 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (newGeometry.getType() !== currentColliderType) { // We need to recreate the shape this.PhysX.destroy(currentShape); + let shapeFlags = undefined; + if (isTrigger) { + shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; + } else { + shapeFlags = this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE; + } + if (this.debugColliders) { + shapeFlags |= this.PhysX.PxShapeFlagEnum.eVISUALIZATION; + } const shape = this.createShapeFromGeometry( newGeometry, undefined, undefined, - undefined /*TODO*/, + shapeFlags, collider ); result?.pxShapeMap.set(node.gltfObjectIndex, shape); @@ -1664,8 +1649,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(poseA); this.PhysX.destroy(poseB); - //TODO toogle debug view - physxJoint.setConstraintFlag(this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, true); + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); this.nodeToJoint.set(node.gltfObjectIndex, physxJoint); @@ -1945,6 +1932,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.debugJoints ? 1 : 0 ); this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); + for (const joint of this.nodeToJoint.values()) { + joint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); + } + for (const shapePtr of this.shapeToNode.keys()) { + const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); + shape.setFlag(this.PhysX.PxShapeFlagEnum.eVISUALIZATION, this.debugColliders); + } } initializeSimulation( @@ -2077,13 +2074,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { console.log("Created scene"); const shapeFlags = new this.PhysX.PxShapeFlags( this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | - this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE | - this.PhysX.PxShapeFlagEnum.eVISUALIZATION + this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE ); - const triggerFlags = new this.PhysX.PxShapeFlags( - this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE | this.PhysX.PxShapeFlagEnum.eVISUALIZATION - ); + const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); const alwaysConvexMeshes = dynamicMeshColliderCount > 1 || From 2f265afa05bb1bbbde5e328de5f45d4ba80bf0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 10 Feb 2026 13:00:47 +0100 Subject: [PATCH 60/93] Fix cleanup --- source/GltfState/phyiscs_controller.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 854749a3..cdd9f153 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1026,7 +1026,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { quat.equals(scaleAxis, quat.create()) === false ) { const data = createBoxVertexData(x, y, z); - return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } let geometry = undefined; if (referenceType === "eBOX") { @@ -1050,7 +1050,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { @@ -1061,13 +1061,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scale[0] !== scale[2] ) { const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } height *= scale[1]; radiusTop *= scale[0]; radiusBottom *= scale[0]; const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, data.indices); + return this.createConvexPxMesh(data.vertices); } generateSphere(radius, scale, scaleAxis, reference) { @@ -1077,7 +1077,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } if (scale.every((value) => value === scale[0]) === false) { const data = createCapsuleVertexData(radius, radius, 0); - return this.createConvexPxMesh(data.vertices, data.indices, scale, scaleAxis); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } else { radius *= scale[0]; } @@ -2328,6 +2328,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } this.nodeToActor.clear(); + this.nodeToTrigger.clear(); + if (scenePointer) { scenePointer.release(); } From 4d789a1f20a78f9439924a9eb6d7419e2baf182f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 10 Feb 2026 16:42:23 +0100 Subject: [PATCH 61/93] Enable CCD --- source/GltfState/phyiscs_controller.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index cdd9f153..96d7bc78 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1349,6 +1349,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { word1 = Math.pow(2, 32) - 1; } + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_DISCRETE_CONTACT; + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_CCD_CONTACT; + return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); } @@ -1439,6 +1442,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (type === "kinematic") { actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); } + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, true); const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; if (motion) { const gltfAngularVelocity = motion?.angularVelocity; @@ -1980,6 +1984,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { sceneDesc.set_gravity(tmpVec); sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); + sceneDesc.flags |= this.PhysX.PxSceneFlagEnum.eENABLE_CCD; + this.scene = this.physics.createScene(sceneDesc); let triggerCallback = undefined; From 12879d699858e56f2675c86ebe912c37c9408174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 11 Feb 2026 19:05:18 +0100 Subject: [PATCH 62/93] Fix kinematic actors with velocity --- source/GltfState/phyiscs_controller.js | 96 ++++++++++++++------------ 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 96d7bc78..9ba40ebd 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2158,59 +2158,63 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; if (motion && motion.isKinematic) { - const worldTransform = node.physicsTransform ?? node.worldTransform; - const targetPosition = vec3.create(); - const targetRotation = quat.create(); - if (motion.linearVelocity !== undefined) { - const linearVelocity = vec3.create(); - vec3.scale(linearVelocity, motion.linearVelocity, deltaTime); - targetPosition[0] = worldTransform[12] + linearVelocity[0]; - targetPosition[1] = worldTransform[13] + linearVelocity[1]; - targetPosition[2] = worldTransform[14] + linearVelocity[2]; - } - if (motion.angularVelocity !== undefined) { - // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise - const angularVelocity = quat.create(); - quat.rotateX( - angularVelocity, - angularVelocity, - -motion.angularVelocity[0] * deltaTime - ); - quat.rotateY( - angularVelocity, - angularVelocity, - -motion.angularVelocity[1] * deltaTime - ); - quat.rotateZ( - angularVelocity, - angularVelocity, - -motion.angularVelocity[2] * deltaTime - ); - let currentRotation = quat.create(); + if (motion.linearVelocity !== undefined || motion.angularVelocity !== undefined) { + const worldTransform = node.physicsTransform ?? node.worldTransform; + const targetPosition = vec3.create(); + targetPosition[0] = worldTransform[12]; + targetPosition[1] = worldTransform[13]; + targetPosition[2] = worldTransform[14]; + let targetRotation = quat.create(); if (node.physicsTransform !== undefined) { - mat4.getRotation(currentRotation, worldTransform); + mat4.getRotation(targetRotation, worldTransform); } else { - currentRotation = node.worldQuaternion; + targetRotation = node.worldQuaternion; } - quat.multiply(targetRotation, angularVelocity, currentRotation); - } - const pos = new this.PhysX.PxVec3(...targetPosition); - const rot = new this.PhysX.PxQuat(...targetRotation); - const transform = new this.PhysX.PxTransform(pos, rot); + if (motion.linearVelocity !== undefined) { + const linearVelocity = vec3.create(); + vec3.scale(linearVelocity, motion.linearVelocity, deltaTime); + targetPosition[0] += linearVelocity[0]; + targetPosition[1] += linearVelocity[1]; + targetPosition[2] += linearVelocity[2]; + } + if (motion.angularVelocity !== undefined) { + // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise + const angularVelocity = quat.create(); + quat.rotateX( + angularVelocity, + angularVelocity, + -motion.angularVelocity[0] * deltaTime + ); + quat.rotateY( + angularVelocity, + angularVelocity, + -motion.angularVelocity[1] * deltaTime + ); + quat.rotateZ( + angularVelocity, + angularVelocity, + -motion.angularVelocity[2] * deltaTime + ); + quat.multiply(targetRotation, angularVelocity, targetRotation); + } + const pos = new this.PhysX.PxVec3(...targetPosition); + const rot = new this.PhysX.PxQuat(...targetRotation); + const transform = new this.PhysX.PxTransform(pos, rot); - actor.setKinematicTarget(transform); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(transform); + actor.setKinematicTarget(transform); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(transform); - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, targetRotation, targetPosition); + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, targetRotation, targetPosition); - const scaledPhysicsTransform = mat4.create(); - mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); + const scaledPhysicsTransform = mat4.create(); + mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); - node.physicsTransform = physicsTransform; - node.scaledPhysicsTransform = scaledPhysicsTransform; + node.physicsTransform = physicsTransform; + node.scaledPhysicsTransform = scaledPhysicsTransform; + } } else if (motion && motion.gravityFactor !== 1.0) { const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); actor.addForce(force); From e419896bfcb5f8149e74a7d34fe52621f7206e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 11 Feb 2026 19:05:34 +0100 Subject: [PATCH 63/93] Fix joint space calculation --- source/GltfState/phyiscs_controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 9ba40ebd..4a412a68 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1591,6 +1591,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { mat4.getTranslation(nodePosition, node.worldTransform); const offsetPosition = vec3.create(); vec3.subtract(offsetPosition, nodePosition, actorPosition); + vec3.transformQuat(offsetPosition, offsetPosition, inverseActorRotation); return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; } From 4aa5ba6420e05a42821b569b4ace36aef9ef1d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 11 Feb 2026 19:34:39 +0100 Subject: [PATCH 64/93] Fix custom gravity --- source/GltfState/phyiscs_controller.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 4a412a68..4cccac8b 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1985,7 +1985,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { sceneDesc.set_gravity(tmpVec); sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); - sceneDesc.flags |= this.PhysX.PxSceneFlagEnum.eENABLE_CCD; + const sceneFlags = new this.PhysX.PxSceneFlags( + this.PhysX.PxSceneFlagEnum.eENABLE_CCD | this.PhysX.PxSceneFlagEnum.eENABLE_PCM + ); + sceneDesc.flags = sceneFlags; this.scene = this.physics.createScene(sceneDesc); let triggerCallback = undefined; @@ -2218,7 +2221,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } else if (motion && motion.gravityFactor !== 1.0) { const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); - actor.addForce(force); + actor.addForce(force, this.PhysX.PxForceModeEnum.eACCELERATION); this.PhysX.destroy(force); } } From 1bd3bb32da858120a833bdf73f4d12abdbbfff6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 12 Feb 2026 13:55:39 +0100 Subject: [PATCH 65/93] Add physX substepping --- source/GltfState/phyiscs_controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 4cccac8b..c34e0724 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2226,7 +2226,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - this.scene.simulate(deltaTime); + this.scene.simulate(deltaTime / 2); + if (!this.scene.fetchResults(true)) { + console.warn("PhysX: fetchResults failed"); + } + this.scene.simulate(deltaTime / 2); if (!this.scene.fetchResults(true)) { console.warn("PhysX: fetchResults failed"); } From 8e5fc5788cf1586a58406fa7290af26f079d3b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 12 Feb 2026 15:22:55 +0100 Subject: [PATCH 66/93] Improve error handling for inertia --- source/GltfState/phyiscs_controller.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index c34e0724..9d451e23 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -743,13 +743,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { !quat.exactEquals(motion.inertiaOrientation, quat.create()) ) { const intertiaRotMat = mat3.create(); - mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); const inertiaDiagonalMat = mat3.create(); inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; + if ( + quat.length(motion.inertiaOrientation) > 1.0e-5 || + quat.length(motion.inertiaOrientation) < 1.0e-5 + ) { + mat3.identity(intertiaRotMat); + console.warn( + "PhysX: Invalid inertia orientation quaternion, ignoring rotation" + ); + } else { + mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); + } + const inertiaTensor = mat3.create(); mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); @@ -777,11 +788,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(col1); this.PhysX.destroy(col2); this.PhysX.destroy(pxInertiaTensor); + actor.setMassSpaceInertiaTensor(inertia); } else { inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); } - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); } else { if (motion.mass === undefined) { this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); From 1d93811475f1bc3144aa664a1481e072a15b23ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 09:59:11 +0100 Subject: [PATCH 67/93] Handle velocities for animated kinematic flag --- source/GltfState/phyiscs_controller.js | 61 +++++++++++++++++--------- source/gltf/node.js | 5 +++ 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 9d451e23..f67c41a7 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -149,7 +149,7 @@ class PhysicsController { constructor() { this.engine = undefined; this.staticActors = []; - this.kinematicActors = []; + this.kinematicActors = []; // This list is not updated if a dynamic actor is switched to kinematic at runtime this.dynamicActors = []; this.triggerNodes = []; this.compoundTriggerNodes = new Map(); // Map of compound trigger node index to set of included colliders @@ -818,6 +818,23 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; if (motion.animatedPropertyObjects.isKinematic.dirty) { actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); + if (motion.isKinematic) { + const linearVelocity = actor.getLinearVelocity(); + motion.computedLinearVelocity = [ + linearVelocity.x, + linearVelocity.y, + linearVelocity.z + ]; + const angularVelocity = actor.getAngularVelocity(); + motion.computedAngularVelocity = [ + angularVelocity.x, + angularVelocity.y, + angularVelocity.z + ]; + } else { + motion.computedLinearVelocity = undefined; + motion.computedAngularVelocity = undefined; + } } if (motion.animatedPropertyObjects.mass.dirty) { actor.setMass(motion.mass); @@ -838,10 +855,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (motion.animatedPropertyObjects.linearVelocity.dirty) { const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); actor.setLinearVelocity(pxVelocity); + motion.computedLinearVelocity = undefined; } if (motion.animatedPropertyObjects.angularVelocity.dirty) { const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); actor.setAngularVelocity(pxVelocity); + motion.computedAngularVelocity = undefined; } } @@ -2174,7 +2193,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; if (motion && motion.isKinematic) { - if (motion.linearVelocity !== undefined || motion.angularVelocity !== undefined) { + const linearVelocity = motion.computedLinearVelocity ?? motion.linearVelocity; + const angularVelocity = motion.computedAngularVelocity ?? motion.angularVelocity; + if (linearVelocity !== undefined || angularVelocity !== undefined) { const worldTransform = node.physicsTransform ?? node.worldTransform; const targetPosition = vec3.create(); targetPosition[0] = worldTransform[12]; @@ -2186,32 +2207,32 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } else { targetRotation = node.worldQuaternion; } - if (motion.linearVelocity !== undefined) { - const linearVelocity = vec3.create(); - vec3.scale(linearVelocity, motion.linearVelocity, deltaTime); - targetPosition[0] += linearVelocity[0]; - targetPosition[1] += linearVelocity[1]; - targetPosition[2] += linearVelocity[2]; + if (linearVelocity !== undefined) { + const acceleration = vec3.create(); + vec3.scale(acceleration, linearVelocity, deltaTime); + targetPosition[0] += acceleration[0]; + targetPosition[1] += acceleration[1]; + targetPosition[2] += acceleration[2]; } - if (motion.angularVelocity !== undefined) { + if (angularVelocity !== undefined) { // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise - const angularVelocity = quat.create(); + const angularAcceleration = quat.create(); quat.rotateX( - angularVelocity, - angularVelocity, - -motion.angularVelocity[0] * deltaTime + angularAcceleration, + angularAcceleration, + -angularVelocity[0] * deltaTime ); quat.rotateY( - angularVelocity, - angularVelocity, - -motion.angularVelocity[1] * deltaTime + angularAcceleration, + angularAcceleration, + -angularVelocity[1] * deltaTime ); quat.rotateZ( - angularVelocity, - angularVelocity, - -motion.angularVelocity[2] * deltaTime + angularAcceleration, + angularAcceleration, + -angularVelocity[2] * deltaTime ); - quat.multiply(targetRotation, angularVelocity, targetRotation); + quat.multiply(targetRotation, angularAcceleration, targetRotation); } const pos = new this.PhysX.PxVec3(...targetPosition); const rot = new this.PhysX.PxQuat(...targetRotation); diff --git a/source/gltf/node.js b/source/gltf/node.js index 53531869..764e13b7 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -306,6 +306,11 @@ class KHR_physics_rigid_bodies_motion extends GltfObject { this.linearVelocity = [0, 0, 0]; this.angularVelocity = [0, 0, 0]; this.gravityFactor = 1; + + // Non glTF + // We need to store the computed velocities on switching between kinematic and dynamic. + this.computedLinearVelocity = undefined; + this.computedAngularVelocity = undefined; } } From 2e0b23098ba72e48b544172e2533d1e1c9a547dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 11:13:06 +0100 Subject: [PATCH 68/93] Fix warning and substepping --- source/GltfState/phyiscs_controller.js | 41 +++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index f67c41a7..6ea3221d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -817,7 +817,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; if (motion.animatedPropertyObjects.isKinematic.dirty) { - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); if (motion.isKinematic) { const linearVelocity = actor.getLinearVelocity(); motion.computedLinearVelocity = [ @@ -835,6 +834,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { motion.computedLinearVelocity = undefined; motion.computedAngularVelocity = undefined; } + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, !motion.isKinematic); + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); } if (motion.animatedPropertyObjects.mass.dirty) { actor.setMass(motion.mass); @@ -1473,7 +1474,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (type === "kinematic") { actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); } - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, true); + actor.setRigidBodyFlag( + this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, + type !== "kinematic" + ); const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; if (motion) { const gltfAngularVelocity = motion?.angularVelocity; @@ -2172,19 +2176,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - simulateStep(state, deltaTime) { - if (!this.scene) { - this.reset = false; - return; - } - if (this.reset === true) { - this._resetSimulation(); - this.reset = false; - return; - } - - this.changeDebugVisualization(); - + subStepSimulation(state, deltaTime) { for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; if (node.dirtyTransform) { @@ -2263,10 +2255,23 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (!this.scene.fetchResults(true)) { console.warn("PhysX: fetchResults failed"); } - this.scene.simulate(deltaTime / 2); - if (!this.scene.fetchResults(true)) { - console.warn("PhysX: fetchResults failed"); + } + + simulateStep(state, deltaTime) { + if (!this.scene) { + this.reset = false; + return; } + if (this.reset === true) { + this._resetSimulation(); + this.reset = false; + return; + } + + this.changeDebugVisualization(); + + this.subStepSimulation(state, deltaTime / 2); + this.subStepSimulation(state, deltaTime / 2); for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; From c81d0e51ba5bdb786e388a221be76ad64054e83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 12:23:30 +0100 Subject: [PATCH 69/93] Move reset to gltf --- source/gltf/gltf.js | 11 ++++++ source/gltf/gltf_utils.js | 55 ++++++++++++++++++++++++++++- source/gltf/interactivity.js | 67 ++---------------------------------- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index f8186b2c..f547e580 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -19,6 +19,7 @@ import { gltfSkin } from "./skin.js"; import { gltfVariant } from "./variant.js"; import { gltfGraph } from "./interactivity.js"; import { KHR_physics_rigid_bodies } from "./rigid_bodies.js"; +import { recurseAllAnimatedProperties } from "./gltf_utils.js"; const allowedExtensions = [ "KHR_accessor_float64", @@ -271,6 +272,16 @@ class glTF extends GltfObject { return nonDisjointAnimations; } + + resetAnimatedProperties() { + const resetAnimatedProperty = (path, propertyName, parent, readOnly) => { + if (readOnly) { + return; + } + parent.animatedPropertyObjects[propertyName].rest(); + }; + recurseAllAnimatedProperties(this, resetAnimatedProperty); + } } function enforceVariantsUniqueness(variants) { diff --git a/source/gltf/gltf_utils.js b/source/gltf/gltf_utils.js index 6e8576c9..c487efff 100644 --- a/source/gltf/gltf_utils.js +++ b/source/gltf/gltf_utils.js @@ -1,6 +1,7 @@ import { vec3 } from "gl-matrix"; import { jsToGl } from "./utils.js"; import { gltfAccessor } from "./accessor.js"; +import { GltfObject } from "./gltf_object.js"; function getSceneExtents(gltf, sceneIndex, outMin, outMax) { for (const i of [0, 1, 2]) { @@ -212,4 +213,56 @@ function getMorphedNodeIndices(gltf) { return morphedNodes; } -export { getSceneExtents, getAnimatedIndices, getMorphedNodeIndices }; +function recurseAllAnimatedProperties(gltfObject, callable, currentPath = "") { + if (gltfObject === undefined || !(gltfObject instanceof GltfObject)) { + return; + } + + // Call for all animated properties of this gltfObject + for (const property of gltfObject.constructor.animatedProperties) { + if (gltfObject[property] === undefined) { + continue; + } + callable(currentPath, property, gltfObject, false); + } + + // Call for all read-only animated properties of this gltfObject + for (const property of gltfObject.constructor.readOnlyAnimatedProperties) { + if (gltfObject[property] === undefined) { + continue; + } + callable(currentPath, property, gltfObject, true); + } + + // Recurse into all GltfObject + for (const key in gltfObject) { + if (gltfObject[key] instanceof GltfObject) { + recurseAllAnimatedProperties(gltfObject[key], callable, currentPath + "/" + key); + } else if (Array.isArray(gltfObject[key])) { + if (gltfObject[key].length === 0 || !(gltfObject[key][0] instanceof GltfObject)) { + continue; + } + for (let i = 0; i < gltfObject[key].length; i++) { + recurseAllAnimatedProperties( + gltfObject[key][i], + callable, + currentPath + "/" + key + "/" + i + ); + } + } + } + + // Recurse into all extensions + for (const extensionName in gltfObject.extensions) { + const extension = gltfObject.extensions[extensionName]; + if (extension instanceof GltfObject) { + recurseAllAnimatedProperties( + extension, + callable, + currentPath + "/extensions/" + extensionName + ); + } + } +} + +export { getSceneExtents, getAnimatedIndices, getMorphedNodeIndices, recurseAllAnimatedProperties }; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index 47827429..2f148780 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -1,6 +1,7 @@ import { GltfObject } from "./gltf_object"; import * as interactivity from "@khronosgroup/gltf-interactivity-sample-engine"; import { mat4 } from "gl-matrix"; +import { recurseAllAnimatedProperties } from "./gltf_utils"; class gltfGraph extends GltfObject { static animatedProperties = []; @@ -330,13 +331,7 @@ class SampleViewerDecorator extends interactivity.ADecorator { this.behaveEngine.clearScheduledDelays(); this.behaveEngine.clearValueEvaluationCache(); - const resetAnimatedProperty = (path, propertyName, parent, readOnly) => { - if (readOnly) { - return; - } - parent.animatedPropertyObjects[propertyName].rest(); - }; - this.recurseAllAnimatedProperties(this.world.gltf, resetAnimatedProperty); + this.world.gltf.resetAnimatedProperties(); } processNodeStarted(node) { @@ -452,62 +447,6 @@ class SampleViewerDecorator extends interactivity.ADecorator { return currentNode; } - recurseAllAnimatedProperties(gltfObject, callable, currentPath = "") { - if (gltfObject === undefined || !(gltfObject instanceof GltfObject)) { - return; - } - - // Call for all animated properties of this gltfObject - for (const property of gltfObject.constructor.animatedProperties) { - if (gltfObject[property] === undefined) { - continue; - } - callable(currentPath, property, gltfObject, false); - } - - // Call for all read-only animated properties of this gltfObject - for (const property of gltfObject.constructor.readOnlyAnimatedProperties) { - if (gltfObject[property] === undefined) { - continue; - } - callable(currentPath, property, gltfObject, true); - } - - // Recurse into all GltfObject - for (const key in gltfObject) { - if (gltfObject[key] instanceof GltfObject) { - this.recurseAllAnimatedProperties( - gltfObject[key], - callable, - currentPath + "/" + key - ); - } else if (Array.isArray(gltfObject[key])) { - if (gltfObject[key].length === 0 || !(gltfObject[key][0] instanceof GltfObject)) { - continue; - } - for (let i = 0; i < gltfObject[key].length; i++) { - this.recurseAllAnimatedProperties( - gltfObject[key][i], - callable, - currentPath + "/" + key + "/" + i - ); - } - } - } - - // Recurse into all extensions - for (const extensionName in gltfObject.extensions) { - const extension = gltfObject.extensions[extensionName]; - if (extension instanceof GltfObject) { - this.recurseAllAnimatedProperties( - extension, - callable, - currentPath + "/extensions/" + extensionName - ); - } - } - } - registerKnownPointers() { // The engine is checking if a path is valid so we do not need to handle this here if (this.world === undefined) { @@ -586,7 +525,7 @@ class SampleViewerDecorator extends interactivity.ADecorator { false ); }; - this.recurseAllAnimatedProperties(this.world.gltf, registerFunction); + recurseAllAnimatedProperties(this.world.gltf, registerFunction); // Special pointers that need to be handled manually From 947d4bfe9075f0810f73a84ed8fc5d41804eabd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 12:23:40 +0100 Subject: [PATCH 70/93] Add undefined check --- source/gltf/gltf_object.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/gltf/gltf_object.js b/source/gltf/gltf_object.js index ba7c9037..a00855ae 100644 --- a/source/gltf/gltf_object.js +++ b/source/gltf/gltf_object.js @@ -40,7 +40,7 @@ class GltfObject { isDirty() { for (const prop in this.animatedPropertyObjects) { - if (this.animatedPropertyObjects[prop].dirty) { + if (this.animatedPropertyObjects[prop]?.dirty) { return true; } } @@ -49,7 +49,9 @@ class GltfObject { resetDirtyFlags() { for (const prop in this.animatedPropertyObjects) { - this.animatedPropertyObjects[prop].dirty = false; + if (this.animatedPropertyObjects[prop]) { + this.animatedPropertyObjects[prop].dirty = false; + } } } } From 8ff3c065f082b241cbb6caf7fdd74573272d313b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 12:42:29 +0100 Subject: [PATCH 71/93] Fix setGlobalPose --- source/GltfState/phyiscs_controller.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 6ea3221d..75c58d20 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -259,11 +259,7 @@ class PhysicsController { } parentRigidBody = node; } else if (currentRigidBody === undefined && rigidBody.collider !== undefined) { - if (animatedNodeIndices.has(node.gltfObjectIndex)) { - this.kinematicActors.push(node); - } else { - this.staticActors.push(node); - } + this.staticActors.push(node); } if (rigidBody.collider?.geometry?.mesh !== undefined) { if (!rigidBody.collider.geometry.convexHull) { @@ -720,7 +716,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { actor.setKinematicTarget(pxTransform); } else { - actor.setGlobalPos(pxTransform); + actor.setGlobalPose(pxTransform); } this.PhysX.destroy(pxPos); this.PhysX.destroy(pxRot); From c0b89bee4f58b24967fb13cddb129b6890da9f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 13:02:29 +0100 Subject: [PATCH 72/93] Do not use physics transform for animated dynamic bodies --- source/GltfState/phyiscs_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 75c58d20..b802ae89 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2272,7 +2272,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion && !motion.isKinematic) { + if (motion && !motion.isKinematic && !node.dirtyTransform) { const transform = actor.getGlobalPose(); const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); const rotation = quat.fromValues( From 087c636a57302501008877190e8da9f4055f58ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 15:07:43 +0100 Subject: [PATCH 73/93] Attach triggers to already existing actors --- source/GltfState/phyiscs_controller.js | 161 +++++++++++++++++++------ 1 file changed, 121 insertions(+), 40 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index b802ae89..87ecd849 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -125,6 +125,25 @@ class PhysicsUtils { computedWorldTransform, offsetChanged, scaleChanged, + false, + ...args + ); + } + + // Found a trigger + if ( + node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.mesh !== undefined || + node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.shape !== undefined + ) { + customFunction( + gltf, + node, + node.extensions.KHR_physics_rigid_bodies.trigger, + actorNode, + computedWorldTransform, + offsetChanged, + scaleChanged, + true, ...args ); } @@ -152,6 +171,7 @@ class PhysicsController { this.kinematicActors = []; // This list is not updated if a dynamic actor is switched to kinematic at runtime this.dynamicActors = []; this.triggerNodes = []; + this.independentTriggerNodes = []; // Trigger nodes that are not not part of another actor this.compoundTriggerNodes = new Map(); // Map of compound trigger node index to set of included colliders this.triggerToCompound = new Map(); // Map of trigger node index to compound trigger node index this.nodeToMotion = new Map(); @@ -302,6 +322,9 @@ class PhysicsController { } } else { this.triggerNodes.push(node); + if (parentRigidBody === undefined) { + this.independentTriggerNodes.push(node); + } } } } @@ -333,6 +356,7 @@ class PhysicsController { this.dynamicActors, this.jointNodes, this.triggerNodes, + this.independentTriggerNodes, this.nodeToMotion, this.hasRuntimeAnimationTargets, staticMeshColliderCount, @@ -346,6 +370,7 @@ class PhysicsController { this.dynamicActors = []; this.jointNodes = []; this.triggerNodes = []; + this.independentTriggerNodes = []; this.nodeToMotion.clear(); this.compoundTriggerNodes.clear(); this.triggerToCompound.clear(); @@ -408,7 +433,7 @@ class PhysicsController { } updateColliders(state, node, isTrigger = false) { - this.engine.updateActorTransform(node, isTrigger); + this.engine.updateActorTransform(node); let collider = undefined; if (isTrigger) { @@ -430,6 +455,22 @@ class PhysicsController { ); } + if ( + !isTrigger && + (node.extensions?.KHR_physics_rigid_bodies?.trigger?.mesh !== undefined || + node.extensions?.KHR_physics_rigid_bodies?.trigger?.shape !== undefined) + ) { + this.engine.updateCollider( + state.gltf, + node, + node.extensions?.KHR_physics_rigid_bodies?.trigger, + node, + node.worldTransform, + false, + node.dirtyScale, + true + ); + } if (!isTrigger) { for (const childIndex of node.children) { const childNode = state.gltf.nodes[childIndex]; @@ -469,7 +510,7 @@ class PhysicsController { this.updateColliders(state, actorNode); } - for (const node of this.triggerNodes) { + for (const node of this.independentTriggerNodes) { this.updateColliders(state, node, true); } @@ -519,6 +560,7 @@ class PhysicsInterface { dynamicActors, jointNodes, triggerNodes, + independentTriggerNodes, nodeToMotion, hasRuntimeAnimationTargets, staticMeshColliderCount, @@ -639,7 +681,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToActor = new Map(); this.nodeToMotion = new Map(); this.nodeToJoint = new Map(); - this.nodeToTrigger = new Map(); this.shapeToNode = new Map(); this.filterData = []; this.physXFilterData = []; @@ -698,11 +739,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - updateActorTransform(node, isTrigger = false) { + updateActorTransform(node) { if (node.dirtyTransform) { - const actor = isTrigger - ? this.nodeToTrigger.get(node.gltfObjectIndex)?.actor - : this.nodeToActor.get(node.gltfObjectIndex)?.actor; + const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; if (actor === undefined) { return; } @@ -869,11 +908,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { worldTransform, offsetChanged, scaleChanged, - isTrigger = false + isTrigger ) { - const lookup = isTrigger ? this.nodeToTrigger : this.nodeToActor; - - const result = lookup.get(actorNode.gltfObjectIndex); + const result = this.nodeToActor.get(actorNode.gltfObjectIndex); const actor = result?.actor; const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); @@ -1450,11 +1487,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } - createActor(gltf, node, shapeFlags, type, noMeshShapes = false) { - let parentNode = node; - while (parentNode.parentNode !== undefined) { - parentNode = parentNode.parentNode; - } + createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { const worldTransform = node.worldTransform; const translation = vec3.create(); mat4.getTranslation(translation, worldTransform); @@ -1498,7 +1531,18 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - const createAndAddShape = (gltf, node, collider, actorNode, worldTransform) => { + const createAndAddShape = ( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger, + noMeshShapes, + flags + ) => { // Calculate offset position const translation = vec3.create(); const shapePosition = vec3.create(); @@ -1536,7 +1580,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { gltf, node, collider, - shapeFlags, + flags, material, physXFilterData, noMeshShapes || collider?.geometry?.convexHull === true, @@ -1558,15 +1602,54 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } }; + // If a node contains trigger and collider combine them + let collider = undefined; - if (type === "trigger") { + if (type !== "trigger") { + collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + false, + noMeshShapes, + shapeFlags + ); collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + if (collider !== undefined) { + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + triggerFlags + ); + } } else { - collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + triggerFlags + ); } - createAndAddShape(gltf, node, collider, node, worldTransform); - if (type !== "trigger") { for (const childIndex of node.children) { const childNode = gltf.nodes[childIndex]; @@ -1587,20 +1670,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(pose); this.scene.addActor(actor); - if (type === "trigger") { - this.nodeToTrigger.set(node.gltfObjectIndex, { - actor, - pxShapeMap: pxShapeMap - }); - } else { - this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); - } + + this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } computeJointOffsetAndActor(node) { let currentNode = node; while (currentNode !== undefined) { if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { + //TODO break; } currentNode = currentNode.parentNode; @@ -1987,6 +2065,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { dynamicActors, jointNodes, triggerNodes, + independentTriggerNodes, nodeToMotion, hasRuntimeAnimationTargets, staticMeshColliderCount, @@ -2125,16 +2204,22 @@ class NvidiaPhysicsInterface extends PhysicsInterface { (staticMeshColliderCount > 0 && dynamicMeshColliderCount > 0); for (const node of staticActors) { - this.createActor(state.gltf, node, shapeFlags, "static"); + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "static"); } for (const node of kinematicActors) { - this.createActor(state.gltf, node, shapeFlags, "kinematic"); + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "kinematic"); } for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, "dynamic", true); + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "dynamic", true); } - for (const node of triggerNodes) { - this.createActor(state.gltf, node, triggerFlags, "trigger", true); + for (const node of independentTriggerNodes) { + if ( + this.nodeToActor.has(node.gltfObjectIndex) || + this.nodeToMotion.has(node.gltfObjectIndex) + ) { + continue; + } + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "trigger", true); } for (const node of jointNodes) { this.createJoint(state.gltf, node); @@ -2143,6 +2228,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(tmpVec); this.PhysX.destroy(sceneDesc); this.PhysX.destroy(shapeFlags); + this.PhysX.destroy(triggerFlags); this.debugStateChanged = true; this.changeDebugVisualization(); @@ -2375,12 +2461,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { actor.actor.release(); } - for (const trigger of this.nodeToTrigger.values()) { - trigger.actor.release(); - } - this.nodeToActor.clear(); - this.nodeToTrigger.clear(); if (scenePointer) { scenePointer.release(); From a17afd71bb2e1a3d8c86b994d1885bdb8ebd5593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 13 Feb 2026 15:47:07 +0100 Subject: [PATCH 74/93] Fix interactivity nodes --- source/GltfState/phyiscs_controller.js | 4 ++-- source/gltf/interactivity.js | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 87ecd849..75eb8295 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -543,7 +543,7 @@ class PhysicsController { } rayCast(rayStart, rayEnd) { - this.engine.rayCast(rayStart, rayEnd); + return this.engine.rayCast(rayStart, rayEnd); } } @@ -2573,7 +2573,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(queryFilterData); if (hasHit) { - const hitCount = hit.getNbAnyHits(); + const hitCount = hitBuffer.getNbAnyHits(); if (hitCount > 1) { console.warn("Raycast hit multiple objects, only the first hit is returned."); } diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index 2f148780..05db199f 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -230,6 +230,10 @@ class SampleViewerDecorator extends interactivity.ADecorator { this.behaveEngine.startAnimation = this.startAnimation; this.behaveEngine.getParentNodeIndex = this.getParentNodeIndex; + this.behaveEngine.applyImpulseToRigidBody = this.applyImpulseToRigidBody; + this.behaveEngine.applyPointImpulseToRigidBody = this.applyPointImpulseToRigidBody; + this.behaveEngine.rayCastRigidBodies = this.rayCastRigidBodies; + this.registerBehaveEngineNode("animation/stop", interactivity.AnimationStop); this.registerBehaveEngineNode("animation/start", interactivity.AnimationStart); this.registerBehaveEngineNode("animation/stopAt", interactivity.AnimationStopAt); From 6c0125c337b36ae717e8c7132786c9e74caa3199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 16 Feb 2026 19:18:50 +0100 Subject: [PATCH 75/93] Fix box collider update --- source/GltfState/phyiscs_controller.js | 20 +++++++------------- source/gltf/implicit_shape.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 75eb8295..83539909 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -916,13 +916,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { let currentGeometry = currentShape.getGeometry(); const currentColliderType = currentGeometry.getType(); - const shapeIndex = collider?.shape; + const shapeIndex = collider?.geometry?.shape; let scale = vec3.fromValues(1, 1, 1); let scaleAxis = quat.create(); if (shapeIndex !== undefined) { // Simple shapes need to be recreated if scale changed // If properties changed we also need to recreate the mesh colliders - const dirty = this.simpleShapes[shapeIndex].isDirty(); + const dirty = gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex].isDirty(); if ( scaleChanged && !dirty && @@ -938,22 +938,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scaleAxis = result.scaleAxis; currentGeometry.scale.scale = scale; currentGeometry.scale.rotation = scaleAxis; - } else if ( - !scaleChanged && - dirty && - currentColliderType !== this.PhysX.PxGeometryTypeEnum.eCONVEXMESH - ) { - // Use geometry from array, if this is a reference we do not need to do anything here since we already updated the array - } else { + } else if (dirty || scaleChanged) { // Recreate simple shape collider const newGeometry = this.generateSimpleShape( gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], scale, scaleAxis ); + currentGeometry.release?.(); if (newGeometry.getType() !== currentColliderType) { // We need to recreate the shape - this.PhysX.destroy(currentShape); let shapeFlags = undefined; if (isTrigger) { shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; @@ -973,7 +967,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { result?.pxShapeMap.set(node.gltfObjectIndex, shape); actor.detachShape(currentShape); actor.attachShape(shape); - currentGeometry = newGeometry; + this.PhysX.destroy(currentShape); } else { currentShape.setGeometry(newGeometry); } @@ -1094,7 +1088,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } let geometry = undefined; - if (referenceType === "eBOX") { + if (referenceType === this.PhysX.PxGeometryTypeEnum.eBOX) { const halfExtents = new this.PhysX.PxVec3( (x / 2) * scale[0], (y / 2) * scale[1], @@ -1146,7 +1140,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } else { radius *= scale[0]; } - if (referenceType === "eSPHERE") { + if (referenceType === this.PhysX.PxGeometryTypeEnum.eSPHERE) { reference.radius = radius; return undefined; } diff --git a/source/gltf/implicit_shape.js b/source/gltf/implicit_shape.js index 104f6185..01451c5a 100644 --- a/source/gltf/implicit_shape.js +++ b/source/gltf/implicit_shape.js @@ -37,6 +37,16 @@ class gltfImplicitShape extends GltfObject { this.sphere.fromJson(json.sphere); } } + + isDirty() { + return ( + (this.plane?.isDirty() ?? false) || + (this.box?.isDirty() ?? false) || + (this.capsule?.isDirty() ?? false) || + (this.cylinder?.isDirty() ?? false) || + (this.sphere?.isDirty() ?? false) + ); + } } class gltfShapeBox extends GltfObject { From 5c5773e8786b6211e6ee3cd05a9afafb47cf18af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 17 Feb 2026 15:14:34 +0100 Subject: [PATCH 76/93] Remove unneeded code --- source/GltfState/phyiscs_controller.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 83539909..7f6f2d7a 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -493,7 +493,6 @@ class PhysicsController { } applyAnimations(state) { - this.engine.updateSimpleShapes(state.gltf); this.engine.updatePhysicMaterials(state.gltf); for (const actorNode of this.staticActors) { @@ -641,27 +640,6 @@ class PhysicsInterface { } } - updateSimpleShapes(gltf) { - if (gltf?.extensions?.KHR_implicit_shapes === undefined) { - return; - } - for (let i = 0; i < gltf.extensions.KHR_implicit_shapes.shapes.length; i++) { - const shape = gltf.extensions.KHR_implicit_shapes.shapes[i]; - if (shape.isDirty()) { - const newGeometry = this.generateSimpleShape( - shape, - vec3.fromValues(1, 1, 1), - quat.create(), - this.simpleShapes[i] - ); - if (newGeometry !== undefined) { - this.simpleShapes[i].release?.(); - this.simpleShapes[i] = newGeometry; - } - } - } - } - updateActorTransform(node) {} updatePhysicsJoint(state, jointNode) {} } @@ -2327,7 +2305,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } - this.scene.simulate(deltaTime / 2); + this.scene.simulate(deltaTime); if (!this.scene.fetchResults(true)) { console.warn("PhysX: fetchResults failed"); } From 94aedbf030f3fa7b5d77780fe6d3196f9116a7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 17 Feb 2026 16:00:20 +0100 Subject: [PATCH 77/93] Make simulation more stable --- source/GltfState/phyiscs_controller.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 7f6f2d7a..6ff4ea0f 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -186,7 +186,8 @@ class PhysicsController { this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; this.pauseTime = undefined; - this.skipFrames = 0; // Skip the first two simulation frames to allow engine to initialize + this.skipFrames = 2; // Skip the first two simulation frames to allow engine to initialize + this.loading = false; } calculateMorphColliders(gltf) { @@ -256,6 +257,7 @@ class PhysicsController { return; } this.skipFrames = 2; + this.loading = true; const morphedNodeIndices = getMorphedNodeIndices(state.gltf); const result = getAnimatedIndices(state.gltf, "/nodes/", [ "translation", @@ -362,6 +364,8 @@ class PhysicsController { staticMeshColliderCount, dynamicMeshColliderCount ); + this.loading = false; + this.simulateStep(state, 0); // Simulate an initial step to ensure everything is up to date before rendering } resetScene() { @@ -409,6 +413,9 @@ class PhysicsController { if (state === undefined) { return; } + if (this.loading) { + return; + } if (this.skipFrames > 0) { this.skipFrames -= 1; return; @@ -425,7 +432,7 @@ class PhysicsController { this.enabled && this.engine && state && - this.timeAccumulator >= this.simulationStepTime + this.timeAccumulator >= this.simulationStepTime * 0.9 ) { this.engine.simulateStep(state, this.timeAccumulator); this.timeAccumulator = 0; @@ -2324,8 +2331,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.changeDebugVisualization(); - this.subStepSimulation(state, deltaTime / 2); - this.subStepSimulation(state, deltaTime / 2); + this.subStepSimulation(state, deltaTime); for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { const node = state.gltf.nodes[nodeIndex]; @@ -2443,7 +2449,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } getDebugLineData() { - if (!this.scene) { + if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { return []; } const result = []; From 95490ad142f8662dccba932c75177f47f021d29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 23 Feb 2026 12:49:16 +0100 Subject: [PATCH 78/93] Handle dirty flag reset for stepping --- source/GltfState/phyiscs_controller.js | 6 +++++- source/GltfView/gltf_view.js | 9 ++++++++- source/Renderer/renderer.js | 11 +++++++---- source/gltf/gltf.js | 12 ++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 6ff4ea0f..0daeec81 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -182,7 +182,7 @@ class PhysicsController { this.morphWeights = new Map(); this.playing = false; - this.enabled = true; + this.enabled = false; this.simulationStepTime = 1 / 60; this.timeAccumulator = 0; this.pauseTime = undefined; @@ -250,10 +250,12 @@ class PhysicsController { state.gltf.extensionsUsed === undefined || state.gltf.extensionsUsed.includes("KHR_physics_rigid_bodies") === false ) { + this.enabled = false; return; } const scene = state.gltf.scenes[sceneIndex]; if (!scene.nodes) { + this.enabled = false; return; } this.skipFrames = 2; @@ -349,8 +351,10 @@ class PhysicsController { this.dynamicActors.length === 0 && this.triggerNodes.length === 0) ) { + this.enabled = false; return; } + this.enabled = true; this.engine.initializeSimulation( state, this.staticActors, diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index ab2d644e..92c8fff0 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -90,7 +90,14 @@ class GltfView { } this.renderer.drawScene(state, scene); - AnimatableProperty.resetAllDirtyFlags(); + + // We do not want to reset the dirty flags when the physics simulation is paused, since changes from interactivity would not be applied after resuming the simulation. + if ( + (state.physicsController.playing && state.physicsController.enabled) || + !state.physicsController.enabled + ) { + state.gltf.resetAllDirtyFlags(); + } } /** diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index 5f49d37c..5ea8b213 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -779,7 +779,6 @@ class gltfRenderer { ); // Physics debug view - //TODO make optional if (state.physicsController.enabled) { const lines = state.physicsController.getDebugLineData(); if (lines.length !== 0) { @@ -794,8 +793,13 @@ class gltfRenderer { this.shader.updateUniform("u_Color", vec4.fromValues(1.0, 0.0, 0.0, 1.0)); const location = this.shader.getAttributeLocation("a_position"); if (location !== null) { - const buffer = this.webGl.context.createBuffer(); - this.webGl.context.bindBuffer(this.webGl.context.ARRAY_BUFFER, buffer); + if (this.physicsDebugBuffer === undefined) { + this.physicsDebugBuffer = this.webGl.context.createBuffer(); + } + this.webGl.context.bindBuffer( + this.webGl.context.ARRAY_BUFFER, + this.physicsDebugBuffer + ); this.webGl.context.bufferData( this.webGl.context.ARRAY_BUFFER, new Float32Array(lines), @@ -813,7 +817,6 @@ class gltfRenderer { this.webGl.context.drawArrays(this.webGl.context.LINES, 0, lines.length / 3); this.webGl.context.disableVertexAttribArray(location); this.webGl.context.bindBuffer(this.webGl.context.ARRAY_BUFFER, null); - this.webGl.context.deleteBuffer(buffer); } } } diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index f547e580..46fabb69 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -20,6 +20,7 @@ import { gltfVariant } from "./variant.js"; import { gltfGraph } from "./interactivity.js"; import { KHR_physics_rigid_bodies } from "./rigid_bodies.js"; import { recurseAllAnimatedProperties } from "./gltf_utils.js"; +import { AnimatableProperty } from "./animatable_property.js"; const allowedExtensions = [ "KHR_accessor_float64", @@ -282,6 +283,17 @@ class glTF extends GltfObject { }; recurseAllAnimatedProperties(this, resetAnimatedProperty); } + + /** + * Reset all dirty flags to false. This should be called after processing all animatable properties that have their dirty flags set to true. + */ + resetAllDirtyFlags() { + AnimatableProperty.resetAllDirtyFlags(); + for (const node of this.nodes) { + node.dirtyScale = false; + node.dirtyTransform = false; + } + } } function enforceVariantsUniqueness(variants) { From 9345d7dab92a3491aecf10c569206bd4a9ef23d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 24 Feb 2026 11:44:36 +0100 Subject: [PATCH 79/93] Fix scale update --- source/GltfState/phyiscs_controller.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 0daeec81..15fcd1a3 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -369,6 +369,7 @@ class PhysicsController { dynamicMeshColliderCount ); this.loading = false; + state.gltf.resetAllDirtyFlags(); this.simulateStep(state, 0); // Simulate an initial step to ensure everything is up to date before rendering } @@ -925,8 +926,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const result = PhysicsUtils.calculateScaleAndAxis(node); scale = result.scale; scaleAxis = result.scaleAxis; - currentGeometry.scale.scale = scale; - currentGeometry.scale.rotation = scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); } else if (dirty || scaleChanged) { // Recreate simple shape collider const newGeometry = this.generateSimpleShape( @@ -961,7 +966,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { currentShape.setGeometry(newGeometry); } } - } else if (collider.mesh !== undefined) { + } else if (collider?.geometry?.mesh !== undefined) { if (scaleChanged) { if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { currentGeometry = this.PhysX.castObject( @@ -978,8 +983,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const result = PhysicsUtils.calculateScaleAndAxis(node); scale = result.scale; scaleAxis = result.scaleAxis; - currentGeometry.scale.scale = scale; - currentGeometry.scale.rotation = scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); } } if (offsetChanged) { From dd32b5b660d655491296dd77c4489e22801cd848 Mon Sep 17 00:00:00 2001 From: David Labode Date: Tue, 24 Feb 2026 16:30:57 +0100 Subject: [PATCH 80/93] Fix colliding triggers --- source/GltfState/phyiscs_controller.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 15fcd1a3..2be8c34d 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1533,7 +1533,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { scaleChanged, isTrigger, noMeshShapes, - flags + shapeFlags, + triggerFlags ) => { // Calculate offset position const translation = vec3.create(); @@ -1572,7 +1573,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { gltf, node, collider, - flags, + isTrigger ? triggerFlags : shapeFlags, material, physXFilterData, noMeshShapes || collider?.geometry?.convexHull === true, @@ -1652,7 +1653,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node, node.dirtyScale, node.dirtyScale, - createAndAddShape + createAndAddShape, + [noMeshShapes, shapeFlags, triggerFlags] ); } } From 48ec7885cc50ed89c44a394c5f8319c9e4039d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 24 Feb 2026 17:06:43 +0100 Subject: [PATCH 81/93] Fix parameters --- source/GltfState/phyiscs_controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 2be8c34d..827a745b 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1610,7 +1610,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined, false, noMeshShapes, - shapeFlags + shapeFlags, + triggerFlags ); collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; if (collider !== undefined) { @@ -1624,6 +1625,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined, true, true, + shapeFlags, triggerFlags ); } @@ -1639,6 +1641,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { undefined, true, true, + shapeFlags, triggerFlags ); } From ed995c3b6bc350eb7e657137c60c74defb070133 Mon Sep 17 00:00:00 2001 From: David Labode Date: Wed, 25 Feb 2026 11:42:58 +0100 Subject: [PATCH 82/93] Fix child colliders broken rotation when scaled --- source/GltfState/phyiscs_controller.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 827a745b..cea999d2 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1005,11 +1005,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // Calculate offset rotation const rotation = quat.create(); - const offsetTransform = mat4.create(); - const inverseShapeTransform = mat4.create(); - mat4.invert(inverseShapeTransform, actorNode.worldTransform); - mat4.multiply(offsetTransform, inverseShapeTransform, worldTransform); - mat4.getRotation(rotation, offsetTransform); + quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); const PxPos = new this.PhysX.PxVec3(...translation); const PxRotation = new this.PhysX.PxQuat(...rotation); @@ -1549,11 +1545,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { // Calculate offset rotation const rotation = quat.create(); - const offsetTransform = mat4.create(); - const inverseShapeTransform = mat4.create(); - mat4.invert(inverseShapeTransform, actorNode.worldTransform); - mat4.multiply(offsetTransform, inverseShapeTransform, worldTransform); - mat4.getRotation(rotation, offsetTransform); + quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); // Calculate scale and scaleAxis const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); From 1697e47edbffd517775acbeb757282d43f44632f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 25 Feb 2026 17:13:05 +0100 Subject: [PATCH 83/93] Disable debug by default --- source/GltfState/phyiscs_controller.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index cea999d2..1c5680fd 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -245,7 +245,7 @@ class PhysicsController { } loadScene(state, sceneIndex) { - this.resetScene(); + this.resetScene(state.gltf); if ( state.gltf.extensionsUsed === undefined || state.gltf.extensionsUsed.includes("KHR_physics_rigid_bodies") === false @@ -373,7 +373,7 @@ class PhysicsController { this.simulateStep(state, 0); // Simulate an initial step to ensure everything is up to date before rendering } - resetScene() { + resetScene(gltf) { this.staticActors = []; this.kinematicActors = []; this.dynamicActors = []; @@ -388,6 +388,10 @@ class PhysicsController { this.hasRuntimeAnimationTargets = false; this.morphWeights.clear(); this.timeAccumulator = 0; + for (const node of gltf?.nodes ?? []) { + node.physicsTransform = undefined; + node.scaledPhysicsTransform = undefined; + } if (this.engine) { this.engine.resetSimulation(); } @@ -681,8 +685,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.triangleMeshes = []; // Debug - this.debugColliders = true; - this.debugJoints = true; + this.debugColliders = false; + this.debugJoints = false; this.debugStateChanged = true; this.MAX_FLOAT = 3.4028234663852885981170418348452e38; From db48fec1c1faacfe6666cf2c434edd7cd7037dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 27 Feb 2026 16:13:34 +0100 Subject: [PATCH 84/93] Fix kinematic velocities --- source/GltfState/phyiscs_controller.js | 48 ++++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 1c5680fd..de03b0a5 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2268,41 +2268,45 @@ class NvidiaPhysicsInterface extends PhysicsInterface { targetPosition[0] = worldTransform[12]; targetPosition[1] = worldTransform[13]; targetPosition[2] = worldTransform[14]; - let targetRotation = quat.create(); + let nodeRotation = quat.create(); if (node.physicsTransform !== undefined) { - mat4.getRotation(targetRotation, worldTransform); + mat4.getRotation(nodeRotation, worldTransform); } else { - targetRotation = node.worldQuaternion; + nodeRotation = node.worldQuaternion; } if (linearVelocity !== undefined) { const acceleration = vec3.create(); vec3.scale(acceleration, linearVelocity, deltaTime); + vec3.transformQuat(acceleration, acceleration, nodeRotation); targetPosition[0] += acceleration[0]; targetPosition[1] += acceleration[1]; targetPosition[2] += acceleration[2]; } if (angularVelocity !== undefined) { - // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise + // Transform angular velocity from local space to world space + // by rotating the velocity axes by the current node rotation. + const localX = vec3.fromValues(1, 0, 0); + const localY = vec3.fromValues(0, 1, 0); + const localZ = vec3.fromValues(0, 0, 1); + vec3.transformQuat(localX, localX, nodeRotation); + vec3.transformQuat(localY, localY, nodeRotation); + vec3.transformQuat(localZ, localZ, nodeRotation); + const angularAcceleration = quat.create(); - quat.rotateX( - angularAcceleration, - angularAcceleration, - -angularVelocity[0] * deltaTime - ); - quat.rotateY( - angularAcceleration, - angularAcceleration, - -angularVelocity[1] * deltaTime - ); - quat.rotateZ( - angularAcceleration, - angularAcceleration, - -angularVelocity[2] * deltaTime - ); - quat.multiply(targetRotation, angularAcceleration, targetRotation); + const qX = quat.create(); + const qY = quat.create(); + const qZ = quat.create(); + quat.setAxisAngle(qX, localX, angularVelocity[0] * deltaTime); + quat.setAxisAngle(qY, localY, angularVelocity[1] * deltaTime); + quat.setAxisAngle(qZ, localZ, angularVelocity[2] * deltaTime); + quat.multiply(angularAcceleration, qX, angularAcceleration); + quat.multiply(angularAcceleration, qY, angularAcceleration); + quat.multiply(angularAcceleration, qZ, angularAcceleration); + + quat.multiply(nodeRotation, angularAcceleration, nodeRotation); } const pos = new this.PhysX.PxVec3(...targetPosition); - const rot = new this.PhysX.PxQuat(...targetRotation); + const rot = new this.PhysX.PxQuat(...nodeRotation); const transform = new this.PhysX.PxTransform(pos, rot); actor.setKinematicTarget(transform); @@ -2311,7 +2315,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(transform); const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, targetRotation, targetPosition); + mat4.fromRotationTranslation(physicsTransform, nodeRotation, targetPosition); const scaledPhysicsTransform = mat4.create(); mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); From 4ec4428c51e75ac6f045854baeda7aa33886ac0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 27 Feb 2026 16:36:28 +0100 Subject: [PATCH 85/93] Remove TODOs --- source/GltfState/phyiscs_controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index de03b0a5..3e589912 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -597,7 +597,6 @@ class PhysicsInterface { generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} generateSphere(radius, scale, scaleAxis, reference) {} generatePlane(width, height, doubleSided, scale, scaleAxis, reference) {} - //TODO Handle non-uniform scale properly (also for parent nodes) generateSimpleShape( shape, scale = vec3.fromValues(1, 1, 1), @@ -1671,7 +1670,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { let currentNode = node; while (currentNode !== undefined) { if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { - //TODO break; } currentNode = currentNode.parentNode; From 0f64c7693289b1b44cf0360a5b1a953a00be0955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 17 Mar 2026 17:31:31 +0100 Subject: [PATCH 86/93] Fix worldquaternion not resetting --- source/GltfState/phyiscs_controller.js | 2 +- source/gltf/animatable_property.js | 8 ++++---- source/gltf/gltf.js | 6 +++++- source/gltf/interactivity.js | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 3e589912..c3930de1 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -2270,7 +2270,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { if (node.physicsTransform !== undefined) { mat4.getRotation(nodeRotation, worldTransform); } else { - nodeRotation = node.worldQuaternion; + nodeRotation = quat.clone(node.worldQuaternion); } if (linearVelocity !== undefined) { const acceleration = vec3.create(); diff --git a/source/gltf/animatable_property.js b/source/gltf/animatable_property.js index 9c5e7622..61d5546c 100644 --- a/source/gltf/animatable_property.js +++ b/source/gltf/animatable_property.js @@ -31,11 +31,11 @@ class AnimatableProperty { } rest() { + if (!this.dirty) { + this.dirty = true; + AnimatableProperty.dirtyFlagList.push(this); + } if (this.animatedValue !== null) { - if (!this.dirty) { - this.dirty = true; - AnimatableProperty.dirtyFlagList.push(this); - } this.animatedValue = null; } } diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index 46fabb69..a3c7fbe9 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -274,7 +274,7 @@ class glTF extends GltfObject { return nonDisjointAnimations; } - resetAnimatedProperties() { + resetAnimatedProperties(sceneIndex = -1) { const resetAnimatedProperty = (path, propertyName, parent, readOnly) => { if (readOnly) { return; @@ -282,6 +282,10 @@ class glTF extends GltfObject { parent.animatedPropertyObjects[propertyName].rest(); }; recurseAllAnimatedProperties(this, resetAnimatedProperty); + if (sceneIndex >= 0) { + const scene = this.scenes[sceneIndex]; + scene.applyTransformHierarchy(this); + } } /** diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js index 05db199f..af5023cc 100644 --- a/source/gltf/interactivity.js +++ b/source/gltf/interactivity.js @@ -335,7 +335,7 @@ class SampleViewerDecorator extends interactivity.ADecorator { this.behaveEngine.clearScheduledDelays(); this.behaveEngine.clearValueEvaluationCache(); - this.world.gltf.resetAnimatedProperties(); + this.world.gltf.resetAnimatedProperties(this.world.sceneIndex ?? -1); } processNodeStarted(node) { From 4c614e932894023022001590ae00000a795dc44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Fri, 20 Mar 2026 16:38:42 +0100 Subject: [PATCH 87/93] Implement joints after WASM update --- package-lock.json | 14 +- package.json | 2 +- source/GltfState/phyiscs_controller.js | 246 +++++++++++++------------ source/gltf/rigid_bodies.js | 206 +++++++++++++++++++++ 4 files changed, 342 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3d52fe4..f67f9095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "2.7.1" + "physx-js-webidl": "2.7.2" }, "devDependencies": { "@playwright/test": "^1.56.0", @@ -3252,9 +3252,9 @@ } }, "node_modules/physx-js-webidl": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.1.tgz", - "integrity": "sha512-D838Ng/1Qx419L6ySuTnEgFwtFAKWp0cb8voHt1HKjfrr5hMc/Ms/llYDG5HCH72+1EtIInWIwV/HcNeM/C4Gg==" + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.2.tgz", + "integrity": "sha512-erhexgkgYEbeb62Vie9q96MFWC69b2FNF4UkcglkvfWRqjpAZpD0gCzDfBIZqg5TqX+yd4s/m3zQo+cUrzQEWg==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6819,9 +6819,9 @@ "dev": true }, "physx-js-webidl": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.1.tgz", - "integrity": "sha512-D838Ng/1Qx419L6ySuTnEgFwtFAKWp0cb8voHt1HKjfrr5hMc/Ms/llYDG5HCH72+1EtIInWIwV/HcNeM/C4Gg==" + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/physx-js-webidl/-/physx-js-webidl-2.7.2.tgz", + "integrity": "sha512-erhexgkgYEbeb62Vie9q96MFWC69b2FNF4UkcglkvfWRqjpAZpD0gCzDfBIZqg5TqX+yd4s/m3zQo+cUrzQEWg==" }, "picomatch": { "version": "2.3.1", diff --git a/package.json b/package.json index b6b65fa6..1520feae 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "globals": "^15.5.0", "jpeg-js": "^0.4.3", "json-ptr": "^3.1.0", - "physx-js-webidl": "2.7.1" + "physx-js-webidl": "2.7.2" }, "devDependencies": { "@playwright/test": "^1.56.0", diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index c3930de1..fb018dec 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1666,7 +1666,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } - computeJointOffsetAndActor(node) { + computeJointOffsetAndActor(node, referencedJoint) { let currentNode = node; while (currentNode !== undefined) { if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { @@ -1674,16 +1674,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } currentNode = currentNode.parentNode; } + + const nodeWorldRot = node.worldQuaternion; + const localPhysXRot = referencedJoint?.localRotation; + if (localPhysXRot !== undefined) { + quat.multiply(nodeWorldRot, node.worldQuaternion, localPhysXRot); + } + if (currentNode === undefined) { const pos = vec3.create(); mat4.getTranslation(pos, node.worldTransform); - return { actor: undefined, offsetPosition: pos, offsetRotation: node.worldQuaternion }; + + return { actor: undefined, offsetPosition: pos, offsetRotation: nodeWorldRot }; } const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; const inverseActorRotation = quat.create(); quat.invert(inverseActorRotation, currentNode.worldQuaternion); const offsetRotation = quat.create(); - quat.multiply(offsetRotation, inverseActorRotation, node.worldQuaternion); + quat.multiply(offsetRotation, inverseActorRotation, nodeWorldRot); const actorPosition = vec3.create(); mat4.getTranslation(actorPosition, currentNode.worldTransform); @@ -1719,6 +1727,33 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return null; } + convertAxisIndexToAngularDriveEnum(axisIndex) { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6DriveEnum.eTWIST; + case 1: + return 6; // Currently not exposed via bindings + case 2: + return 7; // Currently not exposed via bindings + } + return null; + } + + validateSwingLimits(joint) { + // Check if swing limits are symmetric (cone) or asymmetric (pyramid) + if (joint.swingLimit1 && joint.swingLimit2) { + const limit1 = joint.swingLimit1; + const limit2 = joint.swingLimit2; + + const isSymmetric1 = Math.abs(limit1.min + limit1.max) < 1e-6; // Centered around 0 + const isSymmetric2 = Math.abs(limit2.min + limit2.max) < 1e-6; + + // Return if this is a cone limit (symmetric and same range) vs pyramid limit + return isSymmetric1 && isSymmetric2; + } + return false; + } + createJoint(gltf, node) { const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; const referencedJoint = @@ -1729,8 +1764,11 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return; } - const resultA = this.computeJointOffsetAndActor(node); - const resultB = this.computeJointOffsetAndActor(gltf.nodes[joint.connectedNode]); + const resultA = this.computeJointOffsetAndActor(node, referencedJoint); + const resultB = this.computeJointOffsetAndActor( + gltf.nodes[joint.connectedNode], + referencedJoint + ); const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); @@ -1754,6 +1792,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(poseA); this.PhysX.destroy(poseB); + physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); + physxJoint.setConstraintFlag( this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, this.debugJoints @@ -1774,9 +1814,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); - let angularYLimit = undefined; - let angularZLimit = undefined; - for (const limit of referencedJoint.limits) { const lock = limit.min === 0 && limit.max === 0; const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); @@ -1786,134 +1823,118 @@ class NvidiaPhysicsInterface extends PhysicsInterface { limit.max ?? this.MAX_FLOAT, spring ); - if (limit.linearAxes.includes(0)) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eX, - lock - ? this.PhysX.PxD6MotionEnum.eLOCKED - : this.PhysX.PxD6MotionEnum.eLIMITED - ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eX, linearLimitPair); - } - if (limit.linearAxes.includes(1)) { + for (const axis of limit.linearAxes) { + const result = referencedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eY, + physxAxis, lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eY, linearLimitPair); + if (!lock) { + physxJoint.setLinearLimit(physxAxis, linearLimitPair); + } } - if (limit.linearAxes.includes(2)) { + this.PhysX.destroy(linearLimitPair); + } + if (limit.angularAxes && limit.angularAxes.length > 0) { + for (const axis of limit.angularAxes) { + const result = referencedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eZ, + physxAxis, lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setLinearLimit(this.PhysX.PxD6AxisEnum.eZ, linearLimitPair); } - this.PhysX.destroy(linearLimitPair); } - if (limit.angularAxes && limit.angularAxes.length > 0) { - const angularLimitPair = new this.PhysX.PxJointAngularLimitPair( - limit.min ?? -Math.PI / 2, - limit.max ?? Math.PI / 2, - spring + this.PhysX.destroy(spring); + } + + if (referencedJoint.twistLimit !== undefined) { + if (!(referencedJoint.twistLimit.min === 0 && referencedJoint.twistLimit.max === 0)) { + const limitPair = new this.PhysX.PxJointAngularLimitPair( + referencedJoint.twistLimit.min ?? -Math.PI, + referencedJoint.twistLimit.max ?? Math.PI, + new this.PhysX.PxSpring( + referencedJoint.twistLimit.stiffness ?? 0, + referencedJoint.twistLimit.damping + ) ); - if (lock) { - if (limit.angularAxes.includes(0)) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eTWIST, - this.PhysX.PxD6MotionEnum.eLOCKED - ); - physxJoint.setTwistLimit(angularLimitPair); - } - if (limit.angularAxes.includes(1)) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING1, - this.PhysX.PxD6MotionEnum.eLOCKED - ); - angularYLimit = limit; - } - if (limit.angularAxes.includes(2)) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING2, - this.PhysX.PxD6MotionEnum.eLOCKED - ); - angularZLimit = limit; - } - } else if (limit.angularAxes.includes(0)) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eTWIST, - this.PhysX.PxD6MotionEnum.eLIMITED - ); - physxJoint.setTwistLimit(angularLimitPair); - } else if (limit.angularAxes.includes(1)) { - angularYLimit = limit; - } else if (limit.angularAxes.includes(2)) { - angularZLimit = limit; - } - this.PhysX.destroy(angularLimitPair); + physxJoint.setTwistLimit(limitPair); + this.PhysX.destroy(limitPair); } - this.PhysX.destroy(spring); } - if (angularYLimit !== undefined && angularZLimit !== undefined) { + if ( + referencedJoint.swingLimit1 !== undefined && + referencedJoint.swingLimit2 !== undefined + ) { if ( - angularYLimit.stiffness !== angularZLimit.stiffness || - angularYLimit.damping !== angularZLimit.damping + referencedJoint.swingLimit1.stiffness !== referencedJoint.swingLimit2.stiffness || + referencedJoint.swingLimit1.damping !== referencedJoint.swingLimit2.damping ) { console.warn( "PhysX does not support different stiffness/damping for swing limits." ); } else { const spring = new this.PhysX.PxSpring( - angularYLimit.stiffness ?? 0, - angularYLimit.damping + referencedJoint.swingLimit1.stiffness ?? 0, + referencedJoint.swingLimit1.damping ); let yMin = -Math.PI / 2; let yMax = Math.PI / 2; let zMin = -Math.PI / 2; let zMax = Math.PI / 2; - if (angularYLimit.min !== undefined) { - yMin = angularYLimit.min; + if (referencedJoint.swingLimit1.min !== undefined) { + yMin = referencedJoint.swingLimit1.min; } - if (angularYLimit.max !== undefined) { - yMax = angularYLimit.max; + if (referencedJoint.swingLimit1.max !== undefined) { + yMax = referencedJoint.swingLimit1.max; } - if (angularZLimit.min !== undefined) { - zMin = angularZLimit.min; + if (referencedJoint.swingLimit2.min !== undefined) { + zMin = referencedJoint.swingLimit2.min; } - if (angularZLimit.max !== undefined) { - zMax = angularZLimit.max; + if (referencedJoint.swingLimit2.max !== undefined) { + zMax = referencedJoint.swingLimit2.max; } - const jointLimitCone = new this.PhysX.PxJointLimitPyramid( - yMin, - yMax, - zMin, - zMax, - spring - ); - physxJoint.setPyramidSwingLimit(jointLimitCone); - this.PhysX.destroy(spring); - if (yMin !== yMax) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING1, - this.PhysX.PxD6MotionEnum.eLIMITED + const isSymmetric = this.validateSwingLimits(referencedJoint); + if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { + // Fixed limit is already set + } else if (isSymmetric) { + const swing1Angle = Math.max(Math.abs(yMin), Math.abs(yMax)); + const swing2Angle = Math.max(Math.abs(zMin), Math.abs(zMax)); + const jointLimitCone = new this.PhysX.PxJointLimitCone( + swing1Angle, + swing2Angle, + spring ); - } - if (zMin !== zMax) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING2, - this.PhysX.PxD6MotionEnum.eLIMITED + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); + } else { + const jointLimitCone = new this.PhysX.PxJointLimitPyramid( + yMin, + yMax, + zMin, + zMax, + spring ); + physxJoint.setPyramidSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); } + this.PhysX.destroy(spring); } - } else if (angularYLimit !== undefined || angularZLimit !== undefined) { - const singleLimit = angularYLimit ?? angularZLimit; - if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { + } else if ( + referencedJoint.swingLimit1 !== undefined || + referencedJoint.swingLimit2 !== undefined + ) { + const singleLimit = referencedJoint.swingLimit1 ?? referencedJoint.swingLimit2; + if (singleLimit.min === 0 && singleLimit.max === 0) { + // Fixed limit is already set + } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { console.warn( "PhysX requires symmetric limits for swing limits in single axis mode." ); @@ -1922,21 +1943,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { singleLimit.stiffness ?? 0, singleLimit.damping ); - const maxY = angularYLimit?.max ?? Math.PI / 2; - const maxZ = angularZLimit?.max ?? Math.PI / 2; + const maxY = referencedJoint.swingLimit1?.max ?? Math.PI; + const maxZ = referencedJoint.swingLimit2?.max ?? Math.PI; const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); - if (angularYLimit !== undefined) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING1, - this.PhysX.PxD6MotionEnum.eLIMITED - ); - } - if (angularZLimit !== undefined) { - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eSWING2, - this.PhysX.PxD6MotionEnum.eLIMITED - ); - } physxJoint.setSwingLimit(jointLimitCone); this.PhysX.destroy(spring); this.PhysX.destroy(jointLimitCone); @@ -1955,19 +1964,20 @@ class NvidiaPhysicsInterface extends PhysicsInterface { drive.maxForce ?? this.MAX_FLOAT, drive.mode === "acceleration" ); + const result = referencedJoint.getRotatedAxisAndSign(drive.axis); if (drive.type === "linear") { - const axis = this.convertAxisIndexToEnum(drive.axis, "linear"); + const axis = this.convertAxisIndexToEnum(result.axis, "linear"); physxJoint.setDrive(axis, physxDrive); if (drive.positionTarget !== undefined) { - positionTarget[drive.axis] = drive.positionTarget; + positionTarget[result.axis] = drive.positionTarget; } if (drive.velocityTarget !== undefined) { - linearVelocityTarget[drive.axis] = drive.velocityTarget; + linearVelocityTarget[result.axis] = drive.velocityTarget; } } else if (drive.type === "angular") { if (drive.positionTarget !== undefined) { // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise - switch (drive.axis) { + switch (result.axis) { case 0: { quat.rotateX(angleTarget, angleTarget, -drive.positionTarget); break; @@ -1984,10 +1994,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } if (drive.velocityTarget !== undefined) { - angularVelocityTarget[drive.axis] = drive.velocityTarget; + angularVelocityTarget[result.axis] = drive.velocityTarget; } - const axis = this.convertAxisIndexToEnum(drive.axis, "angular"); + const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); physxJoint.setDrive(axis, physxDrive); } this.PhysX.destroy(physxDrive); diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js index 8101e9f3..04d7d638 100644 --- a/source/gltf/rigid_bodies.js +++ b/source/gltf/rigid_bodies.js @@ -1,5 +1,6 @@ import { GltfObject } from "./gltf_object"; import { objectsFromJsons } from "./utils"; +import { quat } from "gl-matrix"; class KHR_physics_rigid_bodies extends GltfObject { static animatedProperties = []; @@ -46,12 +47,217 @@ class gltfPhysicsJoint extends GltfObject { super(); this.limits = []; this.drives = []; + + // non glTF + this.twistLimit = undefined; + this.swingLimit1 = undefined; + this.swingLimit2 = undefined; + this.localRotation = quat.create(); + this.twistAxis = 0; // 0 = X, 1 = Y, 2 = Z + } + + getRotatedAxisAndSign(axis) { + let result = { + axis: axis, + sign: 1 + }; + if (this.twistAxis === 0) { + return result; // No rotation needed + } + if (this.twistAxis === 1) { + if (axis === 0) { + result.axis = 1; + } else if (axis === 1) { + result.axis = 0; + result.sign = -1; + } else { + result.axis = 2; + } + } else { + if (axis === 0) { + result.axis = 2; + result.sign = -1; + } else if (axis === 1) { + result.axis = 1; + } else { + result.axis = 0; + } + } + return result; } fromJson(json) { super.fromJson(json); this.limits = objectsFromJsons(json.limits, gltfPhysicsJointLimit); this.drives = objectsFromJsons(json.drives, gltfPhysicsJointDrive); + + const freeAxes = new Set([0, 1, 2]); + const limitAxes = new Map(); + const fixedAxes = new Map(); + let isCylinderical = false; + + for (const limit of this.limits) { + if (limit.angularAxes !== undefined) { + for (const axis of limit.angularAxes) { + if (limit.min === 0 && limit.max === 0) { + if (fixedAxes.has(axis)) { + console.warn( + `Joint ${this.name}: Multiple limits on the same axis ${axis} is not supported.` + ); + } else { + fixedAxes.set(axis, limit); + } + } else { + if (limitAxes.has(axis)) { + console.warn( + `Joint ${this.name}: Multiple limits on the same axis ${axis} is not supported.` + ); + } else { + limitAxes.set(axis, limit); + } + } + freeAxes.delete(axis); + } + if (limit.angularAxes.length > 1) { + isCylinderical = true; + } + } + } + + // Handle cylindrical joints (cone/ellipse limits) + if (isCylinderical) { + this._handleCylindricalLimits(limitAxes, fixedAxes); + return; + } + + if (freeAxes.size === 0) { + // All axes are constrained + if (limitAxes.size === 0) { + // All axes are fixed (locked) + this.twistLimit = fixedAxes.get(0); + this.swingLimit1 = fixedAxes.get(1); + this.swingLimit2 = fixedAxes.get(2); + } else { + // Mix of fixed and limited axes + this._handleMixedConstraints(limitAxes, fixedAxes); + } + } else if (freeAxes.size === 1) { + // Two axes are constrained, one is free + const freeAxis = Array.from(freeAxes)[0]; + this._handleTwoConstrainedAxes(limitAxes, fixedAxes, freeAxis); + } else if (freeAxes.size === 2) { + // One axis is constrained, two are free + const constrainedAxis = [0, 1, 2].find((axis) => !freeAxes.has(axis)); + this._handleOneConstrainedAxis(limitAxes, fixedAxes, constrainedAxis); + } + } + + _handleMixedConstraints(limitAxes, fixedAxes) { + // Find the axis with the largest angular range to use as twist + let maxAxis = -1; + let maxRange = 0; + + for (const [axis, limit] of limitAxes.entries()) { + const range = limit.max - limit.min; + if (range > maxRange) { + maxRange = range; + maxAxis = axis; + } + } + + if (maxAxis === -1) { + // No limited axes, all are fixed + this.twistLimit = fixedAxes.get(0); + this.swingLimit1 = fixedAxes.get(1); + this.swingLimit2 = fixedAxes.get(2); + return; + } + + // Use the axis with largest range as twist axis + this._assignLimitsWithTwistAxis(limitAxes, fixedAxes, maxAxis); + } + + _handleTwoConstrainedAxes(limitAxes, fixedAxes, freeAxis) { + // Two constrained axes should use swing limits (cone/pyramid) + // The free axis becomes the twist axis + const constrainedAxes = [0, 1, 2].filter((axis) => axis !== freeAxis); + + // Calculate local rotation to align free axis with PhysX twist axis (X-axis) + this.localRotation = this._calculateLocalRotation(freeAxis); + + // Free axis becomes twist axis (may be free or have some constraint) + this.twistLimit = limitAxes.get(freeAxis) || fixedAxes.get(freeAxis); + + // Constrained axes become swing limits + this.swingLimit1 = limitAxes.get(constrainedAxes[0]) || fixedAxes.get(constrainedAxes[0]); + this.swingLimit2 = limitAxes.get(constrainedAxes[1]) || fixedAxes.get(constrainedAxes[1]); + } + + _handleOneConstrainedAxis(limitAxes, fixedAxes, constrainedAxis) { + // Use the constrained axis as twist axis + this._assignLimitsWithTwistAxis(limitAxes, fixedAxes, constrainedAxis); + } + + _assignLimitsWithTwistAxis(limitAxes, fixedAxes, twistAxis) { + // Calculate local rotation to align twist axis with PhysX convention (X-axis) + this.localRotation = this._calculateLocalRotation(twistAxis); + + // Assign limits based on the chosen twist axis + this.twistLimit = limitAxes.get(twistAxis) || fixedAxes.get(twistAxis); + + // Assign swing limits for the other two axes + const swingAxes = [0, 1, 2].filter((axis) => axis !== twistAxis); + this.swingLimit1 = limitAxes.get(swingAxes[0]) || fixedAxes.get(swingAxes[0]); + this.swingLimit2 = limitAxes.get(swingAxes[1]) || fixedAxes.get(swingAxes[1]); + } + + _calculateLocalRotation(twistAxis) { + // Calculate rotation to align the chosen twist axis with PhysX X-axis + const rotation = quat.create(); + this.twistAxis = twistAxis; + + switch (twistAxis) { + case 0: // X-axis is already aligned + quat.identity(rotation); + break; + case 1: // Y-axis -> rotate X-axis to align with Y-axis + // Rotate 90 degrees around Z-axis + quat.fromEuler(rotation, 0, 0, 90); + break; + case 2: // Z-axis -> rotate X-axis to align with Z-axis + // Rotate -90 degrees around Y-axis + quat.fromEuler(rotation, 0, -90, 0); + break; + } + + return rotation; + } + + _handleCylindricalLimits(limitAxes, fixedAxes) { + // Handle limits that constrain multiple axes together (cone/ellipse) + // Find the limit that affects multiple axes + for (const [axis, limit] of limitAxes.entries()) { + if (limit.angularAxes && limit.angularAxes.length > 1) { + // This is a cone/ellipse limit + const affectedAxes = limit.angularAxes; + const freeAxis = [0, 1, 2].find((axis) => !affectedAxes.includes(axis)); + + if (freeAxis !== undefined) { + // Free axis becomes twist + this.localRotation = this._calculateLocalRotation(freeAxis); + this.twistLimit = limitAxes.get(freeAxis) || fixedAxes.get(freeAxis); + + // Cone limit affects both swing axes equally + this.swingLimit1 = limit; + this.swingLimit2 = limit; + } else { + // All axes are in the cone - use first axis as twist + this.swingLimit1 = limit; + this.swingLimit2 = limit; + } + break; + } + } } } From 42397eed19cd7958c3ea552398028dfa3982a3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 24 Mar 2026 16:19:49 +0100 Subject: [PATCH 88/93] Create multiple simplifiedJoints per Joint --- source/GltfState/phyiscs_controller.js | 226 ++++++++++++++++--------- source/gltf/rigid_bodies.js | 185 +++++++++++++------- 2 files changed, 269 insertions(+), 142 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index fb018dec..aae7747c 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -673,7 +673,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.scene = undefined; this.nodeToActor = new Map(); this.nodeToMotion = new Map(); - this.nodeToJoint = new Map(); + this.nodeToSimplifiedJoints = new Map(); this.shapeToNode = new Map(); this.filterData = []; this.physXFilterData = []; @@ -1018,42 +1018,55 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } updatePhysicsJoint(state, jointNode) { - const pxJoint = this.nodeToJoint.get(jointNode.gltfObjectIndex); - if (pxJoint === undefined) { + const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); + if (pxJoints === undefined) { return; } - const jointIndex = jointNode.extensions?.KHR_physics_rigid_bodies?.joint?.joint; - const gltfJoint = state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[jointIndex]; - if ( - jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects - .enableCollision.dirty - ) { - pxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision + const gltfJoint = + state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[ + jointNode.extensions.KHR_physics_rigid_bodies.joint.joint + ]; + const simplifiedJoints = gltfJoint.simplifiedPhysicsJoints; + if (simplifiedJoints.length !== pxJoints.length) { + console.warn( + "Number of simplified joints does not match number of PhysX joints. Skipping joint update." ); + return; } - for (const limit of gltfJoint.limits) { - if (limit.animatedPropertyObjects.min.dirty) { - } - if (limit.animatedPropertyObjects.max.dirty) { - } - if (limit.animatedPropertyObjects.stiffness.dirty) { + for (let i = 0; i < simplifiedJoints.length; i++) { + const pxJoint = pxJoints[i]; + const simplifiedJoint = simplifiedJoints[i]; + if ( + jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects + .enableCollision.dirty + ) { + pxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision + ); } - if (limit.animatedPropertyObjects.damping.dirty) { + for (const limit of simplifiedJoint.limits) { + if (limit.animatedPropertyObjects.min.dirty) { + } + if (limit.animatedPropertyObjects.max.dirty) { + } + if (limit.animatedPropertyObjects.stiffness.dirty) { + } + if (limit.animatedPropertyObjects.damping.dirty) { + } } - } - for (const drive of gltfJoint.drives) { - if (drive.animatedPropertyObjects.stiffness.dirty) { - } - if (drive.animatedPropertyObjects.damping.dirty) { - } - if (drive.animatedPropertyObjects.maxForce.dirty) { - } - if (drive.animatedPropertyObjects.positionTarget.dirty) { - } - if (drive.animatedPropertyObjects.velocityTarget.dirty) { + for (const drive of simplifiedJoint.drives) { + if (drive.animatedPropertyObjects.stiffness.dirty) { + } + if (drive.animatedPropertyObjects.damping.dirty) { + } + if (drive.animatedPropertyObjects.maxForce.dirty) { + } + if (drive.animatedPropertyObjects.positionTarget.dirty) { + } + if (drive.animatedPropertyObjects.velocityTarget.dirty) { + } } } } @@ -1745,8 +1758,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const limit1 = joint.swingLimit1; const limit2 = joint.swingLimit2; - const isSymmetric1 = Math.abs(limit1.min + limit1.max) < 1e-6; // Centered around 0 - const isSymmetric2 = Math.abs(limit2.min + limit2.max) < 1e-6; + const isSymmetric1 = + Math.abs(limit1.min + limit1.max) < 1e-6 || limit1.min === undefined; // Centered around 0 + const isSymmetric2 = + Math.abs(limit2.min + limit2.max) < 1e-6 || limit2.min === undefined; // Return if this is a cone limit (symmetric and same range) vs pyramid limit return isSymmetric1 && isSymmetric2; @@ -1763,11 +1778,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { console.error("Referenced joint not found:", joint.joint); return; } + const simplifiedJoints = []; + for (const simplifiedJoint of referencedJoint.simplifiedPhysicsJoints) { + const physxJoint = this.createSimplifiedJoint(gltf, node, joint, simplifiedJoint); + simplifiedJoints.push(physxJoint); + } + this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); + } - const resultA = this.computeJointOffsetAndActor(node, referencedJoint); + createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { + const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); const resultB = this.computeJointOffsetAndActor( gltf.nodes[joint.connectedNode], - referencedJoint + simplifiedJoint ); const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); @@ -1799,8 +1822,6 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.debugJoints ); - this.nodeToJoint.set(node.gltfObjectIndex, physxJoint); - physxJoint.setConstraintFlag( this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, joint.enableCollision @@ -1814,17 +1835,22 @@ class NvidiaPhysicsInterface extends PhysicsInterface { physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); - for (const limit of referencedJoint.limits) { + for (const limit of simplifiedJoint.limits) { const lock = limit.min === 0 && limit.max === 0; const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); - if (limit.linearAxes && limit.linearAxes.length > 0) { + const isDistanceLimit = + limit.linearAxes && + limit.linearAxes.length === 3 && + (limit.min === undefined || limit.min === 0) && + limit.max !== 0; + if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( limit.min ?? -this.MAX_FLOAT, limit.max ?? this.MAX_FLOAT, spring ); for (const axis of limit.linearAxes) { - const result = referencedJoint.getRotatedAxisAndSign(axis); + const result = simplifiedJoint.getRotatedAxisAndSign(axis); const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); physxJoint.setMotion( physxAxis, @@ -1838,9 +1864,29 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } this.PhysX.destroy(linearLimitPair); } + if (isDistanceLimit) { + const linearLimit = new this.PhysX.PxJointLinearLimit( + limit.max ?? this.MAX_FLOAT, + spring + ); + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eX, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eY, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setMotion( + this.PhysX.PxD6AxisEnum.eZ, + this.PhysX.PxD6MotionEnum.eLIMITED + ); + physxJoint.setDistanceLimit(linearLimit); + this.PhysX.destroy(linearLimit); + } if (limit.angularAxes && limit.angularAxes.length > 0) { for (const axis of limit.angularAxes) { - const result = referencedJoint.getRotatedAxisAndSign(axis); + const result = simplifiedJoint.getRotatedAxisAndSign(axis); const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); physxJoint.setMotion( physxAxis, @@ -1853,14 +1899,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(spring); } - if (referencedJoint.twistLimit !== undefined) { - if (!(referencedJoint.twistLimit.min === 0 && referencedJoint.twistLimit.max === 0)) { + if (simplifiedJoint.twistLimit !== undefined) { + if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { const limitPair = new this.PhysX.PxJointAngularLimitPair( - referencedJoint.twistLimit.min ?? -Math.PI, - referencedJoint.twistLimit.max ?? Math.PI, + simplifiedJoint.twistLimit.min ?? -Math.PI, + simplifiedJoint.twistLimit.max ?? Math.PI, new this.PhysX.PxSpring( - referencedJoint.twistLimit.stiffness ?? 0, - referencedJoint.twistLimit.damping + simplifiedJoint.twistLimit.stiffness ?? 0, + simplifiedJoint.twistLimit.damping ) ); physxJoint.setTwistLimit(limitPair); @@ -1869,39 +1915,39 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } if ( - referencedJoint.swingLimit1 !== undefined && - referencedJoint.swingLimit2 !== undefined + simplifiedJoint.swingLimit1 !== undefined && + simplifiedJoint.swingLimit2 !== undefined ) { if ( - referencedJoint.swingLimit1.stiffness !== referencedJoint.swingLimit2.stiffness || - referencedJoint.swingLimit1.damping !== referencedJoint.swingLimit2.damping + simplifiedJoint.swingLimit1.stiffness !== simplifiedJoint.swingLimit2.stiffness || + simplifiedJoint.swingLimit1.damping !== simplifiedJoint.swingLimit2.damping ) { console.warn( "PhysX does not support different stiffness/damping for swing limits." ); } else { const spring = new this.PhysX.PxSpring( - referencedJoint.swingLimit1.stiffness ?? 0, - referencedJoint.swingLimit1.damping + simplifiedJoint.swingLimit1.stiffness ?? 0, + simplifiedJoint.swingLimit1.damping ); let yMin = -Math.PI / 2; let yMax = Math.PI / 2; let zMin = -Math.PI / 2; let zMax = Math.PI / 2; - if (referencedJoint.swingLimit1.min !== undefined) { - yMin = referencedJoint.swingLimit1.min; + if (simplifiedJoint.swingLimit1.min !== undefined) { + yMin = simplifiedJoint.swingLimit1.min; } - if (referencedJoint.swingLimit1.max !== undefined) { - yMax = referencedJoint.swingLimit1.max; + if (simplifiedJoint.swingLimit1.max !== undefined) { + yMax = simplifiedJoint.swingLimit1.max; } - if (referencedJoint.swingLimit2.min !== undefined) { - zMin = referencedJoint.swingLimit2.min; + if (simplifiedJoint.swingLimit2.min !== undefined) { + zMin = simplifiedJoint.swingLimit2.min; } - if (referencedJoint.swingLimit2.max !== undefined) { - zMax = referencedJoint.swingLimit2.max; + if (simplifiedJoint.swingLimit2.max !== undefined) { + zMax = simplifiedJoint.swingLimit2.max; } - const isSymmetric = this.validateSwingLimits(referencedJoint); + const isSymmetric = this.validateSwingLimits(simplifiedJoint); if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { // Fixed limit is already set } else if (isSymmetric) { @@ -1928,10 +1974,10 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(spring); } } else if ( - referencedJoint.swingLimit1 !== undefined || - referencedJoint.swingLimit2 !== undefined + simplifiedJoint.swingLimit1 !== undefined || + simplifiedJoint.swingLimit2 !== undefined ) { - const singleLimit = referencedJoint.swingLimit1 ?? referencedJoint.swingLimit2; + const singleLimit = simplifiedJoint.swingLimit1 ?? simplifiedJoint.swingLimit2; if (singleLimit.min === 0 && singleLimit.max === 0) { // Fixed limit is already set } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { @@ -1943,8 +1989,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { singleLimit.stiffness ?? 0, singleLimit.damping ); - const maxY = referencedJoint.swingLimit1?.max ?? Math.PI; - const maxZ = referencedJoint.swingLimit2?.max ?? Math.PI; + const maxY = simplifiedJoint.swingLimit1?.max ?? Math.PI; + const maxZ = simplifiedJoint.swingLimit2?.max ?? Math.PI; const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); physxJoint.setSwingLimit(jointLimitCone); this.PhysX.destroy(spring); @@ -1957,44 +2003,56 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const linearVelocityTarget = vec3.fromValues(0, 0, 0); const angularVelocityTarget = vec3.fromValues(0, 0, 0); - for (const drive of referencedJoint.drives) { + for (const drive of simplifiedJoint.drives) { const physxDrive = new this.PhysX.PxD6JointDrive( drive.stiffness, drive.damping, drive.maxForce ?? this.MAX_FLOAT, drive.mode === "acceleration" ); - const result = referencedJoint.getRotatedAxisAndSign(drive.axis); + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); if (drive.type === "linear") { const axis = this.convertAxisIndexToEnum(result.axis, "linear"); physxJoint.setDrive(axis, physxDrive); if (drive.positionTarget !== undefined) { - positionTarget[result.axis] = drive.positionTarget; + positionTarget[result.axis] = drive.positionTarget * result.sign; } if (drive.velocityTarget !== undefined) { - linearVelocityTarget[result.axis] = drive.velocityTarget; + linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; } } else if (drive.type === "angular") { if (drive.positionTarget !== undefined) { // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise switch (result.axis) { case 0: { - quat.rotateX(angleTarget, angleTarget, -drive.positionTarget); + quat.rotateX( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); break; } case 1: { - quat.rotateY(angleTarget, angleTarget, -drive.positionTarget); + quat.rotateY( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); break; } case 2: { - quat.rotateZ(angleTarget, angleTarget, -drive.positionTarget); + quat.rotateZ( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); break; } } } if (drive.velocityTarget !== undefined) { - angularVelocityTarget[result.axis] = drive.velocityTarget; + angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation } const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); @@ -2047,11 +2105,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.debugJoints ? 1 : 0 ); this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); - for (const joint of this.nodeToJoint.values()) { - joint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); + } } for (const shapePtr of this.shapeToNode.keys()) { const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); @@ -2456,10 +2516,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } this.triangleMeshes = []; - for (const joint of this.nodeToJoint.values()) { - joint.release(); + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.release(); + } } - this.nodeToJoint.clear(); + this.nodeToSimplifiedJoints.clear(); for (const actor of this.nodeToActor.values()) { actor.actor.release(); diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js index 04d7d638..0c02431f 100644 --- a/source/gltf/rigid_bodies.js +++ b/source/gltf/rigid_bodies.js @@ -41,91 +41,40 @@ class gltfCollisionFilter extends GltfObject { } } -class gltfPhysicsJoint extends GltfObject { - static animatedProperties = []; - constructor() { - super(); - this.limits = []; - this.drives = []; +class simplifiedPhysicsJoint { + constructor(limits, drives) { + this.limits = limits; + this.drives = drives; - // non glTF this.twistLimit = undefined; this.swingLimit1 = undefined; this.swingLimit2 = undefined; this.localRotation = quat.create(); this.twistAxis = 0; // 0 = X, 1 = Y, 2 = Z - } - - getRotatedAxisAndSign(axis) { - let result = { - axis: axis, - sign: 1 - }; - if (this.twistAxis === 0) { - return result; // No rotation needed - } - if (this.twistAxis === 1) { - if (axis === 0) { - result.axis = 1; - } else if (axis === 1) { - result.axis = 0; - result.sign = -1; - } else { - result.axis = 2; - } - } else { - if (axis === 0) { - result.axis = 2; - result.sign = -1; - } else if (axis === 1) { - result.axis = 1; - } else { - result.axis = 0; - } - } - return result; - } - - fromJson(json) { - super.fromJson(json); - this.limits = objectsFromJsons(json.limits, gltfPhysicsJointLimit); - this.drives = objectsFromJsons(json.drives, gltfPhysicsJointDrive); + this.isCylindrical = false; const freeAxes = new Set([0, 1, 2]); const limitAxes = new Map(); const fixedAxes = new Map(); - let isCylinderical = false; for (const limit of this.limits) { if (limit.angularAxes !== undefined) { for (const axis of limit.angularAxes) { if (limit.min === 0 && limit.max === 0) { - if (fixedAxes.has(axis)) { - console.warn( - `Joint ${this.name}: Multiple limits on the same axis ${axis} is not supported.` - ); - } else { - fixedAxes.set(axis, limit); - } + fixedAxes.set(axis, limit); } else { - if (limitAxes.has(axis)) { - console.warn( - `Joint ${this.name}: Multiple limits on the same axis ${axis} is not supported.` - ); - } else { - limitAxes.set(axis, limit); - } + limitAxes.set(axis, limit); } freeAxes.delete(axis); } if (limit.angularAxes.length > 1) { - isCylinderical = true; + this.isCylindrical = true; } } } // Handle cylindrical joints (cone/ellipse limits) - if (isCylinderical) { + if (this.isCylindrical) { this._handleCylindricalLimits(limitAxes, fixedAxes); return; } @@ -152,6 +101,36 @@ class gltfPhysicsJoint extends GltfObject { } } + getRotatedAxisAndSign(axis) { + let result = { + axis: axis, + sign: 1 + }; + if (this.twistAxis === 0) { + return result; // No rotation needed + } + if (this.twistAxis === 1) { + if (axis === 0) { + result.axis = 1; + } else if (axis === 1) { + result.axis = 0; + result.sign = -1; + } else { + result.axis = 2; + } + } else { + if (axis === 0) { + result.axis = 2; + result.sign = -1; + } else if (axis === 1) { + result.axis = 1; + } else { + result.axis = 0; + } + } + return result; + } + _handleMixedConstraints(limitAxes, fixedAxes) { // Find the axis with the largest angular range to use as twist let maxAxis = -1; @@ -261,6 +240,92 @@ class gltfPhysicsJoint extends GltfObject { } } +class gltfPhysicsJoint extends GltfObject { + static animatedProperties = []; + constructor() { + super(); + this.limits = []; + this.drives = []; + + // non glTF + this.simplifiedPhysicsJoints = []; + } + + _getUniqueDrives(drivesCopy) { + const definedLinearDrives = new Set(); + const definedAngularDrives = new Set(); + const result = []; + for (let i = drivesCopy.length - 1; i >= 0; i--) { + if (drivesCopy[i].type === "linear" && definedLinearDrives.has(drivesCopy[i].axis)) { + continue; + } + if (drivesCopy[i].type === "angular" && definedAngularDrives.has(drivesCopy[i].axis)) { + continue; + } + if (drivesCopy[i].type === "linear") { + definedLinearDrives.add(drivesCopy[i].axis); + result.push(drivesCopy[i]); + drivesCopy.splice(i, 1); + } else { + definedAngularDrives.add(drivesCopy[i].axis); + result.push(drivesCopy[i]); + drivesCopy.splice(i, 1); + } + } + return result; + } + + fromJson(json) { + super.fromJson(json); + this.limits = objectsFromJsons(json.limits, gltfPhysicsJointLimit); + this.drives = objectsFromJsons(json.drives, gltfPhysicsJointDrive); + + const definedLinearAxes = new Set(); + const definedAngularAxes = new Set(); + let currentLimits = []; + const drivesCopy = this.drives.slice(); + + let needToCreateNewJoint = false; + for (const limit of this.limits) { + for (const axis of limit.angularAxes || []) { + if (definedAngularAxes.has(axis)) { + needToCreateNewJoint = true; + } + } + for (const axis of limit.linearAxes || []) { + if (definedLinearAxes.has(axis)) { + needToCreateNewJoint = true; + } + } + if (needToCreateNewJoint) { + const drives = this._getUniqueDrives(drivesCopy); + this.simplifiedPhysicsJoints.push( + new simplifiedPhysicsJoint(currentLimits, drives) + ); + currentLimits = []; + definedLinearAxes.clear(); + definedAngularAxes.clear(); + needToCreateNewJoint = false; + } + currentLimits.push(limit); + for (const axis of limit.angularAxes || []) { + definedAngularAxes.add(axis); + } + for (const axis of limit.linearAxes || []) { + definedLinearAxes.add(axis); + } + } + if (currentLimits.length > 0) { + const drives = this._getUniqueDrives(drivesCopy); + this.simplifiedPhysicsJoints.push(new simplifiedPhysicsJoint(currentLimits, drives)); + } + while (drivesCopy.length > 0) { + const drives = this._getUniqueDrives(drivesCopy); + this.simplifiedPhysicsJoints.push(new simplifiedPhysicsJoint([], drives)); + } + } +} + class gltfPhysicsJointLimit extends GltfObject { static animatedProperties = ["min", "max", "stiffness", "damping"]; constructor() { From b73cba3a714cd3cab90b805c51fafe8dd4f78cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 25 Mar 2026 11:21:29 +0100 Subject: [PATCH 89/93] Handle animated joints --- source/GltfState/phyiscs_controller.js | 357 +++++++++++++++---------- 1 file changed, 212 insertions(+), 145 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index aae7747c..1e0fb2cf 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1046,27 +1046,73 @@ class NvidiaPhysicsInterface extends PhysicsInterface { ); } for (const limit of simplifiedJoint.limits) { - if (limit.animatedPropertyObjects.min.dirty) { - } - if (limit.animatedPropertyObjects.max.dirty) { - } - if (limit.animatedPropertyObjects.stiffness.dirty) { - } - if (limit.animatedPropertyObjects.damping.dirty) { + if ( + limit.animatedPropertyObjects.min.dirty || + limit.animatedPropertyObjects.max.dirty || + limit.animatedPropertyObjects.stiffness.dirty || + limit.animatedPropertyObjects.damping.dirty + ) { + this._setLimitValues(pxJoint, simplifiedJoint, limit); } } + if ( + simplifiedJoint.twistLimit && + (simplifiedJoint.twistLimit.animatedPropertyObjects.min.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.max.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.damping.dirty) + ) { + this._setTwistLimitValues(pxJoint, simplifiedJoint); + } + + if ( + (simplifiedJoint.swingLimit1 && + (simplifiedJoint.swingLimit1.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.damping.dirty)) || + (simplifiedJoint.swingLimit2 && + (simplifiedJoint.swingLimit2.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.damping.dirty)) + ) { + this._setSwingLimitValues(pxJoint, simplifiedJoint); + } + + let positionTargetDirty = false; + let velocityTargetDirty = false; + const linearVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + const angularVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + pxJoint.getDriveVelocity(linearVelocityTarget, angularVelocityTarget); for (const drive of simplifiedJoint.drives) { - if (drive.animatedPropertyObjects.stiffness.dirty) { - } - if (drive.animatedPropertyObjects.damping.dirty) { + if ( + drive.animatedPropertyObjects.stiffness.dirty || + drive.animatedPropertyObjects.damping.dirty || + drive.animatedPropertyObjects.maxForce.dirty + ) { + this._setDriveValues(pxJoint, simplifiedJoint, drive); } - if (drive.animatedPropertyObjects.maxForce.dirty) { + if (drive.animatedPropertyObjects.velocityTarget.dirty) { + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); + velocityTargetDirty = true; } if (drive.animatedPropertyObjects.positionTarget.dirty) { + positionTargetDirty = true; } - if (drive.animatedPropertyObjects.velocityTarget.dirty) { - } + } + + if (positionTargetDirty) { + this._setDrivePositionTarget(pxJoint, simplifiedJoint); + } + if (velocityTargetDirty) { + pxJoint.setDriveVelocity(linearVelocityTarget, angularVelocityTarget); } } } @@ -1786,119 +1832,58 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); } - createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { - const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); - const resultB = this.computeJointOffsetAndActor( - gltf.nodes[joint.connectedNode], - simplifiedJoint - ); - - const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); - const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); - const poseA = new this.PhysX.PxTransform(pos, rot); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - - const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); - const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); - const poseB = new this.PhysX.PxTransform(posB, rotB); - this.PhysX.destroy(posB); - this.PhysX.destroy(rotB); - - const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( - this.physics, - resultA.actor, - poseA, - resultB.actor, - poseB - ); - this.PhysX.destroy(poseA); - this.PhysX.destroy(poseB); - - physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); - - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); - - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - joint.enableCollision - ); - - // Do not restict any axis by default - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); - - for (const limit of simplifiedJoint.limits) { - const lock = limit.min === 0 && limit.max === 0; - const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); - const isDistanceLimit = - limit.linearAxes && - limit.linearAxes.length === 3 && - (limit.min === undefined || limit.min === 0) && - limit.max !== 0; - if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { - const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( - limit.min ?? -this.MAX_FLOAT, - limit.max ?? this.MAX_FLOAT, - spring + _setLimitValues(physxJoint, simplifiedJoint, limit) { + const lock = limit.min === 0 && limit.max === 0; + const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); + const isDistanceLimit = + limit.linearAxes && + limit.linearAxes.length === 3 && + (limit.min === undefined || limit.min === 0) && + limit.max !== 0; + if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { + const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( + limit.min ?? -this.MAX_FLOAT, + limit.max ?? this.MAX_FLOAT, + spring + ); + for (const axis of limit.linearAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setMotion( + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - for (const axis of limit.linearAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setMotion( - physxAxis, - lock - ? this.PhysX.PxD6MotionEnum.eLOCKED - : this.PhysX.PxD6MotionEnum.eLIMITED - ); - if (!lock) { - physxJoint.setLinearLimit(physxAxis, linearLimitPair); - } + if (!lock) { + physxJoint.setLinearLimit(physxAxis, linearLimitPair); } - this.PhysX.destroy(linearLimitPair); } - if (isDistanceLimit) { - const linearLimit = new this.PhysX.PxJointLinearLimit( - limit.max ?? this.MAX_FLOAT, - spring - ); - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eX, - this.PhysX.PxD6MotionEnum.eLIMITED - ); - physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eY, - this.PhysX.PxD6MotionEnum.eLIMITED - ); + this.PhysX.destroy(linearLimitPair); + } + if (isDistanceLimit) { + const linearLimit = new this.PhysX.PxJointLinearLimit( + limit.max ?? this.MAX_FLOAT, + spring + ); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setDistanceLimit(linearLimit); + this.PhysX.destroy(linearLimit); + } + if (limit.angularAxes && limit.angularAxes.length > 0) { + for (const axis of limit.angularAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); physxJoint.setMotion( - this.PhysX.PxD6AxisEnum.eZ, - this.PhysX.PxD6MotionEnum.eLIMITED + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED ); - physxJoint.setDistanceLimit(linearLimit); - this.PhysX.destroy(linearLimit); - } - if (limit.angularAxes && limit.angularAxes.length > 0) { - for (const axis of limit.angularAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); - physxJoint.setMotion( - physxAxis, - lock - ? this.PhysX.PxD6MotionEnum.eLOCKED - : this.PhysX.PxD6MotionEnum.eLIMITED - ); - } } - this.PhysX.destroy(spring); } + this.PhysX.destroy(spring); + } + _setTwistLimitValues(physxJoint, simplifiedJoint) { if (simplifiedJoint.twistLimit !== undefined) { if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { const limitPair = new this.PhysX.PxJointAngularLimitPair( @@ -1913,7 +1898,9 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(limitPair); } } + } + _setSwingLimitValues(physxJoint, simplifiedJoint) { if ( simplifiedJoint.swingLimit1 !== undefined && simplifiedJoint.swingLimit2 !== undefined @@ -1997,29 +1984,48 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(jointLimitCone); } } + } + _setDriveValues(physxJoint, simplifiedJoint, drive) { + const physxDrive = new this.PhysX.PxD6JointDrive( + drive.stiffness, + drive.damping, + drive.maxForce ?? this.MAX_FLOAT, + drive.mode === "acceleration" + ); + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + const axis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setDrive(axis, physxDrive); + } else if (drive.type === "angular") { + const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); + physxJoint.setDrive(axis, physxDrive); + } + this.PhysX.destroy(physxDrive); + } + + _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + if (drive.velocityTarget !== undefined) { + linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; + } + } else if (drive.type === "angular") { + if (drive.velocityTarget !== undefined) { + angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation + } + } + } + + _setDrivePositionTarget(physxJoint, simplifiedJoint) { const positionTarget = vec3.fromValues(0, 0, 0); const angleTarget = quat.create(); - const linearVelocityTarget = vec3.fromValues(0, 0, 0); - const angularVelocityTarget = vec3.fromValues(0, 0, 0); - for (const drive of simplifiedJoint.drives) { - const physxDrive = new this.PhysX.PxD6JointDrive( - drive.stiffness, - drive.damping, - drive.maxForce ?? this.MAX_FLOAT, - drive.mode === "acceleration" - ); const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); if (drive.type === "linear") { - const axis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setDrive(axis, physxDrive); if (drive.positionTarget !== undefined) { positionTarget[result.axis] = drive.positionTarget * result.sign; } - if (drive.velocityTarget !== undefined) { - linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; - } } else if (drive.type === "angular") { if (drive.positionTarget !== undefined) { // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise @@ -2050,31 +2056,92 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } } - - if (drive.velocityTarget !== undefined) { - angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation - } - - const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); - physxJoint.setDrive(axis, physxDrive); } - this.PhysX.destroy(physxDrive); } - const posTarget = new this.PhysX.PxVec3(...positionTarget); const rotTarget = new this.PhysX.PxQuat(...angleTarget); const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); physxJoint.setDrivePosition(targetTransform); + this.PhysX.destroy(posTarget); + this.PhysX.destroy(rotTarget); + this.PhysX.destroy(targetTransform); + } + + createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { + const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); + const resultB = this.computeJointOffsetAndActor( + gltf.nodes[joint.connectedNode], + simplifiedJoint + ); + + const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); + const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); + const poseA = new this.PhysX.PxTransform(pos, rot); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + + const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); + const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); + const poseB = new this.PhysX.PxTransform(posB, rotB); + this.PhysX.destroy(posB); + this.PhysX.destroy(rotB); + + const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( + this.physics, + resultA.actor, + poseA, + resultB.actor, + poseB + ); + this.PhysX.destroy(poseA); + this.PhysX.destroy(poseB); + + physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); + + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); + + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + joint.enableCollision + ); + + // Do not restict any axis by default + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); + + for (const limit of simplifiedJoint.limits) { + this._setLimitValues(physxJoint, simplifiedJoint, limit); + } + + this._setTwistLimitValues(physxJoint, simplifiedJoint); + this._setSwingLimitValues(physxJoint, simplifiedJoint); + + const linearVelocityTarget = vec3.fromValues(0, 0, 0); + const angularVelocityTarget = vec3.fromValues(0, 0, 0); + + for (const drive of simplifiedJoint.drives) { + this._setDriveValues(physxJoint, simplifiedJoint, drive); + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); + } + this._setDrivePositionTarget(physxJoint, simplifiedJoint); const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); physxJoint.setDriveVelocity(linVel, angVel); - - this.PhysX.destroy(posTarget); - this.PhysX.destroy(rotTarget); this.PhysX.destroy(linVel); this.PhysX.destroy(angVel); - this.PhysX.destroy(targetTransform); return physxJoint; } From ae5c9b6302c1c2fd1f32ef6f86f13cb1ccc1d058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 25 Mar 2026 11:54:31 +0100 Subject: [PATCH 90/93] Restructure files --- source/GltfState/phyiscs_controller.js | 2339 +-------------------- source/PhysicsEngines/PhysX.js | 2084 ++++++++++++++++++ source/PhysicsEngines/PhysicsInterface.js | 101 + source/gltf/physics_utils.js | 156 ++ 4 files changed, 2347 insertions(+), 2333 deletions(-) create mode 100644 source/PhysicsEngines/PhysX.js create mode 100644 source/PhysicsEngines/PhysicsInterface.js create mode 100644 source/gltf/physics_utils.js diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index 1e0fb2cf..e7d24ced 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -1,168 +1,6 @@ -import { getAnimatedIndices, getMorphedNodeIndices } from "../gltf/gltf_utils"; -import PhysX from "physx-js-webidl"; -// The import is needed for rollup to include the wasm file in the build output -// eslint-disable-next-line no-unused-vars -import PhysXBinaryFile from "physx-js-webidl/physx-js-webidl.wasm"; -import { gltfPhysicsMaterial } from "../gltf/rigid_bodies"; -import { - createBoxVertexData, - createCapsuleVertexData, - createCylinderVertexData -} from "../geometry_generator"; -import { vec3, mat4, quat, mat3 } from "gl-matrix"; - -class PhysicsUtils { - static calculateScaleAndAxis(node) { - const scaleFactor = vec3.clone(node.scale); - let scaleRotation = quat.create(); - - let currentNode = node.parentNode; - const currentRotation = quat.clone(node.rotation); - - while (currentNode !== undefined) { - if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { - const localScale = currentNode.scale; - vec3.transformQuat(localScale, currentNode.scale, scaleRotation); - vec3.multiply(scaleFactor, scaleFactor, localScale); - scaleRotation = quat.clone(currentRotation); - } - const nextRotation = quat.clone(currentNode.rotation); - quat.multiply(currentRotation, currentRotation, nextRotation); - currentNode = currentNode.parentNode; - } - return { scale: scaleFactor, scaleAxis: scaleRotation }; - } - - /** - * Converts triangle strip indices to triangle list indices - * @param {Uint32Array|Array} stripIndices - The triangle strip indices - * @returns {Uint32Array} - Triangle list indices - */ - static convertTriangleStripToTriangles(stripIndices) { - if (stripIndices.length < 3) { - return new Uint32Array(0); - } - - const triangleCount = stripIndices.length - 2; - const triangleIndices = new Uint32Array(triangleCount * 3); - let triangleIndex = 0; - - for (let i = 0; i < triangleCount; i++) { - if (i % 2 === 0) { - // Even triangle: maintain winding order - triangleIndices[triangleIndex++] = stripIndices[i]; - triangleIndices[triangleIndex++] = stripIndices[i + 1]; - triangleIndices[triangleIndex++] = stripIndices[i + 2]; - } else { - // Odd triangle: reverse winding order - triangleIndices[triangleIndex++] = stripIndices[i]; - triangleIndices[triangleIndex++] = stripIndices[i + 2]; - triangleIndices[triangleIndex++] = stripIndices[i + 1]; - } - } - - return triangleIndices; - } - - /** - * Converts triangle fan indices to triangle list indices - * @param {Uint32Array|Array} fanIndices - The triangle fan indices - * @returns {Uint32Array} - Triangle list indices - */ - static convertTriangleFanToTriangles(fanIndices) { - if (fanIndices.length < 3) { - return new Uint32Array(0); - } - - const triangleCount = fanIndices.length - 2; - const triangleIndices = new Uint32Array(triangleCount * 3); - let triangleIndex = 0; - - const centerVertex = fanIndices[0]; - - for (let i = 1; i < fanIndices.length - 1; i++) { - triangleIndices[triangleIndex++] = fanIndices[i]; - triangleIndices[triangleIndex++] = fanIndices[i + 1]; - triangleIndices[triangleIndex++] = centerVertex; - } - - return triangleIndices; - } - - static recurseCollider( - gltf, - node, - collider, - actorNode, - offsetChanged, - scaleChanged, - customFunction, - args = [] - ) { - // Do not add other motion bodies' shapes to this actor - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { - return; - } - - const computedWorldTransform = node.worldTransform; - if (node.animatedPropertyObjects.scale.dirty) { - scaleChanged = true; - } - if (node.isLocalTransformDirty()) { - offsetChanged = true; - } - - // Found a collider geometry - if ( - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.mesh !== undefined || - node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape !== undefined - ) { - customFunction( - gltf, - node, - node.extensions.KHR_physics_rigid_bodies.collider, - actorNode, - computedWorldTransform, - offsetChanged, - scaleChanged, - false, - ...args - ); - } - - // Found a trigger - if ( - node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.mesh !== undefined || - node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.shape !== undefined - ) { - customFunction( - gltf, - node, - node.extensions.KHR_physics_rigid_bodies.trigger, - actorNode, - computedWorldTransform, - offsetChanged, - scaleChanged, - true, - ...args - ); - } - - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - this.recurseCollider( - gltf, - childNode, - collider, - actorNode, - offsetChanged, - scaleChanged, - customFunction, - args - ); - } - } -} +import { getAnimatedIndices } from "../gltf/gltf_utils"; +import { NvidiaPhysicsInterface } from "../PhysicsEngines/PhysX"; +import { PhysicsUtils } from "../gltf/physics_utils"; class PhysicsController { constructor() { @@ -260,7 +98,9 @@ class PhysicsController { } this.skipFrames = 2; this.loading = true; - const morphedNodeIndices = getMorphedNodeIndices(state.gltf); + + // Morphing physics colliders was dropped from the spec. + // const morphedNodeIndices = getMorphedNodeIndices(state.gltf); const result = getAnimatedIndices(state.gltf, "/nodes/", [ "translation", "rotation", @@ -268,7 +108,6 @@ class PhysicsController { ]); let dynamicMeshColliderCount = 0; let staticMeshColliderCount = 0; - const animatedNodeIndices = result.animatedIndices; this.hasRuntimeAnimationTargets = result.runtimeChanges; const gatherRigidBodies = (nodeIndex, currentRigidBody) => { let parentRigidBody = currentRigidBody; @@ -562,2170 +401,4 @@ class PhysicsController { } } -class PhysicsInterface { - constructor() { - this.simpleShapes = []; - } - - async initializeEngine() {} - initializeSimulation( - state, - staticActors, - kinematicActors, - dynamicActors, - jointNodes, - triggerNodes, - independentTriggerNodes, - nodeToMotion, - hasRuntimeAnimationTargets, - staticMeshColliderCount, - dynamicMeshColliderCount - ) {} - pauseSimulation() {} - resumeSimulation() {} - resetSimulation() {} - stopSimulation() {} - enableDebugColliders(enable) {} - enableDebugJoints(enable) {} - - applyImpulse(nodeIndex, linearImpulse, angularImpulse) {} - applyPointImpulse(nodeIndex, impulse, position) {} - rayCast(rayStart, rayEnd) {} - - generateBox(x, y, z, scale, scaleAxis, reference) {} - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} - generateSphere(radius, scale, scaleAxis, reference) {} - generatePlane(width, height, doubleSided, scale, scaleAxis, reference) {} - generateSimpleShape( - shape, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create(), - reference = undefined - ) { - switch (shape.type) { - case "box": - return this.generateBox( - shape.box.size[0], - shape.box.size[1], - shape.box.size[2], - scale, - scaleAxis, - reference - ); - case "capsule": - return this.generateCapsule( - shape.capsule.height, - shape.capsule.radiusTop, - shape.capsule.radiusBottom, - scale, - scaleAxis, - reference - ); - case "cylinder": - return this.generateCylinder( - shape.cylinder.height, - shape.cylinder.radiusTop, - shape.cylinder.radiusBottom, - scale, - scaleAxis, - reference - ); - case "sphere": - return this.generateSphere(shape.sphere.radius, scale, scaleAxis, reference); - case "plane": - return this.generatePlane( - shape.plane.width, - shape.plane.height, - shape.plane.doubleSided, - scale, - scaleAxis, - reference - ); - } - } - - generateSimpleShapes(gltf) { - this.simpleShapes = []; - if (gltf?.extensions?.KHR_implicit_shapes === undefined) { - return; - } - for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { - this.simpleShapes.push(this.generateSimpleShape(shape)); - } - } - - updateActorTransform(node) {} - updatePhysicsJoint(state, jointNode) {} -} - -class NvidiaPhysicsInterface extends PhysicsInterface { - constructor() { - super(); - this.PhysX = undefined; - this.physics = undefined; - this.defaultMaterial = undefined; - this.tolerances = undefined; - - this.reset = false; - - // Needs to be reset for each scene - this.scene = undefined; - this.nodeToActor = new Map(); - this.nodeToMotion = new Map(); - this.nodeToSimplifiedJoints = new Map(); - this.shapeToNode = new Map(); - this.filterData = []; - this.physXFilterData = []; - this.physXMaterials = []; - - // Need for memory management - this.convexMeshes = []; - this.triangleMeshes = []; - - // Debug - this.debugColliders = false; - this.debugJoints = false; - this.debugStateChanged = true; - - this.MAX_FLOAT = 3.4028234663852885981170418348452e38; - } - - async initializeEngine() { - this.PhysX = await PhysX({ locateFile: () => "./libs/physx-js-webidl.wasm" }); - const version = this.PhysX.PHYSICS_VERSION; - console.log( - "PhysX loaded! Version: " + - ((version >> 24) & 0xff) + - "." + - ((version >> 16) & 0xff) + - "." + - ((version >> 8) & 0xff) - ); - - const allocator = new this.PhysX.PxDefaultAllocator(); - const errorCb = new this.PhysX.PxDefaultErrorCallback(); - const foundation = this.PhysX.CreateFoundation(version, allocator, errorCb); - console.log("Created PxFoundation"); - - this.tolerances = new this.PhysX.PxTolerancesScale(); - this.tolerances.speed = 9.81; - this.physics = this.PhysX.CreatePhysics(version, foundation, this.tolerances); - this.defaultMaterial = this.createPhysXMaterial(new gltfPhysicsMaterial()); - console.log("Created PxPhysics"); - return this.PhysX; - } - - updatePhysicMaterials(gltf) { - const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; - if (materials === undefined) { - return; - } - for (let i = 0; i < materials.length; i++) { - const material = materials[i]; - if (material.isDirty()) { - const physXMaterial = this.physXMaterials[i]; - physXMaterial.setStaticFriction(material.staticFriction); - physXMaterial.setDynamicFriction(material.dynamicFriction); - physXMaterial.setRestitution(material.restitution); - } - } - } - - updateActorTransform(node) { - if (node.dirtyTransform) { - const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; - if (actor === undefined) { - return; - } - const pxPos = new this.PhysX.PxVec3( - node.worldTransform[12], - node.worldTransform[13], - node.worldTransform[14] - ); - const pxRot = new this.PhysX.PxQuat(...node.worldQuaternion); - const pxTransform = new this.PhysX.PxTransform(pxPos, pxRot); - if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { - actor.setKinematicTarget(pxTransform); - } else { - actor.setGlobalPose(pxTransform); - } - this.PhysX.destroy(pxPos); - this.PhysX.destroy(pxRot); - this.PhysX.destroy(pxTransform); - } - } - - calculateMassAndInertia(motion, actor) { - const pos = new this.PhysX.PxVec3(0, 0, 0); - if (motion.centerOfMass !== undefined) { - pos.x = motion.centerOfMass[0]; - pos.y = motion.centerOfMass[1]; - pos.z = motion.centerOfMass[2]; - } - const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); - if (motion.inertiaDiagonal !== undefined) { - let inertia = undefined; - if ( - motion.inertiaOrientation !== undefined && - !quat.exactEquals(motion.inertiaOrientation, quat.create()) - ) { - const intertiaRotMat = mat3.create(); - - const inertiaDiagonalMat = mat3.create(); - inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; - inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; - inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; - - if ( - quat.length(motion.inertiaOrientation) > 1.0e-5 || - quat.length(motion.inertiaOrientation) < 1.0e-5 - ) { - mat3.identity(intertiaRotMat); - console.warn( - "PhysX: Invalid inertia orientation quaternion, ignoring rotation" - ); - } else { - mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); - } - - const inertiaTensor = mat3.create(); - mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); - - const col0 = new this.PhysX.PxVec3( - inertiaTensor[0], - inertiaTensor[1], - inertiaTensor[2] - ); - const col1 = new this.PhysX.PxVec3( - inertiaTensor[3], - inertiaTensor[4], - inertiaTensor[5] - ); - const col2 = new this.PhysX.PxVec3( - inertiaTensor[6], - inertiaTensor[7], - inertiaTensor[8] - ); - const pxInertiaTensor = new this.PhysX.PxMat33(col0, col1, col2); - inertia = this.PhysX.PxMassProperties.prototype.getMassSpaceInertia( - pxInertiaTensor, - rot - ); - this.PhysX.destroy(col0); - this.PhysX.destroy(col1); - this.PhysX.destroy(col2); - this.PhysX.destroy(pxInertiaTensor); - actor.setMassSpaceInertiaTensor(inertia); - } else { - inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); - } - } else { - if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); - } else { - this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( - actor, - motion.mass, - pos - ); - } - } - - const pose = new this.PhysX.PxTransform(pos, rot); - actor.setCMassLocalPose(pose); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(pose); - } - - updateMotion(actorNode) { - const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; - const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; - if (motion.animatedPropertyObjects.isKinematic.dirty) { - if (motion.isKinematic) { - const linearVelocity = actor.getLinearVelocity(); - motion.computedLinearVelocity = [ - linearVelocity.x, - linearVelocity.y, - linearVelocity.z - ]; - const angularVelocity = actor.getAngularVelocity(); - motion.computedAngularVelocity = [ - angularVelocity.x, - angularVelocity.y, - angularVelocity.z - ]; - } else { - motion.computedLinearVelocity = undefined; - motion.computedAngularVelocity = undefined; - } - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, !motion.isKinematic); - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); - } - if (motion.animatedPropertyObjects.mass.dirty) { - actor.setMass(motion.mass); - } - if ( - motion.animatedPropertyObjects.centerOfMass.dirty || - motion.animatedPropertyObjects.inertiaOrientation.dirty || - motion.animatedPropertyObjects.inertiaDiagonal.dirty - ) { - this.calculateMassAndInertia(motion, actor); - } - if (motion.animatedPropertyObjects.gravityFactor.dirty) { - actor.setActorFlag( - this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, - motion.gravityFactor !== 1.0 - ); - } - if (motion.animatedPropertyObjects.linearVelocity.dirty) { - const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); - actor.setLinearVelocity(pxVelocity); - motion.computedLinearVelocity = undefined; - } - if (motion.animatedPropertyObjects.angularVelocity.dirty) { - const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); - actor.setAngularVelocity(pxVelocity); - motion.computedAngularVelocity = undefined; - } - } - - updateCollider( - gltf, - node, - collider, - actorNode, - worldTransform, - offsetChanged, - scaleChanged, - isTrigger - ) { - const result = this.nodeToActor.get(actorNode.gltfObjectIndex); - const actor = result?.actor; - const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); - - let currentGeometry = currentShape.getGeometry(); - const currentColliderType = currentGeometry.getType(); - const shapeIndex = collider?.geometry?.shape; - let scale = vec3.fromValues(1, 1, 1); - let scaleAxis = quat.create(); - if (shapeIndex !== undefined) { - // Simple shapes need to be recreated if scale changed - // If properties changed we also need to recreate the mesh colliders - const dirty = gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex].isDirty(); - if ( - scaleChanged && - !dirty && - currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH - ) { - // Update convex mesh scale - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxConvexMeshGeometry - ); - const result = PhysicsUtils.calculateScaleAndAxis(node); - scale = result.scale; - scaleAxis = result.scaleAxis; - const pxScale = new this.PhysX.PxVec3(...scale); - const pxRotation = new this.PhysX.PxQuat(...scaleAxis); - const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); - currentGeometry.scale = meshScale; - this.PhysX.destroy(pxScale); - this.PhysX.destroy(pxRotation); - } else if (dirty || scaleChanged) { - // Recreate simple shape collider - const newGeometry = this.generateSimpleShape( - gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], - scale, - scaleAxis - ); - currentGeometry.release?.(); - if (newGeometry.getType() !== currentColliderType) { - // We need to recreate the shape - let shapeFlags = undefined; - if (isTrigger) { - shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; - } else { - shapeFlags = this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE; - } - if (this.debugColliders) { - shapeFlags |= this.PhysX.PxShapeFlagEnum.eVISUALIZATION; - } - const shape = this.createShapeFromGeometry( - newGeometry, - undefined, - undefined, - shapeFlags, - collider - ); - result?.pxShapeMap.set(node.gltfObjectIndex, shape); - actor.detachShape(currentShape); - actor.attachShape(shape); - this.PhysX.destroy(currentShape); - } else { - currentShape.setGeometry(newGeometry); - } - } - } else if (collider?.geometry?.mesh !== undefined) { - if (scaleChanged) { - if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxConvexMeshGeometry - ); - } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxTriangleMeshGeometry - ); - } - // apply scale - const result = PhysicsUtils.calculateScaleAndAxis(node); - scale = result.scale; - scaleAxis = result.scaleAxis; - const pxScale = new this.PhysX.PxVec3(...scale); - const pxRotation = new this.PhysX.PxQuat(...scaleAxis); - const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); - currentGeometry.scale = meshScale; - this.PhysX.destroy(pxScale); - this.PhysX.destroy(pxRotation); - } - } - if (offsetChanged) { - // Calculate offset position - const translation = vec3.create(); - const shapePosition = vec3.create(); - mat4.getTranslation(shapePosition, actorNode.worldTransform); - const invertedActorRotation = quat.create(); - quat.invert(invertedActorRotation, actorNode.worldQuaternion); - const offsetPosition = vec3.create(); - mat4.getTranslation(offsetPosition, worldTransform); - vec3.subtract(translation, offsetPosition, shapePosition); - vec3.transformQuat(translation, translation, invertedActorRotation); - - // Calculate offset rotation - const rotation = quat.create(); - quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); - - const PxPos = new this.PhysX.PxVec3(...translation); - const PxRotation = new this.PhysX.PxQuat(...rotation); - const pose = new this.PhysX.PxTransform(PxPos, PxRotation); - currentShape.setLocalPose(pose); - } - } - - updatePhysicsJoint(state, jointNode) { - const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); - if (pxJoints === undefined) { - return; - } - const gltfJoint = - state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[ - jointNode.extensions.KHR_physics_rigid_bodies.joint.joint - ]; - const simplifiedJoints = gltfJoint.simplifiedPhysicsJoints; - if (simplifiedJoints.length !== pxJoints.length) { - console.warn( - "Number of simplified joints does not match number of PhysX joints. Skipping joint update." - ); - return; - } - for (let i = 0; i < simplifiedJoints.length; i++) { - const pxJoint = pxJoints[i]; - const simplifiedJoint = simplifiedJoints[i]; - if ( - jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects - .enableCollision.dirty - ) { - pxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision - ); - } - for (const limit of simplifiedJoint.limits) { - if ( - limit.animatedPropertyObjects.min.dirty || - limit.animatedPropertyObjects.max.dirty || - limit.animatedPropertyObjects.stiffness.dirty || - limit.animatedPropertyObjects.damping.dirty - ) { - this._setLimitValues(pxJoint, simplifiedJoint, limit); - } - } - - if ( - simplifiedJoint.twistLimit && - (simplifiedJoint.twistLimit.animatedPropertyObjects.min.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.max.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.damping.dirty) - ) { - this._setTwistLimitValues(pxJoint, simplifiedJoint); - } - - if ( - (simplifiedJoint.swingLimit1 && - (simplifiedJoint.swingLimit1.animatedPropertyObjects.min.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.max.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.damping.dirty)) || - (simplifiedJoint.swingLimit2 && - (simplifiedJoint.swingLimit2.animatedPropertyObjects.min.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.max.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.damping.dirty)) - ) { - this._setSwingLimitValues(pxJoint, simplifiedJoint); - } - - let positionTargetDirty = false; - let velocityTargetDirty = false; - const linearVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); - const angularVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); - pxJoint.getDriveVelocity(linearVelocityTarget, angularVelocityTarget); - for (const drive of simplifiedJoint.drives) { - if ( - drive.animatedPropertyObjects.stiffness.dirty || - drive.animatedPropertyObjects.damping.dirty || - drive.animatedPropertyObjects.maxForce.dirty - ) { - this._setDriveValues(pxJoint, simplifiedJoint, drive); - } - if (drive.animatedPropertyObjects.velocityTarget.dirty) { - this._getDriveVelocityTarget( - simplifiedJoint, - drive, - linearVelocityTarget, - angularVelocityTarget - ); - velocityTargetDirty = true; - } - if (drive.animatedPropertyObjects.positionTarget.dirty) { - positionTargetDirty = true; - } - } - - if (positionTargetDirty) { - this._setDrivePositionTarget(pxJoint, simplifiedJoint); - } - if (velocityTargetDirty) { - pxJoint.setDriveVelocity(linearVelocityTarget, angularVelocityTarget); - } - } - } - - mapCombineMode(mode) { - switch (mode) { - case "average": - return this.PhysX.PxCombineModeEnum.eAVERAGE; - case "minimum": - return this.PhysX.PxCombineModeEnum.eMIN; - case "maximum": - return this.PhysX.PxCombineModeEnum.eMAX; - case "multiply": - return this.PhysX.PxCombineModeEnum.eMULTIPLY; - } - } - - // Either create a box or update an existing one. Returns only newly created geometry - generateBox(x, y, z, scale, scaleAxis, reference) { - let referenceType = undefined; - if (reference !== undefined) { - referenceType = reference.getType(); - } - if ( - scale.every((value) => value === scale[0]) === false && - quat.equals(scaleAxis, quat.create()) === false - ) { - const data = createBoxVertexData(x, y, z); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } - let geometry = undefined; - if (referenceType === this.PhysX.PxGeometryTypeEnum.eBOX) { - const halfExtents = new this.PhysX.PxVec3( - (x / 2) * scale[0], - (y / 2) * scale[1], - (z / 2) * scale[2] - ); - reference.halfExtents = halfExtents; - this.PhysX.destroy(halfExtents); - } else { - geometry = new this.PhysX.PxBoxGeometry( - (x / 2) * scale[0], - (y / 2) * scale[1], - (z / 2) * scale[2] - ); - } - - return geometry; - } - - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { - const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } - - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) { - if ( - (quat.equals(scaleAxis, quat.create()) === false && - scale.every((value) => value === scale[0]) === false) || - radiusTop !== radiusBottom || - scale[0] !== scale[2] - ) { - const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } - height *= scale[1]; - radiusTop *= scale[0]; - radiusBottom *= scale[0]; - const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices); - } - - generateSphere(radius, scale, scaleAxis, reference) { - let referenceType = undefined; - if (reference !== undefined) { - referenceType = reference.getType(); - } - if (scale.every((value) => value === scale[0]) === false) { - const data = createCapsuleVertexData(radius, radius, 0); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } else { - radius *= scale[0]; - } - if (referenceType === this.PhysX.PxGeometryTypeEnum.eSPHERE) { - reference.radius = radius; - return undefined; - } - const geometry = new this.PhysX.PxSphereGeometry(radius); - return geometry; - } - - generatePlane(width, height, doubleSided, scale, scaleAxis, reference) { - if (reference !== undefined) { - //TODO handle update - return undefined; - } - const geometry = new this.PhysX.PxPlaneGeometry(); - return geometry; - } - - createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const malloc = (f, q) => { - const nDataBytes = f.length * f.BYTES_PER_ELEMENT; - if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); - let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); - dataHeap.set(new Uint8Array(f.buffer)); - return q; - }; - const des = new this.PhysX.PxConvexMeshDesc(); - des.points.stride = vertices.BYTES_PER_ELEMENT * 3; - des.points.count = vertices.length / 3; - des.points.data = malloc(vertices); - - let flag = 0; - flag |= this.PhysX.PxConvexFlagEnum.eCOMPUTE_CONVEX; - flag |= this.PhysX.PxConvexFlagEnum.eSHIFT_VERTICES; - //flag |= this.PhysX.PxConvexFlagEnum.eDISABLE_MESH_VALIDATION; - const pxflags = new this.PhysX.PxConvexFlags(flag); - des.flags = pxflags; - const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); - cookingParams.planeTolerance = 0.0007; //Default - const tri = this.PhysX.CreateConvexMesh(cookingParams, des); - this.convexMeshes.push(tri); - - const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); - const PxQuat = new this.PhysX.PxQuat(...scaleAxis); - const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); - const f = new this.PhysX.PxConvexMeshGeometryFlags(); - const geometry = new this.PhysX.PxConvexMeshGeometry(tri, ms, f); - this.PhysX.destroy(PxScale); - this.PhysX.destroy(PxQuat); - this.PhysX.destroy(ms); - this.PhysX.destroy(pxflags); - this.PhysX.destroy(cookingParams); - this.PhysX.destroy(des); - return geometry; - } - - collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { - let positionDataArray = []; - let positionCount = 0; - let indexDataArray = []; - let indexCount = 0; - - for (const primitive of mesh.primitives) { - const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; - const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); - - if (primitive.targets !== undefined) { - let morphWeights = mesh.weights; - if (morphWeights !== undefined) { - // Calculate morphed vertex positions on CPU - const morphPositionData = []; - for (const target of primitive.targets) { - if (target.POSITION !== undefined) { - const morphAccessor = gltf.accessors[target.POSITION]; - morphPositionData.push( - morphAccessor.getNormalizedDeinterlacedView(gltf) - ); - } else { - morphPositionData.push(undefined); - } - } - for (let i = 0; i < positionData.length / 3; i++) { - for (let j = 0; j < morphWeights.length; j++) { - const morphData = morphPositionData[j]; - if (morphWeights[j] === 0 || morphData === undefined) { - continue; - } - positionData[i * 3] += morphData[i * 3] * morphWeights[j]; - positionData[i * 3 + 1] += morphData[i * 3 + 1] * morphWeights[j]; - positionData[i * 3 + 2] += morphData[i * 3 + 2] * morphWeights[j]; - } - } - } - } - - positionDataArray.push(positionData); - positionCount += positionAccessor.count; - if (computeIndices) { - let indexData = undefined; - if (primitive.indices !== undefined) { - const indexAccessor = gltf.accessors[primitive.indices]; - indexData = indexAccessor.getNormalizedDeinterlacedView(gltf); - } else { - const array = Array.from(Array(positionAccessor.count).keys()); - indexData = new Uint32Array(array); - } - if (primitive.mode === 5) { - indexData = PhysicsUtils.convertTriangleStripToTriangles(indexData); - } else if (primitive.mode === 6) { - indexData = PhysicsUtils.convertTriangleFanToTriangles(indexData); - } else if (primitive.mode !== undefined && primitive.mode !== 4) { - console.warn( - "Unsupported primitive mode for physics mesh collider creation: " + - primitive.mode - ); - } - indexDataArray.push(indexData); - indexCount += indexData.length; - } - } - - const positionData = new Float32Array(positionCount * 3); - const indexData = new Uint32Array(indexCount); - let offset = 0; - for (const positionChunk of positionDataArray) { - positionData.set(positionChunk, offset); - offset += positionChunk.length; - } - offset = 0; - for (const indexChunk of indexDataArray) { - indexData.set(indexChunk, offset); - offset += indexChunk.length; - } - return { vertices: positionData, indices: indexData }; - } - - createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); - return this.createConvexPxMesh(result.vertices, scale, scaleAxis); - } - - createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); - const malloc = (f, q) => { - const nDataBytes = f.length * f.BYTES_PER_ELEMENT; - if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); - let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); - dataHeap.set(new Uint8Array(f.buffer)); - return q; - }; - const des = new this.PhysX.PxTriangleMeshDesc(); - des.points.stride = vertices.BYTES_PER_ELEMENT * 3; - des.points.count = vertices.length / 3; - des.points.data = malloc(vertices); - - des.triangles.stride = indices.BYTES_PER_ELEMENT * 3; - des.triangles.count = indices.length / 3; - des.triangles.data = malloc(indices); - - const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); - const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); - this.triangleMeshes.push(tri); - - const PxScale = new this.PhysX.PxVec3(1, 1, 1); - const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); - - if (scale !== undefined) { - PxScale.x = scale[0]; - PxScale.y = scale[1]; - PxScale.z = scale[2]; - } - if (scaleAxis !== undefined) { - PxQuat.x = scaleAxis[0]; - PxQuat.y = scaleAxis[1]; - PxQuat.z = scaleAxis[2]; - PxQuat.w = scaleAxis[3]; - } - const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); - const f = new this.PhysX.PxMeshGeometryFlags(); - const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); - this.PhysX.destroy(PxScale); - this.PhysX.destroy(PxQuat); - this.PhysX.destroy(ms); - this.PhysX.destroy(cookingParams); - this.PhysX.destroy(des); - return geometry; - } - - collidesWith(filterA, filterB) { - if (filterB.collideWithSystems.length > 0) { - for (const system of filterB.collideWithSystems) { - if (filterA.collisionSystems.includes(system)) { - return true; - } - } - return false; - } else if (filterB.notCollideWithSystems.length > 0) { - for (const system of filterB.notCollideWithSystems) { - if (filterA.collisionSystems.includes(system)) { - return false; - } - } - return true; - } - return true; - } - - computeFilterData(gltf) { - // Default filter is sign bit - const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; - this.filterData = new Array(32).fill(0); - this.filterData[31] = Math.pow(2, 32) - 1; // Default filter with all bits set - let filterCount = filters?.length ?? 0; - if (filterCount > 31) { - filterCount = 31; - console.warn( - "PhysX supports a maximum of 31 collision filters. Additional filters will be ignored." - ); - } - - for (let i = 0; i < filterCount; i++) { - let bitMask = 0; - for (let j = 0; j < filterCount; j++) { - if ( - this.collidesWith(filters[i], filters[j]) && - this.collidesWith(filters[j], filters[i]) - ) { - bitMask |= 1 << j; - } - } - this.filterData[i] = bitMask; - } - } - - createPhysXMaterial(gltfPhysicsMaterial) { - if (gltfPhysicsMaterial === undefined) { - return this.defaultMaterial; - } - - const physxMaterial = this.physics.createMaterial( - gltfPhysicsMaterial.staticFriction, - gltfPhysicsMaterial.dynamicFriction, - gltfPhysicsMaterial.restitution - ); - if (gltfPhysicsMaterial.frictionCombine !== undefined) { - physxMaterial.setFrictionCombineMode( - this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) - ); - } - if (gltfPhysicsMaterial.restitutionCombine !== undefined) { - physxMaterial.setRestitutionCombineMode( - this.mapCombineMode(gltfPhysicsMaterial.restitutionCombine) - ); - } - return physxMaterial; - } - - createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { - let word0 = null; - let word1 = null; - if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { - word0 = 1 << collisionFilter; - word1 = this.filterData[collisionFilter]; - } else { - // Default filter id is signed bit and all bits set to collide with everything - word0 = Math.pow(2, 31); - word1 = Math.pow(2, 32) - 1; - } - - additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_DISCRETE_CONTACT; - additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_CCD_CONTACT; - - return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); - } - - createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { - if (physXMaterial === undefined) { - if (glTFCollider?.physicsMaterial !== undefined) { - physXMaterial = this.physXMaterials[glTFCollider.physicsMaterial]; - } else { - physXMaterial = this.defaultMaterial; - } - } - const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); - - if (physXFilterData === undefined) { - physXFilterData = - this.physXFilterData[ - glTFCollider?.collisionFilter ?? this.physXFilterData.length - 1 - ]; - } - - shape.setSimulationFilterData(physXFilterData); - - return shape; - } - - createShape( - gltf, - node, - collider, - shapeFlags, - physXMaterial, - physXFilterData, - convexHull, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create() - ) { - let geometry = undefined; - if (collider?.geometry?.shape !== undefined) { - if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { - const simpleShape = - gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; - geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); - } else { - geometry = this.simpleShapes[collider.geometry.shape]; - } - } else if (collider?.geometry?.mesh !== undefined) { - const mesh = gltf.meshes[collider.geometry.mesh]; - if (convexHull === true) { - geometry = this.createConvexMesh(gltf, mesh, scale, scaleAxis); - } else { - geometry = this.createPxMesh(gltf, mesh, scale, scaleAxis); - } - } - - if (geometry === undefined) { - return undefined; - } - - const shape = this.createShapeFromGeometry( - geometry, - physXMaterial, - physXFilterData, - shapeFlags, - collider - ); - - this.shapeToNode.set(shape.ptr, node.gltfObjectIndex); - return shape; - } - - createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { - const worldTransform = node.worldTransform; - const translation = vec3.create(); - mat4.getTranslation(translation, worldTransform); - const pos = new this.PhysX.PxVec3(...translation); - const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); - const pose = new this.PhysX.PxTransform(pos, rotation); - let actor = null; - const pxShapeMap = new Map(); - if (type === "static" || type === "trigger") { - actor = this.physics.createRigidStatic(pose); - } else { - actor = this.physics.createRigidDynamic(pose); - if (type === "kinematic") { - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); - } - actor.setRigidBodyFlag( - this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, - type !== "kinematic" - ); - const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion) { - const gltfAngularVelocity = motion?.angularVelocity; - const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); - actor.setAngularVelocity(angularVelocity, true); - this.PhysX.destroy(angularVelocity); - - const gltfLinearVelocity = motion?.linearVelocity; - const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); - actor.setLinearVelocity(linearVelocity, true); - this.PhysX.destroy(linearVelocity); - - if (motion.mass !== undefined) { - actor.setMass(motion.mass); - } - - this.calculateMassAndInertia(motion, actor); - - if (motion.gravityFactor !== 1.0) { - actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); - } - } - } - - const createAndAddShape = ( - gltf, - node, - collider, - actorNode, - worldTransform, - offsetChanged, - scaleChanged, - isTrigger, - noMeshShapes, - shapeFlags, - triggerFlags - ) => { - // Calculate offset position - const translation = vec3.create(); - const shapePosition = vec3.create(); - mat4.getTranslation(shapePosition, actorNode.worldTransform); - const invertedActorRotation = quat.create(); - quat.invert(invertedActorRotation, actorNode.worldQuaternion); - const offsetPosition = vec3.create(); - mat4.getTranslation(offsetPosition, worldTransform); - vec3.subtract(translation, offsetPosition, shapePosition); - vec3.transformQuat(translation, translation, invertedActorRotation); - - // Calculate offset rotation - const rotation = quat.create(); - quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); - - // Calculate scale and scaleAxis - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); - - const materialIndex = collider?.physicsMaterial; - const material = - materialIndex !== undefined - ? this.physXMaterials[materialIndex] - : this.defaultMaterial; - - const physXFilterData = - collider?.collisionFilter !== undefined - ? this.physXFilterData[collider.collisionFilter] - : this.physXFilterData[this.physXFilterData.length - 1]; - - const shape = this.createShape( - gltf, - node, - collider, - isTrigger ? triggerFlags : shapeFlags, - material, - physXFilterData, - noMeshShapes || collider?.geometry?.convexHull === true, - scale, - scaleAxis - ); - - if (shape !== undefined) { - const PxPos = new this.PhysX.PxVec3(...translation); - const PxRotation = new this.PhysX.PxQuat(...rotation); - const pose = new this.PhysX.PxTransform(PxPos, PxRotation); - shape.setLocalPose(pose); - - actor.attachShape(shape); - pxShapeMap.set(node.gltfObjectIndex, shape); - this.PhysX.destroy(PxPos); - this.PhysX.destroy(PxRotation); - this.PhysX.destroy(pose); - } - }; - - // If a node contains trigger and collider combine them - - let collider = undefined; - if (type !== "trigger") { - collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - false, - noMeshShapes, - shapeFlags, - triggerFlags - ); - collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; - if (collider !== undefined) { - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - true, - true, - shapeFlags, - triggerFlags - ); - } - } else { - collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - true, - true, - shapeFlags, - triggerFlags - ); - } - - if (type !== "trigger") { - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - PhysicsUtils.recurseCollider( - gltf, - childNode, - undefined, - node, - node.dirtyScale, - node.dirtyScale, - createAndAddShape, - [noMeshShapes, shapeFlags, triggerFlags] - ); - } - } - - this.PhysX.destroy(pos); - this.PhysX.destroy(rotation); - this.PhysX.destroy(pose); - - this.scene.addActor(actor); - - this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); - } - - computeJointOffsetAndActor(node, referencedJoint) { - let currentNode = node; - while (currentNode !== undefined) { - if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { - break; - } - currentNode = currentNode.parentNode; - } - - const nodeWorldRot = node.worldQuaternion; - const localPhysXRot = referencedJoint?.localRotation; - if (localPhysXRot !== undefined) { - quat.multiply(nodeWorldRot, node.worldQuaternion, localPhysXRot); - } - - if (currentNode === undefined) { - const pos = vec3.create(); - mat4.getTranslation(pos, node.worldTransform); - - return { actor: undefined, offsetPosition: pos, offsetRotation: nodeWorldRot }; - } - const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; - const inverseActorRotation = quat.create(); - quat.invert(inverseActorRotation, currentNode.worldQuaternion); - const offsetRotation = quat.create(); - quat.multiply(offsetRotation, inverseActorRotation, nodeWorldRot); - - const actorPosition = vec3.create(); - mat4.getTranslation(actorPosition, currentNode.worldTransform); - const nodePosition = vec3.create(); - mat4.getTranslation(nodePosition, node.worldTransform); - const offsetPosition = vec3.create(); - vec3.subtract(offsetPosition, nodePosition, actorPosition); - vec3.transformQuat(offsetPosition, offsetPosition, inverseActorRotation); - - return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; - } - - convertAxisIndexToEnum(axisIndex, type) { - if (type === "linear") { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6AxisEnum.eX; - case 1: - return this.PhysX.PxD6AxisEnum.eY; - case 2: - return this.PhysX.PxD6AxisEnum.eZ; - } - } else if (type === "angular") { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6AxisEnum.eTWIST; - case 1: - return this.PhysX.PxD6AxisEnum.eSWING1; - case 2: - return this.PhysX.PxD6AxisEnum.eSWING2; - } - } - return null; - } - - convertAxisIndexToAngularDriveEnum(axisIndex) { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6DriveEnum.eTWIST; - case 1: - return 6; // Currently not exposed via bindings - case 2: - return 7; // Currently not exposed via bindings - } - return null; - } - - validateSwingLimits(joint) { - // Check if swing limits are symmetric (cone) or asymmetric (pyramid) - if (joint.swingLimit1 && joint.swingLimit2) { - const limit1 = joint.swingLimit1; - const limit2 = joint.swingLimit2; - - const isSymmetric1 = - Math.abs(limit1.min + limit1.max) < 1e-6 || limit1.min === undefined; // Centered around 0 - const isSymmetric2 = - Math.abs(limit2.min + limit2.max) < 1e-6 || limit2.min === undefined; - - // Return if this is a cone limit (symmetric and same range) vs pyramid limit - return isSymmetric1 && isSymmetric2; - } - return false; - } - - createJoint(gltf, node) { - const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; - const referencedJoint = - gltf.extensions?.KHR_physics_rigid_bodies?.physicsJoints[joint.joint]; - - if (referencedJoint === undefined) { - console.error("Referenced joint not found:", joint.joint); - return; - } - const simplifiedJoints = []; - for (const simplifiedJoint of referencedJoint.simplifiedPhysicsJoints) { - const physxJoint = this.createSimplifiedJoint(gltf, node, joint, simplifiedJoint); - simplifiedJoints.push(physxJoint); - } - this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); - } - - _setLimitValues(physxJoint, simplifiedJoint, limit) { - const lock = limit.min === 0 && limit.max === 0; - const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); - const isDistanceLimit = - limit.linearAxes && - limit.linearAxes.length === 3 && - (limit.min === undefined || limit.min === 0) && - limit.max !== 0; - if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { - const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( - limit.min ?? -this.MAX_FLOAT, - limit.max ?? this.MAX_FLOAT, - spring - ); - for (const axis of limit.linearAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setMotion( - physxAxis, - lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED - ); - if (!lock) { - physxJoint.setLinearLimit(physxAxis, linearLimitPair); - } - } - this.PhysX.destroy(linearLimitPair); - } - if (isDistanceLimit) { - const linearLimit = new this.PhysX.PxJointLinearLimit( - limit.max ?? this.MAX_FLOAT, - spring - ); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setDistanceLimit(linearLimit); - this.PhysX.destroy(linearLimit); - } - if (limit.angularAxes && limit.angularAxes.length > 0) { - for (const axis of limit.angularAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); - physxJoint.setMotion( - physxAxis, - lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED - ); - } - } - this.PhysX.destroy(spring); - } - - _setTwistLimitValues(physxJoint, simplifiedJoint) { - if (simplifiedJoint.twistLimit !== undefined) { - if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { - const limitPair = new this.PhysX.PxJointAngularLimitPair( - simplifiedJoint.twistLimit.min ?? -Math.PI, - simplifiedJoint.twistLimit.max ?? Math.PI, - new this.PhysX.PxSpring( - simplifiedJoint.twistLimit.stiffness ?? 0, - simplifiedJoint.twistLimit.damping - ) - ); - physxJoint.setTwistLimit(limitPair); - this.PhysX.destroy(limitPair); - } - } - } - - _setSwingLimitValues(physxJoint, simplifiedJoint) { - if ( - simplifiedJoint.swingLimit1 !== undefined && - simplifiedJoint.swingLimit2 !== undefined - ) { - if ( - simplifiedJoint.swingLimit1.stiffness !== simplifiedJoint.swingLimit2.stiffness || - simplifiedJoint.swingLimit1.damping !== simplifiedJoint.swingLimit2.damping - ) { - console.warn( - "PhysX does not support different stiffness/damping for swing limits." - ); - } else { - const spring = new this.PhysX.PxSpring( - simplifiedJoint.swingLimit1.stiffness ?? 0, - simplifiedJoint.swingLimit1.damping - ); - let yMin = -Math.PI / 2; - let yMax = Math.PI / 2; - let zMin = -Math.PI / 2; - let zMax = Math.PI / 2; - if (simplifiedJoint.swingLimit1.min !== undefined) { - yMin = simplifiedJoint.swingLimit1.min; - } - if (simplifiedJoint.swingLimit1.max !== undefined) { - yMax = simplifiedJoint.swingLimit1.max; - } - if (simplifiedJoint.swingLimit2.min !== undefined) { - zMin = simplifiedJoint.swingLimit2.min; - } - if (simplifiedJoint.swingLimit2.max !== undefined) { - zMax = simplifiedJoint.swingLimit2.max; - } - - const isSymmetric = this.validateSwingLimits(simplifiedJoint); - if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { - // Fixed limit is already set - } else if (isSymmetric) { - const swing1Angle = Math.max(Math.abs(yMin), Math.abs(yMax)); - const swing2Angle = Math.max(Math.abs(zMin), Math.abs(zMax)); - const jointLimitCone = new this.PhysX.PxJointLimitCone( - swing1Angle, - swing2Angle, - spring - ); - physxJoint.setSwingLimit(jointLimitCone); - this.PhysX.destroy(jointLimitCone); - } else { - const jointLimitCone = new this.PhysX.PxJointLimitPyramid( - yMin, - yMax, - zMin, - zMax, - spring - ); - physxJoint.setPyramidSwingLimit(jointLimitCone); - this.PhysX.destroy(jointLimitCone); - } - this.PhysX.destroy(spring); - } - } else if ( - simplifiedJoint.swingLimit1 !== undefined || - simplifiedJoint.swingLimit2 !== undefined - ) { - const singleLimit = simplifiedJoint.swingLimit1 ?? simplifiedJoint.swingLimit2; - if (singleLimit.min === 0 && singleLimit.max === 0) { - // Fixed limit is already set - } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { - console.warn( - "PhysX requires symmetric limits for swing limits in single axis mode." - ); - } else { - const spring = new this.PhysX.PxSpring( - singleLimit.stiffness ?? 0, - singleLimit.damping - ); - const maxY = simplifiedJoint.swingLimit1?.max ?? Math.PI; - const maxZ = simplifiedJoint.swingLimit2?.max ?? Math.PI; - const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); - physxJoint.setSwingLimit(jointLimitCone); - this.PhysX.destroy(spring); - this.PhysX.destroy(jointLimitCone); - } - } - } - - _setDriveValues(physxJoint, simplifiedJoint, drive) { - const physxDrive = new this.PhysX.PxD6JointDrive( - drive.stiffness, - drive.damping, - drive.maxForce ?? this.MAX_FLOAT, - drive.mode === "acceleration" - ); - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - const axis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setDrive(axis, physxDrive); - } else if (drive.type === "angular") { - const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); - physxJoint.setDrive(axis, physxDrive); - } - this.PhysX.destroy(physxDrive); - } - - _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - if (drive.velocityTarget !== undefined) { - linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; - } - } else if (drive.type === "angular") { - if (drive.velocityTarget !== undefined) { - angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation - } - } - } - - _setDrivePositionTarget(physxJoint, simplifiedJoint) { - const positionTarget = vec3.fromValues(0, 0, 0); - const angleTarget = quat.create(); - for (const drive of simplifiedJoint.drives) { - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - if (drive.positionTarget !== undefined) { - positionTarget[result.axis] = drive.positionTarget * result.sign; - } - } else if (drive.type === "angular") { - if (drive.positionTarget !== undefined) { - // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise - switch (result.axis) { - case 0: { - quat.rotateX( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign - ); - break; - } - case 1: { - quat.rotateY( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign - ); - break; - } - case 2: { - quat.rotateZ( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign - ); - break; - } - } - } - } - } - const posTarget = new this.PhysX.PxVec3(...positionTarget); - const rotTarget = new this.PhysX.PxQuat(...angleTarget); - const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); - physxJoint.setDrivePosition(targetTransform); - this.PhysX.destroy(posTarget); - this.PhysX.destroy(rotTarget); - this.PhysX.destroy(targetTransform); - } - - createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { - const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); - const resultB = this.computeJointOffsetAndActor( - gltf.nodes[joint.connectedNode], - simplifiedJoint - ); - - const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); - const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); - const poseA = new this.PhysX.PxTransform(pos, rot); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - - const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); - const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); - const poseB = new this.PhysX.PxTransform(posB, rotB); - this.PhysX.destroy(posB); - this.PhysX.destroy(rotB); - - const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( - this.physics, - resultA.actor, - poseA, - resultB.actor, - poseB - ); - this.PhysX.destroy(poseA); - this.PhysX.destroy(poseB); - - physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); - - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); - - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - joint.enableCollision - ); - - // Do not restict any axis by default - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); - - for (const limit of simplifiedJoint.limits) { - this._setLimitValues(physxJoint, simplifiedJoint, limit); - } - - this._setTwistLimitValues(physxJoint, simplifiedJoint); - this._setSwingLimitValues(physxJoint, simplifiedJoint); - - const linearVelocityTarget = vec3.fromValues(0, 0, 0); - const angularVelocityTarget = vec3.fromValues(0, 0, 0); - - for (const drive of simplifiedJoint.drives) { - this._setDriveValues(physxJoint, simplifiedJoint, drive); - this._getDriveVelocityTarget( - simplifiedJoint, - drive, - linearVelocityTarget, - angularVelocityTarget - ); - } - this._setDrivePositionTarget(physxJoint, simplifiedJoint); - - const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); - const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); - physxJoint.setDriveVelocity(linVel, angVel); - this.PhysX.destroy(linVel); - this.PhysX.destroy(angVel); - - return physxJoint; - } - - changeDebugVisualization() { - if (!this.scene || !this.debugStateChanged) { - return; - } - this.debugStateChanged = false; - this.scene.setVisualizationParameter( - this.PhysX.eSCALE, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eWORLD_AXES, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eACTOR_AXES, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eCOLLISION_SHAPES, - this.debugColliders ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eJOINT_LOCAL_FRAMES, - this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); - for (const joints of this.nodeToSimplifiedJoints.values()) { - for (const joint of joints) { - joint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); - } - } - for (const shapePtr of this.shapeToNode.keys()) { - const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); - shape.setFlag(this.PhysX.PxShapeFlagEnum.eVISUALIZATION, this.debugColliders); - } - } - - initializeSimulation( - state, - staticActors, - kinematicActors, - dynamicActors, - jointNodes, - triggerNodes, - independentTriggerNodes, - nodeToMotion, - hasRuntimeAnimationTargets, - staticMeshColliderCount, - dynamicMeshColliderCount - ) { - if (!this.PhysX) { - return; - } - this.nodeToMotion = nodeToMotion; - this.generateSimpleShapes(state.gltf); - this.computeFilterData(state.gltf); - for (let i = 0; i < this.filterData.length; i++) { - const physXFilterData = this.createPhysXCollisionFilter(i); - this.physXFilterData.push(physXFilterData); - } - - const materials = state.gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; - if (materials !== undefined) { - for (const gltfMaterial of materials) { - const physxMaterial = this.createPhysXMaterial(gltfMaterial); - this.physXMaterials.push(physxMaterial); - } - } - - const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); - const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); - sceneDesc.set_gravity(tmpVec); - sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); - sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); - const sceneFlags = new this.PhysX.PxSceneFlags( - this.PhysX.PxSceneFlagEnum.eENABLE_CCD | this.PhysX.PxSceneFlagEnum.eENABLE_PCM - ); - sceneDesc.flags = sceneFlags; - - this.scene = this.physics.createScene(sceneDesc); - let triggerCallback = undefined; - - if (triggerNodes.length > 0) { - console.log("Enabling trigger report callback"); - triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); - triggerCallback.onTrigger = (pairs, count) => { - for (const compoundTrigger of state.physicsController.compoundTriggerNodes.values()) { - compoundTrigger.added.clear(); - compoundTrigger.removed.clear(); - } - console.log("Trigger callback called with", count, "pairs"); - for (let i = 0; i < count; i++) { - const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); - const triggerShape = pair.triggerShape; - const otherShape = pair.otherShape; - const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); - const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); - if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { - state.graphController.rigidBodyTriggerEntered( - triggerNodeIndex, - otherNodeIndex, - nodeToMotion.get(otherNodeIndex) - ); - } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { - state.graphController.rigidBodyTriggerExited( - triggerNodeIndex, - otherNodeIndex, - nodeToMotion.get(otherNodeIndex) - ); - } - const compoundTriggers = - state.physicsController.triggerToCompound.get(triggerNodeIndex); - if (compoundTriggers !== undefined) { - for (const compoundTriggerIndex of compoundTriggers) { - const compoundTriggerInfo = - state.physicsController.compoundTriggerNodes.get( - compoundTriggerIndex - ); - if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { - compoundTriggerInfo.added.add(otherNodeIndex); - } else if ( - pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST - ) { - compoundTriggerInfo.removed.add(otherNodeIndex); - } - } - } - } - - for (const [ - idx, - compoundTrigger - ] of state.physicsController.compoundTriggerNodes.entries()) { - for (const addedNodeIndex of compoundTrigger.added) { - if (!compoundTrigger.previous.has(addedNodeIndex)) { - compoundTrigger.previous.set(addedNodeIndex, 1); - state.graphController.rigidBodyTriggerEntered( - idx, - addedNodeIndex, - nodeToMotion.get(addedNodeIndex) - ); - } else { - const currentCount = compoundTrigger.previous.get(addedNodeIndex); - compoundTrigger.previous.set(addedNodeIndex, currentCount + 1); - } - } - for (const removedNodeIndex of compoundTrigger.removed) { - const currentCount = compoundTrigger.previous.get(removedNodeIndex); - if (currentCount > 1) { - compoundTrigger.previous.set(removedNodeIndex, currentCount - 1); - } else { - compoundTrigger.previous.delete(removedNodeIndex); - state.graphController.rigidBodyTriggerExited( - idx, - removedNodeIndex, - nodeToMotion.get(removedNodeIndex) - ); - } - } - } - }; - triggerCallback.onConstraintBreak = (constraints, count) => {}; - triggerCallback.onWake = (actors, count) => {}; - triggerCallback.onSleep = (actors, count) => {}; - triggerCallback.onContact = (pairHeaders, pairs, count) => {}; - sceneDesc.simulationEventCallback = triggerCallback; - } - - this.scene = this.physics.createScene(sceneDesc); - - console.log("Created scene"); - const shapeFlags = new this.PhysX.PxShapeFlags( - this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | - this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE - ); - - const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); - - const alwaysConvexMeshes = - dynamicMeshColliderCount > 1 || - (staticMeshColliderCount > 0 && dynamicMeshColliderCount > 0); - - for (const node of staticActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "static"); - } - for (const node of kinematicActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "kinematic"); - } - for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "dynamic", true); - } - for (const node of independentTriggerNodes) { - if ( - this.nodeToActor.has(node.gltfObjectIndex) || - this.nodeToMotion.has(node.gltfObjectIndex) - ) { - continue; - } - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "trigger", true); - } - for (const node of jointNodes) { - this.createJoint(state.gltf, node); - } - - this.PhysX.destroy(tmpVec); - this.PhysX.destroy(sceneDesc); - this.PhysX.destroy(shapeFlags); - this.PhysX.destroy(triggerFlags); - - this.debugStateChanged = true; - this.changeDebugVisualization(); - } - - enableDebugColliders(enable) { - this.debugColliders = enable; - this.debugStateChanged = true; - } - - enableDebugJoints(enable) { - this.debugJoints = enable; - this.debugStateChanged = true; - } - - applyTransformRecursively(gltf, node, parentTransform) { - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { - return; - } - const localTransform = node.getLocalTransform(); - const globalTransform = mat4.create(); - mat4.multiply(globalTransform, parentTransform, localTransform); - node.scaledPhysicsTransform = globalTransform; - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - this.applyTransformRecursively(gltf, childNode, globalTransform); - } - } - - subStepSimulation(state, deltaTime) { - for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { - const node = state.gltf.nodes[nodeIndex]; - if (node.dirtyTransform) { - // Node transform is currently animated - continue; - } - const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion && motion.isKinematic) { - const linearVelocity = motion.computedLinearVelocity ?? motion.linearVelocity; - const angularVelocity = motion.computedAngularVelocity ?? motion.angularVelocity; - if (linearVelocity !== undefined || angularVelocity !== undefined) { - const worldTransform = node.physicsTransform ?? node.worldTransform; - const targetPosition = vec3.create(); - targetPosition[0] = worldTransform[12]; - targetPosition[1] = worldTransform[13]; - targetPosition[2] = worldTransform[14]; - let nodeRotation = quat.create(); - if (node.physicsTransform !== undefined) { - mat4.getRotation(nodeRotation, worldTransform); - } else { - nodeRotation = quat.clone(node.worldQuaternion); - } - if (linearVelocity !== undefined) { - const acceleration = vec3.create(); - vec3.scale(acceleration, linearVelocity, deltaTime); - vec3.transformQuat(acceleration, acceleration, nodeRotation); - targetPosition[0] += acceleration[0]; - targetPosition[1] += acceleration[1]; - targetPosition[2] += acceleration[2]; - } - if (angularVelocity !== undefined) { - // Transform angular velocity from local space to world space - // by rotating the velocity axes by the current node rotation. - const localX = vec3.fromValues(1, 0, 0); - const localY = vec3.fromValues(0, 1, 0); - const localZ = vec3.fromValues(0, 0, 1); - vec3.transformQuat(localX, localX, nodeRotation); - vec3.transformQuat(localY, localY, nodeRotation); - vec3.transformQuat(localZ, localZ, nodeRotation); - - const angularAcceleration = quat.create(); - const qX = quat.create(); - const qY = quat.create(); - const qZ = quat.create(); - quat.setAxisAngle(qX, localX, angularVelocity[0] * deltaTime); - quat.setAxisAngle(qY, localY, angularVelocity[1] * deltaTime); - quat.setAxisAngle(qZ, localZ, angularVelocity[2] * deltaTime); - quat.multiply(angularAcceleration, qX, angularAcceleration); - quat.multiply(angularAcceleration, qY, angularAcceleration); - quat.multiply(angularAcceleration, qZ, angularAcceleration); - - quat.multiply(nodeRotation, angularAcceleration, nodeRotation); - } - const pos = new this.PhysX.PxVec3(...targetPosition); - const rot = new this.PhysX.PxQuat(...nodeRotation); - const transform = new this.PhysX.PxTransform(pos, rot); - - actor.setKinematicTarget(transform); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(transform); - - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, nodeRotation, targetPosition); - - const scaledPhysicsTransform = mat4.create(); - mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); - - node.physicsTransform = physicsTransform; - node.scaledPhysicsTransform = scaledPhysicsTransform; - } - } else if (motion && motion.gravityFactor !== 1.0) { - const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); - actor.addForce(force, this.PhysX.PxForceModeEnum.eACCELERATION); - this.PhysX.destroy(force); - } - } - - this.scene.simulate(deltaTime); - if (!this.scene.fetchResults(true)) { - console.warn("PhysX: fetchResults failed"); - } - } - - simulateStep(state, deltaTime) { - if (!this.scene) { - this.reset = false; - return; - } - if (this.reset === true) { - this._resetSimulation(); - this.reset = false; - return; - } - - this.changeDebugVisualization(); - - this.subStepSimulation(state, deltaTime); - - for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { - const node = state.gltf.nodes[nodeIndex]; - const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion && !motion.isKinematic && !node.dirtyTransform) { - const transform = actor.getGlobalPose(); - const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); - const rotation = quat.fromValues( - transform.q.x, - transform.q.y, - transform.q.z, - transform.q.w - ); - - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, rotation, position); - - node.physicsTransform = physicsTransform; - - const rotationBetween = quat.create(); - - let parentNode = node; - while (parentNode.parentNode !== undefined) { - parentNode = parentNode.parentNode; - } - - quat.invert(rotationBetween, node.worldQuaternion); - quat.multiply(rotationBetween, rotation, rotationBetween); - - const rotMat = mat3.create(); - mat3.fromQuat(rotMat, rotationBetween); - - const scaleRot = mat3.create(); - mat3.fromMat4(scaleRot, node.worldTransform); - - mat3.multiply(scaleRot, rotMat, scaleRot); - - const scaledPhysicsTransform = mat4.create(); - scaledPhysicsTransform[0] = scaleRot[0]; - scaledPhysicsTransform[1] = scaleRot[1]; - scaledPhysicsTransform[2] = scaleRot[2]; - scaledPhysicsTransform[4] = scaleRot[3]; - scaledPhysicsTransform[5] = scaleRot[4]; - scaledPhysicsTransform[6] = scaleRot[5]; - scaledPhysicsTransform[8] = scaleRot[6]; - scaledPhysicsTransform[9] = scaleRot[7]; - scaledPhysicsTransform[10] = scaleRot[8]; - scaledPhysicsTransform[12] = position[0]; - scaledPhysicsTransform[13] = position[1]; - scaledPhysicsTransform[14] = position[2]; - - node.scaledPhysicsTransform = scaledPhysicsTransform; - for (const childIndex of node.children) { - const childNode = state.gltf.nodes[childIndex]; - this.applyTransformRecursively( - state.gltf, - childNode, - node.scaledPhysicsTransform - ); - } - } - } - } - - resetSimulation() { - this.reset = true; - this.simulateStep({}, 0); - } - - _resetSimulation() { - const scenePointer = this.scene; - this.scene = undefined; - this.filterData = []; - for (const physXFilterData of this.physXFilterData) { - this.PhysX.destroy(physXFilterData); - } - this.physXFilterData = []; - - for (const material of this.physXMaterials) { - material.release(); - } - this.physXMaterials = []; - - for (const shape of this.simpleShapes) { - shape.destroy?.(); - } - this.simpleShapes = []; - - for (const convexMesh of this.convexMeshes) { - convexMesh.release(); - } - this.convexMeshes = []; - - for (const triangleMesh of this.triangleMeshes) { - triangleMesh.release(); - } - this.triangleMeshes = []; - - for (const joints of this.nodeToSimplifiedJoints.values()) { - for (const joint of joints) { - joint.release(); - } - } - this.nodeToSimplifiedJoints.clear(); - - for (const actor of this.nodeToActor.values()) { - actor.actor.release(); - } - - this.nodeToActor.clear(); - - if (scenePointer) { - scenePointer.release(); - } - - this.shapeToNode.clear(); - } - - getDebugLineData() { - if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { - return []; - } - const result = []; - const rb = this.scene.getRenderBuffer(); - for (let i = 0; i < rb.getNbLines(); i++) { - const line = this.PhysX.NativeArrayHelpers.prototype.getDebugLineAt(rb.getLines(), i); - - result.push(line.pos0.x); - result.push(line.pos0.y); - result.push(line.pos0.z); - result.push(line.pos1.x); - result.push(line.pos1.y); - result.push(line.pos1.z); - } - return result; - } - - applyImpulse(nodeIndex, linearImpulse, angularImpulse) { - if (!this.scene) { - return; - } - const motionNode = this.nodeToMotion.get(nodeIndex); - if (!motionNode) { - return; - } - const actorEntry = this.nodeToActor.get(nodeIndex); - if (!actorEntry) { - return; - } - const actor = actorEntry.actor; - - const linImpulse = new this.PhysX.PxVec3(...linearImpulse); - const angImpulse = new this.PhysX.PxVec3(...angularImpulse); - actor.addForce(linImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); - actor.addTorque(angImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); - this.PhysX.destroy(linImpulse); - this.PhysX.destroy(angImpulse); - } - - applyPointImpulse(nodeIndex, impulse, position) { - if (!this.scene) { - return; - } - const motionNode = this.nodeToMotion.get(nodeIndex); - if (!motionNode) { - return; - } - const actorEntry = this.nodeToActor.get(nodeIndex); - if (!actorEntry) { - return; - } - const actor = actorEntry.actor; - - const pxImpulse = new this.PhysX.PxVec3(...impulse); - const pxPosition = new this.PhysX.PxVec3(...position); - this.PhysX.PxRigidBodyExt.prototype.addForceAtPos( - actor, - pxImpulse, - pxPosition, - this.PhysX.PxForceModeEnum.eIMPULSE - ); - this.PhysX.destroy(pxImpulse); - this.PhysX.destroy(pxPosition); - } - - rayCast(rayStart, rayEnd) { - const result = {}; - result.hitNodeIndex = -1; - if (!this.scene) { - return result; - } - const origin = new this.PhysX.PxVec3(...rayStart); - const directionVec = vec3.create(); - vec3.subtract(directionVec, rayEnd, rayStart); - vec3.normalize(directionVec, directionVec); - const direction = new this.PhysX.PxVec3(...directionVec); - const maxDistance = vec3.distance(rayStart, rayEnd); - - const hitBuffer = new this.PhysX.PxRaycastBuffer10(); - const hitFlags = new this.PhysX.PxHitFlags(this.PhysX.PxHitFlagEnum.eDEFAULT); - - const queryFilterData = new this.PhysX.PxQueryFilterData(); - queryFilterData.set_flags( - this.PhysX.PxQueryFlagEnum.eSTATIC | this.PhysX.PxQueryFlagEnum.eDYNAMIC - ); - - const hasHit = this.scene.raycast( - origin, - direction, - maxDistance, - hitBuffer, - hitFlags, - queryFilterData - ); - - this.PhysX.destroy(origin); - this.PhysX.destroy(direction); - this.PhysX.destroy(hitFlags); - this.PhysX.destroy(queryFilterData); - - if (hasHit) { - const hitCount = hitBuffer.getNbAnyHits(); - if (hitCount > 1) { - console.warn("Raycast hit multiple objects, only the first hit is returned."); - } - const hit = hitBuffer.getAnyHit(0); - const fraction = hit.distance / maxDistance; - const hitNormal = vec3.fromValues(hit.normal.x, hit.normal.y, hit.normal.z); - const hitNodeIndex = this.shapeToNode.get(hit.shape.ptr); - if (hitNodeIndex === undefined) { - return result; - } - return { - hitNodeIndex: hitNodeIndex, - hitFraction: fraction, - hitNormal: hitNormal - }; - } else { - return result; - } - } -} - export { PhysicsController }; diff --git a/source/PhysicsEngines/PhysX.js b/source/PhysicsEngines/PhysX.js new file mode 100644 index 00000000..6ae06875 --- /dev/null +++ b/source/PhysicsEngines/PhysX.js @@ -0,0 +1,2084 @@ +import PhysX from "physx-js-webidl"; +// The import is needed for rollup to include the wasm file in the build output +// eslint-disable-next-line no-unused-vars +import PhysXBinaryFile from "physx-js-webidl/physx-js-webidl.wasm"; +import { gltfPhysicsMaterial } from "../gltf/rigid_bodies"; +import { + createBoxVertexData, + createCapsuleVertexData, + createCylinderVertexData +} from "../geometry_generator"; +import { vec3, mat4, quat, mat3 } from "gl-matrix"; +import { PhysicsInterface } from "./PhysicsInterface"; +import { PhysicsUtils } from "../gltf/physics_utils"; + +class NvidiaPhysicsInterface extends PhysicsInterface { + constructor() { + super(); + this.PhysX = undefined; + this.physics = undefined; + this.defaultMaterial = undefined; + this.tolerances = undefined; + + this.reset = false; + + // Needs to be reset for each scene + this.scene = undefined; + this.nodeToActor = new Map(); + this.nodeToMotion = new Map(); + this.nodeToSimplifiedJoints = new Map(); + this.shapeToNode = new Map(); + this.filterData = []; + this.physXFilterData = []; + this.physXMaterials = []; + + // Need for memory management + this.convexMeshes = []; + this.triangleMeshes = []; + + // Debug + this.debugColliders = false; + this.debugJoints = false; + this.debugStateChanged = true; + + this.MAX_FLOAT = 3.4028234663852885981170418348452e38; + } + + async initializeEngine() { + this.PhysX = await PhysX({ locateFile: () => "./libs/physx-js-webidl.wasm" }); + const version = this.PhysX.PHYSICS_VERSION; + console.log( + "PhysX loaded! Version: " + + ((version >> 24) & 0xff) + + "." + + ((version >> 16) & 0xff) + + "." + + ((version >> 8) & 0xff) + ); + + const allocator = new this.PhysX.PxDefaultAllocator(); + const errorCb = new this.PhysX.PxDefaultErrorCallback(); + const foundation = this.PhysX.CreateFoundation(version, allocator, errorCb); + console.log("Created PxFoundation"); + + this.tolerances = new this.PhysX.PxTolerancesScale(); + this.tolerances.speed = 9.81; + this.physics = this.PhysX.CreatePhysics(version, foundation, this.tolerances); + this.defaultMaterial = this.createPhysXMaterial(new gltfPhysicsMaterial()); + console.log("Created PxPhysics"); + return this.PhysX; + } + + updatePhysicMaterials(gltf) { + const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials === undefined) { + return; + } + for (let i = 0; i < materials.length; i++) { + const material = materials[i]; + if (material.isDirty()) { + const physXMaterial = this.physXMaterials[i]; + physXMaterial.setStaticFriction(material.staticFriction); + physXMaterial.setDynamicFriction(material.dynamicFriction); + physXMaterial.setRestitution(material.restitution); + } + } + } + + updateActorTransform(node) { + if (node.dirtyTransform) { + const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; + if (actor === undefined) { + return; + } + const pxPos = new this.PhysX.PxVec3( + node.worldTransform[12], + node.worldTransform[13], + node.worldTransform[14] + ); + const pxRot = new this.PhysX.PxQuat(...node.worldQuaternion); + const pxTransform = new this.PhysX.PxTransform(pxPos, pxRot); + if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { + actor.setKinematicTarget(pxTransform); + } else { + actor.setGlobalPose(pxTransform); + } + this.PhysX.destroy(pxPos); + this.PhysX.destroy(pxRot); + this.PhysX.destroy(pxTransform); + } + } + + calculateMassAndInertia(motion, actor) { + const pos = new this.PhysX.PxVec3(0, 0, 0); + if (motion.centerOfMass !== undefined) { + pos.x = motion.centerOfMass[0]; + pos.y = motion.centerOfMass[1]; + pos.z = motion.centerOfMass[2]; + } + const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); + if (motion.inertiaDiagonal !== undefined) { + let inertia = undefined; + if ( + motion.inertiaOrientation !== undefined && + !quat.exactEquals(motion.inertiaOrientation, quat.create()) + ) { + const intertiaRotMat = mat3.create(); + + const inertiaDiagonalMat = mat3.create(); + inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; + inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; + inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; + + if ( + quat.length(motion.inertiaOrientation) > 1.0e-5 || + quat.length(motion.inertiaOrientation) < 1.0e-5 + ) { + mat3.identity(intertiaRotMat); + console.warn( + "PhysX: Invalid inertia orientation quaternion, ignoring rotation" + ); + } else { + mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); + } + + const inertiaTensor = mat3.create(); + mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); + + const col0 = new this.PhysX.PxVec3( + inertiaTensor[0], + inertiaTensor[1], + inertiaTensor[2] + ); + const col1 = new this.PhysX.PxVec3( + inertiaTensor[3], + inertiaTensor[4], + inertiaTensor[5] + ); + const col2 = new this.PhysX.PxVec3( + inertiaTensor[6], + inertiaTensor[7], + inertiaTensor[8] + ); + const pxInertiaTensor = new this.PhysX.PxMat33(col0, col1, col2); + inertia = this.PhysX.PxMassProperties.prototype.getMassSpaceInertia( + pxInertiaTensor, + rot + ); + this.PhysX.destroy(col0); + this.PhysX.destroy(col1); + this.PhysX.destroy(col2); + this.PhysX.destroy(pxInertiaTensor); + actor.setMassSpaceInertiaTensor(inertia); + } else { + inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } + } else { + if (motion.mass === undefined) { + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); + } else { + this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( + actor, + motion.mass, + pos + ); + } + } + + const pose = new this.PhysX.PxTransform(pos, rot); + actor.setCMassLocalPose(pose); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(pose); + } + + updateMotion(actorNode) { + const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; + const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; + if (motion.animatedPropertyObjects.isKinematic.dirty) { + if (motion.isKinematic) { + const linearVelocity = actor.getLinearVelocity(); + motion.computedLinearVelocity = [ + linearVelocity.x, + linearVelocity.y, + linearVelocity.z + ]; + const angularVelocity = actor.getAngularVelocity(); + motion.computedAngularVelocity = [ + angularVelocity.x, + angularVelocity.y, + angularVelocity.z + ]; + } else { + motion.computedLinearVelocity = undefined; + motion.computedAngularVelocity = undefined; + } + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, !motion.isKinematic); + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); + } + if (motion.animatedPropertyObjects.mass.dirty) { + actor.setMass(motion.mass); + } + if ( + motion.animatedPropertyObjects.centerOfMass.dirty || + motion.animatedPropertyObjects.inertiaOrientation.dirty || + motion.animatedPropertyObjects.inertiaDiagonal.dirty + ) { + this.calculateMassAndInertia(motion, actor); + } + if (motion.animatedPropertyObjects.gravityFactor.dirty) { + actor.setActorFlag( + this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, + motion.gravityFactor !== 1.0 + ); + } + if (motion.animatedPropertyObjects.linearVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); + actor.setLinearVelocity(pxVelocity); + motion.computedLinearVelocity = undefined; + } + if (motion.animatedPropertyObjects.angularVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); + actor.setAngularVelocity(pxVelocity); + motion.computedAngularVelocity = undefined; + } + } + + updateCollider( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger + ) { + const result = this.nodeToActor.get(actorNode.gltfObjectIndex); + const actor = result?.actor; + const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); + + let currentGeometry = currentShape.getGeometry(); + const currentColliderType = currentGeometry.getType(); + const shapeIndex = collider?.geometry?.shape; + let scale = vec3.fromValues(1, 1, 1); + let scaleAxis = quat.create(); + if (shapeIndex !== undefined) { + // Simple shapes need to be recreated if scale changed + // If properties changed we also need to recreate the mesh colliders + const dirty = gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex].isDirty(); + if ( + scaleChanged && + !dirty && + currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH + ) { + // Update convex mesh scale + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + const result = PhysicsUtils.calculateScaleAndAxis(node); + scale = result.scale; + scaleAxis = result.scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); + } else if (dirty || scaleChanged) { + // Recreate simple shape collider + const newGeometry = this.generateSimpleShape( + gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], + scale, + scaleAxis + ); + currentGeometry.release?.(); + if (newGeometry.getType() !== currentColliderType) { + // We need to recreate the shape + let shapeFlags = undefined; + if (isTrigger) { + shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; + } else { + shapeFlags = this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE; + } + if (this.debugColliders) { + shapeFlags |= this.PhysX.PxShapeFlagEnum.eVISUALIZATION; + } + const shape = this.createShapeFromGeometry( + newGeometry, + undefined, + undefined, + shapeFlags, + collider + ); + result?.pxShapeMap.set(node.gltfObjectIndex, shape); + actor.detachShape(currentShape); + actor.attachShape(shape); + this.PhysX.destroy(currentShape); + } else { + currentShape.setGeometry(newGeometry); + } + } + } else if (collider?.geometry?.mesh !== undefined) { + if (scaleChanged) { + if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxTriangleMeshGeometry + ); + } + // apply scale + const result = PhysicsUtils.calculateScaleAndAxis(node); + scale = result.scale; + scaleAxis = result.scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); + } + } + if (offsetChanged) { + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, worldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); + + const PxPos = new this.PhysX.PxVec3(...translation); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + currentShape.setLocalPose(pose); + } + } + + updatePhysicsJoint(state, jointNode) { + const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); + if (pxJoints === undefined) { + return; + } + const gltfJoint = + state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[ + jointNode.extensions.KHR_physics_rigid_bodies.joint.joint + ]; + const simplifiedJoints = gltfJoint.simplifiedPhysicsJoints; + if (simplifiedJoints.length !== pxJoints.length) { + console.warn( + "Number of simplified joints does not match number of PhysX joints. Skipping joint update." + ); + return; + } + for (let i = 0; i < simplifiedJoints.length; i++) { + const pxJoint = pxJoints[i]; + const simplifiedJoint = simplifiedJoints[i]; + if ( + jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects + .enableCollision.dirty + ) { + pxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision + ); + } + for (const limit of simplifiedJoint.limits) { + if ( + limit.animatedPropertyObjects.min.dirty || + limit.animatedPropertyObjects.max.dirty || + limit.animatedPropertyObjects.stiffness.dirty || + limit.animatedPropertyObjects.damping.dirty + ) { + this._setLimitValues(pxJoint, simplifiedJoint, limit); + } + } + + if ( + simplifiedJoint.twistLimit && + (simplifiedJoint.twistLimit.animatedPropertyObjects.min.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.max.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.damping.dirty) + ) { + this._setTwistLimitValues(pxJoint, simplifiedJoint); + } + + if ( + (simplifiedJoint.swingLimit1 && + (simplifiedJoint.swingLimit1.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.damping.dirty)) || + (simplifiedJoint.swingLimit2 && + (simplifiedJoint.swingLimit2.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.damping.dirty)) + ) { + this._setSwingLimitValues(pxJoint, simplifiedJoint); + } + + let positionTargetDirty = false; + let velocityTargetDirty = false; + const linearVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + const angularVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + pxJoint.getDriveVelocity(linearVelocityTarget, angularVelocityTarget); + for (const drive of simplifiedJoint.drives) { + if ( + drive.animatedPropertyObjects.stiffness.dirty || + drive.animatedPropertyObjects.damping.dirty || + drive.animatedPropertyObjects.maxForce.dirty + ) { + this._setDriveValues(pxJoint, simplifiedJoint, drive); + } + if (drive.animatedPropertyObjects.velocityTarget.dirty) { + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); + velocityTargetDirty = true; + } + if (drive.animatedPropertyObjects.positionTarget.dirty) { + positionTargetDirty = true; + } + } + + if (positionTargetDirty) { + this._setDrivePositionTarget(pxJoint, simplifiedJoint); + } + if (velocityTargetDirty) { + pxJoint.setDriveVelocity(linearVelocityTarget, angularVelocityTarget); + } + } + } + + mapCombineMode(mode) { + switch (mode) { + case "average": + return this.PhysX.PxCombineModeEnum.eAVERAGE; + case "minimum": + return this.PhysX.PxCombineModeEnum.eMIN; + case "maximum": + return this.PhysX.PxCombineModeEnum.eMAX; + case "multiply": + return this.PhysX.PxCombineModeEnum.eMULTIPLY; + } + } + + // Either create a box or update an existing one. Returns only newly created geometry + generateBox(x, y, z, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); + } + if ( + scale.every((value) => value === scale[0]) === false && + quat.equals(scaleAxis, quat.create()) === false + ) { + const data = createBoxVertexData(x, y, z); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + } + let geometry = undefined; + if (referenceType === this.PhysX.PxGeometryTypeEnum.eBOX) { + const halfExtents = new this.PhysX.PxVec3( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] + ); + reference.halfExtents = halfExtents; + this.PhysX.destroy(halfExtents); + } else { + geometry = new this.PhysX.PxBoxGeometry( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] + ); + } + + return geometry; + } + + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { + const data = createCapsuleVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + } + + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { + if ( + (quat.equals(scaleAxis, quat.create()) === false && + scale.every((value) => value === scale[0]) === false) || + radiusTop !== radiusBottom || + scale[0] !== scale[2] + ) { + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + } + height *= scale[1]; + radiusTop *= scale[0]; + radiusBottom *= scale[0]; + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices); + } + + generateSphere(radius, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); + } + if (scale.every((value) => value === scale[0]) === false) { + const data = createCapsuleVertexData(radius, radius, 0); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + } else { + radius *= scale[0]; + } + if (referenceType === this.PhysX.PxGeometryTypeEnum.eSPHERE) { + reference.radius = radius; + return undefined; + } + const geometry = new this.PhysX.PxSphereGeometry(radius); + return geometry; + } + + generatePlane(width, height, doubleSided, scale, scaleAxis, reference) { + if (reference !== undefined) { + //TODO handle update + return undefined; + } + const geometry = new this.PhysX.PxPlaneGeometry(); + return geometry; + } + + createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxConvexMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); + + let flag = 0; + flag |= this.PhysX.PxConvexFlagEnum.eCOMPUTE_CONVEX; + flag |= this.PhysX.PxConvexFlagEnum.eSHIFT_VERTICES; + //flag |= this.PhysX.PxConvexFlagEnum.eDISABLE_MESH_VALIDATION; + const pxflags = new this.PhysX.PxConvexFlags(flag); + des.flags = pxflags; + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + cookingParams.planeTolerance = 0.0007; //Default + const tri = this.PhysX.CreateConvexMesh(cookingParams, des); + this.convexMeshes.push(tri); + + const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); + const PxQuat = new this.PhysX.PxQuat(...scaleAxis); + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxConvexMeshGeometryFlags(); + const geometry = new this.PhysX.PxConvexMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(pxflags); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } + + collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { + let positionDataArray = []; + let positionCount = 0; + let indexDataArray = []; + let indexCount = 0; + + for (const primitive of mesh.primitives) { + const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; + const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + + if (primitive.targets !== undefined) { + let morphWeights = mesh.weights; + if (morphWeights !== undefined) { + // Calculate morphed vertex positions on CPU + const morphPositionData = []; + for (const target of primitive.targets) { + if (target.POSITION !== undefined) { + const morphAccessor = gltf.accessors[target.POSITION]; + morphPositionData.push( + morphAccessor.getNormalizedDeinterlacedView(gltf) + ); + } else { + morphPositionData.push(undefined); + } + } + for (let i = 0; i < positionData.length / 3; i++) { + for (let j = 0; j < morphWeights.length; j++) { + const morphData = morphPositionData[j]; + if (morphWeights[j] === 0 || morphData === undefined) { + continue; + } + positionData[i * 3] += morphData[i * 3] * morphWeights[j]; + positionData[i * 3 + 1] += morphData[i * 3 + 1] * morphWeights[j]; + positionData[i * 3 + 2] += morphData[i * 3 + 2] * morphWeights[j]; + } + } + } + } + + positionDataArray.push(positionData); + positionCount += positionAccessor.count; + if (computeIndices) { + let indexData = undefined; + if (primitive.indices !== undefined) { + const indexAccessor = gltf.accessors[primitive.indices]; + indexData = indexAccessor.getNormalizedDeinterlacedView(gltf); + } else { + const array = Array.from(Array(positionAccessor.count).keys()); + indexData = new Uint32Array(array); + } + if (primitive.mode === 5) { + indexData = PhysicsUtils.convertTriangleStripToTriangles(indexData); + } else if (primitive.mode === 6) { + indexData = PhysicsUtils.convertTriangleFanToTriangles(indexData); + } else if (primitive.mode !== undefined && primitive.mode !== 4) { + console.warn( + "Unsupported primitive mode for physics mesh collider creation: " + + primitive.mode + ); + } + indexDataArray.push(indexData); + indexCount += indexData.length; + } + } + + const positionData = new Float32Array(positionCount * 3); + const indexData = new Uint32Array(indexCount); + let offset = 0; + for (const positionChunk of positionDataArray) { + positionData.set(positionChunk, offset); + offset += positionChunk.length; + } + offset = 0; + for (const indexChunk of indexDataArray) { + indexData.set(indexChunk, offset); + offset += indexChunk.length; + } + return { vertices: positionData, indices: indexData }; + } + + createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); + return this.createConvexPxMesh(result.vertices, scale, scaleAxis); + } + + createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxTriangleMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); + + des.triangles.stride = indices.BYTES_PER_ELEMENT * 3; + des.triangles.count = indices.length / 3; + des.triangles.data = malloc(indices); + + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); + this.triangleMeshes.push(tri); + + const PxScale = new this.PhysX.PxVec3(1, 1, 1); + const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); + + if (scale !== undefined) { + PxScale.x = scale[0]; + PxScale.y = scale[1]; + PxScale.z = scale[2]; + } + if (scaleAxis !== undefined) { + PxQuat.x = scaleAxis[0]; + PxQuat.y = scaleAxis[1]; + PxQuat.z = scaleAxis[2]; + PxQuat.w = scaleAxis[3]; + } + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxMeshGeometryFlags(); + const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } + + collidesWith(filterA, filterB) { + if (filterB.collideWithSystems.length > 0) { + for (const system of filterB.collideWithSystems) { + if (filterA.collisionSystems.includes(system)) { + return true; + } + } + return false; + } else if (filterB.notCollideWithSystems.length > 0) { + for (const system of filterB.notCollideWithSystems) { + if (filterA.collisionSystems.includes(system)) { + return false; + } + } + return true; + } + return true; + } + + computeFilterData(gltf) { + // Default filter is sign bit + const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; + this.filterData = new Array(32).fill(0); + this.filterData[31] = Math.pow(2, 32) - 1; // Default filter with all bits set + let filterCount = filters?.length ?? 0; + if (filterCount > 31) { + filterCount = 31; + console.warn( + "PhysX supports a maximum of 31 collision filters. Additional filters will be ignored." + ); + } + + for (let i = 0; i < filterCount; i++) { + let bitMask = 0; + for (let j = 0; j < filterCount; j++) { + if ( + this.collidesWith(filters[i], filters[j]) && + this.collidesWith(filters[j], filters[i]) + ) { + bitMask |= 1 << j; + } + } + this.filterData[i] = bitMask; + } + } + + createPhysXMaterial(gltfPhysicsMaterial) { + if (gltfPhysicsMaterial === undefined) { + return this.defaultMaterial; + } + + const physxMaterial = this.physics.createMaterial( + gltfPhysicsMaterial.staticFriction, + gltfPhysicsMaterial.dynamicFriction, + gltfPhysicsMaterial.restitution + ); + if (gltfPhysicsMaterial.frictionCombine !== undefined) { + physxMaterial.setFrictionCombineMode( + this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) + ); + } + if (gltfPhysicsMaterial.restitutionCombine !== undefined) { + physxMaterial.setRestitutionCombineMode( + this.mapCombineMode(gltfPhysicsMaterial.restitutionCombine) + ); + } + return physxMaterial; + } + + createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { + let word0 = null; + let word1 = null; + if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { + word0 = 1 << collisionFilter; + word1 = this.filterData[collisionFilter]; + } else { + // Default filter id is signed bit and all bits set to collide with everything + word0 = Math.pow(2, 31); + word1 = Math.pow(2, 32) - 1; + } + + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_DISCRETE_CONTACT; + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_CCD_CONTACT; + + return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); + } + + createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { + if (physXMaterial === undefined) { + if (glTFCollider?.physicsMaterial !== undefined) { + physXMaterial = this.physXMaterials[glTFCollider.physicsMaterial]; + } else { + physXMaterial = this.defaultMaterial; + } + } + const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); + + if (physXFilterData === undefined) { + physXFilterData = + this.physXFilterData[ + glTFCollider?.collisionFilter ?? this.physXFilterData.length - 1 + ]; + } + + shape.setSimulationFilterData(physXFilterData); + + return shape; + } + + createShape( + gltf, + node, + collider, + shapeFlags, + physXMaterial, + physXFilterData, + convexHull, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + let geometry = undefined; + if (collider?.geometry?.shape !== undefined) { + if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { + const simpleShape = + gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; + geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); + } else { + geometry = this.simpleShapes[collider.geometry.shape]; + } + } else if (collider?.geometry?.mesh !== undefined) { + const mesh = gltf.meshes[collider.geometry.mesh]; + if (convexHull === true) { + geometry = this.createConvexMesh(gltf, mesh, scale, scaleAxis); + } else { + geometry = this.createPxMesh(gltf, mesh, scale, scaleAxis); + } + } + + if (geometry === undefined) { + return undefined; + } + + const shape = this.createShapeFromGeometry( + geometry, + physXMaterial, + physXFilterData, + shapeFlags, + collider + ); + + this.shapeToNode.set(shape.ptr, node.gltfObjectIndex); + return shape; + } + + createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { + const worldTransform = node.worldTransform; + const translation = vec3.create(); + mat4.getTranslation(translation, worldTransform); + const pos = new this.PhysX.PxVec3(...translation); + const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); + const pose = new this.PhysX.PxTransform(pos, rotation); + let actor = null; + const pxShapeMap = new Map(); + if (type === "static" || type === "trigger") { + actor = this.physics.createRigidStatic(pose); + } else { + actor = this.physics.createRigidDynamic(pose); + if (type === "kinematic") { + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); + } + actor.setRigidBodyFlag( + this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, + type !== "kinematic" + ); + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion) { + const gltfAngularVelocity = motion?.angularVelocity; + const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); + actor.setAngularVelocity(angularVelocity, true); + this.PhysX.destroy(angularVelocity); + + const gltfLinearVelocity = motion?.linearVelocity; + const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); + actor.setLinearVelocity(linearVelocity, true); + this.PhysX.destroy(linearVelocity); + + if (motion.mass !== undefined) { + actor.setMass(motion.mass); + } + + this.calculateMassAndInertia(motion, actor); + + if (motion.gravityFactor !== 1.0) { + actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); + } + } + } + + const createAndAddShape = ( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger, + noMeshShapes, + shapeFlags, + triggerFlags + ) => { + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, worldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); + + // Calculate scale and scaleAxis + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); + + const materialIndex = collider?.physicsMaterial; + const material = + materialIndex !== undefined + ? this.physXMaterials[materialIndex] + : this.defaultMaterial; + + const physXFilterData = + collider?.collisionFilter !== undefined + ? this.physXFilterData[collider.collisionFilter] + : this.physXFilterData[this.physXFilterData.length - 1]; + + const shape = this.createShape( + gltf, + node, + collider, + isTrigger ? triggerFlags : shapeFlags, + material, + physXFilterData, + noMeshShapes || collider?.geometry?.convexHull === true, + scale, + scaleAxis + ); + + if (shape !== undefined) { + const PxPos = new this.PhysX.PxVec3(...translation); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + shape.setLocalPose(pose); + + actor.attachShape(shape); + pxShapeMap.set(node.gltfObjectIndex, shape); + this.PhysX.destroy(PxPos); + this.PhysX.destroy(PxRotation); + this.PhysX.destroy(pose); + } + }; + + // If a node contains trigger and collider combine them + + let collider = undefined; + if (type !== "trigger") { + collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + false, + noMeshShapes, + shapeFlags, + triggerFlags + ); + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + if (collider !== undefined) { + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + shapeFlags, + triggerFlags + ); + } + } else { + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + shapeFlags, + triggerFlags + ); + } + + if (type !== "trigger") { + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + PhysicsUtils.recurseCollider( + gltf, + childNode, + undefined, + node, + node.dirtyScale, + node.dirtyScale, + createAndAddShape, + [noMeshShapes, shapeFlags, triggerFlags] + ); + } + } + + this.PhysX.destroy(pos); + this.PhysX.destroy(rotation); + this.PhysX.destroy(pose); + + this.scene.addActor(actor); + + this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); + } + + computeJointOffsetAndActor(node, referencedJoint) { + let currentNode = node; + while (currentNode !== undefined) { + if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { + break; + } + currentNode = currentNode.parentNode; + } + + const nodeWorldRot = node.worldQuaternion; + const localPhysXRot = referencedJoint?.localRotation; + if (localPhysXRot !== undefined) { + quat.multiply(nodeWorldRot, node.worldQuaternion, localPhysXRot); + } + + if (currentNode === undefined) { + const pos = vec3.create(); + mat4.getTranslation(pos, node.worldTransform); + + return { actor: undefined, offsetPosition: pos, offsetRotation: nodeWorldRot }; + } + const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; + const inverseActorRotation = quat.create(); + quat.invert(inverseActorRotation, currentNode.worldQuaternion); + const offsetRotation = quat.create(); + quat.multiply(offsetRotation, inverseActorRotation, nodeWorldRot); + + const actorPosition = vec3.create(); + mat4.getTranslation(actorPosition, currentNode.worldTransform); + const nodePosition = vec3.create(); + mat4.getTranslation(nodePosition, node.worldTransform); + const offsetPosition = vec3.create(); + vec3.subtract(offsetPosition, nodePosition, actorPosition); + vec3.transformQuat(offsetPosition, offsetPosition, inverseActorRotation); + + return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; + } + + convertAxisIndexToEnum(axisIndex, type) { + if (type === "linear") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eX; + case 1: + return this.PhysX.PxD6AxisEnum.eY; + case 2: + return this.PhysX.PxD6AxisEnum.eZ; + } + } else if (type === "angular") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eTWIST; + case 1: + return this.PhysX.PxD6AxisEnum.eSWING1; + case 2: + return this.PhysX.PxD6AxisEnum.eSWING2; + } + } + return null; + } + + convertAxisIndexToAngularDriveEnum(axisIndex) { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6DriveEnum.eTWIST; + case 1: + return 6; // Currently not exposed via bindings + case 2: + return 7; // Currently not exposed via bindings + } + return null; + } + + validateSwingLimits(joint) { + // Check if swing limits are symmetric (cone) or asymmetric (pyramid) + if (joint.swingLimit1 && joint.swingLimit2) { + const limit1 = joint.swingLimit1; + const limit2 = joint.swingLimit2; + + const isSymmetric1 = + Math.abs(limit1.min + limit1.max) < 1e-6 || limit1.min === undefined; // Centered around 0 + const isSymmetric2 = + Math.abs(limit2.min + limit2.max) < 1e-6 || limit2.min === undefined; + + // Return if this is a cone limit (symmetric and same range) vs pyramid limit + return isSymmetric1 && isSymmetric2; + } + return false; + } + + createJoint(gltf, node) { + const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; + const referencedJoint = + gltf.extensions?.KHR_physics_rigid_bodies?.physicsJoints[joint.joint]; + + if (referencedJoint === undefined) { + console.error("Referenced joint not found:", joint.joint); + return; + } + const simplifiedJoints = []; + for (const simplifiedJoint of referencedJoint.simplifiedPhysicsJoints) { + const physxJoint = this.createSimplifiedJoint(gltf, node, joint, simplifiedJoint); + simplifiedJoints.push(physxJoint); + } + this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); + } + + _setLimitValues(physxJoint, simplifiedJoint, limit) { + const lock = limit.min === 0 && limit.max === 0; + const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); + const isDistanceLimit = + limit.linearAxes && + limit.linearAxes.length === 3 && + (limit.min === undefined || limit.min === 0) && + limit.max !== 0; + if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { + const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( + limit.min ?? -this.MAX_FLOAT, + limit.max ?? this.MAX_FLOAT, + spring + ); + for (const axis of limit.linearAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setMotion( + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED + ); + if (!lock) { + physxJoint.setLinearLimit(physxAxis, linearLimitPair); + } + } + this.PhysX.destroy(linearLimitPair); + } + if (isDistanceLimit) { + const linearLimit = new this.PhysX.PxJointLinearLimit( + limit.max ?? this.MAX_FLOAT, + spring + ); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setDistanceLimit(linearLimit); + this.PhysX.destroy(linearLimit); + } + if (limit.angularAxes && limit.angularAxes.length > 0) { + for (const axis of limit.angularAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); + physxJoint.setMotion( + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED + ); + } + } + this.PhysX.destroy(spring); + } + + _setTwistLimitValues(physxJoint, simplifiedJoint) { + if (simplifiedJoint.twistLimit !== undefined) { + if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { + const limitPair = new this.PhysX.PxJointAngularLimitPair( + simplifiedJoint.twistLimit.min ?? -Math.PI, + simplifiedJoint.twistLimit.max ?? Math.PI, + new this.PhysX.PxSpring( + simplifiedJoint.twistLimit.stiffness ?? 0, + simplifiedJoint.twistLimit.damping + ) + ); + physxJoint.setTwistLimit(limitPair); + this.PhysX.destroy(limitPair); + } + } + } + + _setSwingLimitValues(physxJoint, simplifiedJoint) { + if ( + simplifiedJoint.swingLimit1 !== undefined && + simplifiedJoint.swingLimit2 !== undefined + ) { + if ( + simplifiedJoint.swingLimit1.stiffness !== simplifiedJoint.swingLimit2.stiffness || + simplifiedJoint.swingLimit1.damping !== simplifiedJoint.swingLimit2.damping + ) { + console.warn( + "PhysX does not support different stiffness/damping for swing limits." + ); + } else { + const spring = new this.PhysX.PxSpring( + simplifiedJoint.swingLimit1.stiffness ?? 0, + simplifiedJoint.swingLimit1.damping + ); + let yMin = -Math.PI / 2; + let yMax = Math.PI / 2; + let zMin = -Math.PI / 2; + let zMax = Math.PI / 2; + if (simplifiedJoint.swingLimit1.min !== undefined) { + yMin = simplifiedJoint.swingLimit1.min; + } + if (simplifiedJoint.swingLimit1.max !== undefined) { + yMax = simplifiedJoint.swingLimit1.max; + } + if (simplifiedJoint.swingLimit2.min !== undefined) { + zMin = simplifiedJoint.swingLimit2.min; + } + if (simplifiedJoint.swingLimit2.max !== undefined) { + zMax = simplifiedJoint.swingLimit2.max; + } + + const isSymmetric = this.validateSwingLimits(simplifiedJoint); + if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { + // Fixed limit is already set + } else if (isSymmetric) { + const swing1Angle = Math.max(Math.abs(yMin), Math.abs(yMax)); + const swing2Angle = Math.max(Math.abs(zMin), Math.abs(zMax)); + const jointLimitCone = new this.PhysX.PxJointLimitCone( + swing1Angle, + swing2Angle, + spring + ); + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); + } else { + const jointLimitCone = new this.PhysX.PxJointLimitPyramid( + yMin, + yMax, + zMin, + zMax, + spring + ); + physxJoint.setPyramidSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); + } + this.PhysX.destroy(spring); + } + } else if ( + simplifiedJoint.swingLimit1 !== undefined || + simplifiedJoint.swingLimit2 !== undefined + ) { + const singleLimit = simplifiedJoint.swingLimit1 ?? simplifiedJoint.swingLimit2; + if (singleLimit.min === 0 && singleLimit.max === 0) { + // Fixed limit is already set + } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { + console.warn( + "PhysX requires symmetric limits for swing limits in single axis mode." + ); + } else { + const spring = new this.PhysX.PxSpring( + singleLimit.stiffness ?? 0, + singleLimit.damping + ); + const maxY = simplifiedJoint.swingLimit1?.max ?? Math.PI; + const maxZ = simplifiedJoint.swingLimit2?.max ?? Math.PI; + const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(spring); + this.PhysX.destroy(jointLimitCone); + } + } + } + + _setDriveValues(physxJoint, simplifiedJoint, drive) { + const physxDrive = new this.PhysX.PxD6JointDrive( + drive.stiffness, + drive.damping, + drive.maxForce ?? this.MAX_FLOAT, + drive.mode === "acceleration" + ); + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + const axis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setDrive(axis, physxDrive); + } else if (drive.type === "angular") { + const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); + physxJoint.setDrive(axis, physxDrive); + } + this.PhysX.destroy(physxDrive); + } + + _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + if (drive.velocityTarget !== undefined) { + linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; + } + } else if (drive.type === "angular") { + if (drive.velocityTarget !== undefined) { + angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation + } + } + } + + _setDrivePositionTarget(physxJoint, simplifiedJoint) { + const positionTarget = vec3.fromValues(0, 0, 0); + const angleTarget = quat.create(); + for (const drive of simplifiedJoint.drives) { + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + if (drive.positionTarget !== undefined) { + positionTarget[result.axis] = drive.positionTarget * result.sign; + } + } else if (drive.type === "angular") { + if (drive.positionTarget !== undefined) { + // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise + switch (result.axis) { + case 0: { + quat.rotateX( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + case 1: { + quat.rotateY( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + case 2: { + quat.rotateZ( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + } + } + } + } + const posTarget = new this.PhysX.PxVec3(...positionTarget); + const rotTarget = new this.PhysX.PxQuat(...angleTarget); + const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); + physxJoint.setDrivePosition(targetTransform); + this.PhysX.destroy(posTarget); + this.PhysX.destroy(rotTarget); + this.PhysX.destroy(targetTransform); + } + + createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { + const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); + const resultB = this.computeJointOffsetAndActor( + gltf.nodes[joint.connectedNode], + simplifiedJoint + ); + + const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); + const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); + const poseA = new this.PhysX.PxTransform(pos, rot); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + + const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); + const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); + const poseB = new this.PhysX.PxTransform(posB, rotB); + this.PhysX.destroy(posB); + this.PhysX.destroy(rotB); + + const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( + this.physics, + resultA.actor, + poseA, + resultB.actor, + poseB + ); + this.PhysX.destroy(poseA); + this.PhysX.destroy(poseB); + + physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); + + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); + + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + joint.enableCollision + ); + + // Do not restict any axis by default + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); + + for (const limit of simplifiedJoint.limits) { + this._setLimitValues(physxJoint, simplifiedJoint, limit); + } + + this._setTwistLimitValues(physxJoint, simplifiedJoint); + this._setSwingLimitValues(physxJoint, simplifiedJoint); + + const linearVelocityTarget = vec3.fromValues(0, 0, 0); + const angularVelocityTarget = vec3.fromValues(0, 0, 0); + + for (const drive of simplifiedJoint.drives) { + this._setDriveValues(physxJoint, simplifiedJoint, drive); + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); + } + this._setDrivePositionTarget(physxJoint, simplifiedJoint); + + const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); + const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); + physxJoint.setDriveVelocity(linVel, angVel); + this.PhysX.destroy(linVel); + this.PhysX.destroy(angVel); + + return physxJoint; + } + + changeDebugVisualization() { + if (!this.scene || !this.debugStateChanged) { + return; + } + this.debugStateChanged = false; + this.scene.setVisualizationParameter( + this.PhysX.eSCALE, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eWORLD_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eACTOR_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eCOLLISION_SHAPES, + this.debugColliders ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eJOINT_LOCAL_FRAMES, + this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); + } + } + for (const shapePtr of this.shapeToNode.keys()) { + const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); + shape.setFlag(this.PhysX.PxShapeFlagEnum.eVISUALIZATION, this.debugColliders); + } + } + + initializeSimulation( + state, + staticActors, + kinematicActors, + dynamicActors, + jointNodes, + triggerNodes, + independentTriggerNodes, + nodeToMotion, + _hasRuntimeAnimationTargets, + _staticMeshColliderCount, + _dynamicMeshColliderCount + ) { + if (!this.PhysX) { + return; + } + this.nodeToMotion = nodeToMotion; + this.generateSimpleShapes(state.gltf); + this.computeFilterData(state.gltf); + for (let i = 0; i < this.filterData.length; i++) { + const physXFilterData = this.createPhysXCollisionFilter(i); + this.physXFilterData.push(physXFilterData); + } + + const materials = state.gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials !== undefined) { + for (const gltfMaterial of materials) { + const physxMaterial = this.createPhysXMaterial(gltfMaterial); + this.physXMaterials.push(physxMaterial); + } + } + + const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); + const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); + sceneDesc.set_gravity(tmpVec); + sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); + sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); + const sceneFlags = new this.PhysX.PxSceneFlags( + this.PhysX.PxSceneFlagEnum.eENABLE_CCD | this.PhysX.PxSceneFlagEnum.eENABLE_PCM + ); + sceneDesc.flags = sceneFlags; + + this.scene = this.physics.createScene(sceneDesc); + let triggerCallback = undefined; + + if (triggerNodes.length > 0) { + console.log("Enabling trigger report callback"); + triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); + triggerCallback.onTrigger = (pairs, count) => { + for (const compoundTrigger of state.physicsController.compoundTriggerNodes.values()) { + compoundTrigger.added.clear(); + compoundTrigger.removed.clear(); + } + console.log("Trigger callback called with", count, "pairs"); + for (let i = 0; i < count; i++) { + const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); + const triggerShape = pair.triggerShape; + const otherShape = pair.otherShape; + const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); + const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + state.graphController.rigidBodyTriggerEntered( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex) + ); + } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { + state.graphController.rigidBodyTriggerExited( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex) + ); + } + const compoundTriggers = + state.physicsController.triggerToCompound.get(triggerNodeIndex); + if (compoundTriggers !== undefined) { + for (const compoundTriggerIndex of compoundTriggers) { + const compoundTriggerInfo = + state.physicsController.compoundTriggerNodes.get( + compoundTriggerIndex + ); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + compoundTriggerInfo.added.add(otherNodeIndex); + } else if ( + pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST + ) { + compoundTriggerInfo.removed.add(otherNodeIndex); + } + } + } + } + + for (const [ + idx, + compoundTrigger + ] of state.physicsController.compoundTriggerNodes.entries()) { + for (const addedNodeIndex of compoundTrigger.added) { + if (!compoundTrigger.previous.has(addedNodeIndex)) { + compoundTrigger.previous.set(addedNodeIndex, 1); + state.graphController.rigidBodyTriggerEntered( + idx, + addedNodeIndex, + nodeToMotion.get(addedNodeIndex) + ); + } else { + const currentCount = compoundTrigger.previous.get(addedNodeIndex); + compoundTrigger.previous.set(addedNodeIndex, currentCount + 1); + } + } + for (const removedNodeIndex of compoundTrigger.removed) { + const currentCount = compoundTrigger.previous.get(removedNodeIndex); + if (currentCount > 1) { + compoundTrigger.previous.set(removedNodeIndex, currentCount - 1); + } else { + compoundTrigger.previous.delete(removedNodeIndex); + state.graphController.rigidBodyTriggerExited( + idx, + removedNodeIndex, + nodeToMotion.get(removedNodeIndex) + ); + } + } + } + }; + + // All callbacks need to be defined + triggerCallback.onConstraintBreak = (_constraints, _count) => {}; + triggerCallback.onWake = (_actors, _count) => {}; + triggerCallback.onSleep = (_actors, _count) => {}; + triggerCallback.onContact = (_pairHeaders, _pairs, _count) => {}; + sceneDesc.simulationEventCallback = triggerCallback; + } + + this.scene = this.physics.createScene(sceneDesc); + + console.log("Created scene"); + const shapeFlags = new this.PhysX.PxShapeFlags( + this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | + this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE + ); + + const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); + + for (const node of staticActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "static"); + } + for (const node of kinematicActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "kinematic"); + } + for (const node of dynamicActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "dynamic", true); + } + for (const node of independentTriggerNodes) { + if ( + this.nodeToActor.has(node.gltfObjectIndex) || + this.nodeToMotion.has(node.gltfObjectIndex) + ) { + continue; + } + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "trigger", true); + } + for (const node of jointNodes) { + this.createJoint(state.gltf, node); + } + + this.PhysX.destroy(tmpVec); + this.PhysX.destroy(sceneDesc); + this.PhysX.destroy(shapeFlags); + this.PhysX.destroy(triggerFlags); + + this.debugStateChanged = true; + this.changeDebugVisualization(); + } + + enableDebugColliders(enable) { + this.debugColliders = enable; + this.debugStateChanged = true; + } + + enableDebugJoints(enable) { + this.debugJoints = enable; + this.debugStateChanged = true; + } + + applyTransformRecursively(gltf, node, parentTransform) { + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; + } + const localTransform = node.getLocalTransform(); + const globalTransform = mat4.create(); + mat4.multiply(globalTransform, parentTransform, localTransform); + node.scaledPhysicsTransform = globalTransform; + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.applyTransformRecursively(gltf, childNode, globalTransform); + } + } + + subStepSimulation(state, deltaTime) { + // eslint-disable-next-line no-unused-vars + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + if (node.dirtyTransform) { + // Node transform is currently animated + continue; + } + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion && motion.isKinematic) { + const linearVelocity = motion.computedLinearVelocity ?? motion.linearVelocity; + const angularVelocity = motion.computedAngularVelocity ?? motion.angularVelocity; + if (linearVelocity !== undefined || angularVelocity !== undefined) { + const worldTransform = node.physicsTransform ?? node.worldTransform; + const targetPosition = vec3.create(); + targetPosition[0] = worldTransform[12]; + targetPosition[1] = worldTransform[13]; + targetPosition[2] = worldTransform[14]; + let nodeRotation = quat.create(); + if (node.physicsTransform !== undefined) { + mat4.getRotation(nodeRotation, worldTransform); + } else { + nodeRotation = quat.clone(node.worldQuaternion); + } + if (linearVelocity !== undefined) { + const acceleration = vec3.create(); + vec3.scale(acceleration, linearVelocity, deltaTime); + vec3.transformQuat(acceleration, acceleration, nodeRotation); + targetPosition[0] += acceleration[0]; + targetPosition[1] += acceleration[1]; + targetPosition[2] += acceleration[2]; + } + if (angularVelocity !== undefined) { + // Transform angular velocity from local space to world space + // by rotating the velocity axes by the current node rotation. + const localX = vec3.fromValues(1, 0, 0); + const localY = vec3.fromValues(0, 1, 0); + const localZ = vec3.fromValues(0, 0, 1); + vec3.transformQuat(localX, localX, nodeRotation); + vec3.transformQuat(localY, localY, nodeRotation); + vec3.transformQuat(localZ, localZ, nodeRotation); + + const angularAcceleration = quat.create(); + const qX = quat.create(); + const qY = quat.create(); + const qZ = quat.create(); + quat.setAxisAngle(qX, localX, angularVelocity[0] * deltaTime); + quat.setAxisAngle(qY, localY, angularVelocity[1] * deltaTime); + quat.setAxisAngle(qZ, localZ, angularVelocity[2] * deltaTime); + quat.multiply(angularAcceleration, qX, angularAcceleration); + quat.multiply(angularAcceleration, qY, angularAcceleration); + quat.multiply(angularAcceleration, qZ, angularAcceleration); + + quat.multiply(nodeRotation, angularAcceleration, nodeRotation); + } + const pos = new this.PhysX.PxVec3(...targetPosition); + const rot = new this.PhysX.PxQuat(...nodeRotation); + const transform = new this.PhysX.PxTransform(pos, rot); + + actor.setKinematicTarget(transform); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(transform); + + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, nodeRotation, targetPosition); + + const scaledPhysicsTransform = mat4.create(); + mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); + + node.physicsTransform = physicsTransform; + node.scaledPhysicsTransform = scaledPhysicsTransform; + } + } else if (motion && motion.gravityFactor !== 1.0) { + const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); + actor.addForce(force, this.PhysX.PxForceModeEnum.eACCELERATION); + this.PhysX.destroy(force); + } + } + + this.scene.simulate(deltaTime); + if (!this.scene.fetchResults(true)) { + console.warn("PhysX: fetchResults failed"); + } + } + + simulateStep(state, deltaTime) { + if (!this.scene) { + this.reset = false; + return; + } + if (this.reset === true) { + this._resetSimulation(); + this.reset = false; + return; + } + + this.changeDebugVisualization(); + + this.subStepSimulation(state, deltaTime); + + // eslint-disable-next-line no-unused-vars + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion && !motion.isKinematic && !node.dirtyTransform) { + const transform = actor.getGlobalPose(); + const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); + const rotation = quat.fromValues( + transform.q.x, + transform.q.y, + transform.q.z, + transform.q.w + ); + + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, rotation, position); + + node.physicsTransform = physicsTransform; + + const rotationBetween = quat.create(); + + let parentNode = node; + while (parentNode.parentNode !== undefined) { + parentNode = parentNode.parentNode; + } + + quat.invert(rotationBetween, node.worldQuaternion); + quat.multiply(rotationBetween, rotation, rotationBetween); + + const rotMat = mat3.create(); + mat3.fromQuat(rotMat, rotationBetween); + + const scaleRot = mat3.create(); + mat3.fromMat4(scaleRot, node.worldTransform); + + mat3.multiply(scaleRot, rotMat, scaleRot); + + const scaledPhysicsTransform = mat4.create(); + scaledPhysicsTransform[0] = scaleRot[0]; + scaledPhysicsTransform[1] = scaleRot[1]; + scaledPhysicsTransform[2] = scaleRot[2]; + scaledPhysicsTransform[4] = scaleRot[3]; + scaledPhysicsTransform[5] = scaleRot[4]; + scaledPhysicsTransform[6] = scaleRot[5]; + scaledPhysicsTransform[8] = scaleRot[6]; + scaledPhysicsTransform[9] = scaleRot[7]; + scaledPhysicsTransform[10] = scaleRot[8]; + scaledPhysicsTransform[12] = position[0]; + scaledPhysicsTransform[13] = position[1]; + scaledPhysicsTransform[14] = position[2]; + + node.scaledPhysicsTransform = scaledPhysicsTransform; + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + this.applyTransformRecursively( + state.gltf, + childNode, + node.scaledPhysicsTransform + ); + } + } + } + } + + resetSimulation() { + this.reset = true; + this.simulateStep({}, 0); + } + + _resetSimulation() { + const scenePointer = this.scene; + this.scene = undefined; + this.filterData = []; + for (const physXFilterData of this.physXFilterData) { + this.PhysX.destroy(physXFilterData); + } + this.physXFilterData = []; + + for (const material of this.physXMaterials) { + material.release(); + } + this.physXMaterials = []; + + for (const shape of this.simpleShapes) { + shape.destroy?.(); + } + this.simpleShapes = []; + + for (const convexMesh of this.convexMeshes) { + convexMesh.release(); + } + this.convexMeshes = []; + + for (const triangleMesh of this.triangleMeshes) { + triangleMesh.release(); + } + this.triangleMeshes = []; + + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.release(); + } + } + this.nodeToSimplifiedJoints.clear(); + + for (const actor of this.nodeToActor.values()) { + actor.actor.release(); + } + + this.nodeToActor.clear(); + + if (scenePointer) { + scenePointer.release(); + } + + this.shapeToNode.clear(); + } + + getDebugLineData() { + if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { + return []; + } + const result = []; + const rb = this.scene.getRenderBuffer(); + for (let i = 0; i < rb.getNbLines(); i++) { + const line = this.PhysX.NativeArrayHelpers.prototype.getDebugLineAt(rb.getLines(), i); + + result.push(line.pos0.x); + result.push(line.pos0.y); + result.push(line.pos0.z); + result.push(line.pos1.x); + result.push(line.pos1.y); + result.push(line.pos1.z); + } + return result; + } + + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { + if (!this.scene) { + return; + } + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; + } + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; + + const linImpulse = new this.PhysX.PxVec3(...linearImpulse); + const angImpulse = new this.PhysX.PxVec3(...angularImpulse); + actor.addForce(linImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + actor.addTorque(angImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + this.PhysX.destroy(linImpulse); + this.PhysX.destroy(angImpulse); + } + + applyPointImpulse(nodeIndex, impulse, position) { + if (!this.scene) { + return; + } + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; + } + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; + + const pxImpulse = new this.PhysX.PxVec3(...impulse); + const pxPosition = new this.PhysX.PxVec3(...position); + this.PhysX.PxRigidBodyExt.prototype.addForceAtPos( + actor, + pxImpulse, + pxPosition, + this.PhysX.PxForceModeEnum.eIMPULSE + ); + this.PhysX.destroy(pxImpulse); + this.PhysX.destroy(pxPosition); + } + + rayCast(rayStart, rayEnd) { + const result = {}; + result.hitNodeIndex = -1; + if (!this.scene) { + return result; + } + const origin = new this.PhysX.PxVec3(...rayStart); + const directionVec = vec3.create(); + vec3.subtract(directionVec, rayEnd, rayStart); + vec3.normalize(directionVec, directionVec); + const direction = new this.PhysX.PxVec3(...directionVec); + const maxDistance = vec3.distance(rayStart, rayEnd); + + const hitBuffer = new this.PhysX.PxRaycastBuffer10(); + const hitFlags = new this.PhysX.PxHitFlags(this.PhysX.PxHitFlagEnum.eDEFAULT); + + const queryFilterData = new this.PhysX.PxQueryFilterData(); + queryFilterData.set_flags( + this.PhysX.PxQueryFlagEnum.eSTATIC | this.PhysX.PxQueryFlagEnum.eDYNAMIC + ); + + const hasHit = this.scene.raycast( + origin, + direction, + maxDistance, + hitBuffer, + hitFlags, + queryFilterData + ); + + this.PhysX.destroy(origin); + this.PhysX.destroy(direction); + this.PhysX.destroy(hitFlags); + this.PhysX.destroy(queryFilterData); + + if (hasHit) { + const hitCount = hitBuffer.getNbAnyHits(); + if (hitCount > 1) { + console.warn("Raycast hit multiple objects, only the first hit is returned."); + } + const hit = hitBuffer.getAnyHit(0); + const fraction = hit.distance / maxDistance; + const hitNormal = vec3.fromValues(hit.normal.x, hit.normal.y, hit.normal.z); + const hitNodeIndex = this.shapeToNode.get(hit.shape.ptr); + if (hitNodeIndex === undefined) { + return result; + } + return { + hitNodeIndex: hitNodeIndex, + hitFraction: fraction, + hitNormal: hitNormal + }; + } else { + return result; + } + } +} + +export { NvidiaPhysicsInterface }; diff --git a/source/PhysicsEngines/PhysicsInterface.js b/source/PhysicsEngines/PhysicsInterface.js new file mode 100644 index 00000000..9ab56f63 --- /dev/null +++ b/source/PhysicsEngines/PhysicsInterface.js @@ -0,0 +1,101 @@ +/* eslint-disable no-unused-vars */ +import { quat, vec3 } from "gl-matrix"; + +class PhysicsInterface { + constructor() { + this.simpleShapes = []; + } + + async initializeEngine() {} + initializeSimulation( + state, + staticActors, + kinematicActors, + dynamicActors, + jointNodes, + triggerNodes, + independentTriggerNodes, + nodeToMotion, + hasRuntimeAnimationTargets, + staticMeshColliderCount, + dynamicMeshColliderCount + ) {} + pauseSimulation() {} + resumeSimulation() {} + resetSimulation() {} + stopSimulation() {} + enableDebugColliders(enable) {} + enableDebugJoints(enable) {} + + applyImpulse(nodeIndex, linearImpulse, angularImpulse) {} + applyPointImpulse(nodeIndex, impulse, position) {} + rayCast(rayStart, rayEnd) {} + + generateBox(x, y, z, scale, scaleAxis, reference) {} + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateSphere(radius, scale, scaleAxis, reference) {} + generatePlane(width, height, doubleSided, scale, scaleAxis, reference) {} + generateSimpleShape( + shape, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create(), + reference = undefined + ) { + switch (shape.type) { + case "box": + return this.generateBox( + shape.box.size[0], + shape.box.size[1], + shape.box.size[2], + scale, + scaleAxis, + reference + ); + case "capsule": + return this.generateCapsule( + shape.capsule.height, + shape.capsule.radiusTop, + shape.capsule.radiusBottom, + scale, + scaleAxis, + reference + ); + case "cylinder": + return this.generateCylinder( + shape.cylinder.height, + shape.cylinder.radiusTop, + shape.cylinder.radiusBottom, + scale, + scaleAxis, + reference + ); + case "sphere": + return this.generateSphere(shape.sphere.radius, scale, scaleAxis, reference); + case "plane": + return this.generatePlane( + shape.plane.width, + shape.plane.height, + shape.plane.doubleSided, + scale, + scaleAxis, + reference + ); + } + } + + generateSimpleShapes(gltf) { + this.simpleShapes = []; + if (gltf?.extensions?.KHR_implicit_shapes === undefined) { + return; + } + for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { + this.simpleShapes.push(this.generateSimpleShape(shape)); + } + } + + updateActorTransform(node) {} + updatePhysicsJoint(state, jointNode) {} +} + +export { PhysicsInterface }; diff --git a/source/gltf/physics_utils.js b/source/gltf/physics_utils.js new file mode 100644 index 00000000..45ee14b3 --- /dev/null +++ b/source/gltf/physics_utils.js @@ -0,0 +1,156 @@ +import { quat, vec3 } from "gl-matrix"; + +class PhysicsUtils { + static calculateScaleAndAxis(node) { + const scaleFactor = vec3.clone(node.scale); + let scaleRotation = quat.create(); + + let currentNode = node.parentNode; + const currentRotation = quat.clone(node.rotation); + + while (currentNode !== undefined) { + if (vec3.equals(currentNode.scale, vec3.fromValues(1, 1, 1)) === false) { + const localScale = currentNode.scale; + vec3.transformQuat(localScale, currentNode.scale, scaleRotation); + vec3.multiply(scaleFactor, scaleFactor, localScale); + scaleRotation = quat.clone(currentRotation); + } + const nextRotation = quat.clone(currentNode.rotation); + quat.multiply(currentRotation, currentRotation, nextRotation); + currentNode = currentNode.parentNode; + } + return { scale: scaleFactor, scaleAxis: scaleRotation }; + } + + /** + * Converts triangle strip indices to triangle list indices + * @param {Uint32Array|Array} stripIndices - The triangle strip indices + * @returns {Uint32Array} - Triangle list indices + */ + static convertTriangleStripToTriangles(stripIndices) { + if (stripIndices.length < 3) { + return new Uint32Array(0); + } + + const triangleCount = stripIndices.length - 2; + const triangleIndices = new Uint32Array(triangleCount * 3); + let triangleIndex = 0; + + for (let i = 0; i < triangleCount; i++) { + if (i % 2 === 0) { + // Even triangle: maintain winding order + triangleIndices[triangleIndex++] = stripIndices[i]; + triangleIndices[triangleIndex++] = stripIndices[i + 1]; + triangleIndices[triangleIndex++] = stripIndices[i + 2]; + } else { + // Odd triangle: reverse winding order + triangleIndices[triangleIndex++] = stripIndices[i]; + triangleIndices[triangleIndex++] = stripIndices[i + 2]; + triangleIndices[triangleIndex++] = stripIndices[i + 1]; + } + } + + return triangleIndices; + } + + /** + * Converts triangle fan indices to triangle list indices + * @param {Uint32Array|Array} fanIndices - The triangle fan indices + * @returns {Uint32Array} - Triangle list indices + */ + static convertTriangleFanToTriangles(fanIndices) { + if (fanIndices.length < 3) { + return new Uint32Array(0); + } + + const triangleCount = fanIndices.length - 2; + const triangleIndices = new Uint32Array(triangleCount * 3); + let triangleIndex = 0; + + const centerVertex = fanIndices[0]; + + for (let i = 1; i < fanIndices.length - 1; i++) { + triangleIndices[triangleIndex++] = fanIndices[i]; + triangleIndices[triangleIndex++] = fanIndices[i + 1]; + triangleIndices[triangleIndex++] = centerVertex; + } + + return triangleIndices; + } + + static recurseCollider( + gltf, + node, + collider, + motionNode, + offsetChanged, + scaleChanged, + customFunction, + args = [] + ) { + // Do not add other motion bodies' shapes to this actor + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; + } + + const computedWorldTransform = node.worldTransform; + if (node.animatedPropertyObjects.scale.dirty) { + scaleChanged = true; + } + if (node.isLocalTransformDirty()) { + offsetChanged = true; + } + + // Found a collider geometry + if ( + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.mesh !== undefined || + node.extensions?.KHR_physics_rigid_bodies?.collider?.geometry?.shape !== undefined + ) { + customFunction( + gltf, + node, + node.extensions.KHR_physics_rigid_bodies.collider, + motionNode, + computedWorldTransform, + offsetChanged, + scaleChanged, + false, + ...args + ); + } + + // Found a trigger + if ( + node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.mesh !== undefined || + node.extensions?.KHR_physics_rigid_bodies?.trigger?.geometry?.shape !== undefined + ) { + customFunction( + gltf, + node, + node.extensions.KHR_physics_rigid_bodies.trigger, + motionNode, + computedWorldTransform, + offsetChanged, + scaleChanged, + true, + ...args + ); + } + + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.recurseCollider( + gltf, + childNode, + collider, + motionNode, + offsetChanged, + scaleChanged, + customFunction, + args + ); + } + } +} + +export { PhysicsUtils }; From e06c1b35893de81b84ca5d5eec83a1b4b0b70fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 25 Mar 2026 16:14:42 +0100 Subject: [PATCH 91/93] Restructure/Comments --- source/GltfState/phyiscs_controller.js | 80 +- source/PhysicsEngines/PhysX.js | 3510 +++++++++++---------- source/PhysicsEngines/PhysicsInterface.js | 78 +- source/gltf/physics_utils.js | 23 + source/gltf/rigid_bodies.js | 20 + 5 files changed, 1919 insertions(+), 1792 deletions(-) diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/phyiscs_controller.js index e7d24ced..957e0d5e 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/phyiscs_controller.js @@ -28,6 +28,7 @@ class PhysicsController { this.loading = false; } + // Morphing colliders was dropped from the spec, but we keep the code here in case we want to add support for it in the future. calculateMorphColliders(gltf) { for (const node of this.morphedColliders) { const mesh = gltf.meshes[node.mesh]; @@ -75,6 +76,11 @@ class PhysicsController { } } + /** + * Initializes the physics engine. This must be called before loading any scenes. + * Currently, only "NvidiaPhysX" is supported. + * @param {string} engine + */ async initializeEngine(engine) { if (engine === "NvidiaPhysX") { this.engine = new NvidiaPhysicsInterface(); @@ -82,6 +88,13 @@ class PhysicsController { } } + /** + * 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. + * @param {GltfState} state + * @param {number} sceneIndex + */ loadScene(state, sceneIndex) { this.resetScene(state.gltf); if ( @@ -212,6 +225,10 @@ class PhysicsController { this.simulateStep(state, 0); // Simulate an initial step to ensure everything is up to date before rendering } + /** + * Resets the current physics state. + * @param {glTF} gltf + */ resetScene(gltf) { this.staticActors = []; this.kinematicActors = []; @@ -236,27 +253,34 @@ class PhysicsController { } } - stopSimulation() { - this.playing = false; - this.enabled = false; - if (this.engine) { - this.engine.stopSimulation(); - } - } - + /** + * Resumes the physics simulation if it was paused. If the simulation is not paused, this function does nothing. + */ resumeSimulation() { - if (this.engine) { - this.enabled = true; + if (this.engine && this.enabled) { this.playing = true; } } + /** + * Pauses the physics simulation. If the simulation is already paused, this function does nothing. + */ pauseSimulation() { - this.pauseTime = performance.now(); - this.enabled = true; - this.playing = false; + if (this.engine && this.enabled && this.playing) { + this.pauseTime = performance.now(); + this.playing = false; + } } + /** + * 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, in this case simulationStepTime is used as deltaTime. + * @param {GltfState} state + * @param {number} deltaTime + */ simulateStep(state, deltaTime) { if (state === undefined) { return; @@ -264,11 +288,12 @@ class PhysicsController { if (this.loading) { return; } + // We always need to apply animations, since the dirty flags get cleared each frame. + this._applyAnimations(state); if (this.skipFrames > 0) { this.skipFrames -= 1; return; } - this.applyAnimations(state); this.timeAccumulator += deltaTime; if (this.pauseTime !== undefined) { this.timeAccumulator = this.simulationStepTime; @@ -287,7 +312,7 @@ class PhysicsController { } } - updateColliders(state, node, isTrigger = false) { + _updateColliders(state, node, isTrigger = false) { this.engine.updateActorTransform(node); let collider = undefined; @@ -347,40 +372,51 @@ class PhysicsController { } } - applyAnimations(state) { + _applyAnimations(state) { this.engine.updatePhysicMaterials(state.gltf); for (const actorNode of this.staticActors) { - this.updateColliders(state, actorNode); + this._updateColliders(state, actorNode); } for (const actorNode of this.kinematicActors) { this.engine.updateMotion(actorNode); - this.updateColliders(state, actorNode); + this._updateColliders(state, actorNode); } for (const actorNode of this.dynamicActors) { this.engine.updateMotion(actorNode); - this.updateColliders(state, actorNode); + this._updateColliders(state, actorNode); } for (const node of this.independentTriggerNodes) { - this.updateColliders(state, node, true); + this._updateColliders(state, node, true); } for (const jointNode of this.jointNodes) { - this.engine.updatePhysicsJoint(state, jointNode); //TODO + this.engine.updatePhysicsJoint(state, jointNode); } } + /** + * Enable debug visualization of physics colliders. + * The exact visualization depends on the physics engine implementation. + * @param {boolean} enable + */ enableDebugColliders(enable) { this.engine.enableDebugColliders(enable); } + /** + * Enable debug visualization of physics joints. + * The exact visualization depends on the physics engine implementation. + * @param {boolean} enable + */ enableDebugJoints(enable) { this.engine.enableDebugJoints(enable); } + // Used by the renderer to get debug lines for physics visualization, if supported by the physics engine. getDebugLineData() { if (this.engine) { return this.engine.getDebugLineData(); @@ -388,6 +424,8 @@ class PhysicsController { return []; } + // Functions called by KHR_interactivity + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { this.engine.applyImpulse(nodeIndex, linearImpulse, angularImpulse); } diff --git a/source/PhysicsEngines/PhysX.js b/source/PhysicsEngines/PhysX.js index 6ae06875..214df437 100644 --- a/source/PhysicsEngines/PhysX.js +++ b/source/PhysicsEngines/PhysX.js @@ -44,6 +44,8 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.MAX_FLOAT = 3.4028234663852885981170418348452e38; } + //region General + async initializeEngine() { this.PhysX = await PhysX({ locateFile: () => "./libs/physx-js-webidl.wasm" }); const version = this.PhysX.PHYSICS_VERSION; @@ -69,884 +71,785 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.PhysX; } - updatePhysicMaterials(gltf) { - const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; - if (materials === undefined) { + changeDebugVisualization() { + if (!this.scene || !this.debugStateChanged) { return; } - for (let i = 0; i < materials.length; i++) { - const material = materials[i]; - if (material.isDirty()) { - const physXMaterial = this.physXMaterials[i]; - physXMaterial.setStaticFriction(material.staticFriction); - physXMaterial.setDynamicFriction(material.dynamicFriction); - physXMaterial.setRestitution(material.restitution); + this.debugStateChanged = false; + this.scene.setVisualizationParameter( + this.PhysX.eSCALE, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eWORLD_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eACTOR_AXES, + this.debugColliders || this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eCOLLISION_SHAPES, + this.debugColliders ? 1 : 0 + ); + this.scene.setVisualizationParameter( + this.PhysX.eJOINT_LOCAL_FRAMES, + this.debugJoints ? 1 : 0 + ); + this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints + ); } } - } - - updateActorTransform(node) { - if (node.dirtyTransform) { - const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; - if (actor === undefined) { - return; - } - const pxPos = new this.PhysX.PxVec3( - node.worldTransform[12], - node.worldTransform[13], - node.worldTransform[14] - ); - const pxRot = new this.PhysX.PxQuat(...node.worldQuaternion); - const pxTransform = new this.PhysX.PxTransform(pxPos, pxRot); - if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { - actor.setKinematicTarget(pxTransform); - } else { - actor.setGlobalPose(pxTransform); - } - this.PhysX.destroy(pxPos); - this.PhysX.destroy(pxRot); - this.PhysX.destroy(pxTransform); + for (const shapePtr of this.shapeToNode.keys()) { + const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); + shape.setFlag(this.PhysX.PxShapeFlagEnum.eVISUALIZATION, this.debugColliders); } } - calculateMassAndInertia(motion, actor) { - const pos = new this.PhysX.PxVec3(0, 0, 0); - if (motion.centerOfMass !== undefined) { - pos.x = motion.centerOfMass[0]; - pos.y = motion.centerOfMass[1]; - pos.z = motion.centerOfMass[2]; + initializeSimulation( + state, + staticActors, + kinematicActors, + dynamicActors, + jointNodes, + triggerNodes, + independentTriggerNodes, + nodeToMotion, + _hasRuntimeAnimationTargets, + _staticMeshColliderCount, + _dynamicMeshColliderCount + ) { + if (!this.PhysX) { + return; } - const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); - if (motion.inertiaDiagonal !== undefined) { - let inertia = undefined; - if ( - motion.inertiaOrientation !== undefined && - !quat.exactEquals(motion.inertiaOrientation, quat.create()) - ) { - const intertiaRotMat = mat3.create(); - - const inertiaDiagonalMat = mat3.create(); - inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; - inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; - inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; - - if ( - quat.length(motion.inertiaOrientation) > 1.0e-5 || - quat.length(motion.inertiaOrientation) < 1.0e-5 - ) { - mat3.identity(intertiaRotMat); - console.warn( - "PhysX: Invalid inertia orientation quaternion, ignoring rotation" - ); - } else { - mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); - } - - const inertiaTensor = mat3.create(); - mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); - - const col0 = new this.PhysX.PxVec3( - inertiaTensor[0], - inertiaTensor[1], - inertiaTensor[2] - ); - const col1 = new this.PhysX.PxVec3( - inertiaTensor[3], - inertiaTensor[4], - inertiaTensor[5] - ); - const col2 = new this.PhysX.PxVec3( - inertiaTensor[6], - inertiaTensor[7], - inertiaTensor[8] - ); - const pxInertiaTensor = new this.PhysX.PxMat33(col0, col1, col2); - inertia = this.PhysX.PxMassProperties.prototype.getMassSpaceInertia( - pxInertiaTensor, - rot - ); - this.PhysX.destroy(col0); - this.PhysX.destroy(col1); - this.PhysX.destroy(col2); - this.PhysX.destroy(pxInertiaTensor); - actor.setMassSpaceInertiaTensor(inertia); - } else { - inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); - actor.setMassSpaceInertiaTensor(inertia); - this.PhysX.destroy(inertia); - } - } else { - if (motion.mass === undefined) { - this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); - } else { - this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( - actor, - motion.mass, - pos - ); - } + this.nodeToMotion = nodeToMotion; + this.generateSimpleShapes(state.gltf); + this.computeFilterData(state.gltf); + for (let i = 0; i < this.filterData.length; i++) { + const physXFilterData = this.createPhysXCollisionFilter(i); + this.physXFilterData.push(physXFilterData); } - const pose = new this.PhysX.PxTransform(pos, rot); - actor.setCMassLocalPose(pose); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(pose); - } - - updateMotion(actorNode) { - const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; - const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; - if (motion.animatedPropertyObjects.isKinematic.dirty) { - if (motion.isKinematic) { - const linearVelocity = actor.getLinearVelocity(); - motion.computedLinearVelocity = [ - linearVelocity.x, - linearVelocity.y, - linearVelocity.z - ]; - const angularVelocity = actor.getAngularVelocity(); - motion.computedAngularVelocity = [ - angularVelocity.x, - angularVelocity.y, - angularVelocity.z - ]; - } else { - motion.computedLinearVelocity = undefined; - motion.computedAngularVelocity = undefined; + const materials = state.gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials !== undefined) { + for (const gltfMaterial of materials) { + const physxMaterial = this.createPhysXMaterial(gltfMaterial); + this.physXMaterials.push(physxMaterial); } - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, !motion.isKinematic); - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); - } - if (motion.animatedPropertyObjects.mass.dirty) { - actor.setMass(motion.mass); - } - if ( - motion.animatedPropertyObjects.centerOfMass.dirty || - motion.animatedPropertyObjects.inertiaOrientation.dirty || - motion.animatedPropertyObjects.inertiaDiagonal.dirty - ) { - this.calculateMassAndInertia(motion, actor); - } - if (motion.animatedPropertyObjects.gravityFactor.dirty) { - actor.setActorFlag( - this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, - motion.gravityFactor !== 1.0 - ); - } - if (motion.animatedPropertyObjects.linearVelocity.dirty) { - const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); - actor.setLinearVelocity(pxVelocity); - motion.computedLinearVelocity = undefined; - } - if (motion.animatedPropertyObjects.angularVelocity.dirty) { - const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); - actor.setAngularVelocity(pxVelocity); - motion.computedAngularVelocity = undefined; } - } - updateCollider( - gltf, - node, - collider, - actorNode, - worldTransform, - offsetChanged, - scaleChanged, - isTrigger - ) { - const result = this.nodeToActor.get(actorNode.gltfObjectIndex); - const actor = result?.actor; - const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); + const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); + const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); + sceneDesc.set_gravity(tmpVec); + sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); + sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); + const sceneFlags = new this.PhysX.PxSceneFlags( + this.PhysX.PxSceneFlagEnum.eENABLE_CCD | this.PhysX.PxSceneFlagEnum.eENABLE_PCM + ); + sceneDesc.flags = sceneFlags; - let currentGeometry = currentShape.getGeometry(); - const currentColliderType = currentGeometry.getType(); - const shapeIndex = collider?.geometry?.shape; - let scale = vec3.fromValues(1, 1, 1); - let scaleAxis = quat.create(); - if (shapeIndex !== undefined) { - // Simple shapes need to be recreated if scale changed - // If properties changed we also need to recreate the mesh colliders - const dirty = gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex].isDirty(); - if ( - scaleChanged && - !dirty && - currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH - ) { - // Update convex mesh scale - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxConvexMeshGeometry - ); - const result = PhysicsUtils.calculateScaleAndAxis(node); - scale = result.scale; - scaleAxis = result.scaleAxis; - const pxScale = new this.PhysX.PxVec3(...scale); - const pxRotation = new this.PhysX.PxQuat(...scaleAxis); - const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); - currentGeometry.scale = meshScale; - this.PhysX.destroy(pxScale); - this.PhysX.destroy(pxRotation); - } else if (dirty || scaleChanged) { - // Recreate simple shape collider - const newGeometry = this.generateSimpleShape( - gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], - scale, - scaleAxis - ); - currentGeometry.release?.(); - if (newGeometry.getType() !== currentColliderType) { - // We need to recreate the shape - let shapeFlags = undefined; - if (isTrigger) { - shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; - } else { - shapeFlags = this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE; + this.scene = this.physics.createScene(sceneDesc); + let triggerCallback = undefined; + + if (triggerNodes.length > 0) { + console.log("Enabling trigger report callback"); + triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); + triggerCallback.onTrigger = (pairs, count) => { + for (const compoundTrigger of state.physicsController.compoundTriggerNodes.values()) { + compoundTrigger.added.clear(); + compoundTrigger.removed.clear(); + } + console.log("Trigger callback called with", count, "pairs"); + for (let i = 0; i < count; i++) { + const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); + const triggerShape = pair.triggerShape; + const otherShape = pair.otherShape; + const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); + const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + state.graphController.rigidBodyTriggerEntered( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex) + ); + } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { + state.graphController.rigidBodyTriggerExited( + triggerNodeIndex, + otherNodeIndex, + nodeToMotion.get(otherNodeIndex) + ); } - if (this.debugColliders) { - shapeFlags |= this.PhysX.PxShapeFlagEnum.eVISUALIZATION; + const compoundTriggers = + state.physicsController.triggerToCompound.get(triggerNodeIndex); + if (compoundTriggers !== undefined) { + for (const compoundTriggerIndex of compoundTriggers) { + const compoundTriggerInfo = + state.physicsController.compoundTriggerNodes.get( + compoundTriggerIndex + ); + if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { + compoundTriggerInfo.added.add(otherNodeIndex); + } else if ( + pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST + ) { + compoundTriggerInfo.removed.add(otherNodeIndex); + } + } } - const shape = this.createShapeFromGeometry( - newGeometry, - undefined, - undefined, - shapeFlags, - collider - ); - result?.pxShapeMap.set(node.gltfObjectIndex, shape); - actor.detachShape(currentShape); - actor.attachShape(shape); - this.PhysX.destroy(currentShape); - } else { - currentShape.setGeometry(newGeometry); } - } - } else if (collider?.geometry?.mesh !== undefined) { - if (scaleChanged) { - if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxConvexMeshGeometry - ); - } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { - currentGeometry = this.PhysX.castObject( - currentGeometry, - this.PhysX.PxTriangleMeshGeometry - ); + + for (const [ + idx, + compoundTrigger + ] of state.physicsController.compoundTriggerNodes.entries()) { + for (const addedNodeIndex of compoundTrigger.added) { + if (!compoundTrigger.previous.has(addedNodeIndex)) { + compoundTrigger.previous.set(addedNodeIndex, 1); + state.graphController.rigidBodyTriggerEntered( + idx, + addedNodeIndex, + nodeToMotion.get(addedNodeIndex) + ); + } else { + const currentCount = compoundTrigger.previous.get(addedNodeIndex); + compoundTrigger.previous.set(addedNodeIndex, currentCount + 1); + } + } + for (const removedNodeIndex of compoundTrigger.removed) { + const currentCount = compoundTrigger.previous.get(removedNodeIndex); + if (currentCount > 1) { + compoundTrigger.previous.set(removedNodeIndex, currentCount - 1); + } else { + compoundTrigger.previous.delete(removedNodeIndex); + state.graphController.rigidBodyTriggerExited( + idx, + removedNodeIndex, + nodeToMotion.get(removedNodeIndex) + ); + } + } } - // apply scale - const result = PhysicsUtils.calculateScaleAndAxis(node); - scale = result.scale; - scaleAxis = result.scaleAxis; - const pxScale = new this.PhysX.PxVec3(...scale); - const pxRotation = new this.PhysX.PxQuat(...scaleAxis); - const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); - currentGeometry.scale = meshScale; - this.PhysX.destroy(pxScale); - this.PhysX.destroy(pxRotation); - } + }; + + // All callbacks need to be defined + triggerCallback.onConstraintBreak = (_constraints, _count) => {}; + triggerCallback.onWake = (_actors, _count) => {}; + triggerCallback.onSleep = (_actors, _count) => {}; + triggerCallback.onContact = (_pairHeaders, _pairs, _count) => {}; + sceneDesc.simulationEventCallback = triggerCallback; } - if (offsetChanged) { - // Calculate offset position - const translation = vec3.create(); - const shapePosition = vec3.create(); - mat4.getTranslation(shapePosition, actorNode.worldTransform); - const invertedActorRotation = quat.create(); - quat.invert(invertedActorRotation, actorNode.worldQuaternion); - const offsetPosition = vec3.create(); - mat4.getTranslation(offsetPosition, worldTransform); - vec3.subtract(translation, offsetPosition, shapePosition); - vec3.transformQuat(translation, translation, invertedActorRotation); - // Calculate offset rotation - const rotation = quat.create(); - quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); + this.scene = this.physics.createScene(sceneDesc); - const PxPos = new this.PhysX.PxVec3(...translation); - const PxRotation = new this.PhysX.PxQuat(...rotation); - const pose = new this.PhysX.PxTransform(PxPos, PxRotation); - currentShape.setLocalPose(pose); - } - } + console.log("Created scene"); + const shapeFlags = new this.PhysX.PxShapeFlags( + this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | + this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE + ); - updatePhysicsJoint(state, jointNode) { - const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); - if (pxJoints === undefined) { - return; + const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); + + for (const node of staticActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "static"); } - const gltfJoint = - state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[ - jointNode.extensions.KHR_physics_rigid_bodies.joint.joint - ]; - const simplifiedJoints = gltfJoint.simplifiedPhysicsJoints; - if (simplifiedJoints.length !== pxJoints.length) { - console.warn( - "Number of simplified joints does not match number of PhysX joints. Skipping joint update." - ); - return; + for (const node of kinematicActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "kinematic"); } - for (let i = 0; i < simplifiedJoints.length; i++) { - const pxJoint = pxJoints[i]; - const simplifiedJoint = simplifiedJoints[i]; - if ( - jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects - .enableCollision.dirty - ) { - pxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision - ); - } - for (const limit of simplifiedJoint.limits) { - if ( - limit.animatedPropertyObjects.min.dirty || - limit.animatedPropertyObjects.max.dirty || - limit.animatedPropertyObjects.stiffness.dirty || - limit.animatedPropertyObjects.damping.dirty - ) { - this._setLimitValues(pxJoint, simplifiedJoint, limit); - } - } - + for (const node of dynamicActors) { + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "dynamic", true); + } + for (const node of independentTriggerNodes) { if ( - simplifiedJoint.twistLimit && - (simplifiedJoint.twistLimit.animatedPropertyObjects.min.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.max.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.twistLimit.animatedPropertyObjects.damping.dirty) + this.nodeToActor.has(node.gltfObjectIndex) || + this.nodeToMotion.has(node.gltfObjectIndex) ) { - this._setTwistLimitValues(pxJoint, simplifiedJoint); + continue; } + this.createActor(state.gltf, node, shapeFlags, triggerFlags, "trigger", true); + } + for (const node of jointNodes) { + this.createJoint(state.gltf, node); + } - if ( - (simplifiedJoint.swingLimit1 && - (simplifiedJoint.swingLimit1.animatedPropertyObjects.min.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.max.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.swingLimit1.animatedPropertyObjects.damping.dirty)) || - (simplifiedJoint.swingLimit2 && - (simplifiedJoint.swingLimit2.animatedPropertyObjects.min.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.max.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.stiffness.dirty || - simplifiedJoint.swingLimit2.animatedPropertyObjects.damping.dirty)) - ) { - this._setSwingLimitValues(pxJoint, simplifiedJoint); - } + this.PhysX.destroy(tmpVec); + this.PhysX.destroy(sceneDesc); + this.PhysX.destroy(shapeFlags); + this.PhysX.destroy(triggerFlags); - let positionTargetDirty = false; - let velocityTargetDirty = false; - const linearVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); - const angularVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); - pxJoint.getDriveVelocity(linearVelocityTarget, angularVelocityTarget); - for (const drive of simplifiedJoint.drives) { - if ( - drive.animatedPropertyObjects.stiffness.dirty || - drive.animatedPropertyObjects.damping.dirty || - drive.animatedPropertyObjects.maxForce.dirty - ) { - this._setDriveValues(pxJoint, simplifiedJoint, drive); - } - if (drive.animatedPropertyObjects.velocityTarget.dirty) { - this._getDriveVelocityTarget( - simplifiedJoint, - drive, - linearVelocityTarget, - angularVelocityTarget - ); - velocityTargetDirty = true; - } - if (drive.animatedPropertyObjects.positionTarget.dirty) { - positionTargetDirty = true; - } - } + this.debugStateChanged = true; + this.changeDebugVisualization(); + } - if (positionTargetDirty) { - this._setDrivePositionTarget(pxJoint, simplifiedJoint); - } - if (velocityTargetDirty) { - pxJoint.setDriveVelocity(linearVelocityTarget, angularVelocityTarget); - } - } + enableDebugColliders(enable) { + this.debugColliders = enable; + this.debugStateChanged = true; } - mapCombineMode(mode) { - switch (mode) { - case "average": - return this.PhysX.PxCombineModeEnum.eAVERAGE; - case "minimum": - return this.PhysX.PxCombineModeEnum.eMIN; - case "maximum": - return this.PhysX.PxCombineModeEnum.eMAX; - case "multiply": - return this.PhysX.PxCombineModeEnum.eMULTIPLY; - } + enableDebugJoints(enable) { + this.debugJoints = enable; + this.debugStateChanged = true; } - // Either create a box or update an existing one. Returns only newly created geometry - generateBox(x, y, z, scale, scaleAxis, reference) { - let referenceType = undefined; - if (reference !== undefined) { - referenceType = reference.getType(); - } - if ( - scale.every((value) => value === scale[0]) === false && - quat.equals(scaleAxis, quat.create()) === false - ) { - const data = createBoxVertexData(x, y, z); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + applyTransformRecursively(gltf, node, parentTransform) { + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; } - let geometry = undefined; - if (referenceType === this.PhysX.PxGeometryTypeEnum.eBOX) { - const halfExtents = new this.PhysX.PxVec3( - (x / 2) * scale[0], - (y / 2) * scale[1], - (z / 2) * scale[2] - ); - reference.halfExtents = halfExtents; - this.PhysX.destroy(halfExtents); - } else { - geometry = new this.PhysX.PxBoxGeometry( - (x / 2) * scale[0], - (y / 2) * scale[1], - (z / 2) * scale[2] - ); + const localTransform = node.getLocalTransform(); + const globalTransform = mat4.create(); + mat4.multiply(globalTransform, parentTransform, localTransform); + node.scaledPhysicsTransform = globalTransform; + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.applyTransformRecursively(gltf, childNode, globalTransform); } - - return geometry; } - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { - const data = createCapsuleVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } + subStepSimulation(state, deltaTime) { + // eslint-disable-next-line no-unused-vars + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + if (node.dirtyTransform) { + // Node transform is currently animated + continue; + } + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion && motion.isKinematic) { + const linearVelocity = motion.computedLinearVelocity ?? motion.linearVelocity; + const angularVelocity = motion.computedAngularVelocity ?? motion.angularVelocity; + if (linearVelocity !== undefined || angularVelocity !== undefined) { + const worldTransform = node.physicsTransform ?? node.worldTransform; + const targetPosition = vec3.create(); + targetPosition[0] = worldTransform[12]; + targetPosition[1] = worldTransform[13]; + targetPosition[2] = worldTransform[14]; + let nodeRotation = quat.create(); + if (node.physicsTransform !== undefined) { + mat4.getRotation(nodeRotation, worldTransform); + } else { + nodeRotation = quat.clone(node.worldQuaternion); + } + if (linearVelocity !== undefined) { + const acceleration = vec3.create(); + vec3.scale(acceleration, linearVelocity, deltaTime); + vec3.transformQuat(acceleration, acceleration, nodeRotation); + targetPosition[0] += acceleration[0]; + targetPosition[1] += acceleration[1]; + targetPosition[2] += acceleration[2]; + } + if (angularVelocity !== undefined) { + // Transform angular velocity from local space to world space + // by rotating the velocity axes by the current node rotation. + const localX = vec3.fromValues(1, 0, 0); + const localY = vec3.fromValues(0, 1, 0); + const localZ = vec3.fromValues(0, 0, 1); + vec3.transformQuat(localX, localX, nodeRotation); + vec3.transformQuat(localY, localY, nodeRotation); + vec3.transformQuat(localZ, localZ, nodeRotation); + + const angularAcceleration = quat.create(); + const qX = quat.create(); + const qY = quat.create(); + const qZ = quat.create(); + quat.setAxisAngle(qX, localX, angularVelocity[0] * deltaTime); + quat.setAxisAngle(qY, localY, angularVelocity[1] * deltaTime); + quat.setAxisAngle(qZ, localZ, angularVelocity[2] * deltaTime); + quat.multiply(angularAcceleration, qX, angularAcceleration); + quat.multiply(angularAcceleration, qY, angularAcceleration); + quat.multiply(angularAcceleration, qZ, angularAcceleration); + + quat.multiply(nodeRotation, angularAcceleration, nodeRotation); + } + const pos = new this.PhysX.PxVec3(...targetPosition); + const rot = new this.PhysX.PxQuat(...nodeRotation); + const transform = new this.PhysX.PxTransform(pos, rot); + + actor.setKinematicTarget(transform); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(transform); + + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, nodeRotation, targetPosition); + + const scaledPhysicsTransform = mat4.create(); + mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); + + node.physicsTransform = physicsTransform; + node.scaledPhysicsTransform = scaledPhysicsTransform; + } + } else if (motion && motion.gravityFactor !== 1.0) { + const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); + actor.addForce(force, this.PhysX.PxForceModeEnum.eACCELERATION); + this.PhysX.destroy(force); + } + } - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { - if ( - (quat.equals(scaleAxis, quat.create()) === false && - scale.every((value) => value === scale[0]) === false) || - radiusTop !== radiusBottom || - scale[0] !== scale[2] - ) { - const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + this.scene.simulate(deltaTime); + if (!this.scene.fetchResults(true)) { + console.warn("PhysX: fetchResults failed"); } - height *= scale[1]; - radiusTop *= scale[0]; - radiusBottom *= scale[0]; - const data = createCylinderVertexData(radiusTop, radiusBottom, height); - return this.createConvexPxMesh(data.vertices); } - generateSphere(radius, scale, scaleAxis, reference) { - let referenceType = undefined; - if (reference !== undefined) { - referenceType = reference.getType(); - } - if (scale.every((value) => value === scale[0]) === false) { - const data = createCapsuleVertexData(radius, radius, 0); - return this.createConvexPxMesh(data.vertices, scale, scaleAxis); - } else { - radius *= scale[0]; + simulateStep(state, deltaTime) { + if (!this.scene) { + this.reset = false; + return; } - if (referenceType === this.PhysX.PxGeometryTypeEnum.eSPHERE) { - reference.radius = radius; - return undefined; + if (this.reset === true) { + this._resetSimulation(); + this.reset = false; + return; } - const geometry = new this.PhysX.PxSphereGeometry(radius); - return geometry; - } - generatePlane(width, height, doubleSided, scale, scaleAxis, reference) { - if (reference !== undefined) { - //TODO handle update - return undefined; - } - const geometry = new this.PhysX.PxPlaneGeometry(); - return geometry; - } + this.changeDebugVisualization(); - createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const malloc = (f, q) => { - const nDataBytes = f.length * f.BYTES_PER_ELEMENT; - if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); - let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); - dataHeap.set(new Uint8Array(f.buffer)); - return q; - }; - const des = new this.PhysX.PxConvexMeshDesc(); - des.points.stride = vertices.BYTES_PER_ELEMENT * 3; - des.points.count = vertices.length / 3; - des.points.data = malloc(vertices); + this.subStepSimulation(state, deltaTime); - let flag = 0; - flag |= this.PhysX.PxConvexFlagEnum.eCOMPUTE_CONVEX; - flag |= this.PhysX.PxConvexFlagEnum.eSHIFT_VERTICES; - //flag |= this.PhysX.PxConvexFlagEnum.eDISABLE_MESH_VALIDATION; - const pxflags = new this.PhysX.PxConvexFlags(flag); - des.flags = pxflags; - const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); - cookingParams.planeTolerance = 0.0007; //Default - const tri = this.PhysX.CreateConvexMesh(cookingParams, des); - this.convexMeshes.push(tri); + // eslint-disable-next-line no-unused-vars + for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { + const node = state.gltf.nodes[nodeIndex]; + const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; + if (motion && !motion.isKinematic && !node.dirtyTransform) { + const transform = actor.getGlobalPose(); + const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); + const rotation = quat.fromValues( + transform.q.x, + transform.q.y, + transform.q.z, + transform.q.w + ); - const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); - const PxQuat = new this.PhysX.PxQuat(...scaleAxis); - const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); - const f = new this.PhysX.PxConvexMeshGeometryFlags(); - const geometry = new this.PhysX.PxConvexMeshGeometry(tri, ms, f); - this.PhysX.destroy(PxScale); - this.PhysX.destroy(PxQuat); - this.PhysX.destroy(ms); - this.PhysX.destroy(pxflags); - this.PhysX.destroy(cookingParams); - this.PhysX.destroy(des); - return geometry; - } + const physicsTransform = mat4.create(); + mat4.fromRotationTranslation(physicsTransform, rotation, position); - collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { - let positionDataArray = []; - let positionCount = 0; - let indexDataArray = []; - let indexCount = 0; + node.physicsTransform = physicsTransform; - for (const primitive of mesh.primitives) { - const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; - const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + const rotationBetween = quat.create(); - if (primitive.targets !== undefined) { - let morphWeights = mesh.weights; - if (morphWeights !== undefined) { - // Calculate morphed vertex positions on CPU - const morphPositionData = []; - for (const target of primitive.targets) { - if (target.POSITION !== undefined) { - const morphAccessor = gltf.accessors[target.POSITION]; - morphPositionData.push( - morphAccessor.getNormalizedDeinterlacedView(gltf) - ); - } else { - morphPositionData.push(undefined); - } - } - for (let i = 0; i < positionData.length / 3; i++) { - for (let j = 0; j < morphWeights.length; j++) { - const morphData = morphPositionData[j]; - if (morphWeights[j] === 0 || morphData === undefined) { - continue; - } - positionData[i * 3] += morphData[i * 3] * morphWeights[j]; - positionData[i * 3 + 1] += morphData[i * 3 + 1] * morphWeights[j]; - positionData[i * 3 + 2] += morphData[i * 3 + 2] * morphWeights[j]; - } - } + let parentNode = node; + while (parentNode.parentNode !== undefined) { + parentNode = parentNode.parentNode; } - } - positionDataArray.push(positionData); - positionCount += positionAccessor.count; - if (computeIndices) { - let indexData = undefined; - if (primitive.indices !== undefined) { - const indexAccessor = gltf.accessors[primitive.indices]; - indexData = indexAccessor.getNormalizedDeinterlacedView(gltf); - } else { - const array = Array.from(Array(positionAccessor.count).keys()); - indexData = new Uint32Array(array); - } - if (primitive.mode === 5) { - indexData = PhysicsUtils.convertTriangleStripToTriangles(indexData); - } else if (primitive.mode === 6) { - indexData = PhysicsUtils.convertTriangleFanToTriangles(indexData); - } else if (primitive.mode !== undefined && primitive.mode !== 4) { - console.warn( - "Unsupported primitive mode for physics mesh collider creation: " + - primitive.mode + quat.invert(rotationBetween, node.worldQuaternion); + quat.multiply(rotationBetween, rotation, rotationBetween); + + const rotMat = mat3.create(); + mat3.fromQuat(rotMat, rotationBetween); + + const scaleRot = mat3.create(); + mat3.fromMat4(scaleRot, node.worldTransform); + + mat3.multiply(scaleRot, rotMat, scaleRot); + + const scaledPhysicsTransform = mat4.create(); + scaledPhysicsTransform[0] = scaleRot[0]; + scaledPhysicsTransform[1] = scaleRot[1]; + scaledPhysicsTransform[2] = scaleRot[2]; + scaledPhysicsTransform[4] = scaleRot[3]; + scaledPhysicsTransform[5] = scaleRot[4]; + scaledPhysicsTransform[6] = scaleRot[5]; + scaledPhysicsTransform[8] = scaleRot[6]; + scaledPhysicsTransform[9] = scaleRot[7]; + scaledPhysicsTransform[10] = scaleRot[8]; + scaledPhysicsTransform[12] = position[0]; + scaledPhysicsTransform[13] = position[1]; + scaledPhysicsTransform[14] = position[2]; + + node.scaledPhysicsTransform = scaledPhysicsTransform; + for (const childIndex of node.children) { + const childNode = state.gltf.nodes[childIndex]; + this.applyTransformRecursively( + state.gltf, + childNode, + node.scaledPhysicsTransform ); } - indexDataArray.push(indexData); - indexCount += indexData.length; } } + } - const positionData = new Float32Array(positionCount * 3); - const indexData = new Uint32Array(indexCount); - let offset = 0; - for (const positionChunk of positionDataArray) { - positionData.set(positionChunk, offset); - offset += positionChunk.length; + resetSimulation() { + this.reset = true; + this.simulateStep({}, 0); + } + + _resetSimulation() { + const scenePointer = this.scene; + this.scene = undefined; + this.filterData = []; + for (const physXFilterData of this.physXFilterData) { + this.PhysX.destroy(physXFilterData); } - offset = 0; - for (const indexChunk of indexDataArray) { - indexData.set(indexChunk, offset); - offset += indexChunk.length; + this.physXFilterData = []; + + for (const material of this.physXMaterials) { + material.release(); + } + this.physXMaterials = []; + + for (const shape of this.simpleShapes) { + shape.destroy?.(); + } + this.simpleShapes = []; + + for (const convexMesh of this.convexMeshes) { + convexMesh.release(); + } + this.convexMeshes = []; + + for (const triangleMesh of this.triangleMeshes) { + triangleMesh.release(); + } + this.triangleMeshes = []; + + for (const joints of this.nodeToSimplifiedJoints.values()) { + for (const joint of joints) { + joint.release(); + } + } + this.nodeToSimplifiedJoints.clear(); + + for (const actor of this.nodeToActor.values()) { + actor.actor.release(); } - return { vertices: positionData, indices: indexData }; - } - createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); - return this.createConvexPxMesh(result.vertices, scale, scaleAxis); - } + this.nodeToActor.clear(); - createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { - const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); - const malloc = (f, q) => { - const nDataBytes = f.length * f.BYTES_PER_ELEMENT; - if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); - let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); - dataHeap.set(new Uint8Array(f.buffer)); - return q; - }; - const des = new this.PhysX.PxTriangleMeshDesc(); - des.points.stride = vertices.BYTES_PER_ELEMENT * 3; - des.points.count = vertices.length / 3; - des.points.data = malloc(vertices); + if (scenePointer) { + scenePointer.release(); + } - des.triangles.stride = indices.BYTES_PER_ELEMENT * 3; - des.triangles.count = indices.length / 3; - des.triangles.data = malloc(indices); + this.shapeToNode.clear(); + } - const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); - const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); - this.triangleMeshes.push(tri); + getDebugLineData() { + if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { + return []; + } + const result = []; + const rb = this.scene.getRenderBuffer(); + for (let i = 0; i < rb.getNbLines(); i++) { + const line = this.PhysX.NativeArrayHelpers.prototype.getDebugLineAt(rb.getLines(), i); - const PxScale = new this.PhysX.PxVec3(1, 1, 1); - const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); + result.push(line.pos0.x); + result.push(line.pos0.y); + result.push(line.pos0.z); + result.push(line.pos1.x); + result.push(line.pos1.y); + result.push(line.pos1.z); + } + return result; + } - if (scale !== undefined) { - PxScale.x = scale[0]; - PxScale.y = scale[1]; - PxScale.z = scale[2]; + applyImpulse(nodeIndex, linearImpulse, angularImpulse) { + if (!this.scene) { + return; } - if (scaleAxis !== undefined) { - PxQuat.x = scaleAxis[0]; - PxQuat.y = scaleAxis[1]; - PxQuat.z = scaleAxis[2]; - PxQuat.w = scaleAxis[3]; + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; } - const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); - const f = new this.PhysX.PxMeshGeometryFlags(); - const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); - this.PhysX.destroy(PxScale); - this.PhysX.destroy(PxQuat); - this.PhysX.destroy(ms); - this.PhysX.destroy(cookingParams); - this.PhysX.destroy(des); - return geometry; + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; + + const linImpulse = new this.PhysX.PxVec3(...linearImpulse); + const angImpulse = new this.PhysX.PxVec3(...angularImpulse); + actor.addForce(linImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + actor.addTorque(angImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); + this.PhysX.destroy(linImpulse); + this.PhysX.destroy(angImpulse); } - collidesWith(filterA, filterB) { - if (filterB.collideWithSystems.length > 0) { - for (const system of filterB.collideWithSystems) { - if (filterA.collisionSystems.includes(system)) { - return true; - } - } - return false; - } else if (filterB.notCollideWithSystems.length > 0) { - for (const system of filterB.notCollideWithSystems) { - if (filterA.collisionSystems.includes(system)) { - return false; - } - } - return true; + applyPointImpulse(nodeIndex, impulse, position) { + if (!this.scene) { + return; } - return true; + const motionNode = this.nodeToMotion.get(nodeIndex); + if (!motionNode) { + return; + } + const actorEntry = this.nodeToActor.get(nodeIndex); + if (!actorEntry) { + return; + } + const actor = actorEntry.actor; + + const pxImpulse = new this.PhysX.PxVec3(...impulse); + const pxPosition = new this.PhysX.PxVec3(...position); + this.PhysX.PxRigidBodyExt.prototype.addForceAtPos( + actor, + pxImpulse, + pxPosition, + this.PhysX.PxForceModeEnum.eIMPULSE + ); + this.PhysX.destroy(pxImpulse); + this.PhysX.destroy(pxPosition); } - computeFilterData(gltf) { - // Default filter is sign bit - const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; - this.filterData = new Array(32).fill(0); - this.filterData[31] = Math.pow(2, 32) - 1; // Default filter with all bits set - let filterCount = filters?.length ?? 0; - if (filterCount > 31) { - filterCount = 31; - console.warn( - "PhysX supports a maximum of 31 collision filters. Additional filters will be ignored." - ); + rayCast(rayStart, rayEnd) { + const result = {}; + result.hitNodeIndex = -1; + if (!this.scene) { + return result; } + const origin = new this.PhysX.PxVec3(...rayStart); + const directionVec = vec3.create(); + vec3.subtract(directionVec, rayEnd, rayStart); + vec3.normalize(directionVec, directionVec); + const direction = new this.PhysX.PxVec3(...directionVec); + const maxDistance = vec3.distance(rayStart, rayEnd); - for (let i = 0; i < filterCount; i++) { - let bitMask = 0; - for (let j = 0; j < filterCount; j++) { - if ( - this.collidesWith(filters[i], filters[j]) && - this.collidesWith(filters[j], filters[i]) - ) { - bitMask |= 1 << j; - } + const hitBuffer = new this.PhysX.PxRaycastBuffer10(); + const hitFlags = new this.PhysX.PxHitFlags(this.PhysX.PxHitFlagEnum.eDEFAULT); + + const queryFilterData = new this.PhysX.PxQueryFilterData(); + queryFilterData.set_flags( + this.PhysX.PxQueryFlagEnum.eSTATIC | this.PhysX.PxQueryFlagEnum.eDYNAMIC + ); + + const hasHit = this.scene.raycast( + origin, + direction, + maxDistance, + hitBuffer, + hitFlags, + queryFilterData + ); + + this.PhysX.destroy(origin); + this.PhysX.destroy(direction); + this.PhysX.destroy(hitFlags); + this.PhysX.destroy(queryFilterData); + + if (hasHit) { + const hitCount = hitBuffer.getNbAnyHits(); + if (hitCount > 1) { + console.warn("Raycast hit multiple objects, only the first hit is returned."); } - this.filterData[i] = bitMask; + const hit = hitBuffer.getAnyHit(0); + const fraction = hit.distance / maxDistance; + const hitNormal = vec3.fromValues(hit.normal.x, hit.normal.y, hit.normal.z); + const hitNodeIndex = this.shapeToNode.get(hit.shape.ptr); + if (hitNodeIndex === undefined) { + return result; + } + return { + hitNodeIndex: hitNodeIndex, + hitFraction: fraction, + hitNormal: hitNormal + }; + } else { + return result; } } - createPhysXMaterial(gltfPhysicsMaterial) { - if (gltfPhysicsMaterial === undefined) { - return this.defaultMaterial; - } + //endregion - const physxMaterial = this.physics.createMaterial( - gltfPhysicsMaterial.staticFriction, - gltfPhysicsMaterial.dynamicFriction, - gltfPhysicsMaterial.restitution - ); - if (gltfPhysicsMaterial.frictionCombine !== undefined) { - physxMaterial.setFrictionCombineMode( - this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) - ); - } - if (gltfPhysicsMaterial.restitutionCombine !== undefined) { - physxMaterial.setRestitutionCombineMode( - this.mapCombineMode(gltfPhysicsMaterial.restitutionCombine) + //region Updates + + updateActorTransform(node) { + if (node.dirtyTransform) { + const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; + if (actor === undefined) { + return; + } + const pxPos = new this.PhysX.PxVec3( + node.worldTransform[12], + node.worldTransform[13], + node.worldTransform[14] ); + const pxRot = new this.PhysX.PxQuat(...node.worldQuaternion); + const pxTransform = new this.PhysX.PxTransform(pxPos, pxRot); + if (node?.extensions?.KHR_physics_rigid_bodies?.motion?.isKinematic) { + actor.setKinematicTarget(pxTransform); + } else { + actor.setGlobalPose(pxTransform); + } + this.PhysX.destroy(pxPos); + this.PhysX.destroy(pxRot); + this.PhysX.destroy(pxTransform); } - return physxMaterial; } - createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { - let word0 = null; - let word1 = null; - if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { - word0 = 1 << collisionFilter; - word1 = this.filterData[collisionFilter]; - } else { - // Default filter id is signed bit and all bits set to collide with everything - word0 = Math.pow(2, 31); - word1 = Math.pow(2, 32) - 1; + updatePhysicMaterials(gltf) { + const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; + if (materials === undefined) { + return; + } + for (let i = 0; i < materials.length; i++) { + const material = materials[i]; + if (material.isDirty()) { + const physXMaterial = this.physXMaterials[i]; + physXMaterial.setStaticFriction(material.staticFriction); + physXMaterial.setDynamicFriction(material.dynamicFriction); + physXMaterial.setRestitution(material.restitution); + } } - - additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_DISCRETE_CONTACT; - additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_CCD_CONTACT; - - return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); } - createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { - if (physXMaterial === undefined) { - if (glTFCollider?.physicsMaterial !== undefined) { - physXMaterial = this.physXMaterials[glTFCollider.physicsMaterial]; + updateMotion(actorNode) { + const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; + const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; + if (motion.animatedPropertyObjects.isKinematic.dirty) { + if (motion.isKinematic) { + const linearVelocity = actor.getLinearVelocity(); + motion.computedLinearVelocity = [ + linearVelocity.x, + linearVelocity.y, + linearVelocity.z + ]; + const angularVelocity = actor.getAngularVelocity(); + motion.computedAngularVelocity = [ + angularVelocity.x, + angularVelocity.y, + angularVelocity.z + ]; } else { - physXMaterial = this.defaultMaterial; + motion.computedLinearVelocity = undefined; + motion.computedAngularVelocity = undefined; } + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, !motion.isKinematic); + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, motion.isKinematic); } - const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); - - if (physXFilterData === undefined) { - physXFilterData = - this.physXFilterData[ - glTFCollider?.collisionFilter ?? this.physXFilterData.length - 1 - ]; + if (motion.animatedPropertyObjects.mass.dirty) { + actor.setMass(motion.mass); + } + if ( + motion.animatedPropertyObjects.centerOfMass.dirty || + motion.animatedPropertyObjects.inertiaOrientation.dirty || + motion.animatedPropertyObjects.inertiaDiagonal.dirty + ) { + this.calculateMassAndInertia(motion, actor); + } + if (motion.animatedPropertyObjects.gravityFactor.dirty) { + actor.setActorFlag( + this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, + motion.gravityFactor !== 1.0 + ); + } + if (motion.animatedPropertyObjects.linearVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.linearVelocity); + actor.setLinearVelocity(pxVelocity); + motion.computedLinearVelocity = undefined; + } + if (motion.animatedPropertyObjects.angularVelocity.dirty) { + const pxVelocity = new this.PhysX.PxVec3(...motion.angularVelocity); + actor.setAngularVelocity(pxVelocity); + motion.computedAngularVelocity = undefined; } - - shape.setSimulationFilterData(physXFilterData); - - return shape; } - createShape( + updateCollider( gltf, node, collider, - shapeFlags, - physXMaterial, - physXFilterData, - convexHull, - scale = vec3.fromValues(1, 1, 1), - scaleAxis = quat.create() + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger ) { - let geometry = undefined; - if (collider?.geometry?.shape !== undefined) { - if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { - const simpleShape = - gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; - geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); - } else { - geometry = this.simpleShapes[collider.geometry.shape]; - } - } else if (collider?.geometry?.mesh !== undefined) { - const mesh = gltf.meshes[collider.geometry.mesh]; - if (convexHull === true) { - geometry = this.createConvexMesh(gltf, mesh, scale, scaleAxis); - } else { - geometry = this.createPxMesh(gltf, mesh, scale, scaleAxis); - } - } - - if (geometry === undefined) { - return undefined; - } - - const shape = this.createShapeFromGeometry( - geometry, - physXMaterial, - physXFilterData, - shapeFlags, - collider - ); - - this.shapeToNode.set(shape.ptr, node.gltfObjectIndex); - return shape; - } - - createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { - const worldTransform = node.worldTransform; - const translation = vec3.create(); - mat4.getTranslation(translation, worldTransform); - const pos = new this.PhysX.PxVec3(...translation); - const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); - const pose = new this.PhysX.PxTransform(pos, rotation); - let actor = null; - const pxShapeMap = new Map(); - if (type === "static" || type === "trigger") { - actor = this.physics.createRigidStatic(pose); - } else { - actor = this.physics.createRigidDynamic(pose); - if (type === "kinematic") { - actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); - } - actor.setRigidBodyFlag( - this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, - type !== "kinematic" - ); - const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion) { - const gltfAngularVelocity = motion?.angularVelocity; - const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); - actor.setAngularVelocity(angularVelocity, true); - this.PhysX.destroy(angularVelocity); - - const gltfLinearVelocity = motion?.linearVelocity; - const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); - actor.setLinearVelocity(linearVelocity, true); - this.PhysX.destroy(linearVelocity); - - if (motion.mass !== undefined) { - actor.setMass(motion.mass); - } - - this.calculateMassAndInertia(motion, actor); + const result = this.nodeToActor.get(actorNode.gltfObjectIndex); + const actor = result?.actor; + const currentShape = result?.pxShapeMap.get(node.gltfObjectIndex); - if (motion.gravityFactor !== 1.0) { - actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); + let currentGeometry = currentShape.getGeometry(); + const currentColliderType = currentGeometry.getType(); + const shapeIndex = collider?.geometry?.shape; + let scale = vec3.fromValues(1, 1, 1); + let scaleAxis = quat.create(); + if (shapeIndex !== undefined) { + // Simple shapes need to be recreated if scale changed + // If properties changed we also need to recreate the mesh colliders + const dirty = gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex].isDirty(); + if ( + scaleChanged && + !dirty && + currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH + ) { + // Update convex mesh scale + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + const result = PhysicsUtils.calculateScaleAndAxis(node); + scale = result.scale; + scaleAxis = result.scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); + } else if (dirty || scaleChanged) { + // Recreate simple shape collider + const newGeometry = this.generateSimpleShape( + gltf.extensions.KHR_implicit_shapes.shapes[shapeIndex], + scale, + scaleAxis + ); + currentGeometry.release?.(); + if (newGeometry.getType() !== currentColliderType) { + // We need to recreate the shape + let shapeFlags = undefined; + if (isTrigger) { + shapeFlags = this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE; + } else { + shapeFlags = this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE; + } + if (this.debugColliders) { + shapeFlags |= this.PhysX.PxShapeFlagEnum.eVISUALIZATION; + } + const shape = this.createShapeFromGeometry( + newGeometry, + undefined, + undefined, + shapeFlags, + collider + ); + result?.pxShapeMap.set(node.gltfObjectIndex, shape); + actor.detachShape(currentShape); + actor.attachShape(shape); + this.PhysX.destroy(currentShape); + } else { + currentShape.setGeometry(newGeometry); } } - } - - const createAndAddShape = ( - gltf, - node, - collider, - actorNode, - worldTransform, - offsetChanged, - scaleChanged, - isTrigger, - noMeshShapes, - shapeFlags, - triggerFlags - ) => { + } else if (collider?.geometry?.mesh !== undefined) { + if (scaleChanged) { + if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eCONVEXMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxConvexMeshGeometry + ); + } else if (currentColliderType === this.PhysX.PxGeometryTypeEnum.eTRIANGLEMESH) { + currentGeometry = this.PhysX.castObject( + currentGeometry, + this.PhysX.PxTriangleMeshGeometry + ); + } + // apply scale + const result = PhysicsUtils.calculateScaleAndAxis(node); + scale = result.scale; + scaleAxis = result.scaleAxis; + const pxScale = new this.PhysX.PxVec3(...scale); + const pxRotation = new this.PhysX.PxQuat(...scaleAxis); + const meshScale = new this.PhysX.PxMeshScale(pxScale, pxRotation); + currentGeometry.scale = meshScale; + this.PhysX.destroy(pxScale); + this.PhysX.destroy(pxRotation); + } + } + if (offsetChanged) { // Calculate offset position const translation = vec3.create(); const shapePosition = vec3.create(); @@ -962,1123 +865,1244 @@ class NvidiaPhysicsInterface extends PhysicsInterface { const rotation = quat.create(); quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); - // Calculate scale and scaleAxis - const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); - - const materialIndex = collider?.physicsMaterial; - const material = - materialIndex !== undefined - ? this.physXMaterials[materialIndex] - : this.defaultMaterial; - - const physXFilterData = - collider?.collisionFilter !== undefined - ? this.physXFilterData[collider.collisionFilter] - : this.physXFilterData[this.physXFilterData.length - 1]; - - const shape = this.createShape( - gltf, - node, - collider, - isTrigger ? triggerFlags : shapeFlags, - material, - physXFilterData, - noMeshShapes || collider?.geometry?.convexHull === true, - scale, - scaleAxis - ); - - if (shape !== undefined) { - const PxPos = new this.PhysX.PxVec3(...translation); - const PxRotation = new this.PhysX.PxQuat(...rotation); - const pose = new this.PhysX.PxTransform(PxPos, PxRotation); - shape.setLocalPose(pose); - - actor.attachShape(shape); - pxShapeMap.set(node.gltfObjectIndex, shape); - this.PhysX.destroy(PxPos); - this.PhysX.destroy(PxRotation); - this.PhysX.destroy(pose); - } - }; - - // If a node contains trigger and collider combine them + const PxPos = new this.PhysX.PxVec3(...translation); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + currentShape.setLocalPose(pose); + } + } - let collider = undefined; - if (type !== "trigger") { - collider = node.extensions?.KHR_physics_rigid_bodies?.collider; - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - false, - noMeshShapes, - shapeFlags, - triggerFlags - ); - collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; - if (collider !== undefined) { - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - true, - true, - shapeFlags, - triggerFlags - ); - } - } else { - collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; - createAndAddShape( - gltf, - node, - collider, - node, - worldTransform, - undefined, - undefined, - true, - true, - shapeFlags, - triggerFlags + updatePhysicsJoint(state, jointNode) { + const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); + if (pxJoints === undefined) { + return; + } + const gltfJoint = + state.gltf.extensions.KHR_physics_rigid_bodies.physicsJoints[ + jointNode.extensions.KHR_physics_rigid_bodies.joint.joint + ]; + const simplifiedJoints = gltfJoint.simplifiedPhysicsJoints; + if (simplifiedJoints.length !== pxJoints.length) { + console.warn( + "Number of simplified joints does not match number of PhysX joints. Skipping joint update." ); + return; } - - if (type !== "trigger") { - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - PhysicsUtils.recurseCollider( - gltf, - childNode, - undefined, - node, - node.dirtyScale, - node.dirtyScale, - createAndAddShape, - [noMeshShapes, shapeFlags, triggerFlags] + for (let i = 0; i < simplifiedJoints.length; i++) { + const pxJoint = pxJoints[i]; + const simplifiedJoint = simplifiedJoints[i]; + if ( + jointNode.extensions.KHR_physics_rigid_bodies.joint.animatedPropertyObjects + .enableCollision.dirty + ) { + pxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + jointNode.extensions.KHR_physics_rigid_bodies.joint.enableCollision ); } - } - - this.PhysX.destroy(pos); - this.PhysX.destroy(rotation); - this.PhysX.destroy(pose); - - this.scene.addActor(actor); - - this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); - } - - computeJointOffsetAndActor(node, referencedJoint) { - let currentNode = node; - while (currentNode !== undefined) { - if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { - break; + for (const limit of simplifiedJoint.limits) { + if ( + limit.animatedPropertyObjects.min.dirty || + limit.animatedPropertyObjects.max.dirty || + limit.animatedPropertyObjects.stiffness.dirty || + limit.animatedPropertyObjects.damping.dirty + ) { + this._setLimitValues(pxJoint, simplifiedJoint, limit); + } } - currentNode = currentNode.parentNode; - } - - const nodeWorldRot = node.worldQuaternion; - const localPhysXRot = referencedJoint?.localRotation; - if (localPhysXRot !== undefined) { - quat.multiply(nodeWorldRot, node.worldQuaternion, localPhysXRot); - } - - if (currentNode === undefined) { - const pos = vec3.create(); - mat4.getTranslation(pos, node.worldTransform); - - return { actor: undefined, offsetPosition: pos, offsetRotation: nodeWorldRot }; - } - const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; - const inverseActorRotation = quat.create(); - quat.invert(inverseActorRotation, currentNode.worldQuaternion); - const offsetRotation = quat.create(); - quat.multiply(offsetRotation, inverseActorRotation, nodeWorldRot); - - const actorPosition = vec3.create(); - mat4.getTranslation(actorPosition, currentNode.worldTransform); - const nodePosition = vec3.create(); - mat4.getTranslation(nodePosition, node.worldTransform); - const offsetPosition = vec3.create(); - vec3.subtract(offsetPosition, nodePosition, actorPosition); - vec3.transformQuat(offsetPosition, offsetPosition, inverseActorRotation); - return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; - } + if ( + simplifiedJoint.twistLimit && + (simplifiedJoint.twistLimit.animatedPropertyObjects.min.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.max.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.twistLimit.animatedPropertyObjects.damping.dirty) + ) { + this._setTwistLimitValues(pxJoint, simplifiedJoint); + } - convertAxisIndexToEnum(axisIndex, type) { - if (type === "linear") { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6AxisEnum.eX; - case 1: - return this.PhysX.PxD6AxisEnum.eY; - case 2: - return this.PhysX.PxD6AxisEnum.eZ; + if ( + (simplifiedJoint.swingLimit1 && + (simplifiedJoint.swingLimit1.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit1.animatedPropertyObjects.damping.dirty)) || + (simplifiedJoint.swingLimit2 && + (simplifiedJoint.swingLimit2.animatedPropertyObjects.min.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.max.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.stiffness.dirty || + simplifiedJoint.swingLimit2.animatedPropertyObjects.damping.dirty)) + ) { + this._setSwingLimitValues(pxJoint, simplifiedJoint); } - } else if (type === "angular") { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6AxisEnum.eTWIST; - case 1: - return this.PhysX.PxD6AxisEnum.eSWING1; - case 2: - return this.PhysX.PxD6AxisEnum.eSWING2; + + let positionTargetDirty = false; + let velocityTargetDirty = false; + const linearVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + const angularVelocityTarget = new this.PhysX.PxVec3(0, 0, 0); + pxJoint.getDriveVelocity(linearVelocityTarget, angularVelocityTarget); + for (const drive of simplifiedJoint.drives) { + if ( + drive.animatedPropertyObjects.stiffness.dirty || + drive.animatedPropertyObjects.damping.dirty || + drive.animatedPropertyObjects.maxForce.dirty + ) { + this._setDriveValues(pxJoint, simplifiedJoint, drive); + } + if (drive.animatedPropertyObjects.velocityTarget.dirty) { + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); + velocityTargetDirty = true; + } + if (drive.animatedPropertyObjects.positionTarget.dirty) { + positionTargetDirty = true; + } } - } - return null; - } - convertAxisIndexToAngularDriveEnum(axisIndex) { - switch (axisIndex) { - case 0: - return this.PhysX.PxD6DriveEnum.eTWIST; - case 1: - return 6; // Currently not exposed via bindings - case 2: - return 7; // Currently not exposed via bindings + if (positionTargetDirty) { + this._setDrivePositionTarget(pxJoint, simplifiedJoint); + } + if (velocityTargetDirty) { + pxJoint.setDriveVelocity(linearVelocityTarget, angularVelocityTarget); + } } - return null; } - validateSwingLimits(joint) { - // Check if swing limits are symmetric (cone) or asymmetric (pyramid) - if (joint.swingLimit1 && joint.swingLimit2) { - const limit1 = joint.swingLimit1; - const limit2 = joint.swingLimit2; - - const isSymmetric1 = - Math.abs(limit1.min + limit1.max) < 1e-6 || limit1.min === undefined; // Centered around 0 - const isSymmetric2 = - Math.abs(limit2.min + limit2.max) < 1e-6 || limit2.min === undefined; - - // Return if this is a cone limit (symmetric and same range) vs pyramid limit - return isSymmetric1 && isSymmetric2; - } - return false; - } + //endregion - createJoint(gltf, node) { - const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; - const referencedJoint = - gltf.extensions?.KHR_physics_rigid_bodies?.physicsJoints[joint.joint]; + //region Geometry - if (referencedJoint === undefined) { - console.error("Referenced joint not found:", joint.joint); - return; + // Either create a box or update an existing one. Returns only newly created geometry + generateBox(x, y, z, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); } - const simplifiedJoints = []; - for (const simplifiedJoint of referencedJoint.simplifiedPhysicsJoints) { - const physxJoint = this.createSimplifiedJoint(gltf, node, joint, simplifiedJoint); - simplifiedJoints.push(physxJoint); + if ( + scale.every((value) => value === scale[0]) === false && + quat.equals(scaleAxis, quat.create()) === false + ) { + const data = createBoxVertexData(x, y, z); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } - this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); - } - - _setLimitValues(physxJoint, simplifiedJoint, limit) { - const lock = limit.min === 0 && limit.max === 0; - const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); - const isDistanceLimit = - limit.linearAxes && - limit.linearAxes.length === 3 && - (limit.min === undefined || limit.min === 0) && - limit.max !== 0; - if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { - const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( - limit.min ?? -this.MAX_FLOAT, - limit.max ?? this.MAX_FLOAT, - spring + let geometry = undefined; + if (referenceType === this.PhysX.PxGeometryTypeEnum.eBOX) { + const halfExtents = new this.PhysX.PxVec3( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] ); - for (const axis of limit.linearAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setMotion( - physxAxis, - lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED - ); - if (!lock) { - physxJoint.setLinearLimit(physxAxis, linearLimitPair); - } - } - this.PhysX.destroy(linearLimitPair); - } - if (isDistanceLimit) { - const linearLimit = new this.PhysX.PxJointLinearLimit( - limit.max ?? this.MAX_FLOAT, - spring + reference.halfExtents = halfExtents; + this.PhysX.destroy(halfExtents); + } else { + geometry = new this.PhysX.PxBoxGeometry( + (x / 2) * scale[0], + (y / 2) * scale[1], + (z / 2) * scale[2] ); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eLIMITED); - physxJoint.setDistanceLimit(linearLimit); - this.PhysX.destroy(linearLimit); - } - if (limit.angularAxes && limit.angularAxes.length > 0) { - for (const axis of limit.angularAxes) { - const result = simplifiedJoint.getRotatedAxisAndSign(axis); - const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); - physxJoint.setMotion( - physxAxis, - lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED - ); - } } - this.PhysX.destroy(spring); + + return geometry; } - _setTwistLimitValues(physxJoint, simplifiedJoint) { - if (simplifiedJoint.twistLimit !== undefined) { - if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { - const limitPair = new this.PhysX.PxJointAngularLimitPair( - simplifiedJoint.twistLimit.min ?? -Math.PI, - simplifiedJoint.twistLimit.max ?? Math.PI, - new this.PhysX.PxSpring( - simplifiedJoint.twistLimit.stiffness ?? 0, - simplifiedJoint.twistLimit.damping - ) - ); - physxJoint.setTwistLimit(limitPair); - this.PhysX.destroy(limitPair); - } - } + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { + const data = createCapsuleVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } - _setSwingLimitValues(physxJoint, simplifiedJoint) { + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { if ( - simplifiedJoint.swingLimit1 !== undefined && - simplifiedJoint.swingLimit2 !== undefined - ) { - if ( - simplifiedJoint.swingLimit1.stiffness !== simplifiedJoint.swingLimit2.stiffness || - simplifiedJoint.swingLimit1.damping !== simplifiedJoint.swingLimit2.damping - ) { - console.warn( - "PhysX does not support different stiffness/damping for swing limits." - ); - } else { - const spring = new this.PhysX.PxSpring( - simplifiedJoint.swingLimit1.stiffness ?? 0, - simplifiedJoint.swingLimit1.damping - ); - let yMin = -Math.PI / 2; - let yMax = Math.PI / 2; - let zMin = -Math.PI / 2; - let zMax = Math.PI / 2; - if (simplifiedJoint.swingLimit1.min !== undefined) { - yMin = simplifiedJoint.swingLimit1.min; - } - if (simplifiedJoint.swingLimit1.max !== undefined) { - yMax = simplifiedJoint.swingLimit1.max; - } - if (simplifiedJoint.swingLimit2.min !== undefined) { - zMin = simplifiedJoint.swingLimit2.min; - } - if (simplifiedJoint.swingLimit2.max !== undefined) { - zMax = simplifiedJoint.swingLimit2.max; - } - - const isSymmetric = this.validateSwingLimits(simplifiedJoint); - if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { - // Fixed limit is already set - } else if (isSymmetric) { - const swing1Angle = Math.max(Math.abs(yMin), Math.abs(yMax)); - const swing2Angle = Math.max(Math.abs(zMin), Math.abs(zMax)); - const jointLimitCone = new this.PhysX.PxJointLimitCone( - swing1Angle, - swing2Angle, - spring - ); - physxJoint.setSwingLimit(jointLimitCone); - this.PhysX.destroy(jointLimitCone); - } else { - const jointLimitCone = new this.PhysX.PxJointLimitPyramid( - yMin, - yMax, - zMin, - zMax, - spring - ); - physxJoint.setPyramidSwingLimit(jointLimitCone); - this.PhysX.destroy(jointLimitCone); - } - this.PhysX.destroy(spring); - } - } else if ( - simplifiedJoint.swingLimit1 !== undefined || - simplifiedJoint.swingLimit2 !== undefined + (quat.equals(scaleAxis, quat.create()) === false && + scale.every((value) => value === scale[0]) === false) || + radiusTop !== radiusBottom || + scale[0] !== scale[2] ) { - const singleLimit = simplifiedJoint.swingLimit1 ?? simplifiedJoint.swingLimit2; - if (singleLimit.min === 0 && singleLimit.max === 0) { - // Fixed limit is already set - } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { - console.warn( - "PhysX requires symmetric limits for swing limits in single axis mode." - ); - } else { - const spring = new this.PhysX.PxSpring( - singleLimit.stiffness ?? 0, - singleLimit.damping - ); - const maxY = simplifiedJoint.swingLimit1?.max ?? Math.PI; - const maxZ = simplifiedJoint.swingLimit2?.max ?? Math.PI; - const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); - physxJoint.setSwingLimit(jointLimitCone); - this.PhysX.destroy(spring); - this.PhysX.destroy(jointLimitCone); - } + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } + height *= scale[1]; + radiusTop *= scale[0]; + radiusBottom *= scale[0]; + const data = createCylinderVertexData(radiusTop, radiusBottom, height); + return this.createConvexPxMesh(data.vertices); } - _setDriveValues(physxJoint, simplifiedJoint, drive) { - const physxDrive = new this.PhysX.PxD6JointDrive( - drive.stiffness, - drive.damping, - drive.maxForce ?? this.MAX_FLOAT, - drive.mode === "acceleration" - ); - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - const axis = this.convertAxisIndexToEnum(result.axis, "linear"); - physxJoint.setDrive(axis, physxDrive); - } else if (drive.type === "angular") { - const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); - physxJoint.setDrive(axis, physxDrive); + generateSphere(radius, scale, scaleAxis, reference) { + let referenceType = undefined; + if (reference !== undefined) { + referenceType = reference.getType(); + } + if (scale.every((value) => value === scale[0]) === false) { + const data = createCapsuleVertexData(radius, radius, 0); + return this.createConvexPxMesh(data.vertices, scale, scaleAxis); + } else { + radius *= scale[0]; } - this.PhysX.destroy(physxDrive); + if (referenceType === this.PhysX.PxGeometryTypeEnum.eSPHERE) { + reference.radius = radius; + return undefined; + } + const geometry = new this.PhysX.PxSphereGeometry(radius); + return geometry; } - _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - if (drive.velocityTarget !== undefined) { - linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; - } - } else if (drive.type === "angular") { - if (drive.velocityTarget !== undefined) { - angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation - } + generatePlane(reference) { + if (reference !== undefined) { + // Nothing to update + return undefined; } + const geometry = new this.PhysX.PxPlaneGeometry(); + return geometry; } - _setDrivePositionTarget(physxJoint, simplifiedJoint) { - const positionTarget = vec3.fromValues(0, 0, 0); - const angleTarget = quat.create(); - for (const drive of simplifiedJoint.drives) { - const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); - if (drive.type === "linear") { - if (drive.positionTarget !== undefined) { - positionTarget[result.axis] = drive.positionTarget * result.sign; - } - } else if (drive.type === "angular") { - if (drive.positionTarget !== undefined) { - // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise - switch (result.axis) { - case 0: { - quat.rotateX( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign - ); - break; - } - case 1: { - quat.rotateY( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign + createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxConvexMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); + + let flag = 0; + flag |= this.PhysX.PxConvexFlagEnum.eCOMPUTE_CONVEX; + flag |= this.PhysX.PxConvexFlagEnum.eSHIFT_VERTICES; + //flag |= this.PhysX.PxConvexFlagEnum.eDISABLE_MESH_VALIDATION; + const pxflags = new this.PhysX.PxConvexFlags(flag); + des.flags = pxflags; + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + cookingParams.planeTolerance = 0.0007; //Default + const tri = this.PhysX.CreateConvexMesh(cookingParams, des); + this.convexMeshes.push(tri); + + const PxScale = new this.PhysX.PxVec3(scale[0], scale[1], scale[2]); + const PxQuat = new this.PhysX.PxQuat(...scaleAxis); + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxConvexMeshGeometryFlags(); + const geometry = new this.PhysX.PxConvexMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(pxflags); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } + + collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { + let positionDataArray = []; + let positionCount = 0; + let indexDataArray = []; + let indexCount = 0; + + for (const primitive of mesh.primitives) { + const positionAccessor = gltf.accessors[primitive.attributes.POSITION]; + const positionData = positionAccessor.getNormalizedDeinterlacedView(gltf); + + if (primitive.targets !== undefined) { + let morphWeights = mesh.weights; + if (morphWeights !== undefined) { + // Calculate morphed vertex positions on CPU + const morphPositionData = []; + for (const target of primitive.targets) { + if (target.POSITION !== undefined) { + const morphAccessor = gltf.accessors[target.POSITION]; + morphPositionData.push( + morphAccessor.getNormalizedDeinterlacedView(gltf) ); - break; + } else { + morphPositionData.push(undefined); } - case 2: { - quat.rotateZ( - angleTarget, - angleTarget, - -drive.positionTarget * result.sign - ); - break; + } + for (let i = 0; i < positionData.length / 3; i++) { + for (let j = 0; j < morphWeights.length; j++) { + const morphData = morphPositionData[j]; + if (morphWeights[j] === 0 || morphData === undefined) { + continue; + } + positionData[i * 3] += morphData[i * 3] * morphWeights[j]; + positionData[i * 3 + 1] += morphData[i * 3 + 1] * morphWeights[j]; + positionData[i * 3 + 2] += morphData[i * 3 + 2] * morphWeights[j]; } } } } - } - const posTarget = new this.PhysX.PxVec3(...positionTarget); - const rotTarget = new this.PhysX.PxQuat(...angleTarget); - const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); - physxJoint.setDrivePosition(targetTransform); - this.PhysX.destroy(posTarget); - this.PhysX.destroy(rotTarget); - this.PhysX.destroy(targetTransform); - } - createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { - const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); - const resultB = this.computeJointOffsetAndActor( - gltf.nodes[joint.connectedNode], - simplifiedJoint - ); + positionDataArray.push(positionData); + positionCount += positionAccessor.count; + if (computeIndices) { + let indexData = undefined; + if (primitive.indices !== undefined) { + const indexAccessor = gltf.accessors[primitive.indices]; + indexData = indexAccessor.getNormalizedDeinterlacedView(gltf); + } else { + const array = Array.from(Array(positionAccessor.count).keys()); + indexData = new Uint32Array(array); + } + if (primitive.mode === 5) { + indexData = PhysicsUtils.convertTriangleStripToTriangles(indexData); + } else if (primitive.mode === 6) { + indexData = PhysicsUtils.convertTriangleFanToTriangles(indexData); + } else if (primitive.mode !== undefined && primitive.mode !== 4) { + console.warn( + "Unsupported primitive mode for physics mesh collider creation: " + + primitive.mode + ); + } + indexDataArray.push(indexData); + indexCount += indexData.length; + } + } - const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); - const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); - const poseA = new this.PhysX.PxTransform(pos, rot); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); + const positionData = new Float32Array(positionCount * 3); + const indexData = new Uint32Array(indexCount); + let offset = 0; + for (const positionChunk of positionDataArray) { + positionData.set(positionChunk, offset); + offset += positionChunk.length; + } + offset = 0; + for (const indexChunk of indexDataArray) { + indexData.set(indexChunk, offset); + offset += indexChunk.length; + } + return { vertices: positionData, indices: indexData }; + } - const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); - const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); - const poseB = new this.PhysX.PxTransform(posB, rotB); - this.PhysX.destroy(posB); - this.PhysX.destroy(rotB); + createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); + return this.createConvexPxMesh(result.vertices, scale, scaleAxis); + } - const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( - this.physics, - resultA.actor, - poseA, - resultB.actor, - poseB - ); - this.PhysX.destroy(poseA); - this.PhysX.destroy(poseB); + createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { + const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); + const malloc = (f, q) => { + const nDataBytes = f.length * f.BYTES_PER_ELEMENT; + if (q === undefined) q = this.PhysX._webidl_malloc(nDataBytes); + let dataHeap = new Uint8Array(this.PhysX.HEAPU8.buffer, q, nDataBytes); + dataHeap.set(new Uint8Array(f.buffer)); + return q; + }; + const des = new this.PhysX.PxTriangleMeshDesc(); + des.points.stride = vertices.BYTES_PER_ELEMENT * 3; + des.points.count = vertices.length / 3; + des.points.data = malloc(vertices); - physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); + des.triangles.stride = indices.BYTES_PER_ELEMENT * 3; + des.triangles.count = indices.length / 3; + des.triangles.data = malloc(indices); - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); + const cookingParams = new this.PhysX.PxCookingParams(this.tolerances); + const tri = this.PhysX.CreateTriangleMesh(cookingParams, des); + this.triangleMeshes.push(tri); - physxJoint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, - joint.enableCollision - ); + const PxScale = new this.PhysX.PxVec3(1, 1, 1); + const PxQuat = new this.PhysX.PxQuat(0, 0, 0, 1); - // Do not restict any axis by default - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); - physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); + if (scale !== undefined) { + PxScale.x = scale[0]; + PxScale.y = scale[1]; + PxScale.z = scale[2]; + } + if (scaleAxis !== undefined) { + PxQuat.x = scaleAxis[0]; + PxQuat.y = scaleAxis[1]; + PxQuat.z = scaleAxis[2]; + PxQuat.w = scaleAxis[3]; + } + const ms = new this.PhysX.PxMeshScale(PxScale, PxQuat); + const f = new this.PhysX.PxMeshGeometryFlags(); + const geometry = new this.PhysX.PxTriangleMeshGeometry(tri, ms, f); + this.PhysX.destroy(PxScale); + this.PhysX.destroy(PxQuat); + this.PhysX.destroy(ms); + this.PhysX.destroy(cookingParams); + this.PhysX.destroy(des); + return geometry; + } - for (const limit of simplifiedJoint.limits) { - this._setLimitValues(physxJoint, simplifiedJoint, limit); + collidesWith(filterA, filterB) { + if (filterB.collideWithSystems.length > 0) { + for (const system of filterB.collideWithSystems) { + if (filterA.collisionSystems.includes(system)) { + return true; + } + } + return false; + } else if (filterB.notCollideWithSystems.length > 0) { + for (const system of filterB.notCollideWithSystems) { + if (filterA.collisionSystems.includes(system)) { + return false; + } + } + return true; } + return true; + } - this._setTwistLimitValues(physxJoint, simplifiedJoint); - this._setSwingLimitValues(physxJoint, simplifiedJoint); + //endregion - const linearVelocityTarget = vec3.fromValues(0, 0, 0); - const angularVelocityTarget = vec3.fromValues(0, 0, 0); + //region Shapes - for (const drive of simplifiedJoint.drives) { - this._setDriveValues(physxJoint, simplifiedJoint, drive); - this._getDriveVelocityTarget( - simplifiedJoint, - drive, - linearVelocityTarget, - angularVelocityTarget + computeFilterData(gltf) { + // Default filter is sign bit + const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; + this.filterData = new Array(32).fill(0); + this.filterData[31] = Math.pow(2, 32) - 1; // Default filter with all bits set + let filterCount = filters?.length ?? 0; + if (filterCount > 31) { + filterCount = 31; + console.warn( + "PhysX supports a maximum of 31 collision filters. Additional filters will be ignored." ); } - this._setDrivePositionTarget(physxJoint, simplifiedJoint); - - const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); - const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); - physxJoint.setDriveVelocity(linVel, angVel); - this.PhysX.destroy(linVel); - this.PhysX.destroy(angVel); - return physxJoint; - } - - changeDebugVisualization() { - if (!this.scene || !this.debugStateChanged) { - return; - } - this.debugStateChanged = false; - this.scene.setVisualizationParameter( - this.PhysX.eSCALE, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eWORLD_AXES, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eACTOR_AXES, - this.debugColliders || this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eCOLLISION_SHAPES, - this.debugColliders ? 1 : 0 - ); - this.scene.setVisualizationParameter( - this.PhysX.eJOINT_LOCAL_FRAMES, - this.debugJoints ? 1 : 0 - ); - this.scene.setVisualizationParameter(this.PhysX.eJOINT_LIMITS, this.debugJoints ? 1 : 0); - for (const joints of this.nodeToSimplifiedJoints.values()) { - for (const joint of joints) { - joint.setConstraintFlag( - this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, - this.debugJoints - ); + for (let i = 0; i < filterCount; i++) { + let bitMask = 0; + for (let j = 0; j < filterCount; j++) { + if ( + this.collidesWith(filters[i], filters[j]) && + this.collidesWith(filters[j], filters[i]) + ) { + bitMask |= 1 << j; + } } - } - for (const shapePtr of this.shapeToNode.keys()) { - const shape = this.PhysX.wrapPointer(shapePtr, this.PhysX.PxShape); - shape.setFlag(this.PhysX.PxShapeFlagEnum.eVISUALIZATION, this.debugColliders); + this.filterData[i] = bitMask; } } - initializeSimulation( - state, - staticActors, - kinematicActors, - dynamicActors, - jointNodes, - triggerNodes, - independentTriggerNodes, - nodeToMotion, - _hasRuntimeAnimationTargets, - _staticMeshColliderCount, - _dynamicMeshColliderCount - ) { - if (!this.PhysX) { - return; - } - this.nodeToMotion = nodeToMotion; - this.generateSimpleShapes(state.gltf); - this.computeFilterData(state.gltf); - for (let i = 0; i < this.filterData.length; i++) { - const physXFilterData = this.createPhysXCollisionFilter(i); - this.physXFilterData.push(physXFilterData); - } - - const materials = state.gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; - if (materials !== undefined) { - for (const gltfMaterial of materials) { - const physxMaterial = this.createPhysXMaterial(gltfMaterial); - this.physXMaterials.push(physxMaterial); - } + createPhysXMaterial(gltfPhysicsMaterial) { + if (gltfPhysicsMaterial === undefined) { + return this.defaultMaterial; } - const tmpVec = new this.PhysX.PxVec3(0, -9.81, 0); - const sceneDesc = new this.PhysX.PxSceneDesc(this.tolerances); - sceneDesc.set_gravity(tmpVec); - sceneDesc.set_cpuDispatcher(this.PhysX.DefaultCpuDispatcherCreate(0)); - sceneDesc.set_filterShader(this.PhysX.DefaultFilterShader()); - const sceneFlags = new this.PhysX.PxSceneFlags( - this.PhysX.PxSceneFlagEnum.eENABLE_CCD | this.PhysX.PxSceneFlagEnum.eENABLE_PCM + const physxMaterial = this.physics.createMaterial( + gltfPhysicsMaterial.staticFriction, + gltfPhysicsMaterial.dynamicFriction, + gltfPhysicsMaterial.restitution ); - sceneDesc.flags = sceneFlags; - - this.scene = this.physics.createScene(sceneDesc); - let triggerCallback = undefined; - - if (triggerNodes.length > 0) { - console.log("Enabling trigger report callback"); - triggerCallback = new this.PhysX.PxSimulationEventCallbackImpl(); - triggerCallback.onTrigger = (pairs, count) => { - for (const compoundTrigger of state.physicsController.compoundTriggerNodes.values()) { - compoundTrigger.added.clear(); - compoundTrigger.removed.clear(); - } - console.log("Trigger callback called with", count, "pairs"); - for (let i = 0; i < count; i++) { - const pair = this.PhysX.NativeArrayHelpers.prototype.getTriggerPairAt(pairs, i); - const triggerShape = pair.triggerShape; - const otherShape = pair.otherShape; - const triggerNodeIndex = this.shapeToNode.get(triggerShape.ptr); - const otherNodeIndex = this.shapeToNode.get(otherShape.ptr); - if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { - state.graphController.rigidBodyTriggerEntered( - triggerNodeIndex, - otherNodeIndex, - nodeToMotion.get(otherNodeIndex) - ); - } else if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST) { - state.graphController.rigidBodyTriggerExited( - triggerNodeIndex, - otherNodeIndex, - nodeToMotion.get(otherNodeIndex) - ); - } - const compoundTriggers = - state.physicsController.triggerToCompound.get(triggerNodeIndex); - if (compoundTriggers !== undefined) { - for (const compoundTriggerIndex of compoundTriggers) { - const compoundTriggerInfo = - state.physicsController.compoundTriggerNodes.get( - compoundTriggerIndex - ); - if (pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_FOUND) { - compoundTriggerInfo.added.add(otherNodeIndex); - } else if ( - pair.status === this.PhysX.PxPairFlagEnum.eNOTIFY_TOUCH_LOST - ) { - compoundTriggerInfo.removed.add(otherNodeIndex); - } - } - } - } - - for (const [ - idx, - compoundTrigger - ] of state.physicsController.compoundTriggerNodes.entries()) { - for (const addedNodeIndex of compoundTrigger.added) { - if (!compoundTrigger.previous.has(addedNodeIndex)) { - compoundTrigger.previous.set(addedNodeIndex, 1); - state.graphController.rigidBodyTriggerEntered( - idx, - addedNodeIndex, - nodeToMotion.get(addedNodeIndex) - ); - } else { - const currentCount = compoundTrigger.previous.get(addedNodeIndex); - compoundTrigger.previous.set(addedNodeIndex, currentCount + 1); - } - } - for (const removedNodeIndex of compoundTrigger.removed) { - const currentCount = compoundTrigger.previous.get(removedNodeIndex); - if (currentCount > 1) { - compoundTrigger.previous.set(removedNodeIndex, currentCount - 1); - } else { - compoundTrigger.previous.delete(removedNodeIndex); - state.graphController.rigidBodyTriggerExited( - idx, - removedNodeIndex, - nodeToMotion.get(removedNodeIndex) - ); - } - } - } - }; + if (gltfPhysicsMaterial.frictionCombine !== undefined) { + physxMaterial.setFrictionCombineMode( + this.mapCombineMode(gltfPhysicsMaterial.frictionCombine) + ); + } + if (gltfPhysicsMaterial.restitutionCombine !== undefined) { + physxMaterial.setRestitutionCombineMode( + this.mapCombineMode(gltfPhysicsMaterial.restitutionCombine) + ); + } + return physxMaterial; + } - // All callbacks need to be defined - triggerCallback.onConstraintBreak = (_constraints, _count) => {}; - triggerCallback.onWake = (_actors, _count) => {}; - triggerCallback.onSleep = (_actors, _count) => {}; - triggerCallback.onContact = (_pairHeaders, _pairs, _count) => {}; - sceneDesc.simulationEventCallback = triggerCallback; + createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { + let word0 = null; + let word1 = null; + if (collisionFilter !== undefined && collisionFilter < this.filterData.length - 1) { + word0 = 1 << collisionFilter; + word1 = this.filterData[collisionFilter]; + } else { + // Default filter id is signed bit and all bits set to collide with everything + word0 = Math.pow(2, 31); + word1 = Math.pow(2, 32) - 1; } - this.scene = this.physics.createScene(sceneDesc); - - console.log("Created scene"); - const shapeFlags = new this.PhysX.PxShapeFlags( - this.PhysX.PxShapeFlagEnum.eSCENE_QUERY_SHAPE | - this.PhysX.PxShapeFlagEnum.eSIMULATION_SHAPE - ); + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_DISCRETE_CONTACT; + additionalFlags |= this.PhysX.PxPairFlagEnum.eDETECT_CCD_CONTACT; - const triggerFlags = new this.PhysX.PxShapeFlags(this.PhysX.PxShapeFlagEnum.eTRIGGER_SHAPE); + return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); + } - for (const node of staticActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "static"); - } - for (const node of kinematicActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "kinematic"); - } - for (const node of dynamicActors) { - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "dynamic", true); - } - for (const node of independentTriggerNodes) { - if ( - this.nodeToActor.has(node.gltfObjectIndex) || - this.nodeToMotion.has(node.gltfObjectIndex) - ) { - continue; + createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { + if (physXMaterial === undefined) { + if (glTFCollider?.physicsMaterial !== undefined) { + physXMaterial = this.physXMaterials[glTFCollider.physicsMaterial]; + } else { + physXMaterial = this.defaultMaterial; } - this.createActor(state.gltf, node, shapeFlags, triggerFlags, "trigger", true); } - for (const node of jointNodes) { - this.createJoint(state.gltf, node); + const shape = this.physics.createShape(geometry, physXMaterial, true, shapeFlags); + + if (physXFilterData === undefined) { + physXFilterData = + this.physXFilterData[ + glTFCollider?.collisionFilter ?? this.physXFilterData.length - 1 + ]; } - this.PhysX.destroy(tmpVec); - this.PhysX.destroy(sceneDesc); - this.PhysX.destroy(shapeFlags); - this.PhysX.destroy(triggerFlags); + shape.setSimulationFilterData(physXFilterData); - this.debugStateChanged = true; - this.changeDebugVisualization(); + return shape; } - enableDebugColliders(enable) { - this.debugColliders = enable; - this.debugStateChanged = true; - } + createShape( + gltf, + node, + collider, + shapeFlags, + physXMaterial, + physXFilterData, + convexHull, + scale = vec3.fromValues(1, 1, 1), + scaleAxis = quat.create() + ) { + let geometry = undefined; + if (collider?.geometry?.shape !== undefined) { + if (scale[0] !== 1 || scale[1] !== 1 || scale[2] !== 1) { + const simpleShape = + gltf.extensions.KHR_implicit_shapes.shapes[collider.geometry.shape]; + geometry = this.generateSimpleShape(simpleShape, scale, scaleAxis); + } else { + geometry = this.simpleShapes[collider.geometry.shape]; + } + } else if (collider?.geometry?.mesh !== undefined) { + const mesh = gltf.meshes[collider.geometry.mesh]; + if (convexHull === true) { + geometry = this.createConvexMesh(gltf, mesh, scale, scaleAxis); + } else { + geometry = this.createPxMesh(gltf, mesh, scale, scaleAxis); + } + } - enableDebugJoints(enable) { - this.debugJoints = enable; - this.debugStateChanged = true; + if (geometry === undefined) { + return undefined; + } + + const shape = this.createShapeFromGeometry( + geometry, + physXMaterial, + physXFilterData, + shapeFlags, + collider + ); + + this.shapeToNode.set(shape.ptr, node.gltfObjectIndex); + return shape; } - applyTransformRecursively(gltf, node, parentTransform) { - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { - return; - } - const localTransform = node.getLocalTransform(); - const globalTransform = mat4.create(); - mat4.multiply(globalTransform, parentTransform, localTransform); - node.scaledPhysicsTransform = globalTransform; - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - this.applyTransformRecursively(gltf, childNode, globalTransform); + mapCombineMode(mode) { + switch (mode) { + case "average": + return this.PhysX.PxCombineModeEnum.eAVERAGE; + case "minimum": + return this.PhysX.PxCombineModeEnum.eMIN; + case "maximum": + return this.PhysX.PxCombineModeEnum.eMAX; + case "multiply": + return this.PhysX.PxCombineModeEnum.eMULTIPLY; } } - subStepSimulation(state, deltaTime) { - // eslint-disable-next-line no-unused-vars - for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { - const node = state.gltf.nodes[nodeIndex]; - if (node.dirtyTransform) { - // Node transform is currently animated - continue; + //endregion + + //region Actors + + createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { + const worldTransform = node.worldTransform; + const translation = vec3.create(); + mat4.getTranslation(translation, worldTransform); + const pos = new this.PhysX.PxVec3(...translation); + const rotation = new this.PhysX.PxQuat(...node.worldQuaternion); + const pose = new this.PhysX.PxTransform(pos, rotation); + let actor = null; + const pxShapeMap = new Map(); + if (type === "static" || type === "trigger") { + actor = this.physics.createRigidStatic(pose); + } else { + actor = this.physics.createRigidDynamic(pose); + if (type === "kinematic") { + actor.setRigidBodyFlag(this.PhysX.PxRigidBodyFlagEnum.eKINEMATIC, true); } + actor.setRigidBodyFlag( + this.PhysX.PxRigidBodyFlagEnum.eENABLE_CCD, + type !== "kinematic" + ); const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion && motion.isKinematic) { - const linearVelocity = motion.computedLinearVelocity ?? motion.linearVelocity; - const angularVelocity = motion.computedAngularVelocity ?? motion.angularVelocity; - if (linearVelocity !== undefined || angularVelocity !== undefined) { - const worldTransform = node.physicsTransform ?? node.worldTransform; - const targetPosition = vec3.create(); - targetPosition[0] = worldTransform[12]; - targetPosition[1] = worldTransform[13]; - targetPosition[2] = worldTransform[14]; - let nodeRotation = quat.create(); - if (node.physicsTransform !== undefined) { - mat4.getRotation(nodeRotation, worldTransform); - } else { - nodeRotation = quat.clone(node.worldQuaternion); - } - if (linearVelocity !== undefined) { - const acceleration = vec3.create(); - vec3.scale(acceleration, linearVelocity, deltaTime); - vec3.transformQuat(acceleration, acceleration, nodeRotation); - targetPosition[0] += acceleration[0]; - targetPosition[1] += acceleration[1]; - targetPosition[2] += acceleration[2]; - } - if (angularVelocity !== undefined) { - // Transform angular velocity from local space to world space - // by rotating the velocity axes by the current node rotation. - const localX = vec3.fromValues(1, 0, 0); - const localY = vec3.fromValues(0, 1, 0); - const localZ = vec3.fromValues(0, 0, 1); - vec3.transformQuat(localX, localX, nodeRotation); - vec3.transformQuat(localY, localY, nodeRotation); - vec3.transformQuat(localZ, localZ, nodeRotation); + if (motion) { + const gltfAngularVelocity = motion?.angularVelocity; + const angularVelocity = new this.PhysX.PxVec3(...gltfAngularVelocity); + actor.setAngularVelocity(angularVelocity, true); + this.PhysX.destroy(angularVelocity); - const angularAcceleration = quat.create(); - const qX = quat.create(); - const qY = quat.create(); - const qZ = quat.create(); - quat.setAxisAngle(qX, localX, angularVelocity[0] * deltaTime); - quat.setAxisAngle(qY, localY, angularVelocity[1] * deltaTime); - quat.setAxisAngle(qZ, localZ, angularVelocity[2] * deltaTime); - quat.multiply(angularAcceleration, qX, angularAcceleration); - quat.multiply(angularAcceleration, qY, angularAcceleration); - quat.multiply(angularAcceleration, qZ, angularAcceleration); + const gltfLinearVelocity = motion?.linearVelocity; + const linearVelocity = new this.PhysX.PxVec3(...gltfLinearVelocity); + actor.setLinearVelocity(linearVelocity, true); + this.PhysX.destroy(linearVelocity); - quat.multiply(nodeRotation, angularAcceleration, nodeRotation); - } - const pos = new this.PhysX.PxVec3(...targetPosition); - const rot = new this.PhysX.PxQuat(...nodeRotation); - const transform = new this.PhysX.PxTransform(pos, rot); + if (motion.mass !== undefined) { + actor.setMass(motion.mass); + } + + this.calculateMassAndInertia(motion, actor); + + if (motion.gravityFactor !== 1.0) { + actor.setActorFlag(this.PhysX.PxActorFlagEnum.eDISABLE_GRAVITY, true); + } + } + } + + const createAndAddShape = ( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger, + noMeshShapes, + shapeFlags, + triggerFlags + ) => { + // Calculate offset position + const translation = vec3.create(); + const shapePosition = vec3.create(); + mat4.getTranslation(shapePosition, actorNode.worldTransform); + const invertedActorRotation = quat.create(); + quat.invert(invertedActorRotation, actorNode.worldQuaternion); + const offsetPosition = vec3.create(); + mat4.getTranslation(offsetPosition, worldTransform); + vec3.subtract(translation, offsetPosition, shapePosition); + vec3.transformQuat(translation, translation, invertedActorRotation); + + // Calculate offset rotation + const rotation = quat.create(); + quat.multiply(rotation, invertedActorRotation, node.worldQuaternion); + + // Calculate scale and scaleAxis + const { scale, scaleAxis } = PhysicsUtils.calculateScaleAndAxis(node); + + const materialIndex = collider?.physicsMaterial; + const material = + materialIndex !== undefined + ? this.physXMaterials[materialIndex] + : this.defaultMaterial; + + const physXFilterData = + collider?.collisionFilter !== undefined + ? this.physXFilterData[collider.collisionFilter] + : this.physXFilterData[this.physXFilterData.length - 1]; - actor.setKinematicTarget(transform); - this.PhysX.destroy(pos); - this.PhysX.destroy(rot); - this.PhysX.destroy(transform); + const shape = this.createShape( + gltf, + node, + collider, + isTrigger ? triggerFlags : shapeFlags, + material, + physXFilterData, + noMeshShapes || collider?.geometry?.convexHull === true, + scale, + scaleAxis + ); - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, nodeRotation, targetPosition); + if (shape !== undefined) { + const PxPos = new this.PhysX.PxVec3(...translation); + const PxRotation = new this.PhysX.PxQuat(...rotation); + const pose = new this.PhysX.PxTransform(PxPos, PxRotation); + shape.setLocalPose(pose); - const scaledPhysicsTransform = mat4.create(); - mat4.scale(scaledPhysicsTransform, physicsTransform, node.worldScale); + actor.attachShape(shape); + pxShapeMap.set(node.gltfObjectIndex, shape); + this.PhysX.destroy(PxPos); + this.PhysX.destroy(PxRotation); + this.PhysX.destroy(pose); + } + }; - node.physicsTransform = physicsTransform; - node.scaledPhysicsTransform = scaledPhysicsTransform; - } - } else if (motion && motion.gravityFactor !== 1.0) { - const force = new this.PhysX.PxVec3(0, -9.81 * motion.gravityFactor, 0); - actor.addForce(force, this.PhysX.PxForceModeEnum.eACCELERATION); - this.PhysX.destroy(force); + // If a node contains trigger and collider combine them + + let collider = undefined; + if (type !== "trigger") { + collider = node.extensions?.KHR_physics_rigid_bodies?.collider; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + false, + noMeshShapes, + shapeFlags, + triggerFlags + ); + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + if (collider !== undefined) { + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + shapeFlags, + triggerFlags + ); } + } else { + collider = node.extensions?.KHR_physics_rigid_bodies?.trigger; + createAndAddShape( + gltf, + node, + collider, + node, + worldTransform, + undefined, + undefined, + true, + true, + shapeFlags, + triggerFlags + ); } - this.scene.simulate(deltaTime); - if (!this.scene.fetchResults(true)) { - console.warn("PhysX: fetchResults failed"); + if (type !== "trigger") { + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + PhysicsUtils.recurseCollider( + gltf, + childNode, + undefined, + node, + node.dirtyScale, + node.dirtyScale, + createAndAddShape, + [noMeshShapes, shapeFlags, triggerFlags] + ); + } } + + this.PhysX.destroy(pos); + this.PhysX.destroy(rotation); + this.PhysX.destroy(pose); + + this.scene.addActor(actor); + + this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } - simulateStep(state, deltaTime) { - if (!this.scene) { - this.reset = false; - return; + computeJointOffsetAndActor(node, referencedJoint) { + let currentNode = node; + while (currentNode !== undefined) { + if (this.nodeToActor.has(currentNode.gltfObjectIndex)) { + break; + } + currentNode = currentNode.parentNode; } - if (this.reset === true) { - this._resetSimulation(); - this.reset = false; - return; + + const nodeWorldRot = node.worldQuaternion; + const localPhysXRot = referencedJoint?.localRotation; + if (localPhysXRot !== undefined) { + quat.multiply(nodeWorldRot, node.worldQuaternion, localPhysXRot); } - this.changeDebugVisualization(); + if (currentNode === undefined) { + const pos = vec3.create(); + mat4.getTranslation(pos, node.worldTransform); - this.subStepSimulation(state, deltaTime); + return { actor: undefined, offsetPosition: pos, offsetRotation: nodeWorldRot }; + } + const actor = this.nodeToActor.get(currentNode.gltfObjectIndex)?.actor; + const inverseActorRotation = quat.create(); + quat.invert(inverseActorRotation, currentNode.worldQuaternion); + const offsetRotation = quat.create(); + quat.multiply(offsetRotation, inverseActorRotation, nodeWorldRot); - // eslint-disable-next-line no-unused-vars - for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { - const node = state.gltf.nodes[nodeIndex]; - const motion = node.extensions?.KHR_physics_rigid_bodies?.motion; - if (motion && !motion.isKinematic && !node.dirtyTransform) { - const transform = actor.getGlobalPose(); - const position = vec3.fromValues(transform.p.x, transform.p.y, transform.p.z); - const rotation = quat.fromValues( - transform.q.x, - transform.q.y, - transform.q.z, - transform.q.w - ); + const actorPosition = vec3.create(); + mat4.getTranslation(actorPosition, currentNode.worldTransform); + const nodePosition = vec3.create(); + mat4.getTranslation(nodePosition, node.worldTransform); + const offsetPosition = vec3.create(); + vec3.subtract(offsetPosition, nodePosition, actorPosition); + vec3.transformQuat(offsetPosition, offsetPosition, inverseActorRotation); - const physicsTransform = mat4.create(); - mat4.fromRotationTranslation(physicsTransform, rotation, position); + return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; + } - node.physicsTransform = physicsTransform; + calculateMassAndInertia(motion, actor) { + const pos = new this.PhysX.PxVec3(0, 0, 0); + if (motion.centerOfMass !== undefined) { + pos.x = motion.centerOfMass[0]; + pos.y = motion.centerOfMass[1]; + pos.z = motion.centerOfMass[2]; + } + const rot = new this.PhysX.PxQuat(this.PhysX.PxIDENTITYEnum.PxIdentity); + if (motion.inertiaDiagonal !== undefined) { + let inertia = undefined; + if ( + motion.inertiaOrientation !== undefined && + !quat.exactEquals(motion.inertiaOrientation, quat.create()) + ) { + const intertiaRotMat = mat3.create(); - const rotationBetween = quat.create(); + const inertiaDiagonalMat = mat3.create(); + inertiaDiagonalMat[0] = motion.inertiaDiagonal[0]; + inertiaDiagonalMat[4] = motion.inertiaDiagonal[1]; + inertiaDiagonalMat[8] = motion.inertiaDiagonal[2]; - let parentNode = node; - while (parentNode.parentNode !== undefined) { - parentNode = parentNode.parentNode; + if ( + quat.length(motion.inertiaOrientation) > 1.0e-5 || + quat.length(motion.inertiaOrientation) < 1.0e-5 + ) { + mat3.identity(intertiaRotMat); + console.warn( + "PhysX: Invalid inertia orientation quaternion, ignoring rotation" + ); + } else { + mat3.fromQuat(intertiaRotMat, motion.inertiaOrientation); } - quat.invert(rotationBetween, node.worldQuaternion); - quat.multiply(rotationBetween, rotation, rotationBetween); + const inertiaTensor = mat3.create(); + mat3.multiply(inertiaTensor, intertiaRotMat, inertiaDiagonalMat); - const rotMat = mat3.create(); - mat3.fromQuat(rotMat, rotationBetween); + const col0 = new this.PhysX.PxVec3( + inertiaTensor[0], + inertiaTensor[1], + inertiaTensor[2] + ); + const col1 = new this.PhysX.PxVec3( + inertiaTensor[3], + inertiaTensor[4], + inertiaTensor[5] + ); + const col2 = new this.PhysX.PxVec3( + inertiaTensor[6], + inertiaTensor[7], + inertiaTensor[8] + ); + const pxInertiaTensor = new this.PhysX.PxMat33(col0, col1, col2); + inertia = this.PhysX.PxMassProperties.prototype.getMassSpaceInertia( + pxInertiaTensor, + rot + ); + this.PhysX.destroy(col0); + this.PhysX.destroy(col1); + this.PhysX.destroy(col2); + this.PhysX.destroy(pxInertiaTensor); + actor.setMassSpaceInertiaTensor(inertia); + } else { + inertia = new this.PhysX.PxVec3(...motion.inertiaDiagonal); + actor.setMassSpaceInertiaTensor(inertia); + this.PhysX.destroy(inertia); + } + } else { + if (motion.mass === undefined) { + this.PhysX.PxRigidBodyExt.prototype.updateMassAndInertia(actor, 1.0, pos); + } else { + this.PhysX.PxRigidBodyExt.prototype.setMassAndUpdateInertia( + actor, + motion.mass, + pos + ); + } + } - const scaleRot = mat3.create(); - mat3.fromMat4(scaleRot, node.worldTransform); + const pose = new this.PhysX.PxTransform(pos, rot); + actor.setCMassLocalPose(pose); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); + this.PhysX.destroy(pose); + } - mat3.multiply(scaleRot, rotMat, scaleRot); + //endregion - const scaledPhysicsTransform = mat4.create(); - scaledPhysicsTransform[0] = scaleRot[0]; - scaledPhysicsTransform[1] = scaleRot[1]; - scaledPhysicsTransform[2] = scaleRot[2]; - scaledPhysicsTransform[4] = scaleRot[3]; - scaledPhysicsTransform[5] = scaleRot[4]; - scaledPhysicsTransform[6] = scaleRot[5]; - scaledPhysicsTransform[8] = scaleRot[6]; - scaledPhysicsTransform[9] = scaleRot[7]; - scaledPhysicsTransform[10] = scaleRot[8]; - scaledPhysicsTransform[12] = position[0]; - scaledPhysicsTransform[13] = position[1]; - scaledPhysicsTransform[14] = position[2]; + //region Joints - node.scaledPhysicsTransform = scaledPhysicsTransform; - for (const childIndex of node.children) { - const childNode = state.gltf.nodes[childIndex]; - this.applyTransformRecursively( - state.gltf, - childNode, - node.scaledPhysicsTransform - ); - } + convertAxisIndexToEnum(axisIndex, type) { + if (type === "linear") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eX; + case 1: + return this.PhysX.PxD6AxisEnum.eY; + case 2: + return this.PhysX.PxD6AxisEnum.eZ; + } + } else if (type === "angular") { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6AxisEnum.eTWIST; + case 1: + return this.PhysX.PxD6AxisEnum.eSWING1; + case 2: + return this.PhysX.PxD6AxisEnum.eSWING2; } } + return null; } - resetSimulation() { - this.reset = true; - this.simulateStep({}, 0); + convertAxisIndexToAngularDriveEnum(axisIndex) { + switch (axisIndex) { + case 0: + return this.PhysX.PxD6DriveEnum.eTWIST; + case 1: + return 6; // Currently not exposed via bindings + case 2: + return 7; // Currently not exposed via bindings + } + return null; } - _resetSimulation() { - const scenePointer = this.scene; - this.scene = undefined; - this.filterData = []; - for (const physXFilterData of this.physXFilterData) { - this.PhysX.destroy(physXFilterData); - } - this.physXFilterData = []; + validateSwingLimits(joint) { + // Check if swing limits are symmetric (cone) or asymmetric (pyramid) + if (joint.swingLimit1 && joint.swingLimit2) { + const limit1 = joint.swingLimit1; + const limit2 = joint.swingLimit2; - for (const material of this.physXMaterials) { - material.release(); + const isSymmetric1 = + Math.abs(limit1.min + limit1.max) < 1e-6 || limit1.min === undefined; // Centered around 0 + const isSymmetric2 = + Math.abs(limit2.min + limit2.max) < 1e-6 || limit2.min === undefined; + + // Return if this is a cone limit (symmetric and same range) vs pyramid limit + return isSymmetric1 && isSymmetric2; } - this.physXMaterials = []; + return false; + } - for (const shape of this.simpleShapes) { - shape.destroy?.(); + createJoint(gltf, node) { + const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; + const referencedJoint = + gltf.extensions?.KHR_physics_rigid_bodies?.physicsJoints[joint.joint]; + + if (referencedJoint === undefined) { + console.error("Referenced joint not found:", joint.joint); + return; } - this.simpleShapes = []; + const simplifiedJoints = []; + for (const simplifiedJoint of referencedJoint.simplifiedPhysicsJoints) { + const physxJoint = this.createSimplifiedJoint(gltf, node, joint, simplifiedJoint); + simplifiedJoints.push(physxJoint); + } + this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); + } - for (const convexMesh of this.convexMeshes) { - convexMesh.release(); + _setLimitValues(physxJoint, simplifiedJoint, limit) { + const lock = limit.min === 0 && limit.max === 0; + const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); + const isDistanceLimit = + limit.linearAxes && + limit.linearAxes.length === 3 && + (limit.min === undefined || limit.min === 0) && + limit.max !== 0; + if (limit.linearAxes && limit.linearAxes.length > 0 && !isDistanceLimit) { + const linearLimitPair = new this.PhysX.PxJointLinearLimitPair( + limit.min ?? -this.MAX_FLOAT, + limit.max ?? this.MAX_FLOAT, + spring + ); + for (const axis of limit.linearAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setMotion( + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED + ); + if (!lock) { + physxJoint.setLinearLimit(physxAxis, linearLimitPair); + } + } + this.PhysX.destroy(linearLimitPair); } - this.convexMeshes = []; + if (isDistanceLimit) { + const linearLimit = new this.PhysX.PxJointLinearLimit( + limit.max ?? this.MAX_FLOAT, + spring + ); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eLIMITED); + physxJoint.setDistanceLimit(linearLimit); + this.PhysX.destroy(linearLimit); + } + if (limit.angularAxes && limit.angularAxes.length > 0) { + for (const axis of limit.angularAxes) { + const result = simplifiedJoint.getRotatedAxisAndSign(axis); + const physxAxis = this.convertAxisIndexToEnum(result.axis, "angular"); + physxJoint.setMotion( + physxAxis, + lock ? this.PhysX.PxD6MotionEnum.eLOCKED : this.PhysX.PxD6MotionEnum.eLIMITED + ); + } + } + this.PhysX.destroy(spring); + } - for (const triangleMesh of this.triangleMeshes) { - triangleMesh.release(); + _setTwistLimitValues(physxJoint, simplifiedJoint) { + if (simplifiedJoint.twistLimit !== undefined) { + if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { + const limitPair = new this.PhysX.PxJointAngularLimitPair( + simplifiedJoint.twistLimit.min ?? -Math.PI, + simplifiedJoint.twistLimit.max ?? Math.PI, + new this.PhysX.PxSpring( + simplifiedJoint.twistLimit.stiffness ?? 0, + simplifiedJoint.twistLimit.damping + ) + ); + physxJoint.setTwistLimit(limitPair); + this.PhysX.destroy(limitPair); + } } - this.triangleMeshes = []; + } - for (const joints of this.nodeToSimplifiedJoints.values()) { - for (const joint of joints) { - joint.release(); + _setSwingLimitValues(physxJoint, simplifiedJoint) { + if ( + simplifiedJoint.swingLimit1 !== undefined && + simplifiedJoint.swingLimit2 !== undefined + ) { + if ( + simplifiedJoint.swingLimit1.stiffness !== simplifiedJoint.swingLimit2.stiffness || + simplifiedJoint.swingLimit1.damping !== simplifiedJoint.swingLimit2.damping + ) { + console.warn( + "PhysX does not support different stiffness/damping for swing limits." + ); + } else { + const spring = new this.PhysX.PxSpring( + simplifiedJoint.swingLimit1.stiffness ?? 0, + simplifiedJoint.swingLimit1.damping + ); + let yMin = -Math.PI / 2; + let yMax = Math.PI / 2; + let zMin = -Math.PI / 2; + let zMax = Math.PI / 2; + if (simplifiedJoint.swingLimit1.min !== undefined) { + yMin = simplifiedJoint.swingLimit1.min; + } + if (simplifiedJoint.swingLimit1.max !== undefined) { + yMax = simplifiedJoint.swingLimit1.max; + } + if (simplifiedJoint.swingLimit2.min !== undefined) { + zMin = simplifiedJoint.swingLimit2.min; + } + if (simplifiedJoint.swingLimit2.max !== undefined) { + zMax = simplifiedJoint.swingLimit2.max; + } + + const isSymmetric = this.validateSwingLimits(simplifiedJoint); + if (yMin === 0 && yMax === 0 && zMin === 0 && zMax === 0) { + // Fixed limit is already set + } else if (isSymmetric) { + const swing1Angle = Math.max(Math.abs(yMin), Math.abs(yMax)); + const swing2Angle = Math.max(Math.abs(zMin), Math.abs(zMax)); + const jointLimitCone = new this.PhysX.PxJointLimitCone( + swing1Angle, + swing2Angle, + spring + ); + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); + } else { + const jointLimitCone = new this.PhysX.PxJointLimitPyramid( + yMin, + yMax, + zMin, + zMax, + spring + ); + physxJoint.setPyramidSwingLimit(jointLimitCone); + this.PhysX.destroy(jointLimitCone); + } + this.PhysX.destroy(spring); + } + } else if ( + simplifiedJoint.swingLimit1 !== undefined || + simplifiedJoint.swingLimit2 !== undefined + ) { + const singleLimit = simplifiedJoint.swingLimit1 ?? simplifiedJoint.swingLimit2; + if (singleLimit.min === 0 && singleLimit.max === 0) { + // Fixed limit is already set + } else if (singleLimit.min && -1 * singleLimit.min !== singleLimit.max) { + console.warn( + "PhysX requires symmetric limits for swing limits in single axis mode." + ); + } else { + const spring = new this.PhysX.PxSpring( + singleLimit.stiffness ?? 0, + singleLimit.damping + ); + const maxY = simplifiedJoint.swingLimit1?.max ?? Math.PI; + const maxZ = simplifiedJoint.swingLimit2?.max ?? Math.PI; + const jointLimitCone = new this.PhysX.PxJointLimitCone(maxY, maxZ, spring); + physxJoint.setSwingLimit(jointLimitCone); + this.PhysX.destroy(spring); + this.PhysX.destroy(jointLimitCone); } } - this.nodeToSimplifiedJoints.clear(); - - for (const actor of this.nodeToActor.values()) { - actor.actor.release(); - } - - this.nodeToActor.clear(); + } - if (scenePointer) { - scenePointer.release(); + _setDriveValues(physxJoint, simplifiedJoint, drive) { + const physxDrive = new this.PhysX.PxD6JointDrive( + drive.stiffness, + drive.damping, + drive.maxForce ?? this.MAX_FLOAT, + drive.mode === "acceleration" + ); + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + const axis = this.convertAxisIndexToEnum(result.axis, "linear"); + physxJoint.setDrive(axis, physxDrive); + } else if (drive.type === "angular") { + const axis = this.convertAxisIndexToAngularDriveEnum(result.axis); + physxJoint.setDrive(axis, physxDrive); } - - this.shapeToNode.clear(); + this.PhysX.destroy(physxDrive); } - getDebugLineData() { - if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { - return []; + _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + if (drive.velocityTarget !== undefined) { + linearVelocityTarget[result.axis] = drive.velocityTarget * result.sign; + } + } else if (drive.type === "angular") { + if (drive.velocityTarget !== undefined) { + angularVelocityTarget[result.axis] = drive.velocityTarget * result.sign * -1; // PhysX angular velocity is in opposite direction of rotation + } } - const result = []; - const rb = this.scene.getRenderBuffer(); - for (let i = 0; i < rb.getNbLines(); i++) { - const line = this.PhysX.NativeArrayHelpers.prototype.getDebugLineAt(rb.getLines(), i); + } - result.push(line.pos0.x); - result.push(line.pos0.y); - result.push(line.pos0.z); - result.push(line.pos1.x); - result.push(line.pos1.y); - result.push(line.pos1.z); + _setDrivePositionTarget(physxJoint, simplifiedJoint) { + const positionTarget = vec3.fromValues(0, 0, 0); + const angleTarget = quat.create(); + for (const drive of simplifiedJoint.drives) { + const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); + if (drive.type === "linear") { + if (drive.positionTarget !== undefined) { + positionTarget[result.axis] = drive.positionTarget * result.sign; + } + } else if (drive.type === "angular") { + if (drive.positionTarget !== undefined) { + // gl-matrix seems to apply rotations clockwise for positive angles, gltf uses counter-clockwise + switch (result.axis) { + case 0: { + quat.rotateX( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + case 1: { + quat.rotateY( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + case 2: { + quat.rotateZ( + angleTarget, + angleTarget, + -drive.positionTarget * result.sign + ); + break; + } + } + } + } } - return result; + const posTarget = new this.PhysX.PxVec3(...positionTarget); + const rotTarget = new this.PhysX.PxQuat(...angleTarget); + const targetTransform = new this.PhysX.PxTransform(posTarget, rotTarget); + physxJoint.setDrivePosition(targetTransform); + this.PhysX.destroy(posTarget); + this.PhysX.destroy(rotTarget); + this.PhysX.destroy(targetTransform); } - applyImpulse(nodeIndex, linearImpulse, angularImpulse) { - if (!this.scene) { - return; - } - const motionNode = this.nodeToMotion.get(nodeIndex); - if (!motionNode) { - return; - } - const actorEntry = this.nodeToActor.get(nodeIndex); - if (!actorEntry) { - return; - } - const actor = actorEntry.actor; + createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { + const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); + const resultB = this.computeJointOffsetAndActor( + gltf.nodes[joint.connectedNode], + simplifiedJoint + ); - const linImpulse = new this.PhysX.PxVec3(...linearImpulse); - const angImpulse = new this.PhysX.PxVec3(...angularImpulse); - actor.addForce(linImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); - actor.addTorque(angImpulse, this.PhysX.PxForceModeEnum.eIMPULSE); - this.PhysX.destroy(linImpulse); - this.PhysX.destroy(angImpulse); - } + const pos = new this.PhysX.PxVec3(...resultA.offsetPosition); + const rot = new this.PhysX.PxQuat(...resultA.offsetRotation); + const poseA = new this.PhysX.PxTransform(pos, rot); + this.PhysX.destroy(pos); + this.PhysX.destroy(rot); - applyPointImpulse(nodeIndex, impulse, position) { - if (!this.scene) { - return; - } - const motionNode = this.nodeToMotion.get(nodeIndex); - if (!motionNode) { - return; - } - const actorEntry = this.nodeToActor.get(nodeIndex); - if (!actorEntry) { - return; - } - const actor = actorEntry.actor; + const posB = new this.PhysX.PxVec3(...resultB.offsetPosition); + const rotB = new this.PhysX.PxQuat(...resultB.offsetRotation); + const poseB = new this.PhysX.PxTransform(posB, rotB); + this.PhysX.destroy(posB); + this.PhysX.destroy(rotB); - const pxImpulse = new this.PhysX.PxVec3(...impulse); - const pxPosition = new this.PhysX.PxVec3(...position); - this.PhysX.PxRigidBodyExt.prototype.addForceAtPos( - actor, - pxImpulse, - pxPosition, - this.PhysX.PxForceModeEnum.eIMPULSE + const physxJoint = this.PhysX.PxTopLevelFunctions.prototype.D6JointCreate( + this.physics, + resultA.actor, + poseA, + resultB.actor, + poseB ); - this.PhysX.destroy(pxImpulse); - this.PhysX.destroy(pxPosition); - } - - rayCast(rayStart, rayEnd) { - const result = {}; - result.hitNodeIndex = -1; - if (!this.scene) { - return result; - } - const origin = new this.PhysX.PxVec3(...rayStart); - const directionVec = vec3.create(); - vec3.subtract(directionVec, rayEnd, rayStart); - vec3.normalize(directionVec, directionVec); - const direction = new this.PhysX.PxVec3(...directionVec); - const maxDistance = vec3.distance(rayStart, rayEnd); + this.PhysX.destroy(poseA); + this.PhysX.destroy(poseB); - const hitBuffer = new this.PhysX.PxRaycastBuffer10(); - const hitFlags = new this.PhysX.PxHitFlags(this.PhysX.PxHitFlagEnum.eDEFAULT); + physxJoint.setAngularDriveConfig(this.PhysX.PxD6AngularDriveConfigEnum.eSWING_TWIST); - const queryFilterData = new this.PhysX.PxQueryFilterData(); - queryFilterData.set_flags( - this.PhysX.PxQueryFlagEnum.eSTATIC | this.PhysX.PxQueryFlagEnum.eDYNAMIC + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eVISUALIZATION, + this.debugJoints ); - const hasHit = this.scene.raycast( - origin, - direction, - maxDistance, - hitBuffer, - hitFlags, - queryFilterData + physxJoint.setConstraintFlag( + this.PhysX.PxConstraintFlagEnum.eCOLLISION_ENABLED, + joint.enableCollision ); - this.PhysX.destroy(origin); - this.PhysX.destroy(direction); - this.PhysX.destroy(hitFlags); - this.PhysX.destroy(queryFilterData); + // Do not restict any axis by default + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eX, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eY, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eZ, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eTWIST, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING1, this.PhysX.PxD6MotionEnum.eFREE); + physxJoint.setMotion(this.PhysX.PxD6AxisEnum.eSWING2, this.PhysX.PxD6MotionEnum.eFREE); - if (hasHit) { - const hitCount = hitBuffer.getNbAnyHits(); - if (hitCount > 1) { - console.warn("Raycast hit multiple objects, only the first hit is returned."); - } - const hit = hitBuffer.getAnyHit(0); - const fraction = hit.distance / maxDistance; - const hitNormal = vec3.fromValues(hit.normal.x, hit.normal.y, hit.normal.z); - const hitNodeIndex = this.shapeToNode.get(hit.shape.ptr); - if (hitNodeIndex === undefined) { - return result; - } - return { - hitNodeIndex: hitNodeIndex, - hitFraction: fraction, - hitNormal: hitNormal - }; - } else { - return result; + for (const limit of simplifiedJoint.limits) { + this._setLimitValues(physxJoint, simplifiedJoint, limit); + } + + this._setTwistLimitValues(physxJoint, simplifiedJoint); + this._setSwingLimitValues(physxJoint, simplifiedJoint); + + const linearVelocityTarget = vec3.fromValues(0, 0, 0); + const angularVelocityTarget = vec3.fromValues(0, 0, 0); + + for (const drive of simplifiedJoint.drives) { + this._setDriveValues(physxJoint, simplifiedJoint, drive); + this._getDriveVelocityTarget( + simplifiedJoint, + drive, + linearVelocityTarget, + angularVelocityTarget + ); } + this._setDrivePositionTarget(physxJoint, simplifiedJoint); + + const linVel = new this.PhysX.PxVec3(...linearVelocityTarget); + const angVel = new this.PhysX.PxVec3(...angularVelocityTarget); + physxJoint.setDriveVelocity(linVel, angVel); + this.PhysX.destroy(linVel); + this.PhysX.destroy(angVel); + + return physxJoint; } + + // endregion } export { NvidiaPhysicsInterface }; diff --git a/source/PhysicsEngines/PhysicsInterface.js b/source/PhysicsEngines/PhysicsInterface.js index 9ab56f63..dce20e9b 100644 --- a/source/PhysicsEngines/PhysicsInterface.js +++ b/source/PhysicsEngines/PhysicsInterface.js @@ -6,6 +6,10 @@ class PhysicsInterface { this.simpleShapes = []; } + // Functions to be implemented by physics engine wrappers + + // Start functions from PhysicsController + async initializeEngine() {} initializeSimulation( state, @@ -20,10 +24,8 @@ class PhysicsInterface { staticMeshColliderCount, dynamicMeshColliderCount ) {} - pauseSimulation() {} - resumeSimulation() {} resetSimulation() {} - stopSimulation() {} + simulateStep(state, deltaTime) {} enableDebugColliders(enable) {} enableDebugJoints(enable) {} @@ -31,11 +33,45 @@ class PhysicsInterface { applyPointImpulse(nodeIndex, impulse, position) {} rayCast(rayStart, rayEnd) {} - generateBox(x, y, z, scale, scaleAxis, reference) {} - generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} - generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} - generateSphere(radius, scale, scaleAxis, reference) {} - generatePlane(width, height, doubleSided, scale, scaleAxis, reference) {} + updateActorTransform(node) {} + updatePhysicsJoint(state, jointNode) {} + updatePhysicsMaterials(gltf) {} + updateCollider( + gltf, + node, + collider, + actorNode, + worldTransform, + offsetChanged, + scaleChanged, + isTrigger + ) {} + updateMotion(actorNode) {} + + // End functions from PhysicsController + + generateSimpleShapes(gltf) { + this.simpleShapes = []; + if (gltf?.extensions?.KHR_implicit_shapes === undefined) { + return; + } + for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { + this.simpleShapes.push(this.generateSimpleShape(shape)); + } + } + + /** + * Generates a simple physics shape based on the provided gltfImplicitShape. + * The scale and scaleAxis parameters should be used to apply additional scaling to the shape. + * The reference parameter can be used to update an already existing shape instead of creating a new one, + * if the physics engine supports it. + * + * @param {gltfImplicitShape} shape + * @param {vec3} scale + * @param {quat} scaleAxis + * @param {any | undefined} reference + * @returns + */ generateSimpleShape( shape, scale = vec3.fromValues(1, 1, 1), @@ -73,29 +109,15 @@ class PhysicsInterface { case "sphere": return this.generateSphere(shape.sphere.radius, scale, scaleAxis, reference); case "plane": - return this.generatePlane( - shape.plane.width, - shape.plane.height, - shape.plane.doubleSided, - scale, - scaleAxis, - reference - ); - } - } - - generateSimpleShapes(gltf) { - this.simpleShapes = []; - if (gltf?.extensions?.KHR_implicit_shapes === undefined) { - return; - } - for (const shape of gltf.extensions.KHR_implicit_shapes.shapes) { - this.simpleShapes.push(this.generateSimpleShape(shape)); + return this.generatePlane(reference); } } - updateActorTransform(node) {} - updatePhysicsJoint(state, jointNode) {} + generateBox(x, y, z, scale, scaleAxis, reference) {} + generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, reference) {} + generateSphere(radius, scale, scaleAxis, reference) {} + generatePlane(reference) {} } export { PhysicsInterface }; diff --git a/source/gltf/physics_utils.js b/source/gltf/physics_utils.js index 45ee14b3..5989aa59 100644 --- a/source/gltf/physics_utils.js +++ b/source/gltf/physics_utils.js @@ -1,6 +1,12 @@ import { quat, vec3 } from "gl-matrix"; class PhysicsUtils { + /** + * Returns the cumulative scale and scale axis from the node up to the root, + * which can be used to properly scale physics shapes + * @param {gltfNode} node + * @returns {{scale: vec3, scaleAxis: quat}} + */ static calculateScaleAndAxis(node) { const scaleFactor = vec3.clone(node.scale); let scaleRotation = quat.create(); @@ -78,6 +84,23 @@ class PhysicsUtils { return triangleIndices; } + /** + * Recursively traverses the node hierarchy of a motion to find all colliders and triggers, and applies the custom function to them. + * The custom function has the following signature: + * function(gltf, node, collider/trigger, motionNode, computedWorldTransform, offsetChanged, scaleChanged, isTrigger, ...args) + * offsetChanged and scaleChanged are cumulative values that indicate whether any node in the hierarchy has a dirty offset or scale, + * which can be used to determine if the physics shape needs to be updated. + * isTrigger indicates whether the current geometry is a trigger or a collider. + * + * @param {gltf} gltf + * @param {gltfNode} node + * @param {KHR_physics_rigid_bodies_collider | KHR_physics_rigid_bodies_trigger} collider + * @param {gltfNode} motionNode + * @param {boolean} offsetChanged + * @param {boolean} scaleChanged + * @param {Function} customFunction + * @param {Array} args + */ static recurseCollider( gltf, node, diff --git a/source/gltf/rigid_bodies.js b/source/gltf/rigid_bodies.js index 0c02431f..123e732b 100644 --- a/source/gltf/rigid_bodies.js +++ b/source/gltf/rigid_bodies.js @@ -41,6 +41,15 @@ class gltfCollisionFilter extends GltfObject { } } +/** + * glTF allows defining multiple limits and drives for a joint,which can lead + * to complex combinations of constraints that are not directly supported by + * common physics engines. The simplifiedPhysicsJoint class takes the limits + * and drives defined in a gltfPhysicsJoint and simplifies them into one or + * more sets of constraints that can be more easily implemented in a physics engine. + * Each simplifiedPhysicsJoint represents a single set of constraints (e.g., one twist limit and two swing limits) + * along with the necessary local rotation to align the joint's axes with the physics engine's expected axes. + */ class simplifiedPhysicsJoint { constructor(limits, drives) { this.limits = limits; @@ -101,6 +110,12 @@ class simplifiedPhysicsJoint { } } + /** + * Input the glTF defined axis and get the corresponding axis and sign + * after applying the local rotation to always align twist with the X-axis. + * @param {number} axis + * @returns {{axis: number, sign: number}} + */ getRotatedAxisAndSign(axis) { let result = { axis: axis, @@ -215,6 +230,7 @@ class simplifiedPhysicsJoint { _handleCylindricalLimits(limitAxes, fixedAxes) { // Handle limits that constrain multiple axes together (cone/ellipse) // Find the limit that affects multiple axes + // eslint-disable-next-line no-unused-vars for (const [axis, limit] of limitAxes.entries()) { if (limit.angularAxes && limit.angularAxes.length > 1) { // This is a cone/ellipse limit @@ -285,6 +301,7 @@ class gltfPhysicsJoint extends GltfObject { let currentLimits = []; const drivesCopy = this.drives.slice(); + // If multiple limits affect the same axis, we create separate simplified joints for each combination of constraints. let needToCreateNewJoint = false; for (const limit of this.limits) { for (const axis of limit.angularAxes || []) { @@ -315,10 +332,13 @@ class gltfPhysicsJoint extends GltfObject { definedLinearAxes.add(axis); } } + // Add remaining limits and drives as a simplified joint if (currentLimits.length > 0) { const drives = this._getUniqueDrives(drivesCopy); this.simplifiedPhysicsJoints.push(new simplifiedPhysicsJoint(currentLimits, drives)); } + + // If there are any drives left that were not included in the previous joints, we create a new simplified joint for them without limits. while (drivesCopy.length > 0) { const drives = this._getUniqueDrives(drivesCopy); this.simplifiedPhysicsJoints.push(new simplifiedPhysicsJoint([], drives)); From af0979a28ea216da2ab6f902287bfe06adcd951e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Wed, 25 Mar 2026 18:31:24 +0100 Subject: [PATCH 92/93] Add more documentation --- source/PhysicsEngines/PhysX.js | 483 ++++++++++++++++++++++++++++++++- source/gltf/physics_utils.js | 25 +- 2 files changed, 492 insertions(+), 16 deletions(-) diff --git a/source/PhysicsEngines/PhysX.js b/source/PhysicsEngines/PhysX.js index 214df437..49ec88c8 100644 --- a/source/PhysicsEngines/PhysX.js +++ b/source/PhysicsEngines/PhysX.js @@ -13,6 +13,10 @@ import { PhysicsInterface } from "./PhysicsInterface"; import { PhysicsUtils } from "../gltf/physics_utils"; class NvidiaPhysicsInterface extends PhysicsInterface { + /** + * Creates a new NvidiaPhysicsInterface instance, initializing all internal + * state maps, debug flags, and placeholders for the PhysX engine objects. + */ constructor() { super(); this.PhysX = undefined; @@ -46,6 +50,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region General + /** + * Asynchronously loads and initializes the PhysX WebAssembly module, creating the + * PhysX foundation, tolerances scale, physics object, and a default physics material. + * + * @async + * @returns {Promise} The initialized PhysX module instance. + */ async initializeEngine() { this.PhysX = await PhysX({ locateFile: () => "./libs/physx-js-webidl.wasm" }); const version = this.PhysX.PHYSICS_VERSION; @@ -71,6 +82,12 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.PhysX; } + /** + * Applies the current debug visualization flags to the active PhysX scene. + * Enables or disables rendering of collision shapes, joint frames, joint limits, + * actor axes, and world axes based on {@link debugColliders} and {@link debugJoints}. + * Does nothing if there is no active scene or if the state has not changed. + */ changeDebugVisualization() { if (!this.scene || !this.debugStateChanged) { return; @@ -111,6 +128,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Sets up the PhysX scene and populates it with actors and joints derived from the + * provided glTF node lists. Creates collision filters, physics materials, and the + * PhysX scene descriptor before adding static, kinematic, dynamic, trigger, and + * joint actors. + * + * @param {object} state - The current viewer state, containing the glTF asset and controllers. + * @param {Array} staticActors - Nodes to be created as static rigid body actors. + * @param {Array} kinematicActors - Nodes to be created as kinematic rigid body actors. + * @param {Array} dynamicActors - Nodes to be created as dynamic rigid body actors. + * @param {Array} jointNodes - Nodes carrying joint definitions. + * @param {Array} triggerNodes - Nodes designated as trigger volumes. + * @param {Array} independentTriggerNodes - Trigger nodes not already covered by another actor. + * @param {Map} nodeToMotion - Mapping from node index to motion data. + * @param {boolean} _hasRuntimeAnimationTargets - Unused; reserved for future use. + * @param {number} _staticMeshColliderCount - Unused; reserved for future use. + * @param {number} _dynamicMeshColliderCount - Unused; reserved for future use. + */ initializeSimulation( state, staticActors, @@ -285,30 +320,34 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.changeDebugVisualization(); } + /** + * Enables or disables debug visualization of collision shapes. + * + * @param {boolean} enable - `true` to show collision shape debug rendering; `false` to hide it. + */ enableDebugColliders(enable) { this.debugColliders = enable; this.debugStateChanged = true; } + /** + * Enables or disables debug visualization of physics joints. + * + * @param {boolean} enable - `true` to show joint debug rendering; `false` to hide it. + */ enableDebugJoints(enable) { this.debugJoints = enable; this.debugStateChanged = true; } - applyTransformRecursively(gltf, node, parentTransform) { - if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { - return; - } - const localTransform = node.getLocalTransform(); - const globalTransform = mat4.create(); - mat4.multiply(globalTransform, parentTransform, localTransform); - node.scaledPhysicsTransform = globalTransform; - for (const childIndex of node.children) { - const childNode = gltf.nodes[childIndex]; - this.applyTransformRecursively(gltf, childNode, globalTransform); - } - } - + /** + * Executes a single fixed-duration physics sub-step. Before stepping, applies + * kinematic targets for nodes with velocity overrides and non-unit gravity factors. + * After stepping, calls `scene.fetchResults` to commit the simulation results. + * + * @param {object} state - The current viewer state. + * @param {number} deltaTime - The duration of the sub-step in seconds. + */ subStepSimulation(state, deltaTime) { // eslint-disable-next-line no-unused-vars for (const [nodeIndex, { actor, pxShapeMap }] of this.nodeToActor.entries()) { @@ -395,6 +434,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Advances the physics simulation by one frame. Checks for a pending reset, + * updates debug visualization, runs {@link subStepSimulation}, then reads back + * actor poses and propagates them to the corresponding glTF nodes and their children. + * + * @param {object} state - The current viewer state. + * @param {number} deltaTime - The elapsed time since the last frame, in seconds. + */ simulateStep(state, deltaTime) { if (!this.scene) { this.reset = false; @@ -464,7 +511,7 @@ class NvidiaPhysicsInterface extends PhysicsInterface { node.scaledPhysicsTransform = scaledPhysicsTransform; for (const childIndex of node.children) { const childNode = state.gltf.nodes[childIndex]; - this.applyTransformRecursively( + PhysicsUtils.applyTransformRecursively( state.gltf, childNode, node.scaledPhysicsTransform @@ -474,11 +521,21 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Schedules a simulation reset on the next call to {@link simulateStep}. + * Triggers {@link _resetSimulation} immediately by calling `simulateStep` with a + * zero delta time. + */ resetSimulation() { this.reset = true; this.simulateStep({}, 0); } + /** + * Immediately tears down the current PhysX scene, releasing all actors, shapes, + * joints, meshes, materials, and filter data. Clears all internal caches so + * the simulation can be re-initialized from scratch. + */ _resetSimulation() { const scenePointer = this.scene; this.scene = undefined; @@ -528,6 +585,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.shapeToNode.clear(); } + /** + * Retrieves the current debug render-buffer line data from the PhysX scene. + * Returns an interleaved flat array of `[x0, y0, z0, x1, y1, z1, ...]` for + * each debug line segment. + * + * @returns {number[]} A flat array of line endpoint coordinates, or an empty + * array if there is no active scene or debug visualization is disabled. + */ getDebugLineData() { if (!this.scene || (this.debugColliders === false && this.debugJoints === false)) { return []; @@ -547,6 +612,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return result; } + /** + * Applies a linear and an angular impulse to the dynamic actor associated with + * the given node index. + * + * @param {number} nodeIndex - Index of the target glTF node. + * @param {number[]} linearImpulse - World-space linear impulse as `[x, y, z]`. + * @param {number[]} angularImpulse - World-space angular impulse as `[x, y, z]`. + */ applyImpulse(nodeIndex, linearImpulse, angularImpulse) { if (!this.scene) { return; @@ -569,6 +642,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(angImpulse); } + /** + * Applies a linear impulse at a specific world-space position on the actor + * associated with the given node index. The off-centre application will also + * generate a corresponding angular impulse. + * + * @param {number} nodeIndex - Index of the target glTF node. + * @param {number[]} impulse - World-space impulse vector as `[x, y, z]`. + * @param {number[]} position - World-space point of application as `[x, y, z]`. + */ applyPointImpulse(nodeIndex, impulse, position) { if (!this.scene) { return; @@ -595,6 +677,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(pxPosition); } + /** + * Performs a ray-cast between two world-space points and returns information + * about the first shape hit. + * + * @param {number[]} rayStart - World-space ray origin as `[x, y, z]`. + * @param {number[]} rayEnd - World-space ray terminus as `[x, y, z]`. + * @returns {{ hitNodeIndex: number, hitFraction?: number, hitNormal?: Float32Array }} + * 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. + */ rayCast(rayStart, rayEnd) { const result = {}; result.hitNodeIndex = -1; @@ -656,6 +748,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region Updates + /** + * Synchronises the PhysX actor pose with the node's current world transform when + * the node has been flagged as dirty (e.g. by an animation). For kinematic actors + * `setKinematicTarget` is used; for others `setGlobalPose` is used directly. + * + * @param {object} node - The glTF node whose actor transform should be updated. + */ updateActorTransform(node) { if (node.dirtyTransform) { const actor = this.nodeToActor.get(node.gltfObjectIndex)?.actor; @@ -680,6 +779,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Updates the PhysX material parameters (static friction, dynamic friction, + * restitution) for any glTF physics materials that have been marked dirty. + * + * @param {object} gltf - The glTF asset whose `KHR_physics_rigid_bodies` extension + * contains the physics materials list. + */ updatePhysicMaterials(gltf) { const materials = gltf.extensions?.KHR_physics_rigid_bodies?.physicsMaterials; if (materials === undefined) { @@ -696,6 +802,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Applies any dirty motion-property changes (kinematic flag, mass, inertia, + * gravity factor, linear velocity, angular velocity) from the glTF node's motion + * extension to the corresponding PhysX actor. + * + * @param {object} actorNode - The glTF node whose motion properties should be updated. + */ updateMotion(actorNode) { const motion = actorNode.extensions?.KHR_physics_rigid_bodies?.motion; const actor = this.nodeToActor.get(actorNode.gltfObjectIndex).actor; @@ -748,6 +861,20 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Updates the geometry and/or local pose of a collider shape attached to an actor. + * Handles scale changes for convex/triangle mesh geometries and recreates simple + * shapes when their defining properties have changed. + * + * @param {object} gltf - The glTF asset. + * @param {object} node - The node that owns the collider shape. + * @param {object} collider - The glTF collider descriptor for the node. + * @param {object} actorNode - The node that owns the PhysX actor. + * @param {Float32Array} worldTransform - The 4x4 world transform of the collider node. + * @param {boolean} offsetChanged - `true` if the shape's local pose must be recomputed. + * @param {boolean} scaleChanged - `true` if the geometry scale must be updated. + * @param {boolean} isTrigger - `true` if the shape is a trigger volume. + */ updateCollider( gltf, node, @@ -872,6 +999,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Propagates dirty joint-property changes (collision flag, limits, drives) from + * the glTF joint extension to the corresponding PhysX D6 joint constraints. + * + * @param {object} state - The current viewer state, providing access to the glTF asset. + * @param {object} jointNode - The glTF node whose joint properties should be updated. + */ updatePhysicsJoint(state, jointNode) { const pxJoints = this.nodeToSimplifiedJoints.get(jointNode.gltfObjectIndex); if (pxJoints === undefined) { @@ -976,6 +1110,21 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region Geometry + /** + * Creates or updates a PhysX box geometry with the given dimensions and scale. + * If a non-uniform scale with a non-identity axis quaternion is detected the box + * is approximated as a convex mesh instead. + * + * @param {number} x - Full extent along the X axis (before scaling). + * @param {number} y - Full extent along the Y axis (before scaling). + * @param {number} z - Full extent along the Z axis (before scaling). + * @param {number[]} scale - Per-axis scale factors as `[sx, sy, sz]`. + * @param {quat} scaleAxis - Quaternion describing the orientation of the scale axes. + * @param {object} [reference] - An existing PhysX geometry object to update in-place. + * If supplied and the type matches, the geometry is mutated rather than re-created. + * @returns {object|undefined} A new `PxBoxGeometry` (or `PxConvexMeshGeometry`), + * or `undefined` when the reference geometry was updated in-place. + */ // Either create a box or update an existing one. Returns only newly created geometry generateBox(x, y, z, scale, scaleAxis, reference) { let referenceType = undefined; @@ -1009,11 +1158,35 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } + /** + * Creates a PhysX convex mesh geometry approximating a capsule shape. + * + * @param {number} height - The height of the cylindrical mid-section. + * @param {number} radiusTop - Radius of the top hemisphere. + * @param {number} radiusBottom - Radius of the bottom hemisphere. + * @param {number[]} scale - Per-axis scale factors as `[sx, sy, sz]`. + * @param {quat} scaleAxis - Quaternion describing the orientation of the scale axes. + * @param {object} _reference - Unused; reserved for API consistency. + * @returns {object} The created `PxConvexMeshGeometry`. + */ generateCapsule(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { const data = createCapsuleVertexData(radiusTop, radiusBottom, height); return this.createConvexPxMesh(data.vertices, scale, scaleAxis); } + /** + * Creates a PhysX convex mesh geometry approximating a cylinder shape. + * Falls back to a scaled convex hull representation when non-uniform scaling + * or different top/bottom radii are detected. + * + * @param {number} height - The height of the cylinder. + * @param {number} radiusTop - Radius of the top face. + * @param {number} radiusBottom - Radius of the bottom face. + * @param {number[]} scale - Per-axis scale factors as `[sx, sy, sz]`. + * @param {quat} scaleAxis - Quaternion describing the orientation of the scale axes. + * @param {object} _reference - Unused; reserved for API consistency. + * @returns {object} The created `PxConvexMeshGeometry`. + */ generateCylinder(height, radiusTop, radiusBottom, scale, scaleAxis, _reference) { if ( (quat.equals(scaleAxis, quat.create()) === false && @@ -1031,6 +1204,17 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return this.createConvexPxMesh(data.vertices); } + /** + * Creates or updates a PhysX sphere geometry. If a non-uniform scale is detected + * the sphere is approximated as a convex mesh instead. + * + * @param {number} radius - The radius of the sphere before scaling. + * @param {number[]} scale - Per-axis scale factors as `[sx, sy, sz]`. + * @param {quat} scaleAxis - Quaternion describing the orientation of the scale axes. + * @param {object} [reference] - An existing PhysX geometry object to update in-place. + * @returns {object|undefined} A new `PxSphereGeometry` (or `PxConvexMeshGeometry`), + * or `undefined` when the reference geometry was updated in-place. + */ generateSphere(radius, scale, scaleAxis, reference) { let referenceType = undefined; if (reference !== undefined) { @@ -1050,6 +1234,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } + /** + * Creates a PhysX infinite plane geometry. + * + * @param {object} [reference] - An existing PhysX geometry object. If supplied, + * no new geometry is created because plane geometry has no mutable properties. + * @returns {object|undefined} A new `PxPlaneGeometry`, or `undefined` if a + * reference was provided. + */ generatePlane(reference) { if (reference !== undefined) { // Nothing to update @@ -1059,6 +1251,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } + /** + * Cooks a PhysX convex mesh from the provided vertex data and wraps it in a + * `PxConvexMeshGeometry` with the given scale and scale-axis rotation. + * The resulting `PxConvexMesh` is tracked in {@link convexMeshes} for later cleanup. + * + * @param {Float32Array} vertices - Flat array of vertex positions `[x, y, z, ...]`. + * @param {number[]} [scale] - Per-axis scale factors; defaults to `[1, 1, 1]`. + * @param {quat} [scaleAxis] - Quaternion for scale-axis rotation; defaults to identity. + * @returns {object} The created `PxConvexMeshGeometry`. + */ createConvexPxMesh(vertices, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { const malloc = (f, q) => { const nDataBytes = f.length * f.BYTES_PER_ELEMENT; @@ -1097,6 +1299,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } + /** + * Extracts and flattens vertex position and (optionally) index data from all + * primitives of a glTF mesh. Morph targets are applied on the CPU using the + * mesh's current weights. Triangle-strip and triangle-fan primitives are + * converted to indexed triangles automatically. + * + * @param {object} gltf - The glTF asset. + * @param {object} mesh - The glTF mesh to collect data from. + * @param {boolean} [computeIndices=true] - Whether to collect index data in + * addition to vertex positions. + * @returns {{ vertices: Float32Array, indices: Uint32Array }} The collected + * geometry data. + */ collectVerticesAndIndicesFromMesh(gltf, mesh, computeIndices = true) { let positionDataArray = []; let positionCount = 0; @@ -1177,11 +1392,30 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return { vertices: positionData, indices: indexData }; } + /** + * Creates a PhysX convex hull mesh geometry from the vertex data of a glTF mesh. + * + * @param {object} gltf - The glTF asset. + * @param {object} mesh - The glTF mesh to build the convex hull from. + * @param {number[]} [scale] - Per-axis scale factors; defaults to `[1, 1, 1]`. + * @param {quat} [scaleAxis] - Quaternion for scale-axis rotation; defaults to identity. + * @returns {object} The created `PxConvexMeshGeometry`. + */ createConvexMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { const result = this.collectVerticesAndIndicesFromMesh(gltf, mesh, false); return this.createConvexPxMesh(result.vertices, scale, scaleAxis); } + /** + * Cooks a PhysX triangle mesh from the vertex and index data of a glTF mesh. + * The resulting `PxTriangleMesh` is tracked in {@link triangleMeshes} for later cleanup. + * + * @param {object} gltf - The glTF asset. + * @param {object} mesh - The glTF mesh to build the triangle mesh from. + * @param {number[]} [scale] - Per-axis scale factors; defaults to `[1, 1, 1]`. + * @param {quat} [scaleAxis] - Quaternion for scale-axis rotation; defaults to identity. + * @returns {object} The created `PxTriangleMeshGeometry`. + */ createPxMesh(gltf, mesh, scale = vec3.fromValues(1, 1, 1), scaleAxis = quat.create()) { const { vertices, indices } = this.collectVerticesAndIndicesFromMesh(gltf, mesh, true); const malloc = (f, q) => { @@ -1229,6 +1463,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return geometry; } + /** + * Determines whether two collision filters should generate contacts with each other. + * `filterB`'s include/exclude system lists are checked against `filterA`'s + * collision systems. + * + * @param {object} filterA - The first collision filter descriptor. + * @param {object} filterB - The second collision filter descriptor whose rules are evaluated. + * @returns {boolean} `true` if the two filters should produce collision events. + */ collidesWith(filterA, filterB) { if (filterB.collideWithSystems.length > 0) { for (const system of filterB.collideWithSystems) { @@ -1252,6 +1495,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region Shapes + /** + * Pre-computes a bit-mask collision matrix for all collision filters defined in + * the glTF `KHR_physics_rigid_bodies` extension. Each entry `filterData[i]` is a + * bitmask of filter indices that filter `i` should collide with. Index 31 is + * reserved as the default filter (collides with everything). A maximum of 31 + * user-defined filters are supported. + * + * @param {object} gltf - The glTF asset containing the collision filter definitions. + */ computeFilterData(gltf) { // Default filter is sign bit const filters = gltf.extensions?.KHR_physics_rigid_bodies?.collisionFilters; @@ -1279,6 +1531,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Creates a new PhysX material from a glTF physics material descriptor. + * Returns the default material when `undefined` is passed. + * + * @param {object|undefined} gltfPhysicsMaterial - The glTF physics material + * (from `KHR_physics_rigid_bodies`), or `undefined` to use the default material. + * @returns {object} The created (or default) `PxMaterial`. + */ createPhysXMaterial(gltfPhysicsMaterial) { if (gltfPhysicsMaterial === undefined) { return this.defaultMaterial; @@ -1302,6 +1562,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return physxMaterial; } + /** + * Creates a `PxFilterData` object encoding the collision filter bit-masks for a + * given filter index, along with CCD contact detection flags. + * + * @param {number|undefined} collisionFilter - Index into the pre-computed filter + * table, or `undefined` to apply the default filter (collides with everything). + * @param {number} [additionalFlags=0] - Extra `PxPairFlag` bits to OR into word2. + * @returns {object} The created `PxFilterData`. + */ createPhysXCollisionFilter(collisionFilter, additionalFlags = 0) { let word0 = null; let word1 = null; @@ -1320,6 +1589,22 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return new this.PhysX.PxFilterData(word0, word1, additionalFlags, 0); } + /** + * Creates a PhysX shape from an existing geometry object, assigning the correct + * material and collision filter data. Material and filter data are resolved from + * the glTF collider descriptor when not provided explicitly. + * + * @param {object} geometry - The PhysX geometry to attach to the shape. + * @param {object|undefined} physXMaterial - The `PxMaterial` to use, or `undefined` + * to derive it from `glTFCollider`. + * @param {object|undefined} physXFilterData - The `PxFilterData` to use, or `undefined` + * to derive it from `glTFCollider`. + * @param {object} shapeFlags - `PxShapeFlags` controlling the shape's role + * (simulation, scene-query, trigger, etc.). + * @param {object} glTFCollider - The glTF collider descriptor used to resolve + * material and filter data when the explicit arguments are `undefined`. + * @returns {object} The created `PxShape`. + */ createShapeFromGeometry(geometry, physXMaterial, physXFilterData, shapeFlags, glTFCollider) { if (physXMaterial === undefined) { if (glTFCollider?.physicsMaterial !== undefined) { @@ -1342,6 +1627,24 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } + /** + * Creates a PhysX shape for a given glTF node and collider descriptor. + * Resolves the geometry from either an implicit shape index or a glTF mesh, + * then delegates to {@link createShapeFromGeometry} and records the + * shape-to-node mapping. + * + * @param {object} gltf - The glTF asset. + * @param {object} node - The glTF node the shape belongs to. + * @param {object} collider - The glTF collider descriptor. + * @param {object} shapeFlags - `PxShapeFlags` for the new shape. + * @param {object|undefined} physXMaterial - Override material, or `undefined`. + * @param {object|undefined} physXFilterData - Override filter data, or `undefined`. + * @param {boolean} convexHull - `true` to force a convex-hull mesh for mesh colliders. + * @param {number[]} [scale] - Per-axis scale factors; defaults to `[1, 1, 1]`. + * @param {quat} [scaleAxis] - Quaternion for scale-axis rotation; defaults to identity. + * @returns {object|undefined} The created `PxShape`, or `undefined` if no + * geometry could be resolved. + */ createShape( gltf, node, @@ -1387,6 +1690,13 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return shape; } + /** + * Maps a glTF material friction/restitution combine mode string to the + * corresponding PhysX `PxCombineModeEnum` value. + * + * @param {string} mode - One of `'average'`, `'minimum'`, `'maximum'`, or `'multiply'`. + * @returns {number} The matching `PxCombineModeEnum` constant. + */ mapCombineMode(mode) { switch (mode) { case "average": @@ -1404,6 +1714,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region Actors + /** + * Creates a PhysX rigid body actor for a glTF node, attaches all collider and + * trigger shapes (including those from child nodes), configures mass/inertia + * and initial velocities, then adds the actor to the active scene. + * + * @param {object} gltf - The glTF asset. + * @param {object} node - The root node of the actor. + * @param {object} shapeFlags - `PxShapeFlags` used for simulation/query shapes. + * @param {object} triggerFlags - `PxShapeFlags` used for trigger shapes. + * @param {'static'|'kinematic'|'dynamic'|'trigger'} type - Actor type. + * @param {boolean} [noMeshShapes=false] - When `true`, mesh colliders are + * treated as convex hulls rather than exact triangle meshes. + */ createActor(gltf, node, shapeFlags, triggerFlags, type, noMeshShapes = false) { const worldTransform = node.worldTransform; const translation = vec3.create(); @@ -1592,6 +1915,18 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToActor.set(node.gltfObjectIndex, { actor, pxShapeMap: pxShapeMap }); } + /** + * Walks up the node hierarchy to find the nearest ancestor that owns a PhysX + * actor, then computes the local offset (position and rotation) of the given + * node relative to that actor's frame. Used to determine joint attachment frames. + * + * @param {object} node - The joint attachment node. + * @param {object} referencedJoint - The simplified joint descriptor, which may + * supply a local rotation override. + * @returns {{ actor: object|undefined, offsetPosition: vec3, offsetRotation: quat }} + * The resolved actor (or `undefined` for world-relative), offset position, + * and offset rotation. + */ computeJointOffsetAndActor(node, referencedJoint) { let currentNode = node; while (currentNode !== undefined) { @@ -1630,6 +1965,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return { actor: actor, offsetPosition: offsetPosition, offsetRotation: offsetRotation }; } + /** + * Configures the mass, inertia tensor, and center-of-mass pose of a PhysX + * rigid body actor from the `KHR_physics_rigid_bodies` motion properties. + * Falls back to automatic mass/inertia estimation via `PxRigidBodyExt` when + * no explicit values are provided. + * + * @param {object} motion - The glTF motion extension object containing mass, + * inertiaDiagonal, inertiaOrientation, and centerOfMass properties. + * @param {object} actor - The PhysX `PxRigidDynamic` actor to configure. + */ calculateMassAndInertia(motion, actor) { const pos = new this.PhysX.PxVec3(0, 0, 0); if (motion.centerOfMass !== undefined) { @@ -1719,6 +2064,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { //region Joints + /** + * Converts a zero-based axis index and a motion type to the corresponding + * PhysX `PxD6AxisEnum` value. + * + * @param {0|1|2} axisIndex - The axis index (0 = X/Twist, 1 = Y/Swing1, 2 = Z/Swing2). + * @param {'linear'|'angular'} type - Whether to interpret the index as a linear + * or angular axis. + * @returns {number|null} The matching `PxD6AxisEnum` constant, or `null` if the + * combination is not recognised. + */ convertAxisIndexToEnum(axisIndex, type) { if (type === "linear") { switch (axisIndex) { @@ -1742,6 +2097,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return null; } + /** + * Converts a zero-based axis index to the corresponding PhysX D6 angular drive + * enum value. + * + * @param {0|1|2} axisIndex - The axis index (0 = Twist, 1 = Swing1, 2 = Swing2). + * @returns {number|null} The matching `PxD6DriveEnum` constant (or a raw integer + * for axes not yet exposed by the bindings), or `null` if not recognised. + */ convertAxisIndexToAngularDriveEnum(axisIndex) { switch (axisIndex) { case 0: @@ -1754,6 +2117,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return null; } + /** + * Checks whether the two swing limits of a simplified joint are symmetric, + * which determines whether a cone limit or a pyramid limit should be used in PhysX. + * + * @param {object} joint - The simplified joint descriptor containing optional + * `swingLimit1` and `swingLimit2` properties. + * @returns {boolean} `true` if both limits are symmetric (centred around zero), + * allowing a cone limit; `false` if a pyramid limit is required. + */ validateSwingLimits(joint) { // Check if swing limits are symmetric (cone) or asymmetric (pyramid) if (joint.swingLimit1 && joint.swingLimit2) { @@ -1771,6 +2143,15 @@ class NvidiaPhysicsInterface extends PhysicsInterface { return false; } + /** + * Creates the PhysX `PxD6Joint` constraints for a glTF joint node. Each simplified + * physics joint defined on the referenced glTF joint is converted to one PhysX joint, + * and the results are stored in {@link nodeToSimplifiedJoints}. + * + * @param {object} gltf - The glTF asset. + * @param {object} node - The glTF node carrying the `KHR_physics_rigid_bodies.joint` + * extension data. + */ createJoint(gltf, node) { const joint = node.extensions?.KHR_physics_rigid_bodies?.joint; const referencedJoint = @@ -1788,6 +2169,17 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.nodeToSimplifiedJoints.set(node.gltfObjectIndex, simplifiedJoints); } + /** + * Applies the motion and limit parameters for a single simplified-joint limit to + * a PhysX D6 joint. Handles linear pair limits, distance limits (3-axis linear + * constraint), and angular axis locks. + * + * @param {object} physxJoint - The PhysX `PxD6Joint` to configure. + * @param {object} simplifiedJoint - The simplified joint descriptor supplying + * axis-mapping helpers. + * @param {object} limit - The individual limit descriptor (with `linearAxes`, + * `angularAxes`, `min`, `max`, `stiffness`, `damping`). + */ _setLimitValues(physxJoint, simplifiedJoint, limit) { const lock = limit.min === 0 && limit.max === 0; const spring = new this.PhysX.PxSpring(limit.stiffness ?? 0, limit.damping); @@ -1839,6 +2231,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(spring); } + /** + * Applies the twist-limit angular range from a simplified joint to a PhysX D6 + * joint. Does nothing if the simplified joint has no twist limit defined. + * + * @param {object} physxJoint - The PhysX `PxD6Joint` to configure. + * @param {object} simplifiedJoint - The simplified joint descriptor containing + * the optional `twistLimit` property. + */ _setTwistLimitValues(physxJoint, simplifiedJoint) { if (simplifiedJoint.twistLimit !== undefined) { if (!(simplifiedJoint.twistLimit.min === 0 && simplifiedJoint.twistLimit.max === 0)) { @@ -1856,6 +2256,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Applies the swing-limit angular ranges from a simplified joint to a PhysX D6 + * joint. Uses a cone limit when both swing axes are symmetric, a pyramid limit + * when they are asymmetric, and falls back to a single-axis cone when only one + * swing limit is defined. + * + * @param {object} physxJoint - The PhysX `PxD6Joint` to configure. + * @param {object} simplifiedJoint - The simplified joint descriptor containing + * optional `swingLimit1` and/or `swingLimit2` properties. + */ _setSwingLimitValues(physxJoint, simplifiedJoint) { if ( simplifiedJoint.swingLimit1 !== undefined && @@ -1942,6 +2352,16 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Creates and assigns a `PxD6JointDrive` to the appropriate axis of a PhysX D6 + * joint, configuring its stiffness, damping, maximum force, and drive mode. + * + * @param {object} physxJoint - The PhysX `PxD6Joint` to configure. + * @param {object} simplifiedJoint - The simplified joint descriptor supplying + * axis-mapping helpers. + * @param {object} drive - The drive descriptor with `stiffness`, `damping`, + * `maxForce`, `mode`, `type`, and `axis`. + */ _setDriveValues(physxJoint, simplifiedJoint, drive) { const physxDrive = new this.PhysX.PxD6JointDrive( drive.stiffness, @@ -1960,6 +2380,19 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(physxDrive); } + /** + * Accumulates the velocity target for a single drive axis into the provided + * mutable velocity target vectors. Linear and angular axes are handled separately. + * + * @param {object} simplifiedJoint - The simplified joint descriptor supplying + * axis-mapping helpers. + * @param {object} drive - The drive descriptor containing `type`, `axis`, and + * `velocityTarget`. + * @param {object} linearVelocityTarget - `PxVec3` accumulator for linear targets; + * mutated in place. + * @param {object} angularVelocityTarget - `PxVec3` accumulator for angular targets; + * mutated in place. + */ _getDriveVelocityTarget(simplifiedJoint, drive, linearVelocityTarget, angularVelocityTarget) { const result = simplifiedJoint.getRotatedAxisAndSign(drive.axis); if (drive.type === "linear") { @@ -1973,6 +2406,14 @@ class NvidiaPhysicsInterface extends PhysicsInterface { } } + /** + * Computes the aggregate position and orientation drive targets from all drives + * defined on a simplified joint and applies them to the PhysX D6 joint via `setDrivePosition`. + * + * @param {object} physxJoint - The PhysX `PxD6Joint` to configure. + * @param {object} simplifiedJoint - The simplified joint descriptor containing + * the `drives` array. + */ _setDrivePositionTarget(physxJoint, simplifiedJoint) { const positionTarget = vec3.fromValues(0, 0, 0); const angleTarget = quat.create(); @@ -2023,6 +2464,18 @@ class NvidiaPhysicsInterface extends PhysicsInterface { this.PhysX.destroy(targetTransform); } + /** + * Creates a fully configured PhysX `PxD6Joint` for a single simplified joint + * descriptor. Resolves actor references and frame offsets for both bodies, + * sets all motion axes to free, then applies limits, drives, and drive targets + * from the simplified joint data. + * + * @param {object} gltf - The glTF asset. + * @param {object} node - The glTF node that owns the joint. + * @param {object} joint - The `KHR_physics_rigid_bodies.joint` extension data. + * @param {object} simplifiedJoint - The simplified joint descriptor to materialise. + * @returns {object} The created `PxD6Joint`. + */ createSimplifiedJoint(gltf, node, joint, simplifiedJoint) { const resultA = this.computeJointOffsetAndActor(node, simplifiedJoint); const resultB = this.computeJointOffsetAndActor( diff --git a/source/gltf/physics_utils.js b/source/gltf/physics_utils.js index 5989aa59..d144f4b1 100644 --- a/source/gltf/physics_utils.js +++ b/source/gltf/physics_utils.js @@ -1,4 +1,4 @@ -import { quat, vec3 } from "gl-matrix"; +import { quat, vec3, mat4 } from "gl-matrix"; class PhysicsUtils { /** @@ -84,6 +84,29 @@ class PhysicsUtils { return triangleIndices; } + /** + * Recursively propagates a parent world transform down the node hierarchy, + * computing and storing the scaled physics transform for each non-motion node. + * Stops traversal at nodes that carry motion data. + * + * @param {object} gltf - The glTF asset containing the full node array. + * @param {object} node - The current node to process. + * @param {Float32Array} parentTransform - The 4x4 world transform of the parent node. + */ + static applyTransformRecursively(gltf, node, parentTransform) { + if (node.extensions?.KHR_physics_rigid_bodies?.motion !== undefined) { + return; + } + const localTransform = node.getLocalTransform(); + const globalTransform = mat4.create(); + mat4.multiply(globalTransform, parentTransform, localTransform); + node.scaledPhysicsTransform = globalTransform; + for (const childIndex of node.children) { + const childNode = gltf.nodes[childIndex]; + this.applyTransformRecursively(gltf, childNode, globalTransform); + } + } + /** * Recursively traverses the node hierarchy of a motion to find all colliders and triggers, and applies the custom function to them. * The custom function has the following signature: From 1f254cfb24cbc0adafcf59285fd6d6fae1639f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Thu, 26 Mar 2026 16:32:11 +0100 Subject: [PATCH 93/93] Update documentation --- API.md | 155 ++++++++++++++++++ PhysicsEngines.md | 36 ++++ README.md | 93 +++++++++++ package.json | 2 +- source/GltfState/gltf_state.js | 2 +- ...cs_controller.js => physics_controller.js} | 35 +++- source/GltfView/gltf_view.js | 1 - 7 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 PhysicsEngines.md rename source/GltfState/{phyiscs_controller.js => physics_controller.js} (90%) diff --git a/API.md b/API.md index f0d56285..128d7327 100644 --- a/API.md +++ b/API.md @@ -19,6 +19,9 @@ that are then used to display the loaded data with GltfView

GraphController

A controller for managing KHR_interactivity graphs in a glTF scene.

+
PhysicsController
+

Controller for managing the physics simulation of a glTF scene.

+
@@ -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 [GraphController](#GraphController) + + +## 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) ⇒ Object + + + +### physicsController.initializeEngine(engine) +Initializes the physics engine. This must be called before loading any scenes. +Currently, only "NvidiaPhysX" is supported. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| engine | string | + + + +### 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 [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| state | [GltfState](#GltfState) | +| sceneIndex | number | + + + +### physicsController.resetScene(gltf) +Resets the current physics state. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| gltf | glTF | + + + +### physicsController.resumeSimulation() +Resumes the physics simulation if it was paused. If the simulation is not paused, this function does nothing. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + + +### physicsController.pauseSimulation() +Pauses the physics simulation. If the simulation is already paused, this function does nothing. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + + +### 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 [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| state | [GltfState](#GltfState) | +| deltaTime | number | + + + +### physicsController.enableDebugColliders(enable) +Enable debug visualization of physics colliders. +The exact visualization depends on the physics engine implementation. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| enable | boolean | + + + +### physicsController.enableDebugJoints(enable) +Enable debug visualization of physics joints. +The exact visualization depends on the physics engine implementation. + +**Kind**: instance method of [PhysicsController](#PhysicsController) + +| Param | Type | +| --- | --- | +| enable | boolean | + + + +### 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 [PhysicsController](#PhysicsController) + +| Param | Type | Description | +| --- | --- | --- | +| nodeIndex | number | glTF node index of the target dynamic actor. | +| linearImpulse | vec3 | Impulse vector applied to the center of mass, in world space (kg⋅m/s). | +| angularImpulse | vec3 | Angular impulse vector applied around the center of mass, in world space (kg⋅m²/s). | + + + +### 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 [PhysicsController](#PhysicsController) + +| Param | Type | Description | +| --- | --- | --- | +| nodeIndex | number | glTF node index of the target dynamic actor. | +| impulse | vec3 | Impulse vector to apply, in world space (kg⋅m/s). | +| position | vec3 | World-space position at which the impulse is applied. | + + + +### physicsController.rayCast(rayStart, rayEnd) ⇒ Object +Performs a ray-cast between two world-space points and returns information +about the first shape hit. + +**Kind**: instance method of [PhysicsController](#PhysicsController) +**Returns**: Object - 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 | Array.<number> | World-space ray origin as `[x, y, z]`. | +| rayEnd | Array.<number> | World-space ray terminus as `[x, y, z]`. | + diff --git a/PhysicsEngines.md b/PhysicsEngines.md new file mode 100644 index 00000000..6b93da6a --- /dev/null +++ b/PhysicsEngines.md @@ -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. diff --git a/README.md b/README.md index 66cc8e96..6a851537 100644 --- a/README.md +++ b/README.md @@ -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) @@ -64,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) @@ -96,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.* @@ -124,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. diff --git a/package.json b/package.json index 1520feae..bdd9a6a1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "testApp": "npm run build & npx serve ./dist", "test": "npx playwright test", "prepublishOnly": "npm run build && npm run build_docs", - "build_docs": "jsdoc2md source/gltf-sample-renderer.js source/GltfView/gltf_view.js source/GltfState/gltf_state.js source/GltfState/animation_timer.js source/ResourceLoader/resource_loader.js source/gltf/user_camera.js source/gltf/interactivity.js > API.md", + "build_docs": "jsdoc2md source/gltf-sample-renderer.js source/GltfView/gltf_view.js source/GltfState/gltf_state.js source/GltfState/animation_timer.js source/ResourceLoader/resource_loader.js source/gltf/user_camera.js source/gltf/interactivity.js source/GltfState/physics_controller.js > API.md", "lint": "eslint source/**/*.js", "lint:fix": "eslint --fix source/**/*.js", "prettier": "npx prettier source/**/*.js tests/**/*.js tests/**/*.ts --check", diff --git a/source/GltfState/gltf_state.js b/source/GltfState/gltf_state.js index 8a90f893..5ee3eba8 100644 --- a/source/GltfState/gltf_state.js +++ b/source/GltfState/gltf_state.js @@ -1,7 +1,7 @@ import { GraphController } from "../gltf/interactivity.js"; import { UserCamera } from "../gltf/user_camera.js"; import { AnimationTimer } from "./animation_timer.js"; -import { PhysicsController } from "./phyiscs_controller.js"; +import { PhysicsController } from "./physics_controller.js"; /** * GltfState containing a state for visualization in GltfView diff --git a/source/GltfState/phyiscs_controller.js b/source/GltfState/physics_controller.js similarity index 90% rename from source/GltfState/phyiscs_controller.js rename to source/GltfState/physics_controller.js index 957e0d5e..ecdbf818 100644 --- a/source/GltfState/phyiscs_controller.js +++ b/source/GltfState/physics_controller.js @@ -2,6 +2,9 @@ import { getAnimatedIndices } from "../gltf/gltf_utils"; import { NvidiaPhysicsInterface } from "../PhysicsEngines/PhysX"; import { PhysicsUtils } from "../gltf/physics_utils"; +/** + * Controller for managing the physics simulation of a glTF scene. + */ class PhysicsController { constructor() { this.engine = undefined; @@ -277,7 +280,7 @@ class PhysicsController { * 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, in this case simulationStepTime is used as deltaTime. + * Can also be used to manually advance the simulation when it is paused. * @param {GltfState} state * @param {number} deltaTime */ @@ -295,11 +298,9 @@ class PhysicsController { return; } this.timeAccumulator += deltaTime; - if (this.pauseTime !== undefined) { + if (this.pauseTime !== undefined && this.playing) { this.timeAccumulator = this.simulationStepTime; - if (this.playing) { - this.pauseTime = undefined; - } + this.pauseTime = undefined; } if ( this.enabled && @@ -426,14 +427,38 @@ class PhysicsController { // Functions called by KHR_interactivity + /** + * 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. + * @param {number} nodeIndex - glTF node index of the target dynamic actor. + * @param {vec3} linearImpulse - Impulse vector applied to the center of mass, in world space (kg⋅m/s). + * @param {vec3} angularImpulse - Angular impulse vector applied around the center of mass, in world space (kg⋅m²/s). + */ applyImpulse(nodeIndex, linearImpulse, angularImpulse) { this.engine.applyImpulse(nodeIndex, linearImpulse, angularImpulse); } + /** + * 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. + * @param {number} nodeIndex - glTF node index of the target dynamic actor. + * @param {vec3} impulse - Impulse vector to apply, in world space (kg⋅m/s). + * @param {vec3} position - World-space position at which the impulse is applied. + */ applyPointImpulse(nodeIndex, impulse, position) { this.engine.applyPointImpulse(nodeIndex, impulse, position); } + /** + * Performs a ray-cast between two world-space points and returns information + * about the first shape hit. + * + * @param {number[]} rayStart - World-space ray origin as `[x, y, z]`. + * @param {number[]} rayEnd - World-space ray terminus as `[x, y, z]`. + * @returns {{ hitNodeIndex: number, hitFraction: number | undefined, hitNormal: Float32Array | undefined}} + * 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. + */ rayCast(rayStart, rayEnd) { return this.engine.rayCast(rayStart, rayEnd); } diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index 92c8fff0..842ab165 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -1,4 +1,3 @@ -import { AnimatableProperty } from "../gltf/animatable_property.js"; import { GltfState } from "../GltfState/gltf_state.js"; import { gltfRenderer } from "../Renderer/renderer.js"; import { GL } from "../Renderer/webgl.js";