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 */
 /* 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) {
   function smoothOrFlat(flatShading, radius = 7) {
     const widthDivisions = 12;
     const widthDivisions = 12;
     const heightDivisions = 9;
     const heightDivisions = 9;
@@ -15,14 +32,14 @@
   }
   }
 
 
   function basicLambertPhongExample(MaterialCtor, lowPoly, params = {}) {
   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({
     const material = new MaterialCtor(Object.assign({
       color: 'hsl(210,50%,50%)',
       color: 'hsl(210,50%,50%)',
     }, params));
     }, params));
-    return new THREE.Mesh(geometry, material);
+    return {
+      obj3D: new THREE.Mesh(geometry, material),
+      trackball: lowPoly,
+    };
   }
   }
 
 
   function sideExample(side) {
   function sideExample(side) {
@@ -48,6 +65,64 @@
     return base;
     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({
   threejsLessonUtils.addDiagrams({
     smoothShading: {
     smoothShading: {
       create() {
       create() {
@@ -142,6 +217,83 @@
         return basicLambertPhongExample(THREE.MeshToonMaterial);
         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: {
     MeshDepthMaterial: {
       create(props) {
       create(props) {
         const {camera} = 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 data-diagram="MeshToonMaterial"></div>
 </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`
 There are 3 materials that have special uses. `ShadowMaterial`
 is used to get the data created from shadows. We haven't
 is used to get the data created from shadows. We haven't
 covered shadows yet. When we do we'll use this material
 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
 no help from three.js. Both of these topics are large and will be
 covered later.
 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`.
 Most materials share a bunch of settings all defined by `Material`.
 [See the docs](Material)
 [See the docs](Material)
 for all of them but let's go over two of the most commonly used
 for all of them but let's go over two of the most commonly used