Add polygon clipping and area calculation to water contact patch collection
This commit is contained in:
@@ -41,6 +41,128 @@ function createInverseVolumeRotation(rotationDegrees) {
|
|||||||
.invert();
|
.invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cross2d(origin, pointA, pointB) {
|
||||||
|
return (pointA.x - origin.x) * (pointB.y - origin.y) - (pointA.y - origin.y) * (pointB.x - origin.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConvexHull(points) {
|
||||||
|
const sortedPoints = [...points]
|
||||||
|
.map((point) => point.clone())
|
||||||
|
.sort((left, right) => (left.x === right.x ? left.y - right.y : left.x - right.x));
|
||||||
|
const uniquePoints = [];
|
||||||
|
for (const point of sortedPoints) {
|
||||||
|
const lastPoint = uniquePoints.at(-1);
|
||||||
|
if (lastPoint === undefined || Math.abs(point.x - lastPoint.x) > WATER_CONTACT_EPSILON || Math.abs(point.y - lastPoint.y) > WATER_CONTACT_EPSILON) {
|
||||||
|
uniquePoints.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uniquePoints.length <= 2) {
|
||||||
|
return uniquePoints;
|
||||||
|
}
|
||||||
|
const lowerHull = [];
|
||||||
|
for (const point of uniquePoints) {
|
||||||
|
while (lowerHull.length >= 2 && cross2d(lowerHull[lowerHull.length - 2], lowerHull[lowerHull.length - 1], point) <= WATER_CONTACT_EPSILON) {
|
||||||
|
lowerHull.pop();
|
||||||
|
}
|
||||||
|
lowerHull.push(point);
|
||||||
|
}
|
||||||
|
const upperHull = [];
|
||||||
|
for (let index = uniquePoints.length - 1; index >= 0; index -= 1) {
|
||||||
|
const point = uniquePoints[index];
|
||||||
|
if (point === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (upperHull.length >= 2 && cross2d(upperHull[upperHull.length - 2], upperHull[upperHull.length - 1], point) <= WATER_CONTACT_EPSILON) {
|
||||||
|
upperHull.pop();
|
||||||
|
}
|
||||||
|
upperHull.push(point);
|
||||||
|
}
|
||||||
|
lowerHull.pop();
|
||||||
|
upperHull.pop();
|
||||||
|
return [...lowerHull, ...upperHull];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonAgainstVerticalBoundary(polygon, limit, keepGreater) {
|
||||||
|
if (polygon.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const clipped = [];
|
||||||
|
let previousPoint = polygon[polygon.length - 1] ?? null;
|
||||||
|
if (previousPoint === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let previousInside = keepGreater ? previousPoint.x >= limit - WATER_CONTACT_EPSILON : previousPoint.x <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
for (const point of polygon) {
|
||||||
|
const inside = keepGreater ? point.x >= limit - WATER_CONTACT_EPSILON : point.x <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
if (inside !== previousInside) {
|
||||||
|
const deltaX = point.x - previousPoint.x;
|
||||||
|
if (Math.abs(deltaX) > WATER_CONTACT_EPSILON) {
|
||||||
|
const interpolation = (limit - previousPoint.x) / deltaX;
|
||||||
|
clipped.push(new Vector2(limit, previousPoint.y + (point.y - previousPoint.y) * interpolation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inside) {
|
||||||
|
clipped.push(point.clone());
|
||||||
|
}
|
||||||
|
previousPoint = point;
|
||||||
|
previousInside = inside;
|
||||||
|
}
|
||||||
|
return clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonAgainstHorizontalBoundary(polygon, limit, keepGreater) {
|
||||||
|
if (polygon.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const clipped = [];
|
||||||
|
let previousPoint = polygon[polygon.length - 1] ?? null;
|
||||||
|
if (previousPoint === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let previousInside = keepGreater ? previousPoint.y >= limit - WATER_CONTACT_EPSILON : previousPoint.y <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
for (const point of polygon) {
|
||||||
|
const inside = keepGreater ? point.y >= limit - WATER_CONTACT_EPSILON : point.y <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
if (inside !== previousInside) {
|
||||||
|
const deltaY = point.y - previousPoint.y;
|
||||||
|
if (Math.abs(deltaY) > WATER_CONTACT_EPSILON) {
|
||||||
|
const interpolation = (limit - previousPoint.y) / deltaY;
|
||||||
|
clipped.push(new Vector2(previousPoint.x + (point.x - previousPoint.x) * interpolation, limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inside) {
|
||||||
|
clipped.push(point.clone());
|
||||||
|
}
|
||||||
|
previousPoint = point;
|
||||||
|
previousInside = inside;
|
||||||
|
}
|
||||||
|
return clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonToRectangle(polygon, minX, maxX, minZ, maxZ) {
|
||||||
|
let clippedPolygon = polygon;
|
||||||
|
clippedPolygon = clipPolygonAgainstVerticalBoundary(clippedPolygon, minX, true);
|
||||||
|
clippedPolygon = clipPolygonAgainstVerticalBoundary(clippedPolygon, maxX, false);
|
||||||
|
clippedPolygon = clipPolygonAgainstHorizontalBoundary(clippedPolygon, minZ, true);
|
||||||
|
clippedPolygon = clipPolygonAgainstHorizontalBoundary(clippedPolygon, maxZ, false);
|
||||||
|
return clippedPolygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePolygonArea(polygon) {
|
||||||
|
if (polygon.length < 3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let doubledArea = 0;
|
||||||
|
for (let index = 0; index < polygon.length; index += 1) {
|
||||||
|
const point = polygon[index];
|
||||||
|
const nextPoint = polygon[(index + 1) % polygon.length];
|
||||||
|
if (point === undefined || nextPoint === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
doubledArea += point.x * nextPoint.y - nextPoint.x * point.y;
|
||||||
|
}
|
||||||
|
return Math.abs(doubledArea) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -53,6 +175,7 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
|
|
||||||
for (const source of contactBounds) {
|
for (const source of contactBounds) {
|
||||||
const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
|
const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
|
||||||
|
const localCorners = [];
|
||||||
let minX = Number.POSITIVE_INFINITY;
|
let minX = Number.POSITIVE_INFINITY;
|
||||||
let minY = Number.POSITIVE_INFINITY;
|
let minY = Number.POSITIVE_INFINITY;
|
||||||
let minZ = Number.POSITIVE_INFINITY;
|
let minZ = Number.POSITIVE_INFINITY;
|
||||||
@@ -66,6 +189,7 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
localPoint.y -= volume.center.y;
|
localPoint.y -= volume.center.y;
|
||||||
localPoint.z -= volume.center.z;
|
localPoint.z -= volume.center.z;
|
||||||
localPoint.applyQuaternion(inverseRotation);
|
localPoint.applyQuaternion(inverseRotation);
|
||||||
|
localCorners.push(localPoint.clone());
|
||||||
minX = Math.min(minX, localPoint.x);
|
minX = Math.min(minX, localPoint.x);
|
||||||
minY = Math.min(minY, localPoint.y);
|
minY = Math.min(minY, localPoint.y);
|
||||||
minZ = Math.min(minZ, localPoint.z);
|
minZ = Math.min(minZ, localPoint.z);
|
||||||
@@ -82,14 +206,9 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overlapMinX = Math.max(minX, -halfX);
|
const clippedFootprint = clipPolygonToRectangle(buildConvexHull(localCorners.map((corner) => new Vector2(corner.x, corner.z))), -halfX, halfX, -halfZ, halfZ);
|
||||||
const overlapMaxX = Math.min(maxX, halfX);
|
|
||||||
const overlapMinZ = Math.max(minZ, -halfZ);
|
|
||||||
const overlapMaxZ = Math.min(maxZ, halfZ);
|
|
||||||
const overlapWidth = overlapMaxX - overlapMinX;
|
|
||||||
const overlapDepth = overlapMaxZ - overlapMinZ;
|
|
||||||
|
|
||||||
if (overlapWidth <= WATER_CONTACT_EPSILON || overlapDepth <= WATER_CONTACT_EPSILON) {
|
if (calculatePolygonArea(clippedFootprint) <= WATER_CONTACT_EPSILON) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,55 +220,52 @@ export function collectWaterContactPatches(volume, contactBounds) {
|
|||||||
|
|
||||||
let axisX = 1;
|
let axisX = 1;
|
||||||
let axisZ = 0;
|
let axisZ = 0;
|
||||||
let halfWidth = overlapWidth * 0.5;
|
const primaryAxis = new Vector2(1, 0);
|
||||||
let halfDepth = overlapDepth * 0.5;
|
const secondaryAxis = new Vector2(0, 1);
|
||||||
let centerX = (overlapMinX + overlapMaxX) * 0.5;
|
|
||||||
let centerZ = (overlapMinZ + overlapMaxZ) * 0.5;
|
|
||||||
|
|
||||||
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 = 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 projectedSourceX = new Vector2(1, 0).set(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(1, 0).set(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 primaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ;
|
const nextPrimaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ;
|
||||||
|
|
||||||
if (primaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
if (nextPrimaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
||||||
primaryAxis.normalize();
|
primaryAxis.copy(nextPrimaryAxis).normalize();
|
||||||
const secondaryAxis = new Vector2(-primaryAxis.y, primaryAxis.x);
|
secondaryAxis.set(-primaryAxis.y, primaryAxis.x);
|
||||||
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
|
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
|
||||||
secondaryAxis.negate();
|
secondaryAxis.negate();
|
||||||
}
|
}
|
||||||
|
|
||||||
let minPrimary = Number.POSITIVE_INFINITY;
|
|
||||||
let maxPrimary = Number.NEGATIVE_INFINITY;
|
|
||||||
let minSecondary = Number.POSITIVE_INFINITY;
|
|
||||||
let maxSecondary = Number.NEGATIVE_INFINITY;
|
|
||||||
|
|
||||||
for (const corner of corners) {
|
|
||||||
localPoint.copy(corner);
|
|
||||||
localPoint.x -= volume.center.x;
|
|
||||||
localPoint.y -= volume.center.y;
|
|
||||||
localPoint.z -= volume.center.z;
|
|
||||||
localPoint.applyQuaternion(inverseRotation);
|
|
||||||
const projectedPoint = new Vector2(localPoint.x, localPoint.z);
|
|
||||||
const primaryDistance = projectedPoint.dot(primaryAxis);
|
|
||||||
const secondaryDistance = projectedPoint.dot(secondaryAxis);
|
|
||||||
minPrimary = Math.min(minPrimary, primaryDistance);
|
|
||||||
maxPrimary = Math.max(maxPrimary, primaryDistance);
|
|
||||||
minSecondary = Math.min(minSecondary, secondaryDistance);
|
|
||||||
maxSecondary = Math.max(maxSecondary, secondaryDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchCenterPrimary = (minPrimary + maxPrimary) * 0.5;
|
|
||||||
const patchCenterSecondary = (minSecondary + maxSecondary) * 0.5;
|
|
||||||
centerX = primaryAxis.x * patchCenterPrimary + secondaryAxis.x * patchCenterSecondary;
|
|
||||||
centerZ = primaryAxis.y * patchCenterPrimary + secondaryAxis.y * patchCenterSecondary;
|
|
||||||
halfWidth = (maxPrimary - minPrimary) * 0.5;
|
|
||||||
halfDepth = (maxSecondary - minSecondary) * 0.5;
|
|
||||||
axisX = primaryAxis.x;
|
|
||||||
axisZ = primaryAxis.y;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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({
|
patches.push({
|
||||||
x: centerX,
|
x: centerX,
|
||||||
z: centerZ,
|
z: centerZ,
|
||||||
|
|||||||
@@ -100,6 +100,158 @@ function createInverseVolumeRotation(rotationDegrees: Vec3) {
|
|||||||
.invert();
|
.invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cross2d(origin: Vector2, pointA: Vector2, pointB: Vector2) {
|
||||||
|
return (pointA.x - origin.x) * (pointB.y - origin.y) - (pointA.y - origin.y) * (pointB.x - origin.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConvexHull(points: Vector2[]) {
|
||||||
|
const sortedPoints = [...points]
|
||||||
|
.map((point) => point.clone())
|
||||||
|
.sort((left, right) => (left.x === right.x ? left.y - right.y : left.x - right.x));
|
||||||
|
const uniquePoints: Vector2[] = [];
|
||||||
|
|
||||||
|
for (const point of sortedPoints) {
|
||||||
|
const lastPoint = uniquePoints.at(-1);
|
||||||
|
if (lastPoint === undefined || Math.abs(point.x - lastPoint.x) > WATER_CONTACT_EPSILON || Math.abs(point.y - lastPoint.y) > WATER_CONTACT_EPSILON) {
|
||||||
|
uniquePoints.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniquePoints.length <= 2) {
|
||||||
|
return uniquePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerHull: Vector2[] = [];
|
||||||
|
|
||||||
|
for (const point of uniquePoints) {
|
||||||
|
while (lowerHull.length >= 2 && cross2d(lowerHull[lowerHull.length - 2], lowerHull[lowerHull.length - 1], point) <= WATER_CONTACT_EPSILON) {
|
||||||
|
lowerHull.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerHull.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upperHull: Vector2[] = [];
|
||||||
|
|
||||||
|
for (let index = uniquePoints.length - 1; index >= 0; index -= 1) {
|
||||||
|
const point = uniquePoints[index];
|
||||||
|
if (point === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (upperHull.length >= 2 && cross2d(upperHull[upperHull.length - 2], upperHull[upperHull.length - 1], point) <= WATER_CONTACT_EPSILON) {
|
||||||
|
upperHull.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
upperHull.push(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerHull.pop();
|
||||||
|
upperHull.pop();
|
||||||
|
return [...lowerHull, ...upperHull];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonAgainstVerticalBoundary(polygon: Vector2[], limit: number, keepGreater: boolean) {
|
||||||
|
if (polygon.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipped: Vector2[] = [];
|
||||||
|
let previousPoint = polygon[polygon.length - 1] ?? null;
|
||||||
|
|
||||||
|
if (previousPoint === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousInside = keepGreater ? previousPoint.x >= limit - WATER_CONTACT_EPSILON : previousPoint.x <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
|
||||||
|
for (const point of polygon) {
|
||||||
|
const inside = keepGreater ? point.x >= limit - WATER_CONTACT_EPSILON : point.x <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
|
||||||
|
if (inside !== previousInside) {
|
||||||
|
const deltaX = point.x - previousPoint.x;
|
||||||
|
if (Math.abs(deltaX) > WATER_CONTACT_EPSILON) {
|
||||||
|
const interpolation = (limit - previousPoint.x) / deltaX;
|
||||||
|
clipped.push(new Vector2(limit, previousPoint.y + (point.y - previousPoint.y) * interpolation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inside) {
|
||||||
|
clipped.push(point.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPoint = point;
|
||||||
|
previousInside = inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonAgainstHorizontalBoundary(polygon: Vector2[], limit: number, keepGreater: boolean) {
|
||||||
|
if (polygon.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipped: Vector2[] = [];
|
||||||
|
let previousPoint = polygon[polygon.length - 1] ?? null;
|
||||||
|
|
||||||
|
if (previousPoint === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousInside = keepGreater ? previousPoint.y >= limit - WATER_CONTACT_EPSILON : previousPoint.y <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
|
||||||
|
for (const point of polygon) {
|
||||||
|
const inside = keepGreater ? point.y >= limit - WATER_CONTACT_EPSILON : point.y <= limit + WATER_CONTACT_EPSILON;
|
||||||
|
|
||||||
|
if (inside !== previousInside) {
|
||||||
|
const deltaY = point.y - previousPoint.y;
|
||||||
|
if (Math.abs(deltaY) > WATER_CONTACT_EPSILON) {
|
||||||
|
const interpolation = (limit - previousPoint.y) / deltaY;
|
||||||
|
clipped.push(new Vector2(previousPoint.x + (point.x - previousPoint.x) * interpolation, limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inside) {
|
||||||
|
clipped.push(point.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPoint = point;
|
||||||
|
previousInside = inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPolygonToRectangle(polygon: Vector2[], minX: number, maxX: number, minZ: number, maxZ: number) {
|
||||||
|
let clippedPolygon = polygon;
|
||||||
|
clippedPolygon = clipPolygonAgainstVerticalBoundary(clippedPolygon, minX, true);
|
||||||
|
clippedPolygon = clipPolygonAgainstVerticalBoundary(clippedPolygon, maxX, false);
|
||||||
|
clippedPolygon = clipPolygonAgainstHorizontalBoundary(clippedPolygon, minZ, true);
|
||||||
|
clippedPolygon = clipPolygonAgainstHorizontalBoundary(clippedPolygon, maxZ, false);
|
||||||
|
return clippedPolygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePolygonArea(polygon: Vector2[]) {
|
||||||
|
if (polygon.length < 3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let doubledArea = 0;
|
||||||
|
|
||||||
|
for (let index = 0; index < polygon.length; index += 1) {
|
||||||
|
const point = polygon[index];
|
||||||
|
const nextPoint = polygon[(index + 1) % polygon.length];
|
||||||
|
if (point === undefined || nextPoint === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
doubledArea += point.x * nextPoint.y - nextPoint.x * point.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(doubledArea) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] {
|
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] {
|
||||||
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);
|
||||||
@@ -112,6 +264,7 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
|
|||||||
|
|
||||||
for (const source of contactBounds) {
|
for (const source of contactBounds) {
|
||||||
const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
|
const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source);
|
||||||
|
const localCorners: Vector3[] = [];
|
||||||
let minX = Number.POSITIVE_INFINITY;
|
let minX = Number.POSITIVE_INFINITY;
|
||||||
let minY = Number.POSITIVE_INFINITY;
|
let minY = Number.POSITIVE_INFINITY;
|
||||||
let minZ = Number.POSITIVE_INFINITY;
|
let minZ = Number.POSITIVE_INFINITY;
|
||||||
@@ -125,6 +278,7 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
|
|||||||
localPoint.y -= volume.center.y;
|
localPoint.y -= volume.center.y;
|
||||||
localPoint.z -= volume.center.z;
|
localPoint.z -= volume.center.z;
|
||||||
localPoint.applyQuaternion(inverseRotation);
|
localPoint.applyQuaternion(inverseRotation);
|
||||||
|
localCorners.push(localPoint.clone());
|
||||||
minX = Math.min(minX, localPoint.x);
|
minX = Math.min(minX, localPoint.x);
|
||||||
minY = Math.min(minY, localPoint.y);
|
minY = Math.min(minY, localPoint.y);
|
||||||
minZ = Math.min(minZ, localPoint.z);
|
minZ = Math.min(minZ, localPoint.z);
|
||||||
@@ -141,14 +295,15 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overlapMinX = Math.max(minX, -halfX);
|
const clippedFootprint = clipPolygonToRectangle(
|
||||||
const overlapMaxX = Math.min(maxX, halfX);
|
buildConvexHull(localCorners.map((corner) => new Vector2(corner.x, corner.z))),
|
||||||
const overlapMinZ = Math.max(minZ, -halfZ);
|
-halfX,
|
||||||
const overlapMaxZ = Math.min(maxZ, halfZ);
|
halfX,
|
||||||
const overlapWidth = overlapMaxX - overlapMinX;
|
-halfZ,
|
||||||
const overlapDepth = overlapMaxZ - overlapMinZ;
|
halfZ
|
||||||
|
);
|
||||||
|
|
||||||
if (overlapWidth <= WATER_CONTACT_EPSILON || overlapDepth <= WATER_CONTACT_EPSILON) {
|
if (calculatePolygonArea(clippedFootprint) <= WATER_CONTACT_EPSILON) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +315,8 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
|
|||||||
|
|
||||||
let axisX = 1;
|
let axisX = 1;
|
||||||
let axisZ = 0;
|
let axisZ = 0;
|
||||||
let halfWidth = overlapWidth * 0.5;
|
const primaryAxis = new Vector2(1, 0);
|
||||||
let halfDepth = overlapDepth * 0.5;
|
const secondaryAxis = new Vector2(0, 1);
|
||||||
let centerX = (overlapMinX + overlapMaxX) * 0.5;
|
|
||||||
let centerZ = (overlapMinZ + overlapMaxZ) * 0.5;
|
|
||||||
|
|
||||||
if ("kind" in source) {
|
if ("kind" in source) {
|
||||||
const sourceRotation = new Quaternion().setFromEuler(
|
const sourceRotation = new Quaternion().setFromEuler(
|
||||||
@@ -174,56 +327,53 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
|
|||||||
"XYZ"
|
"XYZ"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const projectedSourceX = new Vector2(1, 0)
|
const projectedSourceX = new Vector2(
|
||||||
.set(
|
new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x,
|
||||||
new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x,
|
new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z
|
||||||
new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z
|
);
|
||||||
);
|
const projectedSourceZ = new Vector2(
|
||||||
const projectedSourceZ = new Vector2(1, 0)
|
new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x,
|
||||||
.set(
|
new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z
|
||||||
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 primaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ;
|
|
||||||
|
|
||||||
if (primaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
if (nextPrimaryAxis.lengthSq() > WATER_CONTACT_EPSILON) {
|
||||||
primaryAxis.normalize();
|
primaryAxis.copy(nextPrimaryAxis).normalize();
|
||||||
const secondaryAxis = new Vector2(-primaryAxis.y, primaryAxis.x);
|
secondaryAxis.set(-primaryAxis.y, primaryAxis.x);
|
||||||
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
|
if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) {
|
||||||
secondaryAxis.negate();
|
secondaryAxis.negate();
|
||||||
}
|
}
|
||||||
|
|
||||||
let minPrimary = Number.POSITIVE_INFINITY;
|
|
||||||
let maxPrimary = Number.NEGATIVE_INFINITY;
|
|
||||||
let minSecondary = Number.POSITIVE_INFINITY;
|
|
||||||
let maxSecondary = Number.NEGATIVE_INFINITY;
|
|
||||||
|
|
||||||
for (const corner of corners) {
|
|
||||||
localPoint.copy(corner);
|
|
||||||
localPoint.x -= volume.center.x;
|
|
||||||
localPoint.y -= volume.center.y;
|
|
||||||
localPoint.z -= volume.center.z;
|
|
||||||
localPoint.applyQuaternion(inverseRotation);
|
|
||||||
const projectedPoint = new Vector2(localPoint.x, localPoint.z);
|
|
||||||
const primaryDistance = projectedPoint.dot(primaryAxis);
|
|
||||||
const secondaryDistance = projectedPoint.dot(secondaryAxis);
|
|
||||||
minPrimary = Math.min(minPrimary, primaryDistance);
|
|
||||||
maxPrimary = Math.max(maxPrimary, primaryDistance);
|
|
||||||
minSecondary = Math.min(minSecondary, secondaryDistance);
|
|
||||||
maxSecondary = Math.max(maxSecondary, secondaryDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchCenterPrimary = (minPrimary + maxPrimary) * 0.5;
|
|
||||||
const patchCenterSecondary = (minSecondary + maxSecondary) * 0.5;
|
|
||||||
centerX = primaryAxis.x * patchCenterPrimary + secondaryAxis.x * patchCenterSecondary;
|
|
||||||
centerZ = primaryAxis.y * patchCenterPrimary + secondaryAxis.y * patchCenterSecondary;
|
|
||||||
halfWidth = (maxPrimary - minPrimary) * 0.5;
|
|
||||||
halfDepth = (maxSecondary - minSecondary) * 0.5;
|
|
||||||
axisX = primaryAxis.x;
|
|
||||||
axisZ = primaryAxis.y;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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({
|
patches.push({
|
||||||
x: centerX,
|
x: centerX,
|
||||||
z: centerZ,
|
z: centerZ,
|
||||||
|
|||||||
@@ -130,6 +130,96 @@ describe("water material helpers", () => {
|
|||||||
expect(Math.abs(patches[0]?.axisZ ?? 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 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