three-boids
Version:
Javascript Boid Simulation library
1 lines • 110 kB
Source Map (JSON)
{"version":3,"file":"three-boids.umd.cjs","sources":["../src/boid.config.js","../src/logic/BoidLogic.js","../src/logic/BoidController.js","../src/vision/RaySphere.js","../src/vision/RayController.js","../src/octree/OctreeNode.js","../src/octree/Octree.js","../src/Boids.js"],"sourcesContent":["export default\r\n{\r\n values:{\r\n count:1,\r\n visualRange:0.75046,\r\n protectedRange:0.38377,\r\n enviromentVision:0.5,\r\n objectAvoidFactor:1,\r\n cohesionFactor:0.00408,\r\n matchingFactor:0.06312,\r\n seperationFactor:0.19269,\r\n \r\n minSpeed:2.379,\r\n maxSpeed:5.206,\r\n wallTransparent:false,\r\n turnFactor:0.201,\r\n \r\n },\r\n vision:\r\n {\r\n count:101,\r\n rayAngleLimit:0.115,\r\n far:0.5\r\n }\r\n}","\r\nimport boidConfig from \"../boid.config\";\r\nimport * as THREE from 'three'\r\n\r\nexport default class BoidLogic\r\n{\r\n /**\r\n * \r\n * @param {int} boidCount \r\n * @param {THREE.Box3} box \r\n */\r\n constructor(boidCount,box)\r\n {\r\n this.boundingBox=box\r\n\r\n //start values\r\n this.setUpTweakableValues()\r\n\r\n //initiate\r\n this.#initBoids(boidCount)\r\n }\r\n\r\n /**\r\n * sets up the base values based on a config file\r\n */\r\n setUpTweakableValues()\r\n {\r\n this.visualRange=boidConfig.values.visualRange || defaultValue(1,\"VisualRange\")\r\n this.protectedRange=boidConfig.values.protectedRange || defaultValue(0.5,\"protectedRange\")\r\n this.cohesionFactor=boidConfig.values.cohesionFactor || defaultValue(0.0039,\"cohesionFactor\")\r\n this.matchingFactor=boidConfig.values.matchingFactor || defaultValue(0.0287,\"matchingFactor\")\r\n this.seperationFactor=boidConfig.values.seperationFactor || defaultValue(0.01395,\"seperationFactor\")\r\n this.minSpeed=boidConfig.values.minSpeed/100 || defaultValue(0.005,\"minSpeed\")\r\n this.maxSpeed=boidConfig.values.maxSpeed/100 || defaultValue(0.01,\"maxSpeed\")\r\n this.wallTransparent=boidConfig.values.wallTransparent || defaultValue(false,\"wallTransparent\")\r\n this.turnFactor=boidConfig.values.turnFactor/100 || defaultValue(0.2,\"turnFactor\")\r\n this.objectAvoidFactor=boidConfig.values.objectAvoidFactor || defaultValue(2,\"object avoid\")\r\n }\r\n\r\n /**\r\n * \r\n * @param {int} boidCount \r\n */\r\n #initBoids(boidCount)\r\n {\r\n this.boidCount=boidCount|| defaultValue(1,\"boidCount\")\r\n this.boidArray=[]\r\n this.addBoids(this.boidCount)\r\n }\r\n\r\n /**\r\n * Creates and adds new boids, with randomized aceleration and position\r\n * \r\n * @param {int} count \r\n */\r\n addBoids(count){\r\n \r\n for(let i = 0; i< count; i++)\r\n { \r\n const x= (Math.random()-0.5)*2*this.boundingBox.max.x\r\n const y= (Math.random()-0.5)*2*this.boundingBox.max.y\r\n const z= (Math.random()-0.5)*2*this.boundingBox.max.z\r\n\r\n const vx= (Math.random()-0.5)*2*this.maxSpeed\r\n const vy= (Math.random()-0.5)*2*this.maxSpeed\r\n const vz= (Math.random()-0.5)*2*this.maxSpeed\r\n this.boidArray.push(new Boid(x,y,z,vy,vx,vz))\r\n }\r\n \r\n }\r\n\r\n /**\r\n * Removes boid position references\r\n * \r\n * @param {int} count - amount of boids to remove\r\n */\r\n removeBoids(count)\r\n {\r\n while(count)\r\n {\r\n this.boidArray.pop()\r\n count--\r\n }\r\n \r\n }\r\n\r\n /**\r\n * Updates the boid positions based on other boids and environment objects\r\n * \r\n * @param {[obj]} environmentObjects - array of environment objects close to boids\r\n */\r\n update(environmentObjects,deltaTime)\r\n {\r\n const PROTECTED_RANGE_SQUARED= this.protectedRange**2\r\n const VISUAL_RANGE_SQUARED = this.visualRange**2 \r\n // const MIN_SPEED_SQUARED= this.minSpeed**2\r\n // const MAX_SPEED_SQUARED= this.maxSpeed**2\r\n // console.log(this.cohesionFactor)\r\n \r\n\r\n this.boidArray.forEach((boid,i)=>\r\n {\r\n\r\n //set up the rotation target\r\n // boid.targetX=boid.x\r\n // boid.targetY=boid.y\r\n // boid.targetZ=boid.z\r\n\r\n //zero accum variables\r\n let accum= this.#accumulatorObject()\r\n \r\n //loop through every other boid\r\n this.boidArray.forEach((otherBoid,n)=>\r\n {\r\n //compute differences in xy coords\r\n const dx= boid.position.x - otherBoid.position.x\r\n const dy= boid.position.y - otherBoid.position.y\r\n const dz= boid.position.z - otherBoid.position.z\r\n\r\n //check if they are within visual range\r\n if(Math.abs(dx)<this.visualRange && Math.abs(dy)<this.visualRange&& Math.abs(dz)<this.visualRange)\r\n {\r\n //get the distance between the two boids\r\n const distanceSquared= dx**2+dy**2+dz**2\r\n\r\n //is the distance less than the protected range\r\n if(distanceSquared< PROTECTED_RANGE_SQUARED)\r\n {\r\n //calculate the difference in x/y-coordinates to the nearfield boid\r\n const exp=(1-(distanceSquared/PROTECTED_RANGE_SQUARED))**2\r\n\r\n accum.close_dx+=dx*exp \r\n accum.close_dy+=dy*exp \r\n accum.close_dz+=dz*exp \r\n\r\n }\r\n \r\n //if its not in the protected range, is it in the visual range?\r\n else if(distanceSquared<VISUAL_RANGE_SQUARED)\r\n {\r\n const exp=(1-(distanceSquared/VISUAL_RANGE_SQUARED))**2\r\n //add other boids x/y coords and velocity variables to the accum\r\n accum.xpos_avg+=otherBoid.position.x\r\n accum.ypos_avg+=otherBoid.position.y\r\n accum.zpos_avg+=otherBoid.position.z\r\n accum.xvel_avg+=otherBoid.velocity.x*exp\r\n accum.yvel_avg+=otherBoid.velocity.y*exp\r\n accum.zvel_avg+=otherBoid.velocity.z*exp\r\n\r\n //increment number of boids in visual range\r\n accum.neighboring_boids++\r\n }\r\n }\r\n })\r\n\r\n //checks environmet objects to see if this boid is near an object\r\n if(!environmentObjects[i]){\r\n\r\n //check if there were any boids in the visual range\r\n if(accum.neighboring_boids>0)\r\n {\r\n //average the positions and velocity by number of neighboring boids\r\n \r\n accum.xpos_avg/=accum.neighboring_boids\r\n accum.ypos_avg/=accum.neighboring_boids\r\n accum.zpos_avg/=accum.neighboring_boids\r\n accum.xvel_avg/=accum.neighboring_boids\r\n accum.yvel_avg/=accum.neighboring_boids\r\n accum.zvel_avg/=accum.neighboring_boids\r\n \r\n\r\n \r\n //add cohesion and alignment factors\r\n boid.velocity.x+= (accum.xpos_avg-boid.position.x)*this.cohesionFactor\r\n boid.velocity.x+= (accum.xvel_avg-boid.velocity.x)*this.matchingFactor\r\n // console.log('cohesion factor',(accum.xpos_avg-boid.x)*this.cohesionFactor)\r\n // console.log('matching factor',(accum.xvel_avg-boid.vx)*this.matchingFactor)\r\n\r\n boid.velocity.y+= (accum.ypos_avg-boid.position.y)*this.cohesionFactor\r\n boid.velocity.y+= (accum.yvel_avg-boid.velocity.y)*this.matchingFactor\r\n\r\n boid.velocity.z+= (accum.zpos_avg-boid.position.z)*this.cohesionFactor\r\n boid.velocity.z+= (accum.zvel_avg-boid.velocity.z)*this.matchingFactor\r\n\r\n \r\n \r\n }\r\n\r\n //Add sepperation factor\r\n boid.velocity.x+= (accum.close_dx*this.seperationFactor)\r\n boid.velocity.y+= (accum.close_dy*this.seperationFactor)\r\n boid.velocity.z+= (accum.close_dz*this.seperationFactor)\r\n }\r\n\r\n //there are other objects! get out of the way\r\n else\r\n {\r\n // console.log('avoiding objects')\r\n //avoiding object\r\n const avoidObjExp=(1-environmentObjects[i].distance)**3\r\n\r\n const dx= boid.position.x - environmentObjects[i].position.x\r\n const dy= boid.position.y - environmentObjects[i].position.y\r\n const dz= boid.position.z - environmentObjects[i].position.z\r\n\r\n boid.velocity.x+= dx*avoidObjExp*this.objectAvoidFactor\r\n boid.velocity.y+= dy*avoidObjExp*this.objectAvoidFactor\r\n boid.velocity.z+= dz*avoidObjExp*this.objectAvoidFactor\r\n }\r\n\r\n \r\n\r\n \r\n //the bounding box\r\n boid=(this.wallTransparent)?this.#transparentWall(boid):this.#solidWall(boid)\r\n \r\n // calculate boids speed\r\n //NOTE can get rid of the sqrt, move this check to before each variable(environment -> seperation -> alignment -> cohesion) is added to \r\n //create heirachy with the fish\r\n const speed = Math.sqrt(boid.velocity.x**2+boid.velocity.y**2+boid.velocity.z**2)\r\n\r\n //enforce speedlimits\r\n\r\n if (speed< this.minSpeed)\r\n {\r\n boid.velocity.x= (boid.velocity.x/speed)*this.minSpeed\r\n boid.velocity.y= (boid.velocity.y/speed)*this.minSpeed\r\n boid.velocity.z= (boid.velocity.z/speed)*this.minSpeed\r\n }\r\n if (speed> this.maxSpeed)\r\n {\r\n boid.velocity.x= (boid.velocity.x/speed)*this.maxSpeed\r\n boid.velocity.y= (boid.velocity.y/speed)*this.maxSpeed\r\n boid.velocity.z= (boid.velocity.z/speed)*this.maxSpeed\r\n }\r\n\r\n //NOTE: Math.sqrt is a slow algorithm. better to use a distance/speed squared check. But I am yet to see noticable performace increases. further testing needed\r\n // const speedSquared = boid.vx**2+boid.vy**2+boid.vz**2\r\n\r\n // //enforce speedlimits\r\n \r\n // if (speedSquared< MIN_SPEED_SQUARED)\r\n // { \r\n // const speed= Math.sqrt(speedSquared)\r\n // boid.vx= (boid.vx/speed)*this.minSpeed\r\n // boid.vy= (boid.vy/speed)*this.minSpeed\r\n // boid.vz= (boid.vz/speed)*this.minSpeed\r\n // }\r\n // if (speedSquared> MAX_SPEED_SQUARED)\r\n // {\r\n // const speed= Math.sqrt(speedSquared)\r\n // boid.vx= (boid.vx/speed)*this.maxSpeed\r\n // boid.vy= (boid.vy/speed)*this.maxSpeed\r\n // boid.vz= (boid.vz/speed)*this.maxSpeed\r\n // }\r\n \r\n \r\n const currentPosition= boid.position.clone()\r\n\r\n //update positions\r\n boid.position.x+=boid.velocity.x*deltaTime\r\n boid.position.y+=boid.velocity.y*deltaTime\r\n boid.position.z+=boid.velocity.z*deltaTime\r\n\r\n //update rotation\r\n const m4=new THREE.Matrix4();\r\n m4.lookAt(currentPosition,boid.position, new THREE.Vector3(0,1,0))\r\n boid.rotationMatrix= m4 ;\r\n\r\n })\r\n }\r\n\r\n /**\r\n * An object containing relevant physics accumulations\r\n * \r\n * @returns accumulator obj\r\n */\r\n #accumulatorObject(){\r\n const accum=\r\n {\r\n xpos_avg:0, //position averages\r\n ypos_avg:0,\r\n zpos_avg:0,\r\n\r\n xvel_avg:0, //velocity averages\r\n yvel_avg:0,\r\n zvel_avg:0,\r\n\r\n neighboring_boids:0, //count of neighboring boids within visual range\r\n\r\n close_dx:0, \r\n close_dy:0,\r\n close_dz:0\r\n }\r\n return accum\r\n }\r\n\r\n //returns the main boid\r\n getMain()\r\n {\r\n return this.boidArray[0]\r\n }\r\n\r\n /**\r\n * Keeps boids within a bounding box. \r\n * Bounding box acts as a notice to turn around\r\n * \r\n * @param {obj} boid \r\n * @returns \r\n */\r\n #solidWall(boid)\r\n {\r\n\r\n if(this.boundingBox.max.y<boid.position.y) //top\r\n {\r\n // console.log(this.boundingBox)\r\n boid.velocity.y-=this.turnFactor\r\n }\r\n if(this.boundingBox.min.y>boid.position.y) //bottom\r\n {\r\n boid.velocity.y+=this.turnFactor\r\n }\r\n \r\n if(this.boundingBox.max.x<boid.position.x) //right\r\n {\r\n boid.velocity.x-=this.turnFactor\r\n }\r\n if(this.boundingBox.min.x>boid.position.x) //left\r\n {\r\n boid.velocity.x+=this.turnFactor\r\n }\r\n \r\n if(this.boundingBox.max.z<boid.position.z) //front\r\n {\r\n boid.velocity.z-=this.turnFactor\r\n }\r\n if(this.boundingBox.min.z>boid.position.z) //back\r\n {\r\n boid.velocity.z+=this.turnFactor\r\n }\r\n\r\n return boid\r\n }\r\n\r\n /**\r\n * Keeps boids within a bounding box. \r\n * Bounding box acts as 'portal'.\r\n * \r\n * @param {obj} boid \r\n * @returns \r\n */\r\n #transparentWall(boid)\r\n {\r\n if(this.boundingBox.max.y<boid.y) //top\r\n {\r\n boid.y=this.boundingBox.min.y\r\n }\r\n \r\n if(this.boundingBox.max.x<boid.x) //right\r\n {\r\n boid.x=this.boundingBox.min.x\r\n }\r\n \r\n if(this.boundingBox.min.x>boid.x) //left\r\n {\r\n boid.x=this.boundingBox.max.x\r\n }\r\n \r\n if(this.boundingBox.min.y>boid.y) //bottom\r\n {\r\n boid.y=this.boundingBox.max.y\r\n }\r\n\r\n if(this.boundingBox.max.z<boid.z) //front\r\n {\r\n boid.z=this.boundingBox.min.z\r\n }\r\n\r\n if(this.boundingBox.min.z>boid.z) //back\r\n {\r\n boid.z=this.boundingBox.max.z\r\n }\r\n\r\n return boid\r\n }\r\n\r\n}\r\n\r\nclass Boid\r\n{\r\n constructor(x,y,z,vx,vy,vz)\r\n {\r\n this.position=new THREE.Vector3(x,y,z)\r\n this.velocity=new THREE.Vector3(vx,vy,vz)\r\n this.rotationMatrix=new THREE.Matrix4()\r\n this.targetX=0\r\n this.targetY=0\r\n this.targetZ=0\r\n }\r\n}\r\n\r\nfunction defaultValue(x,name){\r\n console.log(`Defaulted on ${name}`)\r\n return x\r\n}","import BoidLogic from \"./BoidLogic\";\r\nimport boidConfig from \"../boid.config\";\r\nimport * as THREE from 'three'\r\n\r\nexport default class BoidController\r\n{\r\n\r\n /** constructor()\r\n * \r\n * \r\n * boidObject arr = setUp (boidArray)\r\n * \r\n * \r\n */\r\n constructor(count=200, box3, scene)\r\n {\r\n \r\n this.scene=scene\r\n\r\n this.boundingBox= box3\r\n this.boidMeshes= []\r\n this.boidCount=null\r\n\r\n this.#setBoidLogic(count)\r\n\r\n \r\n }\r\n\r\n //#region boids\r\n\r\n /**\r\n * inititate the boid logic\r\n * \r\n * @param {int} count \r\n */\r\n #setBoidLogic(count)\r\n {\r\n this.boidLogic=new BoidLogic(count, this.boundingBox)\r\n }\r\n\r\n getBoidArray()\r\n {\r\n return this.boidLogic.boidArray\r\n }\r\n\r\n getMainBoid()\r\n {\r\n return this.boidLogic.boidArray[0]\r\n\r\n }\r\n\r\n /**\r\n * \r\n * @param {int} count \r\n */\r\n addBoids(count)\r\n {\r\n //add new boids to boidLogic instance\r\n this.boidLogic.addBoids(count)\r\n this.changeModelCount(this.getBoidArray().length)\r\n }\r\n\r\n /**\r\n * \r\n * @param {int} count \r\n */\r\n removeBoids(count)\r\n {\r\n //remove boids from boid logic instance\r\n this.boidLogic.removeBoids(count)\r\n this.changeModelCount(this.getBoidArray().length)\r\n\r\n\r\n }\r\n //#endregion\r\n\r\n //#region utils\r\n\r\n /**\r\n * \r\n * @param {[obj]} environmenObjects \r\n */\r\n update(environmenObjects,deltaTime)\r\n {\r\n //update the logic\r\n this.boidLogic.update(environmenObjects,deltaTime)\r\n\r\n if(this.dummy)\r\n {\r\n for ( let i = 0; i< this.getBoidArray().length; i++ ) {\r\n const boid=this.getBoidArray()[i]\r\n\r\n \r\n for(let n=0; n<this.dummy.length; n++)\r\n {\r\n this.dummy[n].position.copy(boid.position)\r\n\r\n \r\n this.dummy[n].quaternion.setFromRotationMatrix(boid.rotationMatrix)\r\n this.dummy[n].updateMatrix();\r\n this.boidInstancedMesh[n].setMatrixAt( i, this.dummy[n].matrix );\r\n this.boidInstancedMesh[n].instanceMatrix.needsUpdate=true\r\n \r\n }\r\n // this.mixer.setTime(elapsedTime)\r\n // this.boidInstancedMesh[1].setMorphAt(i,this.dummy[1])\r\n }\r\n\r\n }\r\n\r\n //check if debug is active\r\n if(this.debug)\r\n { \r\n //show protected range visualization\r\n if(this.debug.protectedRange)\r\n {\r\n \r\n this.debug.protectedRange.position.copy(this.getMainBoid().position)\r\n }\r\n\r\n //show visual range visualization\r\n if(this.debug.visualRange)\r\n {\r\n this.debug.visualRange.position.copy(this.getMainBoid().position)\r\n }\r\n }\r\n }\r\n\r\n //#endregion\r\n\r\n //#region \r\n \r\n //createDummyMesh\r\n createDummyMesh(model)\r\n {\r\n this.dummy=[]\r\n // this.dummy.push(...this.createDummyMesh(model));\r\n \r\n //native mesh using geometry\r\n if(!model.scene)\r\n {\r\n this.dummy.push(model);\r\n return\r\n }\r\n //gltf model\r\n const baseMesh= this.getBaseMesh(model.scene)\r\n this.dummy.push(...baseMesh.children);\r\n return \r\n }\r\n\r\n getBaseMesh(mesh,parent)\r\n {\r\n // console.log(mesh)\r\n\r\n if(mesh.children.length<1)\r\n {\r\n return parent\r\n }\r\n parent= mesh\r\n\r\n return this.getBaseMesh(mesh.children[0],parent)\r\n }\r\n\r\n setModels(model,minScale,defaultMaterial)\r\n {\r\n // console.log(this.createDummyMesh(model))\r\n \r\n\r\n this.modelScale=minScale\r\n // const glb=model\r\n // console.log('glb',glb)\r\n this.createDummyMesh(model)\r\n\r\n\r\n //expand the boid bounding box\r\n if(!this.localBoidBoundingBox)\r\n {\r\n this.localBoidBoundingBox=new THREE.Box3(new THREE.Vector3(0,0,0),new THREE.Vector3(0,0,0))\r\n\r\n }\r\n this.dummy.forEach(obj=>{\r\n this.localBoidBoundingBox.expandByObject(obj)\r\n })\r\n \r\n this.localBoidBoundingBox.min.multiplyScalar(0.1*this.modelScale)\r\n this.localBoidBoundingBox.max.multiplyScalar(0.1*this.modelScale)\r\n \r\n\r\n this.boidInstancedMesh=[]\r\n\r\n //create instance of instanced mesh\r\n \r\n\r\n this.dummy.forEach((dummyMesh,i)=>\r\n {\r\n // const materialColor= dummyMesh.material.color\r\n let material= dummyMesh.material\r\n // new THREE.MeshLambertMaterial( {color:new THREE.Color(materialColor)} )\r\n\r\n if(defaultMaterial)\r\n {\r\n material=defaultMaterial\r\n }\r\n \r\n \r\n this.boidInstancedMesh[i] = new THREE.InstancedMesh( dummyMesh.geometry, material, this.getBoidArray().length );\r\n }\r\n )\r\n \r\n //fill instanced mesh\r\n for ( let i = 0; i < this.getBoidArray().length; i ++ ) {\r\n const boid=this.boidLogic.boidArray[i]\r\n\r\n const scale= Math.max(Math.random(),this.modelScale)\r\n\r\n for(let n=0; n<this.dummy.length; n++)\r\n {\r\n\r\n this.dummy[n].position.copy(boid.position)\r\n this.dummy[n].scale.set(0.1*scale,0.1*scale,0.1*scale)\r\n this.dummy[n].updateMatrix();\r\n\r\n this.boidInstancedMesh[n].setMatrixAt( i, this.dummy[n].matrix );\r\n\r\n }\r\n \r\n }\r\n \r\n for (let i = 0; i < this.boidInstancedMesh.length; i++) {\r\n this.scene.add( this.boidInstancedMesh[i] );\r\n }\r\n \r\n\r\n \r\n\r\n // this.mixer = new THREE.AnimationMixer( glb.scene );\r\n \r\n // const action = this.mixer.clipAction( glb.animations[ 0 ] );\r\n // // console.log(action)\r\n // action.play();\r\n \r\n }\r\n\r\n removeInstancedMesh()\r\n {\r\n this.boidInstancedMesh.forEach(obj=>{\r\n this.scene.remove(obj)\r\n obj.geometry.dispose()\r\n obj.material.dispose()\r\n })\r\n }\r\n\r\n removeDummyMesh()\r\n {\r\n this.dummy.forEach(obj=>{\r\n this.scene.remove(obj)\r\n obj.geometry.dispose()\r\n obj.material.dispose()\r\n })\r\n }\r\n\r\n changeModelCount()\r\n {\r\n this.removeInstancedMesh()\r\n \r\n \r\n \r\n this.dummy.forEach((dummyMesh,i)=>\r\n {\r\n\r\n let material= dummyMesh.material\r\n\r\n this.boidInstancedMesh[i] = new THREE.InstancedMesh( dummyMesh.geometry, material, this.getBoidArray().length );\r\n }\r\n )\r\n \r\n //fill instanced mesh\r\n for ( let i = 0; i < this.getBoidArray().length; i ++ ) {\r\n const boid=this.boidLogic.boidArray[i]\r\n \r\n const scale= Math.max(Math.random(),this.modelScale)\r\n \r\n for(let n=0; n<this.dummy.length; n++)\r\n {\r\n \r\n this.dummy[n].position.copy(boid.position)\r\n this.dummy[n].scale.set(0.1*scale,0.1*scale,0.1*scale)\r\n this.dummy[n].updateMatrix();\r\n \r\n this.boidInstancedMesh[n].setMatrixAt( i, this.dummy[n].matrix );\r\n \r\n }\r\n \r\n }\r\n \r\n for (let i = 0; i < this.boidInstancedMesh.length; i++) {\r\n this.scene.add( this.boidInstancedMesh[i] );\r\n }\r\n\r\n // console.log(this.boidInstancedMesh)\r\n\r\n // this.mixer = new THREE.AnimationMixer( glb.scene );\r\n // console.log(glb)\r\n // const action = this.mixer.clipAction( glb.animations[ 0 ] );\r\n // // console.log(action)\r\n // action.play();\r\n }\r\n\r\n changeModelMesh(newModel,minScale,defaultMaterial){\r\n this.removeDummyMesh()\r\n this.removeInstancedMesh()\r\n\r\n this.setModels(newModel,minScale,defaultMaterial)\r\n }\r\n\r\n\r\n \r\n //#endregion\r\n\r\n\r\n //#region DEBUG\r\n\r\n /**\r\n * Set up the debug panel\r\n * \r\n * @param {*} gui lil-gui instance\r\n */\r\n viewDebug(gui)\r\n {\r\n this.debug= {}\r\n // this.debug.boidCount= this.boidCount\r\n\r\n //create a gui folder\r\n this.debug.folder= gui.addFolder(\"Boids\")\r\n this.debug.boidCount= this.getBoidArray().length\r\n\r\n //bounding box visualization\r\n this.#debugSolidBorderBox()\r\n //add count tweak\r\n this.#debugCount()\r\n //parameters tweaks\r\n this.#debugValues()\r\n //boid vision visualization\r\n this.#debugVisionRange()\r\n }\r\n\r\n /** \r\n * Setup boid Count tweak\r\n */\r\n #debugCount()\r\n { \r\n\r\n this.debug.folder.add(this.debug,'boidCount').name(\"Entity Count\"). min(4).max(1000).step(4).onFinishChange((count)=>\r\n {\r\n \r\n if(count>this.getBoidArray().length)\r\n {\r\n this.addBoids(count-this.getBoidArray().length)\r\n \r\n }\r\n if(count<this.getBoidArray().length)\r\n {\r\n this.removeBoids(this.getBoidArray().length-count)\r\n\r\n }\r\n })\r\n\r\n }\r\n\r\n /** \r\n * Setup boid values tweaks\r\n */\r\n #debugValues()\r\n {\r\n \r\n \r\n this.debug.folder.add(boidConfig.values,\"objectAvoidFactor\").name(\"Object Avoid Factor\").min(0).max(4).step(0.00001).onChange((num)=>{\r\n this.boidLogic.objectAvoidFactor=num\r\n })\r\n this.debug.folder.add(boidConfig.values,\"enviromentVision\").name(\"Object Visual range\").min(0).max(2).step(0.00001)\r\n this.debug.folder.add(boidConfig.values,\"cohesionFactor\").name(\"Cohesion Factor\").min(0).max(0.05).step(0.00001).onChange((num)=>{\r\n this.boidLogic.cohesionFactor=num\r\n })\r\n this.debug.folder.add(boidConfig.values,\"matchingFactor\").name(\"Matching Factor\").min(0).max(0.1).step(0.00001).onChange((num)=>{\r\n this.boidLogic.matchingFactor=num\r\n })\r\n this.debug.folder.add(boidConfig.values,\"seperationFactor\").name(\"Seperation Factor\").min(0).max(0.5).step(0.00001).onChange((num)=>{\r\n this.boidLogic.seperationFactor=num\r\n })\r\n this.debug.folder.add(boidConfig.values,\"turnFactor\").name(\"Turn Factor\").min(0).max(1).step(0.0001).onChange((num)=>{\r\n this.boidLogic.turnFactor=num/100\r\n })\r\n this.debug.folder.add(boidConfig.values,\"minSpeed\").name(\"Min Speed\").min(0).max(10).step(0.001).onChange((num)=>{\r\n this.boidLogic.minSpeed=num/100\r\n })\r\n this.debug.folder.add(boidConfig.values,\"maxSpeed\").name(\"Max Speed\").min(0).max(10).step(0.001).onChange((num)=>{\r\n this.boidLogic.maxSpeed=num/100\r\n })\r\n \r\n }\r\n\r\n /** \r\n * set up border box tweak\r\n */\r\n #debugSolidBorderBox()\r\n {\r\n this.debug.showBoundingBox=false\r\n this.debug.folder.add(this.debug,'showBoundingBox').name(\"Show Bounding Box\").onChange(()=>\r\n {\r\n if(!this.debug.boundingBoxHelper)\r\n {\r\n this.debug.boundingBoxHelper= new THREE.Box3Helper(this.boundingBox,new THREE.Color('#ff1084'))\r\n this.scene.add(this.debug.boundingBoxHelper)\r\n }\r\n else{\r\n this.scene.remove(this.debug.boundingBoxHelper)\r\n this.debug.boundingBoxHelper.dispose()\r\n this.debug.boundingBoxHelper=null\r\n }\r\n })\r\n \r\n }\r\n\r\n /** \r\n * Setup protected range visualization\r\n */\r\n #debugProtectedRange(needsUpdate=false)\r\n {\r\n if(!needsUpdate)\r\n {\r\n const material= new THREE.MeshBasicMaterial({\r\n color:'red',\r\n opacity:0.5,\r\n transparent:true,\r\n depthWrite:false\r\n })\r\n const geometry= new THREE.SphereGeometry()\r\n\r\n this.debug.protectedRange= new THREE.Mesh(geometry,material)\r\n\r\n const updateScale= new THREE.Vector3(1,1,1).multiplyScalar(boidConfig.values.protectedRange)\r\n \r\n\r\n this.debug.protectedRange.scale.copy(updateScale)\r\n this.debug.protectedRange.position.copy(this.getMainBoid().position)\r\n\r\n this.scene.add(this.debug.protectedRange)\r\n }\r\n else\r\n {\r\n const updateScale= new THREE.Vector3(1,1,1).multiplyScalar(boidConfig.values.protectedRange)\r\n \r\n this.debug.protectedRange.scale.copy(updateScale)\r\n this.scene.add(this.debug.protectedRange)\r\n }\r\n\r\n \r\n }\r\n\r\n /** \r\n * Setup visual range visualization\r\n */\r\n #debugVisualRange(needsUpdate=false)\r\n {\r\n if(!needsUpdate)\r\n {\r\n const material= new THREE.MeshBasicMaterial({\r\n color:'#5bff33',\r\n opacity:0.5,\r\n transparent:true,\r\n depthWrite:false\r\n })\r\n const geometry= new THREE.SphereGeometry()\r\n\r\n this.debug.visualRange= new THREE.Mesh(geometry,material)\r\n\r\n const updateScale= new THREE.Vector3(1,1,1).multiplyScalar(boidConfig.values.visualRange)\r\n this.debug.visualRange.position.copy(this.getMainBoid().position)\r\n \r\n\r\n this.debug.visualRange.scale.copy(updateScale)\r\n\r\n this.scene.add(this.debug.visualRange)\r\n }\r\n else\r\n {\r\n const updateScale= new THREE.Vector3(1,1,1).multiplyScalar(boidConfig.values.visualRange)\r\n \r\n this.debug.visualRange.scale.copy(updateScale)\r\n this.scene.add(this.debug.visualRange)\r\n }\r\n\r\n \r\n }\r\n\r\n /** \r\n * Setup vision teak\r\n */\r\n #debugVisionRange()\r\n {\r\n this.debug.showProtectedRange=false\r\n this.debug.showVisualRange=false\r\n \r\n this.debug.folder.add(this.debug,'showProtectedRange').name(\"Show Protected Range\").onChange((bool)=>\r\n {\r\n if(!bool)\r\n {\r\n this.scene.remove(this.debug.protectedRange)\r\n this.debug.protectedRange.material.dispose()\r\n this.debug.protectedRange.geometry.dispose()\r\n \r\n }\r\n else\r\n {\r\n \r\n this.#debugProtectedRange()\r\n \r\n }\r\n })\r\n \r\n this.debug.folder.add(boidConfig.values,\"protectedRange\").name(\"Protected range\").min(0.1).max(2).step(0.00001).onChange((num)=>{\r\n this.boidLogic.protectedRange=num\r\n if(this.debug.protectedRange&&this.debug.showProtectedRange)\r\n {\r\n this.#debugProtectedRange(true)\r\n }\r\n })\r\n\r\n this.debug.folder.add(this.debug,'showVisualRange').name(\"Show Visual Range\").onChange((bool)=>\r\n {\r\n if(!bool)\r\n {\r\n this.scene.remove(this.debug.visualRange)\r\n this.debug.visualRange.material.dispose()\r\n this.debug.visualRange.geometry.dispose()\r\n \r\n }\r\n else\r\n {\r\n this.#debugVisualRange()\r\n }\r\n })\r\n \r\n this.debug.folder.add(boidConfig.values,\"visualRange\").name(\"Visual range\").min(0.5).max(3).step(0.00001).onChange((num)=>{\r\n this.boidLogic.visualRange=num\r\n if(this.debug.visualRange && this.debug.showVisualRange)\r\n {\r\n this.#debugVisualRange(true)\r\n }\r\n })\r\n \r\n \r\n } \r\n //#endregion\r\n\r\n}\r\n\r\n\r\n","import * as THREE from 'three'\r\nimport boidConfig from '../boid.config'\r\n\r\n\r\nexport default class RaySphere\r\n{\r\n /**\r\n * raySphere controlls the positioning and firing of rays for one object\r\n * - uses a point geometry to position targets for rays. \r\n * \r\n */\r\n constructor()\r\n {\r\n \r\n\r\n //debug\r\n this.debug={\r\n \r\n }\r\n \r\n //set up start params\r\n this.init()\r\n \r\n //targeting mesh\r\n this.pointSphere=this.setUpPointSphere()\r\n\r\n //raycaster\r\n this.rayCaster=this.setUpRayCaster()\r\n\r\n }\r\n\r\n /**\r\n * sets up start parameters\r\n */\r\n init()\r\n {\r\n this.rayCount=boidConfig.vision.count\r\n this.rayAngleLimit=boidConfig.vision.rayAngleLimit\r\n this.rayFar=boidConfig.vision.far\r\n \r\n }\r\n\r\n \r\n /**\r\n * \r\n * @returns clone of the pointsphere\r\n */\r\n getPointSphere()\r\n {\r\n return this.pointSphere.clone()\r\n }\r\n\r\n //#region sphere methods\r\n\r\n /**\r\n * updates point sphere mesh\r\n * \r\n * @returns mesh\r\n */\r\n updatePointSphere()\r\n {\r\n this.rayPositions_vec3Array=this.fibonacci_sphere_vec3()\r\n this.rayPositions_floatArray=this.toFloatArr(this.rayPositions_vec3Array)\r\n\r\n const pointsGeometry= new THREE.BufferGeometry()\r\n pointsGeometry.setAttribute('position',new THREE.BufferAttribute(this.rayPositions_floatArray,3))\r\n\r\n this.pointSphere.geometry.dispose()\r\n this.pointSphere.geometry=pointsGeometry\r\n \r\n\r\n }\r\n \r\n /**\r\n * Creates the point sphere points mesh using a bufferattribute derived from the global float array\r\n * \r\n * @returns mesh\r\n */\r\n setUpPointSphere()\r\n {\r\n this.rayPositions_vec3Array=this.fibonacci_sphere_vec3()\r\n this.rayPositions_floatArray=this.toFloatArr(this.rayPositions_vec3Array)\r\n\r\n //set up the geometry\r\n const pointsGeometry= new THREE.BufferGeometry()\r\n\r\n //get points from global float array\r\n pointsGeometry.setAttribute('position',new THREE.BufferAttribute(this.rayPositions_floatArray,3))\r\n\r\n //set up material\r\n const pointsMaterial= new THREE.PointsMaterial({\r\n color:'green',\r\n size:0.04,\r\n // sizeAttenuation:true,\r\n })\r\n\r\n //create mesh\r\n const particleMesh= new THREE.Points(pointsGeometry,pointsMaterial)\r\n\r\n //no need to add it to scene. its just for ray target positioning\r\n\r\n return particleMesh\r\n }\r\n\r\n /**\r\n * returns points placed quasi equidistantly around a sphere using the Fibonacci sphere algorithm\r\n * - radius is normalized\r\n * - cuttoff value determines the z limit for the points on the sphere\r\n * - adapted from https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere/44164075#44164075\r\n * @returns a THREE.Vector3 array\r\n */\r\n fibonacci_sphere_vec3(){\r\n\r\n const points = []\r\n const phi = Math.PI * (Math.sqrt(5)-1) //golden angle in radians\r\n\r\n for(let i=0; i<this.rayCount; i++)\r\n {\r\n \r\n let y = 1 - (i / (this.rayCount - 1)) * 2 // y goes from 1 to -1\r\n let radius = Math.sqrt(1 - y * y) // radius at y\r\n \r\n let theta = phi * i // golden angle increment\r\n \r\n let x = Math.cos(theta) * radius \r\n let z = Math.sin(theta) * radius \r\n\r\n\r\n if(z<this.rayAngleLimit){\r\n const normalizedTarget=new THREE.Vector3(x,y,z)\r\n normalizedTarget.normalize()\r\n points.push(normalizedTarget)\r\n }\r\n \r\n \r\n }\r\n \r\n return points\r\n }\r\n\r\n /**\r\n * Rotates the point sphere to match the given mesh rotation.\r\n * Returns an array of the sphere vertices shifted to the world position\r\n * \r\n * note! it may be quicker to use the boidPoistion array instead of the boidMesh\r\n * possibly dont need the mesh. may be better with just the vec3 rotation (euler)\r\n * \r\n * @param {*} mesh object mesh\r\n * @returns THREE.Vector3 Array\r\n */\r\n rotateTo(mesh)\r\n {\r\n //[x] convert to rotate with quaternion\r\n this.pointSphere.quaternion.setFromRotationMatrix(mesh.rotationMatrix)\r\n \r\n return this.toWorldVertices()\r\n }\r\n\r\n //#endregion\r\n\r\n //#region ray casting\r\n\r\n /**\r\n * sets up the raycaster.\r\n * - current layer set to 1\r\n \r\n * @returns THREE.Raycaster\r\n */\r\n setUpRayCaster()\r\n {\r\n // if(rayCastValues==undefined){rayCastValues={}}\r\n const rayCaster= new THREE.Raycaster()\r\n rayCaster.layers.set( 1 );\r\n rayCaster.far=this.rayFar\r\n rayCaster.firstHitOnly = true;\r\n\r\n return rayCaster\r\n }\r\n\r\n /**\r\n * Aims the raycaster and checks for intersections\r\n * - averages the found intersections distances and position\r\n * - normalized the final distance with the raycaster Far distance\r\n * \r\n * @param {[THREE.Vector3]} rayTargets array of vec3 directions(normalized)\r\n * @param {THREE.Vector3} origin \r\n * @returns {Object} {distance: int ,position: obj}\r\n */\r\n castRays(rayTargets, origin, environment)\r\n {\r\n \r\n //instanciate the found objects arr\r\n const objectArr=[]\r\n //instanciate accumulator\r\n const sum= {distance:0,position:{x:0,y:0,z:0}}\r\n \r\n let foundArr=[]\r\n\r\n // this.timer('castRays')\r\n //loop through targets, aim raycaster and check for intersection\r\n for(let i=0; i<rayTargets.length;i++)\r\n {\r\n //aims the raycaster\r\n this.rayCaster.set(origin,rayTargets[i])\r\n\r\n //find intersections of environment objects\r\n if(environment.length>1){\r\n foundArr=this.rayCaster.intersectObjects(environment)\r\n }\r\n\r\n else\r\n {\r\n // console.log(environment)\r\n foundArr=this.rayCaster.intersectObject(environment[0])\r\n // console.log(foundArr)\r\n }\r\n\r\n\r\n //if something was found add it to the array\r\n if(foundArr.length>0)\r\n {\r\n objectArr.push(...foundArr)\r\n // console.log(foundArr)\r\n }\r\n }\r\n\r\n\r\n //if there is something intersecting the ray\r\n if(objectArr.length>0)\r\n {\r\n\r\n //sum the values in the array\r\n for(const obj of objectArr){\r\n //[x]: ADD counter(CastRays)\r\n // this.counter('castRays')\r\n\r\n sum.distance+=obj.distance\r\n sum.position.x+=obj.point.x\r\n sum.position.z+=obj.point.z\r\n sum.position.y+=obj.point.y\r\n }\r\n \r\n //if theres more than one value average the values\r\n if(objectArr.length>1)\r\n {\r\n sum.distance/=objectArr.length\r\n sum.position.x/=objectArr.length\r\n sum.position.y/=objectArr.length\r\n sum.position.z/=objectArr.length\r\n }\r\n\r\n //normalize the distance\r\n sum.distance/=this.rayCaster.far\r\n }\r\n \r\n\r\n //return the distance, else return null\r\n return (sum.distance)?sum:null\r\n\r\n }\r\n\r\n \r\n //#endregion\r\n\r\n \r\n //#region utils\r\n\r\n /**\r\n * converts the vertices of the pointsphere mesh to world space\r\n * @returns [THREE.Vector3] array\r\n */\r\n toWorldVertices()\r\n {\r\n const positionAttribute = this.pointSphere.geometry.getAttribute( 'position' );\r\n // console.log(positionAttribute)\r\n const rotatedVerticies=[]\r\n for(let i=0; i<positionAttribute.count;i++)\r\n {\r\n //[x]: ADD counter(toWorldVertices)\r\n // this.counter('toWorldVertices')\r\n\r\n const vertex = new THREE.Vector3();\r\n vertex.fromBufferAttribute( positionAttribute, i );\r\n this.pointSphere.localToWorld( vertex );\r\n rotatedVerticies.push(vertex)\r\n \r\n }\r\n\r\n \r\n // this.timer('rotate')\r\n\r\n return rotatedVerticies\r\n }\r\n\r\n /**\r\n * Converts a THREE.Vector3 array to a Float32Array\r\n * @param {*} arr \r\n * @returns \r\n */\r\n toFloatArr(arr)\r\n {\r\n const floatArr= new Float32Array(arr.length*3)\r\n arr.forEach((vec,i)=>\r\n {\r\n //[x]: ADD counter(toFloatArr)\r\n // this.counter('toFloatArr')\r\n\r\n const i3=i*3\r\n floatArr[i3]=vec.x\r\n floatArr[i3+1]=vec.y\r\n floatArr[i3+2]=vec.z\r\n })\r\n return floatArr\r\n }\r\n\r\n /**\r\n * Converts a Float32Array to a THREE.Vector3 array\r\n * @param {*} arr \r\n * @returns \r\n */\r\n toVec3Arr(arr)\r\n {\r\n \r\n const vec3Arr=[]\r\n for (let i = 0; i < arr.length/3; i++) {\r\n //[x]: ADD counter(toVec3Arr)\r\n // this.counter('toVec3Arr')\r\n\r\n const i3=i*3\r\n const vec= new THREE.Vector3(\r\n arr[i3],//x\r\n arr[i3+1],//y\r\n arr[i3+2]//z\r\n )\r\n // vec.normalize()\r\n vec3Arr.push(vec)\r\n \r\n }\r\n return vec3Arr\r\n }\r\n //#endregion\r\n}\r\n\r\n","import * as THREE from 'three'\r\nimport RaySphere from './RaySphere'\r\n\r\n\r\nexport default class \r\n{\r\n constructor(environmentOctree)\r\n {\r\n this.environmentOctree= environmentOctree\r\n\r\n this.raySphere= new RaySphere()\r\n\r\n this.stagger= {count:0}\r\n\r\n }\r\n\r\n\r\n // NOTE: it may be better to use the standard boid position array,instead of the vec3 arr\r\n\r\n /**\r\n * checks the environment to see if there are world objects within the boids vision\r\n * \r\n * @param {[THREE.Vector3]} boidPositions \r\n * @returns {foundIntersections{boidindex,{distance,position}}} found intersections\r\n */\r\n #checkEnviroment(boidPositions,boidBoundingBox, iStart, iEnd)\r\n {\r\n if(iStart==null||iEnd==null)\r\n {\r\n iStart=0;\r\n iEnd=boidPositions.length\r\n }\r\n \r\n \r\n //initialize return object\r\n const foundIntersections={}\r\n //loop through boidPositions\r\n for(let i = iStart; i<iEnd; i++){\r\n\r\n //finds environments objects that the boid intersects with\r\n // [x] convert to bounding sphere check\r\n let enviromentObjects=[]\r\n if(boidBoundingBox)\r\n {\r\n const size= boidBoundingBox.max.clone()\r\n \r\n size.sub(boidBoundingBox.min)\r\n // boidBoundingBox.getSize(size)\r\n // console.log(boidBoundingBox,size)\r\n\r\n const boundingBox= new THREE.Box3().setFromCenterAndSize(boidPositions[i].position,size)\r\n\r\n // console.log(boundingBox)\r\n enviromentObjects=this.environmentOctree.getObjects(boundingBox)\r\n }\r\n \r\n let environmentIntersections\r\n \r\n //if there are intersections, cast the rays\r\n if(enviromentObjects.length>0)\r\n {\r\n //rotate raySphere to match boid\r\n\r\n const targets= this.raySphere.rotateTo(boidPositions[i])\r\n \r\n //cast rays on that sphere\r\n \r\n environmentIntersections = this.raySphere.castRays(targets,boidPositions[i].position, enviromentObjects)\r\n \r\n }\r\n \r\n //if there are intersections\r\n if(environmentIntersections)\r\n {\r\n //sets a new object in the found intersections obj\r\n // {currentIndex: {distance:k, position: { x,y,z}}}\r\n foundIntersections[i]=environmentIntersections\r\n }\r\n \r\n }\r\n \r\n return foundIntersections\r\n }\r\n\r\n /**\r\n * Performs the raycast check on surrounding environment objects.\r\n * these checks are staggered to improve performance. \r\n * the stagger parameter partitions the array into sections, only one section is checked per function call\r\n * \r\n * @param {*} boidPoistions array of three.js meshes \r\n * @param {*} stagger the amount of partitions for the boidPositions. Keep to factor of the length of the array, no greater than length/2\r\n * @returns \r\n */\r\n update(boidPoistions,boidBoundingBox, stagger)\r\n {\r\n //update the global stagger variable. \r\n this.stagger.count++\r\n\r\n //avoid any large number errors\r\n if(this.stagger.count==10000){this.stagger.count=0} \r\n\r\n //instanciate the window length and the current Position within the cycle\r\n const window= boidPoistions.length/stagger\r\n const shift= this.stagger.count%stagger\r\n \r\n //instanciate the start and end indicies for the window\r\n const iStart=window*shift\r\n const iEnd=window*(shift+1)\r\n \r\n if(iStart%1!=0||iEnd%1!=0)\r\n {\r\n throw 'Boid Array length is not divisible by stagger'\r\n //make sure that the stagger you use is a factor of the boidArray.length value\r\n }\r\n\r\n if(this.debug&&this.debug.showRays)\r\n {\r\n this.#debugUpdate(boidPoistions[0])\r\n }\r\n\r\n //check the environment based on the window\r\n return this.#checkEnviroment(boidPoistions,boidBoundingBox,iStart,iEnd)\r\n }\r\n\r\n \r\n\r\n /**\r\n * sets up debug panel for raycasting.\r\n * Includes:\r\n * - Show/Hide ray targets\r\n * - Tweak ray count\r\n * - Tweak Ray angle\r\n * - Tweak Ray Distance\r\n * \r\n * @param {*} gui lil-gui instance\r\n * @param {*} scene three.js scene\r\n */\r\n setDebug(gui,scene,mainBoid)\r\n {\r\n this.debug={}\r\n const folder= gui.addFolder('Environment Vision')\r\n \r\n this.#debugRays(folder,scene,mainBoid)\r\n this.#debugTweakRays(folder,scene,mainBoid)\r\n }\r\n\r\n \r\n #debugUpdate(boid)\r\n {\r\n this.debug.pointSphere.position.copy(boid.position)\r\n // console.log(boid)\r\n this.debug.pointSphere.quaternion.setFromRotationMatrix(boid.rotationMatrix)\r\n }\r\n \r\n #debugRays(folder,scene,mainBoid)\r\n {\r\n this.debug.showRays=false\r\n