From a4b18401e2e07d849d062003c954772e4d9d4bee Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 17:11:59 +0800 Subject: [PATCH 1/5] fix(physics): preserve kinematic sync semantics --- .../core/src/physics/CharacterController.ts | 8 ++ packages/core/src/physics/Collider.ts | 38 ++++++- packages/core/src/physics/DynamicCollider.ts | 16 ++- .../physics-physx/src/PhysXDynamicCollider.ts | 66 +++++++---- .../src/core/physics/DynamicCollider.test.ts | 104 +++++++++++++++++- tests/src/core/physics/PhysicsScene.test.ts | 16 +-- 6 files changed, 209 insertions(+), 39 deletions(-) diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index f3c9e6f048..fd9020ad21 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -2,6 +2,7 @@ import { ICharacterController } from "@galacean/engine-design"; import { Vector3 } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { Entity } from "../Entity"; +import { Transform } from "../Transform"; import { Collider } from "./Collider"; import { ControllerNonWalkableMode } from "./enums/ControllerNonWalkableMode"; import { ColliderShape } from "./shape"; @@ -162,6 +163,13 @@ export class CharacterController extends Collider { (this._nativeCollider).setSlopeLimit(this._slopeLimit); } + protected override _teleportToEntityTransform( + worldPosition: Transform["worldPosition"], + _worldRotation: Transform["worldRotationQuaternion"] + ): void { + (this._nativeCollider).setWorldPosition(worldPosition); + } + private _syncWorldPositionFromPhysicalSpace(): void { (this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition); } diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index a8a591fe56..f1ffaed36b 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -28,6 +28,9 @@ export class Collider extends Component implements ICustomClone { protected _shapes: ColliderShape[] = []; protected _collisionLayerIndex: number = 0; + @ignoreClone + private _pendingTeleport: boolean = false; + /** * The shapes of this collider. */ @@ -108,12 +111,14 @@ export class Collider extends Component implements ICustomClone { * @internal */ _onUpdate(): void { - if (this._updateFlag.flag) { + if (this._pendingTeleport || this._updateFlag.flag) { const { transform } = this.entity; - (this._nativeCollider).setWorldTransform( - transform.worldPosition, - transform.worldRotationQuaternion - ); + if (this._pendingTeleport) { + this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); + this._pendingTeleport = false; + } else { + this._syncEntityTransformToNative(transform.worldPosition, transform.worldRotationQuaternion); + } const worldScale = transform.lossyWorldScale; const shapes = this._shapes; @@ -141,6 +146,7 @@ export class Collider extends Component implements ICustomClone { */ override _onDisableInScene(): void { this.scene.physics._removeCollider(this); + this._pendingTeleport = true; } /** @@ -164,6 +170,28 @@ export class Collider extends Component implements ICustomClone { this._addNativeShape(this.shapes[i]); } this._setCollisionLayer(); + // Parent transforms may not be cloned yet, so defer world-pose synchronization until the hierarchy is complete. + this._pendingTeleport = true; + } + + /** + * Teleport the native actor without implied velocity. + */ + protected _teleportToEntityTransform( + worldPosition: Transform["worldPosition"], + worldRotation: Transform["worldRotationQuaternion"] + ): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); + } + + /** + * Synchronize the entity transform to the native actor. + */ + protected _syncEntityTransformToNative( + worldPosition: Transform["worldPosition"], + worldRotation: Transform["worldRotationQuaternion"] + ): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 5c8725c604..8832eb3bf2 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -286,10 +286,9 @@ export class DynamicCollider extends Collider { this._isKinematic = value; (this._nativeCollider).setIsKinematic(value); + (this._nativeCollider).setCollisionDetectionMode(this._collisionDetectionMode); - // Resync properties that PhysX ignores/resets during kinematic mode if (!value) { - (this._nativeCollider).setCollisionDetectionMode(this._collisionDetectionMode); if (this._automaticCenterOfMass || this._automaticInertiaTensor) { this._setMassAndUpdateInertia(); } @@ -442,6 +441,19 @@ export class DynamicCollider extends Collider { super.addShape(shape); } + /** + * Synchronize kinematic transform changes through setKinematicTarget so PhysX + * generates contacts along the movement path. + * @internal + */ + protected override _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { + if (this._isKinematic) { + (this._nativeCollider).move(worldPosition, worldRotation); + } else { + super._syncEntityTransformToNative(worldPosition, worldRotation); + } + } + /** * @internal */ diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 207795ec32..4a6158eb7f 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: boolean = false; + constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) { super(physXPhysics); const transform = this._transform(position, rotation); @@ -160,8 +162,37 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli * {@inheritDoc IDynamicCollider.setCollisionDetectionMode } */ setCollisionDetectionMode(value: number): void { + const effectiveMode = + this._isKinematic && value !== CollisionDetectionMode.ContinuousSpeculative + ? CollisionDetectionMode.Discrete + : value; + this._applyCollisionDetectionFlags(effectiveMode); + } + + /** + * {@inheritDoc IDynamicCollider.setUseGravity } + */ + setUseGravity(value: boolean): void { + this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); + } + + /** + * {@inheritDoc IDynamicCollider.setIsKinematic } + */ + setIsKinematic(value: boolean): void { + if (this._isKinematic === value) return; const physX = this._physXPhysics._physX; + if (value) { + this._applyCollisionDetectionFlags(CollisionDetectionMode.Discrete); + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, true); + } else { + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, false); + } + this._isKinematic = value; + } + private _applyCollisionDetectionFlags(value: number): void { + const physX = this._physXPhysics._physX; switch (value) { case CollisionDetectionMode.Continuous: this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, true); @@ -186,24 +217,6 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli } } - /** - * {@inheritDoc IDynamicCollider.setUseGravity } - */ - setUseGravity(value: boolean): void { - this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); - } - - /** - * {@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); - } - } - /** * {@inheritDoc IDynamicCollider.setConstraints } */ @@ -234,20 +247,27 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.move } + * + * PhysX requires a normalized target rotation. */ move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void { + const tempTranslation = PhysXDynamicCollider._tempTranslation; + const tempRotation = PhysXDynamicCollider._tempRotation; + if (rotation) { - this._pxActor.setKinematicTarget(positionOrRotation, rotation); + tempRotation.copyFrom(rotation).normalize(); + this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); return; } - const tempTranslation = PhysXDynamicCollider._tempTranslation; - const tempRotation = PhysXDynamicCollider._tempRotation; - this.getWorldTransform(tempTranslation, tempRotation); if (positionOrRotation instanceof Vector3) { + this.getWorldTransform(tempTranslation, tempRotation); + // Rotations read from PhysX are already normalized. this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); } else { - this._pxActor.setKinematicTarget(tempTranslation, positionOrRotation); + this.getWorldTransform(tempTranslation, tempRotation); + tempRotation.copyFrom(positionOrRotation).normalize(); + this._pxActor.setKinematicTarget(tempTranslation, tempRotation); } } diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 0ed815b9ca..7e036ad71c 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -11,7 +11,7 @@ import { } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { PhysXPhysics, PhysXRuntimeMode } from "@galacean/engine-physics-physx"; -import { Vector3 } from "@galacean/engine-math"; +import { Quaternion, Vector3 } from "@galacean/engine-math"; import { vi, describe, beforeAll, beforeEach, expect, it } from "vitest"; const physXWasmModeUrl = new URL("../../../../packages/physics-physx/libs/physx.release.js", import.meta.url).href; @@ -459,6 +459,32 @@ describe("DynamicCollider", function () { expect(box.transform.position.y).below(1); }); + it("teleports kinematic target collider on re-enable instead of sweeping from stale native pose", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(-10, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.useGravity = false; + boxCollider.isKinematic = true; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + // @ts-ignore - intentionally observe the native boundary used by Collider sync. + const nativeCollider = boxCollider._nativeCollider; + const move = vi.spyOn(nativeCollider, "move"); + const setWorldTransform = vi.spyOn(nativeCollider, "setWorldTransform"); + + box.isActive = false; + box.transform.setPosition(10, 0, 0); + box.isActive = true; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(move).not.toHaveBeenCalled(); + expect(setWorldTransform).toHaveBeenCalledOnce(); + expect(formatValue(box.transform.position.x)).eq(10); + }); + it("constraints", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -503,6 +529,66 @@ describe("DynamicCollider", function () { ).toBeTruthy(); }); + it("CCD mode survives kinematic toggle", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeTruthy(); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + }); + + it("setCollisionDetectionMode in kinematic state defers native CCD flag application", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeFalsy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + }); + + it("keeps speculative CCD enabled for kinematic actors", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const speculativeCCDFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; + boxCollider.isKinematic = true; + expect(speculativeCCDFlag()).toBeTruthy(); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(speculativeCCDFlag()).toBeFalsy(); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; + 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); @@ -560,6 +646,22 @@ describe("DynamicCollider", function () { expect(formatValue(box.transform.position.x)).eq(1); }); + it("normalizes kinematic target rotations", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.isKinematic = true; + // @ts-ignore - observe the value crossing the PhysX boundary. + const setKinematicTarget = vi.spyOn(boxCollider._nativeCollider._pxActor, "setKinematicTarget"); + + boxCollider.move(new Quaternion(0, Math.SQRT2, 0, Math.SQRT2)); + + expect(setKinematicTarget).toHaveBeenCalledOnce(); + const rotation = setKinematicTarget.mock.calls[0][1]; + expect(formatValue(rotation.length())).eq(1); + expect(formatValue(Math.abs(rotation.y))).eq(formatValue(Math.SQRT1_2)); + expect(formatValue(Math.abs(rotation.w))).eq(formatValue(Math.SQRT1_2)); + }); + it("destroy", function () { const entity = rootEntity.createChild("collider"); const collider = entity.addComponent(DynamicCollider); diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index fae4d817c2..f09579d278 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -1488,15 +1488,15 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Test that collision works correctly, A is dynamic and kinematic, B is static. + // Kinematic targets generate contact events for static-kinematic pairs. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, false, false, false); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); @@ -1603,15 +1603,15 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Test that collision works correctly, both A,B are dynamic, kinematic. + // Kinematic targets generate contact events for kinematic pairs. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, true, false, true); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); From 48d247f885b5555efb578fb6e1a981f3a067e52b Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 17:53:33 +0800 Subject: [PATCH 2/5] refactor(physics): reuse collider transform sync --- .../core/src/physics/CharacterController.ts | 17 ----------------- packages/core/src/physics/Collider.ts | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index fd9020ad21..f10a01b3f6 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -116,23 +116,6 @@ export class CharacterController extends Collider { this._updateFlag.flag = true; } - /** - * @internal - */ - override _onUpdate() { - if (this._updateFlag.flag) { - const { transform } = this.entity; - const shapes = this.shapes; - (this._nativeCollider).setWorldPosition(transform.worldPosition); - - const worldScale = transform.lossyWorldScale; - for (let i = 0, n = shapes.length; i < n; i++) { - shapes[i]._nativeShape.setWorldScale(worldScale); - } - this._updateFlag.flag = false; - } - } - /** * @internal */ diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index f1ffaed36b..275285a83a 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -191,7 +191,7 @@ export class Collider extends Component implements ICustomClone { worldPosition: Transform["worldPosition"], worldRotation: Transform["worldRotationQuaternion"] ): void { - (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); + this._teleportToEntityTransform(worldPosition, worldRotation); } /** From b474e58f51f87a6467127ecd0cf31f2147e58618 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 18:32:58 +0800 Subject: [PATCH 3/5] fix(physics): harden PhysX kinematic boundaries --- .../core/src/physics/CharacterController.ts | 25 ++-- packages/core/src/physics/Collider.ts | 38 +----- packages/core/src/physics/DynamicCollider.ts | 16 +-- .../physics-physx/src/PhysXDynamicCollider.ts | 76 +++++------ .../src/core/physics/DynamicCollider.test.ts | 123 +++++++++--------- tests/src/core/physics/PhysicsScene.test.ts | 16 +-- 6 files changed, 125 insertions(+), 169 deletions(-) diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index f10a01b3f6..f3c9e6f048 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -2,7 +2,6 @@ import { ICharacterController } from "@galacean/engine-design"; import { Vector3 } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { Entity } from "../Entity"; -import { Transform } from "../Transform"; import { Collider } from "./Collider"; import { ControllerNonWalkableMode } from "./enums/ControllerNonWalkableMode"; import { ColliderShape } from "./shape"; @@ -116,6 +115,23 @@ export class CharacterController extends Collider { this._updateFlag.flag = true; } + /** + * @internal + */ + override _onUpdate() { + if (this._updateFlag.flag) { + const { transform } = this.entity; + const shapes = this.shapes; + (this._nativeCollider).setWorldPosition(transform.worldPosition); + + const worldScale = transform.lossyWorldScale; + for (let i = 0, n = shapes.length; i < n; i++) { + shapes[i]._nativeShape.setWorldScale(worldScale); + } + this._updateFlag.flag = false; + } + } + /** * @internal */ @@ -146,13 +162,6 @@ export class CharacterController extends Collider { (this._nativeCollider).setSlopeLimit(this._slopeLimit); } - protected override _teleportToEntityTransform( - worldPosition: Transform["worldPosition"], - _worldRotation: Transform["worldRotationQuaternion"] - ): void { - (this._nativeCollider).setWorldPosition(worldPosition); - } - private _syncWorldPositionFromPhysicalSpace(): void { (this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition); } diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index 275285a83a..a8a591fe56 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -28,9 +28,6 @@ export class Collider extends Component implements ICustomClone { protected _shapes: ColliderShape[] = []; protected _collisionLayerIndex: number = 0; - @ignoreClone - private _pendingTeleport: boolean = false; - /** * The shapes of this collider. */ @@ -111,14 +108,12 @@ export class Collider extends Component implements ICustomClone { * @internal */ _onUpdate(): void { - if (this._pendingTeleport || this._updateFlag.flag) { + if (this._updateFlag.flag) { const { transform } = this.entity; - if (this._pendingTeleport) { - this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); - this._pendingTeleport = false; - } else { - this._syncEntityTransformToNative(transform.worldPosition, transform.worldRotationQuaternion); - } + (this._nativeCollider).setWorldTransform( + transform.worldPosition, + transform.worldRotationQuaternion + ); const worldScale = transform.lossyWorldScale; const shapes = this._shapes; @@ -146,7 +141,6 @@ export class Collider extends Component implements ICustomClone { */ override _onDisableInScene(): void { this.scene.physics._removeCollider(this); - this._pendingTeleport = true; } /** @@ -170,28 +164,6 @@ export class Collider extends Component implements ICustomClone { this._addNativeShape(this.shapes[i]); } this._setCollisionLayer(); - // Parent transforms may not be cloned yet, so defer world-pose synchronization until the hierarchy is complete. - this._pendingTeleport = true; - } - - /** - * Teleport the native actor without implied velocity. - */ - protected _teleportToEntityTransform( - worldPosition: Transform["worldPosition"], - worldRotation: Transform["worldRotationQuaternion"] - ): void { - (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); - } - - /** - * Synchronize the entity transform to the native actor. - */ - protected _syncEntityTransformToNative( - worldPosition: Transform["worldPosition"], - worldRotation: Transform["worldRotationQuaternion"] - ): void { - this._teleportToEntityTransform(worldPosition, worldRotation); } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 8832eb3bf2..5c8725c604 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -286,9 +286,10 @@ export class DynamicCollider extends Collider { this._isKinematic = value; (this._nativeCollider).setIsKinematic(value); - (this._nativeCollider).setCollisionDetectionMode(this._collisionDetectionMode); + // Resync properties that PhysX ignores/resets during kinematic mode if (!value) { + (this._nativeCollider).setCollisionDetectionMode(this._collisionDetectionMode); if (this._automaticCenterOfMass || this._automaticInertiaTensor) { this._setMassAndUpdateInertia(); } @@ -441,19 +442,6 @@ export class DynamicCollider extends Collider { super.addShape(shape); } - /** - * Synchronize kinematic transform changes through setKinematicTarget so PhysX - * generates contacts along the movement path. - * @internal - */ - protected override _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { - if (this._isKinematic) { - (this._nativeCollider).move(worldPosition, worldRotation); - } else { - super._syncEntityTransformToNative(worldPosition, worldRotation); - } - } - /** * @internal */ diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 4a6158eb7f..c323683166 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -24,7 +24,7 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli private static _tempTranslation = new Vector3(); private static _tempRotation = new Quaternion(); - private _isKinematic: boolean = false; + private _isKinematic = false; constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) { super(physXPhysics); @@ -182,41 +182,15 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli setIsKinematic(value: boolean): void { if (this._isKinematic === value) return; const physX = this._physXPhysics._physX; - if (value) { - this._applyCollisionDetectionFlags(CollisionDetectionMode.Discrete); - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, true); - } else { - this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, false); + // 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; } - private _applyCollisionDetectionFlags(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; - } - } - /** * {@inheritDoc IDynamicCollider.setConstraints } */ @@ -247,27 +221,27 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.move } - * - * PhysX requires a normalized target rotation. */ move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void { - const tempTranslation = PhysXDynamicCollider._tempTranslation; const tempRotation = PhysXDynamicCollider._tempRotation; if (rotation) { - tempRotation.copyFrom(rotation).normalize(); - this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); + // PhysX validates kinematic targets before normalizing them internally. + const targetRotation = rotation.normalized ? rotation : tempRotation.copyFrom(rotation).normalize(); + this._pxActor.setKinematicTarget(positionOrRotation, targetRotation); return; } + const tempTranslation = PhysXDynamicCollider._tempTranslation; + this.getWorldTransform(tempTranslation, tempRotation); if (positionOrRotation instanceof Vector3) { - this.getWorldTransform(tempTranslation, tempRotation); // Rotations read from PhysX are already normalized. this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); } else { - this.getWorldTransform(tempTranslation, tempRotation); - tempRotation.copyFrom(positionOrRotation).normalize(); - this._pxActor.setKinematicTarget(tempTranslation, tempRotation); + const targetRotation = positionOrRotation.normalized + ? positionOrRotation + : tempRotation.copyFrom(positionOrRotation).normalize(); + this._pxActor.setKinematicTarget(tempTranslation, targetRotation); } } @@ -291,4 +265,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 7e036ad71c..3316b9db5a 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"), @@ -459,32 +474,6 @@ describe("DynamicCollider", function () { expect(box.transform.position.y).below(1); }); - it("teleports kinematic target collider on re-enable instead of sweeping from stale native pose", function () { - const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(-10, 0, 0)); - const boxCollider = box.getComponent(DynamicCollider); - boxCollider.useGravity = false; - boxCollider.isKinematic = true; - - // @ts-ignore - engine.sceneManager.activeScene.physics._update(1 / 60); - - // @ts-ignore - intentionally observe the native boundary used by Collider sync. - const nativeCollider = boxCollider._nativeCollider; - const move = vi.spyOn(nativeCollider, "move"); - const setWorldTransform = vi.spyOn(nativeCollider, "setWorldTransform"); - - box.isActive = false; - box.transform.setPosition(10, 0, 0); - box.isActive = true; - - // @ts-ignore - engine.sceneManager.activeScene.physics._update(1 / 60); - - expect(move).not.toHaveBeenCalled(); - expect(setWorldTransform).toHaveBeenCalledOnce(); - expect(formatValue(box.transform.position.x)).eq(10); - }); - it("constraints", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -529,63 +518,55 @@ describe("DynamicCollider", function () { ).toBeTruthy(); }); - it("CCD mode survives kinematic toggle", function () { + 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); - // @ts-ignore - const physX = boxCollider._nativeCollider._physXPhysics._physX; - const ccdFlag = () => - // @ts-ignore - boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + 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); - boxCollider.isKinematic = false; - expect(ccdFlag()).toBeTruthy(); - expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); - }); - - it("setCollisionDetectionMode in kinematic state defers native CCD flag application", function () { - const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); - const boxCollider = box.getComponent(DynamicCollider); - // @ts-ignore - const physX = boxCollider._nativeCollider._physXPhysics._physX; - const ccdFlag = () => - // @ts-ignore - boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); - - boxCollider.isKinematic = true; - expect(ccdFlag()).toBeFalsy(); - - boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + setRigidBodyFlag.mockClear(); + boxCollider.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; + expect(setRigidBodyFlag).not.toHaveBeenCalledWith(flags.eENABLE_CCD, true); expect(ccdFlag()).toBeFalsy(); - expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.ContinuousDynamic); boxCollider.isKinematic = false; expect(ccdFlag()).toBeTruthy(); + expect(actor.getRigidBodyFlags(flags.eENABLE_CCD_FRICTION)).toBeTruthy(); }); - it("keeps speculative CCD enabled for kinematic actors", function () { + 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); - // @ts-ignore - const physX = boxCollider._nativeCollider._physXPhysics._physX; - const speculativeCCDFlag = () => - // @ts-ignore - boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD); + 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; - boxCollider.isKinematic = true; 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(); }); @@ -646,20 +627,32 @@ describe("DynamicCollider", function () { expect(formatValue(box.transform.position.x)).eq(1); }); - it("normalizes kinematic target rotations", function () { + it("normalizes non-unit kinematic targets without copying unit rotations", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); boxCollider.isKinematic = true; // @ts-ignore - observe the value crossing the PhysX boundary. const setKinematicTarget = vi.spyOn(boxCollider._nativeCollider._pxActor, "setKinematicTarget"); + const normalizedRotation = new Quaternion(); + const targetRotation = new Quaternion(0, Math.SQRT2, 0, Math.SQRT2); + const expectNormalizedRotation = (rotation: Quaternion) => { + expect(formatValue(rotation.length())).eq(1); + expect(formatValue(Math.abs(rotation.y))).eq(formatValue(Math.SQRT1_2)); + expect(formatValue(Math.abs(rotation.w))).eq(formatValue(Math.SQRT1_2)); + }; + + boxCollider.move(normalizedRotation); + expect(setKinematicTarget.mock.calls[0][1]).toBe(normalizedRotation); + + setKinematicTarget.mockClear(); + boxCollider.move(targetRotation); + expectNormalizedRotation(setKinematicTarget.mock.calls[0][1]); - boxCollider.move(new Quaternion(0, Math.SQRT2, 0, Math.SQRT2)); + boxCollider.move(new Vector3(1, 2, 3), targetRotation); + expectNormalizedRotation(setKinematicTarget.mock.calls[1][1]); - expect(setKinematicTarget).toHaveBeenCalledOnce(); - const rotation = setKinematicTarget.mock.calls[0][1]; - expect(formatValue(rotation.length())).eq(1); - expect(formatValue(Math.abs(rotation.y))).eq(formatValue(Math.SQRT1_2)); - expect(formatValue(Math.abs(rotation.w))).eq(formatValue(Math.SQRT1_2)); + expect(setKinematicTarget).toHaveBeenCalledTimes(2); + expect(formatValue(targetRotation.length())).eq(2); }); it("destroy", function () { diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index f09579d278..fae4d817c2 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -1488,15 +1488,15 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Kinematic targets generate contact events for static-kinematic pairs. + // Test that collision works correctly, A is dynamic and kinematic, B is static. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, false, false, false); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); @@ -1603,15 +1603,15 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Kinematic targets generate contact events for kinematic pairs. + // Test that collision works correctly, both A,B are dynamic, kinematic. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, true, false, true); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); From b413f41bb4443005f4151f23114e5e4be55f1e71 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 20:09:31 +0800 Subject: [PATCH 4/5] refactor(physics): drop redundant target normalization --- .../physics-physx/src/PhysXDynamicCollider.ts | 13 ++------ .../src/core/physics/DynamicCollider.test.ts | 30 +------------------ 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index c323683166..2d2718019b 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -223,25 +223,18 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli * {@inheritDoc IDynamicCollider.move } */ move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void { - const tempRotation = PhysXDynamicCollider._tempRotation; - if (rotation) { - // PhysX validates kinematic targets before normalizing them internally. - const targetRotation = rotation.normalized ? rotation : tempRotation.copyFrom(rotation).normalize(); - this._pxActor.setKinematicTarget(positionOrRotation, targetRotation); + this._pxActor.setKinematicTarget(positionOrRotation, rotation); return; } const tempTranslation = PhysXDynamicCollider._tempTranslation; + const tempRotation = PhysXDynamicCollider._tempRotation; this.getWorldTransform(tempTranslation, tempRotation); if (positionOrRotation instanceof Vector3) { - // Rotations read from PhysX are already normalized. this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); } else { - const targetRotation = positionOrRotation.normalized - ? positionOrRotation - : tempRotation.copyFrom(positionOrRotation).normalize(); - this._pxActor.setKinematicTarget(tempTranslation, targetRotation); + this._pxActor.setKinematicTarget(tempTranslation, positionOrRotation); } } diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 3316b9db5a..26c1fac13b 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -11,7 +11,7 @@ import { } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { PhysXPhysics, PhysXRuntimeMode } from "@galacean/engine-physics-physx"; -import { Quaternion, Vector3 } from "@galacean/engine-math"; +import { Vector3 } from "@galacean/engine-math"; import { vi, describe, beforeAll, beforeEach, expect, it } from "vitest"; const physXWasmModeUrl = new URL("../../../../packages/physics-physx/libs/physx.release.js", import.meta.url).href; @@ -627,34 +627,6 @@ describe("DynamicCollider", function () { expect(formatValue(box.transform.position.x)).eq(1); }); - it("normalizes non-unit kinematic targets without copying unit rotations", function () { - const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); - const boxCollider = box.getComponent(DynamicCollider); - boxCollider.isKinematic = true; - // @ts-ignore - observe the value crossing the PhysX boundary. - const setKinematicTarget = vi.spyOn(boxCollider._nativeCollider._pxActor, "setKinematicTarget"); - const normalizedRotation = new Quaternion(); - const targetRotation = new Quaternion(0, Math.SQRT2, 0, Math.SQRT2); - const expectNormalizedRotation = (rotation: Quaternion) => { - expect(formatValue(rotation.length())).eq(1); - expect(formatValue(Math.abs(rotation.y))).eq(formatValue(Math.SQRT1_2)); - expect(formatValue(Math.abs(rotation.w))).eq(formatValue(Math.SQRT1_2)); - }; - - boxCollider.move(normalizedRotation); - expect(setKinematicTarget.mock.calls[0][1]).toBe(normalizedRotation); - - setKinematicTarget.mockClear(); - boxCollider.move(targetRotation); - expectNormalizedRotation(setKinematicTarget.mock.calls[0][1]); - - boxCollider.move(new Vector3(1, 2, 3), targetRotation); - expectNormalizedRotation(setKinematicTarget.mock.calls[1][1]); - - expect(setKinematicTarget).toHaveBeenCalledTimes(2); - expect(formatValue(targetRotation.length())).eq(2); - }); - it("destroy", function () { const entity = rootEntity.createChild("collider"); const collider = entity.addComponent(DynamicCollider); From d98ffe1eeef0ea727e47c0885124435d0dd0db7f Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 20:45:08 +0800 Subject: [PATCH 5/5] style(physics): align inline comments --- packages/physics-physx/src/PhysXDynamicCollider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 2d2718019b..fa65867c88 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -182,7 +182,7 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli setIsKinematic(value: boolean): void { if (this._isKinematic === value) return; const physX = this._physXPhysics._physX; - // PhysX rejects swept CCD when eKINEMATIC is raised. + // 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); @@ -262,7 +262,7 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli 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. + // PhysX rejects enabling swept and speculative CCD at the same time if (enableCCD) { this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, false); }