├── 1-boids-simple ├── index.html └── main.js ├── 2-boids-grids ├── index.html └── main.js ├── 3-boids-webworkers ├── index.html └── main.js ├── LICENSE ├── README.md └── common ├── BoidsController.js ├── BoidsWorker.js ├── BoidsWorkerPlanner.js ├── ControlHelper.js ├── Entity.js ├── Grid.js └── SimpleRenderer.js /1-boids-simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | Boids 3D 23 | 24 | 25 |
26 | Home Page
27 | Boids-Simple: Simulating in browser thread 28 |
29 | 30 | -------------------------------------------------------------------------------- /1-boids-simple/main.js: -------------------------------------------------------------------------------- 1 | import BoidsController from '../common/BoidsController.js' 2 | import SimpleRenderer from '../common/SimpleRenderer.js' 3 | import ControlHelper from '../common/ControlHelper.js' 4 | 5 | 6 | class Application { 7 | constructor() { 8 | this.flockEntityCount = 400; 9 | this.obstacleEntityCount = 50; 10 | this.simpleRenderer = undefined; 11 | this.boidsController = undefined; 12 | this.controlHelper = undefined; 13 | } 14 | 15 | init() { 16 | // create a boids controller with the given boundary [2000, 600, 2000] 17 | this.boidsController = new BoidsController(2000, 600, 2000); 18 | 19 | // create renderer and pass boidsController to render entities 20 | this.simpleRenderer = new SimpleRenderer({boidsController: this.boidsController}); 21 | this.simpleRenderer.init(); 22 | 23 | // create control helper for example controls 24 | this.controlHelper = new ControlHelper(this.boidsController, this.simpleRenderer); 25 | this.controlHelper.init(); 26 | 27 | // add initial entities for an interesting view 28 | this.controlHelper.addBoids(this.flockEntityCount); 29 | this.controlHelper.addObstacles(this.obstacleEntityCount); 30 | 31 | // request the first animation frame 32 | window.requestAnimationFrame(this.render.bind(this)); 33 | } 34 | 35 | render() { 36 | window.requestAnimationFrame(this.render.bind(this)); 37 | 38 | // call statBegin() to measure time that is spend in BoidsController 39 | this.controlHelper.statBegin(); 40 | 41 | // calculate boids entities 42 | this.boidsController.iterate(); 43 | 44 | // update screen by rendering 45 | this.simpleRenderer.render(); 46 | 47 | // call statEnd() to finalize measuring time 48 | this.controlHelper.statEnd(); 49 | } 50 | 51 | } 52 | 53 | // create the application when the document is ready 54 | document.addEventListener('DOMContentLoaded', (new Application()).init()); 55 | -------------------------------------------------------------------------------- /2-boids-grids/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | Boids 3D 23 | 24 | 25 |
26 | Home Page
27 | Boids-with-Grids: Simulating in browser thread
28 | 29 | -------------------------------------------------------------------------------- /2-boids-grids/main.js: -------------------------------------------------------------------------------- 1 | import BoidsController from '../common/BoidsController.js' 2 | import SimpleRenderer from '../common/SimpleRenderer.js' 3 | import ControlHelper from '../common/ControlHelper.js' 4 | 5 | 6 | class Application { 7 | constructor() { 8 | this.flockEntityCount = 400; 9 | this.obstacleEntityCount = 50; 10 | this.simpleRenderer = undefined; 11 | this.boidsController = undefined; 12 | this.controlHelper = undefined; 13 | } 14 | 15 | init() { 16 | // create a boids controller with the given boundary [2000, 600, 2000] 17 | // subdivide the world in to 10*10*10 cubes by passing subDivisionCount as 10 18 | // this will reduce the time spent for finding nearby entities 19 | this.boidsController = new BoidsController(2000, 600, 2000, 10); 20 | 21 | // create renderer and pass boidsController to render entities 22 | this.simpleRenderer = new SimpleRenderer({boidsController: this.boidsController}); 23 | this.simpleRenderer.init(); 24 | 25 | // create control helper for example controls 26 | this.controlHelper = new ControlHelper(this.boidsController, this.simpleRenderer); 27 | this.controlHelper.init(); 28 | 29 | // add initial entities for an interesting view 30 | this.controlHelper.addBoids(this.flockEntityCount); 31 | this.controlHelper.addObstacles(this.obstacleEntityCount); 32 | 33 | // request the first animation frame 34 | window.requestAnimationFrame(this.render.bind(this)); 35 | } 36 | 37 | render() { 38 | window.requestAnimationFrame(this.render.bind(this)); 39 | 40 | // call statBegin() to measure time that is spend in BoidsController 41 | this.controlHelper.statBegin(); 42 | 43 | // calculate boids entities 44 | this.boidsController.iterate(); 45 | 46 | // update screen by rendering 47 | this.simpleRenderer.render(); 48 | 49 | // call statEnd() to finalize measuring time 50 | this.controlHelper.statEnd(); 51 | } 52 | 53 | } 54 | 55 | // create the application when the document is ready 56 | document.addEventListener('DOMContentLoaded', (new Application()).init()); 57 | -------------------------------------------------------------------------------- /3-boids-webworkers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | Boids 3D 23 | 24 | 25 |
26 | Home Page
27 | Boids-with-WebWorkers: Simulating with 4 WebWorkers
28 | 29 | -------------------------------------------------------------------------------- /3-boids-webworkers/main.js: -------------------------------------------------------------------------------- 1 | import BoidsController from '../common/BoidsController.js' 2 | import SimpleRenderer from '../common/SimpleRenderer.js' 3 | import ControlHelper from '../common/ControlHelper.js' 4 | import BoidsWorkerPlanner from '../common/BoidsWorkerPlanner.js' 5 | 6 | 7 | class Application { 8 | constructor() { 9 | this.flockEntityCount = 1000; 10 | this.obstacleEntityCount = 100; 11 | this.simpleRenderer = undefined; 12 | this.boidsController = undefined; 13 | this.controlHelper = undefined; 14 | 15 | this.workerPlanner = undefined; 16 | 17 | this.iterateRequested = false; 18 | } 19 | 20 | init() { 21 | // create a boids controller with the given boundary [2000, 600, 2000] 22 | // subdivide the world in to 10*10*10 cubes by passing subDivisionCount as 10 23 | // this will reduce the time spent for finding nearby entities 24 | this.boidsController = new BoidsController(2000, 600, 2000, 10); 25 | 26 | // create renderer and pass boidsController to render entities 27 | this.simpleRenderer = new SimpleRenderer({boidsController: this.boidsController}); 28 | this.simpleRenderer.init(); 29 | 30 | // create worker planner to run the simulation in WebWorker thread. 31 | // keep the default worker count as 4 32 | this.workerPlanner = new BoidsWorkerPlanner(this.boidsController, this.onWorkerUpdate.bind(this)); 33 | this.workerPlanner.init(); 34 | 35 | // create control helper for example controls 36 | this.controlHelper = new ControlHelper(this.boidsController, this.simpleRenderer, this.workerPlanner); 37 | this.controlHelper.init(); 38 | 39 | // add initial entities for an interesting view 40 | this.controlHelper.addBoids(this.flockEntityCount); 41 | this.controlHelper.addObstacles(this.obstacleEntityCount); 42 | 43 | // request the first animation frame 44 | window.requestAnimationFrame(this.render.bind(this)); 45 | } 46 | 47 | render() { 48 | window.requestAnimationFrame(this.render.bind(this)); 49 | 50 | // call statBegin() to measure time that is spend in BoidsController 51 | this.controlHelper.statBegin(); 52 | 53 | // if the iterate is not requested, make a new iteration reques 54 | if(!this.iterateRequested) { 55 | this.workerPlanner.requestIterate(); 56 | this.iterateRequested = true; 57 | } 58 | 59 | // update screen by rendering 60 | this.simpleRenderer.render(); 61 | } 62 | 63 | onWorkerUpdate() { 64 | // call statEnd() to finalize measuring time 65 | this.controlHelper.statEnd(); 66 | this.iterateRequested = false; 67 | } 68 | 69 | } 70 | 71 | // create the application when the document is ready 72 | document.addEventListener('DOMContentLoaded', (new Application()).init()); 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ercan Gercek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boids JS 2 | 3 | BoidsJS is an implementation of the boids algorithm. This can be used for simulation flock behavior such as birds or fish in 3D space. 4 | 5 | There are three examples provided for similar scenes but three examples differ in performance. 6 | - The first example run in the browser thread and it should slow down when there are many number of entities. 7 | - The second example also runs in the browser thread but it uses the grid implementation for fast lookups. This example should handle more entities in the browser thread. 8 | - In the third example, calculation is done in 4 parallel WebWorker threads. The rendering is still done in the browser thread. Even though the simulation slows down, the UI should be updated in 60fps. 9 | 10 | ## Examples 11 | 12 | [01 - Boids Example](https://ercang.github.io/boids-js/1-boids-simple/) 13 | This example show how to run boids simulation in browser thread. It uses ThreeJS for rendering. 14 | 15 | [02 - Boids with Grid Support](https://ercang.github.io/boids-js/2-boids-grids/) 16 | This example shows how to use grid support for fast lookups. Originally boids algorithm checks near-by entities for calculation and this can be optimized by placing entities in buckets (or grids) 17 | 18 | [03 - Boids with WebWorker Support](https://ercang.github.io/boids-js/3-boids-webworkers/) [Chrome Only] 19 | This example shows how to use WebWorkers for boids calculations. Currently it uses 4 webworkers, FPS meter shows the boids calculation. Actual browser thread is not doing much, so it is exapected to stay at 60FPS. 20 | 21 | **A Note About WebWorkers and Chrome:** Webworker example only works with chrome, because Safari and Firefox does not support import statements in WebWorkers. This is usually not a problem, because using a script packer (eg. Webpack) should overcome this problem. In order to keep the examples simple, a script packet was not used. 22 | 23 | # Class Overview 24 | **BoidsController** class defines a container for boids entities. All entities (flock or obstalces) are added to BoidsController. BoidsController calculates and updates entity positions and velocities. 25 | 26 | **BoidsWorker** is the wrapper for BoidsController to make it work inside a WebWorker context. The responsibility of this class is to create a new BoidsController instance with the received data and run the requested iterations in this isolated context. 27 | 28 | **BoidsWorkerPlanner** is a class to help creating multiple workers and distributing the work to these separate workers. It also deals with synchronization of the data among workers and the application 29 | 30 | **Entity** class defines an entitiy model which has a position and a velocity. Also it has some utiliy methods. 31 | 32 | **Grid** class creates cubic grid for spatial partitioning. This helps lookups to be performed faster for nearby entities. More information can be found here: http://gameprogrammingpatterns.com/spatial-partition.html -------------------------------------------------------------------------------- /common/BoidsController.js: -------------------------------------------------------------------------------- 1 | import Entity from './Entity.js'; 2 | import Grid from './Grid.js' 3 | 4 | /** 5 | * @module BoidsController 6 | * BoidsController class defines a container for boids entities. 7 | * All entities (flock or obstalces) are added to BoidsController. 8 | * BoidsController calculates and updates entity positions and velocities. 9 | */ 10 | export default class BoidsController { 11 | /** 12 | * Constructor for the BoidsController 13 | * @param {Number} boundaryX world size in x axis 14 | * @param {Number} boundaryY world size in y axis 15 | * @param {Number} boundaryZ world size in z axis 16 | * @param {Number} subDivisionCount subdivision count defines the grid size. 17 | * If it is given 10, world will be splitted into 10*10*10 cubes for spatial partitioning. 18 | */ 19 | constructor(boundaryX = 500, boundaryY = 500, boundaryZ = 500, subDivisionCount=1) { 20 | const maxSize = Math.max(boundaryX, boundaryY, boundaryZ); 21 | this.grid = new Grid(maxSize, maxSize/subDivisionCount); 22 | this.subDivisionCount = subDivisionCount; 23 | 24 | this.flockEntities = []; 25 | this.obstacleEntities = []; 26 | 27 | this.boundaryX = boundaryX; 28 | this.boundaryY = boundaryY; 29 | this.boundaryZ = boundaryZ; 30 | 31 | this.aligmentWeight = 2.0; 32 | this.cohesionWeight = 4; 33 | this.separationWeight = 0.3; 34 | 35 | this.maxEntitySpeed = 5; 36 | 37 | this.aligmentRadius = 100; 38 | this.cohesionRadius = 100; 39 | this.separationRadius = 100; 40 | this.obstacleRadius = 100; 41 | } 42 | 43 | /** 44 | * Adds flock entity to boids container 45 | * @param {Entity} entity 46 | */ 47 | addFlockEntity(entity) { 48 | this.grid.addEntity(entity); 49 | this.flockEntities.push(entity); 50 | } 51 | 52 | /** 53 | * Returns flock entities 54 | * @returns {Array} flock entities 55 | */ 56 | getFlockEntities() { 57 | return this.flockEntities; 58 | } 59 | 60 | /** 61 | * Adds obstacle entity to boids controller 62 | * @param {Entity} entity 63 | */ 64 | addObstacleEntity(entity) { 65 | this.grid.addEntity(entity); 66 | this.obstacleEntities.push(entity); 67 | } 68 | 69 | /** 70 | * Returns obstacle entities 71 | * @returns {Array} obstacle entities 72 | */ 73 | getObstacleEntities() { 74 | return this.obstacleEntities; 75 | } 76 | 77 | /** 78 | * Returns world boundary 79 | * @returns {Array} boundary vector 80 | */ 81 | getBoundary() { 82 | return [this.boundaryX, this.boundaryY, this.boundaryZ]; 83 | } 84 | 85 | /** 86 | * Sets max speed for flock entities. 87 | * @param {Number} s 88 | */ 89 | setMaxSpeed(s) { 90 | this.maxEntitySpeed = s; 91 | } 92 | 93 | /** 94 | * Sets aligment weight. This changes how much flock entities are effected by each others alignment 95 | * @param {Number} w 96 | */ 97 | setAligmentWeight(w) { 98 | this.aligmentWeight = w; 99 | } 100 | 101 | /** 102 | * Sets cohesion weight. This changes how much flock entities are inclined to stick together 103 | * @param {Number} w 104 | */ 105 | setCohesionWeight(w) { 106 | this.cohesionWeight = w; 107 | } 108 | 109 | /** 110 | * Sets separation weight. This changes how much flock entities are inclined to separate from each together 111 | * @param {Number} w 112 | */ 113 | setSeparationWeight(w) { 114 | this.separationWeight = w; 115 | } 116 | 117 | /** 118 | * Sets world boundary 119 | * @param {Number} x 120 | * @param {Number} y 121 | * @param {Number} z 122 | */ 123 | setBoundary(x, y, z) { 124 | this.boundaryX = x; 125 | this.boundaryY = y; 126 | this.boundaryZ = z; 127 | } 128 | 129 | /** 130 | * iterate calculates the new position for flock entities. 131 | * start and end indices are used for parallelization of this calculation 132 | * @param {Number} start start index for calculation 133 | * @param {Number} end end index for calculation 134 | */ 135 | iterate(start=0, end=this.flockEntities.length) { 136 | for(let i=start; i { 168 | if(currentEntity != entity && 169 | currentEntity.getType() == Entity.FLOCK_ENTITY && 170 | entity.getDistance(currentEntity) < this.aligmentRadius) { 171 | neighborCount++; 172 | aligmentX += currentEntity.vx; 173 | aligmentY += currentEntity.vy; 174 | aligmentZ += currentEntity.vz; 175 | } 176 | }); 177 | 178 | if(neighborCount > 0) 179 | { 180 | aligmentX /= neighborCount; 181 | aligmentY /= neighborCount; 182 | aligmentZ /= neighborCount; 183 | const aligmentMag = Math.sqrt((aligmentX*aligmentX)+(aligmentY*aligmentY)+(aligmentZ*aligmentZ)); 184 | if(aligmentMag > 0) { 185 | aligmentX /= aligmentMag; 186 | aligmentY /= aligmentMag; 187 | aligmentZ /= aligmentMag; 188 | } 189 | } 190 | 191 | return [aligmentX, aligmentY, aligmentZ]; 192 | } 193 | 194 | /** 195 | * Computes cohesion vector for the given entity 196 | * @param {Entity} entity 197 | * @returns {Array} cohesion vector 198 | */ 199 | computeCohesion(entity) { 200 | let cohX = 0; 201 | let cohY = 0; 202 | let cohZ = 0; 203 | let neighborCount = 0; 204 | 205 | this.grid.getEntitiesInCube(entity.x, entity.y, entity.z, this.cohesionRadius, (currentEntity) => { 206 | if(currentEntity != entity && 207 | currentEntity.getType() == Entity.FLOCK_ENTITY && 208 | entity.getDistance(currentEntity) < this.cohesionRadius) { 209 | neighborCount++; 210 | cohX += currentEntity.x; 211 | cohY += currentEntity.y; 212 | cohZ += currentEntity.z; 213 | } 214 | }); 215 | 216 | if(neighborCount > 0) 217 | { 218 | cohX /= neighborCount; 219 | cohY /= neighborCount; 220 | cohZ /= neighborCount; 221 | 222 | cohX = cohX - entity.x; 223 | cohY = cohY - entity.y; 224 | cohZ = cohZ - entity.z; 225 | 226 | var cohMag = Math.sqrt((cohX*cohX)+(cohY*cohY)+(cohZ*cohZ)); 227 | if(cohMag > 0) { 228 | cohX /= cohMag; 229 | cohY /= cohMag; 230 | cohZ /= cohMag; 231 | } 232 | } 233 | 234 | return [cohX, cohY, cohZ]; 235 | } 236 | 237 | /** 238 | * Computes separation vector for the given entity 239 | * @param {Entity} entity 240 | * @returns {Array} separation vector 241 | */ 242 | computeSeparation(entity) { 243 | let sepX = 0; 244 | let sepY = 0; 245 | let sepZ = 0; 246 | let neighborCount = 0; 247 | 248 | this.grid.getEntitiesInCube(entity.x, entity.y, entity.z, this.separationRadius, (currentEntity) => { 249 | let distance = entity.getDistance(currentEntity); 250 | if(distance <= 0) { 251 | distance = 0.01 252 | } 253 | 254 | if(currentEntity != entity && 255 | currentEntity.getType() == Entity.FLOCK_ENTITY && 256 | distance < this.separationRadius) { 257 | neighborCount++; 258 | const sx = entity.x - currentEntity.x; 259 | const sy = entity.y - currentEntity.y; 260 | const sz = entity.z - currentEntity.z; 261 | sepX += (sx/distance)/distance; 262 | sepY += (sy/distance)/distance; 263 | sepZ += (sz/distance)/distance; 264 | } 265 | }); 266 | 267 | return [sepX, sepY, sepZ]; 268 | } 269 | 270 | /** 271 | * Computes obstacle avoidance vector for the given entity 272 | * @param {Entity} entity 273 | * @returns {Array} obstacle avoidance vector 274 | */ 275 | computeObstacles(entity) { 276 | let avoidX = 0; 277 | let avoidY = 0; 278 | let avoidZ = 0; 279 | 280 | this.grid.getEntitiesInCube(entity.x, entity.y, entity.z, this.obstacleRadius, (currentObstacle) => { 281 | const distance = entity.getDistance(currentObstacle); 282 | if(distance > 0 && 283 | currentObstacle.getType() == Entity.OBSTACLE_ENTITY && 284 | distance < this.obstacleRadius) { 285 | const ox = entity.x - currentObstacle.x; 286 | const oy = entity.y - currentObstacle.y; 287 | const oz = entity.z - currentObstacle.z; 288 | avoidX += (ox/distance)/distance; 289 | avoidY += (oy/distance)/distance; 290 | avoidZ += (oz/distance)/distance; 291 | } 292 | }); 293 | 294 | // avoid boundary limits 295 | const boundaryObstacleRadius = this.obstacleRadius/4; 296 | const distX = this.boundaryX - entity.x; 297 | const distY = this.boundaryY - entity.y; 298 | const distZ = this.boundaryZ - entity.z; 299 | if(entity.x < boundaryObstacleRadius && Math.abs(entity.x) > 0) { 300 | avoidX += 1/entity.x; 301 | } else if(distX < boundaryObstacleRadius && distX > 0) { 302 | avoidX -= 1/distX; 303 | } 304 | if(entity.y < boundaryObstacleRadius && Math.abs(entity.y) > 0) { 305 | avoidY += 1/entity.y; 306 | } else if(distY < boundaryObstacleRadius && distY > 0) { 307 | avoidY -= 1/distY; 308 | } 309 | if(entity.z < boundaryObstacleRadius && Math.abs(entity.z) > 0) { 310 | avoidZ += 1/entity.z; 311 | } else if(distZ < boundaryObstacleRadius && distZ > 0) { 312 | avoidZ -= 1/distZ; 313 | } 314 | 315 | return [avoidX, avoidY, avoidZ]; 316 | } 317 | 318 | /** 319 | * This methods serializes the whole boids controller with entities and 320 | * returns as a simple object. 321 | * @returns {Object} serialized BoidsController data 322 | */ 323 | serialize() { 324 | const flockEntities = []; 325 | const obstacleEntities = []; 326 | this.flockEntities.forEach(entity => { 327 | flockEntities.push(entity.serialize()); 328 | }); 329 | 330 | this.obstacleEntities.forEach(entity => { 331 | obstacleEntities.push(entity.serialize()); 332 | }); 333 | 334 | return { 335 | subDivisionCount: this.subDivisionCount, 336 | boundaryX: this.boundaryX, 337 | boundaryY: this.boundaryY, 338 | boundaryZ: this.boundaryZ, 339 | flockEntities, 340 | obstacleEntities, 341 | aligmentWeight: this.aligmentWeight, 342 | cohesionWeight: this.cohesionWeight, 343 | separationWeight: this.separationWeight, 344 | maxEntitySpeed: this.maxEntitySpeed, 345 | aligmentRadius: this.aligmentRadius, 346 | cohesionRadius: this.cohesionRadius, 347 | separationRadius: this.separationRadius, 348 | obstacleRadius: this.obstacleRadius 349 | } 350 | } 351 | 352 | /** 353 | * This methods serializes only the boids data for the given start and end indices. 354 | * @param {Number} start 355 | * @param {Number} end 356 | * @returns {Object} serialized partial boids data 357 | */ 358 | serializeBoidsData(start=0, end=this.flockEntities.length) { 359 | const flockEntities = []; 360 | for(let i=start; i { 402 | const entity = Entity.deserialize(entityData); 403 | controller.addFlockEntity(entity); 404 | }); 405 | 406 | data.obstacleEntities.forEach(entityData => { 407 | const entity = Entity.deserialize(entityData); 408 | controller.addObstacleEntity(entity); 409 | }); 410 | 411 | return controller; 412 | } 413 | } -------------------------------------------------------------------------------- /common/BoidsWorker.js: -------------------------------------------------------------------------------- 1 | import BoidsController from './BoidsController.js' 2 | 3 | /** 4 | * @module BoidsWorker 5 | * BoidsWorker is the wrapper for BoidsController to make it work inside a WebWorker context 6 | * The responsibility of this class is to create a new BoidsController instance with 7 | * the received data and run the requested iterations in this isolated context. 8 | */ 9 | class BoidsWorker { 10 | constructor() { 11 | this.boidsController = undefined; 12 | } 13 | 14 | /** 15 | * Initializes the boids controller 16 | * @param {Object} data 17 | */ 18 | initializeBoidsController(data) { 19 | this.boidsController = BoidsController.deserialize(data); 20 | } 21 | 22 | /** 23 | * Iterates the BoidsController with the provided parameters 24 | * @param {Number} start 25 | * @param {Number} end 26 | * @param {Object} config 27 | */ 28 | iterateBoidsController(start, end, config) { 29 | this.boidsController.aligmentWeight = config.aligmentWeight; 30 | this.boidsController.cohesionWeight = config.cohesionWeight; 31 | this.boidsController.separationWeight = config.separationWeight; 32 | this.boidsController.maxEntitySpeed = config.maxEntitySpeed; 33 | 34 | this.boidsController.iterate(start, end); 35 | const data = this.boidsController.serializeBoidsData(start, end); 36 | postMessage({action: 'iterateCompleted', data}) 37 | } 38 | 39 | /** 40 | * Updates the internal data of the BoidsController. When other BoidsWorkers have 41 | * new data, it is send to other workers in order to keep all workers in sync. 42 | * @param {Object} data 43 | */ 44 | updateBoidsData(data) { 45 | this.boidsController.applyBoidsData(data); 46 | } 47 | 48 | /** 49 | * Message handler for the worker 50 | */ 51 | onMessage(e) { 52 | if(e.data.action == 'initialData') { 53 | this.initializeBoidsController(e.data.data) 54 | } else if(e.data.action == 'iterate') { 55 | this.iterateBoidsController(e.data.start, e.data.end, e.data.config); 56 | } else if(e.data.action = 'updateBoidsData') { 57 | this.updateBoidsData(e.data.data) 58 | } 59 | } 60 | } 61 | 62 | // create instance 63 | const worker = new BoidsWorker(); 64 | onmessage = worker.onMessage.bind(worker); 65 | -------------------------------------------------------------------------------- /common/BoidsWorkerPlanner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module BoidsWorkerPlanner 3 | * BoidsWorkerPlanner is a class to help creating multiple workers and 4 | * distributing the work to these separate workers. It also deals with 5 | * synchronization of the data among workers and the application 6 | */ 7 | export default class BoidsWorkerPlanner { 8 | constructor(boidsController, updateCallback, workerCount = 4) { 9 | this.boidsController = boidsController; 10 | this.updateCallback = updateCallback; 11 | this.workerCount = workerCount; 12 | this.workers = []; 13 | this.workerCompletedCount = 0; 14 | for(let i=0; i { 19 | worker.onmessage = this.onWorkerMessageReceived.bind(this, index); 20 | }); 21 | } 22 | 23 | /** 24 | * Initializes the worker planner 25 | */ 26 | init() { 27 | this.sendInitialData(); 28 | } 29 | 30 | /** 31 | * Sends the BoidsController data to all workers for initial setup. 32 | */ 33 | sendInitialData() { 34 | // copy boids controller state to web worker 35 | const data = this.boidsController.serialize(); 36 | this.workers.forEach(worker => { 37 | worker.postMessage({action: "initialData", data}); 38 | }); 39 | } 40 | 41 | /** 42 | * This method is called when the application wants all workers to calculate the next iteration. 43 | * This can only be called when the previous request was completed. 44 | */ 45 | requestIterate() { 46 | if(this.workerCompletedCount != 0) { 47 | console.log("Previous request must be completed first!") 48 | return; 49 | } 50 | 51 | const config = { 52 | aligmentWeight: this.boidsController.aligmentWeight, 53 | cohesionWeight: this.boidsController.cohesionWeight, 54 | separationWeight: this.boidsController.separationWeight, 55 | maxEntitySpeed: this.boidsController.maxEntitySpeed, 56 | }; 57 | 58 | const len = this.boidsController.getFlockEntities().length; 59 | const increaseAmount = Math.round(len/this.workerCount); 60 | this.workers.forEach((worker, index) => { 61 | const start = index*increaseAmount; 62 | const end = (index == this.workerCount-1) ? len : (index+1)*increaseAmount - 1; 63 | worker.postMessage({action: "iterate", start, end, config}); 64 | }); 65 | } 66 | 67 | /** 68 | * Message handler for worker classes. This method synchronizes the data and 69 | * lets application know when the data is ready. 70 | */ 71 | onWorkerMessageReceived(index, e) { 72 | if(e.data.action == 'iterateCompleted') { 73 | this.boidsController.applyBoidsData(e.data.data); 74 | this.workers.forEach((worker, wIndex) => { 75 | if(index != wIndex) { 76 | // send this update to other workers 77 | worker.postMessage({action: "updateBoidsData", data: e.data.data}); 78 | } 79 | }); 80 | 81 | this.workerCompletedCount++; 82 | if(this.workerCompletedCount == this.workerCount) { 83 | this.workerCompletedCount = 0; 84 | this.updateCallback(); 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /common/ControlHelper.js: -------------------------------------------------------------------------------- 1 | import Stats from 'https://cdnjs.cloudflare.com/ajax/libs/stats.js/r17/Stats.min.js' 2 | import Entity from './Entity.js' 3 | 4 | let stats = undefined; 5 | 6 | /** 7 | * @module ControlHelper 8 | * A helper class to make examples easier. 9 | */ 10 | export default class ControlHelper { 11 | constructor(boidsController, renderer, workerPlanner) { 12 | this.boidsController = boidsController; 13 | this.renderer = renderer; 14 | this.workerPlanner = workerPlanner; 15 | } 16 | 17 | init() { 18 | // init stats 19 | this.stats = new Stats(); 20 | this.stats.showPanel(0); 21 | document.body.appendChild(this.stats.dom); 22 | 23 | const gui = new dat.GUI(); 24 | gui.add(this.boidsController, 'aligmentWeight',0,5).name('Alignment'); 25 | gui.add(this.boidsController, 'cohesionWeight',0,5).name('Cohesion'); 26 | gui.add(this.boidsController, 'separationWeight',0,5).name('Separation'); 27 | gui.add(this.boidsController, 'maxEntitySpeed',1,10).name('Max Speed'); 28 | 29 | if(this.boidsController.subDivisionCount > 1) { 30 | gui.add(this.renderer.gridVisual, 'visible').name('Show Grid'); 31 | } 32 | 33 | gui.add(this.renderer, 'lockOn').name('Lock Camera'); 34 | this.boidsButton = gui.add(this, 'addBoids'); 35 | this.obstacleButton = gui.add(this, 'addObstacles'); 36 | 37 | this.updateButtonLabels(); 38 | } 39 | 40 | statBegin() { 41 | this.stats.begin(); 42 | } 43 | 44 | statEnd() { 45 | this.stats.end(); 46 | } 47 | 48 | addBoids(count=50) { 49 | const boundary = this.boidsController.getBoundary(); 50 | for(let i=0; i maxVelocity && velocity > 0) { 64 | this.vx = maxVelocity*this.vx/velocity; 65 | this.vy = maxVelocity*this.vy/velocity; 66 | this.vz = maxVelocity*this.vz/velocity; 67 | } 68 | } 69 | 70 | /** 71 | * This method adds the given velocity to the current velocity. 72 | * @param {Number} vx x velocity 73 | * @param {Number} vy y velocity 74 | * @param {Number} vz z velocity 75 | */ 76 | addVelocity(vx, vy, vz) { 77 | this.vx += vx; 78 | this.vy += vy; 79 | this.vz += vz; 80 | } 81 | 82 | /** 83 | * This method moves the entity. 84 | * @param {Number} maxVelocity 85 | * @param {Number} bx 86 | * @param {Number} by 87 | * @param {Number} bz 88 | */ 89 | move(maxVelocity, bx, by, bz) { 90 | this.checkVelocity(maxVelocity); 91 | 92 | let nx = this.x + this.vx; 93 | let ny = this.y + this.vy; 94 | let nz = this.z + this.vz; 95 | 96 | nx = Math.max(0, nx); 97 | nx = Math.min(bx, nx); 98 | ny = Math.max(0, ny); 99 | ny = Math.min(by, ny); 100 | nz = Math.max(0, nz); 101 | nz = Math.min(bz, nz); 102 | 103 | this.grid.moveEntity(this, nx, ny, nz); 104 | } 105 | 106 | /** 107 | * Calculate the distance between the entity and the given entity 108 | * @param {Entity} otherEntity 109 | * @returns {Number} the distance between two entities 110 | */ 111 | getDistance(otherEntity) { 112 | const dx = this.x - otherEntity.x; 113 | const dy = this.y - otherEntity.y; 114 | const dz = this.z - otherEntity.z; 115 | return Math.sqrt((dx*dx)+(dy*dy)+(dz*dz)); 116 | } 117 | 118 | /** 119 | * Serialized the entitiy 120 | * @returns {Object} serialized data 121 | */ 122 | serialize() { 123 | const {id, type, x, y, z, vx, vy, vz} = this; 124 | return { 125 | id, type, x, y, z, vx, vy, vz 126 | } 127 | } 128 | 129 | /** 130 | * Updates the internal data of the entity if the IDs match 131 | * @param {Object} data 132 | */ 133 | updateData(data) { 134 | if(this.id == data.id) { 135 | this.vx = data.vx; 136 | this.vy = data.vy; 137 | this.vz = data.vz; 138 | this.grid.moveEntity(this, data.x, data.y, data.z); 139 | } 140 | } 141 | 142 | /** 143 | * This static method deserializes the given data and returns new Entity instance. 144 | * @param {Object} data 145 | * @returns {Entitiy} deserialized Entitiy instance 146 | */ 147 | static deserialize(data) { 148 | const e = new Entity(data.type, data.x, data.y, data.z, data.vx, data.vy, data.vz); 149 | e.id = data.id; 150 | return e; 151 | } 152 | } -------------------------------------------------------------------------------- /common/Grid.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module Grid 4 | * Grid class creates cubic grid for spatial partitioning. 5 | * This helps lookups to be performed faster for nearby entities. 6 | * More information can be found here: 7 | * http://gameprogrammingpatterns.com/spatial-partition.html 8 | */ 9 | export default class Grid { 10 | /** 11 | * Constructor for the Grid class. Grids can be only be a cube. It takes cellSize as a parameter 12 | * @param {Number} worldSize total world size in units. eg. 1000 13 | * @param {Number} cellSize cell size to divide the world into. eg. 20. 14 | */ 15 | constructor(worldSize, cellSize) { 16 | this.worldSize = worldSize; 17 | this.cellSize = cellSize; 18 | this.cellRowCount = (this.worldSize / this.cellSize)|0; 19 | 20 | this.cellCount = this.cellRowCount*this.cellRowCount*this.cellRowCount; 21 | this.entityList = []; 22 | for(let i=0; i this.cellRowCount-1) { 56 | cellX = this.cellRowCount-1; 57 | } 58 | 59 | if(cellY < 0) { 60 | cellY = 0; 61 | } else if(cellY > this.cellRowCount-1) { 62 | cellY = this.cellRowCount-1; 63 | } 64 | 65 | if(cellZ < 0) { 66 | cellZ = 0; 67 | } else if(cellZ > this.cellRowCount-1) { 68 | cellZ = this.cellRowCount-1; 69 | } 70 | 71 | let index = cellX + cellY*this.cellRowCount + cellZ*this.cellRowCount*this.cellRowCount; 72 | return index|0; 73 | }; 74 | 75 | /** 76 | * Adds the entity to the correspoding grid 77 | * @param {Object} entity 78 | */ 79 | addEntity(entity) { 80 | const index = this.getGridIndex(entity.x, entity.y, entity.z)|0; 81 | entity.setGrid(this); 82 | this.entityList[index].push(entity); 83 | }; 84 | 85 | /** 86 | * Removes the entity from the correspoding grid 87 | * @param {Object} entity 88 | */ 89 | removeEntity(entity) { 90 | const index = this.getGridIndex(entity.x, entity.y, entity.z)|0; 91 | const gridEntities = this.entityList[index]; 92 | const entityIndex = gridEntities.indexOf(entity); 93 | if(entityIndex == -1) 94 | { 95 | // serious error! 96 | throw("removeEntity() can not find the entity to be removed!"); 97 | return; 98 | } 99 | else 100 | { 101 | gridEntities.splice(entityIndex, 1); 102 | entity.setGrid(undefined); 103 | } 104 | }; 105 | 106 | /** 107 | * Moves the entity. Checks the new grid index, if the given position 108 | * requires entitiy move from cell to cell, it handles that transition. 109 | * @param {Object} entity entitiy object 110 | * @param {Number} newX new x position 111 | * @param {Number} newY new y position 112 | * @param {Number} newZ new z position 113 | */ 114 | moveEntity(entity, newX, newY, newZ) { 115 | const oldIndex = this.getGridIndex(entity.x, entity.y, entity.z)|0; 116 | const newIndex = this.getGridIndex(newX, newY, newZ)|0; 117 | 118 | if(oldIndex == newIndex) { 119 | entity.x = newX; 120 | entity.y = newY; 121 | entity.z = newZ; 122 | // no need to update 123 | return; 124 | } 125 | 126 | // remove from the old grid list 127 | const gridEntities = this.entityList[oldIndex]; 128 | const entityIndex = gridEntities.indexOf(entity); 129 | if(entityIndex == -1) 130 | { 131 | // serious error! 132 | throw("moveEntity() can not find the entity to be removed!"); 133 | return; 134 | } 135 | else 136 | { 137 | gridEntities.splice(entityIndex, 1); 138 | } 139 | 140 | // add to the new grid list 141 | entity.x = newX; 142 | entity.y = newY; 143 | entity.z = newZ; 144 | this.entityList[newIndex].push(entity); 145 | }; 146 | 147 | /** 148 | * Finds the corresponding grid for the given x,y,z position and 149 | * returns the entities in that grid. 150 | * @param {Number} x x position to find a cell 151 | * @param {Number} y y position to find a cell 152 | * @param {Number} z z position to find a cell 153 | * @returns {Array} entity list for that grid 154 | */ 155 | getEntitiesInGrid(x, y, z) { 156 | const index = this.getGridIndex(x, y, z)|0; 157 | return this.entityList[index]; 158 | }; 159 | 160 | /** 161 | * Returns the entities in the grid with the given index 162 | * @param {Number} index 163 | * @returns {Array} entity list for that grid 164 | */ 165 | getEntitiesInGridIndex(index) { 166 | if(index < 0 || index >= this.cellCount) 167 | { 168 | throw("getEntitiesInGridIndex() out of bounds!"); 169 | } 170 | 171 | return this.entityList[index|0]; 172 | }; 173 | 174 | /** 175 | * This method finds the entities in the cube that is defined with an origin position and a size. 176 | * The callback is executed for every entity that is found in the cube. 177 | * @param {Number} originX x position for the cube 178 | * @param {Number} originY y position for the cube 179 | * @param {Number} originZ z position for the cube 180 | * @param {Number} size size of the cube 181 | * @param {Function} callback callback is executed for every entity that is found in the cube 182 | */ 183 | getEntitiesInCube(originX, originY, originZ, size, callback) { 184 | const start = this.getGridIndex(originX - size, originY - size, originZ - size); // top left 185 | const topEnd = this.getGridIndex(originX + size, originY - size, originZ - size); // top right 186 | const bottomStart = this.getGridIndex(originX - size, originY + size, originZ - size); // bottom left 187 | const backStart = this.getGridIndex(originX + size, originY + size, originZ + size); // back left 188 | 189 | const index = start; 190 | const width = topEnd - start + 1; 191 | const height = (((bottomStart - start)/this.cellRowCount) + 1)|0; 192 | const depth = (((backStart - start)/(this.cellRowCount*this.cellRowCount)) + 1)|0; 193 | for(let d=0; d= this.cellCount) { 198 | continue; 199 | } 200 | 201 | const currentItems = this.entityList[currentIndex]; 202 | const curLen = currentItems.length; 203 | for(let i=0; i= originX - size && item.x <= originX + size && 207 | item.y >= originY - size && item.y <= originY + size && 208 | item.z >= originZ - size && item.z <= originZ + size) 209 | { 210 | callback(item); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | }; 217 | } 218 | -------------------------------------------------------------------------------- /common/SimpleRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module SimpleRenderer 3 | * SimpleRenderer helps visualizing the entities in the BoidsController and controls the camera. 4 | */ 5 | export default class SimpleRenderer { 6 | constructor({boidsController}) { 7 | this.boidsController = boidsController; 8 | this.isDragging = false; 9 | this.mouseX = 0; 10 | this.mouseY = 0; 11 | this.degX = 45; 12 | this.degY = 60; 13 | const b = this.boidsController.getBoundary(); 14 | this.cameraMax = Math.max(b[0], b[1], b[2]); 15 | this.cameraRadius = this.cameraMax*2/3; 16 | this.lockOn = false; 17 | } 18 | 19 | init() { 20 | this.camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 100000 ); 21 | this.camera.position.z = 0; 22 | 23 | this.scene = new THREE.Scene(); 24 | this.scene.background = new THREE.Color( 0xffffff ); 25 | 26 | this.entityGeometry = new THREE.BoxGeometry( 5, 5, 15 ); 27 | this.obstacleGeometry = new THREE.SphereGeometry( 50, 15, 15 ); 28 | this.entityMaterial = new THREE.MeshNormalMaterial(); 29 | this.obstacleMaterial = new THREE.MeshNormalMaterial(); 30 | 31 | this.createGridVisual(this.boidsController.subDivisionCount); 32 | 33 | // create boundary 34 | const b = this.boidsController.getBoundary(); 35 | const geometry = new THREE.BoxGeometry(b[0], b[1], b[2]); 36 | const wireframe = new THREE.EdgesGeometry(geometry); 37 | const line = new THREE.LineSegments(wireframe); 38 | line.material.color = new THREE.Color( 0x000000 ); 39 | line.material.transparent = false; 40 | line.position.x = b[0]/2; 41 | line.position.y = b[1]/2; 42 | line.position.z = b[2]/2; 43 | this.scene.add(line); 44 | 45 | this.renderer = new THREE.WebGLRenderer({ antialias: true }); 46 | this.renderer.setSize(window.innerWidth, window.innerHeight); 47 | document.body.appendChild(this.renderer.domElement); 48 | 49 | this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this)); 50 | this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this)); 51 | this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this)); 52 | this.renderer.domElement.addEventListener('wheel', this.onMouseWheel.bind(this)); 53 | this.renderer.domElement.addEventListener('touchstart', this.touchStart.bind(this), false); 54 | this.renderer.domElement.addEventListener('touchmove', this.touchMove.bind(this), false); 55 | this.renderer.domElement.addEventListener('touchend', this.touchEnd.bind(this), false); 56 | 57 | this.updateCamera(); 58 | this.render(); 59 | } 60 | 61 | createGridVisual(subdivisionCount) { 62 | this.gridVisual = new THREE.Group(); 63 | const b = this.boidsController.getBoundary(); 64 | const maxLen = Math.max(b[0], b[1], b[2]); 65 | const len = maxLen/subdivisionCount; 66 | for(let x=0; x b[0] || (y+0.5)*len > b[1] || (z+0.5)*len > b[2]) { 70 | continue; 71 | } 72 | 73 | // create boundary wireframe 74 | const geometry = new THREE.BoxGeometry(len, len, len); 75 | const wireframe = new THREE.EdgesGeometry(geometry); 76 | const line = new THREE.LineSegments(wireframe); 77 | //line.material.depthTest = false; 78 | line.material.color = new THREE.Color( 0x999999 ); 79 | line.material.transparent = false; 80 | line.position.x = len/2 + x*len; 81 | line.position.y = len/2 + y*len; 82 | line.position.z = len/2 + z*len; 83 | //this.scene.add(line); 84 | this.gridVisual.add(line); 85 | } 86 | } 87 | } 88 | 89 | this.scene.add(this.gridVisual); 90 | this.gridVisual.visible = false; 91 | } 92 | 93 | touchStart(e) { 94 | const t = e.changedTouches[0]; 95 | this.mouseX = t.pageX; 96 | this.mouseY = t.pageY; 97 | this.isDragging = true; 98 | } 99 | 100 | touchEnd(e) { 101 | this.isDragging = false; 102 | } 103 | 104 | touchMove(e) { 105 | if(!this.isDragging) { 106 | return; 107 | } 108 | 109 | e.preventDefault(); 110 | 111 | const t = e.changedTouches[0]; 112 | 113 | const dx = t.pageX - this.mouseX; 114 | const dy = t.pageY - this.mouseY; 115 | 116 | this.mouseX = t.pageX; 117 | this.mouseY = t.pageY; 118 | 119 | this.degX += dx; 120 | if(this.degX > 360) this.degX = 0; 121 | if(this.degX < 0) this.degX = 360; 122 | 123 | this.degY += dy/3; 124 | this.degY = Math.max(0.1, this.degY); 125 | this.degY = Math.min(179.9, this.degY); 126 | 127 | this.updateCamera(); 128 | } 129 | 130 | onMouseDown(e) { 131 | this.isDragging = true; 132 | this.mouseX = e.offsetX; 133 | this.mouseY = e.offsetY; 134 | } 135 | 136 | onMouseMove(e) { 137 | if(!this.isDragging) { 138 | return; 139 | } 140 | 141 | const dx = e.offsetX - this.mouseX; 142 | const dy = e.offsetY - this.mouseY; 143 | 144 | this.mouseX = e.offsetX; 145 | this.mouseY = e.offsetY; 146 | 147 | this.degX += dx; 148 | if(this.degX > 360) this.degX = 0; 149 | if(this.degX < 0) this.degX = 360; 150 | 151 | this.degY += dy/3; 152 | this.degY = Math.max(0.1, this.degY); 153 | this.degY = Math.min(179.9, this.degY); 154 | 155 | this.updateCamera(); 156 | } 157 | 158 | onMouseUp(e) { 159 | this.isDragging = false; 160 | } 161 | 162 | onMouseWheel(e) { 163 | e.preventDefault(); 164 | this.cameraRadius += e.deltaY * -1; 165 | this.cameraRadius = Math.max(1, this.cameraRadius); 166 | this.cameraRadius = Math.min(this.cameraMax, this.cameraRadius); 167 | this.updateCamera(); 168 | } 169 | 170 | updateCamera() { 171 | let mx=0, my=0, mz=0; 172 | const entities = this.boidsController.getFlockEntities(); 173 | if(this.lockOn && entities.length > 0) { 174 | const mesh = entities[0].mesh; 175 | mx = mesh.position.x; 176 | my = mesh.position.y; 177 | mz = mesh.position.z; 178 | } else { 179 | const b = this.boidsController.getBoundary(); 180 | mx = b[0]/2; 181 | my = b[1]/2; 182 | mz = b[2]/2; 183 | } 184 | 185 | const degXPI = this.degX*Math.PI/180; 186 | const degYPI = this.degY*Math.PI/180; 187 | this.camera.position.x = mx + Math.sin(degXPI)*Math.sin(degYPI)*this.cameraRadius; 188 | this.camera.position.z = mz + Math.cos(degXPI)*Math.sin(degYPI)*this.cameraRadius; 189 | this.camera.position.y = my + Math.cos(degYPI)*this.cameraRadius; 190 | 191 | this.camera.lookAt(mx, my, mz); 192 | } 193 | 194 | render() { 195 | const entities = this.boidsController.getFlockEntities(); 196 | entities.forEach(entity => { 197 | const x = entity.x; 198 | const y = entity.y; 199 | const z = entity.z; 200 | const vx = entity.vx; 201 | const vy = entity.vy; 202 | const vz = entity.vz; 203 | let mesh = entity.mesh; 204 | if(!mesh) { 205 | mesh = new THREE.Mesh(this.entityGeometry, this.entityMaterial); 206 | mesh.localVelocity = {x: 0, y: 0, z: 0}; 207 | this.scene.add(mesh); 208 | entity.mesh = mesh; 209 | } 210 | 211 | // apply asymptotic smoothing 212 | mesh.position.x = 0.9*mesh.position.x + 0.1*x; 213 | mesh.position.y = 0.9*mesh.position.y + 0.1*y; 214 | mesh.position.z = 0.9*mesh.position.z + 0.1*z; 215 | mesh.localVelocity.x = 0.9*mesh.localVelocity.x + 0.1*vx; 216 | mesh.localVelocity.y = 0.9*mesh.localVelocity.y + 0.1*vy; 217 | mesh.localVelocity.z = 0.9*mesh.localVelocity.z + 0.1*vz; 218 | 219 | mesh.lookAt(mesh.position.x + mesh.localVelocity.x, 220 | mesh.position.y + mesh.localVelocity.y, 221 | mesh.position.z + mesh.localVelocity.z); 222 | }); 223 | 224 | const obstacles = this.boidsController.getObstacleEntities(); 225 | obstacles.forEach(entity => { 226 | const x = entity.x; 227 | const y = entity.y; 228 | const z = entity.z; 229 | let mesh = entity.mesh; 230 | if(!mesh) { 231 | mesh = new THREE.Mesh(this.obstacleGeometry, this.obstacleMaterial); 232 | this.scene.add(mesh); 233 | entity.mesh = mesh; 234 | } 235 | 236 | mesh.position.x = x; 237 | mesh.position.y = y; 238 | mesh.position.z = z; 239 | }); 240 | 241 | if(this.lockOn && entities.length > 0) { 242 | this.updateCamera(); 243 | } 244 | 245 | this.renderer.render(this.scene, this.camera); 246 | } 247 | } --------------------------------------------------------------------------------