Browse Source

add scenegraph article

Gregg (Greggman/GMan) Tavares 7 years ago
parent
commit
92b68840dd

File diff suppressed because it is too large
+ 12 - 0
3rdparty/dat.gui.min.js


+ 152 - 0
threejs/lessons/resources/canvas-wrapper.js

@@ -0,0 +1,152 @@
+/*
+ * Copyright 2014, Gregg Tavares.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Gregg Tavares. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/* global define */
+
+(function(root, factory) {  // eslint-disable-line
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['./transform'], factory);
+  } else {
+    // Browser globals
+    const lib = factory();
+    root.wrapCanvasRenderingContext2D = lib.wrap;
+  }
+}(this, function() {
+  'use strict';  // eslint-disable-line
+
+  function duplicate(src) {
+    const d = new window.DOMMatrix();
+    d.a = src.a;
+    d.b = src.b;
+    d.c = src.c;
+    d.d = src.d;
+    d.e = src.e;
+    d.f = src.f;
+    return d;
+  }
+
+  function patchCurrentTransform(ctx) {
+
+    if (ctx.currentTransform) {
+      return ctx;
+    }
+
+    const stack = [];
+
+    ctx.scale = function(scale) {
+      return function(x, y) {
+        ctx.currentTransform.scaleSelf(x, y);
+        scale(x, y);
+      };
+    }(ctx.scale.bind(ctx));
+
+    ctx.rotate = function(rotate) {
+      return function(r) {
+        ctx.currentTransform.rotateSelf(r * 180 / Math.PI);
+        rotate(r);
+      };
+    }(ctx.rotate.bind(ctx));
+
+    ctx.translate = function(translate) {
+      return function(x, y) {
+        ctx.currentTransform.translateSelf(x, y);
+        translate(x, y);
+      };
+    }(ctx.translate.bind(ctx));
+
+    ctx.save = function(save) {
+      return function() {
+        stack.push(duplicate(ctx.currentTransform));
+        save();
+      };
+    }(ctx.save.bind(ctx));
+
+    ctx.restore = function(restore) {
+      return function() {
+        if (stack.length) {
+          ctx.currentTransform = stack.pop();
+        } else {
+          throw new Error('"transform stack empty!');
+        }
+        restore();
+      };
+    }(ctx.restore.bind(ctx));
+
+    ctx.transform = function(transform) {
+      return function(m11, m12, m21, m22, dx, dy) {
+        const m = new DOMMatrix();
+        m.a = m11;
+        m.b = m12;
+        m.c = m21;
+        m.d = m22;
+        m.e = dx;
+        m.f = dy;
+        ctx.currentTransform.multiplySelf(m);
+        transform(m11, m12, m21, m22, dx, dy);
+      };
+    }(ctx.transform.bind(ctx));
+
+    ctx.setTransform = function(setTransform) {
+      return function(m11, m12, m21, m22, dx, dy) {
+        const d = ctx.currentTransform;
+        d.a = m11;
+        d.b = m12;
+        d.c = m21;
+        d.d = m22;
+        d.e = dx;
+        d.f = dy;
+        setTransform(m11, m12, m21, m22, dx, dy);
+      };
+    }(ctx.setTransform.bind(ctx));
+
+    ctx.currentTransform = new DOMMatrix();
+
+    ctx.validateTransformStack = function() {
+      if (stack.length !== 0) {
+        throw new Error('transform stack not 0');
+      }
+    };
+
+    return ctx;
+  }
+
+  function wrap(ctx) {
+    //patchDOMMatrix();
+    return patchCurrentTransform(ctx);
+  }
+
+  return {
+    wrap: wrap,
+  };
+}));
+
+

File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-car.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-generic.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-human.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-solarsystem.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-sun-earth-fixed.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-sun-earth-moon.svg


File diff suppressed because it is too large
+ 0 - 0
threejs/lessons/resources/images/scenegraph-sun-earth.svg


+ 144 - 0
threejs/lessons/resources/moon-orbit.html

