Pārlūkot izejas kodu

Merge pull request #20836 from supereggbert/octree-collision

Simple collision example
Mr.doob 4 gadi atpakaļ
vecāks
revīzija
22d07c8ee7

+ 2 - 1
examples/files.json

@@ -384,7 +384,8 @@
 		"misc_exporter_ply",
 		"misc_exporter_stl",
 		"misc_legacy",
-		"misc_lookat"
+		"misc_lookat",
+		"misc_octree_collisions"
 	],
 	"css2d": [
 		"css2d_label"

+ 137 - 0
examples/js/math/Capsule.js

@@ -0,0 +1,137 @@
+
+THREE.Capsule = ( function () {
+
+	var _v1 = new THREE.Vector3();
+	var _v2 = new THREE.Vector3();
+	var _v3 = new THREE.Vector3();
+
+	var EPS = 1e-10;
+
+	function Capsule( start, end, radius ) {
+
+		this.start = start == undefined ? new THREE.Vector3( 0, 0, 0 ) : start;
+		this.end = end == undefined ? new THREE.Vector3( 0, 1, 0 ) : end;
+		this.radius = radius == undefined ? 1 : radius;
+
+	}
+
+	Object.assign( Capsule.prototype, {
+
+		clone: function () {
+
+			return new Capsule( this.start.clone(), this.end.clone(), this.radius );
+
+		},
+
+		set: function ( start, end, radius ) {
+
+			this.start.copy( start );
+			this.end.copy( end );
+			this.radius = radius;
+
+		},
+
+		copy: function ( capsule ) {
+
+			this.start.copy( capsule.start );
+			this.end.copy( capsule.end );
+			this.radius = capsule.radius;
+
+		},
+
+		getCenter: function ( target ) {
+
+			return target.copy( this.end ).add( this.start ).multiplyScalar( 0.5 );
+
+		},
+
+		translate: function ( v ) {
+
+			this.start.add( v );
+			this.end.add( v );
+
+		},
+
+		checkAABBAxis: function ( p1x, p1y, p2x, p2y, minx, maxx, miny, maxy, radius ) {
+
+			return (
+				( minx - p1x < radius || minx - p2x < radius ) &&
+				( p1x - maxx < radius || p2x - maxx < radius ) &&
+				( miny - p1y < radius || miny - p2y < radius ) &&
+				( p1y - maxy < radius || p2y - maxy < radius )
+			);
+
+		},
+
+		intersectsBox: function ( box ) {
+
+			return (
+				this.checkAABBAxis(
+					this.start.x, this.start.y, this.end.x, this.end.y,
+					box.min.x, box.max.x, box.min.y, box.max.y,
+					this.radius ) &&
+				this.checkAABBAxis(
+					this.start.x, this.start.z, this.end.x, this.end.z,
+					box.min.x, box.max.x, box.min.z, box.max.z,
+					this.radius ) &&
+				this.checkAABBAxis(
+					this.start.y, this.start.z, this.end.y, this.end.z,
+					box.min.y, box.max.y, box.min.z, box.max.z,
+					this.radius )
+			);
+
+		},
+
+		lineLineMinimumPoints: function ( line1, line2 ) {
+
+			var r = _v1.copy( line1.end ).sub( line1.start );
+			var s = _v2.copy( line2.end ).sub( line2.start );
+			var w = _v3.copy( line2.start ).sub( line1.start );
+
+			var a = r.dot( s ),
+				b = r.dot( r ),
+				c = s.dot( s ),
+				d = s.dot( w ),
+				e = r.dot( w );
+
+			var t1, t2, divisor = b * c - a * a;
+
+			if ( Math.abs( divisor ) < EPS ) {
+
+				var d1 = - d / c;
+				var d2 = ( a - d ) / c;
+
+				if ( Math.abs( d1 - 0.5 ) < Math.abs( d2 - 0.5 ) ) {
+
+					t1 = 0;
+					t2 = d1;
+
+				} else {
+
+					t1 = 1;
+					t2 = d2;
+
+				}
+
+			} else {
+
+				t1 = ( d * a + e * c ) / divisor;
+				t2 = ( t1 * a - d ) / c;
+
+			}
+
+			t2 = Math.max( 0, Math.min( 1, t2 ) );
+			t1 = Math.max( 0, Math.min( 1, t1 ) );
+
+			var point1 = r.multiplyScalar( t1 ).add( line1.start );
+			var point2 = s.multiplyScalar( t2 ).add( line2.start );
+
+			return [ point1, point2 ];
+
+		}
+
+	} );
+
+	return Capsule;
+
+} )();

+ 457 - 0
examples/js/math/Octree.js

@@ -0,0 +1,457 @@
+
+THREE.Octree = ( function () {
+
+	var _v1 = new THREE.Vector3();
+	var _v2 = new THREE.Vector3();
+	var _plane = new THREE.Plane();
+	var _line1 = new THREE.Line3();
+	var _line2 = new THREE.Line3();
+	var _sphere = new THREE.Sphere();
+	var _capsule = new THREE.Capsule();
+
+	function Octree( box ) {
+
+		this.triangles = [];
+		this.box = box;
+		this.subTrees = [];
+
+	}
+
+	Object.assign( Octree.prototype, {
+
+		addTriangle: function ( triangle ) {
+
+			if ( ! this.bounds ) this.bounds = new THREE.Box3();
+
+			this.bounds.min.x = Math.min( this.bounds.min.x, triangle.a.x, triangle.b.x, triangle.c.x );
+			this.bounds.min.y = Math.min( this.bounds.min.y, triangle.a.y, triangle.b.y, triangle.c.y );
+			this.bounds.min.z = Math.min( this.bounds.min.z, triangle.a.z, triangle.b.z, triangle.c.z );
+			this.bounds.max.x = Math.max( this.bounds.max.x, triangle.a.x, triangle.b.x, triangle.c.x );
+			this.bounds.max.y = Math.max( this.bounds.max.y, triangle.a.y, triangle.b.y, triangle.c.y );
+			this.bounds.max.z = Math.max( this.bounds.max.z, triangle.a.z, triangle.b.z, triangle.c.z );
+
+			this.triangles.push( triangle );
+
+			return this;
+
+		},
+
+		calcBox: function () {
+
+			this.box = this.bounds.clone();
+
+			// offset small ammount to account for regular grid
+			this.box.min.x -= 0.01;
+			this.box.min.y -= 0.01;
+			this.box.min.z -= 0.01;
+
+			return this;
+
+		},
+
+		split: function ( level ) {
+
+			if ( ! this.box ) return;
+
+			var subTrees = [],
+				halfsize = _v2.copy( this.box.max ).sub( this.box.min ).multiplyScalar( 0.5 ),
+				box, v, triangle;
+
+			for ( var x = 0; x < 2; x ++ ) {
+
+				for ( var y = 0; y < 2; y ++ ) {
+
+					for ( var z = 0; z < 2; z ++ ) {
+
+						box = new THREE.Box3();
+						v = _v1.set( x, y, z );
+
+						box.min.copy( this.box.min ).add( v.multiply( halfsize ) );
+						box.max.copy( box.min ).add( halfsize );
+
+						subTrees.push( new Octree( box ) );
+
+					}
+
+				}
+
+			}
+
+			while ( triangle = this.triangles.pop() ) {
+
+				for ( var i = 0; i < subTrees.length; i ++ ) {
+
+					if ( subTrees[ i ].box.intersectsTriangle( triangle ) ) {
+
+						subTrees[ i ].triangles.push( triangle );
+
+					}
+
+				}
+
+			}
+
+			for ( var i = 0; i < subTrees.length; i ++ ) {
+
+				var len = subTrees[ i ].triangles.length;
+
+				if ( len > 8 && level < 16 ) {
+
+					subTrees[ i ].split( level + 1 );
+
+				}
+
+				if ( len != 0 ) {
+
+					this.subTrees.push( subTrees[ i ] );
+
+				}
+
+			}
+
+			return this;
+
+		},
+
+		build: function () {
+
+			this.calcBox();
+			this.split( 0 );
+
+			return this;
+
+		},
+
+		getRayTriangles: function ( ray, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+				if ( ! ray.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getRayTriangles( ray, triangles );
+
+				}
+
+			}
+
+			return triangles;
+
+		},
+
+		triangleCapsuleIntersect: function ( capsule, triangle ) {
+
+			var point1, point2, line1, line2;
+
+			triangle.getPlane( _plane );
+
+			var d1 = _plane.distanceToPoint( capsule.start ) - capsule.radius;
+			var d2 = _plane.distanceToPoint( capsule.end ) - capsule.radius;
+
+			if ( ( d1 > 0 && d2 > 0 ) || ( d1 < - capsule.radius && d2 < - capsule.radius ) ) {
+
+				return false;
+
+			}
+
+			var delta = Math.abs( d1 / ( Math.abs( d1 ) + Math.abs( d2 ) ) );
+			var intersectPoint = _v1.copy( capsule.start ).lerp( capsule.end, delta );
+
+			if ( triangle.containsPoint( intersectPoint ) ) {
+
+				return { normal: _plane.normal.clone(), point: intersectPoint.clone(), depth: Math.abs( Math.min( d1, d2 ) ) };
+
+			}
+
+			var r2 = capsule.radius * capsule.radius;
+
+			line1 = _line1.set( capsule.start, capsule.end );
+
+			var lines = [
+				[ triangle.a, triangle.b ],
+				[ triangle.b, triangle.c ],
+				[ triangle.c, triangle.a ]
+			];
+
+			for ( var i = 0; i < lines.length; i ++ ) {
+
+				line2 = _line2.set( lines[ i ][ 0 ], lines[ i ][ 1 ] );
+
+				[ point1, point2 ] = capsule.lineLineMinimumPoints( line1, line2 );
+
+				if ( point1.distanceToSquared( point2 ) < r2 ) {
+
+					return { normal: point1.clone().sub( point2 ).normalize(), point: point2.clone(), depth: capsule.radius - point1.distanceTo( point2 ) };
+
+				}
+
+			}
+
+			return false;
+
+		},
+
+		triangleSphereIntersect: function ( sphere, triangle ) {
+
+			triangle.getPlane( _plane );
+
+			if ( ! sphere.intersectsPlane( _plane ) ) return false;
+
+			var depth = Math.abs( _plane.distanceToSphere( sphere ) );
+			var r2 = sphere.radius * sphere.radius - depth * depth;
+
+			var plainPoint = _plane.projectPoint( sphere.center, _v1 );
+
+			if ( triangle.containsPoint( sphere.center ) ) {
+
+				return { normal: _plane.normal.clone(), point: plainPoint.clone(), depth: Math.abs( _plane.distanceToSphere( sphere ) ) };
+
+			}
+
+			var lines = [
+				[ triangle.a, triangle.b ],
+				[ triangle.b, triangle.c ],
+				[ triangle.c, triangle.a ]
+			];
+
+			for ( var i = 0; i < lines.length; i ++ ) {
+
+				_line1.set( lines[ i ][ 0 ], lines[ i ][ 1 ] );
+				_line1.closestPointToPoint( plainPoint, true, _v2 );
+
+				var d = _v2.distanceToSquared( sphere.center );
+
+				if ( d < r2 ) {
+
+					return { normal: sphere.center.clone().sub( _v2 ).normalize(), point: _v2.clone(), depth: sphere.radius - Math.sqrt( d ) };
+
+				}
+
+			}
+
+			return false;
+
+		},
+
+		getSphereTriangles: function ( sphere, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+
+				if ( ! sphere.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getSphereTriangles( sphere, triangles );
+
+				}
+
+			}
+
+		},
+
+		getCapsuleTriangles: function ( capsule, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+
+				if ( ! capsule.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getCapsuleTriangles( capsule, triangles );
+
+				}
+
+			}
+
+		},
+
+		sphereIntersect( sphere ) {
+
+			_sphere.copy( sphere );
+
+			var triangles = [], result, hit = false;
+
+			this.getSphereTriangles( sphere, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				if ( result = this.triangleSphereIntersect( _sphere, triangles[ i ] ) ) {
+
+					hit = true;
+
+					_sphere.center.add( result.normal.multiplyScalar( result.depth ) );
+
+				}
+
+			}
+
+			if ( hit ) {
+
+				var collisionVector = _sphere.center.clone().sub( sphere.center );
+				var depth = collisionVector.length();
+
+				return { normal: collisionVector.normalize(), depth: depth };
+
+			}
+
+			return false;
+
+		},
+
+		capsuleIntersect: function ( capsule ) {
+
+			_capsule.copy( capsule );
+
+			var triangles = [], result, hit = false;
+
+			this.getCapsuleTriangles( _capsule, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				if ( result = this.triangleCapsuleIntersect( _capsule, triangles[ i ] ) ) {
+
+					hit = true;
+
+					_capsule.translate( result.normal.multiplyScalar( result.depth ) );
+
+				}
+
+			}
+
+			if ( hit ) {
+
+				var collisionVector = _capsule.getCenter( new THREE.Vector3() ).sub( capsule.getCenter( _v1 ) );
+				var depth = collisionVector.length();
+
+				return { normal: collisionVector.normalize(), depth: depth };
+
+			}
+
+			return false;
+
+		},
+
+		rayIntersect: function ( ray ) {
+
+			if ( ray.direction.length() === 0 ) return;
+
+			var triangles = [], triangle, position,
+				distance = 1e100,
+				result;
+
+			this.getRayTriangles( ray, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				result = ray.intersectTriangle( triangles[ i ].a, triangles[ i ].b, triangles[ i ].c, true, _v1 );
+
+				if ( result ) {
+
+					var newdistance = result.sub( ray.origin ).length();
+
+					if ( distance > newdistance ) {
+
+						position = result.clone().add( ray.origin );
+						distance = newdistance;
+						triangle = triangles[ i ];
+
+					}
+
+				}
+
+			}
+
+			return distance < 1e100 ? { distance: distance, triangle: triangle, position: position } : false;
+
+		},
+
+		fromGraphNode: function ( group ) {
+
+			group.traverse( ( obj ) => {
+
+				if ( obj.type === 'Mesh' ) {
+
+					obj.updateMatrix();
+					obj.updateWorldMatrix();
+
+					var geometry, isTemp = false;
+
+					if ( obj.geometry.index ) {
+
+						isTemp = true;
+						geometry = obj.geometry.clone().toNonIndexed();
+
+					} else {
+
+						geometry = obj.geometry;
+
+					}
+
+					var positions = geometry.attributes.position.array;
+					var transform = obj.matrixWorld;
+
+					for ( var i = 0; i < positions.length; i += 9 ) {
+
+						var v1 = new THREE.Vector3( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );
+						var v2 = new THREE.Vector3( positions[ i + 3 ], positions[ i + 4 ], positions[ i + 5 ] );
+						var v3 = new THREE.Vector3( positions[ i + 6 ], positions[ i + 7 ], positions[ i + 8 ] );
+
+						v1.applyMatrix4( transform );
+						v2.applyMatrix4( transform );
+						v3.applyMatrix4( transform );
+
+						this.addTriangle( new THREE.Triangle( v1, v2, v3 ) );
+
+					}
+
+					if ( isTemp ) {
+
+						geometry.dispose();
+
+					}
+
+				}
+
+			} );
+
+			this.build();
+
+			return this;
+
+		}
+
+	} );
+
+	return Octree;
+
+} )();

+ 23 - 0
examples/jsm/math/Capsule.d.ts

@@ -0,0 +1,23 @@
+import {
+	Vector3,
+	Line3,
+	Box3
+} from '../../../src/Three';
+
+export class Capsule {
+
+	constructor( start?: Vector3, end?: Vector3, radius?: number );
+	start: Vector3;
+	end: Vector3;
+	radius: number;
+
+	set( start: Vector3, end: Vector3, radius: number ): this;
+	clone(): Capsule;
+	copy( capsule: Capsule ): this;
+	getCenter( target: number ): Vector3;
+	translate( v: Vector3 ): this;
+	checkAABBAxis( p1x: number, p1y: number, p2x: number, p2y: number, minx: number, maxx: number, miny: number, maxy: number, radius: number ): boolean;
+	intersectsBox( box: Box3 ): boolean;
+	lineLineMinimumPoints( line1: Line3, line2: Line3 ): Vector3[];
+
+}

+ 143 - 0
examples/jsm/math/Capsule.js

@@ -0,0 +1,143 @@
+import {
+	Vector3
+} from "../../../build/three.module.js";
+
+
+var Capsule = ( function () {
+
+	var _v1 = new Vector3();
+	var _v2 = new Vector3();
+	var _v3 = new Vector3();
+
+	var EPS = 1e-10;
+
+	function Capsule( start, end, radius ) {
+
+		this.start = start == undefined ? new Vector3( 0, 0, 0 ) : start;
+		this.end = end == undefined ? new Vector3( 0, 1, 0 ) : end;
+		this.radius = radius == undefined ? 1 : radius;
+
+	}
+
+	Object.assign( Capsule.prototype, {
+
+		clone: function () {
+
+			return new Capsule( this.start.clone(), this.end.clone(), this.radius );
+
+		},
+
+		set: function ( start, end, radius ) {
+
+			this.start.copy( start );
+			this.end.copy( end );
+			this.radius = radius;
+
+		},
+
+		copy: function ( capsule ) {
+
+			this.start.copy( capsule.start );
+			this.end.copy( capsule.end );
+			this.radius = capsule.radius;
+
+		},
+
+		getCenter: function ( target ) {
+
+			return target.copy( this.end ).add( this.start ).multiplyScalar( 0.5 );
+
+		},
+
+		translate: function ( v ) {
+
+			this.start.add( v );
+			this.end.add( v );
+
+		},
+
+		checkAABBAxis: function ( p1x, p1y, p2x, p2y, minx, maxx, miny, maxy, radius ) {
+
+			return (
+				( minx - p1x < radius || minx - p2x < radius ) &&
+				( p1x - maxx < radius || p2x - maxx < radius ) &&
+				( miny - p1y < radius || miny - p2y < radius ) &&
+				( p1y - maxy < radius || p2y - maxy < radius )
+			);
+
+		},
+
+		intersectsBox: function ( box ) {
+
+			return (
+				this.checkAABBAxis(
+					this.start.x, this.start.y, this.end.x, this.end.y,
+					box.min.x, box.max.x, box.min.y, box.max.y,
+					this.radius ) &&
+				this.checkAABBAxis(
+					this.start.x, this.start.z, this.end.x, this.end.z,
+					box.min.x, box.max.x, box.min.z, box.max.z,
+					this.radius ) &&
+				this.checkAABBAxis(
+					this.start.y, this.start.z, this.end.y, this.end.z,
+					box.min.y, box.max.y, box.min.z, box.max.z,
+					this.radius )
+			);
+
+		},
+
+		lineLineMinimumPoints: function ( line1, line2 ) {
+
+			var r = _v1.copy( line1.end ).sub( line1.start );
+			var s = _v2.copy( line2.end ).sub( line2.start );
+			var w = _v3.copy( line2.start ).sub( line1.start );
+
+			var a = r.dot( s ),
+				b = r.dot( r ),
+				c = s.dot( s ),
+				d = s.dot( w ),
+				e = r.dot( w );
+
+			var t1, t2, divisor = b * c - a * a;
+
+			if ( Math.abs( divisor ) < EPS ) {
+
+				var d1 = - d / c;
+				var d2 = ( a - d ) / c;
+
+				if ( Math.abs( d1 - 0.5 ) < Math.abs( d2 - 0.5 ) ) {
+
+					t1 = 0;
+					t2 = d1;
+
+				} else {
+
+					t1 = 1;
+					t2 = d2;
+
+				}
+
+			} else {
+
+				t1 = ( d * a + e * c ) / divisor;
+				t2 = ( t1 * a - d ) / c;
+
+			}
+
+			t2 = Math.max( 0, Math.min( 1, t2 ) );
+			t1 = Math.max( 0, Math.min( 1, t1 ) );
+
+			var point1 = r.multiplyScalar( t1 ).add( line1.start );
+			var point2 = s.multiplyScalar( t2 ).add( line2.start );
+
+			return [ point1, point2 ];
+
+		}
+
+	} );
+
+	return Capsule;
+
+} )();
+
+export { Capsule };

+ 32 - 0
examples/jsm/math/Octree.d.ts

@@ -0,0 +1,32 @@
+import {
+	Triangle,
+	Box3,
+	Ray,
+	Sphere,
+	Object3D
+} from '../../../src/Three';
+
+import { Capsule } from './Capsule';
+
+export class Octree {
+
+	constructor( box?: Box3 );
+	triangles: Triangle[];
+	box: Box3;
+	subTrees: Octree[];
+
+	addTriangle( triangle: Triangle ): this;
+	calcBox(): this;
+	split( level: number ): this;
+	build(): this;
+	getRayTriangles( ray: Ray, triangles: Triangle[] ): Triangle[];
+	triangleCapsuleIntersect( capsule: Capsule, triangle: Triangle ): any;
+	triangleSphereIntersect( sphere: Sphere, triangle: Triangle ): any;
+	getSphereTriangles( sphere: Sphere, triangles: Triangle[] ): Triangle[];
+	getCapsuleTriangles( capsule: Capsule, triangles: Triangle[] ): Triangle[];
+	sphereIntersect( sphere: Sphere ): any;
+	capsuleIntersect( capsule: Capsule ): any;
+	rayIntersect( ray: Ray ): any;
+	fromGraphNode( group: Object3D ): this;
+
+}

