185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, PlaneGeometry } from "three";
|
|
|
|
import { createModelInstance } from "../../src/assets/model-instances";
|
|
import { buildGeneratedModelCollider } from "../../src/geometry/model-instance-collider-generation";
|
|
import {
|
|
createFixtureLoadedModelAsset,
|
|
createFixtureLoadedModelAssetFromGeometry,
|
|
createFixtureModelAssetRecord
|
|
} from "../helpers/model-collider-fixtures";
|
|
|
|
describe("buildGeneratedModelCollider", () => {
|
|
it("builds a simple oriented box collider from asset bounds", () => {
|
|
const { asset } = createFixtureLoadedModelAssetFromGeometry("asset-model-simple", new BoxGeometry(2, 4, 6));
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-simple",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "simple",
|
|
visible: true
|
|
}
|
|
});
|
|
|
|
const collider = buildGeneratedModelCollider(modelInstance, asset);
|
|
|
|
expect(collider).not.toBeNull();
|
|
expect(collider).toMatchObject({
|
|
kind: "box",
|
|
mode: "simple",
|
|
visible: true,
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 4,
|
|
z: 6
|
|
}
|
|
});
|
|
});
|
|
|
|
it("builds a static triangle-mesh collider from loaded model geometry", () => {
|
|
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-model-static", new BoxGeometry(2, 1, 3));
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-static",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "static",
|
|
visible: false
|
|
}
|
|
});
|
|
|
|
const collider = buildGeneratedModelCollider(modelInstance, asset, loadedAsset);
|
|
|
|
expect(collider).not.toBeNull();
|
|
expect(collider?.kind).toBe("trimesh");
|
|
|
|
if (collider === null || collider.kind !== "trimesh") {
|
|
throw new Error("Expected a trimesh collider.");
|
|
}
|
|
|
|
expect(collider.mode).toBe("static");
|
|
expect(Array.from(collider.vertices)).toSatisfy((values: number[]) => values.every(Number.isFinite));
|
|
expect(Array.from(collider.indices)).toSatisfy((values: number[]) => values.every(Number.isInteger));
|
|
});
|
|
|
|
it("builds a static-simple compound collider from thin open mesh surfaces", () => {
|
|
const geometry = new PlaneGeometry(4, 3, 4, 3);
|
|
geometry.rotateY(Math.PI * 0.5);
|
|
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-model-static-simple", geometry);
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-static-simple",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "static-simple",
|
|
visible: true
|
|
}
|
|
});
|
|
|
|
const collider = buildGeneratedModelCollider(modelInstance, asset, loadedAsset);
|
|
|
|
expect(collider).not.toBeNull();
|
|
expect(collider).toMatchObject({
|
|
kind: "compound",
|
|
mode: "static-simple",
|
|
decomposition: "surface-voxel-boxes",
|
|
runtimeBehavior: "fixedQueryOnly"
|
|
});
|
|
|
|
if (collider === null || collider.kind !== "compound") {
|
|
throw new Error("Expected a compound collider.");
|
|
}
|
|
|
|
expect(collider.pieces.length).toBeGreaterThanOrEqual(1);
|
|
expect(collider.pieces.every((piece) => piece.kind === "box")).toBe(true);
|
|
|
|
if (collider.pieces[0]?.kind !== "box") {
|
|
throw new Error("Expected the first static-simple collider piece to be a box.");
|
|
}
|
|
|
|
expect(collider.pieces[0].size.x).toBeGreaterThan(0.01);
|
|
});
|
|
|
|
it("builds a terrain heightfield from a regular-grid mesh", () => {
|
|
const geometry = new PlaneGeometry(4, 4, 2, 2);
|
|
geometry.rotateX(-Math.PI * 0.5);
|
|
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-model-terrain", geometry);
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-terrain",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "terrain",
|
|
visible: true
|
|
}
|
|
});
|
|
|
|
const collider = buildGeneratedModelCollider(modelInstance, asset, loadedAsset);
|
|
|
|
expect(collider).not.toBeNull();
|
|
expect(collider?.kind).toBe("heightfield");
|
|
|
|
if (collider === null || collider.kind !== "heightfield") {
|
|
throw new Error("Expected a heightfield collider.");
|
|
}
|
|
|
|
expect(collider).toMatchObject({
|
|
mode: "terrain",
|
|
rows: 3,
|
|
cols: 3
|
|
});
|
|
expect(Array.from(collider.heights)).toSatisfy((values: number[]) => values.every(Number.isFinite));
|
|
});
|
|
|
|
it("fails terrain mode for meshes that are not a clean regular-grid terrain surface", () => {
|
|
const { asset, loadedAsset } = createFixtureLoadedModelAssetFromGeometry("asset-model-invalid-terrain", new BoxGeometry(2, 2, 2));
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-invalid-terrain",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "terrain",
|
|
visible: false
|
|
}
|
|
});
|
|
|
|
expect(() => buildGeneratedModelCollider(modelInstance, asset, loadedAsset)).toThrow("cannot use terrain collision");
|
|
});
|
|
|
|
it("builds explicit convex compound pieces for dynamic mode", () => {
|
|
const template = new Group();
|
|
const material = new MeshBasicMaterial();
|
|
const leftBox = new Mesh(new BoxGeometry(1, 1, 1), material);
|
|
const rightBox = new Mesh(new BoxGeometry(1, 2, 1), material);
|
|
|
|
leftBox.position.set(-1.25, 0.5, 0);
|
|
rightBox.position.set(1.25, 1, 0);
|
|
template.add(leftBox);
|
|
template.add(rightBox);
|
|
template.updateMatrixWorld(true);
|
|
|
|
const asset = createFixtureModelAssetRecord("asset-model-dynamic", template);
|
|
const loadedAsset = createFixtureLoadedModelAsset(asset, template);
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-dynamic",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "dynamic",
|
|
visible: true
|
|
}
|
|
});
|
|
|
|
const collider = buildGeneratedModelCollider(modelInstance, asset, loadedAsset);
|
|
|
|
expect(collider).not.toBeNull();
|
|
expect(collider).toMatchObject({
|
|
kind: "compound",
|
|
mode: "dynamic",
|
|
decomposition: "spatial-bisect",
|
|
runtimeBehavior: "fixedQueryOnly"
|
|
});
|
|
expect(collider?.kind === "compound" ? collider.pieces.length : 0).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|