From b583eaee298bae37965325c5af505db8cac4d4f7 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Thu, 21 May 2026 18:43:16 +0300 Subject: [PATCH] frustum hit test now works with faces also --- .../src/helpers/circularFrustumIntersect.ts | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/client/src/helpers/circularFrustumIntersect.ts b/client/src/helpers/circularFrustumIntersect.ts index ea1d3ba..9d1134f 100644 --- a/client/src/helpers/circularFrustumIntersect.ts +++ b/client/src/helpers/circularFrustumIntersect.ts @@ -147,42 +147,71 @@ export class CircularFrustumIntersection { intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)), intersectsTriangle: (tri: ExtendedTriangle, _index: number, contained: boolean) => { - // If the whole node was CONTAINED, every triangle is inside — fast path if (contained) { const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld); const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); results.push({ object: mesh, point: worldPoint, depth, triangle: tri }); - return !findAll; // stop if we only need first hit + return !findAll; } - // Test all three vertices; take the closest that's inside let bestDepth = Infinity; - let bestLocal: THREE.Vector3 | null = null; + let bestLocal: THREE.Vector3 | undefined = undefined; - for (const v of [tri.a, tri.b, tri.c] as THREE.Vector3[]) { + const tryPoint = (v: THREE.Vector3) => { const d = CircularFrustumIntersection.pointAxialDepth(v, localFrustum); - if (d !== 'NOT_INTERSECTED') { - if (d < bestDepth) { - bestDepth = d; - bestLocal = v; - } + if (d !== 'NOT_INTERSECTED' && (d as number) < bestDepth) { + bestDepth = d as number; + bestLocal = v.clone(); } + }; + + // 1. Test vertices + tryPoint(tri.a); + tryPoint(tri.b); + tryPoint(tri.c); + + // 2. For each edge, find the point closest to the frustum axis ray, + // and also the point closest to the apex. + // This catches triangles that straddle the cone surface. + const edges: [THREE.Vector3, THREE.Vector3][] = [ + [tri.a, tri.b], + [tri.b, tri.c], + [tri.c, tri.a], + ]; + + for (const [a, b] of edges) { + const edge = b.clone().sub(a); + const toA = a.clone().sub(localFrustum.apex); + + // Closest point on edge segment to the axis ray + const edgeDir = edge.clone().normalize(); + const axisDotEdge = localFrustum.axisNormalized.dot(edgeDir); + const denom = 1 - axisDotEdge * axisDotEdge; + + if (Math.abs(denom) > 1e-10) { + const t = ( + localFrustum.axisNormalized.dot(toA) * axisDotEdge + - toA.dot(edgeDir) + ) / denom; + const edgeLen = edge.length(); + const tClamped = Math.max(0, Math.min(edgeLen, t)); + const pointOnEdge = a.clone().addScaledVector(edgeDir, tClamped); + tryPoint(pointOnEdge); + } + + // Closest point on edge to the apex itself + const tApex = Math.max(0, Math.min(1, -toA.dot(edge) / edge.lengthSq())); + tryPoint(a.clone().addScaledVector(edge, tApex)); } - // Also test closest point on triangle to the frustum axis ray - const ray = new THREE.Ray(localFrustum.apex, localFrustum.axisNormalized); - const closest = new THREE.Vector3(); - tri.closestPointToPoint(ray.origin, closest); // ExtendedTriangle has this - const d = CircularFrustumIntersection.pointAxialDepth(closest, localFrustum); - if (d !== 'NOT_INTERSECTED') { - if (d < bestDepth) { - bestDepth = d; - bestLocal = closest; - } - } + // 3. Closest point on the triangle face to the apex + const closestOnFace = new THREE.Vector3(); + tri.closestPointToPoint(localFrustum.apex, closestOnFace); + if (!isNaN(closestOnFace.x)) + tryPoint(closestOnFace); - if (bestLocal) { - const worldPoint = bestLocal.clone().applyMatrix4(mesh.matrixWorld); + if (bestLocal !== undefined) { + const worldPoint = (bestLocal as THREE.Vector3).clone().applyMatrix4(mesh.matrixWorld); const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); results.push({ object: mesh, point: worldPoint, depth: worldDepth, triangle: tri }); return !findAll;