This third workshop will demonstrate the beginnings of how we can create generative systems in 3D to create simple structural forms. So, we will:
Where code is provided, you are expected to write it out yourself rather than copying and pasting it. We know this is tempting, but the point of a university education is for you to learn the necessary skills for your field, and copying and pasting code will not be on any job descriptions.
Please work from the template sandbox you created last week. Find the completed template (that you obviously saved in your codesandbox account) and use the “fork” button to create a duplicate - name this duplicate so you can find it later.
If, for some reason, you did not manage to complete last week’s worksheet. You can use the starter sandbox below:
If you didn’t quite get there last time, go ahead and add a grid helper to your scene.
The eagle eyed amongst you may have noticed that in our starter template we defined a clock but we never actually used it. Well, that’s because we didn’t really need it when spinning a simple cube, but when we want to do more complex things, we may not want to lock them into the frame rate of our project. Three.js by default runs at 60 frames per second. And although we can change that, sometimes it’s good to be able to time operations slower than the framerate.
With that in mind, let’s create our clock object and initialise our delta and interval variables. We’re going to do this at the top on the init() function just after we remove the overlay:
// clock generator to ensure we can clamp some operations at different timed rates if needed
clock = new THREE.Clock();
delta = 0;
interval = 1 / 12; // 12 fps
So, let’s change our update such that we get the delta from our clock object (the amount of time elapsed since we last checked, see here for more info).
Then we create an if statement to check whether that delta is higher that our interval.
Within that is where we can perform our timed operations, and finally we just reset delta to being the modulus of our interval variable:
function update()
{
orbit.update();
//update stuff in here
delta += clock.getDelta();
if (delta > interval) {
// The draw or time dependent code are here
delta = delta % interval;
}
}
console.log("Hi", delta);
.If you’re struggling to figure this out, use the sandbox below. You do not need to fork this sandbox - simply use it to experiment then incorporate the code into your own sandbox you are working on
What we’re building is a kind of random plotter. It’s going to create a point in our 3D world, then make a decision to randomly move to the next location and draw a point there. And so on and so forth, there’s a 2D version developed by Dan Shiffman over here.
Now, we could potentially do this using a simple function, but we’re intermediate level creative coders/technologists now so let’s just go right ahead and create a class. Once you’ve had a go and this, if you’re unsure about the stuff we talked about in the lecture, please ask and we can come and talk it over with you.
Remember a class
is like a blueprint. We can use the “new” keyword to create in instance of that class, assign to a variable and use it later in our code. So, first we need to define the class, and in JavaScript, we add a constructor function which is called when we use the “new” keyword.
Then we’re going to define one other function step()
that our class is going to use when we animate the scene.
Add the following code beneath your init() function:
class Walker {
constructor()
{
}
step()
{
}
}
OK cool, so we have the shell of our class but if we made a new walker right now, it wouldn’t do anything. So let’s populate it with some functionality. First of all, we want to be able to supply a starting position, and because we’re working in 3 dimensions, that position will consist of three coordidates (x,y,z). Then we need to assign these to variables within our class, to do that, we use the “this” keyword.
Finally, we just going to use a single BoxGeometry for now, so let’s go ahead and make that. We will reuse this geometry every time we draw a new “thing” in 3D space. (I’m using the word “thing” as it doesn’t have to be a cube, could be another primitive geometric shape, or even a custom 3D model):
Try using a SphereGeometry and ConeGeometry - and read the list of primitive geometries to try something more interesting than a cube…
constructor(x,y,z)
{
this.x = x;
this.y = y;
this.z = z;
this.dotGeometry = new THREE.BoxGeometry();
}
Next, we want to create a “roll of the dice” type scenario to decide which way we are going to move in 3D space before drawing our next dot. We can use the inbuilt Three js functionlity for creating random numbers (similar to p5 in this way) from the MathUtils object.
Then we just have a series of “if else” statements to make our movement decision, as follows:
step()
{
let choice = THREE.MathUtils.randInt(0,5); // six possible choices
if (choice == 0) {
this.x += 0.5; // right
} else if (choice === 1) {
this.x -= 0.5; // left
} else if (choice === 2) {
this.y += 0.5; // up
} else if (choice === 3) {
this.y -= 0.5; // down
} else if (choice === 4) {
this.z += 0.5; // fore
} else {
this.z -= 0.5; // back
}
}
step()
{
// generate a position relative to the current one
let axis = THREE.MathUtils.randInt(1, 3);
let amnt = THREE.MathUtils.randInt(-1, 1);
if (axis === 1) this.x += amnt;
if (axis === 2) this.y += amnt;
if (axis === 3) this.z += amnt;
}
Then, finally, still in our step() function, we actually want to draw a dot. Now, as we saw last time with drawing a cube, we do actually need quite a few lines of code to draw something because our “thing” is always made up of separate component parts: Geometry+Material are used to create a Mesh.
What we’re doing below is:
this.dotMaterial = new THREE.MeshLambertMaterial({});
this.dotMaterial.color = new THREE.Color(0.1,0.5,0.3);
this.dot = new THREE.Mesh(this.dotGeometry, this.dotMaterial);
this.dot.translateX(this.x);
this.dot.translateY(this.y);
this.dot.translateZ(this.z);
scene.add(this.dot);
So we have our blueprint, now we need to creat an instance of our walker. First step is to declare a global variable at the top of our index.js file:
let walker;
Then, at the bottom of our init() function just above where we call play() let’s actually assign a new instance of Walker to that variable:
walker = new Walker(0,0,0);
Remember, the walker constructor takes parameters as three co-ordinate values for the starting position. Later, we’ll experiment with that, but for now, start the walker at position x=y=z=0.
Next, let’s call our step() function in our slower timed section of update():</p>
function update()
{
orbit.update();
//update stuff in here
delta += clock.getDelta();
if (delta > interval) {
// The draw or time dependent code are here
walker.step();
delta = delta % interval;
}
}
OK, the sound bit. We’re going to trigger a single sound every time we make a new step, to really drive home the feel that this is like an infinite drawing machine. We chose this key tap sound which we’re then going to pitch up.
We have prepared an example sound Gabriele100_Keyboard_Various-Keys_02.mp3.
Or you are more than welcome to create or download your own custom sound but make sure it’s a very short, percussive type sound.
In your “sounds” folder, add the new audio file by updloading and then add the following code to replace your previous sound player code:
//sound
listener = new THREE.AudioListener();
camera.add(listener);
sound = new THREE.PositionalAudio(listener);
audioLoader = new THREE.AudioLoader();
audioLoader.load(
"./sounds/Gabriele100_Keyboard_Various-Keys_02.mp3",
function (buffer) {
sound.setBuffer(buffer);
sound.setRefDistance(10);
sound.setRolloffFactor(0.9);
sound.playbackRate = 10;
sound.offset = 0.1;
sound.setDirectionalCone(180, 230, 0.1);
sound.setLoop(false);
sound.setVolume(0.5);
}
);
You can check out what all the parameters we’re setting on the THREE.PositionalAudio object here.
Now, in the step() function, just below your series of if else statements where the choice of where the next step is made, let’s trigger our sound with a bit of randomness in the start position of the audio file and volume, and ensure our sound only lasts for a short period of time - if you’re using your own sound you will probably need to tweak these values:
sound.isPlaying = false;
sound.offset = 0.0 + Math.random() * 0.05;
sound.setVolume(0.8 + Math.random() * 0.1);
sound.duration = 0.1;
sound.play();
In the above code, we first manually set the isPlaying
flag to false, which will let us retrigger the sound at higher rates (it throws an error otherwise). Then we randomise the place in the audio file where we start playing from and the volume. Finally we just tell it that the duration is only 0.1 seconds.
OK so hopefully now you have made your random audio-visual walker, well done! Leave it running for a while to see how the structure grows over time. Here are a couple of stretch goals for you to work to really extend the knowledge you’ve developed so far:
Super important task: go to file->export to .zip
in your codesandbox and download your project as a zip.
Remember, files in the cloud are just files on someone elses computer. It is critical to keep a good backup.