├── 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 | }
--------------------------------------------------------------------------------