@@ -0,0 +1,144 @@
+<html>
+<head>
+<meta charset="utf8">
+<title>moon orbit</title>
+<link type="text/css" href="../../resources/threejs-tutorials.css" rel="stylesheet" />
+<style>
+body {
+  margin: 0px;
+  background: white;
+}
+canvas {
+  width: 100vw;
+  height: 100vh;
+  display: block;
+}
+p {
+    position: relative;
+}
+#c {
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  z-index: 2;
+  background-color: transparent;
+}
+</style>
+</head>
+<body>
+<p><canvas id="orbit"></canvas>
+<canvas id="c"></canvas></p>
+</body>
+<script src="../../resources/threejs-lessons-helper.js"></script>
+<script src="../../resources/threejs-utils.js"></script>
+<script src="canvas-wrapper.js"></script>
+<script>
+/* global wrapCanvasRenderingContext2D, threejsUtils */
+'use strict';
+
+function main() {
+
+  const root = {
+    name: 'sun',
+    translation: [0, 0],
+    color: 'yellow',
+    radius: 30,
+    speed: 1,
+    children: [
+      {
+        name: 'earth',
+        translation: [-5, 1],
+        color: 'blue',
+        radius: 10,
+        speed: 2,
+        children: [
+          {
+            name: 'moon',
+            translation: [-1, 1],
+            color: 'gray',
+            drawOrbit: true,
+            radius: 5,
+            speed: 36.13,
+            children: [
+            ],
+          },
+        ],
+      },
+    ],
+  };
+
+  const canvas = document.getElementById('c');
+  const ctx = wrapCanvasRenderingContext2D(canvas.getContext('2d'));
+  const orbitCtx = document.getElementById('orbit').getContext('2d');
+
+  const spread = 16;
+  function updateTranslation(node) {
+    node.translation[0] *= spread;
+    node.translation[1] *= spread;
+    node.rotation = 0;
+    node.children.forEach(updateTranslation);
+  }
+  updateTranslation(root);
+
+  let clock = 0;
+  const maxHistory = 400;
+  let curHistory = 0;
+  const history = [];
+
+  function drawTrail(ctx, pos, radius) {
+    ctx.beginPath();
+    ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2, false);
+    ctx.fill();
+  }
+
+  function drawNode(node) {
+    ctx.save();
+    ctx.rotate(node.speed * clock);
+    ctx.translate(node.translation[0], node.translation[1]);
+    ctx.fillStyle = node.color;
+    ctx.strokeStyle = 'black';
+    ctx.beginPath();
+    ctx.arc(0, 0, node.radius, 0, Math.PI * 2, false);
+    ctx.fill();
+    ctx.stroke();
+    if (node.drawOrbit) {
+      const mat = ctx.currentTransform;
+      const point = [mat.e, mat.f];
+      const old = history[curHistory];
+      if (old) {
+        orbitCtx.fillStyle = 'white';
+        drawTrail(orbitCtx, old, 2);
+      }
+      history[curHistory] = point;
+      curHistory = (curHistory + 1) % maxHistory;
+      orbitCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
+      drawTrail(orbitCtx, point, 1);
+    }
+    node.children.forEach(drawNode);
+    ctx.restore();
+  }
+
+  function drawScene() {
+    threejsUtils.resizeCanvasToDisplaySize(ctx.canvas);
+    threejsUtils.resizeCanvasToDisplaySize(orbitCtx.canvas);
+    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+    ctx.save();
+    ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
+    drawNode(root);
+    ctx.restore();
+  }
+
+  function render() {
+    clock += 1 / 60 * 0.25;
+
+    drawScene();
+
+    requestAnimationFrame(render, canvas);
+  }
+  requestAnimationFrame(render, canvas);
+}
+
+main();
+</script>
+</html>
+

+ 386 - 1
threejs/lessons/threejs-scenegraph.md

@@ -1,5 +1,390 @@
 Title: Three.js Scenegraph
 Description: What's a scene graph?
 
-TBD
+This article is part of a series of articles about three.js. The
+first article is [three.js fundamentals](three-fundamentals.html). If
+you haven't read yet you might want to consider starting there.
 
