Add functions for clipping polygons and creating patches from projected points

This commit is contained in:
2026-04-07 05:44:28 +02:00
parent a74a7c02f0
commit b289a8d72c

View File

@@ -35,10 +35,12 @@ function createOrientedBoxCorners(box) {
].map((corner) => corner.applyQuaternion(rotation).add(new Vector3(box.center.x, box.center.y, box.center.z)));
}
function createRotationQuaternion(rotationDegrees) {
return new Quaternion().setFromEuler(new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ"));
}
function createInverseVolumeRotation(rotationDegrees) {
return new Quaternion()
.setFromEuler(new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ"))
.invert();
return createRotationQuaternion(rotationDegrees).invert();
}
function cross2d(origin, pointA, pointB) {
@@ -147,6 +149,45 @@ function clipPolygonToRectangle(polygon, minX, maxX, minZ, maxZ) {
return clippedPolygon;
}
function clipPolygonAgainstPlane3d(polygon, signedDistance) {
if (polygon.length === 0) {
return [];
}
const clipped = [];
let previousPoint = polygon[polygon.length - 1] ?? null;
if (previousPoint === null) {
return [];
}
let previousDistance = signedDistance(previousPoint);
let previousInside = previousDistance >= -WATER_CONTACT_EPSILON;
for (const point of polygon) {
const distance = signedDistance(point);
const inside = distance >= -WATER_CONTACT_EPSILON;
if (inside !== previousInside) {
const interpolation = previousDistance / (previousDistance - distance);
clipped.push(previousPoint.clone().lerp(point, interpolation));
}
if (inside) {
clipped.push(point.clone());
}
previousPoint = point;
previousDistance = distance;
previousInside = inside;
}
return clipped;
}
function clipPolygonToContactVolume(polygon, halfX, minY, maxY, halfZ) {
let clippedPolygon = polygon;
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => point.x + halfX);
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => halfX - point.x);
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => point.y - minY);
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => maxY - point.y);
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => point.z + halfZ);
clippedPolygon = clipPolygonAgainstPlane3d(clippedPolygon, (point) => halfZ - point.z);
return clippedPolygon;
}
function calculatePolygonArea(polygon) {
if (polygon.length < 3) {
return 0;
@@ -163,6 +204,101 @@ function calculatePolygonArea(polygon) {
return Math.abs(doubledArea) * 0.5;
}
function createPatchFromProjectedPoints(projectedPoints, preferredAxis, minimumThickness) {
const hull = buildConvexHull(projectedPoints);
if (hull.length === 0) {
return null;
}
const primaryAxis = preferredAxis !== null && preferredAxis.lengthSq() > WATER_CONTACT_EPSILON ? preferredAxis.clone().normalize() : new Vector2(1, 0);
if (preferredAxis === null || preferredAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
let longestSegmentLength = 0;
for (let index = 0; index < hull.length; index += 1) {
const startPoint = hull[index];
const endPoint = hull[(index + 1) % hull.length];
if (startPoint === undefined || endPoint === undefined) {
continue;
}
const segment = endPoint.clone().sub(startPoint);
const segmentLength = segment.lengthSq();
if (segmentLength > longestSegmentLength) {
longestSegmentLength = segmentLength;
primaryAxis.copy(segment.normalize());
}
}
}
if (primaryAxis.lengthSq() <= WATER_CONTACT_EPSILON) {
return null;
}
const secondaryAxis = new Vector2(-primaryAxis.y, primaryAxis.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 hull) {
const primaryDistance = point.dot(primaryAxis);
const secondaryDistance = point.dot(secondaryAxis);
minPrimary = Math.min(minPrimary, primaryDistance);
maxPrimary = Math.max(maxPrimary, primaryDistance);
minSecondary = Math.min(minSecondary, secondaryDistance);
maxSecondary = Math.max(maxSecondary, secondaryDistance);
}
const halfWidth = (maxPrimary - minPrimary) * 0.5;
let halfDepth = (maxSecondary - minSecondary) * 0.5;
if (halfWidth <= WATER_CONTACT_EPSILON) {
return null;
}
if (halfDepth <= WATER_CONTACT_EPSILON || calculatePolygonArea(hull) <= WATER_CONTACT_EPSILON) {
halfDepth = Math.max(halfDepth, minimumThickness);
}
if (halfDepth <= WATER_CONTACT_EPSILON) {
return null;
}
const patchCenterPrimary = (minPrimary + maxPrimary) * 0.5;
const patchCenterSecondary = (minSecondary + maxSecondary) * 0.5;
return {
x: primaryAxis.x * patchCenterPrimary + secondaryAxis.x * patchCenterSecondary,
z: primaryAxis.y * patchCenterPrimary + secondaryAxis.y * patchCenterSecondary,
halfWidth,
halfDepth,
axisX: primaryAxis.x,
axisZ: primaryAxis.y
};
}
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 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 bandMinimumThickness = Math.max(0.08, Math.min(0.22, surfaceBand * 0.45));
const triangleVertices = [new Vector3(), new Vector3(), new Vector3()];
for (let indexOffset = 0; indexOffset <= source.indices.length - 3; indexOffset += 3) {
const polygon = [];
for (let cornerIndex = 0; cornerIndex < 3; cornerIndex += 1) {
const vertexIndex = source.indices[indexOffset + cornerIndex] ?? 0;
const vertex = triangleVertices[cornerIndex] ?? new Vector3();
vertex.set(source.vertices[vertexIndex * 3] ?? 0, source.vertices[vertexIndex * 3 + 1] ?? 0, source.vertices[vertexIndex * 3 + 2] ?? 0);
vertex.multiply(scale);
if (rotation !== null) {
vertex.applyQuaternion(rotation);
}
vertex.add(position);
vertex.x -= volume.center.x;
vertex.y -= volume.center.y;
vertex.z -= volume.center.z;
vertex.applyQuaternion(inverseRotation);
polygon.push(vertex.clone());
}
const clippedPolygon = clipPolygonToContactVolume(polygon, halfX, surfaceY - surfaceBand, surfaceY + surfaceBand, halfZ);
if (clippedPolygon.length < 2) {
continue;
}
const patch = createPatchFromProjectedPoints(clippedPolygon.map((point) => new Vector2(point.x, point.z)), null, bandMinimumThickness);
if (patch !== null) {
patches.push(patch);
}
}
}
export function collectWaterContactPatches(volume, contactBounds) {
const inverseRotation = createInverseVolumeRotation(volume.rotationDegrees);
const halfX = Math.max(volume.size.x * 0.5, WATER_CONTACT_EPSILON);
@@ -174,6 +310,10 @@ export function collectWaterContactPatches(volume, contactBounds) {
const patches = [];
for (const source of contactBounds) {
if ("kind" in source && source.kind === "triangleMesh") {
appendTriangleMeshContactPatches(patches, source, volume, inverseRotation, halfX, surfaceY, surfaceBand, halfZ);
continue;
}
const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
const localCorners = [];
let minX = Number.POSITIVE_INFINITY;
@@ -218,62 +358,22 @@ export function collectWaterContactPatches(volume, contactBounds) {
continue;
}
let axisX = 1;
let axisZ = 0;
const primaryAxis = new Vector2(1, 0);
const secondaryAxis = new Vector2(0, 1);
let preferredAxis = null;
if ("kind" in source) {
const sourceRotation = new Quaternion().setFromEuler(new Euler((source.rotationDegrees.x * Math.PI) / 180, (source.rotationDegrees.y * Math.PI) / 180, (source.rotationDegrees.z * Math.PI) / 180, "XYZ"));
const sourceRotation = createRotationQuaternion(source.rotationDegrees);
const projectedSourceX = new Vector2(new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x, new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z);
const projectedSourceZ = new Vector2(new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x, new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z);
const nextPrimaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ;
if (nextPrimaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
primaryAxis.copy(nextPrimaryAxis).normalize();
secondaryAxis.set(-primaryAxis.y, primaryAxis.x);
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
secondaryAxis.negate();
}
preferredAxis = nextPrimaryAxis.normalize();
}
}
let minPrimary = Number.POSITIVE_INFINITY;
let maxPrimary = Number.NEGATIVE_INFINITY;
let minSecondary = Number.POSITIVE_INFINITY;
let maxSecondary = Number.NEGATIVE_INFINITY;
for (const point of clippedFootprint) {
const primaryDistance = point.dot(primaryAxis);
const secondaryDistance = point.dot(secondaryAxis);
minPrimary = Math.min(minPrimary, primaryDistance);
maxPrimary = Math.max(maxPrimary, primaryDistance);
minSecondary = Math.min(minSecondary, secondaryDistance);
maxSecondary = Math.max(maxSecondary, secondaryDistance);
const patch = createPatchFromProjectedPoints(clippedFootprint, preferredAxis, Math.max(0.08, Math.min(0.18, surfaceBand * 0.4)));
if (patch !== null) {
patches.push(patch);
}
const halfWidth = (maxPrimary - minPrimary) * 0.5;
const halfDepth = (maxSecondary - minSecondary) * 0.5;
if (halfWidth <= WATER_CONTACT_EPSILON || halfDepth <= WATER_CONTACT_EPSILON) {
continue;
}
const patchCenterPrimary = (minPrimary + maxPrimary) * 0.5;
const patchCenterSecondary = (minSecondary + maxSecondary) * 0.5;
const centerX = primaryAxis.x * patchCenterPrimary + secondaryAxis.x * patchCenterSecondary;
const centerZ = primaryAxis.y * patchCenterPrimary + secondaryAxis.y * patchCenterSecondary;
axisX = primaryAxis.x;
axisZ = primaryAxis.y;
patches.push({
x: centerX,
z: centerZ,
halfWidth,
halfDepth,
axisX,
axisZ
});
}
return patches