CPC_Practicals

Workshop 02 - tooling up

Hello and welcome!

This second workshop will get you up to speed with making all the building blocks for a creative computing three js project. In this workshop 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.

Task 1 - Tooling Up

OK, so here goes with our first task, we’re just going to get to know our development environment a bit. For the first semester, we will be using the most bleeding edge tools on the interplex to facilitate our learning and creativity.

We mean codesandbox, Three.js and Tone.js!

Generally we provide codesandbox links for a here’s what we are building example and a starting point for you to build up from.

Click on the “open sandox” button in the bottom right hand corner of the starting point sandbox and it will open up a new tab with that sandbox ready for you to explore and extend it. 😀

The “fork” button in the top right (of the opened tab) duplicates everything in your personal codesandbox account if you have one set up.

For the rest of the semester you will be working in multiple browser tabs for these worksheets.

You will work from this starting point

Take a look around the starter codesandbox above. You should be able to see that there are tabs across the top with various different file names: index.js, package.json, styles.css, and index.html

index.js is actually empty right now apart from a couple of import statements. In modern JavaScript, we are able to import modules that allow us to use other peoples’ libraries with ease.

As you’ll be aware from the lecture, we’re using Three.js and this is the syntax we use to get Three into our index.js so we can start using it. More will be explained later.

A quick demo

Just to demonstrate how quickly we can start to develop our web project we’re going to add a divider <div> and within that make a button <button> that says “play” on it.

Head into the index.html tab and add the following HTML elements on a new line just after the <body> tag and before the <script> tag:

<div id="overlay">
	<button id="startButton">Play</button>
</div>

You should see a button appear saying “play” on it - that button won’t do anything yet but you can see how quickly we can build an interface… You can also add a bunch of other HTML elements if you want to customise your page.

Now, as we talked about in the lecture, codesandbox is a development environment that kind of bridges the gap between prototyping playground and pro developers sharing ideas.

I’m not going to big it up too much but it’s RUDDY GREAT

It gives us a fully functional development environment with nice code editor, embedded test-browser and console all within the comfy home of our favourite web browser.

We can also collaboratively edit code together, which in these remote-times is pretty incredible and will allow us to help you along the way. There are a whole lot of other features but let’s just leave it there to stop me talking about how wonderful it is…

Please now go to codesandbox and create an account, we recommend using your UWE email but you can also use a personal one if you’d prefer.

Task 2 - The Unholy Trinity: HTML/CSS/JavaScript

We’re working with the basic “vanilla” starter that you get with codesandbox.

OK now we’re going to do a few more steps just to display the current time:

let timer = setInterval(myTimer, 1000); // declare a variable, assign the setInterval function supplying myTimer as the callback with a 1 second interval
function myTimer() 
{
  let d = new Date();
  document.getElementById("display").innerHTML = d.toLocaleTimeString();
}

So you should be able to see how quickly we can dynamically update our index.html through the use of JavaScript and the power of codesandbox. But let’s face it, that’s not particularly creative, and we’re here to make some shapes and noises, so let’s move on to using Three.js to create a simple scene

Task 3 - Three.js

Task 3.1

Starting from now, we’re going to have to work across two browser tabs: this workshop page and your codesandbox project. After each major step, we’ll show expandable sections with example solutions inside so you can see an example solution, and make sure your code in your own sandbox matches up.

We’re going to go easy here. Expand the sections below to see how the index.html styles.css and index.js files should look.

Expand this to see how your index.html file should look
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="overlay">
      <button id="startButton">Play</button>
    </div>

    <script src="src/index.js"></script>
  </body>
</html>
Expand this to see how your index.js file should look
import "./styles.css";
Expand this example styles.css file
body {
  margin: 0;
  background-color: #000;
  color: #fff;
  overscroll-behavior: none;
}

canvas {
  display: block;
}

#overlay {
  position: absolute;
  z-index: 2;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.7); /* slightly transparent*/
}

#overlay button {
  background: #ffffff;
  border: 0;
  color: #000000;
  padding: 16px 20px;
  text-transform: uppercase;
  cursor: pointer;
}

#info {
  position: absolute;
  top: 0px;
  width: 100%;
  padding: 10px;
  box-sizing: border-box;
  text-align: center;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
  pointer-events: none;
  z-index: 1;
}

Task 3.2

