/*! * * threeoctree.js (r60) / https://github.com/collinhover/threeoctree * (sparse) dynamic 3D spatial representation structure for fast searches. * * @author Collin Hover / http://collinhover.com/ * based on Dynamic Octree by Piko3D @ http://www.piko3d.com/ and Octree by Marek Pawlowski @ pawlowski.it * */ ( function ( THREE ) { "use strict"; /*=================================================== utility =====================================================*/ function isNumber ( n ) { return !isNaN( n ) && isFinite( n ); } function isArray ( target ) { return Object.prototype.toString.call( target ) === '[object Array]'; } function toArray ( target ) { return target ? ( isArray ( target ) !== true ? [ target ] : target ) : []; } function indexOfValue( array, value ) { for ( var i = 0, il = array.length; i < il; i ++ ) { if ( array[ i ] === value ) { return i; } } return -1; } function indexOfPropertyWithValue( array, property, value ) { for ( var i = 0, il = array.length; i < il; i ++ ) { if ( array[ i ][ property ] === value ) { return i; } } return -1; } /*=================================================== octree =====================================================*/ THREE.Octree = function ( parameters ) { // handle parameters parameters = parameters || {}; parameters.tree = this; // static properties ( modification is not recommended ) this.nodeCount = 0; this.INDEX_INSIDE_CROSS = -1; this.INDEX_OUTSIDE_OFFSET = 2; this.INDEX_OUTSIDE_POS_X = isNumber( parameters.INDEX_OUTSIDE_POS_X ) ? parameters.INDEX_OUTSIDE_POS_X : 0; this.INDEX_OUTSIDE_NEG_X = isNumber( parameters.INDEX_OUTSIDE_NEG_X ) ? parameters.INDEX_OUTSIDE_NEG_X : 1; this.INDEX_OUTSIDE_POS_Y = isNumber( parameters.INDEX_OUTSIDE_POS_Y ) ? parameters.INDEX_OUTSIDE_POS_Y : 2; this.INDEX_OUTSIDE_NEG_Y = isNumber( parameters.INDEX_OUTSIDE_NEG_Y ) ? parameters.INDEX_OUTSIDE_NEG_Y : 3; this.INDEX_OUTSIDE_POS_Z = isNumber( parameters.INDEX_OUTSIDE_POS_Z ) ? parameters.INDEX_OUTSIDE_POS_Z : 4; this.INDEX_OUTSIDE_NEG_Z = isNumber( parameters.INDEX_OUTSIDE_NEG_Z ) ? parameters.INDEX_OUTSIDE_NEG_Z : 5; this.INDEX_OUTSIDE_MAP = []; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_X ] = { index: this.INDEX_OUTSIDE_POS_X, count: 0, x: 1, y: 0, z: 0 }; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_X ] = { index: this.INDEX_OUTSIDE_NEG_X, count: 0, x: -1, y: 0, z: 0 }; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_Y ] = { index: this.INDEX_OUTSIDE_POS_Y, count: 0, x: 0, y: 1, z: 0 }; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_Y ] = { index: this.INDEX_OUTSIDE_NEG_Y, count: 0, x: 0, y: -1, z: 0 }; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_POS_Z ] = { index: this.INDEX_OUTSIDE_POS_Z, count: 0, x: 0, y: 0, z: 1 }; this.INDEX_OUTSIDE_MAP[ this.INDEX_OUTSIDE_NEG_Z ] = { index: this.INDEX_OUTSIDE_NEG_Z, count: 0, x: 0, y: 0, z: -1 }; this.FLAG_POS_X = 1 << ( this.INDEX_OUTSIDE_POS_X + 1 ); this.FLAG_NEG_X = 1 << ( this.INDEX_OUTSIDE_NEG_X + 1 ); this.FLAG_POS_Y = 1 << ( this.INDEX_OUTSIDE_POS_Y + 1 ); this.FLAG_NEG_Y = 1 << ( this.INDEX_OUTSIDE_NEG_Y + 1 ); this.FLAG_POS_Z = 1 << ( this.INDEX_OUTSIDE_POS_Z + 1 ); this.FLAG_NEG_Z = 1 << ( this.INDEX_OUTSIDE_NEG_Z + 1 ); this.utilVec31Search = new THREE.Vector3(); this.utilVec32Search = new THREE.Vector3(); // pass scene to see octree structure this.scene = parameters.scene; if ( this.scene ) { this.visualGeometry = new THREE.BoxGeometry( 1, 1, 1 ); this.visualMaterial = new THREE.MeshBasicMaterial( { color: 0xFF0066, wireframe: true, wireframeLinewidth: 1 } ); } // properties this.objects = []; this.objectsMap = {}; this.objectsData = []; this.objectsDeferred = []; this.depthMax = isNumber( parameters.depthMax ) ? parameters.depthMax : Infinity; this.objectsThreshold = isNumber( parameters.objectsThreshold ) ? parameters.objectsThreshold : 8; this.overlapPct = isNumber( parameters.overlapPct ) ? parameters.overlapPct : 0.15; this.undeferred = parameters.undeferred || false; this.root = parameters.root instanceof THREE.OctreeNode ? parameters.root : new THREE.OctreeNode( parameters ); }; THREE.Octree.prototype = { update: function () { // add any deferred objects that were waiting for render cycle if ( this.objectsDeferred.length > 0 ) { for ( var i = 0, il = this.objectsDeferred.length; i < il; i ++ ) { var deferred = this.objectsDeferred[ i ]; this.addDeferred( deferred.object, deferred.options ); } this.objectsDeferred.length = 0; } }, add: function ( object, options ) { // add immediately if ( this.undeferred ) { this.updateObject( object ); this.addDeferred( object, options ); } else { // defer add until update called this.objectsDeferred.push( { object: object, options: options } ); } }, addDeferred: function ( object, options ) { var i, l, geometry, faces, useFaces, vertices, useVertices, objectData; // ensure object is not object data if ( object instanceof THREE.OctreeObjectData ) { object = object.object; } // check uuid to avoid duplicates if ( !object.uuid ) { object.uuid = THREE.Math.generateUUID(); } if ( !this.objectsMap[ object.uuid ] ) { // store this.objects.push( object ); this.objectsMap[ object.uuid ] = object; // check options if ( options ) { useFaces = options.useFaces; useVertices = options.useVertices; } if ( useVertices === true ) { geometry = object.geometry; vertices = geometry.vertices; for ( i = 0, l = vertices.length; i < l; i ++ ) { this.addObjectData( object, vertices[ i ] ); } } else if ( useFaces === true ) { geometry = object.geometry; faces = geometry.faces; for ( i = 0, l = faces.length; i < l; i ++ ) { this.addObjectData( object, faces[ i ] ); } } else { this.addObjectData( object ); } } }, addObjectData: function ( object, part ) { var objectData = new THREE.OctreeObjectData( object, part ); // add to tree objects data list this.objectsData.push( objectData ); // add to nodes this.root.addObject( objectData ); }, remove: function ( object ) { var i, l, objectData = object, index, objectsDataRemoved; // ensure object is not object data for index search if ( object instanceof THREE.OctreeObjectData ) { object = object.object; } // check uuid if ( this.objectsMap[ object.uuid ] ) { this.objectsMap[ object.uuid ] = undefined; // check and remove from objects, nodes, and data lists index = indexOfValue( this.objects, object ); if ( index !== -1 ) { this.objects.splice( index, 1 ); // remove from nodes objectsDataRemoved = this.root.removeObject( objectData ); // remove from objects data list for ( i = 0, l = objectsDataRemoved.length; i < l; i ++ ) { objectData = objectsDataRemoved[ i ]; index = indexOfValue( this.objectsData, objectData ); if ( index !== -1 ) { this.objectsData.splice( index, 1 ); } } } } else if ( this.objectsDeferred.length > 0 ) { // check and remove from deferred index = indexOfPropertyWithValue( this.objectsDeferred, 'object', object ); if ( index !== -1 ) { this.objectsDeferred.splice( index, 1 ); } } }, extend: function ( octree ) { var i, l, objectsData, objectData; if ( octree instanceof THREE.Octree ) { // for each object data objectsData = octree.objectsData; for ( i = 0, l = objectsData.length; i < l; i ++ ) { objectData = objectsData[ i ]; this.add( objectData, { useFaces: objectData.faces, useVertices: objectData.vertices } ); } } }, rebuild: function () { var i, l, node, object, objectData, indexOctant, indexOctantLast, objectsUpdate = []; // check all object data for changes in position // assumes all object matrices are up to date for ( i = 0, l = this.objectsData.length; i < l; i ++ ) { objectData = this.objectsData[ i ]; node = objectData.node; // update object objectData.update(); // if position has changed since last organization of object in tree if ( node instanceof THREE.OctreeNode && !objectData.positionLast.equals( objectData.position ) ) { // get octant index of object within current node indexOctantLast = objectData.indexOctant; indexOctant = node.getOctantIndex( objectData ); // if object octant index has changed if ( indexOctant !== indexOctantLast ) { // add to update list objectsUpdate.push( objectData ); } } } // update changed objects for ( i = 0, l = objectsUpdate.length; i < l; i ++ ) { objectData = objectsUpdate[ i ]; // remove object from current node objectData.node.removeObject( objectData ); // add object to tree root this.root.addObject( objectData ); } }, updateObject: function ( object ) { var i, l, parentCascade = [ object ], parent, parentUpdate; // search all parents between object and root for world matrix update parent = object.parent; while ( parent ) { parentCascade.push( parent ); parent = parent.parent; } for ( i = 0, l = parentCascade.length; i < l; i ++ ) { parent = parentCascade[ i ]; if ( parent.matrixWorldNeedsUpdate === true ) { parentUpdate = parent; } } // update world matrix starting at uppermost parent that needs update if ( typeof parentUpdate !== 'undefined' ) { parentUpdate.updateMatrixWorld(); } }, search: function ( position, radius, organizeByObject, direction ) { var i, l, node, objects, objectData, object, results, resultData, resultsObjectsIndices, resultObjectIndex, directionPct; // add root objects objects = [].concat( this.root.objects ); // ensure radius (i.e. distance of ray) is a number if ( !( radius > 0 ) ) { radius = Number.MAX_VALUE; } // if direction passed, normalize and find pct if ( direction instanceof THREE.Vector3 ) { direction = this.utilVec31Search.copy( direction ).normalize(); directionPct = this.utilVec32Search.set( 1, 1, 1 ).divide( direction ); } // search each node of root for ( i = 0, l = this.root.nodesIndices.length; i < l; i ++ ) { node = this.root.nodesByIndex[ this.root.nodesIndices[ i ] ]; objects = node.search( position, radius, objects, direction, directionPct ); } // if should organize results by object if ( organizeByObject === true ) { results = []; resultsObjectsIndices = []; // for each object data found for ( i = 0, l = objects.length; i < l; i ++ ) { objectData = objects[ i ]; object = objectData.object; resultObjectIndex = indexOfValue( resultsObjectsIndices, object ); // if needed, create new result data if ( resultObjectIndex === -1 ) { resultData = { object: object, faces: [], vertices: [] }; results.push( resultData ); resultsObjectsIndices.push( object ); } else { resultData = results[ resultObjectIndex ]; } // object data has faces or vertices, add to list if ( objectData.faces ) { resultData.faces.push( objectData.faces ); } else if ( objectData.vertices ) { resultData.vertices.push( objectData.vertices ); } } } else { results = objects; } return results; }, setRoot: function ( root ) { if ( root instanceof THREE.OctreeNode ) { // store new root this.root = root; // update properties this.root.updateProperties(); } }, getDepthEnd: function () { return this.root.getDepthEnd(); }, getNodeCountEnd: function () { return this.root.getNodeCountEnd(); }, getObjectCountEnd: function () { return this.root.getObjectCountEnd(); }, toConsole: function () { this.root.toConsole(); } }; /*=================================================== object data =====================================================*/ THREE.OctreeObjectData = function ( object, part ) { // properties this.object = object; // handle part by type if ( part instanceof THREE.Face3 ) { this.faces = part; this.face3 = true; this.utilVec31FaceBounds = new THREE.Vector3(); } else if ( part instanceof THREE.Vector3 ) { this.vertices = part; } this.radius = 0; this.position = new THREE.Vector3(); // initial update if ( this.object instanceof THREE.Object3D ) { this.update(); } this.positionLast = this.position.clone(); }; THREE.OctreeObjectData.prototype = { update: function () { if ( this.face3 ) { this.radius = this.getFace3BoundingRadius( this.object, this.faces ); this.position.copy( this.faces.centroid ).applyMatrix4( this.object.matrixWorld ); } else if ( this.vertices ) { this.radius = this.object.material.size || 1; this.position.copy( this.vertices ).applyMatrix4( this.object.matrixWorld ); } else { if ( this.object.geometry ) { if ( this.object.geometry.boundingSphere === null ) { this.object.geometry.computeBoundingSphere(); } this.radius = this.object.geometry.boundingSphere.radius; this.position.copy( this.object.geometry.boundingSphere.center ).applyMatrix4( this.object.matrixWorld ); } else { this.radius = this.object.boundRadius; this.position.setFromMatrixPosition( this.object.matrixWorld ); } } this.radius = this.radius * Math.max( this.object.scale.x, this.object.scale.y, this.object.scale.z ); }, getFace3BoundingRadius: function ( object, face ) { if ( face.centroid === undefined ) face.centroid = new THREE.Vector3(); var geometry = object.geometry || object, vertices = geometry.vertices, centroid = face.centroid, va = vertices[ face.a ], vb = vertices[ face.b ], vc = vertices[ face.c ], centroidToVert = this.utilVec31FaceBounds, radius; centroid.addVectors( va, vb ).add( vc ).divideScalar( 3 ); radius = Math.max( centroidToVert.subVectors( centroid, va ).length(), centroidToVert.subVectors( centroid, vb ).length(), centroidToVert.subVectors( centroid, vc ).length() ); return radius; } }; /*=================================================== node =====================================================*/ THREE.OctreeNode = function ( parameters ) { // utility this.utilVec31Branch = new THREE.Vector3(); this.utilVec31Expand = new THREE.Vector3(); this.utilVec31Ray = new THREE.Vector3(); // handle parameters parameters = parameters || {}; // store or create tree if ( parameters.tree instanceof THREE.Octree ) { this.tree = parameters.tree; } else if ( parameters.parent instanceof THREE.OctreeNode !== true ) { parameters.root = this; this.tree = new THREE.Octree( parameters ); } // basic properties this.id = this.tree.nodeCount ++; this.position = parameters.position instanceof THREE.Vector3 ? parameters.position : new THREE.Vector3(); this.radius = parameters.radius > 0 ? parameters.radius : 1; this.indexOctant = parameters.indexOctant; this.depth = 0; // reset and assign parent this.reset(); this.setParent( parameters.parent ); // additional properties this.overlap = this.radius * this.tree.overlapPct; this.radiusOverlap = this.radius + this.overlap; this.left = this.position.x - this.radiusOverlap; this.right = this.position.x + this.radiusOverlap; this.bottom = this.position.y - this.radiusOverlap; this.top = this.position.y + this.radiusOverlap; this.back = this.position.z - this.radiusOverlap; this.front = this.position.z + this.radiusOverlap; // visual if ( this.tree.scene ) { this.visual = new THREE.Mesh( this.tree.visualGeometry, this.tree.visualMaterial ); this.visual.scale.set( this.radiusOverlap * 2, this.radiusOverlap * 2, this.radiusOverlap * 2 ); this.visual.position.copy( this.position ); this.tree.scene.add( this.visual ); } }; THREE.OctreeNode.prototype = { setParent: function ( parent ) { // store new parent if ( parent !== this && this.parent !== parent ) { this.parent = parent; // update properties this.updateProperties(); } }, updateProperties: function () { var i, l; // properties if ( this.parent instanceof THREE.OctreeNode ) { this.tree = this.parent.tree; this.depth = this.parent.depth + 1; } else { this.depth = 0; } // cascade for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { this.nodesByIndex[ this.nodesIndices[ i ] ].updateProperties(); } }, reset: function ( cascade, removeVisual ) { var i, l, node, nodesIndices = this.nodesIndices || [], nodesByIndex = this.nodesByIndex; this.objects = []; this.nodesIndices = []; this.nodesByIndex = {}; // unset parent in nodes for ( i = 0, l = nodesIndices.length; i < l; i ++ ) { node = nodesByIndex[ nodesIndices[ i ] ]; node.setParent( undefined ); if ( cascade === true ) { node.reset( cascade, removeVisual ); } } // visual if ( removeVisual === true && this.visual && this.visual.parent ) { this.visual.parent.remove( this.visual ); } }, addNode: function ( node, indexOctant ) { node.indexOctant = indexOctant; if ( indexOfValue( this.nodesIndices, indexOctant ) === -1 ) { this.nodesIndices.push( indexOctant ); } this.nodesByIndex[ indexOctant ] = node; if ( node.parent !== this ) { node.setParent( this ); } }, removeNode: function ( indexOctant ) { var index, node; index = indexOfValue( this.nodesIndices, indexOctant ); this.nodesIndices.splice( index, 1 ); node = node || this.nodesByIndex[ indexOctant ]; delete this.nodesByIndex[ indexOctant ]; if ( node.parent === this ) { node.setParent( undefined ); } }, addObject: function ( object ) { var index, indexOctant, node; // get object octant index indexOctant = this.getOctantIndex( object ); // if object fully contained by an octant, add to subtree if ( indexOctant > -1 && this.nodesIndices.length > 0 ) { node = this.branch( indexOctant ); node.addObject( object ); } else if ( indexOctant < -1 && this.parent instanceof THREE.OctreeNode ) { // if object lies outside bounds, add to parent node this.parent.addObject( object ); } else { // add to this objects list index = indexOfValue( this.objects, object ); if ( index === -1 ) { this.objects.push( object ); } // node reference object.node = this; // check if need to expand, split, or both this.checkGrow(); } }, addObjectWithoutCheck: function ( objects ) { var i, l, object; for ( i = 0, l = objects.length; i < l; i ++ ) { object = objects[ i ]; this.objects.push( object ); object.node = this; } }, removeObject: function ( object ) { var i, l, nodesRemovedFrom, removeData; // cascade through tree to find and remove object removeData = this.removeObjectRecursive( object, { searchComplete: false, nodesRemovedFrom: [], objectsDataRemoved: [] } ); // if object removed, try to shrink the nodes it was removed from nodesRemovedFrom = removeData.nodesRemovedFrom; if ( nodesRemovedFrom.length > 0 ) { for ( i = 0, l = nodesRemovedFrom.length; i < l; i ++ ) { nodesRemovedFrom[ i ].shrink(); } } return removeData.objectsDataRemoved; }, removeObjectRecursive: function ( object, removeData ) { var i, l, index = -1, objectData, node, objectRemoved; // find index of object in objects list // search and remove object data (fast) if ( object instanceof THREE.OctreeObjectData ) { // remove from this objects list index = indexOfValue( this.objects, object ); if ( index !== -1 ) { this.objects.splice( index, 1 ); object.node = undefined; removeData.objectsDataRemoved.push( object ); removeData.searchComplete = objectRemoved = true; } } else { // search each object data for object and remove (slow) for ( i = this.objects.length - 1; i >= 0; i -- ) { objectData = this.objects[ i ]; if ( objectData.object === object ) { this.objects.splice( i, 1 ); objectData.node = undefined; removeData.objectsDataRemoved.push( objectData ); objectRemoved = true; if ( !objectData.faces && !objectData.vertices ) { removeData.searchComplete = true; break; } } } } // if object data removed and this is not on nodes removed from if ( objectRemoved === true ) { removeData.nodesRemovedFrom.push( this ); } // if search not complete, search nodes if ( removeData.searchComplete !== true ) { for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; // try removing object from node removeData = node.removeObjectRecursive( object, removeData ); if ( removeData.searchComplete === true ) { break; } } } return removeData; }, checkGrow: function () { // if object count above max if ( this.objects.length > this.tree.objectsThreshold && this.tree.objectsThreshold > 0 ) { this.grow(); } }, grow: function () { var indexOctant, object, objectsExpand = [], objectsExpandOctants = [], objectsSplit = [], objectsSplitOctants = [], objectsRemaining = [], i, l; // for each object for ( i = 0, l = this.objects.length; i < l; i ++ ) { object = this.objects[ i ]; // get object octant index indexOctant = this.getOctantIndex( object ); // if lies within octant if ( indexOctant > -1 ) { objectsSplit.push( object ); objectsSplitOctants.push( indexOctant ); } else if ( indexOctant < -1 ) { // lies outside radius objectsExpand.push( object ); objectsExpandOctants.push( indexOctant ); } else { // lies across bounds between octants objectsRemaining.push( object ); } } // if has objects to split if ( objectsSplit.length > 0) { objectsRemaining = objectsRemaining.concat( this.split( objectsSplit, objectsSplitOctants ) ); } // if has objects to expand if ( objectsExpand.length > 0) { objectsRemaining = objectsRemaining.concat( this.expand( objectsExpand, objectsExpandOctants ) ); } // store remaining this.objects = objectsRemaining; // merge check this.checkMerge(); }, split: function ( objects, octants ) { var i, l, indexOctant, object, node, objectsRemaining; // if not at max depth if ( this.depth < this.tree.depthMax ) { objects = objects || this.objects; octants = octants || []; objectsRemaining = []; // for each object for ( i = 0, l = objects.length; i < l; i ++ ) { object = objects[ i ]; // get object octant index indexOctant = octants[ i ]; // if object contained by octant, branch this tree if ( indexOctant > -1 ) { node = this.branch( indexOctant ); node.addObject( object ); } else { objectsRemaining.push( object ); } } // if all objects, set remaining as new objects if ( objects === this.objects ) { this.objects = objectsRemaining; } } else { objectsRemaining = this.objects; } return objectsRemaining; }, branch: function ( indexOctant ) { var node, overlap, radius, radiusOffset, offset, position; // node exists if ( this.nodesByIndex[ indexOctant ] instanceof THREE.OctreeNode ) { node = this.nodesByIndex[ indexOctant ]; } else { // properties radius = ( this.radiusOverlap ) * 0.5; overlap = radius * this.tree.overlapPct; radiusOffset = radius - overlap; offset = this.utilVec31Branch.set( indexOctant & 1 ? radiusOffset : -radiusOffset, indexOctant & 2 ? radiusOffset : -radiusOffset, indexOctant & 4 ? radiusOffset : -radiusOffset ); position = new THREE.Vector3().addVectors( this.position, offset ); // node node = new THREE.OctreeNode( { tree: this.tree, parent: this, position: position, radius: radius, indexOctant: indexOctant } ); // store this.addNode( node, indexOctant ); } return node; }, expand: function ( objects, octants ) { var i, l, object, objectsRemaining, objectsExpand, indexOctant, flagsOutside, indexOutside, indexOctantInverse, iom = this.tree.INDEX_OUTSIDE_MAP, indexOutsideCounts, infoIndexOutside1, infoIndexOutside2, infoIndexOutside3, indexOutsideBitwise1, indexOutsideBitwise2, infoPotential1, infoPotential2, infoPotential3, indexPotentialBitwise1, indexPotentialBitwise2, octantX, octantY, octantZ, overlap, radius, radiusOffset, radiusParent, overlapParent, offset = this.utilVec31Expand, position, parent; // handle max depth down tree if ( this.tree.root.getDepthEnd() < this.tree.depthMax ) { objects = objects || this.objects; octants = octants || []; objectsRemaining = []; objectsExpand = []; // reset counts for ( i = 0, l = iom.length; i < l; i ++ ) { iom[ i ].count = 0; } // for all outside objects, find outside octants containing most objects for ( i = 0, l = objects.length; i < l; i ++ ) { object = objects[ i ]; // get object octant index indexOctant = octants[ i ] ; // if object outside this, include in calculations if ( indexOctant < -1 ) { // convert octant index to outside flags flagsOutside = -indexOctant - this.tree.INDEX_OUTSIDE_OFFSET; // check against bitwise flags // x if ( flagsOutside & this.tree.FLAG_POS_X ) { iom[ this.tree.INDEX_OUTSIDE_POS_X ].count ++; } else if ( flagsOutside & this.tree.FLAG_NEG_X ) { iom[ this.tree.INDEX_OUTSIDE_NEG_X ].count ++; } // y if ( flagsOutside & this.tree.FLAG_POS_Y ) { iom[ this.tree.INDEX_OUTSIDE_POS_Y ].count ++; } else if ( flagsOutside & this.tree.FLAG_NEG_Y ) { iom[ this.tree.INDEX_OUTSIDE_NEG_Y ].count ++; } // z if ( flagsOutside & this.tree.FLAG_POS_Z ) { iom[ this.tree.INDEX_OUTSIDE_POS_Z ].count ++; } else if ( flagsOutside & this.tree.FLAG_NEG_Z ) { iom[ this.tree.INDEX_OUTSIDE_NEG_Z ].count ++; } // store in expand list objectsExpand.push( object ); } else { objectsRemaining.push( object ); } } // if objects to expand if ( objectsExpand.length > 0 ) { // shallow copy index outside map indexOutsideCounts = iom.slice( 0 ); // sort outside index count so highest is first indexOutsideCounts.sort( function ( a, b ) { return b.count - a.count; } ); // get highest outside indices // first is first infoIndexOutside1 = indexOutsideCounts[ 0 ]; indexOutsideBitwise1 = infoIndexOutside1.index | 1; // second is ( one of next two bitwise OR 1 ) that is not opposite of ( first bitwise OR 1 ) infoPotential1 = indexOutsideCounts[ 1 ]; infoPotential2 = indexOutsideCounts[ 2 ]; infoIndexOutside2 = ( infoPotential1.index | 1 ) !== indexOutsideBitwise1 ? infoPotential1 : infoPotential2; indexOutsideBitwise2 = infoIndexOutside2.index | 1; // third is ( one of next three bitwise OR 1 ) that is not opposite of ( first or second bitwise OR 1 ) infoPotential1 = indexOutsideCounts[ 2 ]; infoPotential2 = indexOutsideCounts[ 3 ]; infoPotential3 = indexOutsideCounts[ 4 ]; indexPotentialBitwise1 = infoPotential1.index | 1; indexPotentialBitwise2 = infoPotential2.index | 1; infoIndexOutside3 = indexPotentialBitwise1 !== indexOutsideBitwise1 && indexPotentialBitwise1 !== indexOutsideBitwise2 ? infoPotential1 : indexPotentialBitwise2 !== indexOutsideBitwise1 && indexPotentialBitwise2 !== indexOutsideBitwise2 ? infoPotential2 : infoPotential3; // get this octant normal based on outside octant indices octantX = infoIndexOutside1.x + infoIndexOutside2.x + infoIndexOutside3.x; octantY = infoIndexOutside1.y + infoIndexOutside2.y + infoIndexOutside3.y; octantZ = infoIndexOutside1.z + infoIndexOutside2.z + infoIndexOutside3.z; // get this octant indices based on octant normal indexOctant = this.getOctantIndexFromPosition( octantX, octantY, octantZ ); indexOctantInverse = this.getOctantIndexFromPosition( -octantX, -octantY, -octantZ ); // properties overlap = this.overlap; radius = this.radius; // radius of parent comes from reversing overlap of this, unless overlap percent is 0 radiusParent = this.tree.overlapPct > 0 ? overlap / ( ( 0.5 * this.tree.overlapPct ) * ( 1 + this.tree.overlapPct ) ) : radius * 2; overlapParent = radiusParent * this.tree.overlapPct; // parent offset is difference between radius + overlap of parent and child radiusOffset = ( radiusParent + overlapParent ) - ( radius + overlap ); offset.set( indexOctant & 1 ? radiusOffset : -radiusOffset, indexOctant & 2 ? radiusOffset : -radiusOffset, indexOctant & 4 ? radiusOffset : -radiusOffset ); position = new THREE.Vector3().addVectors( this.position, offset ); // parent parent = new THREE.OctreeNode( { tree: this.tree, position: position, radius: radiusParent } ); // set self as node of parent parent.addNode( this, indexOctantInverse ); // set parent as root this.tree.setRoot( parent ); // add all expand objects to parent for ( i = 0, l = objectsExpand.length; i < l; i ++ ) { this.tree.root.addObject( objectsExpand[ i ] ); } } // if all objects, set remaining as new objects if ( objects === this.objects ) { this.objects = objectsRemaining; } } else { objectsRemaining = objects; } return objectsRemaining; }, shrink: function () { // merge check this.checkMerge(); // contract check this.tree.root.checkContract(); }, checkMerge: function () { var nodeParent = this, nodeMerge; // traverse up tree as long as node + entire subtree's object count is under minimum while ( nodeParent.parent instanceof THREE.OctreeNode && nodeParent.getObjectCountEnd() < this.tree.objectsThreshold ) { nodeMerge = nodeParent; nodeParent = nodeParent.parent; } // if parent node is not this, merge entire subtree into merge node if ( nodeParent !== this ) { nodeParent.merge( nodeMerge ); } }, merge: function ( nodes ) { var i, l, j, k, node; // handle nodes nodes = toArray( nodes ); for ( i = 0, l = nodes.length; i < l; i ++ ) { node = nodes[ i ]; // gather node + all subtree objects this.addObjectWithoutCheck( node.getObjectsEnd() ); // reset node + entire subtree node.reset( true, true ); // remove node this.removeNode( node.indexOctant, node ); } // merge check this.checkMerge(); }, checkContract: function () { var i, l, node, nodeObjectsCount, nodeHeaviest, nodeHeaviestObjectsCount, outsideHeaviestObjectsCount; // find node with highest object count if ( this.nodesIndices.length > 0 ) { nodeHeaviestObjectsCount = 0; outsideHeaviestObjectsCount = this.objects.length; for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; nodeObjectsCount = node.getObjectCountEnd(); outsideHeaviestObjectsCount += nodeObjectsCount; if ( nodeHeaviest instanceof THREE.OctreeNode === false || nodeObjectsCount > nodeHeaviestObjectsCount ) { nodeHeaviest = node; nodeHeaviestObjectsCount = nodeObjectsCount; } } // subtract heaviest count from outside count outsideHeaviestObjectsCount -= nodeHeaviestObjectsCount; // if should contract if ( outsideHeaviestObjectsCount < this.tree.objectsThreshold && nodeHeaviest instanceof THREE.OctreeNode ) { this.contract( nodeHeaviest ); } } }, contract: function ( nodeRoot ) { var i, l, node; // handle all nodes for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; // if node is not new root if ( node !== nodeRoot ) { // add node + all subtree objects to root nodeRoot.addObjectWithoutCheck( node.getObjectsEnd() ); // reset node + entire subtree node.reset( true, true ); } } // add own objects to root nodeRoot.addObjectWithoutCheck( this.objects ); // reset self this.reset( false, true ); // set new root this.tree.setRoot( nodeRoot ); // contract check on new root nodeRoot.checkContract(); }, getOctantIndex: function ( objectData ) { var i, l, positionObj, radiusObj, position = this.position, radiusOverlap = this.radiusOverlap, overlap = this.overlap, deltaX, deltaY, deltaZ, distX, distY, distZ, distance, indexOctant = 0; // handle type if ( objectData instanceof THREE.OctreeObjectData ) { radiusObj = objectData.radius; positionObj = objectData.position; // update object data position last objectData.positionLast.copy( positionObj ); } else if ( objectData instanceof THREE.OctreeNode ) { positionObj = objectData.position; radiusObj = 0; } // find delta and distance deltaX = positionObj.x - position.x; deltaY = positionObj.y - position.y; deltaZ = positionObj.z - position.z; distX = Math.abs( deltaX ); distY = Math.abs( deltaY ); distZ = Math.abs( deltaZ ); distance = Math.max( distX, distY, distZ ); // if outside, use bitwise flags to indicate on which sides object is outside of if ( distance + radiusObj > radiusOverlap ) { // x if ( distX + radiusObj > radiusOverlap ) { indexOctant = indexOctant ^ ( deltaX > 0 ? this.tree.FLAG_POS_X : this.tree.FLAG_NEG_X ); } // y if ( distY + radiusObj > radiusOverlap ) { indexOctant = indexOctant ^ ( deltaY > 0 ? this.tree.FLAG_POS_Y : this.tree.FLAG_NEG_Y ); } // z if ( distZ + radiusObj > radiusOverlap ) { indexOctant = indexOctant ^ ( deltaZ > 0 ? this.tree.FLAG_POS_Z : this.tree.FLAG_NEG_Z ); } objectData.indexOctant = -indexOctant - this.tree.INDEX_OUTSIDE_OFFSET; return objectData.indexOctant; } // return octant index from delta xyz if ( deltaX - radiusObj > -overlap ) { // x right indexOctant = indexOctant | 1; } else if ( !( deltaX + radiusObj < overlap ) ) { // x left objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS; return objectData.indexOctant; } if ( deltaY - radiusObj > -overlap ) { // y right indexOctant = indexOctant | 2; } else if ( !( deltaY + radiusObj < overlap ) ) { // y left objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS; return objectData.indexOctant; } if ( deltaZ - radiusObj > -overlap ) { // z right indexOctant = indexOctant | 4; } else if ( !( deltaZ + radiusObj < overlap ) ) { // z left objectData.indexOctant = this.tree.INDEX_INSIDE_CROSS; return objectData.indexOctant; } objectData.indexOctant = indexOctant; return objectData.indexOctant; }, getOctantIndexFromPosition: function ( x, y, z ) { var indexOctant = 0; if ( x > 0 ) { indexOctant = indexOctant | 1; } if ( y > 0 ) { indexOctant = indexOctant | 2; } if ( z > 0 ) { indexOctant = indexOctant | 4; } return indexOctant; }, search: function ( position, radius, objects, direction, directionPct ) { var i, l, node, intersects; // test intersects by parameters if ( direction ) { intersects = this.intersectRay( position, direction, radius, directionPct ); } else { intersects = this.intersectSphere( position, radius ); } // if intersects if ( intersects === true ) { // gather objects objects = objects.concat( this.objects ); // search subtree for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; objects = node.search( position, radius, objects, direction ); } } return objects; }, intersectSphere: function ( position, radius ) { var distance = radius * radius, px = position.x, py = position.y, pz = position.z; if ( px < this.left ) { distance -= Math.pow( px - this.left, 2 ); } else if ( px > this.right ) { distance -= Math.pow( px - this.right, 2 ); } if ( py < this.bottom ) { distance -= Math.pow( py - this.bottom, 2 ); } else if ( py > this.top ) { distance -= Math.pow( py - this.top, 2 ); } if ( pz < this.back ) { distance -= Math.pow( pz - this.back, 2 ); } else if ( pz > this.front ) { distance -= Math.pow( pz - this.front, 2 ); } return distance >= 0; }, intersectRay: function ( origin, direction, distance, directionPct ) { if ( typeof directionPct === 'undefined' ) { directionPct = this.utilVec31Ray.set( 1, 1, 1 ).divide( direction ); } var t1 = ( this.left - origin.x ) * directionPct.x, t2 = ( this.right - origin.x ) * directionPct.x, t3 = ( this.bottom - origin.y ) * directionPct.y, t4 = ( this.top - origin.y ) * directionPct.y, t5 = ( this.back - origin.z ) * directionPct.z, t6 = ( this.front - origin.z ) * directionPct.z, tmax = Math.min( Math.min( Math.max( t1, t2), Math.max( t3, t4) ), Math.max( t5, t6) ), tmin; // ray would intersect in reverse direction, i.e. this is behind ray if (tmax < 0) { return false; } tmin = Math.max( Math.max( Math.min( t1, t2), Math.min( t3, t4)), Math.min( t5, t6)); // if tmin > tmax or tmin > ray distance, ray doesn't intersect AABB if ( tmin > tmax || tmin > distance ) { return false; } return true; }, getDepthEnd: function ( depth ) { var i, l, node; if ( this.nodesIndices.length > 0 ) { for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; depth = node.getDepthEnd( depth ); } } else { depth = !depth || this.depth > depth ? this.depth : depth; } return depth; }, getNodeCountEnd: function () { return this.tree.root.getNodeCountRecursive() + 1; }, getNodeCountRecursive: function () { var i, l, count = this.nodesIndices.length; for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { count += this.nodesByIndex[ this.nodesIndices[ i ] ].getNodeCountRecursive(); } return count; }, getObjectsEnd: function ( objects ) { var i, l, node; objects = ( objects || [] ).concat( this.objects ); for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; objects = node.getObjectsEnd( objects ); } return objects; }, getObjectCountEnd: function () { var i, l, count = this.objects.length; for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { count += this.nodesByIndex[ this.nodesIndices[ i ] ].getObjectCountEnd(); } return count; }, getObjectCountStart: function () { var count = this.objects.length, parent = this.parent; while ( parent instanceof THREE.OctreeNode ) { count += parent.objects.length; parent = parent.parent; } return count; }, toConsole: function ( space ) { var i, l, node, spaceAddition = ' '; space = typeof space === 'string' ? space : spaceAddition; console.log( ( this.parent ? space + ' octree NODE > ' : ' octree ROOT > ' ), this, ' // id: ', this.id, ' // indexOctant: ', this.indexOctant, ' // position: ', this.position.x, this.position.y, this.position.z, ' // radius: ', this.radius, ' // depth: ', this.depth ); console.log( ( this.parent ? space + ' ' : ' ' ), '+ objects ( ', this.objects.length, ' ) ', this.objects ); console.log( ( this.parent ? space + ' ' : ' ' ), '+ children ( ', this.nodesIndices.length, ' )', this.nodesIndices, this.nodesByIndex ); for ( i = 0, l = this.nodesIndices.length; i < l; i ++ ) { node = this.nodesByIndex[ this.nodesIndices[ i ] ]; node.toConsole( space + spaceAddition ); } } }; /*=================================================== raycaster additional functionality =====================================================*/ THREE.Raycaster.prototype.intersectOctreeObject = function ( object, recursive ) { var intersects, octreeObject, facesAll, facesSearch; if ( object.object instanceof THREE.Object3D ) { octreeObject = object; object = octreeObject.object; // temporarily replace object geometry's faces with octree object faces facesSearch = octreeObject.faces; facesAll = object.geometry.faces; if ( facesSearch.length > 0 ) { object.geometry.faces = facesSearch; } // intersect intersects = this.intersectObject( object, recursive ); // revert object geometry's faces if ( facesSearch.length > 0 ) { object.geometry.faces = facesAll; } } else { intersects = this.intersectObject( object, recursive ); } return intersects; }; THREE.Raycaster.prototype.intersectOctreeObjects = function ( objects, recursive ) { var i, il, intersects = []; for ( i = 0, il = objects.length; i < il; i ++ ) { intersects = intersects.concat( this.intersectOctreeObject( objects[ i ], recursive ) ); } return intersects; }; }( THREE ) );