fix(physics): align material instancing with renderer semantics

This commit is contained in:
chenmo.gl
2026-09-29 13:21:20 +08:00
parent fac4518721
commit 43a26389f9
19 changed files with 350 additions and 278 deletions

View File

@@ -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 |
| ---- | ---- | ------ |

View File

@@ -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 {
- 合理设置局部偏移,避免碰撞形状过度重叠
- 适当使用触发器代替实际的物理碰撞
- 及时销毁不再使用的碰撞形状及物理材质

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -23,7 +23,9 @@ export abstract class ColliderShape extends DataObject implements ICloneHook<Col
@ignoreClone
protected _id: number;
@assignmentClone
protected _material: PhysicsMaterial = null;
protected _material: PhysicsMaterial | null = null;
@ignoreClone
private _materialInstanced: boolean = false;
private _isTrigger: boolean = false;
private _rotation: Vector3 = new Vector3();
private _position: Vector3 = new Vector3();
@@ -70,25 +72,20 @@ export abstract class ColliderShape extends DataObject implements ICloneHook<Col
}
/**
* Physical material, material can't be null.
* @remarks Reading this property creates a material for this shape if none has been assigned.
* Explicitly assigned materials and materials already created before cloning are shared by reference.
* The current physics material, or null to use the shared default material.
* @remarks Reading this property does not create or clone a material. Materials are shared by reference when cloning shapes.
* Use {@link getInstanceMaterial} to obtain an instance for this shape.
* Destroy the material when it is no longer used by any shape.
*/
get material(): PhysicsMaterial {
if (!this._material) {
this.material = new PhysicsMaterial();
}
get material(): PhysicsMaterial | null {
return this._material;
}
set material(value: PhysicsMaterial) {
if (!value) {
throw new Error("The physics material of the shape can't be null.");
}
set material(value: PhysicsMaterial | null) {
if (this._material !== value) {
this._materialInstanced = false;
this._material = value;
this._nativeShape?.setMaterial(value._nativeMaterial);
this._nativeShape?.setMaterial(value?._nativeMaterial ?? null);
}
}
@@ -146,6 +143,20 @@ export abstract class ColliderShape extends DataObject implements ICloneHook<Col
Engine._physicalObjectsMap[this._id] = this;
}
/**
* Get the instance material for this shape.
* @remarks The first call after assigning a material clones it and assigns the instance to this shape.
* If no material is assigned, creates one with the engine's default properties. Subsequent calls return the same instance.
* @returns The instance material
*/
getInstanceMaterial(): PhysicsMaterial {
if (!this._materialInstanced) {
this.material = this._material ? this._material.clone() : new PhysicsMaterial();
this._materialInstanced = true;
}
return this._material;
}
/**
* Get the distance and the closest point on the shape from a point.
* @param point - Location in world space you want to find the closest point to

View File

@@ -20,7 +20,7 @@ export class ReflectionParser {
const promises: Promise<any>[] = [];
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<any> {
private _resolveValue(value: unknown, originValue?: any): Promise<any> {
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<string, unknown>);
const promises: Promise<any>[] = [];
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);
}

View File

@@ -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();
}
});

View File

@@ -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);

View File

@@ -96,7 +96,6 @@ describe("ColliderShape PhysX", () => {
dynamicCollider.removeShape(shape);
shape._destroy();
shape.material.destroy();
});
it("CapsuleColliderShape", () => {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
});
});

View File

@@ -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

View File

@@ -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);