From now on, we’re going to have to work across two browser tabs: this workshop page and your codesandbox project. After each major step, we’ll show expandable sections with example solutions inside so you can see an example solution, and make sure your code in your own sandbox matches up.

First of all we need to add the Three.js library as a dependency in our project. This is basically just telling our codesandbox “hey please include this file when building as we’re going to import it into our project”. To do this, navigate to the dependencies dropdown (in the explorer tab on the left or in the top left burger menu if you’re using the embedded sandbox below). Then in the “add dependency” field just type “three” and click to add the dependency. Simple!

Now we’re going to add some lines at the very top of our index.js that import our Three.js library and also a thing called OrbitControls which will allow us to interact with our project to scroll around using the mouse:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./styles.css";
let scene, camera, renderer;
let geometry, material, cube;
let colour, intensity, light;
let ambientLight;

let orbit;

let listener, sound, audioLoader;

let clock, delta, interval;

let startButton = document.getElementById("startButton");
startButton.addEventListener("click", init);

OK for now, we’re just going to add an alert() in the our init() function to ensure that we have everything working and we can move on to the next step:

function init() {
    	alert("We have initialised!");
    }

Right now, you should be able to click on the startButton with the label PLAY in the overlay to trigger an alert message in your browser.

If this doesn’t happen in your sandbox, use the expandable sections below to check how your code differs from the provided example solutions. We won’t always provide solutions like this, but we’re going easy in these first couple of sessions 😀

Expand this to see how your index.html file should look
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="overlay">
      <button id="startButton">Play</button>
    </div>

    <script src="src/index.js"></script>
  </body>
</html>
Expand this to see how your index.js file should look
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./styles.css";

let scene, camera, renderer;
let geometry, material, cube;
let colour, intensity, light;
let ambientLight;

let orbit;

let listener, sound, audioLoader;

let clock, delta, interval;

let startButton = document.getElementById("startButton");
startButton.addEventListener("click", init);

function init() {
  alert("Initialised!");
}

Task 3.3

In these next steps, we’re going to adapt this to trigger a simple 3D scene instead of the alert message. Feel free to expand the sections below to see how your code in the index.js and index.html files should look.

These next steps will be a bit of a big chunk as we’ll need to set up all the various bits and pieces we talked about in the lecture for a Three.js scene, camera, objects, and renderer, etc.

Your first action should be to delete the alert code in index.js. Then, we nee to add code to hide the overlay panel when we detect that the “play” button is pressed. This code inside the init() function in index.js should do it:

  // hide the overlay panel
  let overlay = document.getElementById("overlay");
  overlay.remove();

Right, below the section of removing the overlay but still in the the init() function, let’s make our scene and set the background to a light grey:

  //create our scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xdfdfdf);

Still within the init() function, we’re going to add another fundamental part of our project, the camera. We initialise the camera with the folling parameters:

  //create camera
  camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );

Beneath that but still in the init() function, let’s just move the camera back a bit on the z axis by accessing its position propery using the got operator and assigning it to the number 5. This will ensure we can actually see the stuff we’re drawing later and we’re not INSIDE it looking out:

camera.position.z = 5;

