Răsfoiți Sursa

add rendering on demand article

Gregg Tavares 6 ani în urmă
părinte
comite
0bcdf62113

+ 175 - 0
threejs/lessons/threejs-rendering-on-demand.md

@@ -0,0 +1,175 @@
+Title: Three.js Rendering on Demand
+Description: How to use less energy.
+
+The topic might be obvious to many people but just in case ... most Three.js examples render continuously. In other words they setup a `requestAnimationFrame` loop or "rAF loop" something like this
+
+```js
+function render() {
+  ...
+  requestAnimationFrame(render);
+}
+requestAnimationFrame(render);
+```
+
+For something that animates this makes sense but what about for something that does not animate? In that case rendering continuously is a waste of the devices power and if the user is on portable device it wastes the user's battery. 
+
+The most obvious way to solve this is to render once at the start and then render only when something changes. Changes include textures or models finally loading. Data arriving from some external source. The user adjusting a setting or the camera or giving other relevant input.
+
+Let's take an example from [the article on responsiveness](threejs-responsive.html)
+and modify it to render on demand.
+
+First we'll add in the `OrbitControls` so there is something that could change that we can render in response to.
+
+```html
+<script src="resources/threejs/r98/three.min.js"></script>
++<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+```
+
+and set them up
+
+```js
+const fov = 75;
+const aspect = 2;  // the canvas default
+const near = 0.1;
+const far = 5;
+const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+camera.position.z = 2;
+
++const controls = new THREE.OrbitControls(camera, canvas);
++controls.target.set(0, 0, 0);
++controls.update();
+```
+
+Since we won't be animating the cubes anymore we no longer need to keep track of them
+
+```js
+-const cubes = [
+-  makeInstance(geometry, 0x44aa88,  0),
+-  makeInstance(geometry, 0x8844aa, -2),
+-  makeInstance(geometry, 0xaa8844,  2),
+-];
++makeInstance(geometry, 0x44aa88,  0);
++makeInstance(geometry, 0x8844aa, -2);
++makeInstance(geometry, 0xaa8844,  2);
+```
+
+We can remove the code to animate the cubes and the calls to `requestAnimationFrame`
+
+```js
+-function render(time) {
+-  time *= 0.001;
++function render() {
+
+  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);
+```
+
+then we need to render once
+
+```js
+render();
+```
+
+We need to render anytime the `OrbitControls` change the camera settings. Fortunately the `OrbitControls` dispatch
+a `change` event anytime something changes.
+
+```js
+controls.addEventListener('change', render);
+```
+
+We also need to handle the case where the user resizes the window. That was handled automatically before since we were rendering continuously but now what we are not we need to render when the window changes size.
+
+```js
+window.addEventListener('resize', render);
+```
+
+And with that we get something that renders on demand.
+
+{{{example url="../threejs-render-on-demand.html" }}}
+
+The `OrbitControls` have options to add a kind of inertia to make them feel less stiff. We can enable this
+by setting the `enableDamping` property to true and we can set how much inertia by adjusting the `dampingFactor`
+
+```js
+controls.enableDamping = true;
+controls.dampingFactor = 0.1;
+```
+
+With `enableDamping` on we need to call `control.update` in our render function so that the `OrbitControls` can
+continue to give us new camera settings as they smooth out the movement. But that means we can't call `render`
+directly from the `change` event because we'll end up in an infinite loop. The controls will send us a `change` event
+and call `render`, `render` will call `control.update`. `control.update` will send another `change` event.
+
+We can fix that by using `requestAnimationFrame` to call `render` like this.
+
+```js
+-controls.addEventListener('change', render);
++controls.addEventListener('change', () => {
++  requestAnimationFrame(render);
++});
+```
+
+It might be hard to see the difference. Try clicking on the example below and use the arrow keys to move around.
+Then try clicking on the example above and do the same thing and you should be able to tell the difference.
+The one above snaps when you press an arrow key, the one below slides.
+
+{{{example url="../threejs-render-on-demand-w-damping.html" }}}
+
+Let's also add a simple dat.GUI GUI and make its changes render on demand.
+
+```html
+<script src="resources/threejs/r98/three.min.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
++<script src="../3rdparty/dat.gui.min.js"></script>
+```
+
+Let's allow setting the color and x scale of each cube. To be able to set the color we'll use the `ColorGUIHelper` we created in the [article on lights](threejs-lights.html).
+
+First we need to create a GUI
+
+```js
+const gui = new dat.GUI();
+```
+
+and then for each cube we'll create a folder and add 2 controls, one for `material.color` and another for `cube.scale.x`.
+
+```js
+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;
+
++  const folder = gui.addFolder(`Cube${x}`);
++  folder.addColor(new ColorGUIHelper(material, 'color'), 'value').name('color').onChange(render);
++  folder.add(cube.scale, 'x', .1, 1.5).name('scale x').onChange(render);
++  folder.open();
+
+  return cube;
+}
+```
+
+You can see above dat.GUI controls have an `onChange` method that you can pass a callback to be called when the GUI changes a value. In our case we just need it to call `render`. The call to `folder.open` makes the folder start expanded.
+
+{{{example url="../threejs-render-on-demand-w-gui.html" }}}
+
+I hope this gives some idea of how to make three.js render on demand instead of continuously. Apps/pages that render three.js on demand are not as common as most pages using three.js are either games or 3D animated art but examples of pages that might be better rendering on demand would be say a map viewer, a 3d editor, a 3d graph generator, a product catalog, etc...

