From 43a26389f99a58b73cd665570eb032aff669c76e Mon Sep 17 00:00:00 2001 From: "chenmo.gl" Date: Tue, 29 Sep 2026 13:21:20 +0800 Subject: [PATCH] fix(physics): align material instancing with renderer semantics --- docs/en/physics/collider/colliderShape.mdx | 9 +- docs/zh/physics/collider/colliderShape.mdx | 10 +- e2e/case/physx-collision-group.ts | 2 +- e2e/case/physx-collision.ts | 12 +- e2e/case/physx-customUrl.ts | 12 +- e2e/case/physx-deferred-contact.ts | 4 +- packages/core/src/physics/PhysicsMaterial.ts | 16 +- .../core/src/physics/shape/ColliderShape.ts | 37 +- .../resources/parser/ReflectionParser.ts | 9 +- .../core/physics/CharacterController.test.ts | 8 +- tests/src/core/physics/Collider.test.ts | 12 +- tests/src/core/physics/ColliderShape.test.ts | 1 - tests/src/core/physics/Collision.test.ts | 4 +- .../src/core/physics/DynamicCollider.test.ts | 8 +- tests/src/core/physics/HingeJoint.test.ts | 4 +- tests/src/core/physics/Joint.test.ts | 4 +- .../core/physics/MeshColliderShape.test.ts | 76 ---- .../src/core/physics/PhysicsMaterial.test.ts | 396 ++++++++++++------ tests/src/core/physics/SpringJoint.test.ts | 4 +- 19 files changed, 350 insertions(+), 278 deletions(-) diff --git a/docs/en/physics/collider/colliderShape.mdx b/docs/en/physics/collider/colliderShape.mdx index 3470b336d..0671e3d15 100644 --- a/docs/en/physics/collider/colliderShape.mdx +++ b/docs/en/physics/collider/colliderShape.mdx @@ -29,11 +29,14 @@ Collision shapes ([ColliderShape](/apis/core/#ColliderShape)) define the physica ### Physics Material - [**material**](/apis/core/#ColliderShape-material) - Defines the physical material properties of the shape. Shapes without an assigned material use engine-defined defaults. The first read of `shape.material` creates a separate material for that shape; later reads return the same instance. Assigning `shape.material = material` directly does not create an extra default material. + Gets or sets the current material reference without creating or cloning a material. Multiple shapes can share the same material; changing its properties affects every shape referencing it. When no material is assigned, or the property is set to `null`, the shape uses the engine-defined shared default material and `material` returns `null`. -Cloned shapes share materials that were assigned or created by reading `material` before cloning. If the property has not been read or assigned, the source and clone each create a separate material on their first read of `material`. +- [**getInstanceMaterial()**](/apis/core/#ColliderShape-getInstanceMaterial) + Following Renderer's instance-material convention, the first call clones the current material and assigns the instance to this shape. If no material is assigned, it creates an instance with default properties. Later calls return the same instance until a different material or `null` is assigned. -Materials created explicitly or obtained through `material` must be released with `destroy()` when no shape uses them anymore. +Cloned shapes share their material reference; call `getInstanceMaterial()` on the clone to make its material independent. To modify only one shape, use `shape.getInstanceMaterial().bounciness = 0.5`. Editing properties through `shape.material` modifies the shared material. + +Materials created explicitly or obtained through `getInstanceMaterial()` must be released with `destroy()` when no shape uses them anymore. The backend manages the shared default material's lifetime. | Property | Description | Default Value | | ---- | ---- | ------ | diff --git a/docs/zh/physics/collider/colliderShape.mdx b/docs/zh/physics/collider/colliderShape.mdx index 1786c368e..c2b5d410d 100644 --- a/docs/zh/physics/collider/colliderShape.mdx +++ b/docs/zh/physics/collider/colliderShape.mdx @@ -29,11 +29,14 @@ label: Physics ### 物理材质 - [**material**](/apis/core/#ColliderShape-material) - 定义形状的物理材质属性。未指定材质时,形状使用引擎统一定义的默认属性。首次读取 `shape.material` 会为该形状创建独立材质,后续读取返回同一实例。直接赋值 `shape.material = material` 不会额外创建默认材质。 + 返回或设置当前材质引用,读取时不会创建或克隆材质。多个形状可以共享同一材质,对其属性的修改会影响所有引用它的形状。未指定材质或赋值为 `null` 时,形状使用引擎统一定义的共享默认材质,此时 `material` 返回 `null`。 -克隆形状时,已经赋值或通过读取 `material` 创建的材质按引用共享。若克隆前尚未读取或赋值,源形状和副本会在各自首次读取 `material` 时创建独立材质。 +- [**getInstanceMaterial()**](/apis/core/#ColliderShape-getInstanceMaterial) + 与 Renderer 的实例材质接口一致,首次调用会克隆当前材质并赋给该形状;未指定材质时,创建具有默认属性的实例。后续调用返回同一实例,直到重新赋予不同的材质或 `null`。 -显式创建或通过 `material` 取得的材质,需要在所有形状都不再使用后调用 `destroy()` 释放。 +克隆形状时,材质按引用共享;在副本上调用 `getInstanceMaterial()` 可将其独立出来。仅修改某个形状时,使用 `shape.getInstanceMaterial().bounciness = 0.5`;直接修改 `shape.material` 的属性则会修改共享材质。 + +显式创建或通过 `getInstanceMaterial()` 取得的材质,需要在所有形状都不再使用后调用 `destroy()` 释放。后端共享默认材质由后端管理,无需手动释放。 | 属性 | 描述 | 默认值 | | ---- | ---- | ------ | @@ -292,4 +295,3 @@ function createMixedPlatform(): Entity { - 合理设置局部偏移,避免碰撞形状过度重叠 - 适当使用触发器代替实际的物理碰撞 - 及时销毁不再使用的碰撞形状及物理材质 - diff --git a/e2e/case/physx-collision-group.ts b/e2e/case/physx-collision-group.ts index 94263f6d8..c4e74a80b 100644 --- a/e2e/case/physx-collision-group.ts +++ b/e2e/case/physx-collision-group.ts @@ -61,7 +61,7 @@ function createPhysicsSphere( // Add physics const physicsSphere = new SphereColliderShape(); physicsSphere.radius = radius; - physicsSphere.material.bounciness = 0.8; + physicsSphere.getInstanceMaterial().bounciness = 0.8; const sphereCollider = sphereEntity.addComponent(DynamicCollider); sphereCollider.collisionLayer = collisionLayer; diff --git a/e2e/case/physx-collision.ts b/e2e/case/physx-collision.ts index a7e298997..2d15ab3fc 100644 --- a/e2e/case/physx-collision.ts +++ b/e2e/case/physx-collision.ts @@ -46,9 +46,9 @@ function addBox(rootEntity: Entity, cubeSize: number, x: number, y: number, z: n const physicsBox = new BoxColliderShape(); physicsBox.size = new Vector3(cubeSize, cubeSize, cubeSize); - physicsBox.material.staticFriction = 0.1; - physicsBox.material.dynamicFriction = 0.2; - physicsBox.material.bounciness = 1; + physicsBox.getInstanceMaterial().staticFriction = 0.1; + physicsBox.getInstanceMaterial().dynamicFriction = 0.2; + physicsBox.getInstanceMaterial().bounciness = 1; physicsBox.isTrigger = true; const boxCollider = boxEntity.addComponent(StaticCollider); @@ -92,9 +92,9 @@ WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engi const physicsSphere = new SphereColliderShape(); physicsSphere.radius = radius; - physicsSphere.material.staticFriction = 0.1; - physicsSphere.material.dynamicFriction = 0.2; - physicsSphere.material.bounciness = 1; + physicsSphere.getInstanceMaterial().staticFriction = 0.1; + physicsSphere.getInstanceMaterial().dynamicFriction = 0.2; + physicsSphere.getInstanceMaterial().bounciness = 1; const sphereCollider = sphereEntity.addComponent(DynamicCollider); sphereCollider.isKinematic = true; diff --git a/e2e/case/physx-customUrl.ts b/e2e/case/physx-customUrl.ts index ddc1d1a75..7b92356ff 100644 --- a/e2e/case/physx-customUrl.ts +++ b/e2e/case/physx-customUrl.ts @@ -45,9 +45,9 @@ function addBox(rootEntity: Entity, cubeSize: number, x: number, y: number, z: n const physicsBox = new BoxColliderShape(); physicsBox.size = new Vector3(cubeSize, cubeSize, cubeSize); - physicsBox.material.staticFriction = 0.1; - physicsBox.material.dynamicFriction = 0.2; - physicsBox.material.bounciness = 1; + physicsBox.getInstanceMaterial().staticFriction = 0.1; + physicsBox.getInstanceMaterial().dynamicFriction = 0.2; + physicsBox.getInstanceMaterial().bounciness = 1; physicsBox.isTrigger = true; const boxCollider = boxEntity.addComponent(StaticCollider); @@ -97,9 +97,9 @@ WebGLEngine.create({ const physicsSphere = new SphereColliderShape(); physicsSphere.radius = radius; - physicsSphere.material.staticFriction = 0.1; - physicsSphere.material.dynamicFriction = 0.2; - physicsSphere.material.bounciness = 1; + physicsSphere.getInstanceMaterial().staticFriction = 0.1; + physicsSphere.getInstanceMaterial().dynamicFriction = 0.2; + physicsSphere.getInstanceMaterial().bounciness = 1; const sphereCollider = sphereEntity.addComponent(DynamicCollider); sphereCollider.isKinematic = true; diff --git a/e2e/case/physx-deferred-contact.ts b/e2e/case/physx-deferred-contact.ts index a3e903a3d..290acadd5 100644 --- a/e2e/case/physx-deferred-contact.ts +++ b/e2e/case/physx-deferred-contact.ts @@ -44,8 +44,8 @@ WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engi const shape = new BoxColliderShape(); shape.size = new Vector3(2, 2, 2); - shape.material.dynamicFriction = 0; - shape.material.staticFriction = 0; + shape.getInstanceMaterial().dynamicFriction = 0; + shape.getInstanceMaterial().staticFriction = 0; const collider = entity.addComponent(DynamicCollider); collider.addShape(shape); diff --git a/packages/core/src/physics/PhysicsMaterial.ts b/packages/core/src/physics/PhysicsMaterial.ts index c6314a8e9..7f8635c7f 100644 --- a/packages/core/src/physics/PhysicsMaterial.ts +++ b/packages/core/src/physics/PhysicsMaterial.ts @@ -100,7 +100,21 @@ export class PhysicsMaterial { } /** - * Destroy the material when the material is no be used by any shape. + * Clone this material. + * @returns A new material with the same properties + */ + clone(): PhysicsMaterial { + const material = new PhysicsMaterial(); + material.staticFriction = this._staticFriction; + material.dynamicFriction = this._dynamicFriction; + material.bounciness = this._bounciness; + material.frictionCombine = this._frictionCombine; + material.bounceCombine = this._bounceCombine; + return material; + } + + /** + * Destroy the material when it is no longer used by any shape. */ destroy() { !this._destroyed && this._nativeMaterial.destroy(); diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 5cfc628ce..7cb1fc585 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -23,7 +23,9 @@ export abstract class ColliderShape extends DataObject implements ICloneHook[] = []; if (props) { for (const key in props) { - const promise = this._resolveValue(props[key], instance, key).then((v) => { + const promise = this._resolveValue(props[key], instance[key]).then((v) => { instance[key] = v; }); promises.push(promise); @@ -95,7 +95,7 @@ export class ReflectionParser { * 9. { $signal } → signal binding * 10. plain object → recurse values (modify originValue in place if exists) */ - private _resolveValue(value: unknown, instance?: any, key?: string): Promise { + private _resolveValue(value: unknown, originValue?: any): Promise { if (value == null || typeof value !== "object") return Promise.resolve(value); if (Array.isArray(value)) return Promise.all(value.map((v) => this._resolveValue(v))); @@ -163,18 +163,17 @@ export class ReflectionParser { // $signal — signal binding: register listeners on the existing Signal instance if ("$signal" in obj) { - return this._resolveSignal(instance?.[key], obj.$signal as SignalListener[]); + return this._resolveSignal(originValue, obj.$signal as SignalListener[]); } // Plain object — recurse each value, modifying originValue in place or building a new object - const originValue = instance?.[key]; const target = originValue && typeof originValue === "object" && !Array.isArray(originValue) ? originValue : ({} as Record); const promises: Promise[] = []; for (const key in obj) { - promises.push(this._resolveValue(obj[key], target, key).then((v) => (target[key] = v))); + promises.push(this._resolveValue(obj[key], target[key]).then((v) => (target[key] = v))); } return Promise.all(promises).then(() => target); } diff --git a/tests/src/core/physics/CharacterController.test.ts b/tests/src/core/physics/CharacterController.test.ts index 8c7d86e14..8a1a82218 100644 --- a/tests/src/core/physics/CharacterController.test.ts +++ b/tests/src/core/physics/CharacterController.test.ts @@ -42,8 +42,8 @@ describe("CharacterController", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); @@ -140,7 +140,6 @@ describe("CharacterController", function () { expect(shape.collider).toBeFalsy(); shape._destroy(); - shape.material.destroy(); }); it("shape position", () => { @@ -215,7 +214,6 @@ describe("CharacterController", function () { expect(shape.collider).toBeFalsy(); shape._destroy(); - shape.material.destroy(); }); it("preserves contactOffset when transferring a shape to a rigid collider", () => { @@ -258,8 +256,6 @@ describe("CharacterController", function () { controllerEntity.destroy(); rigidEntity.destroy(); probe.destroy(); - shape.material.destroy(); - probeShape.material.destroy(); } }); diff --git a/tests/src/core/physics/Collider.test.ts b/tests/src/core/physics/Collider.test.ts index bd19c6288..b0c4fca62 100644 --- a/tests/src/core/physics/Collider.test.ts +++ b/tests/src/core/physics/Collider.test.ts @@ -69,8 +69,8 @@ describe("physics collider test", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); @@ -105,7 +105,7 @@ describe("physics collider test", function () { boxEntity.transform.position.y = cubeSize / 2; physicsBox = new BoxColliderShape(); physicsBox.size = new Vector3(cubeSize, cubeSize, cubeSize); - physicsBox.material.bounciness = 0.1; + physicsBox.getInstanceMaterial().bounciness = 0.1; const radius = 1.25; sphereEntity.transform.position.x = 0; @@ -309,7 +309,7 @@ describe("physics collider test", function () { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); collider.applyForce(new Vector3(1000, 0, 0)); - collider.shapes[0].material.dynamicFriction = 1; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 1; //@ts-ignore engine.sceneManager.activeScene.physics._update(1); expect(formatValue(collider.linearVelocity.x)).eq(0.97066); @@ -324,7 +324,7 @@ describe("physics collider test", function () { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); collider2.applyForce(new Vector3(1000, 0, 0)); - collider2.shapes[0].material.dynamicFriction = 1; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 1; //@ts-ignore engine.sceneManager.activeScene.physics._update(1); expect(formatValue(collider2.linearVelocity.x)).eq(0.97066); @@ -338,7 +338,7 @@ describe("physics collider test", function () { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); collider.applyForce(new Vector3(1000, 0, 0)); - collider.shapes[0].material.dynamicFriction = 1; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 1; // @ts-ignore engine.sceneManager.activeScene.physics._update(1); expect(formatValue(collider.linearVelocity.x)).eq(0.97066); diff --git a/tests/src/core/physics/ColliderShape.test.ts b/tests/src/core/physics/ColliderShape.test.ts index 9d9aec50a..0e7b80926 100644 --- a/tests/src/core/physics/ColliderShape.test.ts +++ b/tests/src/core/physics/ColliderShape.test.ts @@ -96,7 +96,6 @@ describe("ColliderShape PhysX", () => { dynamicCollider.removeShape(shape); shape._destroy(); - shape.material.destroy(); }); it("CapsuleColliderShape", () => { diff --git a/tests/src/core/physics/Collision.test.ts b/tests/src/core/physics/Collision.test.ts index 647ce8fbc..b157e6c51 100644 --- a/tests/src/core/physics/Collision.test.ts +++ b/tests/src/core/physics/Collision.test.ts @@ -14,8 +14,8 @@ describe("Collision", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 1d41334a5..6bb5ced7c 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -24,8 +24,8 @@ describe("DynamicCollider", function () { planeEntity.transform.setScale(20, 1, 20); const physicsPlane = new PlaneColliderShape(); - physicsPlane.material.dynamicFriction = 0; - physicsPlane.material.staticFriction = 0; + physicsPlane.getInstanceMaterial().dynamicFriction = 0; + physicsPlane.getInstanceMaterial().staticFriction = 0; const planeCollider = planeEntity.addComponent(StaticCollider); planeCollider.addShape(physicsPlane); return planeEntity; @@ -36,8 +36,8 @@ describe("DynamicCollider", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); if (type === DynamicCollider) { diff --git a/tests/src/core/physics/HingeJoint.test.ts b/tests/src/core/physics/HingeJoint.test.ts index 7b5117204..61ac9c4b9 100644 --- a/tests/src/core/physics/HingeJoint.test.ts +++ b/tests/src/core/physics/HingeJoint.test.ts @@ -23,8 +23,8 @@ describe("HingeJoint", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); diff --git a/tests/src/core/physics/Joint.test.ts b/tests/src/core/physics/Joint.test.ts index 75d6326c5..67e9090df 100644 --- a/tests/src/core/physics/Joint.test.ts +++ b/tests/src/core/physics/Joint.test.ts @@ -13,8 +13,8 @@ describe("Joint", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index d71e3d049..6ec566934 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -85,7 +85,6 @@ describe("MeshColliderShape PhysX", () => { [0, 1, 2, 3, 4, 5] ); meshShape.mesh = mesh; - const defaultMaterial = meshShape.material; staticCollider.addShape(meshShape); expect(meshShape).toBeDefined(); @@ -93,7 +92,6 @@ describe("MeshColliderShape PhysX", () => { // Cleanup groundEntity.destroy(); - defaultMaterial?.destroy(); }); it("should detect collision between sphere and triangle mesh", async () => { @@ -103,7 +101,6 @@ describe("MeshColliderShape PhysX", () => { const groundCollider = groundEntity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; // Ground plane at y=0, CCW winding -> normal +Y const mesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); meshShape.mesh = mesh; @@ -114,7 +111,6 @@ describe("MeshColliderShape PhysX", () => { sphereEntity.transform.setPosition(0, 2, 0); const dynamicCollider = sphereEntity.addComponent(DynamicCollider); const sphereShape = new SphereColliderShape(); - const sphereMaterial = sphereShape.material; sphereShape.radius = 0.5; dynamicCollider.addShape(sphereShape); @@ -134,8 +130,6 @@ describe("MeshColliderShape PhysX", () => { // Cleanup groundEntity.destroy(); sphereEntity.destroy(); - meshMaterial?.destroy(); - sphereMaterial?.destroy(); }); it("should support position and rotation offset", () => { @@ -143,7 +137,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -157,7 +150,6 @@ describe("MeshColliderShape PhysX", () => { expect(meshShape.rotation).toEqual(expect.objectContaining({ x: 0, y: 45, z: 0 })); entity.destroy(); - defaultMaterial?.destroy(); }); it("should support physics material", () => { @@ -165,7 +157,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -181,7 +172,6 @@ describe("MeshColliderShape PhysX", () => { expect(meshShape.material.staticFriction).toBe(0.5); entity.destroy(); - defaultMaterial?.destroy(); material?.destroy(); }); }); @@ -193,7 +183,6 @@ describe("MeshColliderShape PhysX", () => { // Create a simple tetrahedron (convex shape) const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; meshShape.isConvex = true; const mesh = createModelMesh(engine, [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1]); @@ -204,7 +193,6 @@ describe("MeshColliderShape PhysX", () => { expect(dynamicCollider.shapes.length).toBe(1); entity.destroy(); - defaultMaterial?.destroy(); }); it("keeps the convex shape when non-convex mode is unsupported", () => { @@ -212,7 +200,6 @@ describe("MeshColliderShape PhysX", () => { const dynamicCollider = entity.addComponent(DynamicCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; const convexMesh = createModelMesh( engine, [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1], @@ -233,7 +220,6 @@ describe("MeshColliderShape PhysX", () => { } finally { consoleErrorSpy.mockRestore(); entity.destroy(); - meshMaterial?.destroy(); } }); @@ -243,7 +229,6 @@ describe("MeshColliderShape PhysX", () => { groundEntity.transform.setPosition(0, -2, 0); const groundCollider = groundEntity.addComponent(StaticCollider); const groundShape = new BoxColliderShape(); - const groundMaterial = groundShape.material; groundShape.size = new Vector3(20, 1, 20); groundCollider.addShape(groundShape); @@ -253,7 +238,6 @@ describe("MeshColliderShape PhysX", () => { const dynamicCollider = convexEntity.addComponent(DynamicCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = true; const mesh = createModelMesh(engine, [0, 0.5, 0, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0, -0.5, 0.5]); meshShape.mesh = mesh; @@ -274,8 +258,6 @@ describe("MeshColliderShape PhysX", () => { groundEntity.destroy(); convexEntity.destroy(); - groundMaterial?.destroy(); - meshMaterial?.destroy(); }); }); @@ -287,7 +269,6 @@ describe("MeshColliderShape PhysX", () => { const triggerCollider = triggerEntity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = true; const mesh = createModelMesh( engine, @@ -302,7 +283,6 @@ describe("MeshColliderShape PhysX", () => { sphereEntity.transform.setPosition(0, 5, 0); const dynamicCollider = sphereEntity.addComponent(DynamicCollider); const sphereShape = new SphereColliderShape(); - const sphereMaterial = sphereShape.material; sphereShape.radius = 0.3; dynamicCollider.addShape(sphereShape); @@ -317,8 +297,6 @@ describe("MeshColliderShape PhysX", () => { triggerEntity.destroy(); sphereEntity.destroy(); - meshMaterial?.destroy(); - sphereMaterial?.destroy(); }); }); @@ -331,7 +309,6 @@ describe("MeshColliderShape PhysX", () => { const groundCollider = groundEntity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; // Flip winding order to make normals face +Y (up) const mesh = createModelMesh( engine, @@ -346,7 +323,6 @@ describe("MeshColliderShape PhysX", () => { sphereEntity.transform.setPosition(3, 2, 0); // At x=3, within scaled range const dynamicCollider = sphereEntity.addComponent(DynamicCollider); const sphereShape = new SphereColliderShape(); - const sphereMaterial = sphereShape.material; sphereShape.radius = 0.5; dynamicCollider.addShape(sphereShape); @@ -360,8 +336,6 @@ describe("MeshColliderShape PhysX", () => { groundEntity.destroy(); sphereEntity.destroy(); - meshMaterial?.destroy(); - sphereMaterial?.destroy(); }); it("should apply correct scale when adding shape to existing collider at runtime", async () => { @@ -377,7 +351,6 @@ describe("MeshColliderShape PhysX", () => { // Add initial shape and run physics to clear _updateFlag const initialShape = new BoxColliderShape(); - const initialMaterial = initialShape.material; initialShape.size = new Vector3(0.1, 0.1, 0.1); // Small box, won't interfere initialShape.position = new Vector3(100, 0, 100); // Far away groundCollider.addShape(initialShape); @@ -390,7 +363,6 @@ describe("MeshColliderShape PhysX", () => { // Now add mesh shape at runtime - this is the critical test // If setWorldScale is not called in _addNativeShape, the mesh will have scale (1,1,1) const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; // Small ground plane: -2 to 2 in local space, but scaled by 2 -> -4 to 4 in world space const mesh = createModelMesh( engine, @@ -407,7 +379,6 @@ describe("MeshColliderShape PhysX", () => { sphereEntity.transform.setPosition(3, 2, 0); const dynamicCollider = sphereEntity.addComponent(DynamicCollider); const sphereShape = new SphereColliderShape(); - const sphereMaterial = sphereShape.material; sphereShape.radius = 0.5; dynamicCollider.addShape(sphereShape); @@ -422,9 +393,6 @@ describe("MeshColliderShape PhysX", () => { groundEntity.destroy(); sphereEntity.destroy(); - initialMaterial?.destroy(); - meshMaterial?.destroy(); - sphereMaterial?.destroy(); }); }); @@ -436,7 +404,6 @@ describe("MeshColliderShape PhysX", () => { entity.transform.setPosition(100, 100, 0); const collider = entity.addComponent(StaticCollider); const shape = new MeshColliderShape(); - const material = shape.material; const vertices = [-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1]; const indices = [ 0, 3, 2, 0, 2, 1, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 3, 7, 6, 3, 6, 2, 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5 @@ -468,7 +435,6 @@ describe("MeshColliderShape PhysX", () => { physicsScene._update(1 / 60); const collided = script.collided; probe.destroy(); - probeShape.material.destroy(); return collided; }; @@ -497,7 +463,6 @@ describe("MeshColliderShape PhysX", () => { } finally { physicsScene.setColliderLayerCollision(Layer.Layer5, Layer.Layer6, true); entity.destroy(); - material.destroy(); mesh.destroy(); replacementMesh.destroy(); } @@ -509,7 +474,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; // Initial mesh const mesh1 = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); @@ -526,14 +490,12 @@ describe("MeshColliderShape PhysX", () => { expect((staticCollider as any)._nativeCollider._shapes).toEqual([(meshShape as any)._nativeShape]); entity.destroy(); - defaultMaterial?.destroy(); }); it("keeps the existing native mesh when runtime mesh recooking fails", () => { const groundEntity = root.createChild("transactionalMeshUpdateGround"); const staticCollider = groundEntity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; const groundMesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); meshShape.mesh = groundMesh; staticCollider.addShape(meshShape); @@ -559,7 +521,6 @@ describe("MeshColliderShape PhysX", () => { consoleErrorSpy.mockRestore(); destroySpy.mockRestore(); groundEntity.destroy(); - meshMaterial?.destroy(); } }); @@ -567,7 +528,6 @@ describe("MeshColliderShape PhysX", () => { const entity = root.createChild("failedMeshReplacementAttachment"); const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = true; meshShape.isTrigger = true; const oldMesh = createModelMesh( @@ -581,7 +541,6 @@ describe("MeshColliderShape PhysX", () => { const dynamicCollider = sphereEntity.addComponent(DynamicCollider); dynamicCollider.isKinematic = true; const sphereShape = new SphereColliderShape(); - const sphereMaterial = sphereShape.material; sphereShape.radius = 0.25; dynamicCollider.addShape(sphereShape); const triggerScript = sphereEntity.addComponent(CollisionScript); @@ -628,8 +587,6 @@ describe("MeshColliderShape PhysX", () => { candidateDestroySpy?.mockRestore(); entity.destroy(); sphereEntity.destroy(); - meshMaterial?.destroy(); - sphereMaterial?.destroy(); } }); }); @@ -643,7 +600,6 @@ describe("MeshColliderShape PhysX", () => { dynamicCollider.isKinematic = false; // Ensure non-kinematic const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = false; // Triangle mesh const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -654,7 +610,6 @@ describe("MeshColliderShape PhysX", () => { errorSpy.mockRestore(); entity.destroy(); - meshMaterial?.destroy(); }); it("should log error when setting isKinematic to false with existing triangle mesh", () => { @@ -665,7 +620,6 @@ describe("MeshColliderShape PhysX", () => { dynamicCollider.isKinematic = true; // Start as kinematic const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = false; // Triangle mesh const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -679,7 +633,6 @@ describe("MeshColliderShape PhysX", () => { errorSpy.mockRestore(); entity.destroy(); - meshMaterial?.destroy(); }); it("should NOT log error when adding triangle mesh to kinematic DynamicCollider", () => { @@ -690,7 +643,6 @@ describe("MeshColliderShape PhysX", () => { dynamicCollider.isKinematic = true; const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = false; // Triangle mesh const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -701,7 +653,6 @@ describe("MeshColliderShape PhysX", () => { errorSpy.mockRestore(); entity.destroy(); - meshMaterial?.destroy(); }); it("should clone a kinematic triangle mesh that participates in raycasts", () => { @@ -712,7 +663,6 @@ describe("MeshColliderShape PhysX", () => { dynamicCollider.automaticInertiaTensor = false; const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; const mesh = createModelMesh(engine, [-5, 0, -5, 5, 0, -5, -5, 0, 5, 5, 0, 5], [0, 2, 1, 1, 2, 3]); meshShape.mesh = mesh; dynamicCollider.addShape(meshShape); @@ -729,7 +679,6 @@ describe("MeshColliderShape PhysX", () => { expect(hit.shape).toBe(clonedShape); clone.destroy(); - meshMaterial?.destroy(); }); it("should NOT log error when adding convex mesh to non-kinematic DynamicCollider", () => { @@ -740,7 +689,6 @@ describe("MeshColliderShape PhysX", () => { dynamicCollider.isKinematic = false; const meshShape = new MeshColliderShape(); - const meshMaterial = meshShape.material; meshShape.isConvex = true; // Convex mesh - should work const mesh = createModelMesh(engine, [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1]); meshShape.mesh = mesh; @@ -751,7 +699,6 @@ describe("MeshColliderShape PhysX", () => { errorSpy.mockRestore(); entity.destroy(); - meshMaterial?.destroy(); }); }); @@ -760,7 +707,6 @@ describe("MeshColliderShape PhysX", () => { const entity = root.createChild("rejectedNativeShape"); const collider = entity.addComponent(StaticCollider); const shape = new BoxColliderShape(); - const material = shape.material; const nativeCollider = (collider as any)._nativeCollider; const attachSpy = vi.spyOn(nativeCollider._pxActor, "attachShape").mockReturnValue(false); @@ -775,7 +721,6 @@ describe("MeshColliderShape PhysX", () => { attachSpy.mockRestore(); entity.destroy(); (shape as any)._destroy(); - material?.destroy(); } }); }); @@ -788,7 +733,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; // Create mesh with releaseData=true so accessible becomes false const mesh = new ModelMesh(engine); @@ -806,7 +750,6 @@ describe("MeshColliderShape PhysX", () => { warnSpy.mockRestore(); entity.destroy(); - defaultMaterial?.destroy(); }); it("should warn when non-convex mesh has no indices", () => { @@ -816,7 +759,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; meshShape.isConvex = false; // Create mesh without indices @@ -833,7 +775,6 @@ describe("MeshColliderShape PhysX", () => { warnSpy.mockRestore(); entity.destroy(); - defaultMaterial?.destroy(); }); }); @@ -843,7 +784,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; meshShape.isConvex = true; const mesh = createModelMesh( @@ -868,14 +808,12 @@ describe("MeshColliderShape PhysX", () => { expect(secondNativeShape).not.toBe(firstNativeShape); entity.destroy(); - defaultMaterial?.destroy(); }); it("should switch to non-convex using cached indices after mesh data is released", () => { const entity = root.createChild("releasedConvexMeshData"); const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh = createModelMesh( engine, [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1], @@ -894,7 +832,6 @@ describe("MeshColliderShape PhysX", () => { expect((staticCollider as any)._nativeCollider._shapes).toEqual([(meshShape as any)._nativeShape]); entity.destroy(); - defaultMaterial?.destroy(); }); it("should keep the convex shape when switching requires missing indices", () => { @@ -904,7 +841,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; meshShape.isConvex = true; // Create convex mesh (no indices needed for convex) @@ -928,7 +864,6 @@ describe("MeshColliderShape PhysX", () => { warnSpy.mockRestore(); entity.destroy(); - defaultMaterial?.destroy(); }); }); @@ -938,7 +873,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh1 = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh1; @@ -962,7 +896,6 @@ describe("MeshColliderShape PhysX", () => { expect((staticCollider as any)._nativeCollider._shapes).toEqual([(meshShape as any)._nativeShape]); entity.destroy(); - defaultMaterial?.destroy(); }); }); @@ -971,7 +904,6 @@ describe("MeshColliderShape PhysX", () => { const entity = root.createChild("releasedMeshData"); const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh = createModelMesh( engine, [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1], @@ -987,7 +919,6 @@ describe("MeshColliderShape PhysX", () => { expect((staticCollider as any)._nativeCollider._shapes).toEqual([(meshShape as any)._nativeShape]); entity.destroy(); - defaultMaterial?.destroy(); }); it("should replace the native shape when changing cookingFlags", () => { @@ -995,7 +926,6 @@ describe("MeshColliderShape PhysX", () => { const staticCollider = entity.addComponent(StaticCollider); const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; const mesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); meshShape.mesh = mesh; @@ -1012,18 +942,14 @@ describe("MeshColliderShape PhysX", () => { expect((staticCollider as any)._nativeCollider._shapes).toEqual([nativeShapeAfter]); entity.destroy(); - defaultMaterial?.destroy(); }); it("should not update when no mesh is set", () => { const meshShape = new MeshColliderShape(); - const defaultMaterial = meshShape.material; // Change cookingFlags without mesh - should not throw meshShape.cookingFlags = MeshColliderShapeCookingFlag.Cleaning; expect(meshShape.cookingFlags).toBe(MeshColliderShapeCookingFlag.Cleaning); - - defaultMaterial?.destroy(); }); }); @@ -1093,7 +1019,6 @@ describe("MeshColliderShape PhysX", () => { const entity = root.createChild("failedMeshCloneSource"); const collider = entity.addComponent(StaticCollider); const shape = new MeshColliderShape(); - const material = shape.material; const mesh = createModelMesh(engine, [-1, 0, -1, 1, 0, -1, 0, 0, 1], [0, 1, 2]); shape.mesh = mesh; collider.addShape(shape); @@ -1122,7 +1047,6 @@ describe("MeshColliderShape PhysX", () => { consoleErrorSpy.mockRestore(); clone?.destroy(); entity.destroy(); - material?.destroy(); } }); }); diff --git a/tests/src/core/physics/PhysicsMaterial.test.ts b/tests/src/core/physics/PhysicsMaterial.test.ts index 6d273b1fc..897332b84 100644 --- a/tests/src/core/physics/PhysicsMaterial.test.ts +++ b/tests/src/core/physics/PhysicsMaterial.test.ts @@ -11,7 +11,8 @@ import { ModelMesh, CharacterController, StaticCollider, - PlaneColliderShape + PlaneColliderShape, + Loader } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { PhysXRuntimeMode } from "@galacean/engine-physics-physx"; @@ -27,7 +28,7 @@ import { describe, beforeAll, beforeEach, afterAll, expect, it, vi } from "vites const runtimeModes = [PhysXRuntimeMode.WebAssembly, PhysXRuntimeMode.WebAssemblySIMD]; describe.each(runtimeModes)("PhysicsMaterial defaults (%s)", (runtimeMode) => { - it("uses the engine's complete default policy for shared and lazy materials", async () => { + it("uses the engine's complete default policy for shared and instance materials", async () => { const originalDefaults = PhysicsMaterial._defaultProperties; const defaults = { staticFriction: 0.2, @@ -58,7 +59,7 @@ describe.each(runtimeModes)("PhysicsMaterial defaults (%s)", (runtimeMode) => { expect(setFrictionCombine).toHaveBeenCalledExactlyOnceWith(PhysicsMaterialCombineMode.Minimum); expect(setBounceCombine).toHaveBeenCalledExactlyOnceWith(PhysicsMaterialCombineMode.Maximum); - material = shape.material; + material = shape.getInstanceMaterial(); for (const key in defaults) { expect(material[key]).toBe(defaults[key]); } @@ -90,9 +91,9 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { planeEntity.transform.setScale(20, 1, 20); const physicsPlane = new PlaneColliderShape(); - physicsPlane.material.dynamicFriction = 0; - physicsPlane.material.staticFriction = 0; - physicsPlane.material.bounciness = 0; + physicsPlane.getInstanceMaterial().dynamicFriction = 0; + physicsPlane.getInstanceMaterial().staticFriction = 0; + physicsPlane.getInstanceMaterial().bounciness = 0; const planeCollider = planeEntity.addComponent(StaticCollider); planeCollider.addShape(physicsPlane); return planeEntity; @@ -103,8 +104,8 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox); @@ -161,6 +162,7 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { } clone = source.clone(); for (const shape of [...shapes, ...clone.getComponent(StaticCollider).shapes]) { + expect(shape.material).toBeNull(); expect((shape._nativeShape as any)._pxMaterial).toBe(physics._defaultMaterial._pxMaterial); } expect(createMaterial).not.toHaveBeenCalled(); @@ -173,44 +175,110 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { }); it.each([BoxColliderShape, SphereColliderShape, CapsuleColliderShape, PlaneColliderShape, MeshColliderShape])( - "creates a private material only on the first material access for %s", + "instantiates the default only through getInstanceMaterial for %s", (Shape) => { const shape = new Shape(); const other = new Shape(); const createMaterial = vi.spyOn(physics._pxPhysics, "createMaterial"); - const material = shape.material; + let material: PhysicsMaterial; try { + expect(shape.material).toBeNull(); + expect(other.material).toBeNull(); + expect(createMaterial).not.toHaveBeenCalled(); + material = shape.getInstanceMaterial(); expect(shape.material).toBe(material); + expect(shape.getInstanceMaterial()).toBe(material); expect(createMaterial).toHaveBeenCalledTimes(1); expect(material.staticFriction).toBe(0.6); expect(material.dynamicFriction).toBe(0.6); expect(material.bounciness).toBe(0); material.bounciness = 1; - expect(other.material).not.toBe(material); - expect(other.material.bounciness).toBe(0); - expect(createMaterial).toHaveBeenCalledTimes(2); + expect(other.material).toBeNull(); if (shape._nativeShape) { expect((shape._nativeShape as any)._pxMaterial).toBe((material._nativeMaterial as any)._pxMaterial); + expect((other._nativeShape as any)._pxMaterial).toBe(physics._defaultMaterial._pxMaterial); } + shape.material = null; + expect(shape.material).toBeNull(); + expect(createMaterial).toHaveBeenCalledTimes(1); + if (shape._nativeShape) { + expect((shape._nativeShape as any)._pxMaterial).toBe(physics._defaultMaterial._pxMaterial); + } + const nextMaterial = shape.getInstanceMaterial(); + expect(nextMaterial).not.toBe(material); + expect(nextMaterial.bounciness).toBe(0); + expect(createMaterial).toHaveBeenCalledTimes(2); } finally { createMaterial.mockRestore(); shape._destroy(); other._destroy(); - material.destroy(); - other.material.destroy(); + material?.destroy(); + shape.material?.destroy(); } } ); - it("keeps untouched clone defaults independent when materials are first accessed", () => { + it("shares assigned materials and clones all properties only for an explicit instance", () => { + const shape = new BoxColliderShape(); + const other = new BoxColliderShape(); + const shared = new PhysicsMaterial(); + shared.staticFriction = 0.2; + shared.dynamicFriction = 0.4; + shared.bounciness = 0.8; + shared.frictionCombine = PhysicsMaterialCombineMode.Minimum; + shared.bounceCombine = PhysicsMaterialCombineMode.Maximum; + const createMaterial = vi.spyOn(physics._pxPhysics, "createMaterial"); + let instance: PhysicsMaterial; + let replacement: PhysicsMaterial; + try { + shape.material = shared; + other.material = shared; + expect(shape.material).toBe(shared); + expect(other.material).toBe(shared); + shape.material.dynamicFriction = 0.3; + expect(other.material.dynamicFriction).toBe(0.3); + expect(createMaterial).not.toHaveBeenCalled(); + + instance = shape.getInstanceMaterial(); + expect(instance).not.toBe(shared); + expect(shape.material).toBe(instance); + expect(other.material).toBe(shared); + for (const key of Object.keys(PhysicsMaterial._defaultProperties)) { + expect(instance[key]).toBe(shared[key]); + } + expect((shape._nativeShape as any)._pxMaterial).toBe((instance._nativeMaterial as any)._pxMaterial); + instance.dynamicFriction = 0.9; + expect(shared.dynamicFriction).toBe(0.3); + shape.material = instance; + expect(shape.getInstanceMaterial()).toBe(instance); + expect(createMaterial).toHaveBeenCalledTimes(1); + + shape.material = shared; + expect(shape.material).toBe(shared); + replacement = shape.getInstanceMaterial(); + expect(replacement).not.toBe(instance); + expect(replacement).not.toBe(shared); + expect(replacement.dynamicFriction).toBe(0.3); + expect(createMaterial).toHaveBeenCalledTimes(2); + } finally { + createMaterial.mockRestore(); + shape._destroy(); + other._destroy(); + shared.destroy(); + instance?.destroy(); + replacement?.destroy(); + } + }); + + it("creates independent instances from untouched clone defaults", () => { const source = rootEntity.createChild("defaultSource"); const collider = source.addComponent(StaticCollider); const shape = new BoxColliderShape(); collider.addShape(shape); const clone = source.clone(); const cloneShape = clone.getComponent(StaticCollider).shapes[0]; - const material = shape.material; - const cloneMaterial = cloneShape.material; + const material = shape.getInstanceMaterial(); + const cloneMaterial = cloneShape.getInstanceMaterial(); try { material.dynamicFriction = 0; expect(cloneMaterial).not.toBe(material); @@ -238,7 +306,7 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { shape.isConvex = true; expect((shape._nativeShape as any)._pxMaterial).toBe(physics._defaultMaterial._pxMaterial); expect(createMaterial).not.toHaveBeenCalled(); - material = shape.material; + material = shape.getInstanceMaterial(); material.bounciness = 0.8; shape.isConvex = false; expect((shape._nativeShape as any)._pxMaterial).toBe((material._nativeMaterial as any)._pxMaterial); @@ -251,7 +319,7 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { } }); - it.each([false, true])("loads an assigned material without invoking the lazy getter (nested: %s)", async (nested) => { + it.each([false, true])("loads a shared material without allocating a default (nested: %s)", async (nested) => { const shape = new BoxColliderShape(); const material = new PhysicsMaterial(); const context = new ParserContext(engine, ParserType.Scene, engine.sceneManager.activeScene); @@ -274,43 +342,82 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { } }); - it("applies late material access and replacement to an active character controller", () => { - const entity = rootEntity.createChild("materialController"); - entity.transform.setPosition(100, 100, 0); - const controller = entity.addComponent(CharacterController); + it("loads an explicitly constructed material and resets it to the shared default", async () => { + Loader.registerClass("PhysicsMaterial", PhysicsMaterial); const shape = new BoxColliderShape(); - controller.addShape(shape); - const dropProbe = () => { - const probe = rootEntity.createChild("materialProbe"); - probe.transform.setPosition(100, 104, 0); - probe.addComponent(DynamicCollider).addShape(new SphereColliderShape()); - for (let i = 0; i < 90; i++) { - engine.sceneManager.activeScene.physics._update(1 / 60); - } - const height = probe.transform.position.y; - probe.destroy(); - return height; - }; - - const defaultRestingHeight = dropProbe(); - const material = shape.material; - material.bounciness = 1; - material.bounceCombine = PhysicsMaterialCombineMode.Maximum; - const replacement = new PhysicsMaterial(); + const context = new ParserContext(engine, ParserType.Scene, engine.sceneManager.activeScene); + const parser = new ReflectionParser(context, []); + const createMaterial = vi.spyOn(physics._pxPhysics, "createMaterial"); + let material: PhysicsMaterial; try { - expect(dropProbe()).toBeGreaterThan(defaultRestingHeight + 1); - controller.enabled = false; - controller.enabled = true; - expect(dropProbe()).toBeGreaterThan(defaultRestingHeight + 1); - shape.material = replacement; - expect(dropProbe()).toBeCloseTo(defaultRestingHeight, 4); + await parser.parseProps(shape, { material: { $type: "PhysicsMaterial", bounciness: 0.7 } }); + material = shape.material; + expect(material).toBeInstanceOf(PhysicsMaterial); + expect(material.bounciness).toBe(0.7); + expect(createMaterial).toHaveBeenCalledTimes(1); + await parser.parseProps(shape, { material: null }); + expect(shape.material).toBeNull(); + expect((shape._nativeShape as any)._pxMaterial).toBe(physics._defaultMaterial._pxMaterial); + expect(createMaterial).toHaveBeenCalledTimes(1); } finally { - entity.destroy(); - material.destroy(); - replacement.destroy(); + createMaterial.mockRestore(); + shape._destroy(); + material?.destroy(); } }); + it.each([false, true])( + "updates active controllers when instancing or resetting material (assigned: %s)", + (assigned) => { + const entity = rootEntity.createChild("materialController"); + entity.transform.setPosition(100, 100, 0); + const controller = entity.addComponent(CharacterController); + const shape = new BoxColliderShape(); + const shared = new PhysicsMaterial(); + if (assigned) shape.material = shared; + controller.addShape(shape); + const dropProbe = () => { + const probe = rootEntity.createChild("materialProbe"); + probe.transform.setPosition(100, 104, 0); + probe.addComponent(DynamicCollider).addShape(new SphereColliderShape()); + for (let i = 0; i < 90; i++) { + engine.sceneManager.activeScene.physics._update(1 / 60); + } + const height = probe.transform.position.y; + probe.destroy(); + return height; + }; + + const defaultRestingHeight = dropProbe(); + const material = shape.getInstanceMaterial(); + material.bounciness = 1; + material.bounceCombine = PhysicsMaterialCombineMode.Maximum; + const replacement = new PhysicsMaterial(); + try { + expect(dropProbe()).toBeGreaterThan(defaultRestingHeight + 1); + controller.enabled = false; + controller.enabled = true; + expect(dropProbe()).toBeGreaterThan(defaultRestingHeight + 1); + expect(shared.bounciness).toBe(0); + shape.material = replacement; + expect(dropProbe()).toBeCloseTo(defaultRestingHeight, 4); + shape.material = material; + expect(dropProbe()).toBeGreaterThan(defaultRestingHeight + 1); + shape.material = null; + expect(shape.material).toBeNull(); + expect(dropProbe()).toBeCloseTo(defaultRestingHeight, 4); + controller.enabled = false; + controller.enabled = true; + expect(dropProbe()).toBeCloseTo(defaultRestingHeight, 4); + } finally { + entity.destroy(); + material.destroy(); + replacement.destroy(); + shared.destroy(); + } + } + ); + it("bounciness", () => { const boxEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 5, 0)); const boxEntity2 = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 5, 0)); @@ -323,8 +430,8 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = true; collider2.automaticInertiaTensor = true; - collider.shapes[0].material.bounciness = 1; - collider2.shapes[0].material.bounciness = 0; + collider.shapes[0].getInstanceMaterial().bounciness = 1; + collider2.shapes[0].getInstanceMaterial().bounciness = 0; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -332,25 +439,42 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { expect(formatValue(boxEntity2.transform.position.y)).eq(0); }); - it("cloned collider shape shares its assigned material without creating a temporary one", () => { - const sourceEntity = addBox(new Vector3(1, 1, 1), StaticCollider, new Vector3()); - const sourceMaterial = sourceEntity.getComponent(StaticCollider).shapes[0].material; - const destroySpy = vi.spyOn(PhysicsMaterial.prototype, "destroy"); - const createMaterial = vi.spyOn(physics._pxPhysics, "createMaterial"); - const cloneEntity = sourceEntity.clone(); - try { - const cloneMaterial = cloneEntity.getComponent(StaticCollider).shapes[0].material; - expect(cloneMaterial).toBe(sourceMaterial); - expect(createMaterial).not.toHaveBeenCalled(); - expect(destroySpy).not.toHaveBeenCalled(); - } finally { - destroySpy.mockRestore(); - createMaterial.mockRestore(); - cloneEntity.destroy(); - sourceEntity.destroy(); - sourceMaterial.destroy(); + it.each([false, true])( + "clones share material references but reset instance ownership (instanced: %s)", + (instanced) => { + const sourceEntity = rootEntity.createChild("materialClone"); + const sourceShape = new BoxColliderShape(); + sourceEntity.addComponent(StaticCollider).addShape(sourceShape); + const sourceMaterial = instanced ? sourceShape.getInstanceMaterial() : new PhysicsMaterial(); + sourceShape.material = sourceMaterial; + sourceMaterial.bounciness = 0.7; + const destroySpy = vi.spyOn(PhysicsMaterial.prototype, "destroy"); + const createMaterial = vi.spyOn(physics._pxPhysics, "createMaterial"); + const cloneEntity = sourceEntity.clone(); + const cloneShape = cloneEntity.getComponent(StaticCollider).shapes[0]; + let cloneMaterial: PhysicsMaterial; + try { + expect(cloneShape.material).toBe(sourceMaterial); + expect(createMaterial).not.toHaveBeenCalled(); + expect(destroySpy).not.toHaveBeenCalled(); + cloneMaterial = cloneShape.getInstanceMaterial(); + expect(cloneMaterial).not.toBe(sourceMaterial); + expect(cloneMaterial.bounciness).toBe(0.7); + expect(cloneShape.getInstanceMaterial()).toBe(cloneMaterial); + expect(createMaterial).toHaveBeenCalledTimes(1); + cloneMaterial.bounciness = 0; + expect(sourceShape.material.bounciness).toBe(0.7); + if (instanced) expect(sourceShape.getInstanceMaterial()).toBe(sourceMaterial); + } finally { + destroySpy.mockRestore(); + createMaterial.mockRestore(); + cloneEntity.destroy(); + sourceEntity.destroy(); + sourceMaterial.destroy(); + cloneMaterial?.destroy(); + } } - }); + ); it("bounceCombine Average", () => { const boxEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 5, 0)); @@ -359,8 +483,8 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticCenterOfMass = true; collider.automaticInertiaTensor = true; - collider.shapes[0].material.bounciness = 1; - collider.shapes[0].material.bounceCombine = PhysicsMaterialCombineMode.Average; + collider.shapes[0].getInstanceMaterial().bounciness = 1; + collider.shapes[0].getInstanceMaterial().bounceCombine = PhysicsMaterialCombineMode.Average; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -371,9 +495,9 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const collider2 = boxEntity2.getComponent(DynamicCollider); collider2.automaticCenterOfMass = true; collider2.automaticInertiaTensor = true; - collider2.shapes[0].material.bounciness = 0.5; - ground.getComponent(StaticCollider).shapes[0].material.bounciness = 0.5; - collider2.shapes[0].material.bounceCombine = PhysicsMaterialCombineMode.Average; + collider2.shapes[0].getInstanceMaterial().bounciness = 0.5; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().bounciness = 0.5; + collider2.shapes[0].getInstanceMaterial().bounceCombine = PhysicsMaterialCombineMode.Average; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -386,9 +510,9 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const collider = boxEntity.getComponent(DynamicCollider); collider.automaticCenterOfMass = true; collider.automaticInertiaTensor = true; - collider.shapes[0].material.bounciness = 1; - collider.shapes[0].material.bounceCombine = PhysicsMaterialCombineMode.Minimum; - ground.getComponent(StaticCollider).shapes[0].material.bounciness = 0; + collider.shapes[0].getInstanceMaterial().bounciness = 1; + collider.shapes[0].getInstanceMaterial().bounceCombine = PhysicsMaterialCombineMode.Minimum; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().bounciness = 0; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -401,9 +525,9 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const collider = boxEntity.getComponent(DynamicCollider); collider.automaticCenterOfMass = true; collider.automaticInertiaTensor = true; - collider.shapes[0].material.bounciness = 0; - collider.shapes[0].material.bounceCombine = PhysicsMaterialCombineMode.Maximum; - ground.getComponent(StaticCollider).shapes[0].material.bounciness = 1; + collider.shapes[0].getInstanceMaterial().bounciness = 0; + collider.shapes[0].getInstanceMaterial().bounceCombine = PhysicsMaterialCombineMode.Maximum; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().bounciness = 1; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -416,9 +540,9 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const collider = boxEntity.getComponent(DynamicCollider); collider.automaticCenterOfMass = true; collider.automaticInertiaTensor = true; - collider.shapes[0].material.bounciness = 1; - collider.shapes[0].material.bounceCombine = PhysicsMaterialCombineMode.Multiply; - ground.getComponent(StaticCollider).shapes[0].material.bounciness = 0.5; + collider.shapes[0].getInstanceMaterial().bounciness = 1; + collider.shapes[0].getInstanceMaterial().bounceCombine = PhysicsMaterialCombineMode.Multiply; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().bounciness = 0.5; // @ts-ignore engine.sceneManager.activeScene.physics._update(2); @@ -438,8 +562,8 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.inertiaTensor.set(10000000, 10000000, 10000000); collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.dynamicFriction = 1; - collider2.shapes[0].material.dynamicFriction = 0.5; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 1; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 0.5; collider.applyForce(new Vector3(0, 0, 1000)); collider2.applyForce(new Vector3(0, 0, 1000)); @@ -462,8 +586,8 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.inertiaTensor.set(10000000, 10000000, 10000000); collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.staticFriction = 2000; - collider2.shapes[0].material.staticFriction = 100; + collider.shapes[0].getInstanceMaterial().staticFriction = 2000; + collider2.shapes[0].getInstanceMaterial().staticFriction = 100; collider.applyForce(new Vector3(0, 0, 1000)); collider2.applyForce(new Vector3(0, 0, 1000)); @@ -483,18 +607,18 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Average; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Average; - collider.shapes[0].material.staticFriction = 2000; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 0; + collider.shapes[0].getInstanceMaterial().staticFriction = 2000; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore engine.sceneManager.activeScene.physics._update(1); expect(boxEntity.transform.position.z).closeTo(0, 0.001); - collider.shapes[0].material.staticFriction = 0; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 2000; + collider.shapes[0].getInstanceMaterial().staticFriction = 0; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 2000; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -511,10 +635,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Minimum; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Minimum; - collider.shapes[0].material.staticFriction = 2000; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 0; + collider.shapes[0].getInstanceMaterial().staticFriction = 2000; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -525,10 +649,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const boxEntity2 = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 0, 0)); const collider2 = boxEntity2.getComponent(DynamicCollider); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Minimum; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Minimum; - collider2.shapes[0].material.staticFriction = 0; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 2000; + collider2.shapes[0].getInstanceMaterial().staticFriction = 0; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 2000; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -545,10 +669,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Maximum; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Maximum; - collider.shapes[0].material.staticFriction = 2000; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 0; + collider.shapes[0].getInstanceMaterial().staticFriction = 2000; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -563,10 +687,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Maximum; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Maximum; - collider2.shapes[0].material.staticFriction = 0; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 2000; + collider2.shapes[0].getInstanceMaterial().staticFriction = 0; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 2000; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -583,10 +707,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Multiply; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Multiply; - collider.shapes[0].material.staticFriction = 10; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 200; + collider.shapes[0].getInstanceMaterial().staticFriction = 10; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 200; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -597,10 +721,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { const boxEntity2 = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 0, 0)); const collider2 = boxEntity2.getComponent(DynamicCollider); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Multiply; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Multiply; - collider2.shapes[0].material.staticFriction = 100; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 20; + collider2.shapes[0].getInstanceMaterial().staticFriction = 100; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 20; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -617,10 +741,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Average; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Average; - collider.shapes[0].material.dynamicFriction = 10; - ground.getComponent(StaticCollider).shapes[0].material.staticFriction = 0; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 10; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().staticFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -635,10 +759,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Average; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Average; - collider2.shapes[0].material.dynamicFriction = 5; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 5; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 5; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 5; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -655,10 +779,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Minimum; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Minimum; - collider.shapes[0].material.dynamicFriction = 10; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 0; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 10; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -673,10 +797,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Minimum; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Minimum; - collider2.shapes[0].material.dynamicFriction = 0; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 10; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 0; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 10; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -693,10 +817,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Maximum; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Maximum; - collider.shapes[0].material.dynamicFriction = 10; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 0; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 10; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 0; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -711,10 +835,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Maximum; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Maximum; - collider2.shapes[0].material.dynamicFriction = 0; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 10; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 0; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 10; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -731,10 +855,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider.automaticInertiaTensor = false; collider.inertiaTensor.set(10000000, 10000000, 10000000); - collider.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Multiply; + collider.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Multiply; - collider.shapes[0].material.dynamicFriction = 2; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 5; + collider.shapes[0].getInstanceMaterial().dynamicFriction = 2; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 5; collider.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore @@ -749,10 +873,10 @@ describe.each(runtimeModes)("PhysicsMaterial (%s)", (runtimeMode) => { collider2.automaticInertiaTensor = false; collider2.inertiaTensor.set(10000000, 10000000, 10000000); - collider2.shapes[0].material.frictionCombine = PhysicsMaterialCombineMode.Multiply; + collider2.shapes[0].getInstanceMaterial().frictionCombine = PhysicsMaterialCombineMode.Multiply; - collider2.shapes[0].material.dynamicFriction = 10; - ground.getComponent(StaticCollider).shapes[0].material.dynamicFriction = 1; + collider2.shapes[0].getInstanceMaterial().dynamicFriction = 10; + ground.getComponent(StaticCollider).shapes[0].getInstanceMaterial().dynamicFriction = 1; collider2.applyForce(new Vector3(0, 0, 1000)); // @ts-ignore diff --git a/tests/src/core/physics/SpringJoint.test.ts b/tests/src/core/physics/SpringJoint.test.ts index 76c083c24..a2f035d33 100644 --- a/tests/src/core/physics/SpringJoint.test.ts +++ b/tests/src/core/physics/SpringJoint.test.ts @@ -13,8 +13,8 @@ describe("SpringJoint", function () { boxEntity.transform.setPosition(pos.x, pos.y, pos.z); const physicsBox = new BoxColliderShape(); - physicsBox.material.dynamicFriction = 0; - physicsBox.material.staticFriction = 0; + physicsBox.getInstanceMaterial().dynamicFriction = 0; + physicsBox.getInstanceMaterial().staticFriction = 0; physicsBox.size = cubeSize; const boxCollider = boxEntity.addComponent(type); boxCollider.addShape(physicsBox);