Still within init(), we’re going to create our renderer. This is the bit that is actually going to draw our cool stuff to the screen. We’re going to enable anti aliasing (see here for a bit more info about what that is. And then we’re going to add our renderer to our HTML page. This creates a HTML5 canvas and enables WebGL to create our 3D world:

  // add a renderer to our document
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

Finally, still within init() we’re going to create our orbit controls which will let us interact with the scene and change the position of the camera using our mouse. Just a couple of lines, and then in the last line of init() we will call our play() function which we’ll define below:>

  //create the orbit controls instance so we can use the mouse move around our scene
  orbit = new OrbitControls(camera, renderer.domElement);
  orbit.enableZoom = true;
  play();

OK below our init() function let’s define our play() and stop() functions. In the play() function, we supply an anonymous function to the renderer which will loop continually and that will call our update() function and our render() function (defined below). This is essentially the basics of building a game engine structure. The stop() function will just be there to kill the game loop if need be:

// start animating using setAnimationLoop
function play() 
{
  renderer.setAnimationLoop(() => {
    update();
    render();
  });
}
// stop animating (not currently used)
function stop() {
  renderer.setAnimationLoop(null);
}

Right, nearly there. Below play() and stop() let’s add our update() function. As you can see, this will be called on loop by the renderer. For now, we’re just going to update our orbit controls object like so:

// our update function can be used to modify our scene 'in between' rendering frames
function update() 
{
  orbit.update();
  // we will add more stuff here later
}

And then finally at this stage, we’re going create our render function in which we will pass our scene and camera to the renderer to render!

// simple render function
function render() 
{
  renderer.render(scene, camera);
}

But, argh! We still can’t see anything apart from a blank screen as we haven’t actually put anything into our scene yet! All that for a blank screen?! Well, not to worry, this whole exercise is building us a template which we can reuse again and again in later tutorials.

Here’s a few expandable sections which should match your sandbox after this last mammoth step:

Expand this to see how your index.html file should look
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="overlay">
      <button id="startButton">Play</button>
    </div>

    <script src="src/index.js"></script>
  </body>
</html>
Expand this to see how your index.js file should look
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./styles.css";

let scene, camera, renderer;
let geometry, material, cube;
let colour, intensity, light;
let ambientLight;

let orbit;

let listener, sound, audioLoader;

let clock, delta, interval;

let startButton = document.getElementById("startButton");
startButton.addEventListener("click", init);

function init() {
  // hide overlay panel
  let overlay = document.getElementById("overlay");
  overlay.remove();

  //create our clock and set interval for 30 fps
  clock = new THREE.Clock();
  delta = 0;
  interval = 1 / 30;

  //create our scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xdfdfdf);
  //create camera
  camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );

  //add a renderer to our document
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  //create the orbit controls instance so we can use the mouse move around our scene
  orbit = new OrbitControls(camera, renderer.domElement);
  orbit.enableZoom = true;
  play();
}

// simple render function
function render() {
  renderer.render(scene, camera);
}

// start animating using setAnimationLoop 
function play() {
  renderer.setAnimationLoop(() => {
    update();
    render();
  });
}

// stop animating (not currently used)
function stop() {
  renderer.setAnimationLoop(null);
}

//our update function can be used to modify our scene 'in between' rendering frames
function update() {
  orbit.update();
  //we will add more stuff here later
}

Task 3.4

OK, now we need to add some light and an object to our scene.

Head back into the init() function, just above the bit where we call play(). Add a couple of lights to our scene, one directional and one ambient:

  // lighting
  colour = 0xffffff;
  intensity = 1;
  light = new THREE.DirectionalLight(colour, intensity);
  light.position.set(-1, 2, 4);
  scene.add(light);
  ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

Now, remember what we were talking about in the lecture regarding the fact that meshes are made up of geometries and materials. Let’s create a very simple cube with a basic materials:

// create a box to spin
  geometry = new THREE.BoxGeometry();
  material = new THREE.MeshNormalMaterial(); 
  cube = new THREE.Mesh(geometry, material);

  scene.add(cube);

Is this thing on?

Hopefully you can now see a cube?!

If not, one common mistake is to forget to include the following line (to move the camera out from the default position, inside the cube!)

camera.position.z = 5;

When you can see the cube, let’s edit the update() function to manipulate the properties of our cube and spin it around a bit.

//our update function can be used to modify our scene 'in between' rendering frames
function update() {
  orbit.update();
  
  //modify the cube rotation
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.04;
  cube.rotation.z -= 0.01;
}

So, this has been a long time coming but because we’ve worked in this way it’s now very easy for us to animate anything in the scene.

Here’s an expandable section which should match where we got to after completing the steps in the previous tasks

Expand this to see how your index.js file should look
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./styles.css";

let scene, camera, renderer;
let geometry, material, cube;
let colour, intensity, light;
let ambientLight;

let orbit;

let listener, sound, audioLoader;

let clock, delta, interval;

let startButton = document.getElementById("startButton");
startButton.addEventListener("click", init);

function init() {
  // hide the overlay panel
  let overlay = document.getElementById("overlay");
  overlay.remove();

  //create our clock and set interval at 30 fps
  clock = new THREE.Clock();
  delta = 0;
  interval = 1 / 30;

  //create our scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xdfdfdf);
  
  //create camera
  camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  camera.position.z = 5;

  // add a renderer to our document
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);

  //create the orbit controls instance so we can use the mouse move around our scene
  orbit = new OrbitControls(camera, renderer.domElement);
  orbit.enableZoom = true;

  // lighting
  colour = 0xffffff;
  intensity = 1;
  light = new THREE.DirectionalLight(colour, intensity);
  light.position.set(-1, 2, 4);
  scene.add(light);
  ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);

  // create a box to spin
  geometry = new THREE.BoxGeometry();
  material = new THREE.MeshNormalMaterial(); // Change this from normal to Phong in step 5
  cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  play(); // start animating
}

