515 lines
10 KiB
TypeScript
515 lines
10 KiB
TypeScript
import { ShaderMaterial } from "three";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { collectWaterContactPatches, createWaterMaterial } from "../../src/rendering/water-material";
|
|
|
|
describe("water material helpers", () => {
|
|
it("builds contact foam patches for bounds that cross the water surface", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 8
|
|
}
|
|
},
|
|
[
|
|
{
|
|
min: {
|
|
x: -1,
|
|
y: 0.8,
|
|
z: -0.75
|
|
},
|
|
max: {
|
|
x: 1,
|
|
y: 1.35,
|
|
z: 0.75
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches).toHaveLength(1);
|
|
expect(patches[0]?.x).toBeCloseTo(0, 5);
|
|
expect(patches[0]?.z).toBeCloseTo(0, 5);
|
|
expect(patches[0]?.halfWidth).toBeGreaterThan(0.9);
|
|
expect(patches[0]?.halfDepth).toBeGreaterThan(0.7);
|
|
});
|
|
|
|
it("ignores bounds that do not overlap the water surface band", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 6,
|
|
y: 2,
|
|
z: 6
|
|
}
|
|
},
|
|
[
|
|
{
|
|
min: {
|
|
x: -1,
|
|
y: -3,
|
|
z: -1
|
|
},
|
|
max: {
|
|
x: 1,
|
|
y: -2,
|
|
z: 1
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches).toHaveLength(0);
|
|
});
|
|
|
|
it("preserves oriented contact regions for rotated boxes", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 10
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "orientedBox",
|
|
center: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 45,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 0.4,
|
|
z: 1
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches).toHaveLength(1);
|
|
expect(Math.abs(patches[0]?.axisX ?? 0)).toBeGreaterThan(0.65);
|
|
expect(Math.abs(patches[0]?.axisZ ?? 0)).toBeGreaterThan(0.65);
|
|
});
|
|
|
|
it("clips rotated contact regions to the water footprint", () => {
|
|
const centeredPatch = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 4,
|
|
y: 2,
|
|
z: 4
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "orientedBox",
|
|
center: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 45,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 3,
|
|
y: 0.4,
|
|
z: 1
|
|
}
|
|
}
|
|
]
|
|
)[0];
|
|
const clippedPatch = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 4,
|
|
y: 2,
|
|
z: 4
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "orientedBox",
|
|
center: {
|
|
x: 2.2,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 45,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 3,
|
|
y: 0.4,
|
|
z: 1
|
|
}
|
|
}
|
|
]
|
|
)[0];
|
|
|
|
expect(centeredPatch).toBeDefined();
|
|
expect(clippedPatch).toBeDefined();
|
|
expect(clippedPatch?.x ?? 999).toBeLessThan(2);
|
|
expect((clippedPatch?.halfWidth ?? 0) * (clippedPatch?.halfDepth ?? 0)).toBeLessThan(
|
|
(centeredPatch?.halfWidth ?? 0) * (centeredPatch?.halfDepth ?? 0)
|
|
);
|
|
expect(Math.abs(clippedPatch?.axisX ?? 0)).toBeGreaterThan(0.65);
|
|
expect(Math.abs(clippedPatch?.axisZ ?? 0)).toBeGreaterThan(0.65);
|
|
});
|
|
|
|
it("builds contact patches for transformed triangle meshes that cross the water surface", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 8,
|
|
y: 2,
|
|
z: 8
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "triangleMesh",
|
|
vertices: new Float32Array([
|
|
-1, 0, -1,
|
|
1, 0, -1,
|
|
1, 0, 1,
|
|
-1, 0, 1
|
|
]),
|
|
indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
|
|
transform: {
|
|
position: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 35,
|
|
y: 28,
|
|
z: 18
|
|
},
|
|
scale: {
|
|
x: 2,
|
|
y: 1,
|
|
z: 1.4
|
|
}
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches).toHaveLength(1);
|
|
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(0.2);
|
|
expect(Math.abs(patches[0]?.axisX ?? 0)).toBeGreaterThan(0.2);
|
|
expect(Math.abs(patches[0]?.axisZ ?? 0)).toBeGreaterThan(0.2);
|
|
});
|
|
|
|
it("merges adjacent triangle mesh strips into one longer foam band", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 10
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "triangleMesh",
|
|
mergeProfile: "aggressive",
|
|
vertices: new Float32Array([
|
|
-2, 0, -1,
|
|
0, 0, -1,
|
|
0, 0, 1,
|
|
-2, 0, 1,
|
|
2, 0, -1,
|
|
2, 0, 1
|
|
]),
|
|
indices: new Uint32Array([
|
|
0, 1, 2,
|
|
0, 2, 3,
|
|
1, 4, 5,
|
|
1, 5, 2
|
|
]),
|
|
transform: {
|
|
position: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 32,
|
|
y: 20,
|
|
z: 12
|
|
},
|
|
scale: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 1
|
|
}
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches).toHaveLength(1);
|
|
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(1.2);
|
|
expect(patches[0]?.halfDepth ?? 0).toBeGreaterThan(0.05);
|
|
});
|
|
|
|
it("only uses aggressive merging for explicitly marked triangle meshes", () => {
|
|
const sharedSource = {
|
|
kind: "triangleMesh" as const,
|
|
vertices: new Float32Array([
|
|
-3, 0, -1,
|
|
-0.4, 0, -1,
|
|
-0.4, 0, 1,
|
|
-3, 0, 1,
|
|
0.4, 0, -1,
|
|
3, 0, -1,
|
|
3, 0, 1,
|
|
0.4, 0, 1
|
|
]),
|
|
indices: new Uint32Array([
|
|
0, 1, 2,
|
|
0, 2, 3,
|
|
4, 5, 6,
|
|
4, 6, 7
|
|
]),
|
|
transform: {
|
|
position: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
scale: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 1
|
|
}
|
|
}
|
|
};
|
|
|
|
const defaultPatches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 10
|
|
}
|
|
},
|
|
[sharedSource]
|
|
);
|
|
const aggressivePatches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 10
|
|
}
|
|
},
|
|
[{
|
|
...sharedSource,
|
|
mergeProfile: "aggressive" as const
|
|
}]
|
|
);
|
|
|
|
expect(defaultPatches.length).toBeGreaterThan(1);
|
|
expect(aggressivePatches).toHaveLength(1);
|
|
});
|
|
|
|
it("does not merge sharply bent triangle mesh strips even in aggressive mode", () => {
|
|
const patches = collectWaterContactPatches(
|
|
{
|
|
center: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 10,
|
|
y: 2,
|
|
z: 10
|
|
}
|
|
},
|
|
[
|
|
{
|
|
kind: "triangleMesh",
|
|
mergeProfile: "aggressive",
|
|
vertices: new Float32Array([
|
|
-2, 0, -1,
|
|
0, 0, -1,
|
|
0, 0, 1,
|
|
-2, 0, 1,
|
|
2, 1.6, -1,
|
|
2, 1.6, 1
|
|
]),
|
|
indices: new Uint32Array([
|
|
0, 1, 2,
|
|
0, 2, 3,
|
|
1, 4, 5,
|
|
1, 5, 2
|
|
]),
|
|
transform: {
|
|
position: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
scale: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 1
|
|
}
|
|
}
|
|
}
|
|
]
|
|
);
|
|
|
|
expect(patches.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
it("builds a shared quality shader material for visible tinted water", () => {
|
|
const result = createWaterMaterial({
|
|
colorHex: "#4da6d9",
|
|
surfaceOpacity: 0.55,
|
|
waveStrength: 0.35,
|
|
opacity: 0.71,
|
|
quality: true,
|
|
wireframe: false,
|
|
isTopFace: true,
|
|
time: 0,
|
|
halfSize: {
|
|
x: 4,
|
|
z: 4
|
|
},
|
|
contactPatches: []
|
|
});
|
|
|
|
expect(result.material).toBeInstanceOf(ShaderMaterial);
|
|
|
|
const material = result.material as ShaderMaterial;
|
|
expect(material.transparent).toBe(true);
|
|
expect(material.uniforms["surfaceOpacity"]?.value).toBeGreaterThan(0.14);
|
|
expect(material.uniforms["waveStrength"]?.value).toBe(0.35);
|
|
expect(material.uniforms["isTopFace"]?.value).toBe(1);
|
|
expect(result.contactPatchesUniform?.value).toHaveLength(6);
|
|
});
|
|
}); |