Jelajahi Sumber

Sphere: Add expandByPoint() and union(). (#21493)

* Sphere: Add expandByPoint() and union().

* Tests: Clean up.
Michael Herzog 4 tahun lalu
induk
melakukan
95ce8f1400

+ 14 - 0
docs/api/en/math/Sphere.html

@@ -72,6 +72,13 @@
 		the distance will be negative.
 		</p>
 
+		<h3>[method:this expandByPoint]( [param:Vector3 point] )</h3>
+		<p>
+		[page:Vector3 point] - [page:Vector3] that should be included in the sphere.<br /><br />
+
+		Expands the boundaries of this sphere to include [page:Vector3 point].
+		</p>
+
 		<h3>[method:Boolean isEmpty]()</h3>
 		<p>
 		Checks to see if the sphere is empty (the radius set to a negative number).</br>
@@ -138,6 +145,13 @@
 		Translate the sphere's center by the provided offset [page:Vector3].
 		</p>
 
+		<h3>[method:this union]( [param:Sphere sphere] )</h3>
+		<p>
+		[page:Sphere sphere] - Bounding sphere that will be unioned with this sphere.<br /><br />
+
+		Expands this sphere to enclose both the original sphere and the given sphere.
+		</p>
+
 		<h2>Source</h2>
 
 		<p>

+ 14 - 0
docs/api/zh/math/Sphere.html

@@ -69,6 +69,13 @@
 		若这个点,则距离将为负值。
 		</p>
 
+		<h3>[method:this expandByPoint]( [param:Vector3 point] )</h3>
+		<p>
+		[page:Vector3 point] - [page:Vector3] that should be included in the sphere.<br /><br />
+
+		Expands the boundaries of this sphere to include [page:Vector3 point].
+		</p>
+
 		<h3>[method:Boolean isEmpty]()</h3>
 		<p>
 		检查球是否为空(the radius set to a negative number).
@@ -134,6 +141,13 @@
 		使用所给定[page:Vector3] offset(偏移量)平移球心。
 		</p>
 
+		<h3>[method:this union]( [param:Sphere sphere] )</h3>
+		<p>
+		[page:Sphere sphere] - Bounding sphere that will be unioned with this sphere.<br /><br />
+
+		Expands this sphere to enclose both the original sphere and the given sphere.
+		</p>
+
 		<h2>源代码</h2>
 
 		<p>

+ 46 - 0
src/math/Sphere.js

@@ -2,6 +2,9 @@ import { Box3 } from './Box3.js';
 import { Vector3 } from './Vector3.js';
 
 const _box = /*@__PURE__*/ new Box3();
+const _v1 = /*@__PURE__*/ new Vector3();
+const _toFarthestPoint = /*@__PURE__*/ new Vector3();
+const _toPoint = /*@__PURE__*/ new Vector3();
 
 class Sphere {
 
@@ -170,6 +173,49 @@ class Sphere {
 
 	}
 
+	expandByPoint( point ) {
+
+		// from https://github.com/juj/MathGeoLib/blob/2940b99b99cfe575dd45103ef20f4019dee15b54/src/Geometry/Sphere.cpp#L649-L671
+
+		_toPoint.subVectors( point, this.center );
+
+		const lengthSq = _toPoint.lengthSq();
+
+		if ( lengthSq > ( this.radius * this.radius ) ) {
+
+			const length = Math.sqrt( lengthSq );
+			const missingRadiusHalf = ( length - this.radius ) * 0.5;
+
+			// Nudge this sphere towards the target point. Add half the missing distance to radius,
+			// and the other half to position. This gives a tighter enclosure, instead of if
+			// the whole missing distance were just added to radius.
+
+			this.center.add( _toPoint.multiplyScalar( missingRadiusHalf / length ) );
+			this.radius += missingRadiusHalf;
+
+		}
+
+		return this;
+
+	}
+
+	union( sphere ) {
+
+		// from https://github.com/juj/MathGeoLib/blob/2940b99b99cfe575dd45103ef20f4019dee15b54/src/Geometry/Sphere.cpp#L759-L769
+
+		// To enclose another sphere into this sphere, we only need to enclose two points:
+		// 1) Enclose the farthest point on the other sphere into this sphere.
+		// 2) Enclose the opposite point of the farthest point into this sphere.
+
+		_toFarthestPoint.subVectors( sphere.center, this.center ).normalize().multiplyScalar( sphere.radius );
+
+		this.expandByPoint( _v1.copy( sphere.center ).add( _toFarthestPoint ) );
+		this.expandByPoint( _v1.copy( sphere.center ).sub( _toFarthestPoint ) );
+
+		return this;
+
+	}
+
 	equals( sphere ) {
 
 		return sphere.center.equals( this.center ) && ( sphere.radius === this.radius );

+ 108 - 71
test/unit/src/math/Sphere.tests.js

@@ -17,38 +17,38 @@ export default QUnit.module( 'Maths', () => {
 	QUnit.module( 'Sphere', () => {
 
 		// INSTANCING
-		QUnit.test( "Instancing", ( assert ) => {
+		QUnit.test( 'Instancing', ( assert ) => {
 
 			var a = new Sphere();
-			assert.ok( a.center.equals( zero3 ), "Passed!" );
-			assert.ok( a.radius == - 1, "Passed!" );
+			assert.ok( a.center.equals( zero3 ), 'Passed!' );
+			assert.ok( a.radius == - 1, 'Passed!' );
 
 			var a = new Sphere( one3.clone(), 1 );
-			assert.ok( a.center.equals( one3 ), "Passed!" );
-			assert.ok( a.radius == 1, "Passed!" );
+			assert.ok( a.center.equals( one3 ), 'Passed!' );
+			assert.ok( a.radius == 1, 'Passed!' );
 
 		} );
 
 		// PUBLIC STUFF
-		QUnit.todo( "isSphere", ( assert ) => {
+		QUnit.todo( 'isSphere', ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			assert.ok( false, 'everything\'s gonna be alright' );
 
 		} );
 
-		QUnit.test( "set", ( assert ) => {
+		QUnit.test( 'set', ( assert ) => {
 
 			var a = new Sphere();
-			assert.ok( a.center.equals( zero3 ), "Passed!" );
-			assert.ok( a.radius == - 1, "Passed!" );
+			assert.ok( a.center.equals( zero3 ), 'Passed!' );
+			assert.ok( a.radius == - 1, 'Passed!' );
 
 			a.set( one3, 1 );
-			assert.ok( a.center.equals( one3 ), "Passed!" );
-			assert.ok( a.radius == 1, "Passed!" );
+			assert.ok( a.center.equals( one3 ), 'Passed!' );
+			assert.ok( a.radius == 1, 'Passed!' );
 
 		} );
 
-		QUnit.test( "setFromPoints", ( assert ) => {
+		QUnit.test( 'setFromPoints', ( assert ) => {
 
 			var a = new Sphere();
 			var expectedCenter = new Vector3( 0.9330126941204071, 0, 0 );
@@ -68,160 +68,160 @@ export default QUnit.module( 'Maths', () => {
 			];
 
 			a.setFromPoints( points );
-			assert.ok( Math.abs( a.center.x - expectedCenter.x ) <= eps, "Default center: check center.x" );
-			assert.ok( Math.abs( a.center.y - expectedCenter.y ) <= eps, "Default center: check center.y" );
-			assert.ok( Math.abs( a.center.z - expectedCenter.z ) <= eps, "Default center: check center.z" );
-			assert.ok( Math.abs( a.radius - expectedRadius ) <= eps, "Default center: check radius" );
+			assert.ok( Math.abs( a.center.x - expectedCenter.x ) <= eps, 'Default center: check center.x' );
+			assert.ok( Math.abs( a.center.y - expectedCenter.y ) <= eps, 'Default center: check center.y' );
+			assert.ok( Math.abs( a.center.z - expectedCenter.z ) <= eps, 'Default center: check center.z' );
+			assert.ok( Math.abs( a.radius - expectedRadius ) <= eps, 'Default center: check radius' );
 
 			var expectedRadius = 2.5946195770400102;
 			a.setFromPoints( points, optionalCenter );
-			assert.ok( Math.abs( a.center.x - optionalCenter.x ) <= eps, "Optional center: check center.x" );
-			assert.ok( Math.abs( a.center.y - optionalCenter.y ) <= eps, "Optional center: check center.y" );
-			assert.ok( Math.abs( a.center.z - optionalCenter.z ) <= eps, "Optional center: check center.z" );
-			assert.ok( Math.abs( a.radius - expectedRadius ) <= eps, "Optional center: check radius" );
+			assert.ok( Math.abs( a.center.x - optionalCenter.x ) <= eps, 'Optional center: check center.x' );
+			assert.ok( Math.abs( a.center.y - optionalCenter.y ) <= eps, 'Optional center: check center.y' );
+			assert.ok( Math.abs( a.center.z - optionalCenter.z ) <= eps, 'Optional center: check center.z' );
+			assert.ok( Math.abs( a.radius - expectedRadius ) <= eps, 'Optional center: check radius' );
 
 		} );
 
-		QUnit.todo( "clone", ( assert ) => {
+		QUnit.todo( 'clone', ( assert ) => {
 
-			assert.ok( false, "everything's gonna be alright" );
+			assert.ok( false, 'everything\'s gonna be alright' );
 
 		} );
 
-		QUnit.test( "copy", ( assert ) => {
+		QUnit.test( 'copy', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 			var b = new Sphere().copy( a );
 
-			assert.ok( b.center.equals( one3 ), "Passed!" );
-			assert.ok( b.radius == 1, "Passed!" );
+			assert.ok( b.center.equals( one3 ), 'Passed!' );
+			assert.ok( b.radius == 1, 'Passed!' );
 
 			// ensure that it is a true copy
 			a.center = zero3;
 			a.radius = 0;
-			assert.ok( b.center.equals( one3 ), "Passed!" );
-			assert.ok( b.radius == 1, "Passed!" );
+			assert.ok( b.center.equals( one3 ), 'Passed!' );
+			assert.ok( b.radius == 1, 'Passed!' );
 
 		} );
 
-		QUnit.test( "isEmpty", ( assert ) => {
+		QUnit.test( 'isEmpty', ( assert ) => {
 
 			var a = new Sphere();
-			assert.ok( a.isEmpty(), "Passed!" );
+			assert.ok( a.isEmpty(), 'Passed!' );
 
 			a.set( one3, 1 );
-			assert.ok( ! a.isEmpty(), "Passed!" );
+			assert.ok( ! a.isEmpty(), 'Passed!' );
 
 			// Negative radius contains no points
-			a.set( one3, -1 );
-			assert.ok( a.isEmpty(), "Passed!" );
+			a.set( one3, - 1 );
+			assert.ok( a.isEmpty(), 'Passed!' );
 
 			// Zero radius contains only the center point
 			a.set( one3, 0 );
-			assert.ok( ! a.isEmpty(), "Passed!" );
+			assert.ok( ! a.isEmpty(), 'Passed!' );
 
 		} );
 
-		QUnit.test( "makeEmpty", ( assert ) => {
+		QUnit.test( 'makeEmpty', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 
-			assert.ok( ! a.isEmpty(), "Passed!" );
+			assert.ok( ! a.isEmpty(), 'Passed!' );
 
 			a.makeEmpty();
-			assert.ok( a.isEmpty(), "Passed!" );
-			assert.ok( a.center.equals( zero3 ), "Passed!" );
+			assert.ok( a.isEmpty(), 'Passed!' );
+			assert.ok( a.center.equals( zero3 ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "containsPoint", ( assert ) => {
+		QUnit.test( 'containsPoint', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 
-			assert.ok( ! a.containsPoint( zero3 ), "Passed!" );
-			assert.ok( a.containsPoint( one3 ), "Passed!" );
+			assert.ok( ! a.containsPoint( zero3 ), 'Passed!' );
+			assert.ok( a.containsPoint( one3 ), 'Passed!' );
 
 			a.set( zero3, 0 );
-			assert.ok( a.containsPoint( a.center ), "Passed!" );
+			assert.ok( a.containsPoint( a.center ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "distanceToPoint", ( assert ) => {
+		QUnit.test( 'distanceToPoint', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 
-			assert.ok( ( a.distanceToPoint( zero3 ) - 0.7320 ) < 0.001, "Passed!" );
-			assert.ok( a.distanceToPoint( one3 ) === - 1, "Passed!" );
+			assert.ok( ( a.distanceToPoint( zero3 ) - 0.7320 ) < 0.001, 'Passed!' );
+			assert.ok( a.distanceToPoint( one3 ) === - 1, 'Passed!' );
 
 		} );
 
-		QUnit.test( "intersectsSphere", ( assert ) => {
+		QUnit.test( 'intersectsSphere', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 			var b = new Sphere( zero3.clone(), 1 );
 			var c = new Sphere( zero3.clone(), 0.25 );
 
-			assert.ok( a.intersectsSphere( b ), "Passed!" );
-			assert.ok( ! a.intersectsSphere( c ), "Passed!" );
+			assert.ok( a.intersectsSphere( b ), 'Passed!' );
+			assert.ok( ! a.intersectsSphere( c ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "intersectsBox", ( assert ) => {
+		QUnit.test( 'intersectsBox', ( assert ) => {
 
 			var a = new Sphere( zero3, 1 );
 			var b = new Sphere( new Vector3( - 5, - 5, - 5 ), 1 );
 			var box = new Box3( zero3, one3 );
 
-			assert.strictEqual( a.intersectsBox( box ), true, "Check unit sphere" );
-			assert.strictEqual( b.intersectsBox( box ), false, "Check shifted sphere" );
+			assert.strictEqual( a.intersectsBox( box ), true, 'Check unit sphere' );
+			assert.strictEqual( b.intersectsBox( box ), false, 'Check shifted sphere' );
 
 		} );
 
-		QUnit.test( "intersectsPlane", ( assert ) => {
+		QUnit.test( 'intersectsPlane', ( assert ) => {
 
 			var a = new Sphere( zero3.clone(), 1 );
 			var b = new Plane( new Vector3( 0, 1, 0 ), 1 );
 			var c = new Plane( new Vector3( 0, 1, 0 ), 1.25 );
 			var d = new Plane( new Vector3( 0, - 1, 0 ), 1.25 );
 
-			assert.ok( a.intersectsPlane( b ), "Passed!" );
-			assert.ok( ! a.intersectsPlane( c ), "Passed!" );
-			assert.ok( ! a.intersectsPlane( d ), "Passed!" );
+			assert.ok( a.intersectsPlane( b ), 'Passed!' );
+			assert.ok( ! a.intersectsPlane( c ), 'Passed!' );
+			assert.ok( ! a.intersectsPlane( d ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "clampPoint", ( assert ) => {
+		QUnit.test( 'clampPoint', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 			var point = new Vector3();
 
 			a.clampPoint( new Vector3( 1, 1, 3 ), point );
-			assert.ok( point.equals( new Vector3( 1, 1, 2 ) ), "Passed!" );
+			assert.ok( point.equals( new Vector3( 1, 1, 2 ) ), 'Passed!' );
 			a.clampPoint( new Vector3( 1, 1, - 3 ), point );
-			assert.ok( point.equals( new Vector3( 1, 1, 0 ) ), "Passed!" );
+			assert.ok( point.equals( new Vector3( 1, 1, 0 ) ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "getBoundingBox", ( assert ) => {
+		QUnit.test( 'getBoundingBox', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 			var aabb = new Box3();
 
 			a.getBoundingBox( aabb );
-			assert.ok( aabb.equals( new Box3( zero3, two3 ) ), "Passed!" );
+			assert.ok( aabb.equals( new Box3( zero3, two3 ) ), 'Passed!' );
 
 			a.set( zero3, 0 );
 			a.getBoundingBox( aabb );
-			assert.ok( aabb.equals( new Box3( zero3, zero3 ) ), "Passed!" );
+			assert.ok( aabb.equals( new Box3( zero3, zero3 ) ), 'Passed!' );
 
 			// Empty sphere produces empty bounding box
 			a.makeEmpty();
 			a.getBoundingBox( aabb );
-			assert.ok( aabb.isEmpty(), "Passed!" );
+			assert.ok( aabb.isEmpty(), 'Passed!' );
 
 		} );
 
-		QUnit.test( "applyMatrix4", ( assert ) => {
+		QUnit.test( 'applyMatrix4', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 			var m = new Matrix4().makeTranslation( 1, - 2, 1 );
@@ -231,31 +231,68 @@ export default QUnit.module( 'Maths', () => {
 			a.clone().applyMatrix4( m ).getBoundingBox( aabb1 );
 			a.getBoundingBox( aabb2 );
 
-			assert.ok( aabb1.equals( aabb2.applyMatrix4( m ) ), "Passed!" );
+			assert.ok( aabb1.equals( aabb2.applyMatrix4( m ) ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "translate", ( assert ) => {
+		QUnit.test( 'translate', ( assert ) => {
 
 			var a = new Sphere( one3.clone(), 1 );
 
 			a.translate( one3.clone().negate() );
-			assert.ok( a.center.equals( zero3 ), "Passed!" );
+			assert.ok( a.center.equals( zero3 ), 'Passed!' );
 
 		} );
 
-		QUnit.test( "equals", ( assert ) => {
+		QUnit.test( 'expandByPoint', ( assert ) => {
+
+			var a = new Sphere( zero3.clone(), 1 );
+			var p = new Vector3( 2, 0, 0 );
+
+			assert.ok( a.containsPoint( p ) === false, 'a does not contain p' );
+
+			a.expandByPoint( p );
+
+			assert.ok( a.containsPoint( p ) === true, 'a does contain p' );
+			assert.ok( a.center.equals( new Vector3( 0.5, 0, 0 ) ), 'Passed!' );
+			assert.ok( a.radius === 1.5, 'Passed!' );
+
+		} );
+
+		QUnit.test( 'union', ( assert ) => {
+
+			var a = new Sphere( zero3.clone(), 1 );
+			var b = new Sphere( new Vector3( 2, 0, 0 ), 1 );
+
+			a.union( b );
+
+			assert.ok( a.center.equals( new Vector3( 1, 0, 0 ) ), 'Passed!' );
+			assert.ok( a.radius === 2, 'Passed!' );
+
+			// d contains c (demonstrates why it is necessary to process two points in union)
+
+			var c = new Sphere( new Vector3(), 1 );
+			var d = new Sphere( new Vector3( 1, 0, 0 ), 4 );
+
+			c.union( d );
+
+			assert.ok( c.center.equals( new Vector3( 1, 0, 0 ) ), 'Passed!' );
+			assert.ok( c.radius === 4, 'Passed!' );
+
+		} );
+
+		QUnit.test( 'equals', ( assert ) => {
 
 			var a = new Sphere();
 			var b = new Sphere( new Vector3( 1, 0, 0 ) );
 			var c = new Sphere( new Vector3( 1, 0, 0 ), 1.0 );
 
-			assert.strictEqual( a.equals( b ), false, "a does not equal b" );
-			assert.strictEqual( a.equals( c ), false, "a does not equal c" );
-			assert.strictEqual( b.equals( c ), false, "b does not equal c" );
+			assert.strictEqual( a.equals( b ), false, 'a does not equal b' );
+			assert.strictEqual( a.equals( c ), false, 'a does not equal c' );
+			assert.strictEqual( b.equals( c ), false, 'b does not equal c' );
 
 			a.copy( b );
-			assert.strictEqual( a.equals( b ), true, "a equals b after copy()" );
+			assert.strictEqual( a.equals( b ), true, 'a equals b after copy()' );
 
 		} );