// stop animating (not currently used)
function stop() {
  renderer.setAnimationLoop(null);
}

// simple render function
function render() {
  renderer.render(scene, camera);
}

// start animating using setAnimationLoop
function play() {
  renderer.setAnimationLoop(() => {
    update();
    render();
  });
}

// our update function is used to modify our scene 'in between' rendering frames
function update() {
  orbit.update();

  // modify our cube rotation
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.04;
  cube.rotation.z -= 0.01;
}

Feel free to use this in your own codesandbox account if your previous steps haven’t gone so well. Be sure to review what went wrong at some point tho, it will help you develop debugging skills!

Experiment time

Task 4

As we discussed in the lecture, one of the things that makes this module different from others around and about is that it is focused on audio-visual stuff, rather than just visual or just audio. With that in mind, let’s add a sound to our project.

We’re going to use Three.js’s sound capabilities which allow us to play back sound files and some simple panning etc. Hopefully you’re now comfortable with the interface of codesandbox. So make a new folder in the root directory (i.e a new folder outside the one called “src”) in your codesandbox project and call it “sounds”.

Use the upload button to upload a sound file. You can select any sound that you already have on your system. Ideally we will use a .mp3 as they’re smaller.

If you don’t have any sounds on your system you can use the one we have made here: simple looping drone sound called CPC_Basic_Drone_Loop.mp3 that you can download if you’d prefer to use this to get going quickly. Download it to your system and upload to your “sounds” folder in your sandbox.

OK so let’s head back into our init() function again. We’re going to go just above that final line where we call play() and add some stuff so that we can generate and listen to sound. We’re going to make a listener (a virtual pair of ears which we will attach to our camera), then we will create a sound emitter which will play back audio. The result will be nice positional audio panning depending on where the camera is.

  //sound for single source and single listener
  listener = new THREE.AudioListener();
  camera.add(listener);
  sound = new THREE.PositionalAudio(listener);

Finally, we’re going to setup the playback buffer of our sound emitter with the loaded mp3 file, and set a bunch of parameters on our positional audio object to that it loops and plays audio in a specific direction etc:

  audioLoader = new THREE.AudioLoader();
  audioLoader.load("./sounds/CPC_Basic_Drone_Loop.mp3", function (buffer) {
    sound.setBuffer(buffer);
    sound.setRefDistance(10);
    sound.setDirectionalCone(180, 230, 0.1);
    sound.setLoop(true);
    sound.setVolume(0.5);
    sound.play();
  });

OK cool, try moving around your 3D world and experiment with how the sound changes based on the position of the camera!

Task 5

“Just one more thing…” as Columbo would say. Being as how we’re in a web browser and those pesky users of our stuff might resize their window. We need to add some code in our main template to ensure that our scene gets resized accordingly.

Let’s define a couple of variable towards the top of index.js:

let sceneHeight, sceneWidth;

Then, just above where we create our new scene in the init() function, let’s initialise those variables to be the window sizes:

sceneWidth = window.innerWidth;
sceneHeight = window.innerHeight;

Now, towards the bottom of init(), just before we call play(), let’s add a thing called an event listener. This is will react when it senses the window being resized and call a function called onWindowResize:

window.addEventListener("resize", onWindowResize, false);

Finally, right at the bottom of your index.js, let’s define the onWindowResize function. This just updates our variables and sets the new size of the scene based on the window size…

function onWindowResize() 
{
  //resize & align
  sceneHeight = window.innerHeight;
  sceneWidth = window.innerWidth;
  renderer.setSize(sceneWidth, sceneHeight);
  camera.aspect = sceneWidth / sceneHeight;
  camera.updateProjectionMatrix();
}

Back up your work

Right we’re at the final task for this workshop hooray, Nice one for making our “hello world”! Remember that this is a template that we can work from from now on. It’s really important that you save the completed thing (and name it so you can find it) in your personal codesandbox account.

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. Make sure you start as you mean to go on and make a “CPC” folder for this module, then another folder inside that one called “Template” to save your exported zip.

Stretch goal - gridhelper

One final thing to experiment with, try adding a grid helper to your scene and see how that changes the visual landscape. This is a tiny stretch goal so we’re not going to tell you exactly how to do it!