Workbook 6, Page 1: The THREE.js library

Workbook 6, Page 1: The THREE.js library

On this page, we'll just deal with the mechanics of using the THREE.js library, and some of the issues specific to using the library in the way we've been programming in class.

Box 1: Check that things work

Just to make sure that things work, here is a simple scene made with the THREE library. You should see a spinning green cube.

If you don't see the picture, something is wrong.

Box 2: Mixing old and new style JavaScript

As we pointed out at the beginning of the semester, JavaScript is a much better language today than it was historically. And, since in class we can assume that the person running our program is using a modern browser, we can program in a modern version of the language.

There is a downside to this: just because we can program in modern JavaScript doesn't mean that everyone else can. So, if we use someone else's code they might have written it in old style JavaScript.

THREE.js was written before there was modern JavaScript. The main parts of it have been updated, but not all of it. We will need to use some of those non-updated parts, which means we need to use the "old style" versions of the whole thing. (I believe that the THREE code itself has been updated, its more that it provides "old style interfaces").

This means:

  1. We must load the three.js file from our html file before we start our program. Our file might load first, so we need to wait for window.onload to finish.
  2. When it loads, THREE will create a global variable (called THREE) in the global namespace of the JavaScript page. We cannot use this variable name.
  3. We can use the THREE variable to access things within the library, even if our program is written in modern JavaScript.

One problem: while the browser is willing to let us mix old-style and modern JavaScript, the tools for error checking might try to tell us we shouldn't use old-style stuff. In particular, Visual Studio Code keeps telling me there is a problem.

Here is the same code as in Box 1, but now made a little more modern, and using the tricks I've been using in previous workbooks to get Visual Studio Code to help me find my bugs:

While this works just fine, Visual Studio Code tells me I have 8 problems. All of the form "Cannot find name 'THREE'" - which shouldn't be surprising. THREE isn't defined in this file anywhere. So while the browser will have no problem at runtime (since we'll load THREE), Visual Studio Code (and more specifically, the TypeScript code checker) needs to be told about THREE.

NOTE: We do not care if your program has type errors or linter warnings, as long as it runs.

If you don't use Visual Studio Code, or prefer to turn off type checking, you don't need to worry about the problems described in the rest of this box. This will just explain why my code looks a little different than you might expect.

You may find that using the type checker and linter will save you tons of time as it helps you find your bugs.

One solution: we can load THREE using modern JavaScript modules. We could use:

import THREE from "three";

But this has the problem that the file we have, "three.js" is not a proper modern JavaScript module. So while it works fine for type checking, the code doesn't run. Since we're not programming in TypeScript, we can't use TypeScript's module loader.

I have found a workaround. If we have the TypeScript definitions for THREE (I have placed them in the THREE/threets directory in the workbook), we can tell the TypeScript checker that the THREE variable has the same type as the THREE module:

/**  @type typeof import("./THREE/threets/index"); */
let THREEmod = THREE;

What I've really done is introduced a new variable THREEmod and told typescript (via the @type comment) that it has the same type as the thing we get when we import the module. The type inferencing system then infers that THREE must have the same type. This is a weird hack, but it's only required to get the nice type checking in Visual Studio Code.

Sometimes (and I can't figure out when), the typescript type checker will still give a warning:

'THREE' refers to a UMD global, but the current
file is a module. Consider adding an import
instead. ts(2686)
which is correct (THREE is an old-fashioned Java import), but is unhelpful (I have considered adding an import, but I can't). The only robust answer I have found is to access the new variable (THREEmod above), rather than THREE.

If you do the magic comment / module assignment anywhere in your project, Visual Studio Code sometimes will pick up on what THREE is. So maybe you didn't even see the problems in 1-three-2.js because Visual Studio Code figured things out.

In future programs, I will do:

/**  @type typeof import("./THREE/threets/index"); */
let T;
// @ts-ignore

Which defines a new variable T that has the right typing information (so TypeScript knows the types). I split the assignment on two lines so I can tell TypeScript to ignore the warning when I use THREE.

Box 3: THREE and HTML

THREE actually draws into a Canvas element. It doesn't use the Canvas 2D API - it uses WebGL (a different API that we will learn about later).

Normally, THREE creates the Canvas element for us. In the examples above there was code

let renderer = new THREE.WebGLRenderer();
renderer.setSize( 200,200 ); // was (window.innerWidth, window.innerHeight );
document.getElementById("three2").appendChild( renderer.domElement );

Which created a WebGLRenderer object, which as part of it has an HTMLCanvasElement (where the drawing will happen). The setSize sets the size of the Canvas (in HTML pixels). The last line finds a container element called three2 (it's a DIV) that I put into my HTML where I want to put the newly created canvas, and puts the Canvas into it.

If we had a Canvas already, we could tell THREE to use it instead. So here is a Canvas in HTML:

And the code looks like:

let canvas = /** @type {HTMLCanvasElement} */ (document.getElementById("three3"));
let renderer = new THREE.WebGLRenderer({"canvas":canvas});

Basically, I had to find the Canvas and pass it to the constructor of the renderer.

There is one subtle thing here that is a common paradigm in THREE programming (in fact, in JavaScript programming in general). Note how I pass the parameters to the WebGLRenderer constructor as an object {"canvas":canvas}. Since JavaScript doesn't support keyword arguments (like Python), we need some mechanism for specifying the parameters we want, without having to specify all of the parameters. So instead of passing a list of parameters, we pass an object with parameters in it.

For example, suppose we have a function like:

function foo(a=1,b=2,c=3,d=4) {
  // do something with a, b, c and d

and we want to call it with the default parameters for everything except d and we wanted d to be 7, we're stuck. We need to write foo(1,2,3,7), if we wanted d=7. If this were Python, we could write foo(d=7), but it's not. So instead we define the function to take one object that holds the parameters:

function foo(object) {
  let a = "a" in object ? object.a : 1;
  let b = "b" in object ? object.b : 2;
  let c = "c" in object ? object.c : 3;
  let d = "d" in object ? object.d : 4;
  // do something with a, b, c and d

so we can write foo({d:7}) (remember JavaScript quoting rules, I don't need quotes around d). This is a little bit of a hassle when we create the function, but very convenient when we use it (especially for functions with lots of arguments). Fortunately, the author of THREE wrote a lot of his code this way.

Summary: Using THREE

So now, you can hopefully create a THREE program with the THREE library. Now lets try to figure out how to use THREE on Page 2.