+Three.js's core is arguably its scene graph. A scene graph in a 3D
+engine is a hierarchy of nodes in a graph where each node represents
+a local space.
+
+<img src="resources/images/scenegraph-generic.svg" align="center">
+
+That's kind of abstract so let's try to give some examples.
+
+One example might be solar system, sun, earth, moon.
+
+<img src="resources/images/scenegraph-solarsystem.svg" align="center">
+
+The Earth orbits the Sun. The Moon orbits the Earth. The Moon
+moves in a circle around the Earth. From the Moon's point of
+view it's rotating in the "local space" of the Earth. Even though
+its motion relative to the Sun is some crazy spirograph like
+curve from the Moon's point of view it just has to concern itself with rotating
+around the Earth's local space.
+
+{{{diagram url="resources/moon-orbit.html" }}}
+
+To think of it another way, you living on the Earth do not have to think
+about the Earth's rotation on its axis nor its rotation around the
+Sun. You just walk or drive or swim or run as though the Earth is
+not moving or rotating at all. You walk, drive, swim, run, and live
+in the Earth's "local space" even though relative to the sun you are
+spinning around the earth at around 1000 miles per hour and around
+the sun at around 67,000 miles per hour. Your position in the solar
+system is similar to that of the moon above but you don't have to concern
+yourself. You just worry about your position relative to the earth its
+"local space".
+
+Let's take it one step at a time. Imagine we want to make
+a diagram of the sun, earth, and moon. We'll start with the sun by
+just making a sphere and putting it at the origin. Note: We're using
+sun, earth, moon as a demonstration of how to use a scenegraph. Of course
+the real sun, earth, and moon use physics but for our purposes we'll
+fake it with a scenegraph.
+
+```
+// an array of objects who's rotation to update
+const objects = [];
+
+// use just one sphere for everything
+const radius = 1;
+const widthSegments = 6;
+const heightSegments = 6;
+const sphereGeometry = new THREE.SphereBufferGeometry(
+    radius, widthSegments, heightSegments);
+
+const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+sunMesh.scale.set(5, 5, 5);  // make the sun large
+scene.add(sunMesh);
+objects.push(sunMesh);
+```
+
+We're using a really low-polygon sphere. Only 6 subdivisions around its equator.
+This is so it's easy to see the rotation.
+
+We're going to reuse the same sphere for everything so we'll set a scale
+for the sun mesh of 5x.
+
+We also set the phong material's `emissive` property to yellow. A phong material's
+emissive property is basically the color that will be drawn with no light hitting
+the surface. Light is added to that color.
+
+Let's also put a single point light in the center of the scene. We'll go into more
+details about point lights later but for now the simple version is a point light
+represents light that eminates from a single point.
+
+```
+{
+  const color = 0xFFFFFF;
+  const intensity = 3;
+  const light = new THREE.PointLight(color, intensity);
+  scene.add(light);
+}
+```
+
+To make it easy to see we're going to put the camera directly above the origin
+looking down. The easist way to do that us to use the `lookAt` function. The `lookAt`
+function will orient the camera from its position to "lookAt the position
+we pass to `lookAt`. Before we do that though we need to tell the camera
+which way the top of the camera is facing or rather which way is "up" for the
+camera. For most situations positive Y being up is good enough but since 
+we are looking straight down we need to tell the camera that positive Z is up.
+
+```
+const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+camera.position.set(0, 50, 0);
+camera.up.set(0, 0, 1);
+camera.lookAt(0, 0, 0);
+```
+
+In the render loop, adapted from previous examples, we're rotating all
+objects in our `objects` array with this code.
+
+```
+objects.forEach((obj) => {
+  obj.rotation.y = time;
+});
+```
+
+Since we added the `sunMesh` to the `objects` array it will rotate.
+
+{{{example url="../threejs-scenegraph-sun.html" }}}
+
+Now let's add an the earth.
+
+```
+const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+earthMesh.position.x = 10;
+scene.add(earthMesh);
+objects.push(earthMesh);
+```
+
+We make a material that is blue but we gave it a small amount of *emissive* blue
+so that it will show up against our black background.
+
+We use the same `sphereGeometry` with our new blue `earthMaterial` to make
+an `earthMesh`. We position that 10 units to the left of the sun
+and add it to the scene.  Since we added it to our `objects` array it will
+rotate too.
+
+{{{example url="../threejs-scenegraph-sun-earth.html" }}}
+
+You can see both the sun and the earth are rotating but the earth is not
+going around the sun. Let's make the earth a child of the sun
+
+```
+-scene.add(earthMesh);
++sunMesh.add(earthMesh);
+``` 
+
+and...
+
+{{{example url="../threejs-scenegraph-sun-earth-orbit.html" }}}
+
+What happened? Why is the earth the same size as the sun and why is it so far away?
+I actually had to move the camera from 50 units above to 150 units above to see the earth.
+
+We made the `earthMesh` a child of the `sunMesh`. The `sunMesh` has
+its scale set to 5x with `sunMesh.scale.set(5, 5, 5)`. That means the
+`sunMesh`s local space is 5 times as big. Anything put in that space
+ will be multiplied by 5. That means the earth is now 5x larger and
+ it's distance from the sun (`earthMesh.position.x = 10`) is also
+ 5x as well.
+
+ Our scene graph currently looks like this
+
+<img src="resources/images/scenegraph-sun-earth.svg" align="center">
+
+To fix it let's add an empty scene graph node. We'll parent both the sun and the earth
+to that node.
+
+```
++const solarSystem = new THREE.Object3D();
++scene.add(solarSystem);
++objects.push(solarSystem);
+
+const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+sunMesh.scale.set(5, 5, 5);
+-scene.add(sunMesh);
++solarSystem.add(sunMesh);
+objects.push(sunMesh);
+
+const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+earthMesh.position.x = 10;
+-sunMesh.add(earthMesh);
++solarSystem.add(earthMesh);
+objects.push(earthMesh);
+```
+
+Here we made a `Object3D`. Like a `Mesh` it is also a node in the scene graph
+but unlike a `Mesh` it has no material or geometry. It just represents a local space.
+
+Our new scene graph looks like this
+
+<img src="resources/images/scenegraph-sun-earth-fixed.svg" align="center">
+
+Both the `sunMesh` and the `earthMesh` are children of the `solarSystem`. All 3
+are being rotated and now because the `earthMesh` is not a child of the `sunMesh`
+it is no longer scaled by 5x.
+
+{{{example url="../threejs-scenegraph-sun-earth-orbit-fixed.html" }}}
+
+Much better. The earth is smaller than the sun and it's rotating around the sun
+and rotating itself.
+
+Continuing that same pattern let's add a moon.
+
+```
++const earthOrbit = new THREE.Object3D();
++earthOrbit.position.x = 10;
++solarSystem.add(earthOrbit);
++objects.push(earthOrbit);
+
+const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+-solarSystem.add(earthMesh);
++earthOrbit.add(earthMesh);
+objects.push(earthMesh);
+
++const moonOrbit = new THREE.Object3D();
++moonOrbit.position.x = 2;
++earthOrbit.add(moonOrbit);
+
++const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
++const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
++moonMesh.scale.set(.5, .5, .5);
++moonOrbit.add(moonMesh);
++objects.push(moonMesh);
+```
+
+Again we added another invisible scene graph node, a `Object3D` called `earthOrbit`
+and added both the `earthMesh` and the `moonMesh` to it. The new scene graph looks like
+this.
+
+<img src="resources/images/scenegraph-sun-earth-moon.svg" align="center">
+
+and here's that
+
+{{{example url="../threejs-scenegraph-sun-earth-moon.html" }}}
+
+You can see the moon follows the spirograph pattern shown at the top
+of this article but we didn't have to manually compute it. We just
+setup our scene graph to do it for us.
+
+It is often useful to draw something to visualize the nodes in the scene graph.
+Three.js has some helpful ummmm, helpers to ummm, ... help with this.
+
+One is called an `AxesHelper`. It draws 3 lines representing the local
+<span style="color:red">X</span>,
+<span style="color:green">Y</span>, and
+<span style="color:blue">Z</span> axes. Let's add one to every node we
+created.
+
+```
+// add an AxesHelper to each node
+objects.forEach((node) => {
+  const axes = new THREE.AxesHelper();
+  axes.material.depthTest = false;
+  axes.renderOrder = 1;
+  node.add(axes);
+});
+```
+
+On our case we want the axes to appear even though they are inside the spheres.
+To do this we set their material's `depthTest` to false which means they will
+not check to see if they are drawing behind something else. We also
+set their `renderOrder` to 1 (the default is 0) so that they get drawn after
+all the spheres. Otherwise a sphere might draw over them and cover them up.
+
+{{{example url="../threejs-scenegraph-sun-earth-moon-axes.html" }}}
+
+We can see the 
+<span style="color:red">x (red)</span> and
+<span style="color:blue">z (blue)</span> axes. Since we are looking
+straight down and each of our objects is only rotating around its
+y axis we don't see much of the <span style="color:green">y (green)</span> axes.
+
+It might be hard to see some of them as there are 2 pairs of overlapping axes. Both the `sunMesh`
+and the `solarSystem` are at the same position. Similarly the `earthMesh` and
+`earthOrbit` are at the same position. Let's add some simple controls to allow us
+to turn them on/off for each node. 
+While we're at it let's also add another helper called the `GridHelper`. It
+makes a 2D grid on the X,Z plane. By default the grid is 10x10 units.
+
+We're also going to use [dat.GUI](https://github.com/dataarts/dat.gui) which is
+a UI library that is very popular with three.js projects. dat.GUI takes an
+object and a property name on that object and based on the type of the property
+automatically makes a UI to manipulate that property.
+
+We want to make both a `GridHelper` and an `AxesHelper` for each node. We need
+a label for each node so we'll get rid of the old loop and switch to calling
+some function to add the helpers for each node
+
+```
+-// add an AxesHelper to each node
+-objects.forEach((node) => {
+-  const axes = new THREE.AxesHelper();
+-  axes.material.depthTest = false;
+-  axes.renderOrder = 1;
+-  node.add(axes);
+-});
+
++function makeAxisGrid(node, label, units) {
++  const helper = new AxisGridHelper(node, units);
++  gui.add(helper, 'visible').name(label);
++}
++
++makeAxisGrid(solarSystem, 'solarSystem', 25);
++makeAxisGrid(sunMesh, 'sunMesh');
++makeAxisGrid(earthOrbit, 'earthOrbit');
++makeAxisGrid(earthMesh, 'earthMesh');
++makeAxisGrid(moonMesh, 'moonMesh');
+```
+
+`makeAxisGrid` makes a `AxisGridHelper` which is class we'll create
+to make dat.GUI happy. Like it says above dat.GUI
+will automagically make a UI that manipulates the named property
+of some object. It will create a different UI depending on the type
+of property. We want it to create a checkbox so we need to specify
+a `bool` property. But, we want both the axes and the grid
+to appear/disappear based on a single property so we'll make a class
+that has a getter and setter for a property. That way we can let dat.GUI
+think it's manipulating a single property but internally we can set
+the visible property of both the `AxesHelper` and `GridHelper` for a node.
+
+```
+// Turns both axes and grid visible on/off
+// dat.GUI requires a property that returns a bool
+// to decide to make a checkbox so we make a setter
+// can getter for `visible` which we can tell dat.GUI
+// to look at.
+class AxisGridHelper {
+  constructor(node, units = 10) {
+    const axes = new THREE.AxesHelper();
+    axes.material.depthTest = false;
+    axes.renderOrder = 2;  // after the grid
+    node.add(axes);
+
+    const grid = new THREE.GridHelper(units, units);
+    grid.material.depthTest = false;
+    grid.renderOrder = 1;
+    node.add(grid);
+
+    this.grid = grid;
+    this.axes = axes;
+    this.visible = false;
+  }
+  get visible() {
+    return this._visible;
+  }
+  set visible(v) {
+    this._visible = v;
+    this.grid.visible = v;
+    this.axes.visible = v;
+  }
+}
+```
+
+One thing to notice is we set the `renderOrder` of the `AxesHelper`
+to 2 and for the `GridHelper` to 1 so that the axes get drawn after the grid.
+Otherwise the grid might overwrite the axes.
+
+{{{example url="../threejs-scenegraph-sun-earth-moon-axes.html" }}}
+
+Turn on the `solarSystem` and you'll see how the earth is exactly 10
+units out from the center just like we set above. You can see how the
+earth is in the *local space* of the `solarSystem`. Similary if you
+turn on the `earthOrbit` you'll see how the moon is exactly 2 units
+from the center of the *local space* of the `earthOrbit`.
+
+A few more examples of scene graphs. An automobile in a simple game world might have a scene graph like this
+
+<img src="resources/images/scenegraph-car.svg" align="center">
+
+If you move the car's body all the wheels will move with it. If you wanted the body
+to bounce separate from the wheels you might parent the body and the wheels to a "frame" node
+that represents the car's frame.
+
+Another example is a human in a game world.
+
+<img src="resources/images/scenegraph-human.svg" align="center">
+
+You can see the scene graph gets pretty complex for a human. In fact
+that scene graph above is simplified. For example you might extend it
+to cover the every finger (at least another 28 nodes) and every toe
+(yet another 28 nodes) plus ones for the and jaw, the eyes and maybe more.
+
+I hope this gives some idea of how scene graphs work and how you might use them.
+Making `Object3D` nodes and parenting things to them is an important step to using
+a 3D engine like three.js well. Often it might seem like some complex math is necessary
+to make something move and rotate the way you want. For example without a scene graph
+computing the motion of the moon or where to put the wheels of the car relative to its
+body would be very complicated but using a scene graph it becomes much easier.
+
+[Next up we'll go over materials and lights](threejs-materials-and-lights.html).

+ 1 - 0
threejs/lessons/toc.html

@@ -4,6 +4,7 @@
     <li><a href="/threejs/lessons/threejs-fundamentals.html">Three.js Fundamentals</a></li>
     <li><a href="/threejs/lessons/threejs-responsive.html">Three.js Responsive Design</a></li>
     <li><a href="/threejs/lessons/threejs-primitives.html">Three.js Primitives</a></li>
+    <li><a href="/threejs/lessons/threejs-scenegraph.html">Three.js Scenegraph</a></li>
   </ul>
 </ul>
 <ul>

+ 74 - 0
threejs/resources/threejs-utils.js

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012, Gregg Tavares.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Gregg Tavares. nor the names of his
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/* global define */
+
+(function(root, factory) {  // eslint-disable-line
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define([], function() {
+      return factory.call(root);
+    });
+  } else {
+    // Browser globals
+    root.threejsUtils = factory.call(root);
+  }
+}(this, function() {
+  'use strict';  // eslint-disable-line
+
+  /** @module threejs-utils */
+
+  /**
+   * Resize a canvas to match the size its displayed.
+   * @param {HTMLCanvasElement} canvas The canvas to resize.
+   * @param {number} [multiplier] amount to multiply by.
+   *    Pass in window.devicePixelRatio for native pixels.
+   * @return {boolean} true if the canvas was resized.
+   * @memberOf module:webgl-utils
+   */
+  function resizeCanvasToDisplaySize(canvas, multiplier) {
+    multiplier = multiplier || 1;
+    const width  = canvas.clientWidth  * multiplier | 0;
+    const height = canvas.clientHeight * multiplier | 0;
+    if (canvas.width !== width ||  canvas.height !== height) {
+      canvas.width  = width;
+      canvas.height = height;
+      return true;
+    }
+    return false;
+  }
+
+  return {
+    resizeCanvasToDisplaySize: resizeCanvasToDisplaySize,
+  };
+
+}));
+

