Browse Source

Add Texture article

Gregg Tavares 7 years ago
parent
commit
1f23298598

+ 3 - 0
.eslintrc.json

@@ -3,6 +3,9 @@
     "browser": true,
     "es6": true
   },
+  "parserOptions": {
+    "ecmaVersion": 8
+  },
   "plugins": [
     "eslint-plugin-html",
     "eslint-plugin-optional-comma-spacing",

BIN
threejs/lessons/resources/images/compressed-but-large-wood-texture.jpg


BIN
threejs/lessons/resources/images/js-console-image-resized-warning.png


BIN
threejs/lessons/resources/images/mip-example.png


BIN
threejs/lessons/resources/images/mip-low-res-enlarged.png


BIN
threejs/lessons/resources/images/mipmap-low-res-enlarged.png


+ 53 - 27
threejs/lessons/resources/threejs-lesson-utils.js

@@ -56,7 +56,7 @@ window.threejsLessonUtils = {
       renderer.domElement.style.transform = transform;
 
       this.renderFuncs.forEach((fn) => {
-          fn(renderer, time);
+        fn(renderer, time);
       });
 
       requestAnimationFrame(render);
@@ -86,25 +86,54 @@ window.threejsLessonUtils = {
     camera.position.z = 15;
     scene.add(camera);
 
-    const obj3D = info.create({scene, camera});
-    const promise = (obj3D instanceof Promise) ? obj3D : Promise.resolve(obj3D);
-
     const root = new THREE.Object3D();
     scene.add(root);
 
-    const resizeFunctions = [];
+    const renderInfo = {
+      pixelRatio: this.pixelRatio,
+      camera,
+      scene,
+      root,
+      renderer: this.renderer,
+      elem,
+    };
+
+    const obj3D = info.create({scene, camera, renderInfo});
+    const promise = (obj3D instanceof Promise) ? obj3D : Promise.resolve(obj3D);
+
     const updateFunctions = [];
+    const resizeFunctions = [];
+
+    const settings = {
+      lights: true,
+      trackball: true,
+      // resize(renderInfo) {
+      // },
+      // update(time, renderInfo) {
+      // },
+      render(renderInfo) {
+        renderInfo.renderer.render(renderInfo.scene, renderInfo.camera);
+      },
+    };
 
     promise.then((result) => {
       const info = result instanceof THREE.Object3D ? {
         obj3D: result,
       } : result;
-      const { obj3D, update, trackball, lights } = info;
-      root.add(obj3D);
+      if (info.obj3D) {
+        root.add(info.obj3D);
+      }
+      if (info.update) {
+        updateFunctions.push(info.update);
+      }
+      if (info.resize) {
+        resizeFunctions.push(info.resize);
+      }
 
+      Object.assign(settings, info);
       targetFOVDeg = camera.fov;
 
-      if (trackball !== false) {
+      if (settings.trackball !== false) {
         const controls = new THREE.TrackballControls(camera, elem);
         controls.noZoom = true;
         controls.noPan = true;
@@ -112,22 +141,17 @@ window.threejsLessonUtils = {
         updateFunctions.push(controls.update.bind(controls));
       }
 
-      if (update) {
-        updateFunctions.push(update);
-      }
-
       // add the lights as children of the camera.
       // this is because TrackbacllControls move the camera.
       // We really want to rotate the object itself but there's no
       // controls for that so we fake it by putting all the lights
       // on the camera so they move with it.
-      if (lights !== false) {
+      if (settings.lights !== false) {
         camera.add(new THREE.HemisphereLight(0xaaaaaa, 0x444444, .5));
         const light = new THREE.DirectionalLight(0xffffff, 1);
         light.position.set(-1, 2, 4 - 15);
         camera.add(light);
       }
-
     });
 
     let oldWidth = -1;
@@ -143,19 +167,20 @@ window.threejsLessonUtils = {
         return;
       }
 
-      const width  = (rect.right - rect.left) * this.pixelRatio;
-      const height = (rect.bottom - rect.top) * this.pixelRatio;
-      const left   = rect.left * this.pixelRatio;
-      const top    = rect.top * this.pixelRatio;
+      renderInfo.width = (rect.right - rect.left) * this.pixelRatio;
+      renderInfo.height = (rect.bottom - rect.top) * this.pixelRatio;
+      renderInfo.left = rect.left * this.pixelRatio;
+      renderInfo.top = rect.top * this.pixelRatio;
 
-      if (width !== oldWidth || height !== oldHeight) {
-        oldWidth = width;
-        oldHeight = height;
-        resizeFunctions.forEach(fn => fn());
+      if (renderInfo.width !== oldWidth || renderInfo.height !== oldHeight) {
+        oldWidth = renderInfo.width;
+        oldHeight = renderInfo.height;
+        resizeFunctions.forEach(fn => fn(renderInfo));
       }
-      updateFunctions.forEach(fn => fn(time));
 
-      const aspect = width / height;
+      updateFunctions.forEach(fn => fn(time, renderInfo));
+
+      const aspect = renderInfo.width / renderInfo.height;
       const fovDeg = aspect >= 1
         ? targetFOVDeg
         : THREE.Math.radToDeg(2 * Math.atan(Math.tan(THREE.Math.degToRad(targetFOVDeg) * .5) / aspect));
@@ -164,9 +189,10 @@ window.threejsLessonUtils = {
       camera.aspect = aspect;
       camera.updateProjectionMatrix();
 
-      renderer.setViewport(left, top, width, height);
-      renderer.setScissor(left, top, width, height);
-      renderer.render(scene, camera);
+      renderer.setViewport(renderInfo.left, renderInfo.top, renderInfo.width, renderInfo.height);
+      renderer.setScissor(renderInfo.left, renderInfo.top, renderInfo.width, renderInfo.height);
+
+      settings.render(renderInfo);
     };
 
     this.renderFuncs.push(render);

+ 245 - 0
threejs/lessons/resources/threejs-textures.js

@@ -0,0 +1,245 @@
+'use strict';
+
+/* global threejsLessonUtils */
+
+{
+  const loader = new THREE.TextureLoader();
+
+  function loadTextureAndPromise(url) {
+    let textureResolve;
+    const promise = new Promise((resolve) => {
+      textureResolve = resolve;
+    });
+    const texture = loader.load(url, (texture) => {
+      textureResolve(texture);
+    });
+    return {
+      texture,
+      promise,
+    };
+  }
+
+  const filterTextureInfo = loadTextureAndPromise('resources/images/mip-example.png');
+  const filterTexture = filterTextureInfo.texture;
+  const filterTexturePromise = filterTextureInfo.promise;
+
+  function filterCube(scale, texture) {
+    const size = 8;
+    const geometry = new THREE.BoxBufferGeometry(size, size, size);
+    const material = new THREE.MeshBasicMaterial({
+      map: texture || filterTexture,
+    });
+    const mesh = new THREE.Mesh(geometry, material);
+    mesh.scale.set(scale, scale, scale);
+    return mesh;
+  }
+
+  function lowResCube(scale, pixelSize = 16) {
+    const mesh = filterCube(scale);
+    const renderTarget = new THREE.WebGLRenderTarget(1, 1, {
+      magFilter: THREE.NearestFilter,
+      minFilter: THREE.NearestFilter,
+    });
+
+    const planeScene = new THREE.Scene();
+
+    const plane = new THREE.PlaneBufferGeometry(1, 1);
+    const planeMaterial = new THREE.MeshBasicMaterial({
+      map: renderTarget.texture,
+    });
+    const planeMesh = new THREE.Mesh(plane, planeMaterial);
+    planeScene.add(planeMesh);
+
+    const planeCamera = new THREE.OrthographicCamera(0, 1, 0, 1, -1, 1);
+    planeCamera.position.z = 1;
+
+    return {
+      obj3D: mesh,
+      update(time, renderInfo) {
+        const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
+        const rtWidth = Math.ceil(width / pixelRatio / pixelSize);
+        const rtHeight = Math.ceil(height / pixelRatio / pixelSize);
+        renderTarget.setSize(rtWidth, rtHeight);
+
+        camera.aspect = rtWidth / rtHeight;
+        camera.updateProjectionMatrix();
+
+        renderer.render(scene, camera, renderTarget);
+      },
+      render(renderInfo) {
+        const { width, height, renderer, pixelRatio } = renderInfo;
+        const viewWidth = width / pixelRatio / pixelSize;
+        const viewHeight = height / pixelRatio / pixelSize;
+        planeCamera.left = -viewWidth / 2;
+        planeCamera.right = viewWidth / 2;
+        planeCamera.top = viewHeight / 2;
+        planeCamera.bottom = -viewHeight / 2;
+        planeCamera.updateProjectionMatrix();
+
+        // compute the difference between our renderTarget size
+        // and the view size. The renderTarget is a multiple pixels magnified pixels
+        // so for example if the view is 15 pixels wide and the magnified pixel size is 10
+        // the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
+        // pixels so
+
+        planeMesh.scale.set(renderTarget.width, renderTarget.height, 1);
+
+        renderer.render(planeScene, planeCamera);
+      },
+    };
+  }
+
+  function createMip(level, numLevels, scale) {
+    const u = level / numLevels;
+    const size = 2 ** (numLevels - level - 1);
+    const halfSize = Math.ceil(size / 2);
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.canvas.width = size * scale;
+    ctx.canvas.height = size * scale;
+    ctx.scale(scale, scale);
+    ctx.fillStyle = level & 1 ? '#DDD' : '#000';
+    ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
+    ctx.fillRect(0, 0, size, size);
+    ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
+    ctx.fillRect(0, 0, halfSize, halfSize);
+    ctx.fillRect(halfSize, halfSize, halfSize, halfSize);
+    return ctx.canvas;
+  }
+
+  threejsLessonUtils.addDiagrams({
+    filterCube: {
+      create() {
+        return filterCube(1);
+      },
+    },
+    filterCubeSmall: {
+      create(info) {
+        return lowResCube(.1, info.renderInfo.pixelRatio);
+      },
+    },
+    filterCubeSmallLowRes: {
+      create() {
+        return lowResCube(1);
+      },
+    },
+    filterCubeMagNearest: {
+      async create() {
+        const texture = await filterTexturePromise;
+        const newTexture = texture.clone();
+        newTexture.magFilter = THREE.NearestFilter;
+        newTexture.needsUpdate = true;
+        return filterCube(1, newTexture);
+      },
+    },
+    filterCubeMagLinear: {
+      async create() {
+        const texture = await filterTexturePromise;
+        const newTexture = texture.clone();
+        newTexture.magFilter = THREE.LinearFilter;
+        newTexture.needsUpdate = true;
+        return filterCube(1, newTexture);
+      },
+    },
+    filterModes: {
+      async create(props) {
+        const { scene, camera, renderInfo } = props;
+        scene.background = new THREE.Color('black');
+        camera.zFar = 150;
+        const texture = await filterTexturePromise;
+        const root = new THREE.Object3D();
+        const depth = 50;
+        const plane = new THREE.PlaneBufferGeometry(1, depth);
+        const mipmap = [];
+        const numMips = 7;
+        for (let i = 0; i < numMips; ++i) {
+          mipmap.push(createMip(i, numMips, 1));
+        }
+
+        // Is this a design flaw in three.js?
+        // AFAIK there's no way to clone a texture really
+        // Textures can share an image and I guess deep down
+        // if the image is the same they might share a WebGLTexture
+        // but no checks for mipmaps I'm guessing. It seems like
+        // they shouldn't be checking for same image, the should be
+        // checking for same WebGLTexture. Given there is more than
+        // WebGL to support maybe they need to abtract WebGLTexture to
+        // PlatformTexture or something?
+
+        const meshInfos = [
+          { x: -1, y:  1, minFilter: THREE.NearestFilter,              magFilter: THREE.NearestFilter },
+          { x:  0, y:  1, minFilter: THREE.LinearFilter,               magFilter: THREE.LinearFilter },
+          { x:  1, y:  1, minFilter: THREE.NearestMipMapNearestFilter, magFilter: THREE.LinearFilter },
+          { x: -1, y: -1, minFilter: THREE.NearestMipMapLinearFilter,  magFilter: THREE.LinearFilter },
+          { x:  0, y: -1, minFilter: THREE.LinearMipMapNearestFilter,  magFilter: THREE.LinearFilter },
+          { x:  1, y: -1, minFilter: THREE.LinearMipMapLinearFilter,   magFilter: THREE.LinearFilter },
+        ].map((info) => {
+          const copyTexture = texture.clone();
+          copyTexture.minFilter = info.minFilter;
+          copyTexture.magFilter = info.magFilter;
+          copyTexture.wrapT = THREE.RepeatWrapping;
+          copyTexture.repeat.y = depth;
+          copyTexture.needsUpdate = true;
+
+          const mipTexture = new THREE.CanvasTexture(mipmap[0]);
+          mipTexture.mipmaps = mipmap;
+          mipTexture.minFilter = info.minFilter;
+          mipTexture.magFilter = info.magFilter;
+          mipTexture.wrapT = THREE.RepeatWrapping;
+          mipTexture.repeat.y = depth;
+
+          const material = new THREE.MeshBasicMaterial({
+            map: copyTexture,
+          });
+
+          const mesh = new THREE.Mesh(plane, material);
+          mesh.rotation.x = Math.PI * .5 * info.y;
+          mesh.position.x = info.x * 1.5;
+          mesh.position.y = info.y;
+          root.add(mesh);
+          return {
+            material,
+            copyTexture,
+            mipTexture,
+          };
+        });
+        scene.add(root);
+
+        renderInfo.elem.addEventListener('click', () => {
+          for (const meshInfo of meshInfos) {
+            const { material, copyTexture, mipTexture } = meshInfo;
+            material.map = material.map === copyTexture ? mipTexture : copyTexture;
+          }
+        });
+
+        return {
+          update(time, renderInfo) {
+            const {camera} = renderInfo;
+            camera.position.y = Math.sin(time * .2) * .5;
+          },
+          trackball: false,
+        };
+      },
+    },
+  });
+
+  const textureDiagrams = {
+    differentColoredMips(parent) {
+      const numMips = 7;
+      for (let i = 0; i < numMips; ++i) {
+        const elem = createMip(i, numMips, 4);
+        elem.className = 'border';
+        elem.style.margin = '1px';
+        parent.appendChild(elem);
+      }
+    },
+  };
+
+  function createTextureDiagram(elem) {
+    const name = elem.dataset.textureDiagram;
+    const info = textureDiagrams[name];
+    info(elem);
+  }
+
+  [...document.querySelectorAll('[data-texture-diagram]')].forEach(createTextureDiagram);
+}
+

+ 278 - 0
threejs/lessons/threejs-material-table.md

@@ -0,0 +1,278 @@
+Title: Material Feature Table
+Description: A Table showing which matierals have which features
+
+The most common materials in three.js are the Mesh materials. Here
+is a table showing which material support which features.
+
+<div>
+<div id="material-table" class="threejs_center"></div>
+<script>
+const materials = [
+  { 
+    name: 'MeshBasicMaterial',
+    shortName: 'Basic',
+    properties: [
+      'alphaMap',
+      'aoMap',
+      'aoMapIntensity',
+      'color',
+      'combine',
+      'envMap',
+      'lightMap',
+      'lightMapIntensity',
+      'map',
+      'reflectivity',
+      'refactionRatio',
+      'specularMap',
+      'wireframe',
+    ],
+  },
+  {
+    name: 'MeshLambertMaterial',
+    shortName: 'Lambert',
+    properties: [
+      'alphaMap',
+      'aoMap',
+      'aoMapIntensity',
+      'color',
+      'combine',
+      'emissive',
+      'emissiveMap',
+      'emissiveIntensity',
+      'envMap',
+      'lightMap',
+      'lightMapIntensity',
+      'map',
+      'reflectivity',
+      'refactionRatio',
+      'specularMap',
+      'wireframe',
+    ],
+  },
+  {
+    name: 'MeshPhongMaterial',
+    shortName: 'Phong',
+    properties: [
+      'alphaMap',
+      'aoMap',
+      'aoMapIntensity',
+      'bumpMap',
+      'bumpScale',
+      'color',
+      'combine',
+      'displacementMap',
+      'displacementScale',
+      'displacementBias',
+      'emissive',
+      'emissiveMap',
+      'emissiveIntensity',
+      'envMap',
+      'lightMap',
+      'lightMapIntensity',
+      'map',
+      'normalMap',
+      'normalMapType',
+      'normalScale',
+      'reflectivity',
+      'refactionRatio',
+      'shininess',
+      'specular',
+      'specularMap',
+      'wireframe',
+    ],
+  },
+  {
+    name: 'MeshStandardMaterial',
+    shortName: 'Standard',
+    properties: [
+      'alphaMap',
+      'aoMap',
+      'aoMapIntensity',
+      'bumpMap',
+      'bumpScale',
+      'color',
+      'displacementMap',
+      'displacementScale',
+      'displacementBias',
+      'emissive',
+      'emissiveMap',
+      'emissiveIntensity',
+      'envMap',
+      'evnMapIntensity',
+      'lightMap',
+      'lightMapIntensity',
+      'map',
+      'metalness',
+      'metalnessMap',
+      'normalMap',
+      'normalMapType',
+      'normalScale',
+      'refactionRatio',
+      'roughness',
+      'roughnessMap',
+      'wireframe',
+    ],
+  },
+  {
+    name: 'MeshPhysicsMaterial',
+    shortName: 'Physics',
+    properties: [
+      'alphaMap',
+      'aoMap',
+      'aoMapIntensity',
+      'bumpMap',
+      'bumpScale',
+      'color',
+      'displacementMap',
+      'displacementScale',
+      'displacementBias',
+      'emissive',
+      'emissiveMap',
+      'emissiveIntensity',
+      'envMap',
+      'evnMapIntensity',
+      'lightMap',
+      'lightMapIntensity',
+      'map',
+      'metalness',
+      'metalnessMap',
+      'normalMap',
+      'normalMapType',
+      'normalScale',
+      'refactionRatio',
+      'roughness',
+      'roughnessMap',
+      'wireframe',
+      'clearCoat',
+      'clearCoatRoughness',
+      'reflectivity',
+    ],
+  },
+];
+
+const allProperties = {};
+materials.forEach((material) => {
+  material.properties.forEach((property) => {
+    allProperties[property] = true;
+  });
+});
+
+function addElem(type, parent, content) {
+  const elem = document.createElement(type);
+  if (content) {
+    elem.textContent = content;
+  }
+  if (parent) {
+    parent.appendChild(elem);
+  }
+  return elem;
+}
+
+const table = document.createElement('table');
+const thead = addElem('thead', table);
+{
+  addElem('td', thead);
+  materials.forEach((material) => {
+    const td = addElem('td', thead);
+    const a = addElem('a', td, material.shortName);
+    a.href = `https://threejs.org/docs/#api/materials/${material.name}`;
+  });
+}
+Object.keys(allProperties).sort().forEach((property) => {
+  const tr = addElem('tr', table);
+  addElem('td', tr, property);
+  materials.forEach((material) => {
+    const hasProperty = material.properties.indexOf(property) >= 0;
+    const td = addElem('td', tr);
+    const a = addElem('a', td, hasProperty ? '•' : '');
+    a.href = `https://threejs.org/docs/#api/materials/${material.name}.${property}`;
+  });
+});
+document.querySelector('#material-table').appendChild(table);
+</script>
+<style>
+#material-table {
+  font-family: monospace;
+  display: flex;
+  justify-content: center;
+}
+#material-table tr:nth-child(even) {
+    background: #def;
+}
+#material-table thead>td {
+    vertical-align: bottom;
+    padding: .5em;
+}
+#material-table thead>td>a {
+    text-orientation: upright;
+    writing-mode: vertical-lr;
+    text-decoration: none;
+    display: block;
+    letter-spacing: -2px;
+}
+#material-table table {
+    border-collapse: collapse;
+    background: #cde;
+}
+#material-table td:nth-child(1) {
+    text-align: right;
+}
+#material-table td {
+    border: 1px solid black;
+    padding: .1em .5em .1em .5em;
+}
+#material-table td {
+  border: 1px solid black;
+}
+@media (max-width: 500px) {
+  #material-table {
+    font-size: small;
+  }
+  #material-table thead>td {
+      vertical-align: bottom;
+      padding: .5em 0 .5em 0;
+  }
+}
+</style>
+</div>
+
+<!--
+```
+phong
+  normalScale: 1,1 (0-1)
+  reflectivity: 0.5 (0-1)
+  refactionRatio: ???
+
+
+
+
+  MeshStandardMaterial
+  alphaMap: green channel
+  aoMap  (needs UV map, red channel)
+  aoMapIntensity: 1
+  bumpMap
+  bumpScale: 1
+  color
+  displacementMap
+  displacementScale
+  displacementBias
+  emissive
+  emissiveMap
+  emissiveIntensity: 1
+  envMap
+  evnMapIntensity: 1
+  lightMap (needs map)
+  lightMapIntensity: 1
+  map
+  metalness: 0.5 (0-1)
+  metalnessMap: (blue)
+  normalMap
+  normalMapType:  THREE.TangentSpaceNormalMap (default), and
+                  THREE.ObjectSpaceNormalMap.
+  normalScale: 1,1 (0-1)
+  refactionRatio: ???
+  roughness: 0.5 (0-1)
+  roughnessMap: (green)
+   wireframe
+```
+-->

+ 610 - 1
threejs/lessons/threejs-textures.md

@@ -1,4 +1,613 @@
 Title: Three.js Textures
 Description: Using Textures in three.js
 
-TBD
+This article is one in a series of articles about three.js.
+The first article was [about three.js fundamentals](threejs-fundamentals.html).
+The [previous article](threejs-setup.html) was about setting up for this article.
+If you haven't read that yet you might want to start there.
+
+Textures are a kind of large topic in Three.js and
+I'm not 100% sure at what level to explain them but I will try.
+There are many topics and many of them inter-relate so it's hard to explain
+them all at once. Here's quick table of contents for this article.
+
+<ul>
+<li><a href="#hello">Hello Texture</a></li>
+<li><a href="#six">6 Textures, a different one on each face of a cube</a></li>
+<li><a href="#loading">Loading Textures</a></li>
+<ul>
+  <li><a href="#easy">The Easy Way</a></li>
+  <li><a href="#wait1">Waiting for a texture to load</a></li>
+  <li><a href="#waitmany">Waiting for multiple textures to load</a></li>
+  <li><a href="#cors">Loading textures from other origins</a></li>
+</ul>
+<li><a href="#memory">Memory Usage</a></li>
+<li><a href="#format">JPG vs PNG</a></li>
+<li><a href="#filtering-and-mips">Filtering and Mips</a></li>
+<li><a href="#uvmanipulation">Repeating, offseting, rotating, wrapping</a></li>
+</ul>
+
+## <a name="hello"></a> Hello Texture
+
+Textures are *generally* images that are most often created
+in some 3rd party program like Photoshop or gIMP. For example let's
+put this image on cube.
+
+<div class="threejs_center">
+  <img src="../resources/images/wall.jpg" style="max-width: 600px;" class="border" >
+</div>
+
+We'll modify one of our first samples. All we need to do is create a `TextureLoader`. Call its
+[`load`](TextureLoader.load) method with the URL of an
+image and and set the material's `map` property to the result instead of setting its `color`.
+
+```
++const loader = new THREE.TextureLoader();
+
+const material = new THREE.MeshBasicMaterial({
+-  color: 0xFF8844,
++  map: loader.load('resources/images/wall.jpg'),
+});
+```
+
+Note that we're using `MeshBasicMaterial` so no need for any lights.
+
+{{{example url="../threejs-textured-cube.html" }}}
+
+## <a name="six"></a> 6 Textures, a different one on each face of a cube
+
+How about 6 textures, one on each face of a cube? 
+
+<div class="threejs_center">
+  <div>
+    <img src="../resources/images/flower-1.jpg" style="max-width: 100px;" class="border" >
+    <img src="../resources/images/flower-2.jpg" style="max-width: 100px;" class="border" >
+    <img src="../resources/images/flower-3.jpg" style="max-width: 100px;" class="border" >
+  </div>
+  <div>
+    <img src="../resources/images/flower-4.jpg" style="max-width: 100px;" class="border" >
+    <img src="../resources/images/flower-5.jpg" style="max-width: 100px;" class="border" >
+    <img src="../resources/images/flower-6.jpg" style="max-width: 100px;" class="border" >
+  </div>
+</div>
+
+We just make 6 materials and pass them as an array when we create the `Mesh`
+
+```
+const loader = new THREE.TextureLoader();
+
+-const material = new THREE.MeshBasicMaterial({
+-  map: loader.load('resources/images/wall.jpg'),
+-});
++const materials = [
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
++  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
++];
+-const cube = new THREE.Mesh(geometry, material);
++const cube = new THREE.Mesh(geometry, materials);
+```
+
+It works!
+
+{{{example url="../threejs-textured-cube-6-textures.html" }}}
+
+It should be noted though that by default the only Geometry that supports multiple
+materials is the `BoxGeometry` and `BoxBufferGeometry`. For other cases you will
+need to build or load custom geometry and/or modify texture coordinates. It's far
+more common to use a [Texture Atlas](https://en.wikipedia.org/wiki/Texture_atlas) 
+if you want to allow multiple images on a single
+geometry.
+
+What are texture coordinates? They are data added to each vertex of a piece of geometry
+that specify what part of the texture corresponds to that specific vertex. 
+We'll go over them when we start building custom geometry.
+
+## <a name="loading"></a> Loading Textures
+
+### <a name="easy"></a> The Easy Way
+
+Most of the code on this site uses the easiest method of loading textures. 
+We create a `TextureLoader` and then call its [`load`](TextureLoader.load) method. 
+This returns a `Texture` object.
+
+```
+const texture = loader.load('resources/images/flower-1.jpg');
+```
+
+It's important to note that using this method our texture will be transparent until
+the image is loaded asychronously by three.js at which point it will update the texture
+with the downloaded image.
+
+This has the big advantage that we don't have to wait for the texture to load and our
+page will start rendering immediately. That's probably okay for a great many use cases
+but if we want we can ask three.js to tell us when the texture has finished downloading.
+
+### <a name="wait1"></a> Waiting for a texture to load
+
+To wait for a texture to load the `load` method of the texture loader takes a callback
+that will be called when the texture has finished loading. Going back to our top example
+we can wait for the texture to load before creating our `Mesh` and adding it to scene
+like this
+
+```
+const loader = new THREE.TextureLoader();
+loader.load('resources/images/wall.jpg', (texture) => {
+  const material = new THREE.MeshBasicMaterial({
+    map: texture,
+  });
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+});
+```
+
+Unless you clear your browser's cache and have a slow connection you're unlikely
+to see the any difference but rest assured it is waiting for the texture to load.
+
+{{{example url="../threejs-textured-cube-wait-for-texture.html" }}}
+
+### <a name="waitmany"></a> Waiting for multiple textures to load
+
+To wait until all textures have loaded you can use a `LoadingManager`. Create one
+and pass it to the `TextureLoader` then set its  [`onLoad`](LoadingManager.onLoad)
+property to a callback.
+
+```
++const loadManager = new THREE.LoadingManager();
+*const loader = new THREE.TextureLoader(loadManager);
+
+const materials = [
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
+  new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
+];
+
++loadManager.onLoad = () => {
++  const cube = new THREE.Mesh(geometry, materials);
++  scene.add(cube);
++  cubes.push(cube);  // add to our list of cubes to rotate
++};
+```
+
+The `LoadingManager` also has an [`onProgress`](LoadingManager.onProgress) property
+we can set to another callback to show a progress indicator.
+
+First we'll add a progress bar in HTML
+
+```
+<body>
+  <canvas id="c"></canvas>
++  <div id="loading">
++    <div class="progress"><div class="progressbar"></div></div>
++  </div>
+</body>
+```
+
+and the CSS for it
+
+```
+#loading {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+#loading .progress {
+    margin: 1.5em;
+    border: 1px solid white;
+    width: 50vw;
+}
+#loading .progressbar {
+    margin: 2px;
+    background: white;
+    height: 1em;
+    transform-origin: top left;
+    transform: scaleX(0);
+}
+
+```
+
+Then in the code we'll update the scale of the `progressbar` in our `onProgress` callback. It gets
+called with the URL of the last item loaded, the number of items loaded so far, and the total
+number of items loaded.
+
+```
++const loadingElem = document.querySelector('#loading');
++const progressBarElem = loadingElem.querySelector('.progressbar');
+
+loadManager.onLoad = () => {
++  loadingElem.style.display = 'none';
+  const cube = new THREE.Mesh(geometry, materials);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+};
+
++loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
++  const progress = itemsLoaded / itemsTotal;
++  progressBarElem.style.transform = `scaleX(${progress})`;
++};
+```
+
+Unless you clear your cache and have a slow connection you might not see
+the loading bar.
+
+{{{example url="../threejs-textured-cube-wait-for-all-textures.html" }}}
+
+## <a name="cors"></a> Loading textures from other origins.
+
+To use images from other servers those servers need to send the correct headers.
+If they don't you can not use the images in three.js and will get an error.
+If you run the server providing the images make sure it
+[sends the correct headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
+If you don't control the server hosting the images and it does not send the 
+permission headers then you can't use the images from that server.
+
+For example [imgur](https://imgur.com), [flickr](https://flickr.com), and
+[github](https://github.com) all send headers allowing you to use images
+hosted on their servers in three.js. Most other websites do not.
+
+## <a name="memory"></a> Memory Usage
+
+Textures are often the part of a three.js app that use the most memory. It's important to understand
+that *in general*, textures take `width * height * 4 * 1.33` bytes of memory. 
+
+Notice that says nothing about compression. I can make a .jpg image and set its compression super
+high. For example let's say I was making a scene of a house. Inside the house there is a table
+and I decide to put this wood texture on the top surface of the table
+
+<div class="threejs_center"><img class="border" src="resources/images/compressed-but-large-wood-texture.jpg" align="center" style="max-width: 300px"></div>
+
+That image is only 157k so it will download relatively quickly but it is actually 
+3024 x 3761 pixels in size. Following the equation above that's
+
+    3024 * 3761 * 4 * 1.33 = 60505764.5
+
+That image will take **60 MEG OF MEMORY!** in three.js. 
+A few textures like that and you'll be out of memory.
+
+I bring this up because it's important to know that using textures has a hidden cost. 
+In order for three.js to use the texture it has to hand it off to the GPU and the 
+GPU *in general* requires the texture data to be uncompressed.
+
+The moral of the story is make your textures small in dimensions not just small 
+in file size. Small in file size = fast to download. Small in dimesions = takes 
+less memory. How small should you make them?
+As small as you can and still look as good as you need them to look.
+
+## <a name="format"></a> JPG vs PNG
+
+This is pretty much the same as regular HTML in that JPGs have lossy compression, 
+PNGs have lossless compression so PNGs are generally slower to download. 
+But, PNGs support transparency. PNGs are also probably the appropriate format
+for non-image data like normal maps, and other kinds of non-image maps which we'll go over later. 
+
+It's important to remember that a JPG doesn't use
+less memory than a PNG in WebGL. See above.
+
+## <a name="filtering-and-mips"></a> Filtering and Mips
+
+Let's apply this 16x16 texture
+
+<div class="threejs_center"><img src="resources/images/mip-low-res-enlarged.png" class="border" align="center"></div>
+
+To a cube
+
+<div class="spread"><div data-diagram="filterCube"></div></div>
+
+Let's draw that cube really small
+
+<div class="spread"><div data-diagram="filterCubeSmall"></div></div>
+
+Hmmm, I guess that's hard to see. Let's magnify that tiny cube
+
+<div class="spread"><div data-diagram="filterCubeSmallLowRes"></div></div>
+
+How does the GPU know which colors to make each pixel it's drawing for the tiny cube?
+What if the cube was so small that it's just 1 or 2 pixels?
+
+This is what filtering is about.
+
+If it was Photoshop, Photoshop would average nearly all the pixels together to figure out what color
+to make those 1 or 2 pixels. That would be a very slow operation. GPUs solve this issue
+using mipmaps.
+
+Mips are copies of the texture, each one half as wide and half as tall as the previous
+mip where the pixels have been blended to make the next smaller mip. Mips are created 
+until we get all the way to a 1x1 pixel mip. For the image above all of the mips would 
+end up being something like this
+
+<div class="threejs_center"><img src="resources/images/mipmap-low-res-enlarged.png" align="center"></div>
+
+Now, when the cube is drawn so small that it's only 1 or 2 pixels large the GPU can choose
+to use just the smallest or next to smallest mip level to decide what color to make the
+tiny cube.
+
+In three you can choose what happens both what happens when the texture is drawn
+larger than its original size and what happens when it's drawn smaller than its
+original size.
+
+For setting the filter when the texture is drawn larger than its original size
+you set [`texture.magFilter`](Texture.magFilter) property to either `THREE.NearestFilter` or
+ `THREE.LinearFilter`.  `NearestFilter` means
+just pick the closet single pixel from the orignal texture. With a low
+resolution texture this gives you a very pixelated look like Minecraft.
+
+`LinearFilter` means choose the 4 pixels from the texture that are closest
+to the where we should be choosing a color from and blend them in the
+appropriate proportions relative to how far away the actual point is from
+each of the 4 pixels.
+
+<div class="spread">
+  <div>
+    <div data-diagram="filterCubeMagNearest" style="height: 250px;"></div>
+    <div class="code">Nearest</div>
+  </div>
+  <div>
+    <div data-diagram="filterCubeMagLinear" style="height: 250px;"></div>
+    <div class="code">Linear</div>
+  </div>
+</div>
+
+For setting the filter when the texture is drawn smaller than its original size
+you set the [`texture.minFilter`](Texture.minFilter) property to one of 6 values.
+
+* `THREE.NearestFilter`
+
+   same as above. Choose the closest pixel in the texture
+
+* `THREE.LinearFilter`
+
+   same as above, Choose 4 pixels from the texture and blend them
+
+* `THREE.NearestMipMapNearestFilter`
+
+   choose the appropriate mip then choose one pixel.
+
+* `THREE.NearestMipMapLinearFilter`
+
+   choose 2 mips, choose one pixel from each, blend the 2 pixels.
+
+* `THREE.LinearMipMapNearestFilter`
+
+   chose the appropriate mip then choose 4 pixels and blend them.
+
+*  `THREE.LinearMipMapLinearFilter`
+
+   choose 2 mips, choose 4 pixels from each and blend all 8 into 1 pixel.
+
+Here's an example showing all 6 settings
+
+<div class="spread">
+  <div data-diagram="filterModes" style="
+    height: 450px; display: flex;
+    align-items: center;
+    justify-content: flex-start;"
+  >
+    <div style="
+      background: rgba(255,0,0,.8);
+      color: white;
+      padding: .5em;
+      margin: 1em;
+      font-size: small;
+      border-radius: .5em;
+      line-height: 1.2;
+      user-select: none;"
+    >click to<br/>change<br/>texture</div>
+  </div>
+</div>
+
+One thing to notice is the top left and top middle using `NearestFilter` and `LinearFilter`
+don't use the mips. Because of that they flicker in the distance because the GPU is
+picking pixels from the original texture. On the left just one pixel is chosen and 
+in the middle 4 are chosen and blended but it's not enough come up with a good
+representative color. The other 4 strips do better with the bottom right,
+`LinearMipMapLinearFilter` being best.
+
+If you click the picture above it will toggle between the texture we've been using above
+and a texture where every mip level is a different color. 
+
+<div class="threejs_center">
+  <div data-texture-diagram="differentColoredMips"></div>
+</div>
+
+This makes it more clear
+what is happening. You can see in the top left and top middle the first mip is used all the way
+into the distance. The top right and bottom middle you can clearly see where a different mip
+is used.
+
+Switching back to the original texture you can see the bottom right is the smoothest,
+highest quality. You might ask why not always use that mode. The most obvious reason
+is sometimes you want things to be pixelated for a retro look or some other reason.
+The next most common reason is that reading 8 pixels and blending them is slower
+than reading 1 pixel and blending. While it's unlikely that a single texture is going
+to be the difference between fast and slow as we progress further into these articles
+we'll eventually have materials that use 4 or 5 textures all at once. 4 textures * 8
+pixels per texture is looking up 32 pixels for ever pixel rendered.
+This can be especially important to consider on mobile devices.
+
+## <a href="uvmanipulation"></a> Repeating, offseting, rotating, wrapping a texture
+
+Textures have settings for repeating, offseting, and rotating a texture.
+
+By default textures in three.js do not repeat. To set whether or not a
+texture repeats there are 2 properties, [`wrapS`](Texture.wrapS) for horizontal wrapping
+and [`wrapT`](Texture.wrapT) for vertical wrapping.
+
+They can be set to one of:
+
+* `THREE.ClampToEdgeWrapping`
+
+   The last pixel on each edge is repeated forever
+
+* `THREE.RepeatWrapping`
+
+   The texture is repeated
+
+* `THREE.MirroredRepeatWrapping`
+
+   The texture is mirrored and repeated.
+
+For example to turn on wrapping in both directions:
+
+```
+someTexture.wrapS = THREE.RepeatWrapping;
+someTexture.wrapT = THREE.RepeatWrapping;
+```
+
+Repeating is set with the [repeat] repeat property.
+
+```
+const timesToRepeatHorizontally = 4;
+const timesToRepeatVertically = 2;
+someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);
+```
+
+Offseting the texture can be done by setting the `offset` property. Textures
+are offset with units where 1 unit = 1 texture size. On other words 0 = no offset 
+and 1 = offset one full texture amount.
+
+```
+const xOffset = .5;   // offset by half the texture
+const yOffset = .25;  // offset by 1/2 the texture
+someTexture.offset.set(xOffset, yOffset);`
+```
+
+Rotating the texture can be set by setting `rotation` property in radians
+as well as the `center` property for choosing the center of rotation.
+It defaults to 0,0 which rotates from the bottom left corner. Like offset
+these units are in texture size so setting them to `.5, .5` would rotate
+around the center of the texture.
+
+```
+someTexture.center.set(.5, .5);
+someTexture.rotation = THREE.Math.degToRad(45); 
+```
+
+Let's modify the top sample above to play with these values
+
+First we'll keep a reference to the texture so we can manipulate it
+
+```
++const texture = loader.load('resources/images/wall.jpg');
+const material = new THREE.MeshBasicMaterial({
+-  map: loader.load('resources/images/wall.jpg');
++  map: texture,
+});
+```
+
+Then we'll use [dat.GUI](https://github.com/dataarts/dat.gui) again to provide a simple interface.
+
+```
+<script src="../3rdparty/dat.gui.min.js"></script>
+```
+
+As we did in previous dat.GUI examples we'll use a simple class to
+give dat.GUI an object that it can manipulate in degrees
+but that will set a property in radians.
+
+```
+class DegRadHelper {
+  constructor(obj, prop) {
+    this.obj = obj;
+    this.prop = prop;
+  }
+  get value() {
+    return THREE.Math.radToDeg(this.obj[this.prop]);
+  }
+  set value(v) {
+    this.obj[this.prop] = THREE.Math.degToRad(v);
+  }
+}
+```
+
+We also need a class that will convert from a string like `"123"` into
+a number like `123` since three.js requires numbers for enum settings
+like `wrapS` and `wrapT` but dat.GUI only uses strings for enums.
+
+```
+class StringToNumberHelper {
+  constructor(obj, prop) {
+    this.obj = obj;
+    this.prop = prop;
+  }
+  get value() {
+    return this.obj[this.prop];
+  }
+  set value(v) {
+    this.obj[this.prop] = parseFloat(v);
+  }
+}
+```
+
+Using those classes we can setup a simple GUI for the settings above
+
+```
+const wrapModes = {
+  'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping,
+  'RepeatWrapping': THREE.RepeatWrapping,
+  'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping,
+};
+
+function updateTexture() {
+  texture.needsUpdate = true;
+}
+
+const gui = new dat.GUI();
+gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes)
+  .name('texture.wrapS')
+  .onChange(updateTexture);
+gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes)
+  .name('texture.wrapT')
+  .onChange(updateTexture);
+gui.add(texture.repeat, 'x', 0, 5).name('texture.repeat.x');
+gui.add(texture.repeat, 'y', 0, 5).name('texture.repeat.y');
+gui.add(texture.offset, 'x', -2, 2).name('texture.offset.x');
+gui.add(texture.offset, 'y', -2, 2).name('texture.offset.y');
+gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x');
+gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y');
+gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360)
+  .name('texture.rotation');
+```
+
+The last thing to note about the example is that if you change `wrapS` or
+`wrapT` on the texture you must also set [`texture.needsUpdate`](Texture.needsUpdate) 
+so three.js knows to apply those settings. The other settings are automatically applied.
+
+{{{example url="../threejs-textured-cube-adjust.html" }}}
+
+This is only one step into the topic of textures. At some point we'll go over
+texture coordinates as well as 9 other types of textures that can be applied
+to materials.
+
+<!--
+alpha 
+ao
+env
+light
+specular
+bumpmap ?
+normalmap ?
+metalness
+roughness
+-->
+
+<canvas id="c"></canvas>
+<script src="../resources/threejs/r94/three.min.js"></script>
+<script src="../resources/threejs/r94/js/controls/TrackballControls.js"></script>
+<script src="resources/threejs-lesson-utils.js"></script>
+<script src="resources/threejs-textures.js"></script>
+
+
+
+

+ 12 - 7
threejs/lessons/toc.html

@@ -1,13 +1,18 @@
 <ul>
   <li>Fundamentals</li>
   <ul>
-    <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>
-    <li><a href="/threejs/lessons/threejs-materials.html">Three.js Materials</a></li>
-    <li><a href="/threejs/lessons/threejs-materials.html">Three.js Setup</a></li>
-    <li><a href="/threejs/lessons/threejs-fog.html">Three.js Fog</a></li>
+    <li><a href="/threejs/lessons/threejs-fundamentals.html">Fundamentals</a></li>
+    <li><a href="/threejs/lessons/threejs-responsive.html">Responsive Design</a></li>
+    <li><a href="/threejs/lessons/threejs-primitives.html">Primitives</a></li>
+    <li><a href="/threejs/lessons/threejs-scenegraph.html">Scenegraph</a></li>
+    <li><a href="/threejs/lessons/threejs-materials.html">Materials</a></li>
+    <li><a href="/threejs/lessons/threejs-setup.html">Setup</a></li>
+    <li><a href="/threejs/lessons/threejs-setup.html">Textures</a></li>
+    <li><a href="/threejs/lessons/threejs-fog.html">Fog</a></li>
+  </ul>
+  <li>Reference</li>
+  <ul>
+    <li><a href="/threejs/lessons/threejs-material-table.html">Material Table</a></li>
   </ul>
 </ul>
 <ul>

BIN
threejs/resources/images/flower-1.jpg


BIN
threejs/resources/images/flower-2.jpg


BIN
threejs/resources/images/flower-3.jpg


BIN
threejs/resources/images/flower-4.jpg


BIN
threejs/resources/images/flower-5.jpg


BIN
threejs/resources/images/flower-6.jpg


BIN
threejs/resources/images/wall.jpg


+ 97 - 0
threejs/threejs-textured-cube-6-textures.html

@@ -0,0 +1,97 @@
+<!-- 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 - Textured Cube - 6 Textures</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const loader = new THREE.TextureLoader();
+
+  const materials = [
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
+  ];
+  const cube = new THREE.Mesh(geometry, materials);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+
+  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();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 148 - 0
threejs/threejs-textured-cube-adjust.html

@@ -0,0 +1,148 @@
+<!-- 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 - Textured Cube - Adjustments</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script src="../3rdparty/dat.gui.min.js"></script>
+<script>
+'use strict';
+
+/* global dat */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const loader = new THREE.TextureLoader();
+
+  const texture = loader.load('resources/images/wall.jpg');
+  const material = new THREE.MeshBasicMaterial({
+    map: texture,
+  });
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+
+  class DegRadHelper {
+    constructor(obj, prop) {
+      this.obj = obj;
+      this.prop = prop;
+    }
+    get value() {
+      return THREE.Math.radToDeg(this.obj[this.prop]);
+    }
+    set value(v) {
+      this.obj[this.prop] = THREE.Math.degToRad(v);
+    }
+  }
+
+  class StringToNumberHelper {
+    constructor(obj, prop) {
+      this.obj = obj;
+      this.prop = prop;
+    }
+    get value() {
+      return this.obj[this.prop];
+    }
+    set value(v) {
+      this.obj[this.prop] = parseFloat(v);
+    }
+  }
+
+  const wrapModes = {
+    'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping,
+    'RepeatWrapping': THREE.RepeatWrapping,
+    'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping,
+  };
+
+  function updateTexture() {
+    texture.needsUpdate = true;
+  }
+
+  const gui = new dat.GUI();
+  gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes)
+    .name('texture.wrapS')
+    .onChange(updateTexture);
+  gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes)
+    .name('texture.wrapT')
+    .onChange(updateTexture);
+  gui.add(texture.repeat, 'x', 0, 5).name('texture.repeat.x');
+  gui.add(texture.repeat, 'y', 0, 5).name('texture.repeat.y');
+  gui.add(texture.offset, 'x', -2, 2).name('texture.offset.x');
+  gui.add(texture.offset, 'y', -2, 2).name('texture.offset.y');
+  gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x');
+  gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y');
+  gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360)
+    .name('texture.rotation');
+
+  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();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 135 - 0
threejs/threejs-textured-cube-wait-for-all-textures.html

@@ -0,0 +1,135 @@
+<!-- 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 - Textured Cube - 6 Textures</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    #loading {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+    }
+    #loading .progress {
+        margin: 1.5em;
+        border: 1px solid white;
+        width: 50vw;
+    }
+    #loading .progressbar {
+        margin: 2px;
+        background: white;
+        height: 1em;
+        transform-origin: top left;
+        transform: scaleX(0);
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+    <div id="loading">
+      <div class="progress"><div class="progressbar"></div></div>
+    </div>
+  </body>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxBufferGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const loadManager = new THREE.LoadingManager();
+  const loader = new THREE.TextureLoader(loadManager);
+
+  const materials = [
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
+    new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
+  ];
+
+  const loadingElem = document.querySelector('#loading');
+  const progressBarElem = loadingElem.querySelector('.progressbar');
+
+  loadManager.onLoad = () => {
+    loadingElem.style.display = 'none';
+    const cube = new THREE.Mesh(geometry, materials);
+    scene.add(cube);
+    cubes.push(cube);  // add to our list of cubes to rotate
+  };
+
+  loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => {
+    const progress = itemsLoaded / itemsTotal;
+    progressBarElem.style.transform = `scaleX(${progress})`;
+  };
+
+  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();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 93 - 0
threejs/threejs-textured-cube-wait-for-texture.html

@@ -0,0 +1,93 @@
+<!-- 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 - Textured Cube - Wait for Texture</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const loader = new THREE.TextureLoader();
+  loader.load('resources/images/wall.jpg', (texture) => {
+    const material = new THREE.MeshBasicMaterial({
+      map: texture,
+    });
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+    cubes.push(cube);  // add to our list of cubes to rotate
+  });
+
+  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();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+

+ 92 - 0
threejs/threejs-textured-cube.html

@@ -0,0 +1,92 @@
+<!-- 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 - Textured Cube</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script>
+'use strict';
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const zNear = 0.1;
+  const zFar = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
+  camera.position.z = 2;
+
+  const scene = new THREE.Scene();
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  const cubes = [];  // just an array we can use to rotate the cubes
+  const loader = new THREE.TextureLoader();
+
+  const material = new THREE.MeshBasicMaterial({
+    map: loader.load('resources/images/wall.jpg'),
+  });
+  const cube = new THREE.Mesh(geometry, material);
+  scene.add(cube);
+  cubes.push(cube);  // add to our list of cubes to rotate
+
+  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();
+    }
+
+    cubes.forEach((cube, ndx) => {
+      const speed = .2 + ndx * .1;
+      const rot = time * speed;
+      cube.rotation.x = rot;
+      cube.rotation.y = rot;
+    });
+
+    renderer.render(scene, camera);
+
+    requestAnimationFrame(render);
+  }
+
+  requestAnimationFrame(render);
+}
+
+main();
+</script>
+</html>
+