Просмотр исходного кода

Ray: New watertight intersectTriangle(). (#33661)

Michael Herzog 2 недель назад
Родитель
Сommit
bd942afdb3
3 измененных файлов с 129 добавлено и 45 удалено
  1. 4 0
      src/core/Raycaster.js
  2. 93 45
      src/math/Ray.js
  3. 32 0
      test/unit/src/math/Ray.tests.js

+ 4 - 0
src/core/Raycaster.js

@@ -185,6 +185,10 @@ class Raycaster {
 	 * be detected. To raycast against both faces of an object, you'll want to set  {@link Material#side}
 	 * to `THREE.DoubleSide`.
 	 *
+	 * Note that a ray hitting a triangle mesh exactly along an edge shared by two faces may be
+	 * reported by both faces, resulting in two coincident intersections (identical point and
+	 * distance) in the returned array.
+	 *
 	 * @param {Object3D} object - The 3D object to check for intersection with the ray.
 	 * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
 	 * Otherwise it only checks intersection with the object.

+ 93 - 45
src/math/Ray.js

@@ -5,10 +5,6 @@ const _segCenter = /*@__PURE__*/ new Vector3();
 const _segDir = /*@__PURE__*/ new Vector3();
 const _diff = /*@__PURE__*/ new Vector3();
 
-const _edge1 = /*@__PURE__*/ new Vector3();
-const _edge2 = /*@__PURE__*/ new Vector3();
-const _normal = /*@__PURE__*/ new Vector3();
-
 /**
  * A ray that emits from an origin in a certain direction. The class is used by
  * {@link Raycaster} to assist with raycasting. Raycasting is used for
@@ -539,76 +535,128 @@ class Ray {
 	 */
 	intersectTriangle( a, b, c, backfaceCulling, target ) {
 
-		// Compute the offset origin, edges, and normal.
+		// Watertight ray/triangle intersection. Reference: Woop, Benthin, Wald,
+		// "Watertight Ray/Triangle Intersection", JCGT vol. 2 no. 1 (2013), Appendix A.
+		// https://jcgt.org/published/0002/01/05/
+
+		const origin = this.origin;
+		const direction = this.direction;
+
+		const dx = direction.x;
+		const dy = direction.y;
+		const dz = direction.z;
+
+		// triangle vertices relative to the ray origin
+
+		const aox = a.x - origin.x, aoy = a.y - origin.y, aoz = a.z - origin.z;
+		const box = b.x - origin.x, boy = b.y - origin.y, boz = b.z - origin.z;
+		const cox = c.x - origin.x, coy = c.y - origin.y, coz = c.z - origin.z;
+
+		// Use the dimension where the ray direction is maximal as the projection
+		// axis (kz) and read every component already permuted into (kx, ky, kz).
+		// kx and ky are swapped when the direction's kz component is negative, to
+		// preserve the winding order of triangles.
+
+		const adx = Math.abs( dx ), ady = Math.abs( dy ), adz = Math.abs( dz );
+
+		let dkx, dky, dkz;
+		let akx, aky, akz, bkx, bky, bkz, ckx, cky, ckz;
+
+		if ( adx >= ady && adx >= adz ) {
 
-		// from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h
+			dkz = dx; akz = aox; bkz = box; ckz = cox;
 
-		_edge1.subVectors( b, a );
-		_edge2.subVectors( c, a );
-		_normal.crossVectors( _edge1, _edge2 );
+			if ( dx >= 0 ) {
 
-		// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
-		// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
-		//   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
-		//   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
-		//   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
-		let DdN = this.direction.dot( _normal );
-		let sign;
+				dkx = dy; dky = dz;
+				akx = aoy; aky = aoz; bkx = boy; bky = boz; ckx = coy; cky = coz;
 
-		if ( DdN > 0 ) {
+			} else {
+
+				dkx = dz; dky = dy;
+				akx = aoz; aky = aoy; bkx = boz; bky = boy; ckx = coz; cky = coy;
+
+			}
+
+		} else if ( ady >= adz ) {
 
-			if ( backfaceCulling ) return null;
-			sign = 1;
+			dkz = dy; akz = aoy; bkz = boy; ckz = coy;
 
-		} else if ( DdN < 0 ) {
+			if ( dy >= 0 ) {
 
-			sign = - 1;
-			DdN = - DdN;
+				dkx = dz; dky = dx;
+				akx = aoz; aky = aox; bkx = boz; bky = box; ckx = coz; cky = cox;
+
+			} else {
+
+				dkx = dx; dky = dz;
+				akx = aox; aky = aoz; bkx = box; bky = boz; ckx = cox; cky = coz;
+
+			}
 
 		} else {
 
-			return null;
+			dkz = dz; akz = aoz; bkz = boz; ckz = coz;
 
-		}
+			if ( dz >= 0 ) {
 
-		_diff.subVectors( this.origin, a );
-		const DdQxE2 = sign * this.direction.dot( _edge2.crossVectors( _diff, _edge2 ) );
+				dkx = dx; dky = dy;
+				akx = aox; aky = aoy; bkx = box; bky = boy; ckx = cox; cky = coy;
 
-		// b1 < 0, no intersection
-		if ( DdQxE2 < 0 ) {
+			} else {
 
-			return null;
+				dkx = dy; dky = dx;
+				akx = aoy; aky = aox; bkx = boy; bky = box; ckx = coy; cky = cox;
+
+			}
 
 		}
 
-		const DdE1xQ = sign * this.direction.dot( _edge1.cross( _diff ) );
+		// a zero direction has no maximal axis and cannot intersect
 
-		// b2 < 0, no intersection
-		if ( DdE1xQ < 0 ) {
+		if ( dkz === 0 ) return null;
 
-			return null;
+		// shear constants that align the ray with the +kz axis
 
-		}
+		const sx = dkx / dkz, sy = dky / dkz, sz = 1 / dkz;
 
-		// b1+b2 > 1, no intersection
-		if ( DdQxE2 + DdE1xQ > DdN ) {
+		// sheared and scaled vertices
 
-			return null;
+		const ax = akx - sx * akz, ay = aky - sy * akz;
+		const bx = bkx - sx * bkz, by = bky - sy * bkz;
+		const cx = ckx - sx * ckz, cy = cky - sy * ckz;
 
-		}
+		// scaled barycentric coordinates (signed edge functions); the shear makes a
+		// shared edge evaluate identically for both adjacent triangles, so the ray
+		// can never fall between them
 
-		// Line intersects triangle, check if ray does.
-		const QdN = - sign * _diff.dot( _normal );
+		const u = cx * by - cy * bx;
+		const v = ax * cy - ay * cx;
+		const w = bx * ay - by * ax;
 
-		// t < 0, no intersection
-		if ( QdN < 0 ) {
+		if ( backfaceCulling ) {
 
-			return null;
+			if ( u < 0 || v < 0 || w < 0 ) return null;
+
+		} else {
+
+			if ( ( u < 0 || v < 0 || w < 0 ) && ( u > 0 || v > 0 || w > 0 ) ) return null;
 
 		}
 
-		// Ray intersects triangle.
-		return this.at( QdN / DdN, target );
+		const det = u + v + w;
+
+		// ray is co-planar with the triangle
+
+		if ( det === 0 ) return null;
+
+		// scaled hit distance; t = tScaled / det must lie in front of the origin
+
+		const tScaled = sz * ( u * akz + v * bkz + w * ckz );
+
+		if ( det > 0 ? tScaled < 0 : tScaled > 0 ) return null;
+
+		return this.at( tScaled / det, target );
 
 	}
 

+ 32 - 0
test/unit/src/math/Ray.tests.js

@@ -431,6 +431,38 @@ export default QUnit.module( 'Maths', () => {
 
 		} );
 
+		QUnit.test( 'intersectTriangle (watertight at shared edges)', ( assert ) => {
+
+			// Two triangles forming a quad and sharing the diagonal edge from
+			// ( -2, -2, -2 ) to ( 2, -2, 2 ). A ray aimed exactly at the midpoint of
+			// that shared edge must be detected: a non-watertight test can let the ray
+			// slip through the seam between the triangles and miss both of them.
+
+			const t1a = new Vector3( - 2, - 2, 2 );
+			const t1b = new Vector3( - 2, - 2, - 2 );
+			const t1c = new Vector3( 2, - 2, 2 );
+
+			const t2a = new Vector3( - 2, - 2, - 2 );
+			const t2b = new Vector3( 2, - 2, - 2 );
+			const t2c = new Vector3( 2, - 2, 2 );
+
+			const seam = new Vector3( 0, - 2, 0 ); // midpoint of the shared edge
+			const origin = new Vector3( - 4, - 9, 0.4 );
+			const direction = new Vector3().subVectors( seam, origin ).normalize();
+			const ray = new Ray( origin, direction );
+
+			const p1 = new Vector3();
+			const p2 = new Vector3();
+			const hit1 = ray.intersectTriangle( t1a, t1b, t1c, false, p1 );
+			const hit2 = ray.intersectTriangle( t2a, t2b, t2c, false, p2 );
+
+			assert.ok( hit1 !== null || hit2 !== null, 'Ray hitting the shared edge is not dropped' );
+
+			const hit = hit1 !== null ? p1 : p2;
+			assert.ok( hit.distanceTo( seam ) <= eps, 'Intersection lies on the shared edge' );
+
+		} );
+
 		QUnit.test( 'applyMatrix4', ( assert ) => {
 
 			let a = new Ray( one3.clone(), new Vector3( 0, 0, 1 ) );

粤ICP备19079148号