This chapter links to the course’s interactive demos and uses MathJax for its formulas. Read it through the course website (or download the file and open it in a browser); a Canvas inline preview will reach neither the demos nor the math. All chapters.

WebGL and three.js for This Course: the Server, the Scene Graph, and the Course Library

The Khronos Group released the WebGL 1.0 specification in March 2011, bringing GPU rendering to the browser without plug-ins. Khronos Group, WebGL 1.0 specification, 2011

Each homework (HW1 to HW6) can be done in either of two tracks, Unity or WebGL, and HW7 and HW8 in JavaScript or Python. The Unity chapter serves the first track; this chapter serves the second. The WebGL track runs in a browser on the course’s own JavaScript library, the same code behind every demo in this text, drawn with three.js, a widely used library over the browser’s WebGL interface. There is nothing to install beyond a browser and Python. The price is that a browser is a stricter host than an editor: a page must be served, modules must be imported by exact relative paths, and errors appear in a console rather than a window. The chapter covers that ground first, then the four things a graphics course needs from three.js (the transform hierarchy, the frame loop, the matrix type and its storage, and the difference between its conventions and the text’s), then the course library and the homework runner. It is not a three.js tutorial: lights, shadows, post-processing and loaders are left out.

Contents
  1. Serving the course: one folder, one server
  2. ES modules and import paths
  3. The browser’s developer tools
  4. three.js’s object model, against Unity’s
  5. The render loop and the clock
  6. Object3D: local, world, parent
  7. Conventions, against the text’s
  8. Geometry, materials, textures, cameras
  9. The course library
  10. How a homework runs and reports
  11. The course rule: implement and replace
  12. Exercises

1 · Serving the course: one folder, one server

Clone or download the homework starter repository, the folder that holds index.html, lib/ and homework/ (git names it UWB-CSS-551-AU26-homework; this chapter calls it css551/), and, in a terminal, start a static web server in it:

cd css551
python3 -m http.server 8000

Then open http://localhost:8000/ for the list of released homework, or http://localhost:8000/homework/run.html?hw=hw01 for the first homework. The server does nothing but hand files to the browser; stop it with Ctrl-C. Any port works, but every page of the course uses relative paths, so the server must be started in css551/ itself, not in a subfolder, or the pages will not find the library.

A diagram: the course folder on disk is read by a static server started with python3 -m http.server 8000, which answers the browser's requests for http://localhost:8000 pages; run.html's import of ../lib/skeletons/runner.js is resolved against run.html's URL. A dashed red box below: a page opened as file:// has origin null, and the browser refuses every module import from it with a CORS error.
One folder, one server, one browser. The course’s pages are ES modules, and a browser loads modules only over HTTP.
Pitfall Double-clicking run.html opens it as file:///…/run.html, and the page stays blank. The browser treats a file page’s origin as null and refuses to load modules from it. The console says so, and it is worth recognizing the message, because it names the file it refused rather than the cause:
Access to script at 'file:///…/css551/lib/skeletons/runner.js' from origin 'null' has been
blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome,
chrome-untrusted, data, http, https.
The fix is never to change the code; it is to serve the folder.

2 · ES modules and import paths

Every file of the course library is an ES module: it says what it shares with export and what it needs with import. A page starts the chain with <script type="module">.

// lib/skeletons/hw01.js, the file you edit in HW1
import { dot, cross, normalize, sub } from '../core/xform.js';

export function advance(p, velocity, dt) { /* yours */ }
Definition (how an import is found) A relative import ('./x.js', '../core/xform.js') is resolved against the URL of the importing file, not the page and not the folder the server runs in. From lib/skeletons/hw01.js, '../core/xform.js' is lib/core/xform.js. The extension is required; a missing .js or a wrong case is a 404 on a case-sensitive server. Each module runs once per page however many times it is imported, and its top-level names are private unless exported.

