diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 207795ec32..fa65867c88 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -24,6 +24,8 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli private static _tempTranslation = new Vector3(); private static _tempRotation = new Quaternion(); + private _isKinematic = false; + constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) { super(physXPhysics); const transform = this._transform(position, rotation); @@ -160,30 +162,11 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli * {@inheritDoc IDynamicCollider.setCollisionDetectionMode } */ setCollisionDetectionMode(value: number): void { - const physX = this._physXPhysics._physX; - - switch (value) { - case CollisionDetectionMode.Continuous: - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, true); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, false); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, false); - break; - case CollisionDetectionMode.ContinuousDynamic: - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, true); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, true); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, false); - break; - case CollisionDetectionMode.ContinuousSpeculative: - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, false); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, false); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, true); - break; - case CollisionDetectionMode.Discrete: - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, false); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, false); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, false); - break; - } + const effectiveMode = + this._isKinematic && value !== CollisionDetectionMode.ContinuousSpeculative + ? CollisionDetectionMode.Discrete + : value; + this._applyCollisionDetectionFlags(effectiveMode); } /** @@ -197,11 +180,15 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli * {@inheritDoc IDynamicCollider.setIsKinematic } */ setIsKinematic(value: boolean): void { - if (value) { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, true); - } else { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, false); + if (this._isKinematic === value) return; + const physX = this._physXPhysics._physX; + // PhysX rejects swept CCD when eKINEMATIC is raised + if (value && this._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD)) { + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, false); + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, false); } + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, value); + this._isKinematic = value; } /** @@ -271,4 +258,24 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli wakeUp(): void { return this._pxActor.wakeUp(); } + + private _applyCollisionDetectionFlags(value: number): void { + const physX = this._physXPhysics._physX; + const enableCCD = value === CollisionDetectionMode.Continuous || value === CollisionDetectionMode.ContinuousDynamic; + // PhysX rejects enabling swept and speculative CCD at the same time + if (enableCCD) { + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, false); + } + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, enableCCD); + this._pxActor.setRigidBodyFlag( + physX.PxRigidBodyFlag.eENABLE_CCD_FRICTION, + value === CollisionDetectionMode.ContinuousDynamic + ); + if (!enableCCD) { + this._pxActor.setRigidBodyFlag( + physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, + value === CollisionDetectionMode.ContinuousSpeculative + ); + } + } } diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 0ed815b9ca..26c1fac13b 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -56,6 +56,21 @@ describe("DynamicCollider", function () { return Math.round(value * 100000) / 100000; } + function getPhysXRigidBody(collider: DynamicCollider) { + const nativeCollider = collider._nativeCollider as any; + return { + actor: nativeCollider._pxActor, + flags: nativeCollider._physXPhysics._physX.PxRigidBodyFlag + }; + } + + function expectFlagDisabledBeforeEnabled(calls: unknown[][], disabledFlag: unknown, enabledFlag: unknown) { + const disableCallIndex = calls.findIndex(([flag, enabled]) => flag === disabledFlag && enabled === false); + const enableCallIndex = calls.findIndex(([flag, enabled]) => flag === enabledFlag && enabled === true); + expect(disableCallIndex).toBeGreaterThanOrEqual(0); + expect(enableCallIndex).toBeGreaterThan(disableCallIndex); + } + beforeAll(async function () { engine = await WebGLEngine.create({ canvas: document.createElement("canvas"), @@ -503,6 +518,58 @@ describe("DynamicCollider", function () { ).toBeTruthy(); }); + it("defers swept CCD across kinematic transitions", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + const { actor, flags } = getPhysXRigidBody(boxCollider); + const setRigidBodyFlag = vi.spyOn(actor, "setRigidBodyFlag"); + const ccdFlag = () => actor.getRigidBodyFlags(flags.eENABLE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeTruthy(); + + setRigidBodyFlag.mockClear(); + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + expectFlagDisabledBeforeEnabled(setRigidBodyFlag.mock.calls, flags.eENABLE_CCD, flags.eKINEMATIC); + + setRigidBodyFlag.mockClear(); + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; + expect(setRigidBodyFlag).not.toHaveBeenCalledWith(flags.eENABLE_CCD, true); + expect(ccdFlag()).toBeFalsy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.ContinuousDynamic); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + expect(actor.getRigidBodyFlags(flags.eENABLE_CCD_FRICTION)).toBeTruthy(); + }); + + it("switches between swept and speculative CCD without overlapping flags", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + const { actor, flags } = getPhysXRigidBody(boxCollider); + const setRigidBodyFlag = vi.spyOn(actor, "setRigidBodyFlag"); + const ccdFlag = () => actor.getRigidBodyFlags(flags.eENABLE_CCD); + const speculativeCCDFlag = () => actor.getRigidBodyFlags(flags.eENABLE_SPECULATIVE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; + expect(speculativeCCDFlag()).toBeTruthy(); + + setRigidBodyFlag.mockClear(); + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expectFlagDisabledBeforeEnabled(setRigidBodyFlag.mock.calls, flags.eENABLE_SPECULATIVE_CCD, flags.eENABLE_CCD); + expect(ccdFlag()).toBeTruthy(); + expect(speculativeCCDFlag()).toBeFalsy(); + + setRigidBodyFlag.mockClear(); + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; + expectFlagDisabledBeforeEnabled(setRigidBodyFlag.mock.calls, flags.eENABLE_CCD, flags.eENABLE_SPECULATIVE_CCD); + expect(speculativeCCDFlag()).toBeTruthy(); + + boxCollider.isKinematic = true; + expect(speculativeCCDFlag()).toBeTruthy(); + }); + it("sleep", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider);