Browse Source

debugging article

Gregg Tavares 6 years ago
parent
commit
09233c8b6f

BIN
threejs/lessons/resources/images/debugging-nan.gif


BIN
threejs/lessons/resources/images/devtools-chrome-disable-cache.jpg


BIN
threejs/lessons/resources/images/devtools-chrome-settings.jpg


BIN
threejs/lessons/resources/images/devtools-chrome.jpg


BIN
threejs/lessons/resources/images/devtools-console-object.gif


BIN
threejs/lessons/resources/images/devtools-enable-safari.jpg


BIN
threejs/lessons/resources/images/devtools-firefox.jpg


BIN
threejs/lessons/resources/images/devtools-safari.jpg


BIN
threejs/lessons/resources/images/nan-banana.png


+ 20 - 0
threejs/lessons/threejs-debugging-glsl.md

@@ -0,0 +1,20 @@
+Title: Debugging Three.js - GLSL
+Description: How to debug GLSL Shaders
+
+Check your shaders
+  Use the shader inspector OR
+  Add a shader dumper
+  Use WEBGL_Debug_Shaders
+
+Change fragment shader
+  color = red
+  show normals
+  show texture coordinates
+  show lighting
+
+Change Vertex Shader
+  simplest shader
+
+
+
+

+ 528 - 0
threejs/lessons/threejs-debugging-javascript.md

@@ -0,0 +1,528 @@
+Title: Three.js Debugging JavaScript
+Description: How to debug JavaScript with THREE.js
+
+Most of this article is not directly about THREE.js but is
+rather about debugging JavaScript in general. It seemed important in
+that many people just starting with THREE.js are also just
+starting with JavaScript so I hope this can help them more easily
+solve any issues they run into.
+
+Debugging is a big topic and I probably can't begin to cover
+everything there is to know but if you're new to JavaScript
+then here's an attempt to give a few pointers. I strongly
+suggest you take some time to learn them. They'll help you 
+enormously in your learning.
+
+## Learn your Browser's Developer Tools
+
+All browsers have developer tools. 
+[Chrome](https://developers.google.com/web/tools/chrome-devtools/),
+[Firefox](https://developer.mozilla.org/son/docs/Tools), 
+[Safari](https://developer.apple.com/safari/tools/), 
+[Edge](https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide).
+
+In Chrome you can click the the `⋮` icon, pick More Tools->Developer Tools
+to get to the developer tools. A keyboard shortcut is also shown there.
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-chrome.jpg" style="width: 789px;"></div>
+
+In Firefox you click the `☰` icon, pick "Web Developer", then pick
+"Toggle Tools"
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-firefox.jpg" style="width: 786px;"></div>
+
+In Safari you first have to enable the Develop menu from the 
+Advanced Safari Preferences.
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-enable-safari.jpg" style="width: 775px;"></div>
+
+Then in the Develop menu you can pick "Show/Connect Web Inspector".
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-safari.jpg" style="width: 777px;"></div>
+
+With Chrome you can also use Chrome on your computer to debug webpages
+running on Chrome on your Android phone or tablet. Similarly with
+Safari you can use your computer to debug webpages running on Safari
+on iPhones and iPads.
+
+I'm most familiar with Chrome so this guide will be using Chrome
+as an example when refering to tools but most browsers have similar
+features so it should be easy to apply anything here to all browsers.
+
+## Turn off the cache
+
+Browsers try to reuse data they've already downloaded. This is great
+for users so if you visit a website a second time many of the files
+used to display the site will not have be downloaded again.
+
+On the other hand this can be bad for web development. You change
+a file on your computer, reload the page, and you don't see the changes
+because the browser uses the version it got last time.
+
+One solution during web development is to turn off the cache. This
+way the browser will always get the newest versions of your files.
+
+First pick settings from the corner menu
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-chrome-settings.jpg" style="width: 778px"></div>
+
+Then pick "Disable Cache (while DevTools is open)".
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-chrome-disable-cache.jpg" style="width: 779px"></div>
+
+## Use the JavaScript console
+
+Inside all devtools is a *console*. It shows warnings and error messages.
+You can print your own info to the console with with `console.log` as in
+
+```
+console.log(someObject.position.x, someObject.position.y, someObject.position.z);
+```
+
+Even cooler, if you log an object you can inspect it. For example if we log
+the root scene object from [the gLTF article](threejs-load-gltf.html)
+
+```
+  {
+    const gltfLoader = new THREE.GLTFLoader();
+    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
+      const root = gltf.scene;
+      scene.add(root);
++      console.log(root);
+```
+
+Then we can expand that object in the JavaScript console
+
+<div class="threejs_center"><img class="border" src="resources/images/devtools-console-object.gif"></div>
+
+You can also use `console.error` which reports the message in red
+in includes a stack trace.
+
+## Put data on screen
+
+Another obvious but often overlooked way is to add `<div>` or `<pre>` tags
+and put data in them.
+
+The most obvious way is to make some HTML elements
+
+```
+<canvas id="c"></canvas>
++<div id="debug">
++  <div>x:<span id="x"></span></div>
++  <div>y:<span id="y"></span></div>
++  <div>z:<span id="z"></span></div>
++</div>
+```
+
+Style them so they stay on top of the canvas. (assuming your canvas
+fills the page)
+
+```
+<style>
+#debug {
+  position: absolute;
+  left: 1em;
+  top: 1em;
+  padding: 1em;
+  background: rgba(0, 0, 0, 0.8);
+  color: white;
+  font-family: monospace;
+}
+</style>
+```
+
+And then looking the elements up and setting their content.
+
+```
+// at init time
+const xElem = document.querySelector('#x');
+const yElem = document.querySelector('#y');
+const zElem = document.querySelector('#z');
+
+// at render or update time
+xElem.textContent = someObject.position.x.toFixed(3);
+yElem.textContent = someObject.position.y.toFixed(3);
+zElem.textContent = someObject.position.z.toFixed(3);
+```
+
+This is more useful for real time values
+
+{{{example url="../threejs-debug-js-html-elements.html" }}}
+
+Another way to put data on the screen is to make a clearing logger.
+I just made that term up but lots of games I've worked on have used this solution. The idea
+is you have a buffer that displays messages for only one frame.
+Any part of your code that wants to display data calls some function
+to add data to that buffer every frame. This is much less work
+than making an element per piece of data above.
+
+For example let's change the HTML from above to just this
+
+```
+<canvas id="c"></canvas>
+<div id="debug">
+  <pre></pre>
+</div>
+```
+
+And let's make simple class to manage this *clear back buffer*.
+
+```
+class ClearingLogger {
+  constructor(elem) {
+    this.elem = elem;
+    this.lines = [];
+  }
+  log(...args) {
+    this.lines.push([...args].join(' '));
+  }
+  render() {
+    this.elem.textContent = this.lines.join('\n');
+    this.lines = [];
+  }
+}
+```
+
+Then let's make a simple example that everytime we click the mouse makes a mesh that moves in a random direction for 2 seconds. We'll start with
+one of the examples from the article on [making things responsive](threejs-responsive.html)
+
+Here's the code that adds a new `Mesh` everytime we click the mouse
+
+```
+const geometry = new THREE.SphereBufferGeometry();
+const material = new THREE.MeshBasicMaterial({color: 'red'});
+
+const things = [];
+
+function rand(min, max) {
+  if (max === undefined) {
+    max = min;
+    min = 0;
+  }
+  return Math.random() * (max - min) + min;
+}
+
+function createThing() {
+  const mesh = new THREE.Mesh(geometry, material);
+  scene.add(mesh);
+  things.push({
+    mesh,
+    timer: 2,
+    velocity: new THREE.Vector3(rand(-5, 5), rand(-5, 5), rand(-5, 5)),
+  });
+}
+
+canvas.addEventListener('click', createThing);
+```
+
+And here's the code that moves the meshes we created, logs them,
+and removes them when their timer has run out
+
+```
+const logger = new ClearingLogger(document.querySelector('#debug pre'));
+
+let then = 0;
+function render(now) {
+  now *= 0.001;  // convert to seconds
+  const deltaTime = now - then;
+  then = now;
+
+  ...
+
+  logger.log('fps:', (1 / deltaTime).toFixed(1));
+  logger.log('num things:', things.length);
+  for (let i = 0; i < things.length;) {
+    const thing = things[i];
+    const mesh = thing.mesh;
+    const pos = mesh.position;
+    logger.log(
+        'timer:', thing.timer.toFixed(3), 
+        'pos:', pos.x.toFixed(3), pos.y.toFixed(3), pos.z.toFixed(3));
+    thing.timer -= deltaTime;
+    if (thing.timer <= 0) {
+      // remove this thing. Note we don't advance `i`
+      things.splice(i, 1);
+      scene.remove(mesh);
+    } else {
+      mesh.position.addScaledVector(thing.velocity, deltaTime);
+      ++i;
+    }
+  }
+
+  renderer.render(scene, camera);
+  logger.render();
+
+  requestAnimationFrame(render);
+}
+```
+
+Now click the mouse a bunch in the example below
+
+{{{example url="../threejs-debug-js-clearing-logger.html" }}}
+
+## Query Parameters
+
+Another thing to remember is that webpages can have data passed
+into them either via query parameters or the anchor, sometimes called
+the search and the hash.
+
+    https://domain/path/?query#anchor
+
+You can use this to make features optional or pass in parameters.
+
+For example let's take the previous example and make it so
+the debug stuff only shows up if we put `?debug=true` in the URL.
+
+First we need some code to parse the query string
+
+```
+/**
+  * Returns the query parameters as a key/value object. 
+  * Example: If the query parameters are
+  *
+  *    abc=123&def=456&name=gman
+  *
+  * Then `getQuery()` will return an object like
+  *
+  *    {
+  *      abc: '123',
+  *      def: '456',
+  *      name: 'gman',
+  *    }
+  */
+function getQuery() {
+  const params = {};
+  const q = (window.location.search || '').substring(1);
+  q.split('&').forEach((pair) => {
+    const keyValue = pair.split('=').map(decodeURIComponent);
+    params[keyValue[0]] = keyValue[1];
+  });
+  return params;
+}
+```
+
+Then we might make the debug element not show by default
+
+```
+<canvas id="c"></canvas>
++<div id="debug" style="display: none;">
+  <pre></pre>
+</div>
+```
+
+Then in the code we read the params and choose to unhide the
+debug info if and only if `?debug=true` is passed in
+
+```
+const query = getQuery();
+const debug = query.debug === 'true';
+const logger = debug
+   ? new ClearingLogger(document.querySelector('#debug pre'))
+   : new DummyLogger();
+if (debug) {
+  document.querySelector('#debug').style.display = '';
+}
+```
+
+We also made a `DummyLogger` that does nothing and chose to use it if `?debug=true` has not been passed in.
+
+```
+class DummyLogger {
+  log() {}
+  render() {}
+}
+```
+
+You can see if we use this url:
+
+<a target="_blank" href="../threejs-debug-js-params.html">threejs-debug-js-params.html</a>
+
+there is no debug info but if we use this url:
+
+<a target="_blank" href="../threejs-debug-js-params.html?debug=true">threejs-debug-js-params.html?debug=true</a>
+
+there is debug info.
+
+Multiple paramters can be passed in by separating with '&' as in `somepage.html?someparam=somevalue&someotherparam=someothervalue`. 
+Using parameters like this we can pass in all kinds of options. Maybe `speed=0.01` to slow down our app for making it easier to understand something or `showHelpers=true` for whether or not to add helpers
+that show the lights, shadow, or camera frustum seen in other lessons.
+
+## Learn to use the Debugger
+
+Every browser has a debugger where you can pause your program
+step through line by line and inspect all the variables.
+
+Teaching you how to use a debugger is too big a topic for this
+article but here's a few links
+
+* [Get Started with Debugging JavaScript in Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/javascript/)
+* [Debugging in Chrome](https://javascript.info/debugging-chrome)
+* [Tips and Tricks for Debugging in Chrome Developer Tools](https://hackernoon.com/tips-and-tricks-for-debugging-in-chrome-developer-tools-458ade27c7ab)
+
+## Check for `NaN` in the debugger or elsewhere
+
+`NaN` is short for Not A Number. It's what JavaScript will assign
+as a value when you do something that doesn't make sense mathwise.
+
+As a simple example
+
+<div class="threejs_center"><img class="border" src="resources/images/nan-banana.png" style="width: 180px;"></div>
+
+Often when I'm making something and nothing appears on the screen
+I'll check some values and if I see `NaN` I will instantly have a 
+place to start looking.
+
+As an example when I first started making the path for the [article about loading gLTF files](threejs-load-gltf.html) I made a curve using the
+`SplineCurve` class which makes a 2D curve.
+
+I then used that curve to move the cars like this
+
+```
+curve.getPointAt(zeroToOnePointOnCurve, car.position);
+```
+
+Internally `curve.getPointAt` calls the `set` function
+on the object passed as the second argument. In this case that
+second argument is `car.position` which is a `Vector3`. `Vector3`'s
+`set` function requires 3 arguments, x, y, and z but `SplineCurve` is a 2D curve
+and so it calls `car.position.set` with just x and y.
+
+The result is that `car.position.set` sets x to x, y to y, and z to `undefined`.
+
+A quick glance in the debugger looking at the car's `matrixWorld`
+showed a bunch of `NaN` values.
+
+<div class="threejs_center"><img class="border" src="resources/images/debugging-nan.gif" style="width: 476px;"></div>
+
+Seeing the matrix had `NaN`s in it suggested something like `position`, `rotation`, `scale` or some other function that affects that matrix had bad data. Working backward from their it was easy
+to track down the issue.
+
+In top of `NaN` there's also `Infinity` which is a similar sign there
+is a math bug somewhere.
+
+## Look In the Code!
+
+THREE.js is Open Source. Don't be afraid to look inside the code!
+You can look inside on [github](https://github.com/mrdoob/three.js).
+You can also look inside by stepping into functions in the debugger.
+When you do that consider using `three.js` instead of the more common
+`three.min.js`. `three.min.js` is the minified, compressed, and therefore
+smaller to download version. `three.js` is the larger but easier to
+debug version. I often switch my code to use `three.js` to step inside
+THREE.js to step through the code and see what is going on.
+
+## Put `requestAnimationFrame` at bottom of your render function.
+
+I see this pattern often
+
+```
+function render() {
+   requestAnimationFrame(render);
+
+   // -- do stuff --
+
+   renderer.render(scene, camera);
+}
+requestAnimationFrame(render);
+```
+
+I'd suggest that putting the call to `requestAnimationFrame` at
+the bottom as in
+
+```
+function render() {
+   // -- do stuff --
+
+   renderer.render(scene, camera);
+
+   requestAnimationFrame(render);
+}
+requestAnimationFrame(render);
+```
+
+The biggest reason is it means your code will stop if you have an
+error. Putting `requestAnimationFrame` at the top means your
+code will keep running even if you have an error since you already
+requested another frame. IMO it's better to find those errors than 
+to ignore them. They could easily be the reason something is not appearing as you expect it to but unless your code stops you might 
+not even notice.
+
+## Check your units!
+
+This bascially means knowning for example when to use degrees vs
+when to use radians. It's unfortunate that THREE.js does not
+consistently use the same units everywhere. Off the top of my head
+the camera's field of view is in degrees. All other angles are in
+radians.
+
+The other place to look out is your world unit size. Until
+recently 3D apps could choose any size they want. One app might choose
+1 unit = 1cm. Another might choose 1 unit = 1 foot. It's actually still
+true that you can chose any units you want for certain applications.
+That said, THREE.js assumes 1 unit = 1 meter. This is important for
+things like physicsally based rendering which uses meters to compute
+lighting effects. It's also important for AR and VR which need to
+deal with real world units like where your phone is or where the VR
+controllers are.
+
+## Making a *Minimal, Complete, Verifiable, Example* for Stack Overflow
+
+If you decide to ask a question about THREE.js it's almost always
+required for you to provide an MCVE which stands for Minimal, Complete,
+Verifiable, Example.
+
+The **Minimal** part is important. Let's say you where having
+an issue with the path moviement in the last example of the [loading a gLTF article](threejs-load-gltf.html). That example has many parts. Listing them out it has
+
+1. A bunch of HTML
+2. Some CSS
+3. Lights
+4. Shadows
+5. DAT.gui code to manipulate shadows
+6. Code to load a .GLTF file
+7. Code to resize the canvas.
+8. Code to move the cars along paths
+
+That's pretty huge. If your question is only about the path following
+part you can remove most of the HTML as you only need a `<canvas>` and a `<script>` tag for THREE.js. You can remove the CSS and the resizing code.
+You can remove .GLTF code because you only care about the path. You
+can remove the lights and the shadows by using a `MeshBasicMaterial`.
+You can certainly remove the DAT.gui code. The code makes a ground plane
+with a texture. It would be easier to use a `GridHelper`. Finally if
+our question is about moving things on a path we could just use
+cubes on the path instead of loaded car models.
+
+Here's a more minimal example taking all the above into account. It
+shrunk from 271 lines to 135. We might consider shrinking it even
+more by simplifying our path.
+
+{{{example url="../threejs-debugging-mcve.html" }}}
+
+I keep the `OrbitController` just because it's useful for others
+to move the camera and figure out what's going on but depending
+on your issue you might be able to remove that as well.
+
+The best thing about making an MCVE is we'll often solve our own
+problem. The process of removing everything that's not needed and
+making the smallest example we can that reproduces the issue more
+often than not leads us to our bug.
+
+On top of that it's respectful of all the people's time who you are
+asking to look at your code on Stack Overflow. By making the minimal
+example you make it much easier for them to help you. You'll also
+learn in the process.
+
+## Put something in front of the camera
+
+This is just another wasy of saying if all else fails start with
+something that works and then slowly add stuff back in. If you get
+a screen with nothing on it then try putting something directly in
+front of the camera. Make a sphere or box, give it a simple material
+like the `MeshBasicMaterial` and make sure you can get that on the screen.
+Then start adding things back a ltitle at time and testing. Eventually
+you'll either reproduce your bug or you'll find it on the way.
+
+---
+
+These were a few tips for debugging JavsScript. Let's also go
+over [some tips for debugging GLSL](threejs-debugging-glsl.html).

+ 10 - 3
threejs/lessons/toc.html

@@ -1,17 +1,24 @@
 <ul>
+  <li>Basics</li>
+  <ul>
+    <li><a href="/threejs/lessons/threejs-fundamentals.html">Fundamentals</a></li>
+    <li><a href="/threejs/lessons/threejs-responsive.html">Responsive Design</a></li>
+    <li><a href="/threejs/lessons/threejs-setup.html">Setup</a></li>
+  </ul>
   <li>Solutions</li>
   <ul>
     <li><a href="/threejs/lessons/threejs-load-obj.html">Load an .OBJ file</a></li>
     <li><a href="/threejs/lessons/threejs-load-gltf.html">Load a .GLTF file</a></li>
   </ul>
+  <li>Tips</li>
+  <ul>
+    <li><a href="/threejs/lessons/threejs-debugging-javascript.html">Debugging JavaScript</a></li>
+  </ul>
   <li>Fundamentals</li>
   <ul>
-    <li><a href="/threejs/lessons/threejs-fundamentals.html">Fundamentals</a></li>
-    <li><a href="/threejs/lessons/threejs-responsive.html">Responsive Design</a></li>
     <li><a href="/threejs/lessons/threejs-primitives.html">Primitives</a></li>
     <li><a href="/threejs/lessons/threejs-scenegraph.html">Scenegraph</a></li>
     <li><a href="/threejs/lessons/threejs-materials.html">Materials</a></li>
-    <li><a href="/threejs/lessons/threejs-setup.html">Setup</a></li>
     <li><a href="/threejs/lessons/threejs-textures.html">Textures</a></li>
     <li><a href="/threejs/lessons/threejs-lights.html">Lights</a></li>
     <li><a href="/threejs/lessons/threejs-cameras.html">Cameras</a></li>

+ 151 - 0
threejs/threejs-debug-js-clearing-logger.html

@@ -0,0 +1,151 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Debug - Clearing Logger</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #debug {
+      position: absolute;
+      left: 1em;
+      top: 1em;
+      padding: 1em;
+      background: rgba(0, 0, 0, 0.9);
+      color: white;
+      font-family: monospace;
+      pointer-events: none;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="debug">
+      <pre></pre>
+    </div>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+class ClearingLogger {
+  constructor(elem) {
+    this.elem = elem;
+    this.lines = [];
+  }
+  log(...args) {
+    this.lines.push([...args].join(' '));
+  }
+  render() {
+    this.elem.textContent = this.lines.join('\n');
+    this.lines = [];
+  }
+}
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 20;
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('cyan');
+
+  const geometry = new THREE.SphereBufferGeometry();
+  const material = new THREE.MeshBasicMaterial({color: 'red'});
+
+  const things = [];
+
+  function rand(min, max) {
+    if (max === undefined) {
+      max = min;
+      min = 0;
+    }
+    return Math.random() * (max - min) + min;
+  }
+
+  function createThing() {
+    const mesh = new THREE.Mesh(geometry, material);
+    scene.add(mesh);
+    things.push({
+      mesh,
+      timer: 2,
+      velocity: new THREE.Vector3(rand(-5, 5), rand(-5, 5), rand(-5, 5)),
+    });
+  }
+
+  canvas.addEventListener('click', createThing);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const logger = new ClearingLogger(document.querySelector('#debug pre'));
+
+  let then = 0;
+  function render(now) {
+    now *= 0.001;  // convert to seconds
+    const deltaTime = now - then;
+    then = now;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    logger.log('fps:', (1 / deltaTime).toFixed(1));
+    logger.log('num things:', things.length);
+    for (let i = 0; i < things.length;) {
+      const thing = things[i];
+      const mesh = thing.mesh;
+      const pos = mesh.position;
+      logger.log(
+          'timer:', thing.timer.toFixed(3),
+          'pos:', pos.x.toFixed(3), pos.y.toFixed(3), pos.z.toFixed(3));
+      thing.timer -= deltaTime;
+      if (thing.timer <= 0) {
+        // remove this thing. Note we don't advance `i`
+        things.splice(i, 1);
+        scene.remove(mesh);
+      } else {
+        mesh.position.addScaledVector(thing.velocity, deltaTime);
+        ++i;
+      }
+    }
+
+    renderer.render(scene, camera);
+    logger.render();
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 106 - 0
threejs/threejs-debug-js-html-elements.html

@@ -0,0 +1,106 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Debug - HTML Elements</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #debug {
+      position: absolute;
+      left: 1em;
+      top: 1em;
+      padding: 1em;
+      background: rgba(0, 0, 0, 0.9);
+      color: white;
+      font-family: monospace;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="debug">
+      <div>x:<span id="x"></span></div>
+      <div>y:<span id="y"></span></div>
+      <div>z:<span id="z"></span></div>
+    </div>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 20;
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('red');
+
+  const geometry = new THREE.SphereBufferGeometry();
+  const material = new THREE.MeshBasicMaterial({color: 'yellow'});
+
+  const mesh = new THREE.Mesh(geometry, material);
+  scene.add(mesh);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  const xElem = document.querySelector('#x');
+  const yElem = document.querySelector('#y');
+  const zElem = document.querySelector('#z');
+
+  function render(time) {
+    time *= 0.001;  // convert to seconds
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    mesh.position.set(
+      Math.sin(time * 1.2) * 5,
+      Math.sin(time * 1.1) * 5,
+      Math.sin(time * 1.3) * 10,
+    );
+    xElem.textContent = mesh.position.x.toFixed(3);
+    yElem.textContent = mesh.position.y.toFixed(3);
+    zElem.textContent = mesh.position.z.toFixed(3);
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 202 - 0
threejs/threejs-debug-js-params.html

@@ -0,0 +1,202 @@
+<!-- Licensed under a BSD license. See license.html for license -->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+    <title>Three.js - Debug - Params</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #debug {
+      position: absolute;
+      left: 1em;
+      top: 1em;
+      padding: 1em;
+      background: rgba(0, 0, 0, 0.9);
+      color: white;
+      font-family: monospace;
+      pointer-events: none;
+    }
+    #info {
+      position: absolute;
+      right: 1em;
+      bottom: 1em;
+      padding: 1em;
+      background: rgba(0, 0, 0, 0.9);
+      color: white;
+      font-family: monospace;
+      pointer-events: none;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="debug" style="display: none;">
+      <pre></pre>
+    </div>
+    <div id="info">click to launch</div>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+/**
+  * Returns the query parameters as a key/value object.
+  * Example: If the query parameters are
+  *
+  *    abc=123&def=456&name=gman
+  *
+  * Then `getQuery()` will return an object like
+  *
+  *    {
+  *      abc: '123',
+  *      def: '456',
+  *      name: 'gman',
+  *    }
+  */
+  function getQuery() {
+  const params = {};
+  const q = (window.location.search || '').substring(1);
+  q.split('&').forEach((pair) => {
+    const keyValue = pair.split('=').map(decodeURIComponent);
+    params[keyValue[0]] = keyValue[1];
+  });
+  return params;
+}
+
+class DummyLogger {
+  log() {}
+  render() {}
+}
+
+class ClearingLogger {
+  constructor(elem) {
+    this.elem = elem;
+    this.lines = [];
+  }
+  log(...args) {
+    this.lines.push([...args].join(' '));
+  }
+  render() {
+    this.elem.textContent = this.lines.join('\n');
+    this.lines = [];
+  }
+}
+
+const query = getQuery();
+const debug = query.debug === 'true';
+const logger = debug
+   ? new ClearingLogger(document.querySelector('#debug pre'))
+   : new DummyLogger();
+if (debug) {
+  document.querySelector('#debug').style.display = '';
+}
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 50;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 20;
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('cyan');
+
+  const geometry = new THREE.SphereBufferGeometry();
+  const material = new THREE.MeshBasicMaterial({color: 'red'});
+
+  const things = [];
+
+  function rand(min, max) {
+    if (max === undefined) {
+      max = min;
+      min = 0;
+    }
+    return Math.random() * (max - min) + min;
+  }
+
+  function createThing() {
+    const mesh = new THREE.Mesh(geometry, material);
+    scene.add(mesh);
+    things.push({
+      mesh,
+      timer: 2,
+      velocity: new THREE.Vector3(rand(-5, 5), rand(-5, 5), rand(-5, 5)),
+    });
+  }
+
+  canvas.addEventListener('click', createThing);
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth;
+    const height = canvas.clientHeight;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  let then = 0;
+  function render(now) {
+    now *= 0.001;  // convert to seconds
+    const deltaTime = now - then;
+    then = now;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    if (things.length === 0) {
+      createThing();
+    }
+
+    logger.log('fps:', (1 / deltaTime).toFixed(1));
+    logger.log('num things:', things.length);
+    for (let i = 0; i < things.length;) {
+      const thing = things[i];
+      const mesh = thing.mesh;
+      const pos = mesh.position;
+      logger.log(
+          'timer:', thing.timer.toFixed(3),
+          'pos:', pos.x.toFixed(3), pos.y.toFixed(3), pos.z.toFixed(3));
+      thing.timer -= deltaTime;
+      if (thing.timer <= 0) {
+        // remove this thing. Note we don't advance `i`
+        things.splice(i, 1);
+        scene.remove(mesh);
+      } else {
+        mesh.position.addScaledVector(thing.velocity, deltaTime);
+        ++i;
+      }
+    }
+
+    renderer.render(scene, camera);
+    logger.render();
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 134 - 0
threejs/threejs-debugging-mcve.html

@@ -0,0 +1,134 @@
+<body>
+  <canvas id="c"></canvas>
+</body>
+<script src="resources/threejs/r94/three.js"></script>
+<script src="resources/threejs/r94/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 45;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 10000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.set(0, 1000, 2000);
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 5, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+  scene.background = new THREE.Color('black');
+
+  scene.add(new THREE.GridHelper(5000, 10));
+
+  let curve;
+  let curveObject;
+  {
+    const controlPoints = [
+      [1.118281, 5.115846, -3.681386],
+      [3.948875, 5.115846, -3.641834],
+      [3.960072, 5.115846, -0.240352],
+      [3.985447, 5.115846, 4.585005],
+      [-3.793631, 5.115846, 4.585006],
+      [-3.826839, 5.115846, -14.736200],
+      [-14.542292, 5.115846, -14.765865],
+      [-14.520929, 5.115846, -3.627002],
+      [-5.452815, 5.115846, -3.634418],
+      [-5.467251, 5.115846, 4.549161],
+      [-13.266233, 5.115846, 4.567083],
+      [-13.250067, 5.115846, -13.499271],
+      [4.081842, 5.115846, -13.435463],
+      [4.125436, 5.115846, -5.334928],
+      [-14.521364, 5.115846, -5.239871],
+      [-14.510466, 5.115846, 5.486727],
+      [5.745666, 5.115846, 5.510492],
+      [5.787942, 5.115846, -14.728308],
+      [-5.423720, 5.115846, -14.761919],
+      [-5.373599, 5.115846, -3.704133],
+      [1.004861, 5.115846, -3.641834],
+    ];
+    const p0 = new THREE.Vector3();
+    const p1 = new THREE.Vector3();
+    curve = new THREE.CatmullRomCurve3(
+      controlPoints.map((p, ndx) => {
+        p0.set(...p);
+        p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
+        return [
+          (new THREE.Vector3()).copy(p0),
+          (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
+          (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
+        ];
+      }).flat(),
+      true,
+    );
+    {
+      const points = curve.getPoints(250);
+      const geometry = new THREE.BufferGeometry().setFromPoints(points);
+      const material = new THREE.LineBasicMaterial({color: 0xff0000});
+      curveObject = new THREE.Line(geometry, material);
+      curveObject.scale.set(100, 100, 100);
+      curveObject.position.y = -621;
+      material.depthTest = false;
+      curveObject.renderOrder = 1;
+      scene.add(curveObject);
+    }
+  }
+
+  const geometry = new THREE.BoxBufferGeometry(100, 100, 300);
+  const material = new THREE.MeshBasicMaterial({color: 'cyan'});
+  const cars = [];
+  for (let i = 0; i < 10; ++i) {
+    const mesh = new THREE.Mesh(geometry, material);
+    scene.add(mesh);
+    cars.push(mesh);
+  }
+
+  // create 2 Vector3s we can use for path calculations
+  const carPosition = new THREE.Vector3();
+  const carTarget = new THREE.Vector3();
+
+  function render(time) {
+    time *= 0.001;  // convert to seconds
+
+    {
+      const pathTime = time * .01;
+      const targetOffset = 0.01;
+      cars.forEach((car, ndx) => {
+        // a number between 0 and 1 to evenly space the cars
+        const u = pathTime + ndx / cars.length;
+
+        // get the first point
+        curve.getPointAt(u % 1, carPosition);
+        carPosition.applyMatrix4(curveObject.matrixWorld);
+
+        // get a second point slightly further down the curve
+        curve.getPointAt((u + targetOffset) % 1, carTarget);
+        carTarget.applyMatrix4(curveObject.matrixWorld);
+
+        // put the car at the first point (temporarily)
+        car.position.copy(carPosition);
+        // point the car the second point
+        car.lookAt(carTarget);
+
+        // put the car between the 2 points
+        car.position.lerpVectors(carPosition, carTarget, 0.5);
+      });
+    }
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>