The browser caches modules aggressively. After editing a file, reload with the cache bypassed (Shift-reload, or Cmd-Shift-R on a Mac, Ctrl-Shift-R elsewhere), or keep the developer tools open with “Disable cache” checked in the Network tab. The homework runner adds a fresh query string to your file’s URL on every load, so your own file is never stale; the library files can be.

Pitfall A syntax error in a module stops the whole chain: nothing that imports it runs, so the page looks dead rather than broken. The console names the file and line. The homework runner catches the most common case, a syntax error in your own file, and prints it above the scene in place of the checks.

3 · The browser’s developer tools

Open them with F12, or Cmd-Option-I on a Mac. Three panels do almost all the work.

Worked example (inspecting a matrix) A THREE.Matrix4 logged with console.log(m) shows an object whose elements array is the sixteen numbers in storage order, down the columns. To see it the way the text prints a matrix, log its rows:
for (let r = 0; r < 4; r++) console.log([0, 1, 2, 3].map((c) => m.elements[4 * c + r].toFixed(3)));
The same line works on the course library’s plain 16-element arrays, since the two share one layout (Section 7). console.table on a 4-by-4 array of rows is another readable form.

4 · three.js’s object model, against Unity’s

Definition (the object model) A Scene is the root of a tree of Object3D nodes. Every node has a transform (position, quaternion or rotation, scale) and children. A Mesh is an Object3D that also holds a BufferGeometry (the arrays of the meshes chapter) and a Material (how to shade it); a PerspectiveCamera is an Object3D that also holds the projection parameters; lights and groups are Object3Ds too. A WebGLRenderer owns the canvas, and renderer.render(scene, camera) draws the tree once from that camera.
A diagram: Scene, the root Object3D, above Object3D with position, quaternion and scale; below it Mesh, PerspectiveCamera, and Group or Light; Mesh points to a BufferGeometry with attributes and an index, and to a Material, a shader with uniforms; a WebGLRenderer box draws every visible Mesh with its camera. A side table maps Scene to scene, Object3D to GameObject plus Transform, Mesh to MeshFilter plus MeshRenderer, Material to Material, and WebGLRenderer to the engine's render loop.
three.js’s classes and their Unity counterparts. A Unity GameObject carries components; a three.js node is the component, by subclass.

The largest difference from Unity is that nothing runs by itself. There is no editor, no Play button and no script lifecycle: your code creates the scene, adds objects, and calls render when the picture should change. The course’s demos are built that way, and so is every homework view.

5 · The render loop and the clock

An animation in a browser is a function that asks to be called before the next screen refresh, does one frame’s work, and asks again. requestAnimationFrame(f) calls f once, with a timestamp in milliseconds; the loop is the function scheduling itself. It is the rasterization chapter’s loop, written by hand:

let last = 0;
function frame(t) {                       // t: milliseconds since the page loaded
  const dt = last ? (t - last) / 1000 : 0;  // the frame's length in seconds
  last = t;
  cube.position.x += 3 * dt;              // 3 units per second, at any frame rate
  renderer.render(scene, camera);
  requestAnimationFrame(frame);           // ask for the next frame
}
requestAnimationFrame(frame);
Worked example (per frame against per second) cube.position.x += 1 in frame moves one unit per refresh: 60 units a second on a 60 Hz display, 144 on a 144 Hz one. += 3 * dt moves 0.1 per frame at 30 Hz, 0.05 at 60 Hz and 0.0208 at 144 Hz, and 3 units after one second on all three. This is the same arithmetic as Unity’s Time.deltaTime (the Unity chapter draws it); the difference is that the browser hands you a timestamp and you compute the step yourself. THREE.Clock’s getDelta() does the subtraction for you.

There is no FixedUpdate. A simulation that needs a constant step, such as the cloth and fluid of the animation chapter, runs the accumulator of the interactive-systems chapter inside frame: add dt to a bank and take fixed steps while the bank holds one. Two browser behaviors matter. A hidden tab gets no animation frames at all, so the first dt after returning can be seconds long and should be clamped. And a page that calls requestAnimationFrame forever keeps the graphics card busy even when nothing changes; the course’s demos render on demand instead, running the loop only while a key is held and drawing once per slider change.

6 · Object3D: local, world, parent

Definition (an Object3D’s transform) position, quaternion and scale are the node’s transform relative to its parent, the \(L\) of the scene-graph chapter; rotation is the same rotation as Euler angles in radians, kept in step with quaternion. matrix is \(L = T\,R\,S\), rebuilt from those fields when matrixAutoUpdate is on (the default); matrixWorld is \(W = W_{\text{parent}}\,L\). Both are refreshed by updateMatrixWorld(), which renderer.render calls for the whole scene before drawing. parent.add(child) makes the link; parent.attach(child) reparents while keeping the world pose. getWorldPosition(v), localToWorld(v) and worldToLocal(v) read and apply \(W\).
Worked example (local to world, by hand, by three.js and by the library) Parent at \((2, 0, 0)\) turned \(90^\circ\) about \(y\); child at local \((1, 0, 0)\); grandchild at local \((0, 1, 0)\) under the child. The rotation chapter’s \(R_y(90^\circ)\) sends \((1, 0, 0)\) to \((0, 0, -1)\), so the child is at \((2, 0, -1)\) and the grandchild at \((2, 1, -1)\), the same chain as the Unity chapter’s worked example and with the same answer:
node -e "import('./lib/vendor/three.module.js').then(T=>{const p=new T.Object3D();p.position.set(2,0,0);p.rotation.y=Math.PI/2;const c=new T.Object3D();c.position.set(1,0,0);p.add(c);const g=new T.Object3D();g.position.set(0,1,0);c.add(g);p.updateMatrixWorld(true);console.log(g.getWorldPosition(new T.Vector3()).toArray().map(v=>+v.toFixed(3)))})"
[ 2, 1, -1 ]
The course library’s matMul(matMul(P, C), G) of the three makeTRS matrices puts the grandchild at the same \((2, 1, -1)\) (figures/numbers-webgl.json, key chain). Note the radians: rotation.y = Math.PI / 2, not 90.
Pitfall rotation.y = 90 is a rotation by 90 radians: 5156.62°, which is 116.62° past fourteen whole turns. The object turns, to a strange angle, and no error is raised. three.js angles are radians everywhere (rotation, Euler, Quaternion.setFromAxisAngle); THREE.MathUtils.degToRad(90) converts. The camera’s fov is the one exception: it is in degrees.

7 · Conventions, against the text’s

three.js agrees with the text on more than Unity does: right-handed, \(y\) up, \(+z\) toward the viewer, column vectors, and matrices stored column by column. A picture drawn from the text matches the screen without mirroring. Three conventions still need naming.

the textthree.jsnote
\(\mathbf{a}\cdot\mathbf{b}\), \(\mathbf{a}\times\mathbf{b}\), \(|\mathbf{a}|\), \(\hat{\mathbf{a}}\)a.dot(b), new Vector3().crossVectors(a, b), a.length(), a.clone().normalize()most Vector3 methods change the vector they are called on and return it; clone() first when the original must survive
a \(4\times4\) matrix, column-majorMatrix4, m.elements[16]stored down the columns, exactly as the library’s arrays: translation at 12, 13, 14. m.multiply(n) is \(m\,n\); v.applyMatrix4(m) is \(m\,v\) with the divide by \(w\)
Euler anglesEuler(x, y, z, 'XYZ'), radiansthe default order 'XYZ' means \(R = R_x R_y R_z\): the \(z\) turn is applied first. The library’s makeTRS uses the same order; Unity’s is different (\(R_y R_x R_z\))
unit quaternion \((x, y, z, w)\)Quaternion(x, y, z, w)same order as the library’s quatFromAxisAngle; setFromAxisAngle(axis, rad); q.multiply(r) is \(q\,r\), \(r\) first
\(P\) of the viewing chaptercamera.projectionMatrixthe same sixteen numbers as the library’s perspective(fov, aspect, near, far): the largest difference for (45, 1.5, 0.1, 100) is \(4.4\times10^{-16}\). Call camera.updateProjectionMatrix() after changing fov, aspect, near or far
\(V\)camera.matrixWorldInversethe camera’s world matrix inverted; a camera looks down its own \(-z\), which is what the viewing chapter’s \(w\) axis says
Two 4 by 4 grids. Left: makeTRS(2, 0.5, minus 1, 0, 30, 0, 2, 2, 2) with each cell's value and its flat index, the index running down the columns and the translation column 2, 0.5, minus 1 at indices 12, 13, 14 highlighted. Right: new Matrix4().set(...elements), the transpose, with the translation now in the bottom row. Below: fromArray keeps the matrix and maps (1, 0, 0) to (3.732, 0.5, minus 2); set transposes it, w becomes 3, and (1, 0, 0) maps to (0.577, 0, 0.333).
One matrix stored, and the same sixteen numbers passed to set. set is the only Matrix4 method that takes its numbers in the order a matrix is printed.
Pitfall: Matrix4.set reads rows m.set(n11, n12, n13, n14, n21, …) takes its sixteen arguments row by row, the way a matrix is written on paper, and stores them down the columns. Every other way in (elements, fromArray, toArray, the library’s arrays) is column by column. So new Matrix4().set(...elements) builds the transpose. For the TRS matrix of the Unity chapter’s worked example (translation \((2, 0.5, -1)\), \(30^\circ\) about \(y\), scale 2):
node -e "Promise.all([import('./lib/vendor/three.module.js'),import('./lib/core/xform.js')]).then(([T,X])=>{const m=X.makeTRS(2,0.5,-1,0,30,0,2,2,2);const good=new T.Matrix4().fromArray(m),bad=new T.Matrix4().set(...m);const p=()=>new T.Vector3(1,0,0);console.log(p().applyMatrix4(good).toArray().map(v=>+v.toFixed(3)));console.log(p().applyMatrix4(bad).toArray().map(v=>+v.toFixed(3)))})"
[ 3.732, 0.5, -2 ]
[ 0.577, 0, 0.333 ]
The transpose has the translation in its bottom row, so \(w = 3\) after the multiply and applyMatrix4 divides by it: the point lands at \((0.577, 0, 0.333)\) instead of \((3.732, 0.5, -2)\). A check that compares sixteen entries shows the transpose at once; a picture only looks “a little off”. Use fromArray for a stored matrix and set only when typing one in by rows.

8 · Geometry, materials, textures, cameras

Geometry. A BufferGeometry holds named attributes, each a typed array cut into fixed-size items: position (3 numbers per vertex), normal (3), uv (2), and an optional index (3 per triangle, counter-clockwise seen from the front). It is the vertex and index arrays of the meshes chapter, laid out for the graphics card:

const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
g.setAttribute('normal',   new THREE.BufferAttribute(new Float32Array(normals), 3));
g.setIndex(indices);       // a plain array of integers works
// after editing an attribute's array in place:
g.attributes.position.needsUpdate = true;

The array is uploaded to the graphics card once; editing it in place changes nothing on screen until needsUpdate is set. computeVertexNormals() computes the averaged normals the meshes chapter derives by hand, and the homework asks you to compute them yourself (Section 11).

Material. MeshBasicMaterial draws a flat colour, MeshStandardMaterial lights with three.js’s physically based model, and ShaderMaterial runs shaders you write, which is how the homework’s lit sphere works; the shader chapter covers it. A material’s side chooses which faces are drawn (FrontSide, the default, or DoubleSide for a quad seen from both sides).

Texture and colour. A Texture wraps an image or a canvas; set texture.colorSpace = THREE.SRGBColorSpace for a photograph or painted image so its colours are converted to linear light before shading, and texture.needsUpdate = true after drawing into its canvas. three.js manages colour for you: new THREE.Color(0x808080) stores the linear value 0.2159, not 0.5, and the renderer converts back to sRGB on output (its outputColorSpace is sRGB by default). The colour chapter explains why shading must happen in linear light.

Camera. new THREE.PerspectiveCamera(fovYDeg, aspect, near, far) holds the parameters of the viewing chapter; position it like any Object3D and aim it with camera.lookAt(x, y, z), which turns its \(-z\) toward the point. When the canvas changes size, set camera.aspect, call updateProjectionMatrix(), and resize the renderer with renderer.setSize(w, h).

9 · The course library

The homework does not start from an empty page. It runs inside the course’s demo library, the code in lib/ behind every demo in this text. Four parts matter for the WebGL track.

modulewhat it gives you
lib/core/xform.jsthe hand-built math core, no three.js: sub, dot, cross, normalize on 3-arrays; makeTRS(tx, ty, tz, rx, ry, rz, sx, sy, sz) (degrees, order XYZ); axisAngleMatrix, quatFromAxisAngle (returns \([x, y, z, w]\)); matMul(a, b); invertTRS (for translate-rotate-scale matrices only); applyMat4(m, p) (returns \([x, y, z, w]\), no divide) and perspectiveDivide; lookAtBasis(eye, at, up) (returns \(u, v, w, V\)); perspective; uvMat3. Every matrix is a plain 16-element array in column-major order
lib/core/scene-shell.jsmakeScene(container): a scene, a PerspectiveCamera at \((3, 3, 6)\) looking at the origin, a renderer, a grid and axes, ready to add objects to
lib/core/demo-shell.js, cockpit.jsthe page frame every demo shares: the scene area, a help panel (the ? button), and the control rail with SliderRow, ButtonRow and ValueTable readouts
lib/core/orbit-camera.jsthe navigation of every 3D demo and homework view: drag to turn the scene, wheel to zoom, W, A, S, D to fly, the up and down arrows (or Space and Ctrl) to rise and sink, Q and E to roll, H for the home view, and Esc to give the keys back to a slide deck
Worked example (the library and three.js agree) makeTRS(2, 0.5, -1, 0, 30, 0, 2, 2, 2) returns the sixteen numbers \((1.732, 0, -1, 0,\; 0, 2, 0, 0,\; 1, 0, 1.732, 0,\; 2, 0.5, -1, 1)\), down the columns; a Matrix4 composed from the same position, rotation and scale has the same elements. A library matrix becomes a three.js one with new THREE.Matrix4().fromArray(m), and back with m4.toArray(), with no reordering in either direction.

10 · How a homework runs and reports

Each homework has one file to edit, lib/skeletons/hwNN.js, whose functions are stubs marked // TODO, and one page that runs it, homework/run.html?hw=hwNN. The page imports your file, runs the homework’s checks on your functions with the published inputs, prints a table under the scene, and draws the scene from your functions, so a wrong function shows twice: as a failed row and as a wrong picture.

  1. Start the server in css551/ and open http://localhost:8000/homework/run.html?hw=hw01.
  2. Read the checks table: each row is one check, with its points, yours (what your function returned) and expected. An untouched skeleton returns placeholders and scores 0.
  3. Implement one function, save, reload (Section 2: your file is never cached), and watch its rows turn green.
  4. A row that says threw means your function raised an error; the message is in the row and the line number is in the console.

The table reports the published inputs only. Grading runs the same checks on a second, hidden set of inputs, so a function that returns the expected numbers without computing them scores on the first and not on the second. The homework pages list every check and its tolerance.