+ 125 - 0
threejs/threejs-scenegraph-car.html

@@ -0,0 +1,125 @@
+<!-- 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 - Scenegraph - Car</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script src="resources/threejs-lessons-helper.js"></script> <!-- you can and should delete this script. it is only used on the site to help with errors -->
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+  renderer.setClearColor(0xAAAAAA);
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(8, 4, 10);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const light = new THREE.DirectionalLight(0xffffff, 1);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+  {
+    const light = new THREE.DirectionalLight(0xffffff, 1);
+    light.position.set(1, -2, -4);
+    scene.add(light);
+  }
+
+  const carWidth = 4;
+  const carHeight = 1;
+  const carLength = 8;
+
+  const bodyGeometry = new THREE.BoxBufferGeometry(carWidth, carHeight, carLength);
+  const bodyMaterial = new THREE.MeshPhongMaterial({color: 0x6688AA});
+  const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
+  scene.add(bodyMesh);
+
+  const wheelRadius = 1;
+  const wheelThickness = .5;
+  const wheelSegments = 6;
+  const wheelGeometry = new THREE.CylinderBufferGeometry(
+      wheelRadius,     // top radius
+      wheelRadius,     // bottom radius
+      wheelThickness,  // height of cylinder
+      wheelSegments);
+  const wheelMaterial = new THREE.MeshPhongMaterial({color: 0x888888});
+  const wheelPositions = [
+    [-carWidth / 2 + wheelThickness / 2, -carHeight / 2,  carLength / 3],
+    [ carWidth / 2 + wheelThickness / 2, -carHeight / 2,  carLength / 3],
+    [-carWidth / 2 + wheelThickness / 2, -carHeight / 2, -carLength / 3],
+    [ carWidth / 2 + wheelThickness / 2, -carHeight / 2, -carLength / 3],
+  ];
+  const wheelMeshes = wheelPositions.map((position) => {
+    const mesh = new THREE.Mesh(wheelGeometry, wheelMaterial);
+    mesh.position.set(...position);
+    mesh.rotation.z = Math.PI * .5;
+    scene.add(mesh);
+    return 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 carPosition = [0, 0, 0];
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+
+    wheelMeshes.forEach((obj) => {
+      obj.rotation.x = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 195 - 0
threejs/threejs-scenegraph-sun-earth-moon-axes-grids.html

@@ -0,0 +1,195 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+        color: white;
+        font-family: monospace;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #ui {
+        position: absolute;
+        right: 1em;
+        top: 1em;
+        background: rgba(64, 64, 64, 0.8);
+        padding: .5em;
+    }
+    #ui .axisgrid {
+        display: flex;
+        align-items: center;
+    }
+    #ui .axisgrid:nth-child(odd) {
+      background: rgba(72, 72, 72, 0.8);
+    }
+    #ui .label {
+        width: 8em;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+    #ui .checkbox {
+        display: flex;
+        align-items: center;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="ui"></div>
+  </body>
+<script src="../3rdparty/dat.gui.min.js"></script>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+/* global dat */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+  const gui = new dat.GUI();
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const solarSystem = new THREE.Object3D();
+  scene.add(solarSystem);
+  objects.push(solarSystem);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  solarSystem.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthOrbit = new THREE.Object3D();
+  earthOrbit.position.x = 10;
+  solarSystem.add(earthOrbit);
+  objects.push(earthOrbit);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthOrbit.add(earthMesh);
+  objects.push(earthMesh);
+
+  const moonOrbit = new THREE.Object3D();
+  moonOrbit.position.x = 2;
+  earthOrbit.add(moonOrbit);
+
+  const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
+  const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
+  moonMesh.scale.set(.5, .5, .5);
+  moonOrbit.add(moonMesh);
+  objects.push(moonMesh);
+
+  // Turns both axes and grid visible on/off
+  // dat.GUI requires a property that returns a bool
+  // to decide to make a checkbox so we make a setter
+  // can getter for `visible` which we can tell dat.GUI
+  // to look at.
+  class AxisGridHelper {
+    constructor(node, units = 10) {
+      const axes = new THREE.AxesHelper();
+      axes.material.depthTest = false;
+      axes.renderOrder = 2;  // after the grid
+      node.add(axes);
+
+      const grid = new THREE.GridHelper(units, units);
+      grid.material.depthTest = false;
+      grid.renderOrder = 1;
+      node.add(grid);
+
+      this.grid = grid;
+      this.axes = axes;
+      this.visible = false;
+    }
+    get visible() {
+      return this._visible;
+    }
+    set visible(v) {
+      this._visible = v;
+      this.grid.visible = v;
+      this.axes.visible = v;
+    }
+  }
+
+  function makeAxisGrid(node, label, units) {
+    const helper = new AxisGridHelper(node, units);
+    gui.add(helper, 'visible').name(label);
+  }
+
+  makeAxisGrid(solarSystem, 'solarSystem', 25);
+  makeAxisGrid(sunMesh, 'sunMesh');
+  makeAxisGrid(earthOrbit, 'earthOrbit');
+  makeAxisGrid(earthMesh, 'earthMesh');
+  makeAxisGrid(moonMesh, 'moonMesh');
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 131 - 0
threejs/threejs-scenegraph-sun-earth-moon-axes.html

@@ -0,0 +1,131 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const solarSystem = new THREE.Object3D();
+  scene.add(solarSystem);
+  objects.push(solarSystem);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  solarSystem.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthOrbit = new THREE.Object3D();
+  earthOrbit.position.x = 10;
+  solarSystem.add(earthOrbit);
+  objects.push(earthOrbit);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthOrbit.add(earthMesh);
+  objects.push(earthMesh);
+
+  const moonOrbit = new THREE.Object3D();
+  moonOrbit.position.x = 2;
+  earthOrbit.add(moonOrbit);
+
+  const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
+  const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
+  moonMesh.scale.set(.5, .5, .5);
+  moonOrbit.add(moonMesh);
+  objects.push(moonMesh);
+
+  // add an AxesHelper to each node
+  objects.forEach((node) => {
+    const axes = new THREE.AxesHelper();
+    axes.material.depthTest = false;
+    axes.renderOrder = 1;
+    node.add(axes);
+  });
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 123 - 0
threejs/threejs-scenegraph-sun-earth-moon.html

@@ -0,0 +1,123 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const solarSystem = new THREE.Object3D();
+  scene.add(solarSystem);
+  objects.push(solarSystem);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  solarSystem.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthOrbit = new THREE.Object3D();
+  earthOrbit.position.x = 10;
+  solarSystem.add(earthOrbit);
+  objects.push(earthOrbit);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthOrbit.add(earthMesh);
+  objects.push(earthMesh);
+
+  const moonOrbit = new THREE.Object3D();
+  moonOrbit.position.x = 2;
+  earthOrbit.add(moonOrbit);
+
+  const moonMaterial = new THREE.MeshPhongMaterial({color: 0x888888, emissive: 0x222222});
+  const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
+  moonMesh.scale.set(.5, .5, .5);
+  moonOrbit.add(moonMesh);
+  objects.push(moonMesh);
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 109 - 0
threejs/threejs-scenegraph-sun-earth-orbit-fixed.html

@@ -0,0 +1,109 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const solarSystem = new THREE.Object3D();
+  scene.add(solarSystem);
+  objects.push(solarSystem);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  solarSystem.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthMesh.position.x = 10;
+  solarSystem.add(earthMesh);
+  objects.push(earthMesh);
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 105 - 0
threejs/threejs-scenegraph-sun-earth-orbit.html

@@ -0,0 +1,105 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  scene.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthMesh.position.x = 10;
+  sunMesh.add(earthMesh);
+  objects.push(earthMesh);
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 105 - 0
threejs/threejs-scenegraph-sun-earth.html

@@ -0,0 +1,105 @@
+<!-- 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 - Scenegraph - Sun Earth</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  scene.add(sunMesh);
+  objects.push(sunMesh);
+
+  const earthMaterial = new THREE.MeshPhongMaterial({color: 0x2233FF, emissive: 0x112244});
+  const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);
+  earthMesh.position.x = 10;
+  scene.add(earthMesh);
+  objects.push(earthMesh);
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

+ 100 - 0
threejs/threejs-scenegraph-sun.html

@@ -0,0 +1,100 @@
+<!-- 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 - Scenegraph - Sun</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r93/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 40;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 1000;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.set(0, 50, 0);
+  camera.up.set(0, 0, 1);
+  camera.lookAt(0, 0, 0);
+
+  const scene = new THREE.Scene();
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 3;
+    const light = new THREE.PointLight(color, intensity);
+    scene.add(light);
+  }
+
+  // an array of objects who's rotation to update
+  const objects = [];
+
+  const radius = 1;
+  const widthSegments = 6;
+  const heightSegments = 6;
+  const sphereGeometry = new THREE.SphereBufferGeometry(
+      radius, widthSegments, heightSegments);
+
+  const sunMaterial = new THREE.MeshPhongMaterial({emissive: 0xFFFF00});
+  const sunMesh = new THREE.Mesh(sphereGeometry, sunMaterial);
+  sunMesh.scale.set(5, 5, 5);
+  scene.add(sunMesh);
+  objects.push(sunMesh);
+
+  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;
+  }
+
+  function render(time) {
+    time *= 0.001;
+
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    objects.forEach((obj) => {
+      obj.rotation.y = time;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+
+

Some files were not shown because too many files changed in this diff