UNPKG

three-boids

Version:
405 lines (331 loc) 15.3 kB
import boidConfig from "../boid.config"; import * as THREE from 'three' export default class BoidLogic { /** * * @param {int} boidCount * @param {THREE.Box3} box */ constructor(boidCount,box) { this.boundingBox=box //start values this.setUpTweakableValues() //initiate this.#initBoids(boidCount) } /** * sets up the base values based on a config file */ setUpTweakableValues() { this.visualRange=boidConfig.values.visualRange || defaultValue(1,"VisualRange") this.protectedRange=boidConfig.values.protectedRange || defaultValue(0.5,"protectedRange") this.cohesionFactor=boidConfig.values.cohesionFactor || defaultValue(0.0039,"cohesionFactor") this.matchingFactor=boidConfig.values.matchingFactor || defaultValue(0.0287,"matchingFactor") this.seperationFactor=boidConfig.values.seperationFactor || defaultValue(0.01395,"seperationFactor") this.minSpeed=boidConfig.values.minSpeed/100 || defaultValue(0.005,"minSpeed") this.maxSpeed=boidConfig.values.maxSpeed/100 || defaultValue(0.01,"maxSpeed") this.wallTransparent=boidConfig.values.wallTransparent || defaultValue(false,"wallTransparent") this.turnFactor=boidConfig.values.turnFactor/100 || defaultValue(0.2,"turnFactor") this.objectAvoidFactor=boidConfig.values.objectAvoidFactor || defaultValue(2,"object avoid") } /** * * @param {int} boidCount */ #initBoids(boidCount) { this.boidCount=boidCount|| defaultValue(1,"boidCount") this.boidArray=[] this.addBoids(this.boidCount) } /** * Creates and adds new boids, with randomized aceleration and position * * @param {int} count */ addBoids(count){ for(let i = 0; i< count; i++) { const x= (Math.random()-0.5)*2*this.boundingBox.max.x const y= (Math.random()-0.5)*2*this.boundingBox.max.y const z= (Math.random()-0.5)*2*this.boundingBox.max.z const vx= (Math.random()-0.5)*2*this.maxSpeed const vy= (Math.random()-0.5)*2*this.maxSpeed const vz= (Math.random()-0.5)*2*this.maxSpeed this.boidArray.push(new Boid(x,y,z,vy,vx,vz)) } } /** * Removes boid position references * * @param {int} count - amount of boids to remove */ removeBoids(count) { while(count) { this.boidArray.pop() count-- } } /** * Updates the boid positions based on other boids and environment objects * * @param {[obj]} environmentObjects - array of environment objects close to boids */ update(environmentObjects,deltaTime) { const PROTECTED_RANGE_SQUARED= this.protectedRange**2 const VISUAL_RANGE_SQUARED = this.visualRange**2 // const MIN_SPEED_SQUARED= this.minSpeed**2 // const MAX_SPEED_SQUARED= this.maxSpeed**2 // console.log(this.cohesionFactor) this.boidArray.forEach((boid,i)=> { //set up the rotation target // boid.targetX=boid.x // boid.targetY=boid.y // boid.targetZ=boid.z //zero accum variables let accum= this.#accumulatorObject() //loop through every other boid this.boidArray.forEach((otherBoid,n)=> { //compute differences in xy coords const dx= boid.position.x - otherBoid.position.x const dy= boid.position.y - otherBoid.position.y const dz= boid.position.z - otherBoid.position.z //check if they are within visual range if(Math.abs(dx)<this.visualRange && Math.abs(dy)<this.visualRange&& Math.abs(dz)<this.visualRange) { //get the distance between the two boids const distanceSquared= dx**2+dy**2+dz**2 //is the distance less than the protected range if(distanceSquared< PROTECTED_RANGE_SQUARED) { //calculate the difference in x/y-coordinates to the nearfield boid const exp=(1-(distanceSquared/PROTECTED_RANGE_SQUARED))**2 accum.close_dx+=dx*exp accum.close_dy+=dy*exp accum.close_dz+=dz*exp } //if its not in the protected range, is it in the visual range? else if(distanceSquared<VISUAL_RANGE_SQUARED) { const exp=(1-(distanceSquared/VISUAL_RANGE_SQUARED))**2 //add other boids x/y coords and velocity variables to the accum accum.xpos_avg+=otherBoid.position.x accum.ypos_avg+=otherBoid.position.y accum.zpos_avg+=otherBoid.position.z accum.xvel_avg+=otherBoid.velocity.x*exp accum.yvel_avg+=otherBoid.velocity.y*exp accum.zvel_avg+=otherBoid.velocity.z*exp //increment number of boids in visual range accum.neighboring_boids++ } } }) //checks environmet objects to see if this boid is near an object if(!environmentObjects[i]){ //check if there were any boids in the visual range if(accum.neighboring_boids>0) { //average the positions and velocity by number of neighboring boids accum.xpos_avg/=accum.neighboring_boids accum.ypos_avg/=accum.neighboring_boids accum.zpos_avg/=accum.neighboring_boids accum.xvel_avg/=accum.neighboring_boids accum.yvel_avg/=accum.neighboring_boids accum.zvel_avg/=accum.neighboring_boids //add cohesion and alignment factors boid.velocity.x+= (accum.xpos_avg-boid.position.x)*this.cohesionFactor boid.velocity.x+= (accum.xvel_avg-boid.velocity.x)*this.matchingFactor // console.log('cohesion factor',(accum.xpos_avg-boid.x)*this.cohesionFactor) // console.log('matching factor',(accum.xvel_avg-boid.vx)*this.matchingFactor) boid.velocity.y+= (accum.ypos_avg-boid.position.y)*this.cohesionFactor boid.velocity.y+= (accum.yvel_avg-boid.velocity.y)*this.matchingFactor boid.velocity.z+= (accum.zpos_avg-boid.position.z)*this.cohesionFactor boid.velocity.z+= (accum.zvel_avg-boid.velocity.z)*this.matchingFactor } //Add sepperation factor boid.velocity.x+= (accum.close_dx*this.seperationFactor) boid.velocity.y+= (accum.close_dy*this.seperationFactor) boid.velocity.z+= (accum.close_dz*this.seperationFactor) } //there are other objects! get out of the way else { // console.log('avoiding objects') //avoiding object const avoidObjExp=(1-environmentObjects[i].distance)**3 const dx= boid.position.x - environmentObjects[i].position.x const dy= boid.position.y - environmentObjects[i].position.y const dz= boid.position.z - environmentObjects[i].position.z boid.velocity.x+= dx*avoidObjExp*this.objectAvoidFactor boid.velocity.y+= dy*avoidObjExp*this.objectAvoidFactor boid.velocity.z+= dz*avoidObjExp*this.objectAvoidFactor } //the bounding box boid=(this.wallTransparent)?this.#transparentWall(boid):this.#solidWall(boid) // calculate boids speed //NOTE can get rid of the sqrt, move this check to before each variable(environment -> seperation -> alignment -> cohesion) is added to //create heirachy with the fish const speed = Math.sqrt(boid.velocity.x**2+boid.velocity.y**2+boid.velocity.z**2) //enforce speedlimits if (speed< this.minSpeed) { boid.velocity.x= (boid.velocity.x/speed)*this.minSpeed boid.velocity.y= (boid.velocity.y/speed)*this.minSpeed boid.velocity.z= (boid.velocity.z/speed)*this.minSpeed } if (speed> this.maxSpeed) { boid.velocity.x= (boid.velocity.x/speed)*this.maxSpeed boid.velocity.y= (boid.velocity.y/speed)*this.maxSpeed boid.velocity.z= (boid.velocity.z/speed)*this.maxSpeed } //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 // const speedSquared = boid.vx**2+boid.vy**2+boid.vz**2 // //enforce speedlimits // if (speedSquared< MIN_SPEED_SQUARED) // { // const speed= Math.sqrt(speedSquared) // boid.vx= (boid.vx/speed)*this.minSpeed // boid.vy= (boid.vy/speed)*this.minSpeed // boid.vz= (boid.vz/speed)*this.minSpeed // } // if (speedSquared> MAX_SPEED_SQUARED) // { // const speed= Math.sqrt(speedSquared) // boid.vx= (boid.vx/speed)*this.maxSpeed // boid.vy= (boid.vy/speed)*this.maxSpeed // boid.vz= (boid.vz/speed)*this.maxSpeed // } const currentPosition= boid.position.clone() //update positions boid.position.x+=boid.velocity.x*deltaTime boid.position.y+=boid.velocity.y*deltaTime boid.position.z+=boid.velocity.z*deltaTime //update rotation const m4=new THREE.Matrix4(); m4.lookAt(currentPosition,boid.position, new THREE.Vector3(0,1,0)) boid.rotationMatrix= m4 ; }) } /** * An object containing relevant physics accumulations * * @returns accumulator obj */ #accumulatorObject(){ const accum= { xpos_avg:0, //position averages ypos_avg:0, zpos_avg:0, xvel_avg:0, //velocity averages yvel_avg:0, zvel_avg:0, neighboring_boids:0, //count of neighboring boids within visual range close_dx:0, close_dy:0, close_dz:0 } return accum } //returns the main boid getMain() { return this.boidArray[0] } /** * Keeps boids within a bounding box. * Bounding box acts as a notice to turn around * * @param {obj} boid * @returns */ #solidWall(boid) { if(this.boundingBox.max.y<boid.position.y) //top { // console.log(this.boundingBox) boid.velocity.y-=this.turnFactor } if(this.boundingBox.min.y>boid.position.y) //bottom { boid.velocity.y+=this.turnFactor } if(this.boundingBox.max.x<boid.position.x) //right { boid.velocity.x-=this.turnFactor } if(this.boundingBox.min.x>boid.position.x) //left { boid.velocity.x+=this.turnFactor } if(this.boundingBox.max.z<boid.position.z) //front { boid.velocity.z-=this.turnFactor } if(this.boundingBox.min.z>boid.position.z) //back { boid.velocity.z+=this.turnFactor } return boid } /** * Keeps boids within a bounding box. * Bounding box acts as 'portal'. * * @param {obj} boid * @returns */ #transparentWall(boid) { if(this.boundingBox.max.y<boid.y) //top { boid.y=this.boundingBox.min.y } if(this.boundingBox.max.x<boid.x) //right { boid.x=this.boundingBox.min.x } if(this.boundingBox.min.x>boid.x) //left { boid.x=this.boundingBox.max.x } if(this.boundingBox.min.y>boid.y) //bottom { boid.y=this.boundingBox.max.y } if(this.boundingBox.max.z<boid.z) //front { boid.z=this.boundingBox.min.z } if(this.boundingBox.min.z>boid.z) //back { boid.z=this.boundingBox.max.z } return boid } } class Boid { constructor(x,y,z,vx,vy,vz) { this.position=new THREE.Vector3(x,y,z) this.velocity=new THREE.Vector3(vx,vy,vz) this.rotationMatrix=new THREE.Matrix4() this.targetX=0 this.targetY=0 this.targetZ=0 } } function defaultValue(x,name){ console.log(`Defaulted on ${name}`) return x }