frustum instead of barycentric hittest for vertices and edges

This commit is contained in:
azykov@mail.ru 2026-05-25 21:22:28 +03:00
parent f5f5dcd84f
commit aeef84b708
No known key found for this signature in database
1 changed files with 60 additions and 70 deletions

View File

@ -85,74 +85,6 @@ export function intersectionResultToBvh(value: IntersectionResult): typeof NOT_I
} }
} }
const BARYCENTRIC_EPSILON = 1e-1;
function triangleFaceEdgeVertexHit(
point: THREE.Vector3,
tri: THREE.Triangle,
// vertexIds?: [Id, Id, Id],
): TriangleHitDetail[] {
const results: TriangleHitDetail[] = [{ kind: 'face' }];
const bary = new THREE.Vector3();
tri.getBarycoord(point, bary);
const [u, v, w] = bary.toArray(); // x = AB, y = AC, z = BC
//TODO if AB is much longer than AC, epsilon has different world size. need to scale
const eps = 1 - BARYCENTRIC_EPSILON;
const onAB = w < BARYCENTRIC_EPSILON;
const onBC = u < BARYCENTRIC_EPSILON;
const onCA = v < BARYCENTRIC_EPSILON;
if (onAB) {
results.unshift({
kind: 'edge',
index: 0,
ptIndexA: 0,
ptIndexB: 1,
ptA: tri.a,
ptB: tri.b,
// idA: vertexIds?.[0],
// idB: vertexIds?.[1],
});
}
if (onBC) {
results.unshift({
kind: 'edge',
index: 1,
ptIndexA: 1,
ptIndexB: 2,
ptA: tri.b,
ptB: tri.c,
// idA: vertexIds?.[1],
// idB: vertexIds?.[2],
});
}
if (onCA) {
results.unshift({
kind: 'edge',
index: 2,
ptIndexA: 2,
ptIndexB: 0,
ptA: tri.c,
ptB: tri.a,
// idA: vertexIds?.[2],
// idB: vertexIds?.[0],
});
}
const onA = u > eps;
const onB = v > eps;
const onC = w > eps;
if (onA) results.unshift({ kind: 'vertex', index: 0, pt: tri.a });
if (onB) results.unshift({ kind: 'vertex', index: 1, pt: tri.b });
if (onC) results.unshift({ kind: 'vertex', index: 2, pt: tri.c });
return results;
}
function closestPointOnEdgeToRay( function closestPointOnEdgeToRay(
start: THREE.Vector3, start: THREE.Vector3,
end: THREE.Vector3, end: THREE.Vector3,
@ -174,6 +106,64 @@ function closestPointOnEdgeToRay(
return start.clone().addScaledVector(edgeDir, clamp(t, 0, edgeLen) / edgeLen); return start.clone().addScaledVector(edgeDir, clamp(t, 0, edgeLen) / edgeLen);
} }
function triangleDetailsByFrustum(
tri: THREE.Triangle,
localFrustum: CircularFrustum,
): TriangleHitDetail[] {
const results: TriangleHitDetail[] = [{ kind: 'face' }];
const verts: [THREE.Vector3, 0 | 1 | 2][] = [
[tri.a, 0],
[tri.b, 1],
[tri.c, 2],
];
const edges: [THREE.Vector3, THREE.Vector3, 0 | 1 | 2, 0 | 1 | 2, 0 | 1 | 2][] = [
[tri.a, tri.b, 0, 0, 1],
[tri.b, tri.c, 1, 1, 2],
[tri.c, tri.a, 2, 2, 0],
];
// A vertex is "in the frustum" if it passes the cone test
const vertexInFrustum = verts.map(([v]) =>
CircularFrustumIntersection.pointAxialDepth(v, localFrustum) !== 'NOT_INTERSECTED'
);
// Promote to vertex hits
for (const [v, idx] of verts) {
if (vertexInFrustum[idx]) {
results.unshift({ kind: 'vertex', index: idx, pt: v });
}
}
// Promote to edge hits: an edge is hit if ANY point along it falls inside the frustum.
// We sample: the two endpoints, the closest point to the axis ray, and the closest to the apex.
for (const [a, b, edgeIdx, ptIndexA, ptIndexB] of edges) {
if (vertexInFrustum[ptIndexA] || vertexInFrustum[ptIndexB]) {
// At least one endpoint inside — edge is hit
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
continue;
}
// Check closest point on edge to frustum axis
const closestToAxis = closestPointOnEdgeToRay(a, b, localFrustum.ray);
if (CircularFrustumIntersection.pointAxialDepth(closestToAxis, localFrustum) !== 'NOT_INTERSECTED') {
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
continue;
}
// Check closest point on edge to apex
const edge = b.clone().sub(a);
const toA = a.clone().sub(localFrustum.apex);
const tApex = clamp(-toA.dot(edge) / edge.lengthSq(), 0, 1);
const closestToApex = a.clone().addScaledVector(edge, tApex);
if (CircularFrustumIntersection.pointAxialDepth(closestToApex, localFrustum) !== 'NOT_INTERSECTED') {
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
}
}
return results;
}
export class CircularFrustumIntersection { export class CircularFrustumIntersection {
public readonly frustum: CircularFrustum; public readonly frustum: CircularFrustum;
@ -353,7 +343,7 @@ export class CircularFrustumIntersection {
const worldPoint = closestContained.clone().applyMatrix4(mesh.matrixWorld); const worldPoint = closestContained.clone().applyMatrix4(mesh.matrixWorld);
const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
results.push( results.push(
...triangleFaceEdgeVertexHit(closestContained, tri) ...triangleDetailsByFrustum(tri, localFrustum)
.map((details) => { .map((details) => {
const closestPoint = getHitClosestPoint(tri, details, worldPoint); const closestPoint = getHitClosestPoint(tri, details, worldPoint);
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum); const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum);
@ -417,7 +407,7 @@ export class CircularFrustumIntersection {
const worldPoint = bestPoint.local.clone().applyMatrix4(mesh.matrixWorld); const worldPoint = bestPoint.local.clone().applyMatrix4(mesh.matrixWorld);
const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
results.push( results.push(
...triangleFaceEdgeVertexHit(bestPoint.local, tri) ...triangleDetailsByFrustum(tri, localFrustum)
.map((details) => { .map((details) => {
const closestPoint = getHitClosestPoint(tri, details, worldPoint); const closestPoint = getHitClosestPoint(tri, details, worldPoint);
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum); const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum);