Browse Source

add primitives article

Gregg Tavares 7 years ago
parent
commit
cfa3d07929

+ 562 - 0
threejs/lessons/resources/threejs-primitives.js

@@ -0,0 +1,562 @@
+'use strict';
+
+function main() {
+
+  const primitives = {
+    BoxBufferGeometry: {
+      create() {
+        const width = 8;
+        const height = 8;
+        const depth = 8;
+        return new THREE.BoxBufferGeometry(width, height, depth);
+      },
+    },
+    CircleBufferGeometry: {
+      create() {
+        const radius = 7;
+        const segments = 24;
+        return new THREE.CircleBufferGeometry(radius, segments);
+      },
+    },
+    ConeBufferGeometry: {
+      create() {
+        const radius = 6;
+        const height = 8;
+        const segments = 16;
+        return new THREE.ConeBufferGeometry(radius, height, segments);
+      },
+    },
+    CylinderBufferGeometry: {
+      create() {
+        const radiusTop = 4;
+        const radiusBottom = 4;
+        const height = 8;
+        const radialSegments = 12;
+        return new THREE.CylinderBufferGeometry(radiusTop, radiusBottom, height, radialSegments);
+      },
+    },
+    DodecahedronBufferGeometry: {
+      create() {
+        const radius = 7;
+        return new THREE.DodecahedronBufferGeometry(radius);
+      },
+    },
+    ExtrudeBufferGeometry: {
+      create() {
+        const shape = new THREE.Shape();
+        const x = -2.5;
+        const y = -5;
+        shape.moveTo(x + 2.5, y + 2.5);
+        shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
+        shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
+        shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
+        shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
+        shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
+        shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
+
+        const extrudeSettings = {
+          steps: 2,
+          depth: 2,
+          bevelEnabled: true,
+          bevelThickness: 1,
+          bevelSize: 1,
+          bevelSegments: 2,
+        };
+
+        return new THREE.ExtrudeBufferGeometry(shape, extrudeSettings);
+      },
+    },
+    IcosahedronBufferGeometry: {
+      create() {
+        const radius = 7;
+        return new THREE.IcosahedronBufferGeometry(radius);
+      },
+    },
+    LatheBufferGeometry: {
+      create() {
+        const points = [];
+        for (let i = 0; i < 10; ++i) {
+          points.push(new THREE.Vector2(Math.sin(i * 0.2) * 3 + 3, (i - 5) * .8));
+        }
+        return new THREE.LatheBufferGeometry(points);
+      },
+    },
+    OctahedronBufferGeometry: {
+      create() {
+        const radius = 7;
+        return new THREE.OctahedronBufferGeometry(radius);
+      },
+    },
+    ParametricBufferGeometry: {
+      create() {
+        /*
+        from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js
+
+        The MIT License
+
+        Copyright © 2010-2018 three.js authors
+
+        Permission is hereby granted, free of charge, to any person obtaining a copy
+        of this software and associated documentation files (the "Software"), to deal
+        in the Software without restriction, including without limitation the rights
+        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+        copies of the Software, and to permit persons to whom the Software is
+        furnished to do so, subject to the following conditions:
+
+        The above copyright notice and this permission notice shall be included in
+        all copies or substantial portions of the Software.
+
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+        THE SOFTWARE.
+
+        */
+        function klein(v, u, target) {
+          u *= Math.PI;
+          v *= 2 * Math.PI;
+          u = u * 2;
+
+          let x;
+          let y;
+          let z;
+
+          if (u < Math.PI) {
+              x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(u) * Math.cos(v);
+              z = -8 * Math.sin(u) - 2 * (1 - Math.cos(u) / 2) * Math.sin(u) * Math.cos(v);
+          } else {
+              x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(v + Math.PI);
+              z = -8 * Math.sin(u);
+          }
+
+          y = -2 * (1 - Math.cos(u) / 2) * Math.sin(v);
+
+          target.set(x, y, z).multiplyScalar(0.75);
+        }
+
+        const slices = 25;
+        const stacks = 25;
+        return new THREE.ParametricBufferGeometry(klein, slices, stacks);
+      },
+    },
+    PlaneBufferGeometry: {
+      create() {
+        const width = 9;
+        const height = 9;
+        const widthSegments = 2;
+        const heightSegments = 2;
+        return new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments);
+      },
+    },
+    PolyhedronBufferGeometry: {
+      create() {
+        const verticesOfCube = [
+            -1, -1, -1,    1, -1, -1,    1,  1, -1,    -1,  1, -1,
+            -1, -1,  1,    1, -1,  1,    1,  1,  1,    -1,  1,  1,
+        ];
+        const indicesOfFaces = [
+            2, 1, 0,    0, 3, 2,
+            0, 4, 7,    7, 3, 0,
+            0, 1, 5,    5, 4, 0,
+            1, 2, 6,    6, 5, 1,
+            2, 3, 7,    7, 6, 2,
+            4, 5, 6,    6, 7, 4,
+        ];
+        const radius = 7;
+        const detail = 2;
+        return new THREE.PolyhedronBufferGeometry(verticesOfCube, indicesOfFaces, radius, detail);
+      },
+    },
+    RingBufferGeometry: {
+      create() {
+        const innerRadius = 2;
+        const outerRadius = 7;
+        const segments = 18;
+        return new THREE.RingBufferGeometry(innerRadius, outerRadius, segments);
+      },
+    },
+    ShapeBufferGeometry: {
+      create() {
+        const shape = new THREE.Shape();
+        const x = -2.5;
+        const y = -5;
+        shape.moveTo(x + 2.5, y + 2.5);
+        shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
+        shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
+        shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
+        shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
+        shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
+        shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
+        return new THREE.ShapeBufferGeometry(shape);
+      },
+    },
+    SphereBufferGeometry: {
+      create() {
+        const radius = 7;
+        const widthSegments = 12;
+        const heightSegments = 8;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+    },
+    TetrahedronBufferGeometry: {
+      create() {
+        const radius = 7;
+        return new THREE.TetrahedronBufferGeometry(radius);
+      },
+    },
+    TextBufferGeometry: {
+      create() {
+        return new Promise((resolve) => {
+          const loader = new THREE.FontLoader();
+
+          loader.load('../resources/threejs/fonts/helvetiker_regular.typeface.json', (font) => {
+            resolve(new THREE.TextBufferGeometry('three.js', {
+              font: font,
+              size: 3.0,
+              height: .2,
+              curveSegments: 12,
+              bevelEnabled: true,
+              bevelThickness: 0.15,
+              bevelSize: .3,
+              bevelSegments: 5,
+            }));
+          });
+        });
+      },
+    },
+    TorusBufferGeometry: {
+      create() {
+        const radius = 5;
+        const tubeRadius = 2;
+        const radialSegments = 8;
+        const tubularSegments = 24;
+        return new THREE.TorusBufferGeometry(radius, tubeRadius, radialSegments, tubularSegments);
+      },
+    },
+    TorusKnotBufferGeometry: {
+      create() {
+        const radius = 3.5;
+        const tube = 1.5;
+        const radialSegments = 8;
+        const tubularSegments = 64;
+        const p = 2;
+        const q = 3;
+        return new THREE.TorusKnotBufferGeometry(radius, tube, tubularSegments, radialSegments, p, q);
+      },
+    },
+    TubeBufferGeometry: {
+      create() {
+        class CustomSinCurve extends THREE.Curve {
+          constructor(scale) {
+            super();
+            this.scale = scale;
+          }
+          getPoint(t) {
+            const tx = t * 3 - 1.5;
+            const ty = Math.sin(2 * Math.PI * t);
+            const tz = 0;
+            return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
+          }
+        }
+
+        const path = new CustomSinCurve(4);
+        const tubularSegments = 20;
+        const radius = 1;
+        const radialSegments = 8;
+        const closed = false;
+        return new THREE.TubeBufferGeometry(path, tubularSegments, radius, radialSegments, closed);
+      },
+    },
+    EdgesGeometry: {
+      create() {
+        const width = 8;
+        const height = 8;
+        const depth = 8;
+        return {
+          lineGeometry: new THREE.EdgesGeometry(new THREE.BoxBufferGeometry(width, height, depth)),
+        };
+      },
+      nonBuffer: false,
+    },
+    WireframeGeometry: {
+      create() {
+        const width = 8;
+        const height = 8;
+        const depth = 8;
+        return {
+          lineGeometry: new THREE.WireframeGeometry(new THREE.BoxBufferGeometry(width, height, depth)),
+        };
+      },
+      nonBuffer: false,
+    },
+    SphereBufferGeometryLow: {
+      create() {
+        const radius = 7;
+        const widthSegments = 5;
+        const heightSegments = 3;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+    },
+    SphereBufferGeometryMedium: {
+      create() {
+        const radius = 7;
+        const widthSegments = 24;
+        const heightSegments = 10;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+    },
+    SphereBufferGeometryHigh: {
+      create() {
+        const radius = 7;
+        const widthSegments = 50;
+        const heightSegments = 50;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+    },
+    SphereBufferGeometryLowSmooth: {
+      create() {
+        const radius = 7;
+        const widthSegments = 5;
+        const heightSegments = 3;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+      showLines: false,
+      flatShading: false,
+    },
+    SphereBufferGeometryMediumSmooth: {
+      create() {
+        const radius = 7;
+        const widthSegments = 24;
+        const heightSegments = 10;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+      showLines: false,
+      flatShading: false,
+    },
+    SphereBufferGeometryHighSmooth: {
+      create() {
+        const radius = 7;
+        const widthSegments = 50;
+        const heightSegments = 50;
+        return new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments);
+      },
+      showLines: false,
+      flatShading: false,
+    },
+    PlaneBufferGeometryLow: {
+      create() {
+        const width = 9;
+        const height = 9;
+        const widthSegments = 1;
+        const heightSegments = 1;
+        return new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments);
+      },
+    },
+    PlaneBufferGeometryHigh: {
+      create() {
+        const width = 9;
+        const height = 9;
+        const widthSegments = 10;
+        const heightSegments = 10;
+        return new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments);
+      },
+    },
+  };
+
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas, alpha: true});
+
+  function addLink(parent, name) {
+    const a = document.createElement('a');
+    a.href = `https://threejs.org/docs/#api/geometries/${name}`;
+    const code = document.createElement('code');
+    code.textContent = name;
+    a.appendChild(code);
+    parent.appendChild(a);
+    return a;
+  }
+
+  function addElem(parent, type, className, text) {
+    const elem = document.createElement(type);
+    elem.className = className;
+    if (text) {
+      elem.textContent = text;
+    }
+    parent.appendChild(elem);
+    return elem;
+  }
+
+  function addDiv(parent, className) {
+    return addElem(parent, 'div', className);
+  }
+
+  document.querySelectorAll('[data-primitive]').forEach(createPrimitiveDOM);
+  document.querySelectorAll('[data-primitive-diagram]').forEach(createPrimitiveDiagram);
+
+  function createPrimitiveDOM(base) {
+    const name = base.dataset.primitive;
+    const info = primitives[name];
+    if (!info) {
+      throw new Error(`no primitive ${name}`);
+    }
+
+    const text = base.innerHTML;
+    base.innerHTML = '';
+
+    const elem = addDiv(base, 'shape');
+
+    const right = addDiv(base, 'desc');
+    addLink(right, name);
+    if (info.nonBuffer !== false) {
+      addElem(right, 'span', '', ', ');
+      addLink(right, name.replace('Buffer', ''));
+    }
+    addDiv(right, '.note').innerHTML = text;
+
+    createPrimitive(elem, info);
+  }
+
+  function createPrimitiveDiagram(base) {
+    const name = base.dataset.primitiveDiagram;
+    const info = primitives[name];
+    if (!info) {
+      throw new Error(`no primitive ${name}`);
+    }
+    createPrimitive(base, info);
+  }
+
+  function createPrimitive(elem, info) {
+    const geometry = info.create();
+    const promise = (geometry instanceof Promise) ? geometry : Promise.resolve(geometry);
+    const scene = new THREE.Scene();
+
+    const root = new THREE.Object3D();
+    scene.add(root);
+
+    scene.add(new THREE.HemisphereLight(0xaaaaaa, 0x444444));
+    const light = new THREE.DirectionalLight(0xffffff, 1);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+
+    const fov = 60;
+    const aspect = 1;
+    const zNear = 0.1;
+    const zFar = 50;
+    const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+    camera.position.z = 15;
+
+    const controls = new THREE.OrbitControls(camera, elem);
+    controls.enableZoom = false;
+    controls.enablePan = false;
+
+    promise.then((geometryInfo) => {
+      if (geometryInfo instanceof THREE.BufferGeometry ||
+          geometryInfo instanceof THREE.Geometry) {
+        const geometry = geometryInfo;
+        geometryInfo = {
+          geometry,
+        };
+      }
+
+      const boxGeometry = geometryInfo.geometry || geometryInfo.lineGeometry;
+      boxGeometry.computeBoundingBox();
+      const centerOffset = new THREE.Vector3();
+      boxGeometry.boundingBox.getCenter(centerOffset).multiplyScalar(-1);
+
+      if (geometryInfo.geometry) {
+        const material = new THREE.MeshPhongMaterial({
+          flatShading: info.flatShading === false ? false : true,
+          side: THREE.DoubleSide,
+        });
+        material.color.setHSL(Math.random(), .5, .5);
+        const mesh = new THREE.Mesh(geometryInfo.geometry, material);
+        mesh.position.copy(centerOffset);
+        root.add(mesh);
+      }
+      if (info.showLines !== false) {
+        const lineMesh = new THREE.LineSegments(
+          geometryInfo.lineGeometry || geometryInfo.geometry,
+          new THREE.LineBasicMaterial({
+            color: geometryInfo.geometry ? 0xffffff : 0x000000,
+            transparent: true,
+            opacity: 0.5,
+          }));
+        lineMesh.position.copy(centerOffset);
+        root.add(lineMesh);
+      }
+    });
+
+    Object.assign(info, {
+      scene,
+      root,
+      elem,
+      camera,
+    });
+  }
+
+  const pixelRatio = 2;  // even on low-res we want hi-res rendering
+
+  function resizeRendererToDisplaySize(renderer) {
+    const canvas = renderer.domElement;
+    const width = canvas.clientWidth * pixelRatio;
+    const height = canvas.clientHeight * pixelRatio;
+    const needResize = canvas.width !== width || canvas.height !== height;
+    if (needResize) {
+      renderer.setSize(width, height, false);
+    }
+    return needResize;
+  }
+
+  // Three r93 needs to render at least once for some reason.
+  const scene = new THREE.Scene();
+  const camera = new THREE.Camera();
+
+  function render(time) {
+    time *= 0.001;
+
+    resizeRendererToDisplaySize(renderer);
+
+    renderer.setScissorTest(false);
+    // renderer.clear();
+
+    // Three r93 needs to render at least once for some reason.
+    renderer.render(scene, camera);
+
+    renderer.setScissorTest(true);
+
+    for (const info of Object.values(primitives)) {
+      const {root, scene, camera, elem} = info;
+      root.rotation.x = time * .1;
+      root.rotation.y = time * .11;
+
+      const rect = elem.getBoundingClientRect();
+      if (rect.bottom < 0 || rect.top  > renderer.domElement.clientHeight ||
+          rect.right  < 0 || rect.left > renderer.domElement.clientWidth) {
+        continue;
+      }
+
+      const width  = (rect.right - rect.left) * pixelRatio;
+      const height = (rect.bottom - rect.top) * pixelRatio;
+      const left   = rect.left * pixelRatio;
+      const top    = rect.top * pixelRatio;
+
+      camera.aspect = width / height;
+      camera.updateProjectionMatrix();
+
+      renderer.setViewport(left, top, width, height);
+      renderer.setScissor(left, top, width, height);
+      renderer.render(scene, camera);
+    }
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+
+

+ 390 - 0
threejs/lessons/threejs-primitives.md

@@ -0,0 +1,390 @@
+Title: Three.js Primitives
+Description: A tour of three.js primitives.
+
+This article one in a series of articles about three.js.
+The first article was [about fundamentals](threejs-fundamentals.html).
+If you haven't read that yet you might want to start there.
+
+Three.js has a large number of primitives. Primitives
+are generally 3D shapes that are generated at runtime
+with a bunch of parameters.
+
+It's common to use primitives for things like a sphere
+for globe or a bunch of boxes to draw a 3D graph. It's
+especially common to use primitives to experiment
+and get started with 3D. For the majority if 3D apps
+it's more common to have an artist make 3D models
+in a 3D modeling program. Later in this series we'll
+cover making and loading data from several 3D modeling
+programs. For now let's go over some of the available
+primitives.
+
+<div class="primitives">
+<div data-primitive="BoxBufferGeometry">A Box</div>
+<div data-primitive="CircleBufferGeometry">A flat circle</div>
+<div data-primitive="ConeBufferGeometry">A Cone</div>
+<div data-primitive="CylinderBufferGeometry">A Cylinder</div>
+<div data-primitive="DodecahedronBufferGeometry">A dodecahedron (12 sides)</div>
+<div data-primitive="ExtrudeBufferGeometry">An extruded 2d shape with optional bevelling.
+Here we are extruding a heart shape. Note this is the basis
+for `TextBufferGeometry` and `TextGeometry` respectively.</div>
+<div data-primitive="IcosahedronBufferGeometry">An icosahedron (20 sides)</div>
+<div data-primitive="LatheBufferGeometry">A shape generated by spinning a line</div>
+<div data-primitive="OctahedronBufferGeometry">An Octahedron (8 sides)</div>
+<div data-primitive="ParametricBufferGeometry">A surface generated by providing a function that takes a 2d point from a grid and returns the corresponding 3d point.</div>
+<div data-primitive="PlaneBufferGeometry">A 2D plane</div>
+<div data-primitive="PolyhedronBufferGeometry">Takes a set of triangles centered around a point and projects them onto a sphere</div>
+<div data-primitive="RingBufferGeometry">A 2D disc with a hole in the center</div>
+<div data-primitive="ShapeBufferGeometry">A 2d outline that gets trianglulated</div>
+<div data-primitive="SphereBufferGeometry">A sphere</div>
+<div data-primitive="TetrahedronBufferGeometry">A terahedron (4 sides)</div>
+<div data-primitive="TextBufferGeometry">3D Text generated from a 3D font and a string</div>
+<div data-primitive="TorusBufferGeometry">A torus (donut)</div>
+<div data-primitive="TorusKnotBufferGeometry">A torus knot</div>
+<div data-primitive="TubeBufferGeometry">A circle traced down a path</div>
+<div data-primitive="EdgesGeometry">A helper object that takes another geometry as input and generates edges only if the angle between faces is greater than some threshold. For example if you look at the box at the top it shows a line going through each face showing every triangle that makes the box. Using an EdgesGeometry instead the middle lines are removed.</div>
+<div data-primitive="WireframeGeometry">Generates geometry that contains one line segment (2 points) per edge in the given geometry. With out this you'd often be missing edges or get extra edges since WebGL generally requires 2 points per line segment. For example if all you had was a single triangle there would only be 3 points. If you tried to draw it using a material with <code>wireframe: true</code> you would only get a single line. Passing that triangle geometry to a <code>WireframeGeometry</code> will generate a new Geometry that has 3 lines segments using 6 points..</div>
+</div>
+
+You might notice of most of them come in pairs of `Geometry`
+or `BufferGeometry`. The difference between the 2 types is effectively flexibility
+vs performance.
+
+`BufferGeometry` based primitves are the performance oriented
+types. The vertices for the geometry are generated directly
+into an efficient typed array format ready to be uploaded to the GPU
+for rendering. This means they are faster to start up
+and take less memory but if you want to modify their
+data they take what is often considered more complex
+programming to manipulate.
+
+`Geometry` based primitives are the more flexible, easier to manipulate
+type. They are built from JavaScript based classes like [`Vector3`](https://threejs.org/docs/api/math/Vector3.html) for
+3D points, [`Face3`](https://threejs.org/docs/index.html#api/core/Face3) for triangles.
+They take quite a bit of memory and before they can be rendered three.js will need to
+convert them to something similar to the corresponding `BufferGeometry` representation.
+
+If you know you are not going to manipulate a primitive or
+if you're comfortable doing the math to manipulate their
+internals then it's best to go with the `BufferGeometry`
+based primitives. If on the other hand you want to change
+a few things before rendering you might find the `Geometry`
+based primitives easier to deal with.
+
+As an simple example a `BufferGeometry`
+can not have new vertices easily added. The number of vertices used is
+decided at creation time, storage is created, and then data for vertices
+are filled in. Where as for `Geometry` that is not true.
+
+We'll go over creating custom geometry in another article. For now
+let's make an example creating each type of primitive. We'll start
+with the [examples from the previous article](threejs-responsive.html).
+
+Near the top let's set a background color
+
+```
+const canvas = document.querySelector('#c');
+const renderer = new THREE.WebGLRenderer({canvas: canvas});
++renderer.setClearColor(0xAAAAAA);
+```
+
+This tells three.js to clear to lightish gray.
+
+The camera needs to change position so that we can see all the
+objects.
+
+```
+-const fov = 75;
++const fov = 40;
+const aspect = 2;  // the canvas default
+const zNear = 0.1;
+-const zFar = 5;
++const zFar = 1000;
+const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+-camera.position.z = 2;
++camera.position.z = 120;
+```
+
+Let's add a function, `addObject`, that takes an x, y position and an `Object3D` and adds
+the object to the scene.
+
+```
+const objects = [];
+const spread = 15;
+
+function addObject(x, y, obj) {
+  obj.position.x = x * spread;
+  obj.position.y = y * spread;
+
+  scene.add(obj);
+  objects.push(obj);
+}
+```
+
+Let's also make a function to create a random colored material.
+We'll use a feature of `Color` that lets you set a color
+based on hue, saturation, and luminance.
+
+`hue` goes from 0 to 1 around the color wheel with
+red at 0, green at .33 and blue at .66. `saturation`
+goes from 0 to 1 with 0 having no color and 1 being
+most saturated. `luminance` goes from 0 to 1
+with 0 being black, 1 being white and 0.5 being
+the maximum amount of color. In other words
+as `luminance` goes from 0.0 to 0.5 the color
+will go from black to `hue`. From 0.5 to 1.0
+the color will go from `hue` to white.
+
+```
+function createMaterial() {
+  const material = new THREE.MeshPhongMaterial({
+    side: THREE.DoubleSide,
+  });
+
+  const hue = Math.random();
+  const saturation = 1;
+  const luminance = .5;
+  material.color.setHSL(hue, saturation, luminance);
+
+  return material;
+}
+```
+
+We also passed `side: THREE.DoubleSide` to the material.
+This tells three to draw both sides of the triangles
+that make up a shape. For a solid shape like a sphere
+or a cube there's usually no reason to draw the
+back sides of triangles as they all face inside the
+shape. In our case though we are drawing a few things
+like the `PlaneBufferGeometry` and the `ShapeBufferGeometry`
+which are 2 dimensional and so have no inside. Without
+setting `side: THREE.DoubleSide` they would disappear
+when looking at their back sides.
+
+I should note that it's faster to draw when **not** setting
+`side: THREE.DoubleSide` so ideally we'd set it only on
+the materials that really need it but in this case we
+are not drawing too much so there isn't much reason to
+worry about it.
+
+Let's make a function, `addSolidGeometry`, that
+we pass a geometry and it creates a random colored
+material via `createMaterial` and adds it to the scene
+via `addObject`.
+
+```
+function addSolidGeometry(x, y, geometry) {
+  const mesh = new THREE.Mesh(geometry, createMaterial());
+  addObject(x, y, mesh);
+}
+```
+
+Now we can use this for the majority of the primitves we create.
+For example creating a box
+
+```
+{
+  const width = 8;
+  const height = 8;
+  const depth = 8;
+  addSolidGeometry(-2, -2, new THREE.BoxBufferGeometry(width, height, depth));
+}
+```
+
+Here's the result
+
+{{{example url="../threejs-primitives.html" }}}
+
+There are a couple of notable exceptions to the pattern above.
+The biggest is probably the `TextBufferGeometry`. It needs to load
+3D font data before it can generate a mesh for the text.
+That data loads asynchronously so we need to wait for it
+to load before trying to create the geometry. You can see below
+we create a `FontLoader` and pass it the url to our font
+and a callback. The callback is called after the font loads.
+In the callback we create the geometry
+and call `addObject` to add it the scene.
+
+```
+{
+  const loader = new THREE.FontLoader();
+  loader.load('resources/threejs/fonts/helvetiker_regular.typeface.json', (font) => {
+    const geometry = new THREE.TextBufferGeometry('three.js', {
+      font: font,
+      size: 3.0,
+      height: .2,
+      curveSegments: 12,
+      bevelEnabled: true,
+      bevelThickness: 0.15,
+      bevelSize: .3,
+      bevelSegments: 5,
+    });
+    const mesh = new THREE.Mesh(geometry, createMaterial());
+    geometry.computeBoundingBox();
+    geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
+
+    const parent = new THREE.Object3D();
+    parent.add(mesh);
+
+    addObject(-1, 1, parent);
+  });
+}
+```
+
+There's one other difference. We want to spin the text around its
+center but by default three.js creates the text off center. To
+work around this we can ask three.js to compute the bounding
+box of the geometry. We can then call the `getCenter` method
+of the bounding box and pass it our mesh's position object.
+`getCenter` copies the center of the box into the position.
+It also returns the position object so we can call `multiplyScaler(-1)`
+to position the entire object such that its
+effective center is um, ... at the center of the object.
+
+If we then just called `addSolidGeometry` like with previous
+examples it would set the position again which is
+no good. So, in this case we create an `Object3D` which
+is the standard node for the three.js scene graph. `Mesh`
+is inherited from `Object3D` as well. We'll cover how the scene graph
+works in another article. For now it's enough to know that
+like DOM nodes, children are drawn relative to their parent.
+By making an `Object3D` and making our mesh a child of that
+we can position the `Object3D` where ever we want and still
+keep the center offset we set earilier.
+
+If we didn't do this the text would spin off center.
+
+{{{example url="../threejs-primitives-text.html" }}}
+
+Notice the one on the left is not spinning around its center
+where as the one on the right is.
+
+The other exceptions are the 2 line based examples for `EdgesGeometry`
+and `WireframeGeometry`. Instead of calling `addSolidGeometry` they call
+`addLineGeomtry` which looks like this
+
+```
+function addLineGeometry(x, y, geometry) {
+  const material = new THREE.LineBasicMaterial({color: 0x000000});
+  const mesh = new THREE.LineSegments(geometry, material);
+  addObject(x, y, mesh);
+}
+```
+
+It creates a black `LineBasicMaterial` and then creates a `LineSegments`
+object which is a wrapper for `Mesh` that helps three know you're rendering
+line segments (2 points per segment).
+
+Each of the primitives has several parameters you can pass on creation
+and it's best to [look in the documentation](https://threejs.org/docs/) for all of them rather than
+repeat them here. You can also click the links above next to each shape
+to take you directly to the docs for that shape.
+
+One other thing that's important to cover is that almost all shapes
+have various settings for how much to subdivde them. A good example
+might be the sphere geometries. Sphere's take parameters for
+how many divisions to make around and how many top to bottom. For example
+
+<div class="spread">
+<div data-primitive-diagram="SphereBufferGeometryLow"></div>
+<div data-primitive-diagram="SphereBufferGeometryMedium"></div>
+<div data-primitive-diagram="SphereBufferGeometryHigh"></div>
+</div>
+
+The first sphere has 5 segments around and 3 high which is 15 segments
+or 30 triangles. The second sphere has 24 segments by 10. That's 240 segments
+or 480 triangles. The last one has 50 by 50 which is 2500 segments or 5000 triangles.
+
+It's up to you to decide how many subdivisions you need. It might
+look like you need the highest resolution but remove the lines
+and the flat shading and we get this
+
+<div class="spread">
+<div data-primitive-diagram="SphereBufferGeometryLowSmooth"></div>
+<div data-primitive-diagram="SphereBufferGeometryMediumSmooth"></div>
+<div data-primitive-diagram="SphereBufferGeometryHighSmooth"></div>
+</div>
+
+It's now not so clear that the one on the right with 5000 triangles
+is entirely better than the one in the middle with only 480.
+If you're only drawing a few spheres, like say a single globe for
+a map of the earth, then a single 10000 triangle sphere is not a bad
+choice. If on the otherhand you're trying to draw 1000 spheres
+then 1000 spheres times 10000 triangles each is 10 million triangles.
+To animate smoothly you need the browser to draw at 60 frames a
+second so you'd be asking the browser to draw 600 million triangles
+per second. That's a lot of computing.
+
+Sometimes it's easy to choose. For example you can also choose
+to subdivide a plane.
+
+<div class="spread">
+<div data-primitive-diagram="PlaneBufferGeometryLow"></div>
+<div data-primitive-diagram="PlaneBufferGeometryHigh"></div>
+</div>
+
+The plane on the left is 2 triangles. The plane on the right
+is 200 triangles. Unlike the sphere there is really no trade off in quality for most
+use cases of a plane. You'd most likely only subdivide a plane
+if you expected to want to modify or warp it in some way. A box
+is similar.
+
+So, choose whatever is appropriate for your situation. The less
+subdivisions you choose the more likely things will run smoothly. You'll have
+to decide for yourself what the correct tradeoff is.
+
+Next up let's go over [how three's scene graph works and how
+to use it](threejs-scenegraph.html).
+
+<canvas id="c"></canvas>
+<script src="../resources/threejs/r93/three.min.js"></script>
+<script src="../resources/threejs/r93/js/controls/OrbitControls.js"></script>
+<script src="resources/threejs-primitives.js"></script>
+<style>
+.spread {
+  display: flex;
+}
+.spread>div {
+  flex: 1 1 auto;
+  height: 150px;
+}
+.primitives {
+}
+.primitives>div {
+  display: flex;
+  align-items: center;
+  margin-bottom: 1em;
+}
+.primitives .shape {
+  flex: 0 0 auto;
+  width: 200px;
+  height: 200px;
+}
+.primitives .desc {
+  word-wrap: break-word;
+  padding: 1em;
+  min-width: 0;
+}
+.primitives .desc code {
+  white-space: normal;
+}
+@media (max-width: 550px) {
+  .primitives .shape {
+      width: 120px;
+      height: 120px;
+  }
+}
+.primitives .desc {
+  flex: 1 1 auto;
+}
+#c {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: -100;
+}
+</style>
+
+

+ 147 - 0
threejs/threejs-primitives-text.html

@@ -0,0 +1,147 @@
+<!-- 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 - Primitives</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.z = 40;
+
+  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 objects = [];
+  const spread = 15;
+
+  function addObject(x, y, obj) {
+    obj.position.x = x * spread;
+    obj.position.y = y * spread;
+
+    scene.add(obj);
+    objects.push(obj);
+  }
+
+  function createMaterial() {
+    const material = new THREE.MeshPhongMaterial({
+      side: THREE.DoubleSide,
+    });
+
+    const hue = Math.random();
+    const saturation = 1;
+    const luminance = .5;
+    material.color.setHSL(hue, saturation, luminance);
+
+    return material;
+  }
+
+  function addSolidGeometry(x, y, geometry) {
+    const mesh = new THREE.Mesh(geometry, createMaterial());
+    addObject(x, y, mesh);
+  }
+
+  {
+    const loader = new THREE.FontLoader();
+    loader.load('resources/threejs/fonts/helvetiker_regular.typeface.json', (font) => {
+      const geometry = new THREE.TextBufferGeometry('three.js', {
+        font: font,
+        size: 3.0,
+        height: .2,
+        curveSegments: 12,
+        bevelEnabled: true,
+        bevelThickness: 0.15,
+        bevelSize: .3,
+        bevelSegments: 5,
+      });
+
+      addSolidGeometry(-.5, 0, geometry);
+
+      const mesh = new THREE.Mesh(geometry, createMaterial());
+      geometry.computeBoundingBox();
+      geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
+
+      const parent = new THREE.Object3D();
+      parent.add(mesh);
+
+      addObject(.5, 0, parent);
+    });
+  }
+
+  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, ndx) => {
+      const speed = .5 + ndx * .05;
+      const rot = time * speed;
+      obj.rotation.x = rot;
+      obj.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+

+ 371 - 0
threejs/threejs-primitives.html

@@ -0,0 +1,371 @@
+<!-- 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 - Primitives</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.z = 120;
+
+  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 objects = [];
+  const spread = 15;
+
+  function addObject(x, y, obj) {
+    obj.position.x = x * spread;
+    obj.position.y = y * spread;
+
+    scene.add(obj);
+    objects.push(obj);
+  }
+
+  function createMaterial() {
+    const material = new THREE.MeshPhongMaterial({
+      side: THREE.DoubleSide,
+    });
+
+    const hue = Math.random();
+    const saturation = 1;
+    const luminance = .5;
+    material.color.setHSL(hue, saturation, luminance);
+
+    return material;
+  }
+
+  function addSolidGeometry(x, y, geometry) {
+    const mesh = new THREE.Mesh(geometry, createMaterial());
+    addObject(x, y, mesh);
+  }
+
+  function addLineGeometry(x, y, geometry) {
+    const material = new THREE.LineBasicMaterial({color: 0x000000});
+    const mesh = new THREE.LineSegments(geometry, material);
+    addObject(x, y, mesh);
+  }
+
+  {
+    const width = 8;
+    const height = 8;
+    const depth = 8;
+    addSolidGeometry(-2, 2, new THREE.BoxBufferGeometry(width, height, depth));
+  }
+  {
+    const radius = 7;
+    const segments = 24;
+    addSolidGeometry(-1, 2, new THREE.CircleBufferGeometry(radius, segments));
+  }
+  {
+    const radius = 6;
+    const height = 8;
+    const segments = 16;
+    addSolidGeometry(0, 2, new THREE.ConeBufferGeometry(radius, height, segments));
+  }
+  {
+    const radiusTop = 4;
+    const radiusBottom = 4;
+    const height = 8;
+    const radialSegments = 12;
+    addSolidGeometry(1, 2, new THREE.CylinderBufferGeometry(radiusTop, radiusBottom, height, radialSegments));
+  }
+  {
+    const radius = 7;
+    addSolidGeometry(2, 2, new THREE.DodecahedronBufferGeometry(radius));
+  }
+  {
+    const shape = new THREE.Shape();
+    const x = -2.5;
+    const y = -5;
+    shape.moveTo(x + 2.5, y + 2.5);
+    shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
+    shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
+    shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
+    shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
+    shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
+    shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
+
+    const extrudeSettings = {
+      steps: 2,
+      depth: 2,
+      bevelEnabled: true,
+      bevelThickness: 1,
+      bevelSize: 1,
+      bevelSegments: 2,
+    };
+
+    addSolidGeometry(-2, 1, new THREE.ExtrudeBufferGeometry(shape, extrudeSettings));
+  }
+  {
+    const radius = 7;
+    addSolidGeometry(-1, 1, new THREE.IcosahedronBufferGeometry(radius));
+  }
+  {
+    const points = [];
+    for (let i = 0; i < 10; ++i) {
+      points.push(new THREE.Vector2(Math.sin(i * 0.2) * 3 + 3, (i - 5) * .8));
+    }
+    addSolidGeometry(0, 1, new THREE.LatheBufferGeometry(points));
+  }
+  {
+    const radius = 7;
+    addSolidGeometry(1, 1, new THREE.OctahedronBufferGeometry(radius));
+  }
+  {
+    /*
+    from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js
+
+    The MIT License
+
+    Copyright © 2010-2018 three.js authors
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+    */
+    function klein(v, u, target) {
+      u *= Math.PI;
+      v *= 2 * Math.PI;
+      u = u * 2;
+
+      let x;
+      let y;
+      let z;
+
+      if (u < Math.PI) {
+          x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(u) * Math.cos(v);
+          z = -8 * Math.sin(u) - 2 * (1 - Math.cos(u) / 2) * Math.sin(u) * Math.cos(v);
+      } else {
+          x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(v + Math.PI);
+          z = -8 * Math.sin(u);
+      }
+
+      y = -2 * (1 - Math.cos(u) / 2) * Math.sin(v);
+
+      target.set(x, y, z).multiplyScalar(0.75);
+    }
+
+    const slices = 25;
+    const stacks = 25;
+    addSolidGeometry(2, 1, new THREE.ParametricBufferGeometry(klein, slices, stacks));
+  }
+  {
+    const width = 9;
+    const height = 9;
+    const widthSegments = 2;
+    const heightSegments = 2;
+    addSolidGeometry(-2, 0, new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments));
+  }
+  {
+    const verticesOfCube = [
+        -1, -1, -1,    1, -1, -1,    1,  1, -1,    -1,  1, -1,
+        -1, -1,  1,    1, -1,  1,    1,  1,  1,    -1,  1,  1,
+    ];
+    const indicesOfFaces = [
+        2, 1, 0,    0, 3, 2,
+        0, 4, 7,    7, 3, 0,
+        0, 1, 5,    5, 4, 0,
+        1, 2, 6,    6, 5, 1,
+        2, 3, 7,    7, 6, 2,
+        4, 5, 6,    6, 7, 4,
+    ];
+    const radius = 7;
+    const detail = 2;
+    addSolidGeometry(-1, 0, new THREE.PolyhedronBufferGeometry(verticesOfCube, indicesOfFaces, radius, detail));
+  }
+  {
+    const innerRadius = 2;
+    const outerRadius = 7;
+    const segments = 18;
+    addSolidGeometry(0, 0, new THREE.RingBufferGeometry(innerRadius, outerRadius, segments));
+  }
+  {
+    const shape = new THREE.Shape();
+    const x = -2.5;
+    const y = -5;
+    shape.moveTo(x + 2.5, y + 2.5);
+    shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
+    shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
+    shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
+    shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
+    shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
+    shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
+    addSolidGeometry(1, 0, new THREE.ShapeBufferGeometry(shape));
+  }
+  {
+    const radius = 7;
+    const widthSegments = 12;
+    const heightSegments = 8;
+    addSolidGeometry(2, 0, new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments));
+  }
+  {
+    const radius = 7;
+    addSolidGeometry(-2, -1, new THREE.TetrahedronBufferGeometry(radius));
+  }
+  {
+    const loader = new THREE.FontLoader();
+    loader.load('resources/threejs/fonts/helvetiker_regular.typeface.json', (font) => {
+      const geometry = new THREE.TextBufferGeometry('three.js', {
+        font: font,
+        size: 3.0,
+        height: .2,
+        curveSegments: 12,
+        bevelEnabled: true,
+        bevelThickness: 0.15,
+        bevelSize: .3,
+        bevelSegments: 5,
+      });
+      const mesh = new THREE.Mesh(geometry, createMaterial());
+      geometry.computeBoundingBox();
+      geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
+
+      const parent = new THREE.Object3D();
+      parent.add(mesh);
+
+      addObject(-1, -1, parent);
+    });
+  }
+  {
+    const radius = 5;
+    const tubeRadius = 2;
+    const radialSegments = 8;
+    const tubularSegments = 24;
+    addSolidGeometry(0, -1, new THREE.TorusBufferGeometry(radius, tubeRadius, radialSegments, tubularSegments));
+  }
+  {
+    const radius = 3.5;
+    const tube = 1.5;
+    const radialSegments = 8;
+    const tubularSegments = 64;
+    const p = 2;
+    const q = 3;
+    addSolidGeometry(1, -1, new THREE.TorusKnotBufferGeometry(radius, tube, tubularSegments, radialSegments, p, q));
+  }
+  {
+    class CustomSinCurve extends THREE.Curve {
+      constructor(scale) {
+        super();
+        this.scale = scale;
+      }
+      getPoint(t) {
+        const tx = t * 3 - 1.5;
+        const ty = Math.sin(2 * Math.PI * t);
+        const tz = 0;
+        return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
+      }
+    }
+
+    const path = new CustomSinCurve(4);
+    const tubularSegments = 20;
+    const radius = 1;
+    const radialSegments = 8;
+    const closed = false;
+    addSolidGeometry(2, -1, new THREE.TubeBufferGeometry(path, tubularSegments, radius, radialSegments, closed));
+  }
+  {
+    const width = 8;
+    const height = 8;
+    const depth = 8;
+    addLineGeometry(-1, -2, new THREE.EdgesGeometry(new THREE.BoxBufferGeometry(width, height, depth)));
+  }
+  {
+    const width = 8;
+    const height = 8;
+    const depth = 8;
+    addLineGeometry(1, -2, new THREE.WireframeGeometry(new THREE.BoxBufferGeometry(width, height, depth)));
+  }
+
+  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, ndx) => {
+      const speed = .1 + ndx * .05;
+      const rot = time * speed;
+      obj.rotation.x = rot;
+      obj.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+
+