Gregg Tavares 7 years ago
parent
commit
d470bd786e
2 changed files with 199 additions and 12 deletions
  1. 157 5
      threejs/lessons/resources/threejs-materials.js
  2. 42 7
      threejs/lessons/threejs-materials.md

+ 157 - 5
threejs/lessons/resources/threejs-materials.js

@@ -3,6 +3,23 @@
 /* global threejsLessonUtils */
 
 {
+  function makeSphere(widthDivisions, heightDivisions) {
+    const radius = 7;
+    return new THREE.SphereBufferGeometry(radius, widthDivisions, heightDivisions);
+  }
+
+  const highPolySphereGeometry = function() {
+    const widthDivisions = 100;
+    const heightDivisions = 50;
+    return makeSphere(widthDivisions, heightDivisions);
+  }();
+
+  const lowPolySphereGeometry = function() {
+    const widthDivisions = 12;
+    const heightDivisions = 9;
+    return makeSphere(widthDivisions, heightDivisions);
+  }();
+
   function smoothOrFlat(flatShading, radius = 7) {
     const widthDivisions = 12;
     const heightDivisions = 9;
@@ -15,14 +32,14 @@
   }
 
   function basicLambertPhongExample(MaterialCtor, lowPoly, params = {}) {
-    const radius = 7;
-    const widthDivisions = lowPoly ? 8 : 100;
-    const heightDivisions = lowPoly ? 5 : 50;
-    const geometry = new THREE.SphereBufferGeometry(radius, widthDivisions, heightDivisions);
+    const geometry = lowPoly ? lowPolySphereGeometry : highPolySphereGeometry;
     const material = new MaterialCtor(Object.assign({
       color: 'hsl(210,50%,50%)',
     }, params));
-    return new THREE.Mesh(geometry, material);
+    return {
+      obj3D: new THREE.Mesh(geometry, material),
+      trackball: lowPoly,
+    };
   }
 
   function sideExample(side) {
@@ -48,6 +65,64 @@
     return base;
   }
 
+  function makeStandardPhysicalMaterialGrid(elem, physical, update) {
+    const numMetal = 5;
+    const numRough = 7;
+    const meshes = [];
+    const MatCtor = physical ? THREE.MeshPhysicalMaterial : THREE.MeshStandardMaterial;
+    const color = physical ? 'hsl(160,50%,50%)' : 'hsl(140,50%,50%)';
+    for (let m = 0; m < numMetal; ++m) {
+      const row = [];
+      for (let r = 0; r < numRough; ++r) {
+        const material = new MatCtor({
+          color,
+          roughness: r / (numRough - 1),
+          metalness: m / (numMetal - 1),
+        });
+        const mesh = new THREE.Mesh(highPolySphereGeometry, material);
+        row.push(mesh);
+      }
+      meshes.push(row);
+    }
+    return {
+      obj3D: null,
+      trackball: false,
+      render(renderInfo) {
+        const {camera, scene, renderer} = renderInfo;
+        const rect = elem.getBoundingClientRect();
+
+        const width = (rect.right - rect.left) * renderInfo.pixelRatio;
+        const height = (rect.bottom - rect.top) * renderInfo.pixelRatio;
+        const left = rect.left * renderInfo.pixelRatio;
+        const top = rect.top * renderInfo.pixelRatio;
+
+        const cellSize = Math.min(width / numRough, height / numMetal) | 0;
+        const xOff = (width - cellSize * numRough) / 2;
+        const yOff = (height - cellSize * numMetal) / 2;
+
+        camera.aspect = 1;
+        camera.updateProjectionMatrix();
+
+        if (update) {
+          update(meshes);
+        }
+
+        for (let m = 0; m < numMetal; ++m) {
+          for (let r = 0; r < numRough; ++r) {
+            const x = left + xOff + r * cellSize;
+            const y = top + yOff + m * cellSize;
+            renderer.setViewport(x, y, cellSize, cellSize);
+            renderer.setScissor(x, y, cellSize, cellSize);
+            const mesh = meshes[m][r];
+            scene.add(mesh);
+            renderer.render(scene, camera);
+            scene.remove(mesh);
+          }
+        }
+      },
+    };
+  }
+
   threejsLessonUtils.addDiagrams({
     smoothShading: {
       create() {
@@ -142,6 +217,83 @@
         return basicLambertPhongExample(THREE.MeshToonMaterial);
       },
     },
+    MeshStandardMaterial: {
+      create(props) {
+        return makeStandardPhysicalMaterialGrid(props.renderInfo.elem, false);
+      },
+    },
+    MeshPhysicalMaterial: {
+      create(props) {
+        const settings = {
+          clearCoat: .5,
+          clearCoatRoughness: 0,
+        };
+
+        function addElem(parent, type, style = {}) {
+          const elem = document.createElement(type);
+          Object.assign(elem.style, style);
+          parent.appendChild(elem);
+          return elem;
+        }
+
+        function addRange(elem, obj, prop, min, max) {
+          const outer = addElem(elem, 'div', {
+            width: '100%',
+            textAlign: 'center',
+            'font-family': 'monospace',
+          });
+
+          const div = addElem(outer, 'div', {
+            textAlign: 'left',
+            display: 'inline-block',
+          });
+
+          const label = addElem(div, 'label', {
+            display: 'inline-block',
+            width: '12em',
+          });
+          label.textContent = prop;
+
+          const num = addElem(div, 'div', {
+            display: 'inline-block',
+            width: '3em',
+          });
+
+          function updateNum() {
+            num.textContent = obj[prop].toFixed(2);
+          }
+          updateNum();
+
+          const input = addElem(div, 'input', {
+          });
+          Object.assign(input, {
+            type: 'range',
+            min: 0,
+            max: 100,
+            value: (obj[prop] - min) / (max - min) * 100,
+          });
+          input.addEventListener('input', () => {
+            obj[prop] = min + (max - min) * input.value / 100;
+            updateNum();
+          });
+        }
+
+        const {elem} = props.renderInfo;
+        addRange(elem, settings, 'clearCoat', 0, 1);
+        addRange(elem, settings, 'clearCoatRoughness', 0, 1);
+        const area = addElem(elem, 'div', {
+          width: '100%',
+          height: '400px',
+        });
+
+        return makeStandardPhysicalMaterialGrid(area, true, (meshes) => {
+          meshes.forEach(row => row.forEach(mesh => {
+            mesh.material.clearCoat = settings.clearCoat;
+            mesh.material.clearCoatRoughness = settings.clearCoatRoughness;
+          }));
+        });
+      },
+    },
     MeshDepthMaterial: {
       create(props) {
         const {camera} = props;

+ 42 - 7
threejs/lessons/threejs-materials.md

@@ -150,6 +150,48 @@ own gradient map. This ends up giving a 2 tone look that looks like a cartoon.
   <div data-diagram="MeshToonMaterial"></div>
 </div>
 
+Next up there are 2 *physically based rendering* materials. Physically Based
+Rendering is often abbreviated PBR.
+
+The materials above use simple math to make materials that look 3D but they
+aren't what actually happens in real world. The 2 PBR materials use much more
+complex math to come close to what actually happens in the real world.
+
+The first one is `MeshStandardMaterial`. The biggest difference between
+`MeshPhongMaterial` and `MeshStandardMaterial` is it uses different parameters.
+`MeshPhongMaterial` had a `shininess` setting. `MeshStandardMaterial` has 2
+settings `roughness` and `metalness`.
+
+At a basic level [`roughness`](MeshStandardMaterial.roughness) is the opposite
+of `shininess`. Something that has a high roughness, like a baseball doesn't 
+have hard reflections where as something that's not rough, like a billiard ball
+is very shiny. Roughness goes from 0 to 1.
+
+The other setting, [`metalness`](MeshStandardMaterial.metalness), says
+how metal the material is. Metals behave differently than non-metals
+and so this setting goes from 0, not metal at all, to 1, 100% metal.
+
+Here's a quick sample of `MeshStandardMaterial` with `roughness` from 0 to 1
+across and `metalness` from 0 to 1 down.
+
+<div data-diagram="MeshStandardMaterial" style="min-height: 400px"></div>
+
+The `MeshPhysicalMaterial` is same as the `MeshStandardMaterial` but it
+adds a `clearCoat` parameter that goes from 0 to 1 for how much to 
+apply a clearcoat gloss layer and a `clearCoatRoughness` parameter
+that specifies how rough the gloss layer is.
+
+Here's the same grid of `roughness` by `metalness` as above but with
+`clearCoat` and `clearCoatRoughness` settings.
+
+<div data-diagram="MeshPhysicalMaterial" style="min-height: 400px"></div>
+
+The various standard materials progress from fastest to slowest
+`MeshBasicMaterial` ➡ `MeshLambertMaterial` ➡ `MeshPhongMaterial` ➡ 
+`MeshStandardMaterial` ➡ `MeshPhysicalMaterial`. The slower materials
+can make more realistic looking scenes but you might need to design
+your code to use the faster materials on low powered or mobile machines.
+
 There are 3 materials that have special uses. `ShadowMaterial`
 is used to get the data created from shadows. We haven't
 covered shadows yet. When we do we'll use this material
@@ -184,13 +226,6 @@ system. `RawShaderMaterial` is for making entirely custom shaders with
 no help from three.js. Both of these topics are large and will be
 covered later.
 
-The last 2 materials we'll mention here are the `MeshStandardMaterial`
-and the `MeshPhysicsMaterial`. Both implement what's called *physically
-based rendering* or often PBR for short. This is a way of computing
-material properties and lights that comes close to the way
-lights and materials work in the real word. We'll cover these in
-more detail in another article.
-
 Most materials share a bunch of settings all defined by `Material`.
 [See the docs](Material)
 for all of them but let's go over two of the most commonly used