Browse Source

add fog article

Gregg Tavares 7 years ago
parent
commit
550e4eaeff

+ 55 - 0
threejs/lessons/resources/threejs-fog.js

@@ -0,0 +1,55 @@
+'use strict';
+
+/* global threejsLessonUtils */
+
+{
+  function fogExample(scene, fog) {
+    scene.fog = fog;
+    const width = 4;
+    const height = 3;
+    const depth = 10;
+    const geometry = new THREE.BoxBufferGeometry(width, height, depth);
+    const material = new THREE.MeshPhongMaterial({color: 'hsl(130,50%,50%)'});
+    return new THREE.Mesh(geometry, material);
+  }
+
+  threejsLessonUtils.addDiagrams({
+    fog: {
+      create(props) {
+        const {scene} = props;
+        const color = 0xFFFFFF;
+        const near = 12;
+        const far = 18;
+        return fogExample(scene, new THREE.Fog(color, near, far));
+      },
+    },
+    fogExp2: {
+      create(props) {
+        const {scene} = props;
+        const color = 0xFFFFFF;
+        const density = 0.1;
+        return fogExample(scene, new THREE.FogExp2(color, density));
+      },
+    },
+    fogBlueBackgroundRed: {
+      create(props) {
+        const {scene} = props;
+        scene.background = new THREE.Color('#F00');
+        const color = '#00F';
+        const near = 12;
+        const far = 18;
+        return fogExample(scene, new THREE.Fog(color, near, far));
+      },
+    },
+    fogBlueBackgroundBlue: {
+      create(props) {
+        const {scene} = props;
+        scene.background = new THREE.Color('#00F');
+        const color = '#00F';
+        const near = 12;
+        const far = 18;
+        return fogExample(scene, new THREE.Fog(color, near, far));
+      },
+    },
+  });
+}

+ 251 - 0
threejs/lessons/threejs-fog.md

