From ea51cf33e05f92daa13dc6180761a45a15db97c9 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 14 May 2025 20:09:40 +0800 Subject: [PATCH] Support collision group and fix the position of characterController synchronization late issue (#2614) * feat: support collision group --------- Co-authored-by: GuoLei1990 --- e2e/case/litePhysics-collision-group.ts | 134 +++++++++++++ e2e/case/physx-collision-group.ts | 146 ++++++++++++++ e2e/config.ts | 10 + .../Physics_litePhysics-collision-group.jpg | 3 + .../Physics_physx-collision-group.jpg | 3 + .../core/src/physics/CharacterController.ts | 5 +- packages/core/src/physics/Collider.ts | 44 ++++- packages/core/src/physics/DynamicCollider.ts | 5 +- packages/core/src/physics/PhysicsScene.ts | 32 +++ .../physics/enums/ColliderShapeChangeFlag.ts | 8 + .../core/src/physics/shape/ColliderShape.ts | 8 +- packages/design/src/physics/ICollider.ts | 6 + packages/design/src/physics/IPhysics.ts | 22 ++- packages/physics-lite/src/LiteCollider.ts | 16 +- .../physics-lite/src/LiteDynamicCollider.ts | 5 +- packages/physics-lite/src/LitePhysics.ts | 52 ++++- packages/physics-lite/src/LitePhysicsScene.ts | 33 +++- .../physics-lite/src/LiteStaticCollider.ts | 5 +- .../src/PhysXCharacterController.ts | 17 +- packages/physics-physx/src/PhysXCollider.ts | 7 + packages/physics-physx/src/PhysXPhysics.ts | 23 ++- .../src/shape/PhysXColliderShape.ts | 21 +- .../core/physics/CharacterController.test.ts | 123 +++++++++++- tests/src/core/physics/Collider.test.ts | 183 +++++++++++++++++- tests/src/core/physics/PhysicsManager.test.ts | 67 ++++++- tests/vitest.config.ts | 3 + 26 files changed, 935 insertions(+), 46 deletions(-) create mode 100644 e2e/case/litePhysics-collision-group.ts create mode 100644 e2e/case/physx-collision-group.ts create mode 100644 e2e/fixtures/originImage/Physics_litePhysics-collision-group.jpg create mode 100644 e2e/fixtures/originImage/Physics_physx-collision-group.jpg create mode 100644 packages/core/src/physics/enums/ColliderShapeChangeFlag.ts diff --git a/e2e/case/litePhysics-collision-group.ts b/e2e/case/litePhysics-collision-group.ts new file mode 100644 index 000000000..baa610bd4 --- /dev/null +++ b/e2e/case/litePhysics-collision-group.ts @@ -0,0 +1,134 @@ +/** + * @title LitePhysics Collision Group + * @category Physics + */ +import { + WebGLEngine, + SphereColliderShape, + DynamicCollider, + BoxColliderShape, + Vector3, + MeshRenderer, + PointLight, + PrimitiveMesh, + Camera, + Script, + StaticCollider, + ColliderShape, + PBRMaterial, + Entity, + Layer +} from "@galacean/engine"; + +import { LitePhysics } from "@galacean/engine-physics-lite"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; +class MoveScript extends Script { + onUpdate() { + this.entity.transform.position.y -= 0.1; + } + + onTriggerEnter(other: ColliderShape) { + // Change color to green when collision occurs + (this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1); + } +} +// Create a sphere with physics +function createPhysicsSphere( + rootEntity: Entity, + name: string, + position: Vector3, + radius: number, + color: Vector3, + collisionLayer: Layer +) { + const sphereEntity = rootEntity.createChild(name); + sphereEntity.transform.setPosition(position.x, position.y, position.z); + sphereEntity.addComponent(MoveScript); + + // Add visual representation + const sphereMtl = new PBRMaterial(rootEntity.engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(color.x, color.y, color.z, 1.0); + sphereMtl.metallic = 0.0; + sphereMtl.roughness = 0.5; + sphereRenderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + // Add physics + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.collisionLayer = collisionLayer; + sphereCollider.addShape(physicsSphere); + + return sphereEntity; +} + +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // Set up ambient lighting + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // Set up camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 3, 15); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // Add point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 10, 0); + const pointLight = light.addComponent(PointLight); + pointLight.intensity = 1.5; + + const groundEntity = rootEntity.createChild("ground"); + + // 设置立方体的位置和大小 + groundEntity.transform.setPosition(0, 1, 0); + // groundEntity.isActive = false; + + // Visual representation of the ground cube + const groundMtl = new PBRMaterial(engine); + groundMtl.baseColor.set(0.5, 0.5, 0.5, 1.0); + groundMtl.roughness = 0.7; + + const cubeSize = new Vector3(10, 0.2, 10); + const groundRenderer = groundEntity.addComponent(MeshRenderer); + groundRenderer.mesh = PrimitiveMesh.createCuboid(engine, cubeSize.x, cubeSize.y, cubeSize.z); + groundRenderer.setMaterial(groundMtl); + + // Physics for the ground cube + const groundCollider = groundEntity.addComponent(StaticCollider); + const groundShape = new BoxColliderShape(); + groundShape.size = cubeSize; + groundCollider.addShape(groundShape); + + groundCollider.collisionLayer = Layer.Layer3; + + const sphere1 = createPhysicsSphere( + rootEntity, + "RedSphere", + new Vector3(-2, 5, 0), + 0.5, + new Vector3(1, 0, 0), + Layer.Layer1 + ); + + const sphere2 = createPhysicsSphere( + rootEntity, + "BlueSphere", + new Vector3(2, 5, 0), + 0.5, + new Vector3(0, 0, 1), + Layer.Layer2 + ); + + scene.physics.setColliderLayerCollision(Layer.Layer2, Layer.Layer3, false); + updateForE2E(engine, 1000, 38); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/physx-collision-group.ts b/e2e/case/physx-collision-group.ts new file mode 100644 index 000000000..1e8da9b03 --- /dev/null +++ b/e2e/case/physx-collision-group.ts @@ -0,0 +1,146 @@ +/** + * @title Physx Collision Group + * @category Physics + */ +import { + WebGLEngine, + SphereColliderShape, + DynamicCollider, + BoxColliderShape, + Vector3, + MeshRenderer, + PointLight, + PrimitiveMesh, + Camera, + Script, + StaticCollider, + ColliderShape, + PBRMaterial, + Entity, + Layer +} from "@galacean/engine"; + +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +class CheckScript extends Script { + onTriggerEnter(other: ColliderShape) { + console.log("onTriggerEnter", other); + // Change color to green when collision occurs + (this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1); + } + + onContactEnter(other: ColliderShape) { + console.log("onContactEnter", other); + // Change color to green when collision occurs + (this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1); + } +} + +// Create a sphere with physics +function createPhysicsSphere( + rootEntity: Entity, + name: string, + position: Vector3, + radius: number, + color: Vector3, + collisionLayer: number +) { + const sphereEntity = rootEntity.createChild(name); + sphereEntity.transform.setPosition(position.x, position.y, position.z); + + // Add visual representation + const sphereMtl = new PBRMaterial(rootEntity.engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(color.x, color.y, color.z, 1.0); + sphereMtl.metallic = 0.0; + sphereMtl.roughness = 0.5; + sphereRenderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + // Add physics + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + physicsSphere.material.bounciness = 0.8; + + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.collisionLayer = collisionLayer; + sphereEntity.addComponent(CheckScript); + sphereCollider.addShape(physicsSphere); + + return sphereEntity; +} + +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // Set up ambient lighting + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // Set up camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + // 调整相机位置以便更好地观察穿透效果 + cameraEntity.transform.setPosition(0, 3, 15); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // Add point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 10, 0); + const pointLight = light.addComponent(PointLight); + pointLight.intensity = 1.5; + + // 创建立方体作为地面 + const groundEntity = rootEntity.createChild("ground"); + + // 设置立方体的位置和大小 + groundEntity.transform.setPosition(0, 1, 0); + + // Visual representation of the ground cube + const groundMtl = new PBRMaterial(engine); + groundMtl.baseColor.set(0.5, 0.5, 0.5, 1.0); + groundMtl.roughness = 0.7; + // 设置半透明以便能看到穿透的球体 + groundMtl.baseColor.a = 0.5; + + const cubeSize = new Vector3(10, 0.2, 10); + const groundRenderer = groundEntity.addComponent(MeshRenderer); + groundRenderer.mesh = PrimitiveMesh.createCuboid(engine, cubeSize.x, cubeSize.y, cubeSize.z); + groundRenderer.setMaterial(groundMtl); + + // Physics for the ground cube + const groundCollider = groundEntity.addComponent(StaticCollider); + const groundShape = new BoxColliderShape(); + groundShape.size = cubeSize; + groundCollider.addShape(groundShape); + + groundCollider.collisionLayer = Layer.Layer3; + + // 创建可以碰撞的红色球体 + const sphere1 = createPhysicsSphere( + rootEntity, + "RedSphere", + new Vector3(-2, 5, 0), + 0.5, + new Vector3(1, 0, 0), + Layer.Layer1 + ); + + // 创建可以穿透的蓝色球体 + const sphere2 = createPhysicsSphere( + rootEntity, + "BlueSphere", + new Vector3(2, 5, 0), + 0.5, + new Vector3(0, 0, 1), + Layer.Layer2 + ); + + scene.physics.setColliderLayerCollision(Layer.Layer2, Layer.Layer3, false); + + updateForE2E(engine, 110); + initScreenshot(engine, camera); +}); diff --git a/e2e/config.ts b/e2e/config.ts index ee5397d7c..f447ce396 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -214,6 +214,16 @@ export const E2E_CONFIG = { category: "Physics", caseFileName: "physx-collision", threshold: 0.1 + }, + "LitePhysics Collision Group": { + category: "Physics", + caseFileName: "litePhysics-collision-group", + threshold: 0.1 + }, + "PhysXPhysics Collision Group": { + category: "Physics", + caseFileName: "physx-collision-group", + threshold: 0.1 } }, Particle: { diff --git a/e2e/fixtures/originImage/Physics_litePhysics-collision-group.jpg b/e2e/fixtures/originImage/Physics_litePhysics-collision-group.jpg new file mode 100644 index 000000000..3d82d023c --- /dev/null +++ b/e2e/fixtures/originImage/Physics_litePhysics-collision-group.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98e836afe6c51a5b510e7d7d462a79de8a7e7c720b81cdac10ffae4bf3e0bdca +size 37799 diff --git a/e2e/fixtures/originImage/Physics_physx-collision-group.jpg b/e2e/fixtures/originImage/Physics_physx-collision-group.jpg new file mode 100644 index 000000000..8150c84df --- /dev/null +++ b/e2e/fixtures/originImage/Physics_physx-collision-group.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd1f39578c6f1f92eaf6910ac91a05f76015e5c22742bf7690a885cd82bfc7e +size 40667 diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index 2cef370d3..aec585278 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -81,13 +81,12 @@ export class CharacterController extends Collider { constructor(entity: Entity) { super(entity); (this._nativeCollider) = PhysicsScene._nativePhysics.createCharacterController(); - this._setUpDirection = this._setUpDirection.bind(this); //@ts-ignore this._upDirection._onValueChanged = this._setUpDirection; - // sync world position to physical space - this._onUpdate(); + // Sync world position to physical space + (this._nativeCollider).setWorldPosition(this.entity.transform.worldPosition); } /** diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index 83708f3e9..64fbb4d4b 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -1,12 +1,14 @@ import { ICollider, IStaticCollider } from "@galacean/engine-design"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; +import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ICustomClone } from "../clone/ComponentCloner"; import { Component } from "../Component"; import { DependentMode, dependentComponents } from "../ComponentsDependencies"; import { Entity } from "../Entity"; +import { Layer } from "../Layer"; import { Transform } from "../Transform"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; import { ColliderShape } from "./shape/ColliderShape"; -import { ICustomClone } from "../clone/ComponentCloner"; +import { ColliderShapeChangeFlag } from "./enums/ColliderShapeChangeFlag"; /** * Base class for all colliders. @@ -24,6 +26,7 @@ export class Collider extends Component implements ICustomClone { protected _updateFlag: BoolUpdateFlag; @deepClone protected _shapes: ColliderShape[] = []; + protected _collisionLayerIndex: number = 0; /** * The shapes of this collider. @@ -32,6 +35,26 @@ export class Collider extends Component implements ICustomClone { return this._shapes; } + /** + * The collision layer of this collider, only support single layer. + * + * @defaultValue `Layer.Layer0` + */ + get collisionLayer(): Layer { + return (1 << this._collisionLayerIndex) as Layer; + } + + set collisionLayer(value: Layer) { + // Check if value is a single layer (power of 2) + const index = Math.log2(value); + if (!Number.isInteger(index)) { + throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)"); + } + + this._collisionLayerIndex = index; + this._nativeCollider.setCollisionLayer(index); + } + /** * @internal */ @@ -52,7 +75,7 @@ export class Collider extends Component implements ICustomClone { } this._shapes.push(shape); this._addNativeShape(shape); - this._handleShapesChanged(); + this._handleShapesChanged(ColliderShapeChangeFlag.Count); } } @@ -65,7 +88,7 @@ export class Collider extends Component implements ICustomClone { if (index !== -1) { this._shapes.splice(index, 1); this._removeNativeShape(shape); - this._handleShapesChanged(); + this._handleShapesChanged(ColliderShapeChangeFlag.Count); } } @@ -78,7 +101,7 @@ export class Collider extends Component implements ICustomClone { this._removeNativeShape(shapes[i]); } shapes.length = 0; - this._handleShapesChanged(); + this._handleShapesChanged(ColliderShapeChangeFlag.Count); } /** @@ -129,12 +152,17 @@ export class Collider extends Component implements ICustomClone { /** * @internal */ - _handleShapesChanged(): void {} + _handleShapesChanged(changeType: ColliderShapeChangeFlag): void { + if (changeType & ColliderShapeChangeFlag.Count) { + this._setCollisionLayer(); + } + } protected _syncNative(): void { for (let i = 0, n = this.shapes.length; i < n; i++) { this._addNativeShape(this.shapes[i]); } + this._setCollisionLayer(); } /** @@ -161,4 +189,8 @@ export class Collider extends Component implements ICustomClone { shape._collider = null; this._nativeCollider.removeShape(shape._nativeShape); } + + private _setCollisionLayer(): void { + this._nativeCollider.setCollisionLayer(this._collisionLayerIndex); + } } diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 2f25595ad..ca04e7dfe 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -3,6 +3,7 @@ import { Quaternion, Vector3 } from "@galacean/engine-math"; import { ignoreClone } from "../clone/CloneManager"; import { Entity } from "../Entity"; import { Collider } from "./Collider"; +import { ColliderShapeChangeFlag } from "./enums/ColliderShapeChangeFlag"; import { PhysicsScene } from "./PhysicsScene"; /** @@ -420,7 +421,9 @@ export class DynamicCollider extends Collider { /** * @internal */ - override _handleShapesChanged(): void { + override _handleShapesChanged(changeType: ColliderShapeChangeFlag): void { + super._handleShapesChanged(changeType); + if (this._automaticCenterOfMass || this._automaticInertiaTensor) { this._setMassAndUpdateInertia(); } diff --git a/packages/core/src/physics/PhysicsScene.ts b/packages/core/src/physics/PhysicsScene.ts index bbcf59863..798d011dc 100644 --- a/packages/core/src/physics/PhysicsScene.ts +++ b/packages/core/src/physics/PhysicsScene.ts @@ -231,6 +231,38 @@ export class PhysicsScene { } } + /** + * Get whether two colliders can collide with each other. + * @param layer1 - The first collision layer + * @param layer2 - The second collision layer + * @returns Whether the colliders should collide + */ + getColliderLayerCollision(layer1: Layer, layer2: Layer): boolean { + const index1 = Math.log2(layer1); + const index2 = Math.log2(layer2); + if (!Number.isInteger(index1) || !Number.isInteger(index1)) { + throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)"); + } + + return PhysicsScene._nativePhysics.getColliderLayerCollision(index1, index2); + } + + /** + * Set whether two colliders can collide with each other. + * @param layer1 - The first collision layer + * @param layer2 - The second collision layer + * @param isCollide - Whether the colliders should collide + */ + setColliderLayerCollision(layer1: Layer, layer2: Layer, isCollide: boolean): void { + const index1 = Math.log2(layer1); + const index2 = Math.log2(layer2); + if (!Number.isInteger(index1) || !Number.isInteger(index1)) { + throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)"); + } + + PhysicsScene._nativePhysics.setColliderLayerCollision(index1, index2, isCollide); + } + /** * Casts a ray through the Scene and returns the first hit. * @param ray - The ray diff --git a/packages/core/src/physics/enums/ColliderShapeChangeFlag.ts b/packages/core/src/physics/enums/ColliderShapeChangeFlag.ts new file mode 100644 index 000000000..e4577d16a --- /dev/null +++ b/packages/core/src/physics/enums/ColliderShapeChangeFlag.ts @@ -0,0 +1,8 @@ +/** + * @internal + */ +export enum ColliderShapeChangeFlag { + Property = 0x1, + Count = 0x2, + Both = Property | Count +} diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index f05667090..8600fd421 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -5,6 +5,7 @@ import { Collider } from "../Collider"; import { deepClone, ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { Engine } from "../../Engine"; +import { ColliderShapeChangeFlag } from "../enums/ColliderShapeChangeFlag"; /** * Abstract class for collider shapes. @@ -52,6 +53,7 @@ export abstract class ColliderShape implements ICustomClone { /** * Contact offset for this shape, the value must be greater than or equal to 0. + * @defaultValue 0.02 */ get contactOffset(): number { return this._contactOffset; @@ -183,18 +185,18 @@ export abstract class ColliderShape implements ICustomClone { this._nativeShape.setIsTrigger(this._isTrigger); this._nativeShape.setMaterial(this._material._nativeMaterial); - this._collider?._handleShapesChanged(); + this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); } @ignoreClone private _setPosition(): void { this._nativeShape.setPosition(this._position); - this._collider?._handleShapesChanged(); + this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); } @ignoreClone private _setRotation(): void { this._nativeShape.setRotation(this._rotation); - this._collider?._handleShapesChanged(); + this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); } } diff --git a/packages/design/src/physics/ICollider.ts b/packages/design/src/physics/ICollider.ts index f9ed026dc..434549cca 100644 --- a/packages/design/src/physics/ICollider.ts +++ b/packages/design/src/physics/ICollider.ts @@ -16,6 +16,12 @@ export interface ICollider { */ removeShape(shape: IColliderShape): void; + /** + * Set the collision group of the collider. + * @param layer - The layer of the collider which the collider belongs to + */ + setCollisionLayer(layer: number): void; + /** * Deletes the collider. */ diff --git a/packages/design/src/physics/IPhysics.ts b/packages/design/src/physics/IPhysics.ts index 4ddaa242f..1bdc930d7 100644 --- a/packages/design/src/physics/IPhysics.ts +++ b/packages/design/src/physics/IPhysics.ts @@ -119,19 +119,35 @@ export interface IPhysics { /** * Create fixed joint. - * @param collider - Affector of joint + * @param collider - collider of joint */ createFixedJoint(collider: ICollider): IFixedJoint; /** * Create hinge joint. - * @param collider - Affector of joint + * @param collider - collider of joint */ createHingeJoint(collider: ICollider): IHingeJoint; /** * Create spring joint - * @param collider - Affector of joint + * @param collider - collider of joint */ createSpringJoint(collider: ICollider): ISpringJoint; + + /** + * Get whether two collision layers can collide with each other. + * @param layer1 - The first collision layer + * @param layer2 - The second collision layer + * @returns Whether the layers should collide + */ + getColliderLayerCollision(layer1: number, layer2: number): boolean; + + /** + * Set whether two collision layers can collide with each other. + * @param layer1 - The first collision layer + * @param layer2 - The second collision layer + * @param isCollide - Whether the layers should collide + */ + setColliderLayerCollision(layer1: number, layer2: number, isCollide: boolean): void; } diff --git a/packages/physics-lite/src/LiteCollider.ts b/packages/physics-lite/src/LiteCollider.ts index 32815750e..bb2b66666 100644 --- a/packages/physics-lite/src/LiteCollider.ts +++ b/packages/physics-lite/src/LiteCollider.ts @@ -1,9 +1,10 @@ import { ICollider } from "@galacean/engine-design"; -import { Quaternion, Ray, Vector3 } from "@galacean/engine"; +import { Layer, Quaternion, Ray, Vector3 } from "@galacean/engine"; import { LiteHitResult } from "./LiteHitResult"; import { LiteColliderShape } from "./shape/LiteColliderShape"; import { LiteTransform } from "./LiteTransform"; import { LitePhysicsScene } from "./LitePhysicsScene"; +import { LitePhysics } from "./LitePhysics"; /** * Abstract class of physical collider. @@ -11,6 +12,7 @@ import { LitePhysicsScene } from "./LitePhysicsScene"; export abstract class LiteCollider implements ICollider { /** @internal */ abstract readonly _isStaticCollider: boolean; + private _litePhysics: LitePhysics; /** @internal */ _scene: LitePhysicsScene; @@ -18,9 +20,12 @@ export abstract class LiteCollider implements ICollider { _shapes: LiteColliderShape[] = []; /** @internal */ _transform: LiteTransform = new LiteTransform(); + /** @internal */ + _collisionLayer: number; - protected constructor() { + protected constructor(litePhysics: LitePhysics) { this._transform.owner = this; + this._litePhysics = litePhysics; } /** @@ -67,6 +72,13 @@ export abstract class LiteCollider implements ICollider { outRotation.set(rotationQuaternion.x, rotationQuaternion.y, rotationQuaternion.z, rotationQuaternion.w); } + /** + * {@inheritDoc ICollider.setCollisionLayer } + */ + setCollisionLayer(collisionLayer: Layer): void { + this._litePhysics.setColliderLayer(this, collisionLayer); + } + /** * {@inheritDoc ICollider.destroy } */ diff --git a/packages/physics-lite/src/LiteDynamicCollider.ts b/packages/physics-lite/src/LiteDynamicCollider.ts index 8d49a4490..dae27bf3a 100644 --- a/packages/physics-lite/src/LiteDynamicCollider.ts +++ b/packages/physics-lite/src/LiteDynamicCollider.ts @@ -1,6 +1,7 @@ import { LiteCollider } from "./LiteCollider"; import { IDynamicCollider } from "@galacean/engine-design"; import { Logger, Quaternion, Vector3 } from "@galacean/engine"; +import { LitePhysics } from "./LitePhysics"; /** * A dynamic collider can act with self-defined movement or physical force @@ -13,8 +14,8 @@ export class LiteDynamicCollider extends LiteCollider implements IDynamicCollide * @param position - The global position * @param rotation - The global rotation */ - constructor(position: Vector3, rotation: Quaternion) { - super(); + constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) { + super(litePhysics); this._transform.setPosition(position.x, position.y, position.z); this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w); } diff --git a/packages/physics-lite/src/LitePhysics.ts b/packages/physics-lite/src/LitePhysics.ts index b666f857f..6bcdafda2 100644 --- a/packages/physics-lite/src/LitePhysics.ts +++ b/packages/physics-lite/src/LitePhysics.ts @@ -1,8 +1,9 @@ -import { Quaternion, Vector3 } from "@galacean/engine"; +import { Quaternion, Vector3, Layer } from "@galacean/engine"; import { IBoxColliderShape, ICapsuleColliderShape, ICharacterController, + ICollider, ICollision, IDynamicCollider, IFixedJoint, @@ -25,6 +26,8 @@ import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape"; import { LitePhysicsManager } from "./LitePhysicsManager"; export class LitePhysics implements IPhysics { + private _layerCollisionMatrix: boolean[] = []; + /** * {@inheritDoc IPhysics.initialize } */ @@ -52,6 +55,7 @@ export class LitePhysics implements IPhysics { onTriggerPersist?: (obj1: number, obj2: number) => void ): LitePhysicsScene { return new LitePhysicsScene( + this, onContactBegin, onContactEnd, onContactPersist, @@ -65,14 +69,14 @@ export class LitePhysics implements IPhysics { * {@inheritDoc IPhysics.createStaticCollider } */ createStaticCollider(position: Vector3, rotation: Quaternion): IStaticCollider { - return new LiteStaticCollider(position, rotation); + return new LiteStaticCollider(this, position, rotation); } /** * {@inheritDoc IPhysics.createDynamicCollider } */ createDynamicCollider(position: Vector3, rotation: Quaternion): IDynamicCollider { - return new LiteDynamicCollider(position, rotation); + return new LiteDynamicCollider(this, position, rotation); } /** @@ -148,4 +152,46 @@ export class LitePhysics implements IPhysics { createSpringJoint(collider: LiteCollider): ISpringJoint { throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!"; } + + /** + * {@inheritDoc IPhysics.setColliderLayer } + */ + setColliderLayer(collider: LiteCollider, layer: Layer): void { + collider._collisionLayer = layer; + } + + /** + * {@inheritDoc IPhysics.getColliderLayerCollision } + */ + getColliderLayerCollision(layer1: number, layer2: number): boolean { + const index = this._getColliderLayerIndex(layer1, layer2); + if (index > -1) { + return this._layerCollisionMatrix[index] ?? true; + } + // If either layer is Layer.Nothing, they cant collide + return false; + } + + /** + * {@inheritDoc IPhysics.setColliderLayerCollision } + */ + setColliderLayerCollision(layer1: number, layer2: number, collide: boolean): void { + const index = this._getColliderLayerIndex(layer1, layer2); + if (index > -1) { + this._layerCollisionMatrix[index] = collide; + } + } + + private _getColliderLayerIndex(layer1: number, layer2: number): number { + if (layer1 === 32 || layer2 === 32) { + return -1; + } + + const min = Math.min(layer1, layer2); + const max = Math.max(layer1, layer2); + + // Calculate a unique index for the layer pair using the triangular number formula + // This ensures that each layer combination maps to a unique index in the collision matrix + return (max * (max + 1)) / 2 + min; + } } diff --git a/packages/physics-lite/src/LitePhysicsScene.ts b/packages/physics-lite/src/LitePhysicsScene.ts index 13e1bb11c..ee482f00c 100644 --- a/packages/physics-lite/src/LitePhysicsScene.ts +++ b/packages/physics-lite/src/LitePhysicsScene.ts @@ -7,6 +7,7 @@ import { LiteStaticCollider } from "./LiteStaticCollider"; import { LiteBoxColliderShape } from "./shape/LiteBoxColliderShape"; import { LiteColliderShape } from "./shape/LiteColliderShape"; import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape"; +import { LitePhysics } from "./LitePhysics"; /** * A manager is a collection of colliders and constraints which can interact. @@ -32,8 +33,10 @@ export class LitePhysicsScene implements IPhysicsScene { private _currentEvents: DisorderedArray = new DisorderedArray(); private _eventMap: Record> = {}; private _eventPool: TriggerEvent[] = []; + private _physics: LitePhysics; constructor( + physics: LitePhysics, onContactEnter?: (collision: ICollision) => void, onContactExit?: (collision: ICollision) => void, onContactStay?: (collision: ICollision) => void, @@ -41,6 +44,7 @@ export class LitePhysicsScene implements IPhysicsScene { onTriggerExit?: (obj1: number, obj2: number) => void, onTriggerStay?: (obj1: number, obj2: number) => void ) { + this._physics = physics; this._onContactEnter = onContactEnter; this._onContactExit = onContactExit; this._onContactStay = onContactStay; @@ -216,7 +220,14 @@ export class LitePhysicsScene implements IPhysicsScene { if (myShape instanceof LiteBoxColliderShape) { LitePhysicsScene._updateWorldBox(myShape, this._box); for (let j = 0, len = colliders.length; j < len; j++) { - const colliderShape = colliders[j]._shapes; + const collider = colliders[j]; + const colliderShape = collider._shapes; + + // Skip collision check if layers can't collide + if (!this._checkColliderCollide(collider, myCollider)) { + continue; + } + for (let k = 0, len = colliderShape.length; k < len; k++) { const shape = colliderShape[k]; const index1 = shape._id; @@ -243,7 +254,14 @@ export class LitePhysicsScene implements IPhysicsScene { } else if (myShape instanceof LiteSphereColliderShape) { LitePhysicsScene._upWorldSphere(myShape, this._sphere); for (let j = 0, len = colliders.length; j < len; j++) { - const colliderShape = colliders[j]._shapes; + const collider = colliders[j]; + const colliderShape = collider._shapes; + + // Skip collision check if layers can't collide + if (!this._checkColliderCollide(collider, myCollider)) { + continue; + } + for (let k = 0, len = colliderShape.length; k < len; k++) { const shape = colliderShape[k]; const index1 = shape._id; @@ -345,6 +363,17 @@ export class LitePhysicsScene implements IPhysicsScene { return isHit; } + + private _checkColliderCollide(collider1: LiteCollider, collider2: LiteCollider): boolean { + const group1 = collider1._collisionLayer; + const group2 = collider2._collisionLayer; + + if (group1 === group2) { + return true; + } + + return this._physics.getColliderLayerCollision(group1, group2); + } } /** diff --git a/packages/physics-lite/src/LiteStaticCollider.ts b/packages/physics-lite/src/LiteStaticCollider.ts index 4706427ff..84e50bbd0 100644 --- a/packages/physics-lite/src/LiteStaticCollider.ts +++ b/packages/physics-lite/src/LiteStaticCollider.ts @@ -1,6 +1,7 @@ import { IStaticCollider } from "@galacean/engine-design"; import { LiteCollider } from "./LiteCollider"; import { Quaternion, Vector3 } from "@galacean/engine"; +import { LitePhysics } from "./LitePhysics"; /** * A static collider component that will not move. @@ -14,8 +15,8 @@ export class LiteStaticCollider extends LiteCollider implements IStaticCollider * @param position - The global position * @param rotation - The global rotation */ - constructor(position: Vector3, rotation: Quaternion) { - super(); + constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) { + super(litePhysics); this._transform.setPosition(position.x, position.y, position.z); this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w); } diff --git a/packages/physics-physx/src/PhysXCharacterController.ts b/packages/physics-physx/src/PhysXCharacterController.ts index 69c1b9940..c284245b4 100644 --- a/packages/physics-physx/src/PhysXCharacterController.ts +++ b/packages/physics-physx/src/PhysXCharacterController.ts @@ -88,6 +88,8 @@ export class PhysXCharacterController implements ICharacterController { * {@inheritDoc ICharacterController.addShape } */ addShape(shape: PhysXColliderShape): void { + // Add shape should sync latest position and world scale to pxController + this._updateShapePosition(shape._position, shape._worldScale); // When CharacterController is disabled, set shape property need check pxController whether exist because of this._pxManager is null and won't create pxController this._pxManager && this._createPXController(this._pxManager, shape); this._shape = shape; @@ -106,6 +108,17 @@ export class PhysXCharacterController implements ICharacterController { this._scene?._removeColliderShape(shape._id); } + /** + * {@inheritDoc ICollider.setCollisionLayer } + */ + setCollisionLayer(layer: number): void { + const actor = this._pxController?.getActor(); + + if (actor) { + this._physXPhysics._physX.setGroup(actor, layer); + } + } + /** * {@inheritDoc ICharacterController.destroy } */ @@ -136,6 +149,8 @@ export class PhysXCharacterController implements ICharacterController { this._pxController = pxManager._getControllerManager().createController(desc); this._pxController.setUUID(shape._id); + + this._updateNativePosition(); } /** @@ -156,7 +171,7 @@ export class PhysXCharacterController implements ICharacterController { this._updateNativePosition(); } - private _updateNativePosition() { + private _updateNativePosition(): void { const worldPosition = this._worldPosition; if (this._pxController && worldPosition) { Vector3.add(worldPosition, this._shapeScaledPosition, PhysXCharacterController._tempVec); diff --git a/packages/physics-physx/src/PhysXCollider.ts b/packages/physics-physx/src/PhysXCollider.ts index 1fbff0529..579d0f757 100644 --- a/packages/physics-physx/src/PhysXCollider.ts +++ b/packages/physics-physx/src/PhysXCollider.ts @@ -61,6 +61,13 @@ export abstract class PhysXCollider implements ICollider { outRotation.set(transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w); } + /** + * {@inheritDoc ICollider.setCollisionLayer } + */ + setCollisionLayer(layer: number): void { + this._physXPhysics._physX.setGroup(this._pxActor, layer); + } + /** * {@inheritDoc ICollider.destroy } */ diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts index 4c25e7aef..325da86be 100644 --- a/packages/physics-physx/src/PhysXPhysics.ts +++ b/packages/physics-physx/src/PhysXPhysics.ts @@ -1,8 +1,9 @@ -import { Quaternion, Vector3, version } from "@galacean/engine"; +import { Quaternion, Vector3, Layer } from "@galacean/engine"; import { IBoxColliderShape, ICapsuleColliderShape, ICharacterController, + ICollider, ICollision, IDynamicCollider, IFixedJoint, @@ -92,7 +93,7 @@ export class PhysXPhysics implements IPhysics { if (runtimeMode == PhysXRuntimeMode.JavaScript) { script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*PXxaQrGL0XsAAAAAAAAAAAAAARQnAQ/physx.release.downgrade.js`; } else if (runtimeMode == PhysXRuntimeMode.WebAssembly) { - script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*0Qq8Rob3_5oAAAAAAAAAAAAAARQnAQ/physx.release.js`; + script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*H4ElTYwBxwgAAAAAAAAAAAAAARQnAQ/physx.release.js`; } }); @@ -147,7 +148,7 @@ export class PhysXPhysics implements IPhysics { onTriggerEnd?: (obj1: number, obj2: number) => void, onTriggerStay?: (obj1: number, obj2: number) => void ): IPhysicsScene { - const manager = new PhysXPhysicsScene( + const scene = new PhysXPhysicsScene( this, physicsManager, onContactBegin, @@ -157,7 +158,7 @@ export class PhysXPhysics implements IPhysics { onTriggerEnd, onTriggerStay ); - return manager; + return scene; } /** @@ -248,6 +249,20 @@ export class PhysXPhysics implements IPhysics { return new PhysXSpringJoint(this, collider); } + /** + * {@inheritDoc IPhysics.getColliderLayerCollision } + */ + getColliderLayerCollision(layer1: number, layer2: number): boolean { + return this._physX.getGroupCollisionFlag(layer1, layer2); + } + + /** + * {@inheritDoc IPhysics.setColliderLayerCollision } + */ + setColliderLayerCollision(layer1: number, layer2: number, isCollide: boolean): void { + this._physX.setGroupCollisionFlag(layer1, layer2, isCollide); + } + private _init(physX: any): void { const version = physX.PX_PHYSICS_VERSION; const defaultErrorCallback = new physX.PxDefaultErrorCallback(); diff --git a/packages/physics-physx/src/shape/PhysXColliderShape.ts b/packages/physics-physx/src/shape/PhysXColliderShape.ts index 676c6dc5f..a8bd1963a 100644 --- a/packages/physics-physx/src/shape/PhysXColliderShape.ts +++ b/packages/physics-physx/src/shape/PhysXColliderShape.ts @@ -31,16 +31,10 @@ export abstract class PhysXColliderShape implements IColliderShape { _controllers: DisorderedArray = new DisorderedArray(); /** @internal */ _contractOffset: number = 0.02; - - protected _physXPhysics: PhysXPhysics; - protected _worldScale: Vector3 = new Vector3(1, 1, 1); - protected _position: Vector3 = new Vector3(); - protected _rotation: Vector3 = new Vector3(); - protected _axis: Quaternion = null; - protected _physXRotation: Quaternion = new Quaternion(); - - private _shapeFlags: ShapeFlag = ShapeFlag.SCENE_QUERY_SHAPE | ShapeFlag.SIMULATION_SHAPE; - + /** @internal */ + _worldScale: Vector3 = new Vector3(1, 1, 1); + /** @internal */ + _position: Vector3 = new Vector3(); /** @internal */ _pxMaterial: any; /** @internal */ @@ -50,6 +44,13 @@ export abstract class PhysXColliderShape implements IColliderShape { /** @internal */ _id: number; + protected _physXPhysics: PhysXPhysics; + protected _rotation: Vector3 = new Vector3(); + protected _axis: Quaternion = null; + protected _physXRotation: Quaternion = new Quaternion(); + + private _shapeFlags: ShapeFlag = ShapeFlag.SCENE_QUERY_SHAPE | ShapeFlag.SIMULATION_SHAPE; + constructor(physXPhysics: PhysXPhysics) { this._physXPhysics = physXPhysics; } diff --git a/tests/src/core/physics/CharacterController.test.ts b/tests/src/core/physics/CharacterController.test.ts index b673fdf58..3bcf817b4 100644 --- a/tests/src/core/physics/CharacterController.test.ts +++ b/tests/src/core/physics/CharacterController.test.ts @@ -9,7 +9,8 @@ import { PlaneColliderShape, DynamicCollider, Script, - ControllerCollisionFlag + ControllerCollisionFlag, + Layer } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; @@ -322,4 +323,124 @@ describe("CharacterController", function () { roleEntity.isActive = false; controller.destroy(); }); + + it("collision group", () => { + const controller = roleEntity.getComponent(CharacterController); + + const obstacleEntity = rootEntity.createChild("obstacle"); + obstacleEntity.transform.position = new Vector3(0, 0, 2); + + const obstacleCollider = obstacleEntity.addComponent(StaticCollider); + const triggerShape = new BoxColliderShape(); + triggerShape.size = new Vector3(1, 1, 1); + triggerShape.isTrigger = true; + obstacleCollider.addShape(triggerShape); + + class TriggerDetectionScript extends Script { + triggerEntered = false; + + onTriggerEnter() { + this.triggerEntered = true; + } + + reset() { + this.triggerEntered = false; + } + } + + const triggerScript = obstacleEntity.addComponent(TriggerDetectionScript); + + roleEntity.layer = Layer.Layer1; + obstacleEntity.layer = Layer.Layer2; + + controller.move(new Vector3(0, 0, 2), 0.0001, 0.1); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(triggerScript.triggerEntered).toBe(true); + + roleEntity.transform.position = new Vector3(0, 0, 0); + triggerScript.reset(); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false); + + controller.move(new Vector3(0, 0, 2), 0.0001, 0.1); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(triggerScript.triggerEntered).toBe(false); + + // 恢复默认设置 + engine.sceneManager.activeScene.physics.setColliderLayerCollision(1, 2, true); + }); + + it("should handle manual collision group setting with trigger", () => { + const controller = roleEntity.getComponent(CharacterController); + + const obstacleEntity = rootEntity.createChild("obstacle"); + obstacleEntity.transform.position = new Vector3(0, 0, 2); + + const obstacleCollider = obstacleEntity.addComponent(StaticCollider); + const triggerShape = new BoxColliderShape(); + triggerShape.size = new Vector3(1, 1, 1); + triggerShape.isTrigger = true; + obstacleCollider.addShape(triggerShape); + + class TriggerDetectionScript extends Script { + triggerEntered = false; + + onTriggerEnter() { + this.triggerEntered = true; + } + + reset() { + this.triggerEntered = false; + } + } + + const triggerScript = obstacleEntity.addComponent(TriggerDetectionScript); + + roleEntity.layer = Layer.Layer1; + obstacleEntity.layer = Layer.Layer2; + + controller.move(new Vector3(0, 0, 2), 0.0001, 0.1); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(triggerScript.triggerEntered).toBe(true); + + roleEntity.transform.position = new Vector3(0, 0, 0); + triggerScript.reset(); + + controller.collisionLayer = Layer.Layer10; + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, false); + + controller.move(new Vector3(0, 0, 2), 0.0001, 0.1); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(triggerScript.triggerEntered).toBe(false); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, true); + + // 恢复默认设置 + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true); + }); + + it("keep entity position when disabled", () => { + roleEntity.transform.position = new Vector3(0, 0, 3); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + const controller = roleEntity.getComponent(CharacterController); + controller.enabled = false; + controller.enabled = true; + // @ts-ignore + controller._syncWorldPositionFromPhysicalSpace(); + expect(roleEntity.transform.position.z).eq(3); + }); }); diff --git a/tests/src/core/physics/Collider.test.ts b/tests/src/core/physics/Collider.test.ts index e8eb0da7b..bb4490137 100644 --- a/tests/src/core/physics/Collider.test.ts +++ b/tests/src/core/physics/Collider.test.ts @@ -1,7 +1,9 @@ import { BoxColliderShape, + Collision, DynamicCollider, Entity, + Layer, PlaneColliderShape, Script, SphereColliderShape, @@ -9,8 +11,9 @@ import { } from "@galacean/engine-core"; import { Vector3 } from "@galacean/engine-math"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { LitePhysics } from "@galacean/engine-physics-lite"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; -import { vi, describe, beforeAll, beforeEach, expect, it } from "vitest"; +import { vi, describe, beforeAll, beforeEach, expect, it, afterEach } from "vitest"; class CollisionScript extends Script { onTriggerEnter = vi.fn(CollisionScript.prototype.onTriggerEnter); @@ -38,6 +41,22 @@ class MoveScript extends Script { } } +class CollisionDetectionScript extends Script { + collisionDetected = false; + + onTriggerEnter() { + this.collisionDetected = true; + } + + onCollisionEnter(other: Collision) { + this.collisionDetected = true; + } + + reset() { + this.collisionDetected = false; + } +} + describe("physics collider test", function () { let engine: WebGLEngine; let rootEntity: Entity; @@ -362,3 +381,165 @@ describe("physics collider test", function () { expect(collider.shapes.length).eq(0); }); }); + +describe("Collider Layer Collision Tests", () => { + describe("LitePhysics Layer Collision", () => { + let engine: WebGLEngine; + let rootEntity: Entity; + let physics: LitePhysics; + + beforeAll(async () => { + physics = new LitePhysics(); + engine = await WebGLEngine.create({ + canvas: document.createElement("canvas"), + physics + }); + rootEntity = engine.sceneManager.activeScene.createRootEntity("root"); + }); + + it("should respect collision group settings", () => { + const entity1 = rootEntity.createChild("entity1"); + const entity2 = rootEntity.createChild("entity2"); + + entity1.transform.position = new Vector3(0, 0, 0); + entity2.transform.position = new Vector3(0, 0, 0); + + const collider1 = entity1.addComponent(DynamicCollider); + const shape1 = new BoxColliderShape(); + shape1.size = new Vector3(1, 1, 1); + collider1.addShape(shape1); + + const collider2 = entity2.addComponent(DynamicCollider); + const shape2 = new BoxColliderShape(); + shape2.size = new Vector3(1, 1, 1); + collider2.addShape(shape2); + + entity1.layer = Layer.Layer1; + entity2.layer = Layer.Layer2; + + const script = entity1.addComponent(CollisionDetectionScript); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + expect(script.collisionDetected).toBe(true); + + script.reset(); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(script.collisionDetected).toBe(false); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true); + }); + + it("should handle manual collision group setting in LitePhysics", () => { + const entity1 = rootEntity.createChild("entity1"); + const entity2 = rootEntity.createChild("entity2"); + + entity1.transform.position = new Vector3(0, 0, 0); + entity2.transform.position = new Vector3(0, 0, 0); + + const collider1 = entity1.addComponent(DynamicCollider); + const shape1 = new BoxColliderShape(); + shape1.size = new Vector3(1, 1, 1); + collider1.addShape(shape1); + + const collider2 = entity2.addComponent(StaticCollider); + const shape2 = new BoxColliderShape(); + shape2.size = new Vector3(1, 1, 1); + shape2.isTrigger = true; + collider2.addShape(shape2); + + const script = entity2.addComponent(CollisionDetectionScript); + + entity1.layer = Layer.Layer1; + entity2.layer = Layer.Layer2; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + expect(script.collisionDetected).toBe(true); + + script.reset(); + + collider1.collisionLayer = Layer.Layer10; + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, false); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(script.collisionDetected).toBe(false); + + // 恢复默认设置 + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, true); + }); + + afterEach(() => { + const entities = rootEntity.children; + for (let i = entities.length - 1; i >= 0; i--) { + entities[i].destroy(); + } + }); + }); + + describe("PhysXPhysics Layer Collision", () => { + let engine: WebGLEngine; + let rootEntity: Entity; + + beforeAll(async () => { + engine = await WebGLEngine.create({ + canvas: document.createElement("canvas"), + physics: new PhysXPhysics() + }); + rootEntity = engine.sceneManager.activeScene.createRootEntity("root"); + }); + + it("should respect collision group settings", () => { + const entity1 = rootEntity.createChild("entity1"); + const entity2 = rootEntity.createChild("entity2"); + + entity1.transform.position = new Vector3(0, 0, 0); + entity2.transform.position = new Vector3(0, 0, 0); + + const collider1 = entity1.addComponent(DynamicCollider); + const shape1 = new BoxColliderShape(); + shape1.size = new Vector3(1, 1, 1); + collider1.addShape(shape1); + + const collider2 = entity2.addComponent(StaticCollider); + const shape2 = new BoxColliderShape(); + shape2.size = new Vector3(1, 1, 1); + collider2.addShape(shape2); + + entity1.layer = Layer.Layer1; + entity2.layer = Layer.Layer2; + + const script = entity1.addComponent(CollisionDetectionScript); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + expect(script.collisionDetected).toBe(true); + + script.reset(); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(script.collisionDetected).toBe(false); + + engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true); + }); + + afterEach(() => { + const entities = rootEntity.children; + for (let i = entities.length - 1; i >= 0; i--) { + entities[i].destroy(); + } + }); + }); +}); diff --git a/tests/src/core/physics/PhysicsManager.test.ts b/tests/src/core/physics/PhysicsManager.test.ts index ec253daa9..1cd294b52 100644 --- a/tests/src/core/physics/PhysicsManager.test.ts +++ b/tests/src/core/physics/PhysicsManager.test.ts @@ -8,6 +8,7 @@ import { Entity, HitResult, Layer, + PhysicsScene, Script, SphereColliderShape, StaticCollider @@ -114,13 +115,16 @@ function setColliderProps(entity: Entity, isDynamic: boolean, isTrigger: boolean describe("Physics Test", () => { describe("LitePhysics", () => { let engineLite: WebGLEngine; - + let physics: LitePhysics; + let physicsScene: PhysicsScene; beforeAll(async () => { + physics = new LitePhysics(); // Init engine with LitePhysics. engineLite = await WebGLEngine.create({ canvas: document.createElement("canvas"), - physics: new LitePhysics() + physics }); + physicsScene = engineLite.sceneManager.activeScene.physics; const rootEntityLitePhysics = engineLite.sceneManager.activeScene.createRootEntity("root_camera"); @@ -342,6 +346,34 @@ describe("Physics Test", () => { expect(collisionTestScript.onTriggerExit).toHaveBeenCalledTimes(1); }); + describe("Collision Group Tests", () => { + it("should set and get collision group settings correctly", () => { + physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer1, true); + expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer1)).to.eq(true); + physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer2, false); + expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer2)).to.eq(false); + physicsScene.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true); + expect(physicsScene.getColliderLayerCollision(Layer.Layer1, Layer.Layer2)).to.eq(true); + }); + + it("should handle edge cases in collision group matrix", () => { + const maxGroup = Layer.Layer31; + + physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, false); + expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(false); + physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, true); + expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(true); + }); + + it("should handle invalid collision groups correctly", () => { + const invalidGroup = -1; + // @ts-ignore + expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, false)).to.throw(); + // @ts-ignore + expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, true)).to.throw(); + }); + }); + afterEach(() => { const root = engineLite.sceneManager.activeScene.findEntityByName("root"); root?.destroy(); @@ -350,6 +382,7 @@ describe("Physics Test", () => { describe("PhysXPhysics", () => { let enginePhysX: WebGLEngine; + let physicsScene: PhysicsScene; beforeAll(async () => { // Init engine with PhysXPhysics. @@ -357,6 +390,8 @@ describe("Physics Test", () => { canvas: document.createElement("canvas"), physics: new PhysXPhysics() }); + physicsScene = enginePhysX.sceneManager.activeScene.physics; + const rootEntityPhysX = enginePhysX.sceneManager.activeScene.createRootEntity("root_camera"); const cameraEntityPhysX = rootEntityPhysX.createChild("camera"); @@ -498,6 +533,34 @@ describe("Physics Test", () => { root.destroy(); }); + describe("Collision Group Tests", () => { + it("should set and get collision group settings correctly", () => { + physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer1, true); + expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer1)).to.eq(true); + physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer2, false); + expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer2)).to.eq(false); + physicsScene.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true); + expect(physicsScene.getColliderLayerCollision(Layer.Layer1, Layer.Layer2)).to.eq(true); + }); + + it("should handle edge cases in collision group matrix", () => { + const maxGroup = Layer.Layer31; + + physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, false); + expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(false); + physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, true); + expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(true); + }); + + it("should handle invalid collision groups correctly", () => { + const invalidGroup = -1; + // @ts-ignore + expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, false)).to.throw(); + // @ts-ignore + expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, true)).to.throw(); + }); + }); + describe("Collision Test", () => { it("Dynamic Trigger vs Dynamic Trigger", () => { const physicsMgr = enginePhysX.physicsManager; diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts index 701479276..782c7177f 100644 --- a/tests/vitest.config.ts +++ b/tests/vitest.config.ts @@ -1,6 +1,9 @@ import { defineProject } from "vitest/config"; export default defineProject({ + server: { + port: 51204 + }, optimizeDeps: { exclude: [ "@galacean/engine",