Add patch merging functionality in water-material
This commit is contained in:
@@ -265,12 +265,96 @@ function createPatchFromProjectedPoints(projectedPoints, preferredAxis, minimumT
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPatchCornerPoints(patch) {
|
||||||
|
const axis = new Vector2(patch.axisX, patch.axisZ);
|
||||||
|
if (axis.lengthSq() <= WATER_CONTACT_EPSILON) {
|
||||||
|
axis.set(1, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
axis.normalize();
|
||||||
|
}
|
||||||
|
const perpendicularAxis = new Vector2(-axis.y, axis.x);
|
||||||
|
const center = new Vector2(patch.x, patch.z);
|
||||||
|
return [
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(-patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(-patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(-patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(-patch.halfDepth))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function measurePatchExtentsInBasis(points, axis) {
|
||||||
|
const perpendicularAxis = new Vector2(-axis.y, axis.x);
|
||||||
|
let minPrimary = Number.POSITIVE_INFINITY;
|
||||||
|
let maxPrimary = Number.NEGATIVE_INFINITY;
|
||||||
|
let minSecondary = Number.POSITIVE_INFINITY;
|
||||||
|
let maxSecondary = Number.NEGATIVE_INFINITY;
|
||||||
|
for (const point of points) {
|
||||||
|
const primaryDistance = point.dot(axis);
|
||||||
|
const secondaryDistance = point.dot(perpendicularAxis);
|
||||||
|
minPrimary = Math.min(minPrimary, primaryDistance);
|
||||||
|
maxPrimary = Math.max(maxPrimary, primaryDistance);
|
||||||
|
minSecondary = Math.min(minSecondary, secondaryDistance);
|
||||||
|
maxSecondary = Math.max(maxSecondary, secondaryDistance);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
minPrimary,
|
||||||
|
maxPrimary,
|
||||||
|
minSecondary,
|
||||||
|
maxSecondary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTriangleMeshContactPatches(rawPatches, minimumThickness) {
|
||||||
|
const clusters = [];
|
||||||
|
for (const patch of rawPatches) {
|
||||||
|
const patchPoints = createPatchCornerPoints(patch);
|
||||||
|
const patchAxis = new Vector2(patch.axisX, patch.axisZ);
|
||||||
|
if (patchAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
|
||||||
|
patchAxis.set(1, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
patchAxis.normalize();
|
||||||
|
}
|
||||||
|
let merged = false;
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
const alignment = Math.abs(cluster.axis.dot(patchAxis));
|
||||||
|
if (alignment < 0.92) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const patchExtents = measurePatchExtentsInBasis(patchPoints, cluster.axis);
|
||||||
|
const primaryGap = Math.max(0, Math.max(cluster.extents.minPrimary - patchExtents.maxPrimary, patchExtents.minPrimary - cluster.extents.maxPrimary));
|
||||||
|
const secondaryGap = Math.max(0, Math.max(cluster.extents.minSecondary - patchExtents.maxSecondary, patchExtents.minSecondary - cluster.extents.maxSecondary));
|
||||||
|
const allowedPrimaryGap = Math.max(0.18, Math.max(patch.halfWidth, (cluster.extents.maxPrimary - cluster.extents.minPrimary) * 0.12));
|
||||||
|
const allowedSecondaryGap = Math.max(minimumThickness * 1.5, Math.max(patch.halfDepth, (cluster.extents.maxSecondary - cluster.extents.minSecondary) * 0.35));
|
||||||
|
if (primaryGap > allowedPrimaryGap || secondaryGap > allowedSecondaryGap) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cluster.points.push(...patchPoints.map((point) => point.clone()));
|
||||||
|
cluster.extents = measurePatchExtentsInBasis(cluster.points, cluster.axis);
|
||||||
|
merged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!merged) {
|
||||||
|
clusters.push({
|
||||||
|
axis: patchAxis,
|
||||||
|
points: patchPoints.map((point) => point.clone()),
|
||||||
|
extents: measurePatchExtentsInBasis(patchPoints, patchAxis)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusters
|
||||||
|
.map((cluster) => createPatchFromProjectedPoints(cluster.points, cluster.axis, minimumThickness))
|
||||||
|
.filter((patch) => patch !== null);
|
||||||
|
}
|
||||||
|
|
||||||
function appendTriangleMeshContactPatches(patches, source, volume, inverseRotation, halfX, surfaceY, surfaceBand, halfZ) {
|
function appendTriangleMeshContactPatches(patches, source, volume, inverseRotation, halfX, surfaceY, surfaceBand, halfZ) {
|
||||||
const position = new Vector3(source.transform?.position.x ?? 0, source.transform?.position.y ?? 0, source.transform?.position.z ?? 0);
|
const position = new Vector3(source.transform?.position.x ?? 0, source.transform?.position.y ?? 0, source.transform?.position.z ?? 0);
|
||||||
const rotation = source.transform !== undefined ? createRotationQuaternion(source.transform.rotationDegrees) : null;
|
const rotation = source.transform !== undefined ? createRotationQuaternion(source.transform.rotationDegrees) : null;
|
||||||
const scale = new Vector3(source.transform?.scale.x ?? 1, source.transform?.scale.y ?? 1, source.transform?.scale.z ?? 1);
|
const scale = new Vector3(source.transform?.scale.x ?? 1, source.transform?.scale.y ?? 1, source.transform?.scale.z ?? 1);
|
||||||
const bandMinimumThickness = Math.max(0.08, Math.min(0.22, surfaceBand * 0.45));
|
const bandMinimumThickness = Math.max(0.08, Math.min(0.22, surfaceBand * 0.45));
|
||||||
const triangleVertices = [new Vector3(), new Vector3(), new Vector3()];
|
const triangleVertices = [new Vector3(), new Vector3(), new Vector3()];
|
||||||
|
const rawPatches = [];
|
||||||
for (let indexOffset = 0; indexOffset <= source.indices.length - 3; indexOffset += 3) {
|
for (let indexOffset = 0; indexOffset <= source.indices.length - 3; indexOffset += 3) {
|
||||||
const polygon = [];
|
const polygon = [];
|
||||||
for (let cornerIndex = 0; cornerIndex < 3; cornerIndex += 1) {
|
for (let cornerIndex = 0; cornerIndex < 3; cornerIndex += 1) {
|
||||||
@@ -294,9 +378,10 @@ function appendTriangleMeshContactPatches(patches, source, volume, inverseRotati
|
|||||||
}
|
}
|
||||||
const patch = createPatchFromProjectedPoints(clippedPolygon.map((point) => new Vector2(point.x, point.z)), null, bandMinimumThickness);
|
const patch = createPatchFromProjectedPoints(clippedPolygon.map((point) => new Vector2(point.x, point.z)), null, bandMinimumThickness);
|
||||||
if (patch !== null) {
|
if (patch !== null) {
|
||||||
patches.push(patch);
|
rawPatches.push(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
patches.push(...mergeTriangleMeshContactPatches(rawPatches, bandMinimumThickness));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectWaterContactPatches(volume, contactBounds) {
|
export function collectWaterContactPatches(volume, contactBounds) {
|
||||||
|
|||||||
@@ -392,6 +392,102 @@ function createPatchFromProjectedPoints(projectedPoints: Vector2[], preferredAxi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPatchCornerPoints(patch: WaterContactPatch) {
|
||||||
|
const axis = new Vector2(patch.axisX, patch.axisZ);
|
||||||
|
if (axis.lengthSq() <= WATER_CONTACT_EPSILON) {
|
||||||
|
axis.set(1, 0);
|
||||||
|
} else {
|
||||||
|
axis.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const perpendicularAxis = new Vector2(-axis.y, axis.x);
|
||||||
|
const center = new Vector2(patch.x, patch.z);
|
||||||
|
return [
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(-patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(-patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(patch.halfDepth)),
|
||||||
|
center.clone().add(axis.clone().multiplyScalar(-patch.halfWidth)).add(perpendicularAxis.clone().multiplyScalar(-patch.halfDepth))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function measurePatchExtentsInBasis(points: Vector2[], axis: Vector2) {
|
||||||
|
const perpendicularAxis = new Vector2(-axis.y, axis.x);
|
||||||
|
let minPrimary = Number.POSITIVE_INFINITY;
|
||||||
|
let maxPrimary = Number.NEGATIVE_INFINITY;
|
||||||
|
let minSecondary = Number.POSITIVE_INFINITY;
|
||||||
|
let maxSecondary = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
for (const point of points) {
|
||||||
|
const primaryDistance = point.dot(axis);
|
||||||
|
const secondaryDistance = point.dot(perpendicularAxis);
|
||||||
|
minPrimary = Math.min(minPrimary, primaryDistance);
|
||||||
|
maxPrimary = Math.max(maxPrimary, primaryDistance);
|
||||||
|
minSecondary = Math.min(minSecondary, secondaryDistance);
|
||||||
|
maxSecondary = Math.max(maxSecondary, secondaryDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minPrimary,
|
||||||
|
maxPrimary,
|
||||||
|
minSecondary,
|
||||||
|
maxSecondary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeTriangleMeshContactPatches(rawPatches: WaterContactPatch[], minimumThickness: number) {
|
||||||
|
const clusters: Array<{
|
||||||
|
axis: Vector2;
|
||||||
|
points: Vector2[];
|
||||||
|
extents: ReturnType<typeof measurePatchExtentsInBasis>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const patch of rawPatches) {
|
||||||
|
const patchPoints = createPatchCornerPoints(patch);
|
||||||
|
const patchAxis = new Vector2(patch.axisX, patch.axisZ);
|
||||||
|
if (patchAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
|
||||||
|
patchAxis.set(1, 0);
|
||||||
|
} else {
|
||||||
|
patchAxis.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged = false;
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
const alignment = Math.abs(cluster.axis.dot(patchAxis));
|
||||||
|
if (alignment < 0.92) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchExtents = measurePatchExtentsInBasis(patchPoints, cluster.axis);
|
||||||
|
const primaryGap = Math.max(0, Math.max(cluster.extents.minPrimary - patchExtents.maxPrimary, patchExtents.minPrimary - cluster.extents.maxPrimary));
|
||||||
|
const secondaryGap = Math.max(0, Math.max(cluster.extents.minSecondary - patchExtents.maxSecondary, patchExtents.minSecondary - cluster.extents.maxSecondary));
|
||||||
|
const allowedPrimaryGap = Math.max(0.18, Math.max(patch.halfWidth, (cluster.extents.maxPrimary - cluster.extents.minPrimary) * 0.12));
|
||||||
|
const allowedSecondaryGap = Math.max(minimumThickness * 1.5, Math.max(patch.halfDepth, (cluster.extents.maxSecondary - cluster.extents.minSecondary) * 0.35));
|
||||||
|
|
||||||
|
if (primaryGap > allowedPrimaryGap || secondaryGap > allowedSecondaryGap) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster.points.push(...patchPoints.map((point) => point.clone()));
|
||||||
|
cluster.extents = measurePatchExtentsInBasis(cluster.points, cluster.axis);
|
||||||
|
merged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!merged) {
|
||||||
|
clusters.push({
|
||||||
|
axis: patchAxis,
|
||||||
|
points: patchPoints.map((point) => point.clone()),
|
||||||
|
extents: measurePatchExtentsInBasis(patchPoints, patchAxis)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters
|
||||||
|
.map((cluster) => createPatchFromProjectedPoints(cluster.points, cluster.axis, minimumThickness))
|
||||||
|
.filter((patch): patch is WaterContactPatch => patch !== null);
|
||||||
|
}
|
||||||
|
|
||||||
function appendTriangleMeshContactPatches(
|
function appendTriangleMeshContactPatches(
|
||||||
patches: WaterContactPatch[],
|
patches: WaterContactPatch[],
|
||||||
source: WaterContactTriangleMesh,
|
source: WaterContactTriangleMesh,
|
||||||
@@ -415,6 +511,7 @@ function appendTriangleMeshContactPatches(
|
|||||||
);
|
);
|
||||||
const bandMinimumThickness = Math.max(0.08, Math.min(0.22, surfaceBand * 0.45));
|
const bandMinimumThickness = Math.max(0.08, Math.min(0.22, surfaceBand * 0.45));
|
||||||
const triangleVertices = [new Vector3(), new Vector3(), new Vector3()];
|
const triangleVertices = [new Vector3(), new Vector3(), new Vector3()];
|
||||||
|
const rawPatches: WaterContactPatch[] = [];
|
||||||
|
|
||||||
for (let indexOffset = 0; indexOffset <= source.indices.length - 3; indexOffset += 3) {
|
for (let indexOffset = 0; indexOffset <= source.indices.length - 3; indexOffset += 3) {
|
||||||
const polygon: Vector3[] = [];
|
const polygon: Vector3[] = [];
|
||||||
@@ -448,9 +545,11 @@ function appendTriangleMeshContactPatches(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (patch !== null) {
|
if (patch !== null) {
|
||||||
patches.push(patch);
|
rawPatches.push(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patches.push(...mergeTriangleMeshContactPatches(rawPatches, bandMinimumThickness));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] {
|
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] {
|
||||||
|
|||||||
@@ -270,12 +270,74 @@ describe("water material helpers", () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(patches.length).toBeGreaterThan(0);
|
expect(patches).toHaveLength(1);
|
||||||
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(0.2);
|
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(0.2);
|
||||||
expect(Math.abs(patches[0]?.axisX ?? 0)).toBeGreaterThan(0.2);
|
expect(Math.abs(patches[0]?.axisX ?? 0)).toBeGreaterThan(0.2);
|
||||||
expect(Math.abs(patches[0]?.axisZ ?? 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",
|
||||||
|
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("builds a shared quality shader material for visible tinted water", () => {
|
it("builds a shared quality shader material for visible tinted water", () => {
|
||||||
const result = createWaterMaterial({
|
const result = createWaterMaterial({
|
||||||
colorHex: "#4da6d9",
|
colorHex: "#4da6d9",
|
||||||
|
|||||||
Reference in New Issue
Block a user