+ 469 - 0
examples/jsm/math/Octree.js

@@ -0,0 +1,469 @@
+import {
+	Box3,
+	Line3,
+	Plane,
+	Sphere,
+	Triangle,
+	Vector3
+} from "../../../build/three.module.js";
+import { Capsule } from "../math/Capsule.js";
+
+
+var Octree = ( function () {
+
+	var _v1 = new Vector3();
+	var _v2 = new Vector3();
+	var _plane = new Plane();
+	var _line1 = new Line3();
+	var _line2 = new Line3();
+	var _sphere = new Sphere();
+	var _capsule = new Capsule();
+
+	function Octree( box ) {
+
+		this.triangles = [];
+		this.box = box;
+		this.subTrees = [];
+
+	}
+
+	Object.assign( Octree.prototype, {
+
+		addTriangle: function ( triangle ) {
+
+			if ( ! this.bounds ) this.bounds = new Box3();
+
+			this.bounds.min.x = Math.min( this.bounds.min.x, triangle.a.x, triangle.b.x, triangle.c.x );
+			this.bounds.min.y = Math.min( this.bounds.min.y, triangle.a.y, triangle.b.y, triangle.c.y );
+			this.bounds.min.z = Math.min( this.bounds.min.z, triangle.a.z, triangle.b.z, triangle.c.z );
+			this.bounds.max.x = Math.max( this.bounds.max.x, triangle.a.x, triangle.b.x, triangle.c.x );
+			this.bounds.max.y = Math.max( this.bounds.max.y, triangle.a.y, triangle.b.y, triangle.c.y );
+			this.bounds.max.z = Math.max( this.bounds.max.z, triangle.a.z, triangle.b.z, triangle.c.z );
+
+			this.triangles.push( triangle );
+
+			return this;
+
+		},
+
+		calcBox: function () {
+
+			this.box = this.bounds.clone();
+
+			// offset small ammount to account for regular grid
+			this.box.min.x -= 0.01;
+			this.box.min.y -= 0.01;
+			this.box.min.z -= 0.01;
+
+			return this;
+
+		},
+
+		split: function ( level ) {
+
+			if ( ! this.box ) return;
+
+			var subTrees = [],
+				halfsize = _v2.copy( this.box.max ).sub( this.box.min ).multiplyScalar( 0.5 ),
+				box, v, triangle;
+
+			for ( var x = 0; x < 2; x ++ ) {
+
+				for ( var y = 0; y < 2; y ++ ) {
+
+					for ( var z = 0; z < 2; z ++ ) {
+
+						box = new Box3();
+						v = _v1.set( x, y, z );
+
+						box.min.copy( this.box.min ).add( v.multiply( halfsize ) );
+						box.max.copy( box.min ).add( halfsize );
+
+						subTrees.push( new Octree( box ) );
+
+					}
+
+				}
+
+			}
+
+			while ( triangle = this.triangles.pop() ) {
+
+				for ( var i = 0; i < subTrees.length; i ++ ) {
+
+					if ( subTrees[ i ].box.intersectsTriangle( triangle ) ) {
+
+						subTrees[ i ].triangles.push( triangle );
+
+					}
+
+				}
+
+			}
+
+			for ( var i = 0; i < subTrees.length; i ++ ) {
+
+				var len = subTrees[ i ].triangles.length;
+
+				if ( len > 8 && level < 16 ) {
+
+					subTrees[ i ].split( level + 1 );
+
+				}
+
+				if ( len != 0 ) {
+
+					this.subTrees.push( subTrees[ i ] );
+
+				}
+
+			}
+
+			return this;
+
+		},
+
+		build: function () {
+
+			this.calcBox();
+			this.split( 0 );
+
+			return this;
+
+		},
+
+		getRayTriangles: function ( ray, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+				if ( ! ray.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getRayTriangles( ray, triangles );
+
+				}
+
+			}
+
+			return triangles;
+
+		},
+
+		triangleCapsuleIntersect: function ( capsule, triangle ) {
+
+			var point1, point2, line1, line2;
+
+			triangle.getPlane( _plane );
+
+			var d1 = _plane.distanceToPoint( capsule.start ) - capsule.radius;
+			var d2 = _plane.distanceToPoint( capsule.end ) - capsule.radius;
+
+			if ( ( d1 > 0 && d2 > 0 ) || ( d1 < - capsule.radius && d2 < - capsule.radius ) ) {
+
+				return false;
+
+			}
+
+			var delta = Math.abs( d1 / ( Math.abs( d1 ) + Math.abs( d2 ) ) );
+			var intersectPoint = _v1.copy( capsule.start ).lerp( capsule.end, delta );
+
+			if ( triangle.containsPoint( intersectPoint ) ) {
+
+				return { normal: _plane.normal.clone(), point: intersectPoint.clone(), depth: Math.abs( Math.min( d1, d2 ) ) };
+
+			}
+
+			var r2 = capsule.radius * capsule.radius;
+
+			line1 = _line1.set( capsule.start, capsule.end );
+
+			var lines = [
+				[ triangle.a, triangle.b ],
+				[ triangle.b, triangle.c ],
+				[ triangle.c, triangle.a ]
+			];
+
+			for ( var i = 0; i < lines.length; i ++ ) {
+
+				line2 = _line2.set( lines[ i ][ 0 ], lines[ i ][ 1 ] );
+
+				[ point1, point2 ] = capsule.lineLineMinimumPoints( line1, line2 );
+
+				if ( point1.distanceToSquared( point2 ) < r2 ) {
+
+					return { normal: point1.clone().sub( point2 ).normalize(), point: point2.clone(), depth: capsule.radius - point1.distanceTo( point2 ) };
+
+				}
+
+			}
+
+			return false;
+
+		},
+
+		triangleSphereIntersect: function ( sphere, triangle ) {
+
+			triangle.getPlane( _plane );
+
+			if ( ! sphere.intersectsPlane( _plane ) ) return false;
+
+			var depth = Math.abs( _plane.distanceToSphere( sphere ) );
+			var r2 = sphere.radius * sphere.radius - depth * depth;
+
+			var plainPoint = _plane.projectPoint( sphere.center, _v1 );
+
+			if ( triangle.containsPoint( sphere.center ) ) {
+
+				return { normal: _plane.normal.clone(), point: plainPoint.clone(), depth: Math.abs( _plane.distanceToSphere( sphere ) ) };
+
+			}
+
+			var lines = [
+				[ triangle.a, triangle.b ],
+				[ triangle.b, triangle.c ],
+				[ triangle.c, triangle.a ]
+			];
+
+			for ( var i = 0; i < lines.length; i ++ ) {
+
+				_line1.set( lines[ i ][ 0 ], lines[ i ][ 1 ] );
+				_line1.closestPointToPoint( plainPoint, true, _v2 );
+
+				var d = _v2.distanceToSquared( sphere.center );
+
+				if ( d < r2 ) {
+
+					return { normal: sphere.center.clone().sub( _v2 ).normalize(), point: _v2.clone(), depth: sphere.radius - Math.sqrt( d ) };
+
+				}
+
+			}
+
+			return false;
+
+		},
+
+		getSphereTriangles: function ( sphere, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+
+				if ( ! sphere.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getSphereTriangles( sphere, triangles );
+
+				}
+
+			}
+
+		},
+
+		getCapsuleTriangles: function ( capsule, triangles ) {
+
+			for ( var i = 0; i < this.subTrees.length; i ++ ) {
+
+				var subTree = this.subTrees[ i ];
+
+				if ( ! capsule.intersectsBox( subTree.box ) ) continue;
+
+				if ( subTree.triangles.length > 0 ) {
+
+					for ( var j = 0; j < subTree.triangles.length; j ++ ) {
+
+						if ( triangles.indexOf( subTree.triangles[ j ] ) === - 1 ) triangles.push( subTree.triangles[ j ] );
+
+					}
+
+				} else {
+
+					subTree.getCapsuleTriangles( capsule, triangles );
+
+				}
+
+			}
+
+		},
+
+		sphereIntersect( sphere ) {
+
+			_sphere.copy( sphere );
+
+			var triangles = [], result, hit = false;
+
+			this.getSphereTriangles( sphere, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				if ( result = this.triangleSphereIntersect( _sphere, triangles[ i ] ) ) {
+
+					hit = true;
+
+					_sphere.center.add( result.normal.multiplyScalar( result.depth ) );
+
+				}
+
+			}
+
+			if ( hit ) {
+
+				var collisionVector = _sphere.center.clone().sub( sphere.center );
+				var depth = collisionVector.length();
+
+				return { normal: collisionVector.normalize(), depth: depth };
+
+			}
+
+			return false;
+
+		},
+
+		capsuleIntersect: function ( capsule ) {
+
+			_capsule.copy( capsule );
+
+			var triangles = [], result, hit = false;
+
+			this.getCapsuleTriangles( _capsule, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				if ( result = this.triangleCapsuleIntersect( _capsule, triangles[ i ] ) ) {
+
+					hit = true;
+
+					_capsule.translate( result.normal.multiplyScalar( result.depth ) );
+
+				}
+
+			}
+
+			if ( hit ) {
+
+				var collisionVector = _capsule.getCenter( new Vector3() ).sub( capsule.getCenter( _v1 ) );
+				var depth = collisionVector.length();
+
+				return { normal: collisionVector.normalize(), depth: depth };
+
+			}
+
+			return false;
+
+		},
+
+		rayIntersect: function ( ray ) {
+
+			if ( ray.direction.length() === 0 ) return;
+
+			var triangles = [], triangle, position,
+				distance = 1e100,
+				result;
+
+			this.getRayTriangles( ray, triangles );
+
+			for ( var i = 0; i < triangles.length; i ++ ) {
+
+				result = ray.intersectTriangle( triangles[ i ].a, triangles[ i ].b, triangles[ i ].c, true, _v1 );
+
+				if ( result ) {
+
+					var newdistance = result.sub( ray.origin ).length();
+
+					if ( distance > newdistance ) {
+
+						position = result.clone().add( ray.origin );
+						distance = newdistance;
+						triangle = triangles[ i ];
+
+					}
+
+				}
+
+			}
+
+			return distance < 1e100 ? { distance: distance, triangle: triangle, position: position } : false;
+
+		},
+
+		fromGraphNode: function ( group ) {
+
+			group.traverse( ( obj ) => {
+
+				if ( obj.type === 'Mesh' ) {
+
+					obj.updateMatrix();
+					obj.updateWorldMatrix();
+
+					var geometry, isTemp = false;
+
+					if ( obj.geometry.index ) {
+
+						isTemp = true;
+						geometry = obj.geometry.clone().toNonIndexed();
+
+					} else {
+
+						geometry = obj.geometry;
+
+					}
+
+					var positions = geometry.attributes.position.array;
+					var transform = obj.matrixWorld;
+
+					for ( var i = 0; i < positions.length; i += 9 ) {
+
+						var v1 = new Vector3( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );
+						var v2 = new Vector3( positions[ i + 3 ], positions[ i + 4 ], positions[ i + 5 ] );
+						var v3 = new Vector3( positions[ i + 6 ], positions[ i + 7 ], positions[ i + 8 ] );
+
+						v1.applyMatrix4( transform );
+						v2.applyMatrix4( transform );
+						v3.applyMatrix4( transform );
+
+						this.addTriangle( new Triangle( v1, v2, v3 ) );
+
+					}
+
+					if ( isTemp ) {
+
+						geometry.dispose();
+
+					}
+
+				}
+
+			} );
+
+			this.build();
+
+			return this;
+
+		}
+
+	} );
+
+	return Octree;
+
+} )();
+
+export { Octree };

