-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathBoids.html
525 lines (375 loc) · 21.2 KB
/
Boids.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
---
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> BOIDS </title>
<!-- CDNs -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.2/p5.min.js" integrity="sha512-1YMgn4j8cIL91s14ByDGmHtBU6+F8bWOMcF47S0cRO3QNm8SKPNexy4s3OCim9fABUtO++nJMtcpWbINWjMSzQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/SimulationLabs/Stylesheets/Default.css" rel="stylesheet">
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
processEscapes: true
}
});
</script>
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>
<!-- Simulations -->
<script src="/SimulationLabs/Scripts/Boids/Boids.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/Circles1.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/Circles2.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/RainbowCircles.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/GrowingTrails.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/RainbowTrails.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/WrongFPSBalls.js" defer></script>
<script src="/SimulationLabs/Scripts/Boids/CorrectFPSBalls.js" defer></script>
</head>
<body>
{% include HeaderBar.html %}
<!-- Page content -->
<div class="section">
<p> This is the boids page </p>
<p> <a href="/SimulationLabs/BoidsSolutions">Task Solutions</a> </p>
<h2> Contents </h2>
<ul>
<li> There's probably a bootstrap component for this... </li>
</ul>
<h2> What you'll create </h2>
<p> The aim of this workshop is to recreate boids, which are a simulation of birds and other flocking entities (like fishes).
Hopefully, you'll be able to apply this in your own projects!
Along the way, we'll learn a little bit about p5.js, rendering, and simulating physics. </p>
<div id="BoidContainer"></div>
<em> (This is not an actual boids sketch btw - this is a random stepper) </em>
<h2> Prerequisites </h2>
<ul>
<li> Know how to program using a procedural language e.g. Python, Java, C#, JavaScript etc. </li>
<li> Basic OOP i.e. you know what a class is </li>
<li> Some physics knowledge - vectors, position, velocity, acceleration </li>
</ul>
<p> We will be coding in JavaScript but don't worry about knowing the ins and outs of the language; you should be able to pick it up as we go! </p>
<h2> Introduction to p5.js </h2>
We will be using JavaScript and a library called <strong> p5.js</strong>, made by the lovely <a href="https://processingfoundation.org/"> Processing Foundation</a>.
<ol>
<li> <p>To get started, create an account for the <a href="https://editor.p5js.org/signup">web editor</a>. </p> </li>
<li> <p> Open a new sketch (File->New) </p> </li>
</ol>
<h3> What is a sketch? </h3>
<p>A sketch is a small program that will work with p5.js to produce a simulation.
Just like a game, your simulation will have frames (an image of your simulation) and p5.js will aim to show 60 frames a second.
You'll be able to code what happens just before a frame is shown, and also code what happens just before the simulation starts!
</p>
<br>
<p> Your new sketch should have 2 functions: </p>
<ul>
<li> <strong> setup() </strong> which is a function executed once at the start </li>
<li> <strong> draw() </strong> which is a function executed every frame </li>
</ul>
We create the canvas (arguments being the width and length) in <strong> setup() </strong> since we only need to do this once, and <strong>background()</strong> (which you can call with RGB colour arguments as below) is called every frame in <strong> draw()</strong>!
<pre>
<code class="language-js">
function setup() {
createCanvas(400, 400);
}
function draw() {
let redV = 128; // if you're new to js, use "let" to declare variables
let greenV = 12;
let blueV = 128;
background(redV, greenV, blueV); // my favourite colour; purple!
}
</code>
</pre>
<p> <em> If you're unfamiliar with RGB values, go <a href="/SimulationLabs/Docs/RGBValues">here</a> </em> </p>
<p>This difference matters when it comes to sketches with changing elements; see the difference in the following sketches, where we draw a circle at the mouse's position every frame. One draws a background once, and the other draws a background every frame. </p>
<h3> Circles 1 - Background Drawn Once </h3>
<div id="Circles1Container"></div>
<pre>
<code class="language-js">
function setup() {
createCanvas(400, 400);
background(222, 222, 222); // called just once!
}
const circleSize = 10; // js also has constants!! The value of variable "circleSize" cannot be changed
function draw() {
circle(mouseX, mouseY, circleSize);
}
</code>
</pre>
<emph> Background is called just once - at the start - and many circles are drawn over it. </emph>
<p>Note the use of <strong>mouseX</strong> and <strong>mouseY</strong> which are variables updated every frame! </p>
<h3> Circles 2 - Background Drawn Every Frame </h3>
<div id="Circles2Container"></div>
<pre>
<code class="language-js">
function setup() {
createCanvas(400, 400);
}
let circleSize = 10;
function draw() {
background(222, 222, 222); // background drawn every frame!
circle(mouseX, mouseY, circleSize);
}
</code>
</pre>
<emph> Background is called every frame and essentially clears the screen by drawing the background over everything in the previous frame. </emph>
<h3> Task: Multicolour </h3>
We can use the <strong>stroke()</strong> and <strong>fill()</strong> functions to change the line colour and fill colour of the circles (and any shape we draw) like below:
<pre>
<code class="language-js">
stroke(255, 0, 0); // outlines are now red!
fill(0, 255, 0); // areas are now green!
</code>
</pre>
<p><strong>Part 1:</strong> Modify the <emph>Circles 2</emph> sketch to have colours of your choice. </p>
<p><strong>Part 2:</strong> Modify the <emph>Circles 1</emph> sketch to have constantly changing colour like so: </p>
<div id="CirclesRainbowContainer"></div>
<strong> <emph>Hint:</emph></strong> The range of colours is 0-255 (anything >255 will be interpreted as 255), so you will have to use modulus (<strong>%</strong>) or if statements to ensure RGB values don't go outside that range!
<h2> Using Arrays and Objects </h2>
<p> To store our boids, we're going to use arrays and OOP. We'll have a look at how to do this in JavaScript using a modified version of our Circles 2 sketch, to create fixed-length trails (with circles of changing size) like below: </p>
<div id="GrowingTrailsContainer"></div>
<em> A trail with a length of 20 circles, where the circles grow to a max size then reset </em>
<p>
To create a fixed-length trail, we can't just not refresh the background every frame; this will lead to circles from before 20-circles-ago staying!
We'll have to save the previous 20 circles, and then draw them every frame. To do this, we'll need: </p>
<ul>
<li>An array storing the circles </li>
<li>An object representing a circle, which stores the position and size of the circle </li>
</ul>
<p> Let's get the growing circle part out of way; this part isn't important, but the rest of the code makes less sense without it: </p>
<pre>
<code class="language-js">
function setup() {
createCanvas(400, 400);
}
const maxCircleSize = 15;
const minCircleSize = 5;
let circleSize = 5;
function draw() {
background(222, 222, 222);
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw circle
circle(mouseX, mouseY, circleSize);
}
</code>
</pre>
<em>Could also use modulo to loop the circleSize, but the if statement is more readable here</em>
<p>Don't worry about how this is performed; it's more important that you understand how objects, classes, and arrays work in JavaScript. </p>
<p> Now let's create a class for the circle! </p>
<h3> Coding the Class </h3>
<pre>
<code class="language-js">
class trailCircle {
constructor(position, size){
this.position = position;
this.size = size;
}
}
</code>
</pre>
<p> Every class has a constructor which is the function that runs whenever an object is created; we can define the constructor for a class as above.
Note the use of <strong>this</strong> inside the constructor! It's a variable for the object being created.
</p>
<p> You might be wondering <em>what</em> position will be, since it's a single variable to represent both x and y.
Fortunately, p5.js provides us with the Vector class which allows us to bundle x and y into an object, and gives us a lot of methods to manipulate them as vectors!
Have a look at the methods on the handy <a href="https://p5js.org/reference/#Vector">p5 reference</a>.
We can use the <strong>createVector()</strong> method to make one!
</p>
<br>
<p>Now we can create an instance of trailCircle like so:</p>
<pre>
<code class="language-js">
let tc1 = new trailCircle(createVector(100,100), 100);
</code>
</pre>
<p>Now we can draw the object to the screen by writing:</p>
<pre>
<code class="language-js">
circle(tc1.position.x, tc1.position.y, tc1.size);
</code>
</pre>
<p>It'd be nice to put this drawing stuff into one place though, so that if we wanted to change how circles are rendered, we only need to change it in one place.
We might also introduce other shapes like squares, which may have a different way of rendering its shape!
Let's add a draw method to our class. </p>
<pre>
<code class="language-js">
class trailCircle {
constructor(position, size){
this.position = position;
this.size = size;
}
draw(){
circle(this.position.x, this.position.y, this.size);
}
}
</code>
</pre>
<h3> Storing the trail </h3>
<p>We can declare an array for the trail and a trail length pretty easily: </p>
<pre>
<code class="language-js">
const trails = [];
const trailLength = 20;
</code>
</pre>
<p> Note <strong>const</strong> here doesn't mean an uneditable array; you can insert and remove from the array, but you can't reassign the <em>trails</em> variable to something else.
It will forever be that array, but you can edit the array as you wish. </p>
<p> Now let's create a method updateTrail to add the current circle to the trail; remember that we have <em>circleSize</em> from the growing circle part of the sketch. </p>
<br>
<p>Our method will need to:</p>
<ul>
<li>Create a new trail object, and add it to the array</li>
<li>If the array is full, then remove the oldest circle in the array</li>
</ul>
<p> I'll arbitrarily decide that the circles will be added to the end of the array, so therefore the oldest circles will be in the front of the array. Here's one implementation of the specification above:</p>
<pre>
<code class="language-js">
function updateTrail(){
if(trails.length == trailLength){ // should remove one
trails.shift(); // this deletes the first element, and shifts the rest down
}
// create trail object to insert
let trailObj = new trailObject(createVector(mouseX, mouseY), circleSize);
trails.push(trailObj);
}
</code>
</pre>
<p>This might be a little confusing because of the use of <em>shift()</em> and <em>push()</em>; they're just JavaScript methods to make our lives easier.
You can see JavaScript's array methods <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift">here</a>!
</p>
<p> We do need to call updateTrail() in draw; let's call it just after we draw the current circle. </p>
<pre>
<code class="language-js">
function draw() {
background(222, 222, 222);
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw circle
circle(mouseX, mouseY, circleSize);
// update stuff
updateTrail();
}
</code>
</pre>
<em>Our updated draw method</em>
<br>
<p> Okay great, now we just need to display the trail! </p>
<h3> Displaying the trail</h3>
<p>All we have to do is loop through our array of trail circles and display each one. I want to draw the oldest first, so I'll iterate from the start to the end of the array. </p>
<pre>
<code class="language-js">
function drawTrail(){
for(let i = 0; i < trails.length; i++){
let trailObj = trails[i];
// draw the circle!
trailObj.draw();
}
}
</code>
</pre>
<p> And now we'll call <em>drawTrail()</em> before the current circle (so we don't draw the trail over the current circle!) </p>
<pre>
<code class="language-js">
function draw() {
background(222, 222, 222); // background drawn every frame!
// update circleSize
circleSize++;
if(circleSize > maxCircleSize){
circleSize = minCircleSize;
}
// draw stuff
drawTrail();
circle(mouseX, mouseY, circleSize);
// update stuff
updateTrail();
}
</code>
</pre>
<em>Our final version of draw()!</em>
<h3> Full Code for Growing Trails </h3>
<p> You can have a look at the full program <a href="https://editor.p5js.org/RexMortem/sketches/hJOgrWgry">here</a></p>
<h3> Task: Rainbow Trails </h3>
<p> With the help of the Growing Trails code, modify your solution to the task Multicolour to create rainbow trails. </p>
<p> You should keep the colour changing code, and store the previous 10 or so circles! Your simulation should look something like this:</p>
<div id="RainbowTrailsContainer"></div>
<p><strong>Tip:</strong> Once you've finished your sketch, sometimes it's a little hard to see whether the trail circles have the colour and position that they should. You can verify this more easily by slowing down your simulation! Use <em>frameRate(10)</em> in setup to set your sketch to run at 10 frames per second. </p>
<h2> May the Force be with you </h2>
<p> To make our boids move, we're going to use forces. We know from physics that $F=ma$, but we're going to simplify things by ignoring mass and jump straight to changing acceleration! </p>
<p> Let's try modelling a simple force; a constant acceleration to the right. </p>
<h3> Accelerating Ball </h3>
<p> First, let's establish a class for this ball to make things clear to ourselves. </p>
<pre>
<code class="language-js">
class Ball {
constructor(){
this.position = createVector(0,0);
this.velocity = createVector(0,0);
this.acceleration = createVector(0,0);
}
draw(){
circle(this.position.x, this.position.y, 10);
}
}
</code>
</pre>
<p> This is all stuff we've basically done, but we also need a way to simulate all the physics.
From physics, we know that acceleration is the rate of change of velocity, and that velocity is the rate of change of position.
</p>
<br>
<p> So to simulate our ball's motion, we would: </p>
<ol>
<li> Update the velocity with acceleration </li>
<li> Update the position with velocity </li>
</ol>
<p> As we'll soon see, we have to do this in a <em>very careful</em> way. </p>
<h4>Enter deltaTime</h4>
<p>Until now, we've not really considered how long each frame takes.
It'd be reasonable to assume each frame takes about 1/60 seconds to process <em>however</em> each frame takes a slightly different amount of time and, when simulations get more complex, each frame might take a non-slight different amount of time!
</p>
<p>To test this theory, console.log the <em>deltaTime</em> variable inside the <em>draw()</em> function; it will set itself to the time taken to process the previous frame! </p>
<h4>Using deltaTime</h4>
<p> We know $v = p/t$ (where ) </p>
<div id="WrongFPSBallsContainer"></div>
<div id="CorrectFPSBallsContainer"></div>
<a href="https://p5js.org/reference/p5/deltaTime/"> delta time docs </a>
<h2> Autonomous Agents Intro </h2>
<p> Simple individual steering behaviours </p>
<p> Key things to mention: </p>
<ul>
</ul>
<h2> Creating boids </h2>
<ul>
<li> Limit to agent's perception (think about cs118 enjoyers) </li>
<li> No leader (could you make a version with a leader?) </li>
<li> Emergent Behaviour </li>
<li> Nonlinearity </li>
<li> Competition and Cooperation </li>
<li> Feedback </li>
</ul>
<p> With room for more behaviours! </p>
<h2> Next Time... </h2>
Next workshop, we'll be looking at using procedural generation.
<h2> Extension Material</h2>
<p> Link to an <a href="/SimulationLabs/BoidsExtension"> extension site </a> with scope for learning how to host your own </p>
<h2> Further Reading </h2>
<ul>
<li> <a href="https://www.red3d.com/cwr/papers/1987/SIGGRAPH87.pdf"> Flocks, Herds, and Schools: A Distributed Behavioural Model</a> - The original boids paper</li>
<li> <a href="https://www.youtube.com/watch?v=yGhfUcPjXuE"> Dear Game Developers, Stop Messing This Up! </a> - YT Video on delta-time and basic physics </li>
<li> <a href="https://natureofcode.com/"> Nature of Code </a> - A book by Daniel Shiffman which these labs take inspiration from! It features a section on boids and many more simulations of things from nature (highly recommend) </li>
<li> <a href="https://www.youtube.com/watch?v=bqtqltqcQhw">Coding Adventure: Boids</a> - Sebastian Lague's brilliant video on boids is the main inspiration for this lab! He takes boids into 3D and explores a raycasting approach to collision avoidance </li>
</ul>
</section>
</body>
</html>