Worked example (reading a failing check) The first four rows for the untouched HW1 skeleton, as the runner prints them:
checkptsyoursexpected
✗advance: position after 1 s at 60 frames per second10(0, 0, 0)(3, 0, 0)
✗advance: position after 1 s at 20 frames per second10(0, 0, 0)(3, 0, 0)
✗hitTest2D circle: inside, just outside, on the circle6(false, false, false)(true, false, true)
✗hitTest2D segment: on it, at tol, beyond tol, past endpoint b7(false, false, false, false)(true, true, false, false)
Read yours against expected as a diagnosis. All zeros, or all false, is a stub. A hit test that is right everywhere except on the boundary compares with < where the definition says ≤. A list that matches the expected one with entries 1 and 4, 2 and 8, 3 and 12 (and so on) swapped is a transpose, the Matrix4.set trap of Section 7. A vector of the right direction and the wrong length is a missing normalize or a missing \(dt\). A result that is right at 60 frames per second and wrong at 20 is a step that ignores \(dt\).

11 · The course rule: implement and replace

Definition (implement and replace) Where a homework says so, the library’s and three.js’s convenience helpers are off limits and the operation is built from primitives, then checked against the helper. The helper is the answer key, not the tool.

The helpers each homework forbids, in both tracks, are listed in one table, in the Unity chapter’s Section 11, whose third column is the three.js list; this chapter does not repeat it, so there is one authority. For the WebGL track the allowed primitives are the library’s sub, dot, cross, normalize (and, where a homework says so, matMul and applyMat4), component access, arithmetic and Math. The head of each skeleton file names its own allowed and off-limits list, and each homework page is the authority for that homework.

Hands-on Four tasks, in a terminal in css551/ and in the browser.
  1. The blank page. Open homework/run.html by double-clicking it, then open the console. Expected: the CORS message of Section 1 naming runner.js, followed by Failed to load resource: net::ERR_FAILED. Start the server and open http://localhost:8000/homework/run.html?hw=hw01 instead; the checks table appears with 0 / 100 on the published inputs.
  2. The chain, checked. Run the command of Section 6 and confirm [ 2, 1, -1 ]; then change Math.PI/2 to 90 and run it again. The grandchild is no longer at \((2, 1, -1)\), and nothing complains.
  3. The transpose. Run the command of Section 7 and confirm the two lines [ 3.732, 0.5, -2 ] and [ 0.577, 0, 0.333 ]. Then print new T.Matrix4().set(...m).transpose().elements and compare it with m: they are equal.
  4. Projection, compared.
    node -e "Promise.all([import('./lib/vendor/three.module.js'),import('./lib/core/xform.js')]).then(([T,X])=>{const c=new T.PerspectiveCamera(45,1.5,0.1,100);const a=c.projectionMatrix.elements,b=X.perspective(45,1.5,0.1,100);console.log(Math.max(...a.map((v,i)=>Math.abs(v-b[i]))))})"
    4.440892098500626e-16
    The library’s \(P\) and three.js’s agree to the last bit of a double: the viewing chapter’s matrix is the one the renderer uses.

12 · Exercises