@@ -0,0 +1,251 @@
+Title: Three.js Fog
+Description: Fog in Three.js
+
+This article is part of a series of articles about three.js. The
+first article is [three.js fundamentals](threejs-fundamentals.html). If
+you haven't read that yet and you're new to three.js you might want to
+consider starting there. If you haven't read about cameras you might
+want to start with [this article](threejs-cameras.html).
+
+Fog in a 3D engine is generally a way of fading to a specific color
+based on the distance from the camera. In three.js you add fog by
+creating `Fog` or `FogExp2` object and setting it on the scene's
+[`fog`](https://threejs.org/docs/#api/scenes/Scene.fog) property.
+
+`Fog` lets you choose `near` and `far` settings which are distances
+from the camera. Anything closer than `near` is unaffected by fog.
+Anything further than `far` is completely the fog color. Parts between
+`near` and `far` fade from their material color to the fog color.
+
+There's also `FogExp2` which grows expotentially with distance from the camera.
+
+To use either type of fog you create one and and assign it to the scene as in
+
+```
+const scene = new THREE.Scene();
+{
+  const color = 0xFFFFFF;  // white
+  const near = 10;
+  const far = 100;
+  scene.fog = new THREE.Fog(color, near, far);
+}
+```
+
+or for `FogExp2` it would be
+
+```
+const scene = new THREE.Scene();
+{
+  const color = 0xFFFFFF;
+  const density = 0.1;
+  scene.fog = new THREE.FogExp2(color, density);
+}
+```
+
+`FogExp2` is closer to reality but `Fog` is used
+more commonly since it lets you choose a place to apply
+the fog so you can decide to show a clear scene
+up to a certain distance and then fade out to some color
+past that distance.
+
+<div class="spread">
+  <div>
+    <div data-diagram="fog"></div>
+    <div class="code">THREE.Fog</div>
+  </div>
+  <div>
+    <div data-diagram="fogExp2"></div>
+    <div class="code">THREE.FogExp2</div>
+  </div>
+</div>
+
+It's important to note that the fog is applied to *things that are rendered*.
+It is part of the calculation of each pixel of the color of the object. 
+What that means is if you want your scene to fade to a certain color you 
+need to set the fog **and** the background color to the same color. 
+The background color is set using the 
+[`scene.background`](https://threejs.org/docs/#api/scenes/Scene.background)
+property. To pick a background color you attach a `THREE.Color` to it. For example
+
+```
+scene.background = new THREE.Color('#F00');  // red
+```
+
+<div class="spread">
+  <div>
+    <div data-diagram="fogBlueBackgroundRed"></div>
+    <div class="code">fog blue, background red</div>
+  </div>
+  <div>
+    <div data-diagram="fogBlueBackgroundBlue"></div>
+    <div class="code">fog blue, background blue</div>
+  </div>
+</div>
+
+Here is one of our previous examples with fog added. The only addition
+is right after setting up the scene we add the fog and set the scene's
+backgound color
+
+```
+const scene = new THREE.Scene();
+
++{
++  const near = 1;
++  const far = 2;
++  const color = 'lightblue';
++  scene.fog = new THREE.Fog(color, near, far);
++  scene.background = new THREE.Color(color);
++}
+```
+
+In the example below the camera's `near` is 0.1 and its `far` is 5.
+The camera is at `z = 2`. The cubes are 1 unit large and at Z = 0.
+This means with a fog setting of `near = 1` and `far = 2` the cubes
+will fade out right around their center.
+
+{{{example url="../threejs-fog.html" }}}
+
+Let's add an interface so we can adjust the fog. Again we'll use 
+[dat.GUI](https://github.com/dataarts/dat.gui). dat.GUI takes
+an object and a property and automagically makes an interface
+for that type of property. We could just simply let it manipulate
+the fog's `near` and `far` properties but it's invalid to have
+`near` be greater than `far` so let's make a helper so dat.GUI
+can manipulate a `near` and `far` property but we'll make sure `near`
+is less than or equal to `far` and `far` is greater than or equal `near`.
+
+```
+// We use this class to pass to dat.gui
+// so when it manipulates near or far
+// near is never > far and far is never < near
+class FogGUIHelper {
+  constructor(fog) {
+    this.fog = fog;
+  }
+  get near() {
+    return this.fog.near;
+  }
+  set near(v) {
+    this.fog.near = v;
+    this.fog.far = Math.max(this.fog.far, v);
+  }
+  get far() {
+    return this.fog.far;
+  }
+  set far(v) {
+    this.fog.far = v;
+    this.fog.near = Math.min(this.fog.near, v);
+  }
+}
+```
+
+We can then add it like this
+
+```
+{
+  const near = 1;
+  const far = 2;
+  const color = 'lightblue';
+  scene.fog = new THREE.Fog(color, near, far);
+  scene.background = new THREE.Color(color);
++
++  const fogGUIHelper = new FogGUIHelper(scene.fog);
++  gui.add(fogGUIHelper, 'near', zNear, zFar).listen();
++  gui.add(fogGUIHelper, 'far', zNear, zFar).listen();
+}
+```
+
+The `zNear` and `zFar` parameter set the minimum and maximum values
+for adjusting the fog. They are set when we setup the camera.
+
+The `.listen()` at the end of the last 2 lines tells dat.GUI to *listen*
+for changes. That way when we change `near` because of an edit to `far`
+or we change `far` in response to an edit to `near` dat.GUI will update
+the other property's UI for us.
+
+It might also be nice to be able to change the fog color but like was
+mentioned above we need to keep both the fog color and the background
+color in sync. So, let's add another *virtual* property to our helper
+that will set both colors when dat.GUI manipulates it.
+
+dat.GUI can manipulate colors in 4 ways, as a CSS 6 digit hex string (eg: `#112233`). As an hue, saturation, value, object (eg: `{h: 60, s: 1, v: }`). 
+As an RGB array (eg: `[255, 128, 64]`). Or, as an RGBA array (eg: `[127, 200, 75, 0.3]`).
+
+It's easiest for our purpose to use the hex string version since that way
+dat.GUI is only manipulating a single value. Fortunately `THREE.Color`
+as a [`getHexString`](https://threejs.org/docs/#api/math/Color.getHexString) method
+we get use to easily get such a string, we just have to prepend a '#' to the front.
+
+```
+// We use this class to pass to dat.gui
+// so when it manipulates near or far
+// near is never > far and far is never < near
++// Also when dat.gui maniplates color we'll
++// update both the fog and background colors.
+class FogGUIHelper {
+*  constructor(fog, backgroundColor) {
+    this.fog = fog;
++    this.backgroundColor = backgroundColor;
+  }
+  get near() {
+    return this.fog.near;
+  }
+  set near(v) {
+    this.fog.near = v;
+    this.fog.far = Math.max(this.fog.far, v);
+  }
+  get far() {
+    return this.fog.far;
+  }
+  set far(v) {
+    this.fog.far = v;
+    this.fog.near = Math.min(this.fog.near, v);
+  }
++  get color() {
++    return `#${this.fog.color.getHexString()}`;
++  }
++  set color(hexString) {
++    this.fog.color.set(hexString);
++    this.backgroundColor.set(hexString);
++  }
+}
+```
+
+We then call `gui.addColor` to add a color UI for our helper's virutal property.
+
+```
+{
+  const near = 1;
+  const far = 2;
+  const color = 'lightblue';
+  scene.fog = new THREE.Fog(color, near, far);
+  scene.background = new THREE.Color(color);
+
+*  const fogGUIHelper = new FogGUIHelper(scene.fog, scene.background);
+  gui.add(fogGUIHelper, 'near', zNear, zFar).listen();
+  gui.add(fogGUIHelper, 'far', zNear, zFar).listen();
++  gui.addColor(fogGUIHelper, 'color');
+}
+```
+
+{{{example url="../threejs-fog-gui.html" }}}
+
+You can see setting `near` to like 1.9 and `far` to 2.0 gives
+a very sharp transition between unfogged and completely fogged.
+where as `near` = 1.1 and `far` = 2.9 should just about be
+the smoothest given our cubes are spinning 2 units away from the camera.
+
+One last thing, there is a boolean [`fog`](https://threejs.org/docs/#api/materials/Material.fog)
+property on a material for whether or not objects rendered
+with that material are affected by fog. It defaults to `true`
+for most materials. As an example of why you might want
+to turn the fog off, imagine you're making a 3D vehicle
+simulator with a view from the driver's seat or cockpit.
+You probably want the fog off for everything inside the vehicle when
+viewing from inside the vehicle.
+
+<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-fog.js"></script>

+ 1 - 0
threejs/lessons/toc.html

@@ -6,6 +6,7 @@
     <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-fog.html">Three.js Fog</a></li>
   </ul>
 </ul>
 <ul>

+ 157 - 0
threejs/threejs-fog-gui.html

@@ -0,0 +1,157 @@
+<!-- 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 - Fog w/GUI</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="../3rdparty/dat.gui.min.js"></script>
+<script src="resources/threejs/r94/three.min.js"></script>
+<script>
+'use strict';
+
+/* global dat */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+  const gui = new dat.GUI();
+
+  const fov = 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();
+
+  // We use this class to pass to dat.gui
+  // so when it manipulates near or far
+  // near is never > far and far is never < near
+  // Also when dat.gui maniplates color we'll
+  // update both the fog and background colors.
+  class FogGUIHelper {
+    constructor(fog, backgroundColor) {
+      this.fog = fog;
+      this.backgroundColor = backgroundColor;
+    }
+    get near() {
+      return this.fog.near;
+    }
+    set near(v) {
+      this.fog.near = v;
+      this.fog.far = Math.max(this.fog.far, v);
+    }
+    get far() {
+      return this.fog.far;
+    }
+    set far(v) {
+      this.fog.far = v;
+      this.fog.near = Math.min(this.fog.near, v);
+    }
+    get color() {
+      return `#${this.fog.color.getHexString()}`;
+    }
+    set color(hexString) {
+      this.fog.color.set(hexString);
+      this.backgroundColor.set(hexString);
+    }
+  }
+
+  {
+    const near = 1;
+    const far = 2;
+    const color = 'lightblue';
+    scene.fog = new THREE.Fog(color, near, far);
+    scene.background = new THREE.Color(color);
+
+    const fogGUIHelper = new FogGUIHelper(scene.fog, scene.background);
+    gui.add(fogGUIHelper, 'near', zNear, zFar).listen();
+    gui.add(fogGUIHelper, 'far', zNear, zFar).listen();
+    gui.addColor(fogGUIHelper, 'color');
+  }
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0),
+    makeInstance(geometry, 0x8844aa, -2),
+    makeInstance(geometry, 0xaa8844,  2),
+  ];
+
+  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 = 1 + 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>
+

+ 115 - 0
threejs/threejs-fog.html

@@ -0,0 +1,115 @@
+<!-- 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 - Fog</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 near = 1;
+    const far = 2;
+    const color = 'lightblue';
+    scene.fog = new THREE.Fog(color, near, far);
+    scene.background = new THREE.Color(color);
+  }
+
+  {
+    const color = 0xFFFFFF;
+    const intensity = 1;
+    const light = new THREE.DirectionalLight(color, intensity);
+    light.position.set(-1, 2, 4);
+    scene.add(light);
+  }
+
+  const boxWidth = 1;
+  const boxHeight = 1;
+  const boxDepth = 1;
+  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+
+  function makeInstance(geometry, color, x) {
+    const material = new THREE.MeshPhongMaterial({color});
+
+    const cube = new THREE.Mesh(geometry, material);
+    scene.add(cube);
+
+    cube.position.x = x;
+
+    return cube;
+  }
+
+  const cubes = [
+    makeInstance(geometry, 0x44aa88,  0),
+    makeInstance(geometry, 0x8844aa, -2),
+    makeInstance(geometry, 0xaa8844,  2),
+  ];
+
+  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 = 1 + 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>
+