Add waterline segment creation and update contact patch logic

This commit is contained in:
2026-04-07 06:00:55 +02:00
parent 0cb68e3737
commit 3c54ba00c9
3 changed files with 215 additions and 5 deletions

View File

@@ -275,6 +275,70 @@ function computeTriangleNormal(pointA, pointB, pointC) {
return normal.normalize();
}
function addUniqueProjectedPoint(points, point) {
const alreadyExists = points.some((candidate) => Math.abs(candidate.x - point.x) <= WATER_CONTACT_EPSILON && Math.abs(candidate.y - point.y) <= WATER_CONTACT_EPSILON);
if (!alreadyExists) {
points.push(point);
}
}
function createWaterlineSegmentFromPolygon(polygon, surfaceY) {
if (polygon.length < 2) {
return null;
}
const intersectionPoints = [];
let previousPoint = polygon[polygon.length - 1] ?? null;
if (previousPoint === null) {
return null;
}
for (const point of polygon) {
const previousDelta = previousPoint.y - surfaceY;
const delta = point.y - surfaceY;
const previousOnPlane = Math.abs(previousDelta) <= WATER_CONTACT_EPSILON;
const onPlane = Math.abs(delta) <= WATER_CONTACT_EPSILON;
if (previousOnPlane && onPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(previousPoint.x, previousPoint.z));
addUniqueProjectedPoint(intersectionPoints, new Vector2(point.x, point.z));
}
else if (previousOnPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(previousPoint.x, previousPoint.z));
}
else if (onPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(point.x, point.z));
}
else if ((previousDelta < 0 && delta > 0) || (previousDelta > 0 && delta < 0)) {
const interpolation = (surfaceY - previousPoint.y) / (point.y - previousPoint.y);
addUniqueProjectedPoint(intersectionPoints, new Vector2(previousPoint.x + (point.x - previousPoint.x) * interpolation, previousPoint.z + (point.z - previousPoint.z) * interpolation));
}
previousPoint = point;
}
if (intersectionPoints.length < 2) {
return null;
}
let startPoint = intersectionPoints[0] ?? null;
let endPoint = intersectionPoints[1] ?? null;
let longestDistanceSquared = -1;
for (let startIndex = 0; startIndex < intersectionPoints.length; startIndex += 1) {
for (let endIndex = startIndex + 1; endIndex < intersectionPoints.length; endIndex += 1) {
const candidateStart = intersectionPoints[startIndex];
const candidateEnd = intersectionPoints[endIndex];
if (candidateStart === undefined || candidateEnd === undefined) {
continue;
}
const distanceSquared = candidateStart.distanceToSquared(candidateEnd);
if (distanceSquared > longestDistanceSquared) {
longestDistanceSquared = distanceSquared;
startPoint = candidateStart;
endPoint = candidateEnd;
}
}
}
if (startPoint === null || endPoint === null || longestDistanceSquared <= WATER_CONTACT_EPSILON) {
return null;
}
return [startPoint.clone(), endPoint.clone()];
}
function createPatchCornerPoints(patch) {
const axis = new Vector2(patch.axisX, patch.axisZ);
if (axis.lengthSq() <= WATER_CONTACT_EPSILON) {
@@ -416,10 +480,15 @@ function appendTriangleMeshContactPatches(patches, source, volume, inverseRotati
continue;
}
const clippedPolygon = clipPolygonToContactVolume(polygon, halfX, surfaceY - surfaceBand, surfaceY + surfaceBand, halfZ);
if (clippedPolygon.length < 2) {
const waterlineSegment = createWaterlineSegmentFromPolygon(clippedPolygon, surfaceY);
if (waterlineSegment === null) {
continue;
}
const patch = createPatchFromProjectedPoints(clippedPolygon.map((point) => new Vector2(point.x, point.z)), null, bandMinimumThickness);
const preferredAxis = waterlineSegment[1].clone().sub(waterlineSegment[0]);
if (preferredAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
continue;
}
const patch = createPatchFromProjectedPoints([waterlineSegment[0], waterlineSegment[1]], preferredAxis, bandMinimumThickness);
if (patch !== null) {
rawPatches.push({
patch,

View File

@@ -410,6 +410,84 @@ function computeTriangleNormal(pointA: Vector3, pointB: Vector3, pointC: Vector3
return normal.normalize();
}
function addUniqueProjectedPoint(points: Vector2[], point: Vector2) {
const alreadyExists = points.some(
(candidate) => Math.abs(candidate.x - point.x) <= WATER_CONTACT_EPSILON && Math.abs(candidate.y - point.y) <= WATER_CONTACT_EPSILON
);
if (!alreadyExists) {
points.push(point);
}
}
function createWaterlineSegmentFromPolygon(polygon: Vector3[], surfaceY: number) {
if (polygon.length < 2) {
return null;
}
const intersectionPoints: Vector2[] = [];
let previousPoint = polygon[polygon.length - 1] ?? null;
if (previousPoint === null) {
return null;
}
for (const point of polygon) {
const previousDelta = previousPoint.y - surfaceY;
const delta = point.y - surfaceY;
const previousOnPlane = Math.abs(previousDelta) <= WATER_CONTACT_EPSILON;
const onPlane = Math.abs(delta) <= WATER_CONTACT_EPSILON;
if (previousOnPlane && onPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(previousPoint.x, previousPoint.z));
addUniqueProjectedPoint(intersectionPoints, new Vector2(point.x, point.z));
} else if (previousOnPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(previousPoint.x, previousPoint.z));
} else if (onPlane) {
addUniqueProjectedPoint(intersectionPoints, new Vector2(point.x, point.z));
} else if ((previousDelta < 0 && delta > 0) || (previousDelta > 0 && delta < 0)) {
const interpolation = (surfaceY - previousPoint.y) / (point.y - previousPoint.y);
addUniqueProjectedPoint(
intersectionPoints,
new Vector2(previousPoint.x + (point.x - previousPoint.x) * interpolation, previousPoint.z + (point.z - previousPoint.z) * interpolation)
);
}
previousPoint = point;
}
if (intersectionPoints.length < 2) {
return null;
}
let startPoint = intersectionPoints[0] ?? null;
let endPoint = intersectionPoints[1] ?? null;
let longestDistanceSquared = -1;
for (let startIndex = 0; startIndex < intersectionPoints.length; startIndex += 1) {
for (let endIndex = startIndex + 1; endIndex < intersectionPoints.length; endIndex += 1) {
const candidateStart = intersectionPoints[startIndex];
const candidateEnd = intersectionPoints[endIndex];
if (candidateStart === undefined || candidateEnd === undefined) {
continue;
}
const distanceSquared = candidateStart.distanceToSquared(candidateEnd);
if (distanceSquared > longestDistanceSquared) {
longestDistanceSquared = distanceSquared;
startPoint = candidateStart;
endPoint = candidateEnd;
}
}
}
if (startPoint === null || endPoint === null || longestDistanceSquared <= WATER_CONTACT_EPSILON) {
return null;
}
return [startPoint.clone(), endPoint.clone()] as const;
}
function createPatchCornerPoints(patch: WaterContactPatch) {
const axis = new Vector2(patch.axisX, patch.axisZ);
if (axis.lengthSq() <= WATER_CONTACT_EPSILON) {
@@ -594,14 +672,20 @@ function appendTriangleMeshContactPatches(
}
const clippedPolygon = clipPolygonToContactVolume(polygon, halfX, surfaceY - surfaceBand, surfaceY + surfaceBand, halfZ);
const waterlineSegment = createWaterlineSegmentFromPolygon(clippedPolygon, surfaceY);
if (clippedPolygon.length < 2) {
if (waterlineSegment === null) {
continue;
}
const preferredAxis = waterlineSegment[1].clone().sub(waterlineSegment[0]);
if (preferredAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
continue;
}
const patch = createPatchFromProjectedPoints(
clippedPolygon.map((point) => new Vector2(point.x, point.z)),
null,
[waterlineSegment[0], waterlineSegment[1]],
preferredAxis,
bandMinimumThickness
);

View File

@@ -274,6 +274,63 @@ describe("water material helpers", () => {
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);
expect(patches[0]?.halfDepth ?? 1).toBeLessThan(0.3);
});
it("uses narrow waterline bands for large sloped triangle surfaces", () => {
const patches = collectWaterContactPatches(
{
center: {
x: 0,
y: 0,
z: 0
},
rotationDegrees: {
x: 0,
y: 0,
z: 0
},
size: {
x: 12,
y: 2,
z: 12
}
},
[
{
kind: "triangleMesh",
mergeProfile: "aggressive",
vertices: new Float32Array([
-4, -1, -3,
4, 1.4, -3,
4, 1.4, 3,
-4, -1, 3
]),
indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
transform: {
position: {
x: 0,
y: 1,
z: 0
},
rotationDegrees: {
x: 0,
y: 0,
z: 0
},
scale: {
x: 1,
y: 1,
z: 1
}
}
}
]
);
expect(patches).toHaveLength(1);
expect(patches[0]?.halfWidth ?? 0).toBeGreaterThan(1.5);
expect(patches[0]?.halfDepth ?? 1).toBeLessThan(0.3);
});
it("merges adjacent triangle mesh strips into one longer foam band", () => {