+ 373 - 0
examples/misc_octree_collisions.html

@@ -0,0 +1,373 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js - misc - octree collisions</title>
+		<meta charset=utf-8 />
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+		<div id="info">Octree threejs demo - basic collisions with static triangle mesh<br />WASD to move, space to jump, use mouse to throw balls and move the camera.</div>
+		<div id="container"></div>
+
+		<script type="module">
+
+			import * as THREE from '../build/three.module.js';
+
+			import Stats from './jsm/libs/stats.module.js';
+
+			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
+
+			import { Octree } from './jsm/math/Octree.js';
+			import { Capsule } from './jsm/math/Capsule.js';
+
+
+			const clock = new THREE.Clock();
+
+			const scene = new THREE.Scene();
+			scene.background = new THREE.Color( 0x88ccff );
+ 
+			const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
+			camera.rotation.order = 'YXZ';
+
+			const ambientlight = new THREE.AmbientLight( 0x6688cc );
+			scene.add( ambientlight );
+
+			const fillLight1 = new THREE.DirectionalLight( 0xff9999, 0.5 );
+			fillLight1.position.set( -1, 1, 2 );
+			scene.add( fillLight1 );
+
+			const fillLight2 = new THREE.DirectionalLight( 0x8888ff, 0.2 );
+			fillLight2.position.set( 0, - 1, 0 );
+			scene.add( fillLight2 );
+
+			const directionalLight = new THREE.DirectionalLight( 0xffffaa, 1.2 );
+			directionalLight.position.set( - 5, 25, - 1 );
+			directionalLight.castShadow = true;
+			directionalLight.shadow.camera.near = 0.01;
+			directionalLight.shadow.camera.far = 500;
+			directionalLight.shadow.camera.right = 30;
+			directionalLight.shadow.camera.left = - 30;
+			directionalLight.shadow.camera.top	= 30;
+			directionalLight.shadow.camera.bottom = - 30;
+			directionalLight.shadow.mapSize.width = 1024;
+			directionalLight.shadow.mapSize.height = 1024;
+			directionalLight.shadow.radius = 4;
+			directionalLight.shadow.bias =  - 0.00006;
+			scene.add( directionalLight );
+
+			const renderer = new THREE.WebGLRenderer( { antialias: true } );
+			renderer.setPixelRatio( 1 );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			renderer.shadowMap.enabled = true;
+			renderer.shadowMap.type = THREE.VSMShadowMap;
+
+			const container = document.getElementById( 'container' );
+
+			container.appendChild( renderer.domElement );
+
+			const stats = new Stats();
+			stats.domElement.style.position = 'absolute';
+			stats.domElement.style.top = '0px';
+
+			container.appendChild( stats.domElement );
+
+			const GRAVITY = 30;
+
+			const NUM_SPHERES = 20;
+			const SPHERE_RADIUS = 0.2;
+
+			const sphereGeometry = new THREE.SphereBufferGeometry( SPHERE_RADIUS, 32, 32 );
+			const sphereMaterial = new THREE.MeshStandardMaterial( { color: 0x888855, roughness: 0.8, metalness: 0.5 } );
+
+			const spheres = [];
+			let sphereIdx = 0;
+
+			for ( let i = 0; i < NUM_SPHERES; i ++ ) {
+
+				let sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
+				sphere.castShadow = true;
+				sphere.receiveShadow = true;
+
+				scene.add( sphere );
+
+				spheres.push( { mesh: sphere, collider: new THREE.Sphere( new THREE.Vector3( 0, - 100, 0 ), SPHERE_RADIUS ), velocity: new THREE.Vector3() } );
+
+			}
+
+			const worldOctree = new Octree();
+
+			const playerCollider = new Capsule( new THREE.Vector3( 0, 0.35, 0 ), new THREE.Vector3( 0, 1, 0 ), 0.35 );
+
+			const playerVelocity = new THREE.Vector3();
+			const playerDirection = new THREE.Vector3();
+
+			let playerOnFloor = false;
+
+			const keyStates = {};
+
+			document.addEventListener( 'keydown', ( event ) => {
+
+				keyStates[ event.code ] = true;
+
+			}, false );
+
+			document.addEventListener( 'keyup', ( event ) => {
+
+				keyStates[ event.code ] = false;
+
+			}, false );
+
+			document.addEventListener( 'mousedown', ( event ) => {
+
+				document.body.requestPointerLock();
+
+			}, false );
+
+			document.body.addEventListener( 'mousemove', ( event ) => {
+
+				if ( document.pointerLockElement === document.body ) {
+
+					camera.rotation.y -= event.movementX / 500;
+					camera.rotation.x -= event.movementY / 500;
+
+				}
+
+			}, false );
+
+			window.addEventListener( 'resize', () => {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}, false );
+
+			document.addEventListener( 'click', () => {
+
+				const sphere = spheres[ sphereIdx ];
+
+				camera.getWorldDirection( playerDirection );
+
+				sphere.collider.center.copy( playerCollider.end );
+				sphere.velocity.copy( playerDirection ).multiplyScalar( 30 );
+
+				sphereIdx = ( sphereIdx + 1 ) % spheres.length;
+
+			}, false );
+
+			function playerCollitions() {
+
+				const result = worldOctree.capsuleIntersect( playerCollider );
+
+				playerOnFloor = false;
+
+				if ( result ) {
+
+					playerOnFloor = result.normal.y > 0;
+
+					if( ! playerOnFloor ) {
+
+						playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) );
+
+					}
+
+					playerCollider.translate( result.normal.multiplyScalar( result.depth ) );
+
+				}
+
+			}
+
+			function updatePlayer( deltaTime ) {
+
+				if ( playerOnFloor ) {
+
+					const damping = Math.exp( - 3 * deltaTime ) - 1;
+					playerVelocity.addScaledVector( playerVelocity, damping );
+
+				} else {
+
+					playerVelocity.y -= GRAVITY * deltaTime;
+
+				}
+
+				const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime );
+				playerCollider.translate( deltaPosition );
+
+				playerCollitions();
+
+				camera.position.copy( playerCollider.end );
+
+			}
+
+			function spheresCollisions(){
+
+				for( let i = 0; i < spheres.length; i ++ ){
+
+					let s1 = spheres[ i ];
+
+					for( let j = i + 1; j < spheres.length; j ++ ){
+
+						let s2 = spheres[ j ];
+
+						let d2 = s1.collider.center.distanceToSquared( s2.collider.center );
+						let r = s1.collider.radius + s2.collider.radius;
+						let r2 = r * r;
+
+						if( d2 < r2 ){
+
+							let normal = s1.collider.clone().center.sub( s2.collider.center ).normalize();
+							let v1 = normal.clone().multiplyScalar( normal.dot( s1.velocity ) );
+							let v2 = normal.clone().multiplyScalar( normal.dot( s2.velocity ) );
+							s1.velocity.add(v2).sub(v1);
+							s2.velocity.add(v1).sub(v2);
+
+							var d = ( r - Math.sqrt(d2) ) / 2;
+
+							s1.collider.center.addScaledVector( normal, d );
+							s2.collider.center.addScaledVector( normal, - d );
+
+						}
+
+					}
+				}
+
+			}
+
+			function updateSpheres( deltaTime ) {
+
+				spheres.forEach( sphere =>{
+
+					sphere.collider.center.addScaledVector( sphere.velocity, deltaTime );
+
+					const result = worldOctree.sphereIntersect( sphere.collider );
+
+					if ( result ) {
+
+						sphere.velocity.addScaledVector( result.normal, - result.normal.dot( sphere.velocity ) * 1.5 );
+						sphere.collider.center.add( result.normal.multiplyScalar( result.depth ) );
+
+					} else {
+
+						sphere.velocity.y -= GRAVITY * deltaTime;
+
+					}
+
+					const damping = Math.exp( - 1.5 * deltaTime ) - 1;
+					sphere.velocity.addScaledVector( sphere.velocity, damping );
+
+					spheresCollisions();
+
+					sphere.mesh.position.copy( sphere.collider.center );
+
+				} );
+
+			}
+
+			function getForwardVector() {
+
+				camera.getWorldDirection( playerDirection );
+				playerDirection.y = 0;
+				playerDirection.normalize();
+
+				return playerDirection;
+
+			}
+
+			function getSideVector() {
+
+				camera.getWorldDirection( playerDirection );
+				playerDirection.y = 0;
+				playerDirection.normalize();
+				playerDirection.cross( camera.up );
+
+				return playerDirection;
+
+			}
+
+			function controls( deltaTime ) {
+
+				const speed = 25;
+
+				if ( playerOnFloor ) {
+
+					if ( keyStates[ 'KeyW' ] ) {
+
+						playerVelocity.add( getForwardVector().multiplyScalar( speed * deltaTime ) );
+
+					}
+
+					if ( keyStates[ 'KeyS' ] ) {
+
+						playerVelocity.add( getForwardVector().multiplyScalar( - speed * deltaTime ) );
+
+					}
+
+					if ( keyStates[ 'KeyA' ] ) {
+
+						playerVelocity.add( getSideVector().multiplyScalar( - speed * deltaTime ) );
+
+					}
+
+					if ( keyStates[ 'KeyD' ] ) {
+
+						playerVelocity.add( getSideVector().multiplyScalar( speed * deltaTime ) );
+
+					}
+
+					if ( keyStates[ 'Space' ] ) {
+
+						playerVelocity.y = 15;
+
+					}
+
+				}
+
+			}
+
+			const loader = new GLTFLoader().setPath( './models/gltf/' );
+
+			loader.load( 'collision-world.glb', ( gltf ) => {
+
+				scene.add( gltf.scene );
+
+				worldOctree.fromGraphNode( gltf.scene );
+
+				gltf.scene.traverse( child => {
+
+					if ( child.type === 'Mesh' ) {
+
+						child.castShadow = true;
+						child.receiveShadow = true;
+
+					}
+
+				} );
+
+				animate();
+
+			});
+
+			function animate() {
+
+				const deltaTime = Math.min( 0.1, clock.getDelta() );
+
+				controls( deltaTime );
+
+				updatePlayer( deltaTime );
+
+				updateSpheres( deltaTime );
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+				requestAnimationFrame( animate );
+
+			}
+
+
+		</script>
+	</body>
+</html>

BIN
examples/models/gltf/collision-world.glb


BIN
examples/screenshots/misc_octree_collisions.jpg


+ 2 - 0
utils/modularize.js

@@ -105,6 +105,8 @@ var files = [
 	{ path: 'math/ImprovedNoise.js', dependencies: [], ignoreList: [] },
 	{ path: 'math/Lut.js', dependencies: [], ignoreList: [] },
 	{ path: 'math/SimplexNoise.js', dependencies: [], ignoreList: [] },
+	{ path: 'math/Capsule.js', dependencies: [], ignoreList: [] },
+	{ path: 'math/Octree.js', dependencies: [ { name: 'Capsule', path: 'math/Capsule.js' } ], ignoreList: [] },
 
 	{ path: 'misc/ConvexObjectBreaker.js', dependencies: [ { name: 'ConvexBufferGeometry', path: 'geometries/ConvexGeometry.js' } ], ignoreList: [ 'Matrix4' ] },
 	{ path: 'misc/GPUComputationRenderer.js', dependencies: [], ignoreList: [] },