+ 119 - 0
threejs/threejs-render-on-demand-w-damping.html

@@ -0,0 +1,119 @@
+<!-- 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 - Rendering on Demand w/Damping</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  {
+    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;
+  }
+
+  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() {
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', () => {
+    requestAnimationFrame(render);
+  });
+  window.addEventListener('resize', render);
+
+  // note: this is a workaround for an OrbitControls issue
+  // in an iframe. Will remove once the issue is fixed in
+  // three.js
+  window.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    window.focus();
+  });
+  window.addEventListener('keydown', (e) => {
+    e.preventDefault();
+  });
+}
+
+main();
+</script>
+</html>
+

+ 129 - 0
threejs/threejs-render-on-demand-w-gui.html

@@ -0,0 +1,129 @@
+<!-- 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 - Rendering on Demand w/GUI</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script src="../3rdparty/dat.gui.min.js"></script>
+<script>
+'use strict';
+
+/* global THREE, 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 near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.enableDamping = true;
+  controls.dampingFactor = 0.05;
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const gui = new dat.GUI();
+
+  const scene = new THREE.Scene();
+
+  {
+    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);
+
+  class ColorGUIHelper {
+    constructor(object, prop) {
+      this.object = object;
+      this.prop = prop;
+    }
+    get value() {
+      return `#${this.object[this.prop].getHexString()}`;
+    }
+    set value(hexString) {
+      this.object[this.prop].set(hexString);
+    }
+  }
+
+  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;
+
+    const folder = gui.addFolder(`Cube${x}`);
+    folder.addColor(new ColorGUIHelper(material, 'color'), 'value').name('color').onChange(render);
+    folder.add(cube.scale, 'x', .1, 1.5).name('scale x').onChange(render);
+    folder.open();
+
+    return cube;
+  }
+
+  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() {
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    controls.update();
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', () => {
+    requestAnimationFrame(render);
+  });
+  window.addEventListener('resize', render);
+}
+
+main();
+</script>
+</html>
+

+ 114 - 0
threejs/threejs-render-on-demand.html

@@ -0,0 +1,114 @@
+<!-- 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 - Rendering on Demand</title>
+    <style>
+    body {
+        margin: 0;
+    }
+    #c {
+        width: 100vw;
+        height: 100vh;
+        display: block;
+    }
+    </style>
+  </head>
+  <body>
+    <canvas id="c" tabindex="1"></canvas>
+  </body>
+<script src="resources/threejs/r98/three.min.js"></script>
+<script src="resources/threejs/r98/js/controls/OrbitControls.js"></script>
+<script>
+'use strict';
+
+/* global THREE */
+
+function main() {
+  const canvas = document.querySelector('#c');
+  const renderer = new THREE.WebGLRenderer({canvas: canvas});
+
+  const fov = 75;
+  const aspect = 2;  // the canvas default
+  const near = 0.1;
+  const far = 5;
+  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+  camera.position.z = 2;
+
+  const controls = new THREE.OrbitControls(camera, canvas);
+  controls.target.set(0, 0, 0);
+  controls.update();
+
+  const scene = new THREE.Scene();
+
+  {
+    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;
+  }
+
+  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() {
+    if (resizeRendererToDisplaySize(renderer)) {
+      const canvas = renderer.domElement;
+      camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      camera.updateProjectionMatrix();
+    }
+
+    renderer.render(scene, camera);
+  }
+  render();
+
+  controls.addEventListener('change', render);
+  window.addEventListener('resize', render);
+
+  // note: this is a workaround for an OrbitControls issue
+  // in an iframe. Will remove once the issue is fixed in
+  // three.js
+  window.addEventListener('mousedown', (e) => {
+    e.preventDefault();
+    window.focus();
+  });
+  window.addEventListener('keydown', (e) => {
+    e.preventDefault();
+  });
+}
+
+main();
+</script>
+</html>
+