Exercise 1 (the import path) A file lib/skeletons/hw03.js contains import { matMul } from 'core/xform.js';. What does the browser report, and what is the fix?
Solution A bare specifier with no ./ or ../ is not a relative path; the browser refuses it (“Failed to resolve module specifier”). From lib/skeletons/, .. is lib/, so the import is '../core/xform.js'.
Exercise 2 (the loop) A loop moves an object 2 units per second with += 2 * dt. On a 90 Hz display, how far does it move per frame, and what goes wrong if the tab is hidden for 5 seconds and dt is not clamped?
Solution \(2/90 = 0.0222\) per frame. A hidden tab gets no frames, so the first frame back has \(dt \approx 5\) and the object jumps 10 units in one step; clamp dt to, say, 0.1 s.
Exercise 3 (local and world) The parent of Section 6 is turned \(180^\circ\) about \(y\) instead. Where are the child and the grandchild?
Solution \(R_y(180^\circ)(1, 0, 0) = (-1, 0, 0)\), so the child is at \((2 - 1, 0, 0) = (1, 0, 0)\) and the grandchild at \((1, 1, 0)\).
Exercise 4 (storage) For the matrix of Section 7, what are elements[4], elements[5], elements[6], and what are they geometrically?
Solution \((0, 2, 0)\): column 1, where the \(y\) axis went. A rotation about \(y\) leaves it in place, and the scale doubles it.
Exercise 5 (the transpose, read from a check) A student’s function for \(T(2, 0.5, -1)\,R_y(30^\circ)\,S(2)\), meant to return a column-major 16-array, returns (1.7321, 0, 1, 2, 0, 2, 0, 0.5, -1, 0, 1.7321, -1, 0, 0, 0, 1). What did they do?
Solution They wrote the rows in order: the translation \((2, 0.5, -1)\) sits at indices 3, 7, 11, the bottom row of a column-major array. It is the transpose of the expected matrix, the result of the Matrix4.set habit applied to a plain array.
Exercise 6 (Euler order) new THREE.Vector3(1, 0, 0).applyEuler(new THREE.Euler(Math.PI / 6, 0, Math.PI / 2)): where does the vector go, and where would it go in the order 'ZYX'?
Solution In the default 'XYZ' the \(z\) turn comes first: \((1, 0, 0) \to (0, 1, 0)\), then \(30^\circ\) about \(x\) gives \((0, 0.866, 0.5)\). In 'ZYX' the \(x\) turn comes first and leaves \((1, 0, 0)\) alone, then the \(z\) turn gives \((0, 1, 0)\) (numbers-webgl.json, key euler).
Exercise 7 (mutation) const d = target.position.sub(eye.position); computes a direction. What else just happened?
Solution sub changes the vector it is called on, so the target has moved to the difference. Write target.position.clone().sub(eye.position), or new THREE.Vector3().subVectors(target.position, eye.position).
Exercise 8 (implement and replace) HW4 forbids Matrix4.lookAt and the library’s lookAtBasis. With the allowed primitives, write the three lines that give the camera’s \(u\), \(v\), \(w\), and say what to check them against.
Solution const w = normalize(sub(eye, at)); const u = normalize(cross(up, w)); const v = cross(w, u);, the basis of the viewing chapter. Check against lookAtBasis(eye, at, up) or against a camera placed at eye with camera.lookAt(at): its world matrix’s first three columns are \(u\), \(v\), \(w\).
Exercise 9 (colour) A material colour is set with new THREE.Color(0x808080), and a shader you wrote outputs it unchanged without three.js’s output conversion. Is the result lighter or darker than the 50% grey the hex code says, and why?
Solution Darker. The colour is stored as linear 0.2159; written straight to the sRGB framebuffer, the pixel gets the code 0.2159 (55 of 255) instead of 0.5 (128). Converting on output (the shader chapter’s colorspace_fragment) restores 128.
Exercise 10 (the projection, by code) Print camera.projectionMatrix.elements for new THREE.PerspectiveCamera(90, 1, 1, 3) and identify the four nonzero entries of its first three columns by their formulas.
Solution \(f = 1/\tan(45^\circ) = 1\): elements[0] \(= f/\text{aspect} = 1\), elements[5] \(= f = 1\); with near \(N = 1\) and far \(F = 3\), elements[10] \(= -(F + N)/(F - N) = -4/2 = -2\); elements[11] \(= -1\) (the \(w = -z\) of the divide); and elements[14] \(= -2FN/(F - N) = -3\), in the fourth column. Printed: 1 0 0 0 0 1 0 0 0 0 -2 -1 0 0 -3 0. The same as the library’s perspective(90, 1, 1, 3).