Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/src/physics/CharacterController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ICharacterController } from "@galacean/engine-design";
import { Vector3 } from "@galacean/engine-math";
import { Quaternion, Vector3 } from "@galacean/engine-math";
import { Engine } from "../Engine";
import { Entity } from "../Entity";
import { Collider } from "./Collider";
Expand Down Expand Up @@ -162,6 +162,10 @@ export class CharacterController extends Collider {
(<ICharacterController>this._nativeCollider).setSlopeLimit(this._slopeLimit);
}

protected override _teleportToEntityTransform(worldPosition: Vector3, _worldRotation: Quaternion): void {
(<ICharacterController>this._nativeCollider).setWorldPosition(worldPosition);
}

private _syncWorldPositionFromPhysicalSpace(): void {
(<ICharacterController>this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition);
}
Expand Down
57 changes: 51 additions & 6 deletions packages/core/src/physics/Collider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ICollider, IStaticCollider } from "@galacean/engine-design";
import { Quaternion, Vector3 } from "@galacean/engine-math";
import { BoolUpdateFlag } from "../BoolUpdateFlag";
import { deepClone, ignoreClone } from "../clone/CloneManager";
import { ICustomClone } from "../clone/ComponentCloner";
Expand Down Expand Up @@ -28,6 +29,17 @@ export class Collider extends Component implements ICustomClone {
protected _shapes: ColliderShape[] = [];
protected _collisionLayerIndex: number = 0;

/**
* A collider must teleport on the next transform sync when its native actor
* already exists at a stale pose, such as after re-entering the scene or after
* clone-time native reconstruction. Ordinary first entry can use subclass sync
* semantics so kinematic actors still use setKinematicTarget.
*/
@ignoreClone
private _pendingReenterTeleport: boolean = false;
@ignoreClone
private _enteredScene: boolean = false;

/**
* The shapes of this collider.
*/
Expand Down Expand Up @@ -108,15 +120,17 @@ export class Collider extends Component implements ICustomClone {
* @internal
*/
_onUpdate(): void {
if (this._updateFlag.flag) {
const shapes = this._shapes;
if (this._pendingReenterTeleport || this._updateFlag.flag) {
const { transform } = this.entity;
(<IStaticCollider>this._nativeCollider).setWorldTransform(
transform.worldPosition,
transform.worldRotationQuaternion
);
if (this._pendingReenterTeleport) {
this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion);
this._pendingReenterTeleport = false;
} else {
this._syncEntityTransformToNative(transform.worldPosition, transform.worldRotationQuaternion);
}

const worldScale = transform.lossyWorldScale;
const shapes = this._shapes;
for (let i = 0, n = shapes.length; i < n; i++) {
shapes[i]._nativeShape?.setWorldScale(worldScale);
}
Expand All @@ -134,6 +148,10 @@ export class Collider extends Component implements ICustomClone {
*/
override _onEnableInScene(): void {
this.scene.physics._addCollider(this);
if (this._enteredScene) {
this._pendingReenterTeleport = true;
}
this._enteredScene = true;
}

/**
Expand All @@ -148,6 +166,7 @@ export class Collider extends Component implements ICustomClone {
*/
_cloneTo(target: Collider): void {
target._syncNative();
target._pendingReenterTeleport = true;
}

/**
Expand All @@ -164,6 +183,32 @@ export class Collider extends Component implements ICustomClone {
this._addNativeShape(this.shapes[i]);
}
this._setCollisionLayer();
// Teleport native actor to entity's current world pose.
// The native actor was created in constructor() with the entity's then-current
// worldPosition/Rotation. On clone, the entity's transform fields are deep-cloned
// AFTER the Component (and its native actor) are constructed, so the native actor's
// pose lags behind the cloned entity transform until this sync.
const { transform } = this.entity;
this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion);
}

/**
* Teleport native actor to a world pose (instant, no implied velocity).
* Used during initialization paths (clone) where the native actor must be re-aligned
* with the entity transform after construction-time pose was based on stale defaults.
*/
protected _teleportToEntityTransform(worldPosition: Vector3, worldRotation: Quaternion): void {
(<IStaticCollider>this._nativeCollider).setWorldTransform(worldPosition, worldRotation);
}

/**
* Sync entity world transform to native actor for per-frame updates.
* Default semantics: teleport (setGlobalPose). Subclasses override to express
* physics-aware movement (e.g. DynamicCollider routes kinematic actors through
* setKinematicTarget to generate contact events on swept motion).
*/
protected _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void {
(<IStaticCollider>this._nativeCollider).setWorldTransform(worldPosition, worldRotation);
}

/**
Expand Down
53 changes: 53 additions & 0 deletions packages/core/src/physics/DynamicCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export class DynamicCollider extends Collider {
private _isKinematic = false;
private _constraints: DynamicColliderConstraints = 0;
private _collisionDetectionMode: CollisionDetectionMode = CollisionDetectionMode.Discrete;
private _kinematicTransformSyncMode: DynamicColliderKinematicTransformSyncMode =
DynamicColliderKinematicTransformSyncMode.Target;
private _sleepThreshold = 5e-3;
private _automaticCenterOfMass = true;
private _automaticInertiaTensor = true;
Expand Down Expand Up @@ -325,6 +327,22 @@ export class DynamicCollider extends Collider {
}
}

/**
* Controls how entity transform changes are synchronized to a kinematic native actor.
*
* @remarks
* `Target` routes transform changes through {@link move}, so PhysX treats the
* actor as moving between frames and can generate swept contacts. `Teleport`
* writes the native pose directly and does not imply velocity.
*/
get kinematicTransformSyncMode(): DynamicColliderKinematicTransformSyncMode {
return this._kinematicTransformSyncMode;
}

set kinematicTransformSyncMode(value: DynamicColliderKinematicTransformSyncMode) {
this._kinematicTransformSyncMode = value;
}

/**
* @internal
*/
Expand Down Expand Up @@ -433,6 +451,30 @@ export class DynamicCollider extends Collider {
super.addShape(shape);
}

/**
* Route per-frame entity → native transform sync to the correct physics API based
* on kinematic state.
*
* PhysX 4.x docs (PxRigidDynamic):
* "If you intend to move a kinematic actor with [setGlobalPose] and want
* collision detection, use setKinematicTarget() instead."
*
* setGlobalPose is a teleport: PhysX skips contact detection between the old
* and new pose. setKinematicTarget tells PhysX the actor is animating to the
* target during the next simulate(), enabling swept contacts. Some compatibility
* layers need transform writes to stay teleport-like, so the sync mode is
* explicit while {@link move} always keeps target semantics.
*
* @internal
*/
protected override _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void {
if (this._isKinematic && this._kinematicTransformSyncMode === DynamicColliderKinematicTransformSyncMode.Target) {
(<IDynamicCollider>this._nativeCollider).move(worldPosition, worldRotation);
} else {
super._syncEntityTransformToNative(worldPosition, worldRotation);
}
}

/**
* @internal
*/
Expand Down Expand Up @@ -460,6 +502,7 @@ export class DynamicCollider extends Collider {
target._angularVelocity.copyFrom(this.angularVelocity);
target._centerOfMass.copyFrom(this.centerOfMass);
target._inertiaTensor.copyFrom(this.inertiaTensor);
target._kinematicTransformSyncMode = this._kinematicTransformSyncMode;
super._cloneTo(target);
}

Expand Down Expand Up @@ -555,6 +598,16 @@ export enum CollisionDetectionMode {
ContinuousSpeculative
}

/**
* Kinematic transform synchronization mode.
*/
export enum DynamicColliderKinematicTransformSyncMode {
/** Synchronize transform changes through PhysX setKinematicTarget. */
Target,
/** Synchronize transform changes by directly teleporting the native actor. */
Teleport
}

/**
* Use these flags to constrain motion of dynamic collider.
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/physics/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export { CharacterController } from "./CharacterController";
export { Collider } from "./Collider";
export { CollisionDetectionMode, DynamicCollider, DynamicColliderConstraints } from "./DynamicCollider";
export {
CollisionDetectionMode,
DynamicCollider,
DynamicColliderConstraints,
DynamicColliderKinematicTransformSyncMode
} from "./DynamicCollider";
export { HitResult } from "./HitResult";
export { PhysicsMaterial } from "./PhysicsMaterial";
export { PhysicsScene } from "./PhysicsScene";
Expand Down
8 changes: 7 additions & 1 deletion packages/physics-lite/src/LiteDynamicCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ export class LiteDynamicCollider extends LiteCollider implements IDynamicCollide
* {@inheritDoc IDynamicCollider.move }
*/
move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void {
throw "Physics-lite don't support move. Use Physics-PhysX instead!";
if (rotation) {
this.setWorldTransform(positionOrRotation as Vector3, rotation);
} else if (positionOrRotation instanceof Vector3) {
this.setWorldTransform(positionOrRotation, this._transform.rotationQuaternion);
} else {
this.setWorldTransform(this._transform.position, positionOrRotation);
}
}

/**
Expand Down
104 changes: 80 additions & 24 deletions packages/physics-physx/src/PhysXDynamicCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli
private static _tempTranslation = new Vector3();
private static _tempRotation = new Quaternion();

/**
* Whether actor is currently kinematic.
* PhysX 拒绝在 kinematic actor 上启用 CCD(会打印警告并忽略),
* 所以 setCollisionDetectionMode 在 kinematic 状态下只缓存目标值,
* 等切回 dynamic 时再真正写到 PhysX。
*/
private _isKinematic: boolean = false;

/**
* Cached collision detection mode. Always reflects user's intent.
* 实际 PhysX CCD flag 可能跟这个不一致(kinematic 时强制 Discrete)。
*/
private _collisionDetectionMode: number = CollisionDetectionMode.Discrete;

constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) {
super(physXPhysics);
const transform = this._transform(position, rotation);
Expand Down Expand Up @@ -143,7 +157,7 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli

/**
* {@inheritDoc IDynamicCollider.setSleepThreshold }
* @default 1e-5f * PxTolerancesScale::speed * PxTolerancesScale::speed
* @default 5e-5f * PxTolerancesScale::speed * PxTolerancesScale::speed
*/
setSleepThreshold(value: number): void {
this._pxActor.setSleepThreshold(value);
Expand All @@ -158,10 +172,52 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli

/**
* {@inheritDoc IDynamicCollider.setCollisionDetectionMode }
*
* PhysX 在 kinematic actor 上调用 setRigidBodyFlag(eENABLE_CCD, true) 会触发警告:
* "kinematic bodies with CCD enabled are not supported! CCD will be ignored"
* 虽然 PhysX 会忽略这次调用而非真的拒绝(切回 dynamic 时 flag 不会自动恢复),
* 但每次 setIsKinematic 切换都会让这个 warning 重复打印,污染日志,
* 同时让 actor 在 dynamic 状态下 CCD flag 状态不确定。
*
* 解决: 只在 dynamic 状态时立即 apply CCD flags。kinematic 时仅缓存到
* `_collisionDetectionMode`,等切回 dynamic 时由 setIsKinematic 重新 apply。
*/
setCollisionDetectionMode(value: number): void {
this._collisionDetectionMode = value;
if (!this._isKinematic) {
this._applyCollisionDetectionFlags(value);
}
}

/**
* {@inheritDoc IDynamicCollider.setUseGravity }
*/
setUseGravity(value: boolean): void {
this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value);
}

/**
* {@inheritDoc IDynamicCollider.setIsKinematic }
*
* 切换 kinematic 状态时同步处理 CCD flag:
* - 切到 kinematic 前先关 CCD(避免 PhysX 警告 + 让状态显式)
* - 切回 dynamic 后恢复用户期望的 CCD mode(来自 `_collisionDetectionMode` 缓存)
*/
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._applyCollisionDetectionFlags(this._collisionDetectionMode);
}
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);
Expand All @@ -186,24 +242,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 }
*/
Expand All @@ -213,34 +251,52 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli

/**
* {@inheritDoc IDynamicCollider.addForce }
*
* PhysX 在 kinematic actor 上调 addForce 是 no-op(doc: "kinematic bodies don't
* respond to forces")。提前 return 避免无意义的 wasm boundary cross。
*
* Sleeping actor 不需要显式 wakeUp — wasm binding 调用 `addForce(force, eFORCE,
* autowake=true)`,PhysX 自动唤醒(已通过 `applyForce on sleeping actor` 测试验证)。
*/
addForce(force: Vector3) {
if (this._isKinematic) return;
this._pxActor.addForce({ x: force.x, y: force.y, z: force.z });
}

/**
* {@inheritDoc IDynamicCollider.addTorque }
*
* 同 addForce — kinematic 提前 return,sleeping 由 PhysX autowake 自动处理。
*/
addTorque(torque: Vector3) {
if (this._isKinematic) return;
this._pxActor.addTorque({ x: torque.x, y: torque.y, z: torque.z });
}

/**
* {@inheritDoc IDynamicCollider.move }
*
* PhysX 要求 setKinematicTarget 的 rotation 是 normalized quaternion,否则会触发
* 内部 assertion / 警告,并把 actor 转到错误的姿态。所以在写入 wasm 边界前统一 normalize。
*/
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);
// current rotation read from PhysX is already normalized; no extra work needed
this._pxActor.setKinematicTarget(positionOrRotation, tempRotation);
} else {
this._pxActor.setKinematicTarget(tempTranslation, positionOrRotation);
this.getWorldTransform(tempTranslation, tempRotation);
tempRotation.copyFrom(positionOrRotation).normalize();
this._pxActor.setKinematicTarget(tempTranslation, tempRotation);
}
}

Expand Down
Loading
Loading