Add functions for clipping polygons and creating patches from projected points
This commit is contained in:
@@ -35,10 +35,12 @@ function createOrientedBoxCorners(box) {
|
|||||||
].map((corner) => corner.applyQuaternion(rotation).add(new Vector3(box.center.x, box.center.y, box.center.z)));
|
].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) {
|
function createInverseVolumeRotation(rotationDegrees) {
|
||||||
return new Quaternion()
|
return createRotationQuaternion(rotationDegrees).invert();
|
||||||
.setFromEuler(new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ"))
|
|
||||||
.invert();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cross2d(origin, pointA, pointB) {
|
function cross2d(origin, pointA, pointB) {
|
||||||
@@ -147,6 +149,45 @@ function clipPolygonToRectangle(polygon, minX, maxX, minZ, maxZ) {
|
|||||||
return clippedPolygon;
|
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) {
|
function calculatePolygonArea(polygon) {
|
||||||
if (polygon.length < 3) {
|
if (polygon.length < 3) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -163,6 +204,101 @@ function calculatePolygonArea(polygon) {
|
|||||||
return Math.abs(doubledArea) * 0.5;
|
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) {
|
export function collectWaterContactPatches(volume, contactBounds) {
|
||||||
const inverseRotation = createInverseVolumeRotation(volume.rotationDegrees);
|
const inverseRotation = createInverseVolumeRotation(volume.rotationDegrees);
|
||||||
const halfX = Math.max(volume.size.x * 0.5, WATER_CONTACT_EPSILON);
|
const halfX = Math.max(volume.size.x * 0.5, WATER_CONTACT_EPSILON);
|
||||||
@@ -174,6 +310,10 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
const patches = [];
|
const patches = [];
|
||||||
|
|
||||||
for (const source of contactBounds) {
|
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 corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
|
||||||
const localCorners = [];
|
const localCorners = [];
|
||||||
let minX = Number.POSITIVE_INFINITY;
|
let minX = Number.POSITIVE_INFINITY;
|
||||||
@@ -218,62 +358,22 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let axisX = 1;
|
let preferredAxis = null;
|
||||||
let axisZ = 0;
|
|
||||||
const primaryAxis = new Vector2(1, 0);
|
|
||||||
const secondaryAxis = new Vector2(0, 1);
|
|
||||||
|
|
||||||
if ("kind" in source) {
|
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 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 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;
|
const nextPrimaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ;
|
||||||
|
|
||||||
if (nextPrimaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
if (nextPrimaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
||||||
primaryAxis.copy(nextPrimaryAxis).normalize();
|
preferredAxis = nextPrimaryAxis.normalize();
|
||||||
secondaryAxis.set(-primaryAxis.y, primaryAxis.x);
|
|
||||||
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
|
|
||||||
secondaryAxis.negate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const patch = createPatchFromProjectedPoints(clippedFootprint, preferredAxis, Math.max(0.08, Math.min(0.18, surfaceBand * 0.4)));
|
||||||
let minPrimary = Number.POSITIVE_INFINITY;
|
if (patch !== null) {
|
||||||
let maxPrimary = Number.NEGATIVE_INFINITY;
|
patches.push(patch);
|
||||||
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 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
|
return patches
|
||||||
|
|||||||
Reference in New Issue
Block a user