Browse Source

Manual: Honor latest changes. (#26354)

Michael Herzog 2 years ago
parent
commit
3469043147
77 changed files with 14629 additions and 11795 deletions
  1. 1 35
      manual/en/lights.html
  2. 1 1
      manual/en/post-processing-3dlut.html
  3. 1 0
      manual/en/post-processing.html
  4. 189 142
      manual/examples/game-check-animations.html
  5. 1066 767
      manual/examples/game-conga-line-w-notes.html
  6. 844 613
      manual/examples/game-conga-line.html
  7. 348 241
      manual/examples/game-just-player.html
  8. 146 112
      manual/examples/game-load-models.html
  9. 557 396
      manual/examples/game-player-input.html
  10. 91 63
      manual/examples/gpw-data-viewer.html
  11. 398 324
      manual/examples/indexed-textures-picking-and-highlighting.html
  12. 427 346
      manual/examples/indexed-textures-picking-debounced.html
  13. 335 274
      manual/examples/indexed-textures-picking.html
  14. 371 301
      manual/examples/indexed-textures-random-colors.html
  15. 128 98
      manual/examples/lights-ambient.html
  16. 154 119
      manual/examples/lights-directional-w-helper.html
  17. 135 105
      manual/examples/lights-directional.html
  18. 131 101
      manual/examples/lights-hemisphere.html
  19. 0 171
      manual/examples/lights-point-physically-correct.html
  20. 147 113
      manual/examples/lights-point.html
  21. 175 135
      manual/examples/lights-rectarea.html
  22. 177 134
      manual/examples/lights-spot-w-helper.html
  23. 258 222
      manual/examples/load-gltf-animated-cars.html
  24. 233 199
      manual/examples/load-gltf-car-path-fixed.html
  25. 230 196
      manual/examples/load-gltf-car-path.html
  26. 167 138
      manual/examples/load-gltf-dump-scenegraph-extra.html
  27. 156 129
      manual/examples/load-gltf-dump-scenegraph.html
  28. 174 146
      manual/examples/load-gltf-rotate-cars-fixed.html
  29. 158 130
      manual/examples/load-gltf-rotate-cars.html
  30. 416 339
      manual/examples/load-gltf-shadows.html
  31. 142 118
      manual/examples/load-gltf.html
  32. 144 122
      manual/examples/load-obj-auto-camera-xz.html
  33. 136 114
      manual/examples/load-obj-auto-camera.html
  34. 111 89
      manual/examples/load-obj-materials-fixed.html
  35. 152 128
      manual/examples/load-obj-materials-windmill2.html
  36. 110 88
      manual/examples/load-obj-materials.html
  37. 103 83
      manual/examples/load-obj-no-materials.html
  38. 103 83
      manual/examples/load-obj-wat.html
  39. 416 314
      manual/examples/lots-of-objects-animated.html
  40. 222 176
      manual/examples/lots-of-objects-merged-vertexcolors.html
  41. 194 150
      manual/examples/lots-of-objects-merged.html
  42. 437 323
      manual/examples/lots-of-objects-morphtargets.html
  43. 342 260
      manual/examples/lots-of-objects-multiple-data-sets.html
  44. 192 148
      manual/examples/lots-of-objects-slow.html
  45. 143 113
      manual/examples/multiple-scenes-controls.html
  46. 150 120
      manual/examples/multiple-scenes-copy-canvas.html
  47. 123 95
      manual/examples/multiple-scenes-generic.html
  48. 133 103
      manual/examples/multiple-scenes-selector.html
  49. 109 87
      manual/examples/multiple-scenes-v1.html
  50. 112 90
      manual/examples/multiple-scenes-v2.html
  51. 114 92
      manual/examples/multiple-scenes-v3.html
  52. 46 31
      manual/examples/offscreencanvas-w-fallback.html
  53. 174 131
      manual/examples/offscreencanvas-w-orbitcontrols.html
  54. 115 84
      manual/examples/offscreencanvas-w-picking.html
  55. 29 21
      manual/examples/offscreencanvas.html
  56. 242 198
      manual/examples/picking-gpu.html
  57. 171 127
      manual/examples/picking-raycaster-complex-geo.html
  58. 190 146
      manual/examples/picking-raycaster-transparency.html
  59. 182 137
      manual/examples/picking-raycaster.html
  60. 245 216
      manual/examples/postprocessing-3dlut-identity.html
  61. 144 121
      manual/examples/postprocessing-3dlut-prep.html
  62. 363 313
      manual/examples/postprocessing-3dlut-w-loader.html
  63. 326 281
      manual/examples/postprocessing-3dlut.html
  64. 168 135
      manual/examples/postprocessing-adobe-lut-to-png-converter.html
  65. 111 93
      manual/examples/postprocessing-custom.html
  66. 133 107
      manual/examples/postprocessing-gui.html
  67. 113 92
      manual/examples/postprocessing.html
  68. 146 112
      manual/examples/primitives-text.html
  69. 421 307
      manual/examples/primitives.html
  70. 1 27
      manual/fr/lights.html
  71. 1 34
      manual/ja/lights.html
  72. 1 0
      manual/ja/post-processing.html
  73. 1 36
      manual/ko/lights.html
  74. 1 0
      manual/ko/post-processing.html
  75. 1 34
      manual/ru/lights.html
  76. 1 26
      manual/zh/lights.html
  77. 1 0
      manual/zh/post-processing.html

+ 1 - 35
manual/en/lights.html

@@ -84,6 +84,7 @@ const texture = loader.load('resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -470,41 +471,6 @@ makeXYZGUI(gui, light.position, 'position');
   <a class="threejs_center" href="/manual/examples/lights-rectarea.html" target="_blank">click here to open in a separate window</a>
   <a class="threejs_center" href="/manual/examples/lights-rectarea.html" target="_blank">click here to open in a separate window</a>
 </div>
 </div>
 
 
-<p></p>
-<p>One thing we didn't cover is that there is a setting on the <a href="/docs/#api/en/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a>
-called <code class="notranslate" translate="no">physicallyCorrectLights</code>. It effects how light falls off as distance from light.
-It only affects <a href="/docs/#api/en/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a> and <a href="/docs/#api/en/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>. <a href="/docs/#api/en/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a> does this automatically.</p>
-<p>For lights though the basic idea is you don't set a distance for them to fade out,
-and you don't set <code class="notranslate" translate="no">intensity</code>. Instead you set the <a href="/docs/#api/en/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a> of
-the light in lumens and then three.js will use physics calculations like real lights.
-The units of three.js in this case are meters and a 60w light bulb would have
-around 800 lumens. There's also a <a href="/docs/#api/en/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a> property. It should
-be set to <code class="notranslate" translate="no">2</code> for realistic decay.</p>
-<p>Let's test that.</p>
-<p>First we'll turn on physically correct lights</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>Then we'll set the <code class="notranslate" translate="no">power</code> to 800 lumens, the <code class="notranslate" translate="no">decay</code> to 2, and
-the <code class="notranslate" translate="no">distance</code> to <code class="notranslate" translate="no">Infinity</code>.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>and we'll add gui so we can change the <code class="notranslate" translate="no">power</code> and <code class="notranslate" translate="no">decay</code></p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">click here to open in a separate window</a>
-</div>
-
 <p></p>
 <p></p>
 <p>It's important to note each light you add to the scene slows down how fast
 <p>It's important to note each light you add to the scene slows down how fast
 three.js renders the scene so you should always try to use as few as
 three.js renders the scene so you should always try to use as few as

+ 1 - 1
manual/en/post-processing-3dlut.html

@@ -180,7 +180,7 @@ composer.addPass(renderBG);
 composer.addPass(renderModel);
 composer.addPass(renderModel);
 composer.addPass(effectLUT);
 composer.addPass(effectLUT);
 composer.addPass(effectLUTNearest);
 composer.addPass(effectLUTNearest);
-composer.addPass(gammaPass);
+composer.addPass(outputPass);
 </pre>
 </pre>
 <p>Let's make some GUI code to select one lut or the other</p>
 <p>Let's make some GUI code to select one lut or the other</p>
 <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutNameIndexMap = {};
 <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutNameIndexMap = {};

+ 1 - 0
manual/en/post-processing.html

@@ -107,6 +107,7 @@ render to the next render target.</p>
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
+import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
 </pre>
 </pre>
 <p>For pretty much any post processing <code class="notranslate" translate="no">EffectComposer.js</code>, and <code class="notranslate" translate="no">RenderPass.js</code>
 <p>For pretty much any post processing <code class="notranslate" translate="no">EffectComposer.js</code>, and <code class="notranslate" translate="no">RenderPass.js</code>
 are required.</p>
 are required.</p>

+ 189 - 142
manual/examples/game-check-animations.html

@@ -88,161 +88,208 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 20, 40);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    Object.values(models).forEach(model => {
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  const mixerInfos = [];
-
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
-
-    prepModelsAndAnimations();
-
-    Object.values(models).forEach((model, ndx) => {
-      const clonedScene = SkeletonUtils.clone(model.gltf.scene);
-      const root = new THREE.Object3D();
-      root.add(clonedScene);
-      scene.add(root);
-      root.position.x = (ndx - 3) * 3;
-
-      const mixer = new THREE.AnimationMixer(clonedScene);
-      const actions = Object.values(model.animations).map((clip) => {
-        return mixer.clipAction(clip);
-      });
-      const mixerInfo = {
-        mixer,
-        actions,
-        actionNdx: -1,
-      };
-      mixerInfos.push(mixerInfo);
-      playNextAction(mixerInfo);
-    });
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 20, 40 );
 
 
-  function playNextAction(mixerInfo) {
-    const {actions, actionNdx} = mixerInfo;
-    const nextActionNdx = (actionNdx + 1) % actions.length;
-    mixerInfo.actionNdx = nextActionNdx;
-    actions.forEach((action, ndx) => {
-      const enabled = ndx === nextActionNdx;
-      action.enabled = enabled;
-      if (enabled) {
-        action.play();
-      }
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  window.addEventListener('keydown', (e) => {
-    const mixerInfo = mixerInfos[e.keyCode - 49];
-    if (!mixerInfo) {
-      return;
-    }
-    playNextAction(mixerInfo);
-  });
-
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to sections
-    const deltaTime = now - then;
-    then = now;
+	function addLight( ...pos ) {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
 
 
-    for (const {mixer} of mixerInfos) {
-      mixer.update(deltaTime);
-    }
+	}
 
 
-    renderer.render(scene, camera);
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
 
 
-    requestAnimationFrame(render);
-  }
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
+
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
+
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
+
+	};
+
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
+
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
+
+			gltfLoader.load( model.url, ( gltf ) => {
+
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		Object.values( models ).forEach( model => {
+
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	const mixerInfos = [];
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		Object.values( models ).forEach( ( model, ndx ) => {
+
+			const clonedScene = SkeletonUtils.clone( model.gltf.scene );
+			const root = new THREE.Object3D();
+			root.add( clonedScene );
+			scene.add( root );
+			root.position.x = ( ndx - 3 ) * 3;
+
+			const mixer = new THREE.AnimationMixer( clonedScene );
+			const actions = Object.values( model.animations ).map( ( clip ) => {
+
+				return mixer.clipAction( clip );
+
+			} );
+			const mixerInfo = {
+				mixer,
+				actions,
+				actionNdx: - 1,
+			};
+			mixerInfos.push( mixerInfo );
+			playNextAction( mixerInfo );
+
+		} );
+
+	}
+
+	function playNextAction( mixerInfo ) {
+
+		const { actions, actionNdx } = mixerInfo;
+		const nextActionNdx = ( actionNdx + 1 ) % actions.length;
+		mixerInfo.actionNdx = nextActionNdx;
+		actions.forEach( ( action, ndx ) => {
+
+			const enabled = ndx === nextActionNdx;
+			action.enabled = enabled;
+			if ( enabled ) {
+
+				action.play();
+
+			}
+
+		} );
+
+	}
+
+	window.addEventListener( 'keydown', ( e ) => {
+
+		const mixerInfo = mixerInfos[ e.keyCode - 49 ];
+		if ( ! mixerInfo ) {
+
+			return;
+
+		}
+
+		playNextAction( mixerInfo );
+
+	} );
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to sections
+		const deltaTime = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		for ( const { mixer } of mixerInfos ) {
+
+			mixer.update( deltaTime );
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 1066 - 767
manual/examples/game-conga-line-w-notes.html

@@ -163,816 +163,1115 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 40, 80);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    const box = new THREE.Box3();
-    const size = new THREE.Vector3();
-    Object.values(models).forEach(model => {
-      box.setFromObject(model.gltf.scene);
-      box.getSize(size);
-      model.size = size.length();
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-        // Should really fix this in .blend file
-        if (clip.name === 'Walk') {
-          clip.duration /= 2;
-        }
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  // Keeps the state of keys/buttons
-  //
-  // You can check
-  //
-  //   inputManager.keys.left.down
-  //
-  // to see if the left key is currently held down
-  // and you can check
-  //
-  //   inputManager.keys.left.justPressed
-  //
-  // To see if the left key was pressed this frame
-  //
-  // Keys are 'left', 'right', 'a', 'b', 'up', 'down'
-  class InputManager {
-    constructor() {
-      this.keys = {};
-      const keyMap = new Map();
-
-      const setKey = (keyName, pressed) => {
-        const keyState = this.keys[keyName];
-        keyState.justPressed = pressed && !keyState.down;
-        keyState.down = pressed;
-      };
-
-      const addKey = (keyCode, name) => {
-        this.keys[name] = { down: false, justPressed: false };
-        keyMap.set(keyCode, name);
-      };
-
-      const setKeyFromKeyCode = (keyCode, pressed) => {
-        const keyName = keyMap.get(keyCode);
-        if (!keyName) {
-          return;
-        }
-        setKey(keyName, pressed);
-      };
-
-      addKey(37, 'left');
-      addKey(39, 'right');
-      addKey(38, 'up');
-      addKey(40, 'down');
-      addKey(90, 'a');
-      addKey(88, 'b');
-
-      window.addEventListener('keydown', (e) => {
-        setKeyFromKeyCode(e.keyCode, true);
-      });
-      window.addEventListener('keyup', (e) => {
-        setKeyFromKeyCode(e.keyCode, false);
-      });
-
-      const sides = [
-        { elem: document.querySelector('#left'),  key: 'left'  },
-        { elem: document.querySelector('#right'), key: 'right' },
-      ];
-
-      // note: not a good design?
-      // The last direction the user presses should take
-      // precedence. Example: User presses L, without letting go of
-      // L user presses R. Input should now be R. User lets off R
-      // Input should now be L.
-      // With this code if user pressed both L and R result is nothing
-
-      const clearKeys = () => {
-        for (const {key} of sides) {
-            setKey(key, false);
-        }
-      };
-
-      const handleMouseMove = (e) => {
-        e.preventDefault();
-        // this is needed because we call preventDefault();
-        // we also gave the canvas a tabindex so it can
-        // become the focus
-        canvas.focus();
-        window.addEventListener('pointermove', handleMouseMove);
-        window.addEventListener('pointerup', handleMouseUp);
-
-        for (const {elem, key} of sides) {
-          let pressed = false;
-          const rect = elem.getBoundingClientRect();
-          const x = e.clientX;
-          const y = e.clientY;
-          const inRect = x >= rect.left && x < rect.right &&
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 40, 80 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
+
+	function addLight( ...pos ) {
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
+
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
+
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
+
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
+
+	};
+
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
+
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
+
+			gltfLoader.load( model.url, ( gltf ) => {
+
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		const box = new THREE.Box3();
+		const size = new THREE.Vector3();
+		Object.values( models ).forEach( model => {
+
+			box.setFromObject( model.gltf.scene );
+			box.getSize( size );
+			model.size = size.length();
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+				// Should really fix this in .blend file
+				if ( clip.name === 'Walk' ) {
+
+					clip.duration /= 2;
+
+				}
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	// Keeps the state of keys/buttons
+	//
+	// You can check
+	//
+	//   inputManager.keys.left.down
+	//
+	// to see if the left key is currently held down
+	// and you can check
+	//
+	//   inputManager.keys.left.justPressed
+	//
+	// To see if the left key was pressed this frame
+	//
+	// Keys are 'left', 'right', 'a', 'b', 'up', 'down'
+	class InputManager {
+
+		constructor() {
+
+			this.keys = {};
+			const keyMap = new Map();
+
+			const setKey = ( keyName, pressed ) => {
+
+				const keyState = this.keys[ keyName ];
+				keyState.justPressed = pressed && ! keyState.down;
+				keyState.down = pressed;
+
+			};
+
+			const addKey = ( keyCode, name ) => {
+
+				this.keys[ name ] = { down: false, justPressed: false };
+				keyMap.set( keyCode, name );
+
+			};
+
+			const setKeyFromKeyCode = ( keyCode, pressed ) => {
+
+				const keyName = keyMap.get( keyCode );
+				if ( ! keyName ) {
+
+					return;
+
+				}
+
+				setKey( keyName, pressed );
+
+			};
+
+			addKey( 37, 'left' );
+			addKey( 39, 'right' );
+			addKey( 38, 'up' );
+			addKey( 40, 'down' );
+			addKey( 90, 'a' );
+			addKey( 88, 'b' );
+
+			window.addEventListener( 'keydown', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, true );
+
+			} );
+			window.addEventListener( 'keyup', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, false );
+
+			} );
+
+			const sides = [
+				{ elem: document.querySelector( '#left' ), key: 'left' },
+				{ elem: document.querySelector( '#right' ), key: 'right' },
+			];
+
+			// note: not a good design?
+			// The last direction the user presses should take
+			// precedence. Example: User presses L, without letting go of
+			// L user presses R. Input should now be R. User lets off R
+			// Input should now be L.
+			// With this code if user pressed both L and R result is nothing
+
+			const clearKeys = () => {
+
+				for ( const { key } of sides ) {
+
+					setKey( key, false );
+
+				}
+
+			};
+
+			const handleMouseMove = ( e ) => {
+
+				e.preventDefault();
+				// this is needed because we call preventDefault();
+				// we also gave the canvas a tabindex so it can
+				// become the focus
+				canvas.focus();
+				window.addEventListener( 'pointermove', handleMouseMove );
+				window.addEventListener( 'pointerup', handleMouseUp );
+
+				for ( const { elem, key } of sides ) {
+
+					let pressed = false;
+					const rect = elem.getBoundingClientRect();
+					const x = e.clientX;
+					const y = e.clientY;
+					const inRect = x >= rect.left && x < rect.right &&
                          y >= rect.top && y < rect.bottom;
                          y >= rect.top && y < rect.bottom;
-          if (inRect) {
-            pressed = true;
-          }
-          setKey(key, pressed);
-        }
-      };
+					if ( inRect ) {
 
 
-      function handleMouseUp() {
-        clearKeys();
-        window.removeEventListener('pointermove', handleMouseMove, {passive: false});
-        window.removeEventListener('pointerup', handleMouseUp);
-      }
+						pressed = true;
 
 
-      const uiElem = document.querySelector('#ui');
-      uiElem.addEventListener('pointerdown', handleMouseMove, {passive: false});
+					}
 
 
-      uiElem.addEventListener('touchstart', (e) => {
-        // prevent scrolling
-        e.preventDefault();
-      }, {passive: false});
-    }
-    update() {
-      for (const keyState of Object.values(this.keys)) {
-        if (keyState.justPressed) {
-          keyState.justPressed = false;
-        }
-      }
-    }
-  }
+					setKey( key, pressed );
 
 
-  // function* waitFrames(numFrames) {
-  //   while (numFrames > 0) {
-  //     --numFrames;
-  //     yield;
-  //   }
-  // }
-
-  function* waitSeconds(duration) {
-    while (duration > 0) {
-      duration -= globals.deltaTime;
-      yield;
-    }
-  }
+				}
 
 
-  class CoroutineRunner {
-    constructor() {
-      this.generatorStacks = [];
-      this.addQueue = [];
-      this.removeQueue = new Set();
-    }
-    isBusy() {
-      return this.addQueue.length + this.generatorStacks.length > 0;
-    }
-    add(generator, delay = 0) {
-      const genStack = [generator];
-      if (delay) {
-        genStack.push(waitSeconds(delay));
-      }
-      this.addQueue.push(genStack);
-    }
-    remove(generator) {
-      this.removeQueue.add(generator);
-    }
-    update() {
-      this._addQueued();
-      this._removeQueued();
-      for (const genStack of this.generatorStacks) {
-        const main = genStack[0];
-        // Handle if one coroutine removes another
-        if (this.removeQueue.has(main)) {
-          continue;
-        }
-        while (genStack.length) {
-          const topGen = genStack[genStack.length - 1];
-          const {value, done} = topGen.next();
-          if (done) {
-            if (genStack.length === 1) {
-              this.removeQueue.add(topGen);
-              break;
-            }
-            genStack.pop();
-          } else if (value) {
-            genStack.push(value);
-          } else {
-            break;
-          }
-        }
-      }
-      this._removeQueued();
-    }
-    _addQueued() {
-      if (this.addQueue.length) {
-        this.generatorStacks.splice(this.generatorStacks.length, 0, ...this.addQueue);
-        this.addQueue = [];
-      }
-    }
-    _removeQueued() {
-      if (this.removeQueue.size) {
-        this.generatorStacks = this.generatorStacks.filter(genStack => !this.removeQueue.has(genStack[0]));
-        this.removeQueue.clear();
-      }
-    }
-  }
+			};
 
 
-  function removeArrayElement(array, element) {
-    const ndx = array.indexOf(element);
-    if (ndx >= 0) {
-      array.splice(ndx, 1);
-    }
-  }
+			function handleMouseUp() {
 
 
-  class SafeArray {
-    constructor() {
-      this.array = [];
-      this.addQueue = [];
-      this.removeQueue = new Set();
-    }
-    get isEmpty() {
-      return this.addQueue.length + this.array.length > 0;
-    }
-    add(element) {
-      this.addQueue.push(element);
-    }
-    remove(element) {
-      this.removeQueue.add(element);
-    }
-    forEach(fn) {
-      this._addQueued();
-      this._removeQueued();
-      for (const element of this.array) {
-        if (this.removeQueue.has(element)) {
-          continue;
-        }
-        fn(element);
-      }
-      this._removeQueued();
-    }
-    _addQueued() {
-      if (this.addQueue.length) {
-        this.array.splice(this.array.length, 0, ...this.addQueue);
-        this.addQueue = [];
-      }
-    }
-    _removeQueued() {
-      if (this.removeQueue.size) {
-        this.array = this.array.filter(element => !this.removeQueue.has(element));
-        this.removeQueue.clear();
-      }
-    }
-  }
+				clearKeys();
+				window.removeEventListener( 'pointermove', handleMouseMove, { passive: false } );
+				window.removeEventListener( 'pointerup', handleMouseUp );
 
 
-  class GameObjectManager {
-    constructor() {
-      this.gameObjects = new SafeArray();
-    }
-    createGameObject(parent, name) {
-      const gameObject = new GameObject(parent, name);
-      this.gameObjects.add(gameObject);
-      return gameObject;
-    }
-    removeGameObject(gameObject) {
-      this.gameObjects.remove(gameObject);
-    }
-    update() {
-      this.gameObjects.forEach(gameObject => gameObject.update());
-    }
-  }
+			}
 
 
-  const kForward = new THREE.Vector3(0, 0, 1);
-  const globals = {
-    camera,
-    canvas,
-    debug: false,
-    time: 0,
-    moveSpeed: 16,
-    deltaTime: 0,
-    player: null,
-    congaLine: [],
-  };
-  const gameObjectManager = new GameObjectManager();
-  const inputManager = new InputManager();
-
-  class GameObject {
-    constructor(parent, name) {
-      this.name = name;
-      this.components = [];
-      this.transform = new THREE.Object3D();
-      this.transform.name = name;
-      parent.add(this.transform);
-    }
-    addComponent(ComponentType, ...args) {
-      const component = new ComponentType(this, ...args);
-      this.components.push(component);
-      return component;
-    }
-    removeComponent(component) {
-      removeArrayElement(this.components, component);
-    }
-    getComponent(ComponentType) {
-      return this.components.find(c => c instanceof ComponentType);
-    }
-    update() {
-      for (const component of this.components) {
-        component.update();
-      }
-    }
-  }
+			const uiElem = document.querySelector( '#ui' );
+			uiElem.addEventListener( 'pointerdown', handleMouseMove, { passive: false } );
 
 
-  // Base for all components
-  class Component {
-    constructor(gameObject) {
-      this.gameObject = gameObject;
-    }
-    update() {
-    }
-  }
+			uiElem.addEventListener( 'touchstart', ( e ) => {
 
 
-  class CameraInfo extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      this.projScreenMatrix = new THREE.Matrix4();
-      this.frustum = new THREE.Frustum();
-    }
-    update() {
-      const {camera} = globals;
-      this.projScreenMatrix.multiplyMatrices(
-          camera.projectionMatrix,
-          camera.matrixWorldInverse);
-      this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
-    }
-  }
+				// prevent scrolling
+				e.preventDefault();
 
 
-  class SkinInstance extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.model = model;
-      this.animRoot = SkeletonUtils.clone(this.model.gltf.scene);
-      this.mixer = new THREE.AnimationMixer(this.animRoot);
-      gameObject.transform.add(this.animRoot);
-      this.actions = {};
-    }
-    setAnimation(animName) {
-      const clip = this.model.animations[animName];
-      // turn off all current actions
-      for (const action of Object.values(this.actions)) {
-        action.enabled = false;
-      }
-      // get or create existing action for clip
-      const action = this.mixer.clipAction(clip);
-      action.enabled = true;
-      action.reset();
-      action.play();
-      this.actions[animName] = action;
-    }
-    update() {
-      this.mixer.update(globals.deltaTime);
-    }
-  }
+			}, { passive: false } );
 
 
-  class FiniteStateMachine {
-    constructor(states, initialState) {
-      this.states = states;
-      this.transition(initialState);
-    }
-    get state() {
-      return this.currentState;
-    }
-    transition(state) {
-      const oldState = this.states[this.currentState];
-      if (oldState && oldState.exit) {
-        oldState.exit.call(this);
-      }
-      this.currentState = state;
-      const newState = this.states[state];
-      if (newState.enter) {
-        newState.enter.call(this);
-      }
-    }
-    update() {
-      const state = this.states[this.currentState];
-      if (state.update) {
-        state.update.call(this);
-      }
-    }
-  }
+		}
+		update() {
 
 
-  const gui = new GUI();
-  gui.add(globals, 'debug').onChange(showHideDebugInfo);
-  gui.close();
+			for ( const keyState of Object.values( this.keys ) ) {
 
 
-  const labelContainerElem = document.querySelector('#labels');
-  function showHideDebugInfo() {
-    labelContainerElem.style.display = globals.debug ? '' : 'none';
-  }
-  showHideDebugInfo();
+				if ( keyState.justPressed ) {
 
 
-  class StateDisplayHelper extends Component {
-    constructor(gameObject, size) {
-      super(gameObject);
-      this.elem = document.createElement('div');
-      labelContainerElem.appendChild(this.elem);
-      this.pos = new THREE.Vector3();
+					keyState.justPressed = false;
 
 
-      this.helper = new THREE.PolarGridHelper(size / 2, 1, 1, 16);
-      gameObject.transform.add(this.helper);
-    }
-    setState(s) {
-      this.elem.textContent = s;
-    }
-    setColor(cssColor) {
-      this.elem.style.color = cssColor;
-      this.helper.material.color.set(cssColor);
-    }
-    update() {
-      this.helper.visible = globals.debug;
-      if (!globals.debug) {
-        return;
-      }
-      const {pos} = this;
-      const {transform} = this.gameObject;
-      const {canvas} = globals;
-      pos.copy(transform.position);
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      pos.project(globals.camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (pos.x *  .5 + .5) * canvas.clientWidth;
-      const y = (pos.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-    }
-  }
+				}
 
 
-  function rand(min, max) {
-    if (max === undefined) {
-      max = min;
-      min = 0;
-    }
-    return Math.random() * (max - min) + min;
-  }
+			}
 
 
-  function makeTextTexture(str) {
-    const ctx = document.createElement('canvas').getContext('2d');
-    ctx.canvas.width = 64;
-    ctx.canvas.height = 64;
-    ctx.font = '60px sans-serif';
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillStyle = '#FFF';
-    ctx.fillText(str, ctx.canvas.width / 2, ctx.canvas.height / 2);
-    return new THREE.CanvasTexture(ctx.canvas);
-  }
-  const noteTexture = makeTextTexture('♪');
-
-  class Note extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      const {transform} = gameObject;
-      const noteMaterial = new THREE.SpriteMaterial({
-        color: new THREE.Color().setHSL(rand(1), 1, 0.5),
-        map: noteTexture,
-        side: THREE.DoubleSide,
-        transparent: true,
-      });
-      const note = new THREE.Sprite(noteMaterial);
-      note.scale.setScalar(3);
-      transform.add(note);
-      this.runner = new CoroutineRunner();
-      const direction = new THREE.Vector3(rand(-0.2, 0.2), 1, rand(-0.2, 0.2));
-
-      function* moveAndRemove() {
-        for (let i = 0; i < 60; ++i) {
-          transform.translateOnAxis(direction, globals.deltaTime * 10);
-          noteMaterial.opacity = 1 - (i / 60);
-          yield;
-        }
-        transform.parent.remove(transform);
-        gameObjectManager.removeGameObject(gameObject);
-      }
+		}
 
 
-      this.runner.add(moveAndRemove());
-    }
-    update() {
-      this.runner.update();
-    }
-  }
+	}
 
 
-  class Player extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      const model = models.knight;
-      globals.playerRadius = model.size / 2;
-      this.text = gameObject.addComponent(StateDisplayHelper, model.size);
-      this.skinInstance = gameObject.addComponent(SkinInstance, model);
-      this.skinInstance.setAnimation('Run');
-      this.turnSpeed = globals.moveSpeed / 4;
-      this.offscreenTimer = 0;
-      this.maxTimeOffScreen = 3;
-      this.runner = new CoroutineRunner();
-
-      function* emitNotes() {
-        for (;;) {
-          yield waitSeconds(rand(0.5, 1));
-          const noteGO = gameObjectManager.createGameObject(scene, 'note');
-          noteGO.transform.position.copy(gameObject.transform.position);
-          noteGO.transform.position.y += 5;
-          noteGO.addComponent(Note);
-        }
-      }
+	// function* waitFrames(numFrames) {
+	//   while (numFrames > 0) {
+	//     --numFrames;
+	//     yield;
+	//   }
+	// }
 
 
-      this.runner.add(emitNotes());
-    }
-    update() {
-      this.runner.update();
-      const {deltaTime, moveSpeed, cameraInfo} = globals;
-      const {transform} = this.gameObject;
-      const delta = (inputManager.keys.left.down  ?  1 : 0) +
-                    (inputManager.keys.right.down ? -1 : 0);
-      transform.rotation.y += this.turnSpeed * delta * deltaTime;
-      transform.translateOnAxis(kForward, moveSpeed * deltaTime);
-
-      const {frustum} = cameraInfo;
-      if (frustum.containsPoint(transform.position)) {
-        this.offscreenTimer = 0;
-      } else {
-        this.offscreenTimer += deltaTime;
-        if (this.offscreenTimer >= this.maxTimeOffScreen) {
-          transform.position.set(0, 0, 0);
-        }
-      }
-    }
-  }
+	function* waitSeconds( duration ) {
 
 
-  // Returns true of obj1 and obj2 are close
-  function isClose(obj1, obj1Radius, obj2, obj2Radius) {
-    const minDist = obj1Radius + obj2Radius;
-    const dist = obj1.position.distanceTo(obj2.position);
-    return dist < minDist;
-  }
+		while ( duration > 0 ) {
 
 
-  // keeps v between -min and +min
-  function minMagnitude(v, min) {
-    return Math.abs(v) > min
-        ? min * Math.sign(v)
-        : v;
-  }
+			duration -= globals.deltaTime;
+			yield;
 
 
-  const aimTowardAndGetDistance = function() {
-    const delta = new THREE.Vector3();
-
-    return function aimTowardAndGetDistance(source, targetPos, maxTurn) {
-      delta.subVectors(targetPos, source.position);
-      // compute the direction we want to be facing
-      const targetRot = Math.atan2(delta.x, delta.z) + Math.PI * 1.5;
-      // rotate in the shortest direction
-      const deltaRot = (targetRot - source.rotation.y + Math.PI * 1.5) % (Math.PI * 2) - Math.PI;
-      // make sure we don't turn faster than maxTurn
-      const deltaRotation = minMagnitude(deltaRot, maxTurn);
-      // keep rotation between 0 and Math.PI * 2
-      source.rotation.y = THREE.MathUtils.euclideanModulo(
-          source.rotation.y + deltaRotation, Math.PI * 2);
-      // return the distance to the target
-      return delta.length();
-    };
-  }();
-
-  class Animal extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.helper = gameObject.addComponent(StateDisplayHelper, model.size);
-      const hitRadius = model.size / 2;
-      const skinInstance = gameObject.addComponent(SkinInstance, model);
-      skinInstance.mixer.timeScale = globals.moveSpeed / 4;
-      const transform = gameObject.transform;
-      const playerTransform = globals.player.gameObject.transform;
-      const maxTurnSpeed = Math.PI * (globals.moveSpeed / 4);
-      const targetHistory = [];
-      let targetNdx = 0;
-
-      function addHistory() {
-        const targetGO = globals.congaLine[targetNdx];
-        const newTargetPos = new THREE.Vector3();
-        newTargetPos.copy(targetGO.transform.position);
-        targetHistory.push(newTargetPos);
-      }
+		}
 
 
-      this.fsm = new FiniteStateMachine({
-        idle: {
-          enter: () => {
-            skinInstance.setAnimation('Idle');
-          },
-          update: () => {
-            // check if player is near
-            if (isClose(transform, hitRadius, playerTransform, globals.playerRadius)) {
-              this.fsm.transition('waitForEnd');
-            }
-          },
-        },
-        waitForEnd: {
-          enter: () => {
-            skinInstance.setAnimation('Jump');
-          },
-          update: () => {
-            // get the gameObject at the end of the conga line
-            const lastGO = globals.congaLine[globals.congaLine.length - 1];
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            const targetPos = lastGO.transform.position;
-            aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
-            // check if last thing in conga line is near
-            if (isClose(transform, hitRadius, lastGO.transform, globals.playerRadius)) {
-              this.fsm.transition('goToLast');
-            }
-          },
-        },
-        goToLast: {
-          enter: () => {
-            // remember who we're following
-            targetNdx = globals.congaLine.length - 1;
-            // add ourselves to the conga line
-            globals.congaLine.push(gameObject);
-            skinInstance.setAnimation('Walk');
-          },
-          update: () => {
-            addHistory();
-            // walk to the oldest point in the history
-            const targetPos = targetHistory[0];
-            const maxVelocity = globals.moveSpeed * globals.deltaTime;
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            const distance = aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
-            const velocity = distance;
-            transform.translateOnAxis(kForward, Math.min(velocity, maxVelocity));
-            if (distance <= maxVelocity) {
-              this.fsm.transition('follow');
-            }
-          },
-        },
-        follow: {
-          update: () => {
-            addHistory();
-            // remove the oldest history and just put ourselves there.
-            const targetPos = targetHistory.shift();
-            transform.position.copy(targetPos);
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            aimTowardAndGetDistance(transform, targetHistory[0], deltaTurnSpeed);
-          },
-        },
-      }, 'idle');
-    }
-    update() {
-      this.fsm.update();
-      const dir = THREE.MathUtils.radToDeg(this.gameObject.transform.rotation.y);
-      this.helper.setState(`${this.fsm.state}:${dir.toFixed(0)}`);
-    }
-  }
+	}
 
 
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
+	class CoroutineRunner {
 
 
-    prepModelsAndAnimations();
+		constructor() {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(camera, 'camera');
-      globals.cameraInfo = gameObject.addComponent(CameraInfo);
-    }
+			this.generatorStacks = [];
+			this.addQueue = [];
+			this.removeQueue = new Set();
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(scene, 'player');
-      globals.player = gameObject.addComponent(Player);
-      globals.congaLine = [gameObject];
-    }
+		}
+		isBusy() {
 
 
-    const animalModelNames = [
-      'pig',
-      'cow',
-      'llama',
-      'pug',
-      'sheep',
-      'zebra',
-      'horse',
-    ];
-    const base = new THREE.Object3D();
-    const offset = new THREE.Object3D();
-    base.add(offset);
-
-    // position animals in a spiral.
-    const numAnimals = 28;
-    const arc = 10;
-    const b = 10 / (2 * Math.PI);
-    let r = 10;
-    let phi = r / b;
-    for (let i = 0; i < numAnimals; ++i) {
-      const name = animalModelNames[rand(animalModelNames.length) | 0];
-      const gameObject = gameObjectManager.createGameObject(scene, name);
-      gameObject.addComponent(Animal, models[name]);
-      base.rotation.y = phi;
-      offset.position.x = r;
-      offset.updateWorldMatrix(true, false);
-      offset.getWorldPosition(gameObject.transform.position);
-      phi += arc / r;
-      r = b * phi;
-    }
-  }
+			return this.addQueue.length + this.generatorStacks.length > 0;
 
 
-  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;
-  }
+		}
+		add( generator, delay = 0 ) {
 
 
-  let then = 0;
-  function render(now) {
-    // convert to seconds
-    globals.time = now * 0.001;
-    // make sure delta time isn't too big.
-    globals.deltaTime = Math.min(globals.time - then, 1 / 20);
-    then = globals.time;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			const genStack = [ generator ];
+			if ( delay ) {
 
 
-    gameObjectManager.update();
-    inputManager.update();
+				genStack.push( waitSeconds( delay ) );
 
 
-    renderer.render(scene, camera);
+			}
 
 
-    requestAnimationFrame(render);
-  }
+			this.addQueue.push( genStack );
+
+		}
+		remove( generator ) {
+
+			this.removeQueue.add( generator );
+
+		}
+		update() {
+
+			this._addQueued();
+			this._removeQueued();
+			for ( const genStack of this.generatorStacks ) {
+
+				const main = genStack[ 0 ];
+				// Handle if one coroutine removes another
+				if ( this.removeQueue.has( main ) ) {
+
+					continue;
+
+				}
+
+				while ( genStack.length ) {
+
+					const topGen = genStack[ genStack.length - 1 ];
+					const { value, done } = topGen.next();
+					if ( done ) {
+
+						if ( genStack.length === 1 ) {
+
+							this.removeQueue.add( topGen );
+							break;
+
+						}
+
+						genStack.pop();
+
+					} else if ( value ) {
+
+						genStack.push( value );
+
+					} else {
+
+						break;
+
+					}
+
+				}
+
+			}
+
+			this._removeQueued();
+
+		}
+		_addQueued() {
+
+			if ( this.addQueue.length ) {
+
+				this.generatorStacks.splice( this.generatorStacks.length, 0, ...this.addQueue );
+				this.addQueue = [];
+
+			}
+
+		}
+		_removeQueued() {
+
+			if ( this.removeQueue.size ) {
+
+				this.generatorStacks = this.generatorStacks.filter( genStack => ! this.removeQueue.has( genStack[ 0 ] ) );
+				this.removeQueue.clear();
+
+			}
+
+		}
+
+	}
+
+	function removeArrayElement( array, element ) {
+
+		const ndx = array.indexOf( element );
+		if ( ndx >= 0 ) {
+
+			array.splice( ndx, 1 );
+
+		}
+
+	}
+
+	class SafeArray {
+
+		constructor() {
+
+			this.array = [];
+			this.addQueue = [];
+			this.removeQueue = new Set();
+
+		}
+		get isEmpty() {
+
+			return this.addQueue.length + this.array.length > 0;
+
+		}
+		add( element ) {
+
+			this.addQueue.push( element );
+
+		}
+		remove( element ) {
+
+			this.removeQueue.add( element );
+
+		}
+		forEach( fn ) {
+
+			this._addQueued();
+			this._removeQueued();
+			for ( const element of this.array ) {
+
+				if ( this.removeQueue.has( element ) ) {
+
+					continue;
+
+				}
+
+				fn( element );
+
+			}
+
+			this._removeQueued();
+
+		}
+		_addQueued() {
+
+			if ( this.addQueue.length ) {
+
+				this.array.splice( this.array.length, 0, ...this.addQueue );
+				this.addQueue = [];
+
+			}
+
+		}
+		_removeQueued() {
+
+			if ( this.removeQueue.size ) {
+
+				this.array = this.array.filter( element => ! this.removeQueue.has( element ) );
+				this.removeQueue.clear();
+
+			}
+
+		}
+
+	}
+
+	class GameObjectManager {
+
+		constructor() {
+
+			this.gameObjects = new SafeArray();
+
+		}
+		createGameObject( parent, name ) {
+
+			const gameObject = new GameObject( parent, name );
+			this.gameObjects.add( gameObject );
+			return gameObject;
+
+		}
+		removeGameObject( gameObject ) {
+
+			this.gameObjects.remove( gameObject );
+
+		}
+		update() {
+
+			this.gameObjects.forEach( gameObject => gameObject.update() );
+
+		}
+
+	}
+
+	const kForward = new THREE.Vector3( 0, 0, 1 );
+	const globals = {
+		camera,
+		canvas,
+		debug: false,
+		time: 0,
+		moveSpeed: 16,
+		deltaTime: 0,
+		player: null,
+		congaLine: [],
+	};
+	const gameObjectManager = new GameObjectManager();
+	const inputManager = new InputManager();
+
+	class GameObject {
+
+		constructor( parent, name ) {
+
+			this.name = name;
+			this.components = [];
+			this.transform = new THREE.Object3D();
+			this.transform.name = name;
+			parent.add( this.transform );
+
+		}
+		addComponent( ComponentType, ...args ) {
+
+			const component = new ComponentType( this, ...args );
+			this.components.push( component );
+			return component;
+
+		}
+		removeComponent( component ) {
+
+			removeArrayElement( this.components, component );
+
+		}
+		getComponent( ComponentType ) {
+
+			return this.components.find( c => c instanceof ComponentType );
+
+		}
+		update() {
+
+			for ( const component of this.components ) {
+
+				component.update();
+
+			}
+
+		}
+
+	}
+
+	// Base for all components
+	class Component {
+
+		constructor( gameObject ) {
+
+			this.gameObject = gameObject;
+
+		}
+		update() {
+		}
+
+	}
+
+	class CameraInfo extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			this.projScreenMatrix = new THREE.Matrix4();
+			this.frustum = new THREE.Frustum();
+
+		}
+		update() {
+
+			const { camera } = globals;
+			this.projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse );
+			this.frustum.setFromProjectionMatrix( this.projScreenMatrix );
+
+		}
+
+	}
+
+	class SkinInstance extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.model = model;
+			this.animRoot = SkeletonUtils.clone( this.model.gltf.scene );
+			this.mixer = new THREE.AnimationMixer( this.animRoot );
+			gameObject.transform.add( this.animRoot );
+			this.actions = {};
+
+		}
+		setAnimation( animName ) {
+
+			const clip = this.model.animations[ animName ];
+			// turn off all current actions
+			for ( const action of Object.values( this.actions ) ) {
+
+				action.enabled = false;
+
+			}
+
+			// get or create existing action for clip
+			const action = this.mixer.clipAction( clip );
+			action.enabled = true;
+			action.reset();
+			action.play();
+			this.actions[ animName ] = action;
+
+		}
+		update() {
+
+			this.mixer.update( globals.deltaTime );
+
+		}
+
+	}
+
+	class FiniteStateMachine {
+
+		constructor( states, initialState ) {
+
+			this.states = states;
+			this.transition( initialState );
+
+		}
+		get state() {
+
+			return this.currentState;
+
+		}
+		transition( state ) {
+
+			const oldState = this.states[ this.currentState ];
+			if ( oldState && oldState.exit ) {
+
+				oldState.exit.call( this );
+
+			}
+
+			this.currentState = state;
+			const newState = this.states[ state ];
+			if ( newState.enter ) {
+
+				newState.enter.call( this );
+
+			}
+
+		}
+		update() {
+
+			const state = this.states[ this.currentState ];
+			if ( state.update ) {
+
+				state.update.call( this );
+
+			}
+
+		}
+
+	}
+
+	const gui = new GUI();
+	gui.add( globals, 'debug' ).onChange( showHideDebugInfo );
+	gui.close();
+
+	const labelContainerElem = document.querySelector( '#labels' );
+	function showHideDebugInfo() {
+
+		labelContainerElem.style.display = globals.debug ? '' : 'none';
+
+	}
+
+	showHideDebugInfo();
+
+	class StateDisplayHelper extends Component {
+
+		constructor( gameObject, size ) {
+
+			super( gameObject );
+			this.elem = document.createElement( 'div' );
+			labelContainerElem.appendChild( this.elem );
+			this.pos = new THREE.Vector3();
+
+			this.helper = new THREE.PolarGridHelper( size / 2, 1, 1, 16 );
+			gameObject.transform.add( this.helper );
+
+		}
+		setState( s ) {
+
+			this.elem.textContent = s;
+
+		}
+		setColor( cssColor ) {
+
+			this.elem.style.color = cssColor;
+			this.helper.material.color.set( cssColor );
+
+		}
+		update() {
+
+			this.helper.visible = globals.debug;
+			if ( ! globals.debug ) {
+
+				return;
+
+			}
+
+			const { pos } = this;
+			const { transform } = this.gameObject;
+			const { canvas } = globals;
+			pos.copy( transform.position );
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			pos.project( globals.camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( pos.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( pos.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+		}
+
+	}
+
+	function rand( min, max ) {
+
+		if ( max === undefined ) {
+
+			max = min;
+			min = 0;
+
+		}
+
+		return Math.random() * ( max - min ) + min;
+
+	}
+
+	function makeTextTexture( str ) {
+
+		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+		ctx.canvas.width = 64;
+		ctx.canvas.height = 64;
+		ctx.font = '60px sans-serif';
+		ctx.textAlign = 'center';
+		ctx.textBaseline = 'middle';
+		ctx.fillStyle = '#FFF';
+		ctx.fillText( str, ctx.canvas.width / 2, ctx.canvas.height / 2 );
+		return new THREE.CanvasTexture( ctx.canvas );
+
+	}
+
+	const noteTexture = makeTextTexture( '♪' );
+
+	class Note extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			const { transform } = gameObject;
+			const noteMaterial = new THREE.SpriteMaterial( {
+				color: new THREE.Color().setHSL( rand( 1 ), 1, 0.5 ),
+				map: noteTexture,
+				side: THREE.DoubleSide,
+				transparent: true,
+			} );
+			const note = new THREE.Sprite( noteMaterial );
+			note.scale.setScalar( 3 );
+			transform.add( note );
+			this.runner = new CoroutineRunner();
+			const direction = new THREE.Vector3( rand( - 0.2, 0.2 ), 1, rand( - 0.2, 0.2 ) );
+
+			function* moveAndRemove() {
+
+				for ( let i = 0; i < 60; ++ i ) {
+
+					transform.translateOnAxis( direction, globals.deltaTime * 10 );
+					noteMaterial.opacity = 1 - ( i / 60 );
+					yield;
+
+				}
+
+				transform.parent.remove( transform );
+				gameObjectManager.removeGameObject( gameObject );
+
+			}
+
+			this.runner.add( moveAndRemove() );
+
+		}
+		update() {
+
+			this.runner.update();
+
+		}
+
+	}
+
+	class Player extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			const model = models.knight;
+			globals.playerRadius = model.size / 2;
+			this.text = gameObject.addComponent( StateDisplayHelper, model.size );
+			this.skinInstance = gameObject.addComponent( SkinInstance, model );
+			this.skinInstance.setAnimation( 'Run' );
+			this.turnSpeed = globals.moveSpeed / 4;
+			this.offscreenTimer = 0;
+			this.maxTimeOffScreen = 3;
+			this.runner = new CoroutineRunner();
+
+			function* emitNotes() {
+
+				for ( ;; ) {
+
+					yield waitSeconds( rand( 0.5, 1 ) );
+					const noteGO = gameObjectManager.createGameObject( scene, 'note' );
+					noteGO.transform.position.copy( gameObject.transform.position );
+					noteGO.transform.position.y += 5;
+					noteGO.addComponent( Note );
+
+				}
+
+			}
+
+			this.runner.add( emitNotes() );
+
+		}
+		update() {
+
+			this.runner.update();
+			const { deltaTime, moveSpeed, cameraInfo } = globals;
+			const { transform } = this.gameObject;
+			const delta = ( inputManager.keys.left.down ? 1 : 0 ) +
+                    ( inputManager.keys.right.down ? - 1 : 0 );
+			transform.rotation.y += this.turnSpeed * delta * deltaTime;
+			transform.translateOnAxis( kForward, moveSpeed * deltaTime );
+
+			const { frustum } = cameraInfo;
+			if ( frustum.containsPoint( transform.position ) ) {
+
+				this.offscreenTimer = 0;
+
+			} else {
+
+				this.offscreenTimer += deltaTime;
+				if ( this.offscreenTimer >= this.maxTimeOffScreen ) {
+
+					transform.position.set( 0, 0, 0 );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	// Returns true of obj1 and obj2 are close
+	function isClose( obj1, obj1Radius, obj2, obj2Radius ) {
+
+		const minDist = obj1Radius + obj2Radius;
+		const dist = obj1.position.distanceTo( obj2.position );
+		return dist < minDist;
+
+	}
+
+	// keeps v between -min and +min
+	function minMagnitude( v, min ) {
+
+		return Math.abs( v ) > min
+			? min * Math.sign( v )
+			: v;
+
+	}
+
+	const aimTowardAndGetDistance = function () {
+
+		const delta = new THREE.Vector3();
+
+		return function aimTowardAndGetDistance( source, targetPos, maxTurn ) {
+
+			delta.subVectors( targetPos, source.position );
+			// compute the direction we want to be facing
+			const targetRot = Math.atan2( delta.x, delta.z ) + Math.PI * 1.5;
+			// rotate in the shortest direction
+			const deltaRot = ( targetRot - source.rotation.y + Math.PI * 1.5 ) % ( Math.PI * 2 ) - Math.PI;
+			// make sure we don't turn faster than maxTurn
+			const deltaRotation = minMagnitude( deltaRot, maxTurn );
+			// keep rotation between 0 and Math.PI * 2
+			source.rotation.y = THREE.MathUtils.euclideanModulo(
+				source.rotation.y + deltaRotation, Math.PI * 2 );
+			// return the distance to the target
+			return delta.length();
+
+		};
+
+	}();
+
+	class Animal extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.helper = gameObject.addComponent( StateDisplayHelper, model.size );
+			const hitRadius = model.size / 2;
+			const skinInstance = gameObject.addComponent( SkinInstance, model );
+			skinInstance.mixer.timeScale = globals.moveSpeed / 4;
+			const transform = gameObject.transform;
+			const playerTransform = globals.player.gameObject.transform;
+			const maxTurnSpeed = Math.PI * ( globals.moveSpeed / 4 );
+			const targetHistory = [];
+			let targetNdx = 0;
+
+			function addHistory() {
+
+				const targetGO = globals.congaLine[ targetNdx ];
+				const newTargetPos = new THREE.Vector3();
+				newTargetPos.copy( targetGO.transform.position );
+				targetHistory.push( newTargetPos );
+
+			}
+
+			this.fsm = new FiniteStateMachine( {
+				idle: {
+					enter: () => {
+
+						skinInstance.setAnimation( 'Idle' );
+
+					},
+					update: () => {
+
+						// check if player is near
+						if ( isClose( transform, hitRadius, playerTransform, globals.playerRadius ) ) {
+
+							this.fsm.transition( 'waitForEnd' );
+
+						}
+
+					},
+				},
+				waitForEnd: {
+					enter: () => {
+
+						skinInstance.setAnimation( 'Jump' );
+
+					},
+					update: () => {
+
+						// get the gameObject at the end of the conga line
+						const lastGO = globals.congaLine[ globals.congaLine.length - 1 ];
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						const targetPos = lastGO.transform.position;
+						aimTowardAndGetDistance( transform, targetPos, deltaTurnSpeed );
+						// check if last thing in conga line is near
+						if ( isClose( transform, hitRadius, lastGO.transform, globals.playerRadius ) ) {
+
+							this.fsm.transition( 'goToLast' );
+
+						}
+
+					},
+				},
+				goToLast: {
+					enter: () => {
+
+						// remember who we're following
+						targetNdx = globals.congaLine.length - 1;
+						// add ourselves to the conga line
+						globals.congaLine.push( gameObject );
+						skinInstance.setAnimation( 'Walk' );
+
+					},
+					update: () => {
+
+						addHistory();
+						// walk to the oldest point in the history
+						const targetPos = targetHistory[ 0 ];
+						const maxVelocity = globals.moveSpeed * globals.deltaTime;
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						const distance = aimTowardAndGetDistance( transform, targetPos, deltaTurnSpeed );
+						const velocity = distance;
+						transform.translateOnAxis( kForward, Math.min( velocity, maxVelocity ) );
+						if ( distance <= maxVelocity ) {
+
+							this.fsm.transition( 'follow' );
+
+						}
+
+					},
+				},
+				follow: {
+					update: () => {
+
+						addHistory();
+						// remove the oldest history and just put ourselves there.
+						const targetPos = targetHistory.shift();
+						transform.position.copy( targetPos );
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						aimTowardAndGetDistance( transform, targetHistory[ 0 ], deltaTurnSpeed );
+
+					},
+				},
+			}, 'idle' );
+
+		}
+		update() {
+
+			this.fsm.update();
+			const dir = THREE.MathUtils.radToDeg( this.gameObject.transform.rotation.y );
+			this.helper.setState( `${this.fsm.state}:${dir.toFixed( 0 )}` );
+
+		}
+
+	}
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( camera, 'camera' );
+			globals.cameraInfo = gameObject.addComponent( CameraInfo );
+
+		}
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( scene, 'player' );
+			globals.player = gameObject.addComponent( Player );
+			globals.congaLine = [ gameObject ];
+
+		}
+
+		const animalModelNames = [
+			'pig',
+			'cow',
+			'llama',
+			'pug',
+			'sheep',
+			'zebra',
+			'horse',
+		];
+		const base = new THREE.Object3D();
+		const offset = new THREE.Object3D();
+		base.add( offset );
+
+		// position animals in a spiral.
+		const numAnimals = 28;
+		const arc = 10;
+		const b = 10 / ( 2 * Math.PI );
+		let r = 10;
+		let phi = r / b;
+		for ( let i = 0; i < numAnimals; ++ i ) {
+
+			const name = animalModelNames[ rand( animalModelNames.length ) | 0 ];
+			const gameObject = gameObjectManager.createGameObject( scene, name );
+			gameObject.addComponent( Animal, models[ name ] );
+			base.rotation.y = phi;
+			offset.position.x = r;
+			offset.updateWorldMatrix( true, false );
+			offset.getWorldPosition( gameObject.transform.position );
+			phi += arc / r;
+			r = b * phi;
+
+		}
+
+	}
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		// convert to seconds
+		globals.time = now * 0.001;
+		// make sure delta time isn't too big.
+		globals.deltaTime = Math.min( globals.time - then, 1 / 20 );
+		then = globals.time;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		gameObjectManager.update();
+		inputManager.update();
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 844 - 613
manual/examples/game-conga-line.html

@@ -163,655 +163,886 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 40, 80);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    const box = new THREE.Box3();
-    const size = new THREE.Vector3();
-    Object.values(models).forEach(model => {
-      box.setFromObject(model.gltf.scene);
-      box.getSize(size);
-      model.size = size.length();
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-        // Should really fix this in .blend file
-        if (clip.name === 'Walk') {
-          clip.duration /= 2;
-        }
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  // Keeps the state of keys/buttons
-  //
-  // You can check
-  //
-  //   inputManager.keys.left.down
-  //
-  // to see if the left key is currently held down
-  // and you can check
-  //
-  //   inputManager.keys.left.justPressed
-  //
-  // To see if the left key was pressed this frame
-  //
-  // Keys are 'left', 'right', 'a', 'b', 'up', 'down'
-  class InputManager {
-    constructor() {
-      this.keys = {};
-      const keyMap = new Map();
-
-      const setKey = (keyName, pressed) => {
-        const keyState = this.keys[keyName];
-        keyState.justPressed = pressed && !keyState.down;
-        keyState.down = pressed;
-      };
-
-      const addKey = (keyCode, name) => {
-        this.keys[name] = { down: false, justPressed: false };
-        keyMap.set(keyCode, name);
-      };
-
-      const setKeyFromKeyCode = (keyCode, pressed) => {
-        const keyName = keyMap.get(keyCode);
-        if (!keyName) {
-          return;
-        }
-        setKey(keyName, pressed);
-      };
-
-      addKey(37, 'left');
-      addKey(39, 'right');
-      addKey(38, 'up');
-      addKey(40, 'down');
-      addKey(90, 'a');
-      addKey(88, 'b');
-
-      window.addEventListener('keydown', (e) => {
-        setKeyFromKeyCode(e.keyCode, true);
-      });
-      window.addEventListener('keyup', (e) => {
-        setKeyFromKeyCode(e.keyCode, false);
-      });
-
-      const sides = [
-        { elem: document.querySelector('#left'),  key: 'left'  },
-        { elem: document.querySelector('#right'), key: 'right' },
-      ];
-
-      // note: not a good design?
-      // The last direction the user presses should take
-      // precedence. Example: User presses L, without letting go of
-      // L user presses R. Input should now be R. User lets off R
-      // Input should now be L.
-      // With this code if user pressed both L and R result is nothing
-
-      const clearKeys = () => {
-        for (const {key} of sides) {
-            setKey(key, false);
-        }
-      };
-
-      const handleMouseMove = (e) => {
-        e.preventDefault();
-        // this is needed because we call preventDefault();
-        // we also gave the canvas a tabindex so it can
-        // become the focus
-        canvas.focus();
-        window.addEventListener('pointermove', handleMouseMove);
-        window.addEventListener('pointerup', handleMouseUp);
-
-        for (const {elem, key} of sides) {
-          let pressed = false;
-          const rect = elem.getBoundingClientRect();
-          const x = e.clientX;
-          const y = e.clientY;
-          const inRect = x >= rect.left && x < rect.right &&
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 40, 80 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
+
+	function addLight( ...pos ) {
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
+
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
+
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
+
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
+
+	};
+
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
+
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
+
+			gltfLoader.load( model.url, ( gltf ) => {
+
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		const box = new THREE.Box3();
+		const size = new THREE.Vector3();
+		Object.values( models ).forEach( model => {
+
+			box.setFromObject( model.gltf.scene );
+			box.getSize( size );
+			model.size = size.length();
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+				// Should really fix this in .blend file
+				if ( clip.name === 'Walk' ) {
+
+					clip.duration /= 2;
+
+				}
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	// Keeps the state of keys/buttons
+	//
+	// You can check
+	//
+	//   inputManager.keys.left.down
+	//
+	// to see if the left key is currently held down
+	// and you can check
+	//
+	//   inputManager.keys.left.justPressed
+	//
+	// To see if the left key was pressed this frame
+	//
+	// Keys are 'left', 'right', 'a', 'b', 'up', 'down'
+	class InputManager {
+
+		constructor() {
+
+			this.keys = {};
+			const keyMap = new Map();
+
+			const setKey = ( keyName, pressed ) => {
+
+				const keyState = this.keys[ keyName ];
+				keyState.justPressed = pressed && ! keyState.down;
+				keyState.down = pressed;
+
+			};
+
+			const addKey = ( keyCode, name ) => {
+
+				this.keys[ name ] = { down: false, justPressed: false };
+				keyMap.set( keyCode, name );
+
+			};
+
+			const setKeyFromKeyCode = ( keyCode, pressed ) => {
+
+				const keyName = keyMap.get( keyCode );
+				if ( ! keyName ) {
+
+					return;
+
+				}
+
+				setKey( keyName, pressed );
+
+			};
+
+			addKey( 37, 'left' );
+			addKey( 39, 'right' );
+			addKey( 38, 'up' );
+			addKey( 40, 'down' );
+			addKey( 90, 'a' );
+			addKey( 88, 'b' );
+
+			window.addEventListener( 'keydown', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, true );
+
+			} );
+			window.addEventListener( 'keyup', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, false );
+
+			} );
+
+			const sides = [
+				{ elem: document.querySelector( '#left' ), key: 'left' },
+				{ elem: document.querySelector( '#right' ), key: 'right' },
+			];
+
+			// note: not a good design?
+			// The last direction the user presses should take
+			// precedence. Example: User presses L, without letting go of
+			// L user presses R. Input should now be R. User lets off R
+			// Input should now be L.
+			// With this code if user pressed both L and R result is nothing
+
+			const clearKeys = () => {
+
+				for ( const { key } of sides ) {
+
+					setKey( key, false );
+
+				}
+
+			};
+
+			const handleMouseMove = ( e ) => {
+
+				e.preventDefault();
+				// this is needed because we call preventDefault();
+				// we also gave the canvas a tabindex so it can
+				// become the focus
+				canvas.focus();
+				window.addEventListener( 'pointermove', handleMouseMove );
+				window.addEventListener( 'pointerup', handleMouseUp );
+
+				for ( const { elem, key } of sides ) {
+
+					let pressed = false;
+					const rect = elem.getBoundingClientRect();
+					const x = e.clientX;
+					const y = e.clientY;
+					const inRect = x >= rect.left && x < rect.right &&
                          y >= rect.top && y < rect.bottom;
                          y >= rect.top && y < rect.bottom;
-          if (inRect) {
-            pressed = true;
-          }
-          setKey(key, pressed);
-        }
-      };
+					if ( inRect ) {
 
 
-      function handleMouseUp() {
-        clearKeys();
-        window.removeEventListener('pointermove', handleMouseMove, {passive: false});
-        window.removeEventListener('pointerup', handleMouseUp);
-      }
+						pressed = true;
 
 
-      const uiElem = document.querySelector('#ui');
-      uiElem.addEventListener('pointerdown', handleMouseMove, {passive: false});
+					}
 
 
-      uiElem.addEventListener('touchstart', (e) => {
-        // prevent scrolling
-        e.preventDefault();
-      }, {passive: false});
-    }
-    update() {
-      for (const keyState of Object.values(this.keys)) {
-        if (keyState.justPressed) {
-          keyState.justPressed = false;
-        }
-      }
-    }
-  }
+					setKey( key, pressed );
 
 
-  function removeArrayElement(array, element) {
-    const ndx = array.indexOf(element);
-    if (ndx >= 0) {
-      array.splice(ndx, 1);
-    }
-  }
+				}
 
 
-  class SafeArray {
-    constructor() {
-      this.array = [];
-      this.addQueue = [];
-      this.removeQueue = new Set();
-    }
-    get isEmpty() {
-      return this.addQueue.length + this.array.length > 0;
-    }
-    add(element) {
-      this.addQueue.push(element);
-    }
-    remove(element) {
-      this.removeQueue.add(element);
-    }
-    forEach(fn) {
-      this._addQueued();
-      this._removeQueued();
-      for (const element of this.array) {
-        if (this.removeQueue.has(element)) {
-          continue;
-        }
-        fn(element);
-      }
-      this._removeQueued();
-    }
-    _addQueued() {
-      if (this.addQueue.length) {
-        this.array.splice(this.array.length, 0, ...this.addQueue);
-        this.addQueue = [];
-      }
-    }
-    _removeQueued() {
-      if (this.removeQueue.size) {
-        this.array = this.array.filter(element => !this.removeQueue.has(element));
-        this.removeQueue.clear();
-      }
-    }
-  }
+			};
 
 
-  class GameObjectManager {
-    constructor() {
-      this.gameObjects = new SafeArray();
-    }
-    createGameObject(parent, name) {
-      const gameObject = new GameObject(parent, name);
-      this.gameObjects.add(gameObject);
-      return gameObject;
-    }
-    removeGameObject(gameObject) {
-      this.gameObjects.remove(gameObject);
-    }
-    update() {
-      this.gameObjects.forEach(gameObject => gameObject.update());
-    }
-  }
+			function handleMouseUp() {
 
 
-  const kForward = new THREE.Vector3(0, 0, 1);
-  const globals = {
-    camera,
-    canvas,
-    debug: true,
-    time: 0,
-    moveSpeed: 16,
-    deltaTime: 0,
-    player: null,
-    congaLine: [],
-  };
-  const gameObjectManager = new GameObjectManager();
-  const inputManager = new InputManager();
-
-  class GameObject {
-    constructor(parent, name) {
-      this.name = name;
-      this.components = [];
-      this.transform = new THREE.Object3D();
-      parent.add(this.transform);
-    }
-    addComponent(ComponentType, ...args) {
-      const component = new ComponentType(this, ...args);
-      this.components.push(component);
-      return component;
-    }
-    removeComponent(component) {
-      removeArrayElement(this.components, component);
-    }
-    getComponent(ComponentType) {
-      return this.components.find(c => c instanceof ComponentType);
-    }
-    update() {
-      for (const component of this.components) {
-        component.update();
-      }
-    }
-  }
+				clearKeys();
+				window.removeEventListener( 'pointermove', handleMouseMove, { passive: false } );
+				window.removeEventListener( 'pointerup', handleMouseUp );
 
 
-  // Base for all components
-  class Component {
-    constructor(gameObject) {
-      this.gameObject = gameObject;
-    }
-    update() {
-    }
-  }
+			}
 
 
-  class CameraInfo extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      this.projScreenMatrix = new THREE.Matrix4();
-      this.frustum = new THREE.Frustum();
-    }
-    update() {
-      const {camera} = globals;
-      this.projScreenMatrix.multiplyMatrices(
-          camera.projectionMatrix,
-          camera.matrixWorldInverse);
-      this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
-    }
-  }
+			const uiElem = document.querySelector( '#ui' );
+			uiElem.addEventListener( 'pointerdown', handleMouseMove, { passive: false } );
 
 
-  class SkinInstance extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.model = model;
-      this.animRoot = SkeletonUtils.clone(this.model.gltf.scene);
-      this.mixer = new THREE.AnimationMixer(this.animRoot);
-      gameObject.transform.add(this.animRoot);
-      this.actions = {};
-    }
-    setAnimation(animName) {
-      const clip = this.model.animations[animName];
-      // turn off all current actions
-      for (const action of Object.values(this.actions)) {
-        action.enabled = false;
-      }
-      // get or create existing action for clip
-      const action = this.mixer.clipAction(clip);
-      action.enabled = true;
-      action.reset();
-      action.play();
-      this.actions[animName] = action;
-    }
-    update() {
-      this.mixer.update(globals.deltaTime);
-    }
-  }
+			uiElem.addEventListener( 'touchstart', ( e ) => {
 
 
-  class FiniteStateMachine {
-    constructor(states, initialState) {
-      this.states = states;
-      this.transition(initialState);
-    }
-    get state() {
-      return this.currentState;
-    }
-    transition(state) {
-      const oldState = this.states[this.currentState];
-      if (oldState && oldState.exit) {
-        oldState.exit.call(this);
-      }
-      this.currentState = state;
-      const newState = this.states[state];
-      if (newState.enter) {
-        newState.enter.call(this);
-      }
-    }
-    update() {
-      const state = this.states[this.currentState];
-      if (state.update) {
-        state.update.call(this);
-      }
-    }
-  }
+				// prevent scrolling
+				e.preventDefault();
 
 
-  const gui = new GUI();
-  gui.add(globals, 'debug').onChange(showHideDebugInfo);
+			}, { passive: false } );
 
 
-  const labelContainerElem = document.querySelector('#labels');
-  function showHideDebugInfo() {
-    labelContainerElem.style.display = globals.debug ? '' : 'none';
-  }
-  class StateDisplayHelper extends Component {
-    constructor(gameObject, size) {
-      super(gameObject);
-      this.elem = document.createElement('div');
-      labelContainerElem.appendChild(this.elem);
-      this.pos = new THREE.Vector3();
-
-      this.helper = new THREE.PolarGridHelper(size / 2, 1, 1, 16);
-      gameObject.transform.add(this.helper);
-    }
-    setState(s) {
-      this.elem.textContent = s;
-    }
-    setColor(cssColor) {
-      this.elem.style.color = cssColor;
-      this.helper.material.color.set(cssColor);
-    }
-    update() {
-      this.helper.visible = globals.debug;
-      if (!globals.debug) {
-        return;
-      }
-      const {pos} = this;
-      const {transform} = this.gameObject;
-      const {canvas} = globals;
-      pos.copy(transform.position);
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      pos.project(globals.camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (pos.x *  .5 + .5) * canvas.clientWidth;
-      const y = (pos.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-    }
-  }
+		}
+		update() {
 
 
-  class Player extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      const model = models.knight;
-      globals.playerRadius = model.size / 2;
-      this.text = gameObject.addComponent(StateDisplayHelper, model.size);
-      this.skinInstance = gameObject.addComponent(SkinInstance, model);
-      this.skinInstance.setAnimation('Run');
-      this.turnSpeed = globals.moveSpeed / 4;
-      this.offscreenTimer = 0;
-      this.maxTimeOffScreen = 3;
+			for ( const keyState of Object.values( this.keys ) ) {
 
 
-    }
-    update() {
-      const {deltaTime, moveSpeed, cameraInfo} = globals;
-      const {transform} = this.gameObject;
-      const delta = (inputManager.keys.left.down  ?  1 : 0) +
-                    (inputManager.keys.right.down ? -1 : 0);
-      transform.rotation.y += this.turnSpeed * delta * deltaTime;
-      transform.translateOnAxis(kForward, moveSpeed * deltaTime);
-
-      const {frustum} = cameraInfo;
-      if (frustum.containsPoint(transform.position)) {
-        this.offscreenTimer = 0;
-      } else {
-        this.offscreenTimer += deltaTime;
-        if (this.offscreenTimer >= this.maxTimeOffScreen) {
-          transform.position.set(0, 0, 0);
-        }
-      }
-    }
-  }
+				if ( keyState.justPressed ) {
 
 
-  // Returns true of obj1 and obj2 are close
-  function isClose(obj1, obj1Radius, obj2, obj2Radius) {
-    const minDist = obj1Radius + obj2Radius;
-    const dist = obj1.position.distanceTo(obj2.position);
-    return dist < minDist;
-  }
+					keyState.justPressed = false;
 
 
-  // keeps v between -min and +min
-  function minMagnitude(v, min) {
-    return Math.abs(v) > min
-        ? min * Math.sign(v)
-        : v;
-  }
+				}
 
 
-  const aimTowardAndGetDistance = function() {
-    const delta = new THREE.Vector3();
-
-    return function aimTowardAndGetDistance(source, targetPos, maxTurn) {
-      delta.subVectors(targetPos, source.position);
-      // compute the direction we want to be facing
-      const targetRot = Math.atan2(delta.x, delta.z) + Math.PI * 1.5;
-      // rotate in the shortest direction
-      const deltaRot = (targetRot - source.rotation.y + Math.PI * 1.5) % (Math.PI * 2) - Math.PI;
-      // make sure we don't turn faster than maxTurn
-      const deltaRotation = minMagnitude(deltaRot, maxTurn);
-      // keep rotation between 0 and Math.PI * 2
-      source.rotation.y = THREE.MathUtils.euclideanModulo(
-          source.rotation.y + deltaRotation, Math.PI * 2);
-      // return the distance to the target
-      return delta.length();
-    };
-  }();
-
-  class Animal extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.helper = gameObject.addComponent(StateDisplayHelper, model.size);
-      const hitRadius = model.size / 2;
-      const skinInstance = gameObject.addComponent(SkinInstance, model);
-      skinInstance.mixer.timeScale = globals.moveSpeed / 4;
-      const transform = gameObject.transform;
-      const playerTransform = globals.player.gameObject.transform;
-      const maxTurnSpeed = Math.PI * (globals.moveSpeed / 4);
-      const targetHistory = [];
-      let targetNdx = 0;
-
-      function addHistory() {
-        const targetGO = globals.congaLine[targetNdx];
-        const newTargetPos = new THREE.Vector3();
-        newTargetPos.copy(targetGO.transform.position);
-        targetHistory.push(newTargetPos);
-      }
+			}
 
 
-      this.fsm = new FiniteStateMachine({
-        idle: {
-          enter: () => {
-            skinInstance.setAnimation('Idle');
-          },
-          update: () => {
-            // check if player is near
-            if (isClose(transform, hitRadius, playerTransform, globals.playerRadius)) {
-              this.fsm.transition('waitForEnd');
-            }
-          },
-        },
-        waitForEnd: {
-          enter: () => {
-            skinInstance.setAnimation('Jump');
-          },
-          update: () => {
-            // get the gameObject at the end of the conga line
-            const lastGO = globals.congaLine[globals.congaLine.length - 1];
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            const targetPos = lastGO.transform.position;
-            aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
-            // check if last thing in conga line is near
-            if (isClose(transform, hitRadius, lastGO.transform, globals.playerRadius)) {
-              this.fsm.transition('goToLast');
-            }
-          },
-        },
-        goToLast: {
-          enter: () => {
-            // remember who we're following
-            targetNdx = globals.congaLine.length - 1;
-            // add ourselves to the conga line
-            globals.congaLine.push(gameObject);
-            skinInstance.setAnimation('Walk');
-          },
-          update: () => {
-            addHistory();
-            // walk to the oldest point in the history
-            const targetPos = targetHistory[0];
-            const maxVelocity = globals.moveSpeed * globals.deltaTime;
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            const distance = aimTowardAndGetDistance(transform, targetPos, deltaTurnSpeed);
-            const velocity = distance;
-            transform.translateOnAxis(kForward, Math.min(velocity, maxVelocity));
-            if (distance <= maxVelocity) {
-              this.fsm.transition('follow');
-            }
-          },
-        },
-        follow: {
-          update: () => {
-            addHistory();
-            // remove the oldest history and just put ourselves there.
-            const targetPos = targetHistory.shift();
-            transform.position.copy(targetPos);
-            const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
-            aimTowardAndGetDistance(transform, targetHistory[0], deltaTurnSpeed);
-          },
-        },
-      }, 'idle');
-    }
-    update() {
-      this.fsm.update();
-      const dir = THREE.MathUtils.radToDeg(this.gameObject.transform.rotation.y);
-      this.helper.setState(`${this.fsm.state}:${dir.toFixed(0)}`);
-    }
-  }
+		}
 
 
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
+	}
 
 
-    prepModelsAndAnimations();
+	function removeArrayElement( array, element ) {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(camera, 'camera');
-      globals.cameraInfo = gameObject.addComponent(CameraInfo);
-    }
+		const ndx = array.indexOf( element );
+		if ( ndx >= 0 ) {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(scene, 'player');
-      globals.player = gameObject.addComponent(Player);
-      globals.congaLine = [gameObject];
-    }
+			array.splice( ndx, 1 );
 
 
-    const animalModelNames = [
-      'pig',
-      'cow',
-      'llama',
-      'pug',
-      'sheep',
-      'zebra',
-      'horse',
-    ];
-    animalModelNames.forEach((name, ndx) => {
-      const gameObject = gameObjectManager.createGameObject(scene, name);
-      gameObject.addComponent(Animal, models[name]);
-      gameObject.transform.position.x = (ndx + 1) * 5;
-    });
-  }
+		}
 
 
-  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;
-  }
+	}
 
 
-  let then = 0;
-  function render(now) {
-    // convert to seconds
-    globals.time = now * 0.001;
-    // make sure delta time isn't too big.
-    globals.deltaTime = Math.min(globals.time - then, 1 / 20);
-    then = globals.time;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	class SafeArray {
 
 
-    gameObjectManager.update();
-    inputManager.update();
+		constructor() {
 
 
-    renderer.render(scene, camera);
+			this.array = [];
+			this.addQueue = [];
+			this.removeQueue = new Set();
 
 
-    requestAnimationFrame(render);
-  }
+		}
+		get isEmpty() {
+
+			return this.addQueue.length + this.array.length > 0;
+
+		}
+		add( element ) {
+
+			this.addQueue.push( element );
+
+		}
+		remove( element ) {
+
+			this.removeQueue.add( element );
+
+		}
+		forEach( fn ) {
+
+			this._addQueued();
+			this._removeQueued();
+			for ( const element of this.array ) {
+
+				if ( this.removeQueue.has( element ) ) {
+
+					continue;
+
+				}
+
+				fn( element );
+
+			}
+
+			this._removeQueued();
+
+		}
+		_addQueued() {
+
+			if ( this.addQueue.length ) {
+
+				this.array.splice( this.array.length, 0, ...this.addQueue );
+				this.addQueue = [];
+
+			}
+
+		}
+		_removeQueued() {
+
+			if ( this.removeQueue.size ) {
+
+				this.array = this.array.filter( element => ! this.removeQueue.has( element ) );
+				this.removeQueue.clear();
+
+			}
+
+		}
+
+	}
+
+	class GameObjectManager {
+
+		constructor() {
+
+			this.gameObjects = new SafeArray();
+
+		}
+		createGameObject( parent, name ) {
+
+			const gameObject = new GameObject( parent, name );
+			this.gameObjects.add( gameObject );
+			return gameObject;
+
+		}
+		removeGameObject( gameObject ) {
+
+			this.gameObjects.remove( gameObject );
+
+		}
+		update() {
+
+			this.gameObjects.forEach( gameObject => gameObject.update() );
+
+		}
+
+	}
+
+	const kForward = new THREE.Vector3( 0, 0, 1 );
+	const globals = {
+		camera,
+		canvas,
+		debug: true,
+		time: 0,
+		moveSpeed: 16,
+		deltaTime: 0,
+		player: null,
+		congaLine: [],
+	};
+	const gameObjectManager = new GameObjectManager();
+	const inputManager = new InputManager();
+
+	class GameObject {
+
+		constructor( parent, name ) {
+
+			this.name = name;
+			this.components = [];
+			this.transform = new THREE.Object3D();
+			parent.add( this.transform );
+
+		}
+		addComponent( ComponentType, ...args ) {
+
+			const component = new ComponentType( this, ...args );
+			this.components.push( component );
+			return component;
+
+		}
+		removeComponent( component ) {
+
+			removeArrayElement( this.components, component );
+
+		}
+		getComponent( ComponentType ) {
+
+			return this.components.find( c => c instanceof ComponentType );
+
+		}
+		update() {
+
+			for ( const component of this.components ) {
+
+				component.update();
+
+			}
+
+		}
+
+	}
+
+	// Base for all components
+	class Component {
+
+		constructor( gameObject ) {
+
+			this.gameObject = gameObject;
+
+		}
+		update() {
+		}
+
+	}
+
+	class CameraInfo extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			this.projScreenMatrix = new THREE.Matrix4();
+			this.frustum = new THREE.Frustum();
+
+		}
+		update() {
+
+			const { camera } = globals;
+			this.projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse );
+			this.frustum.setFromProjectionMatrix( this.projScreenMatrix );
+
+		}
+
+	}
+
+	class SkinInstance extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.model = model;
+			this.animRoot = SkeletonUtils.clone( this.model.gltf.scene );
+			this.mixer = new THREE.AnimationMixer( this.animRoot );
+			gameObject.transform.add( this.animRoot );
+			this.actions = {};
+
+		}
+		setAnimation( animName ) {
+
+			const clip = this.model.animations[ animName ];
+			// turn off all current actions
+			for ( const action of Object.values( this.actions ) ) {
+
+				action.enabled = false;
+
+			}
+
+			// get or create existing action for clip
+			const action = this.mixer.clipAction( clip );
+			action.enabled = true;
+			action.reset();
+			action.play();
+			this.actions[ animName ] = action;
+
+		}
+		update() {
+
+			this.mixer.update( globals.deltaTime );
+
+		}
+
+	}
+
+	class FiniteStateMachine {
+
+		constructor( states, initialState ) {
+
+			this.states = states;
+			this.transition( initialState );
+
+		}
+		get state() {
+
+			return this.currentState;
+
+		}
+		transition( state ) {
+
+			const oldState = this.states[ this.currentState ];
+			if ( oldState && oldState.exit ) {
+
+				oldState.exit.call( this );
+
+			}
+
+			this.currentState = state;
+			const newState = this.states[ state ];
+			if ( newState.enter ) {
+
+				newState.enter.call( this );
+
+			}
+
+		}
+		update() {
+
+			const state = this.states[ this.currentState ];
+			if ( state.update ) {
+
+				state.update.call( this );
+
+			}
+
+		}
+
+	}
+
+	const gui = new GUI();
+	gui.add( globals, 'debug' ).onChange( showHideDebugInfo );
+
+	const labelContainerElem = document.querySelector( '#labels' );
+	function showHideDebugInfo() {
+
+		labelContainerElem.style.display = globals.debug ? '' : 'none';
+
+	}
+
+	class StateDisplayHelper extends Component {
+
+		constructor( gameObject, size ) {
+
+			super( gameObject );
+			this.elem = document.createElement( 'div' );
+			labelContainerElem.appendChild( this.elem );
+			this.pos = new THREE.Vector3();
+
+			this.helper = new THREE.PolarGridHelper( size / 2, 1, 1, 16 );
+			gameObject.transform.add( this.helper );
+
+		}
+		setState( s ) {
+
+			this.elem.textContent = s;
+
+		}
+		setColor( cssColor ) {
+
+			this.elem.style.color = cssColor;
+			this.helper.material.color.set( cssColor );
+
+		}
+		update() {
+
+			this.helper.visible = globals.debug;
+			if ( ! globals.debug ) {
+
+				return;
+
+			}
+
+			const { pos } = this;
+			const { transform } = this.gameObject;
+			const { canvas } = globals;
+			pos.copy( transform.position );
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			pos.project( globals.camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( pos.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( pos.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			this.elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+		}
+
+	}
+
+	class Player extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			const model = models.knight;
+			globals.playerRadius = model.size / 2;
+			this.text = gameObject.addComponent( StateDisplayHelper, model.size );
+			this.skinInstance = gameObject.addComponent( SkinInstance, model );
+			this.skinInstance.setAnimation( 'Run' );
+			this.turnSpeed = globals.moveSpeed / 4;
+			this.offscreenTimer = 0;
+			this.maxTimeOffScreen = 3;
+
+		}
+		update() {
+
+			const { deltaTime, moveSpeed, cameraInfo } = globals;
+			const { transform } = this.gameObject;
+			const delta = ( inputManager.keys.left.down ? 1 : 0 ) +
+                    ( inputManager.keys.right.down ? - 1 : 0 );
+			transform.rotation.y += this.turnSpeed * delta * deltaTime;
+			transform.translateOnAxis( kForward, moveSpeed * deltaTime );
+
+			const { frustum } = cameraInfo;
+			if ( frustum.containsPoint( transform.position ) ) {
+
+				this.offscreenTimer = 0;
+
+			} else {
+
+				this.offscreenTimer += deltaTime;
+				if ( this.offscreenTimer >= this.maxTimeOffScreen ) {
+
+					transform.position.set( 0, 0, 0 );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	// Returns true of obj1 and obj2 are close
+	function isClose( obj1, obj1Radius, obj2, obj2Radius ) {
+
+		const minDist = obj1Radius + obj2Radius;
+		const dist = obj1.position.distanceTo( obj2.position );
+		return dist < minDist;
+
+	}
+
+	// keeps v between -min and +min
+	function minMagnitude( v, min ) {
+
+		return Math.abs( v ) > min
+			? min * Math.sign( v )
+			: v;
+
+	}
+
+	const aimTowardAndGetDistance = function () {
+
+		const delta = new THREE.Vector3();
+
+		return function aimTowardAndGetDistance( source, targetPos, maxTurn ) {
+
+			delta.subVectors( targetPos, source.position );
+			// compute the direction we want to be facing
+			const targetRot = Math.atan2( delta.x, delta.z ) + Math.PI * 1.5;
+			// rotate in the shortest direction
+			const deltaRot = ( targetRot - source.rotation.y + Math.PI * 1.5 ) % ( Math.PI * 2 ) - Math.PI;
+			// make sure we don't turn faster than maxTurn
+			const deltaRotation = minMagnitude( deltaRot, maxTurn );
+			// keep rotation between 0 and Math.PI * 2
+			source.rotation.y = THREE.MathUtils.euclideanModulo(
+				source.rotation.y + deltaRotation, Math.PI * 2 );
+			// return the distance to the target
+			return delta.length();
+
+		};
+
+	}();
+
+	class Animal extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.helper = gameObject.addComponent( StateDisplayHelper, model.size );
+			const hitRadius = model.size / 2;
+			const skinInstance = gameObject.addComponent( SkinInstance, model );
+			skinInstance.mixer.timeScale = globals.moveSpeed / 4;
+			const transform = gameObject.transform;
+			const playerTransform = globals.player.gameObject.transform;
+			const maxTurnSpeed = Math.PI * ( globals.moveSpeed / 4 );
+			const targetHistory = [];
+			let targetNdx = 0;
+
+			function addHistory() {
+
+				const targetGO = globals.congaLine[ targetNdx ];
+				const newTargetPos = new THREE.Vector3();
+				newTargetPos.copy( targetGO.transform.position );
+				targetHistory.push( newTargetPos );
+
+			}
+
+			this.fsm = new FiniteStateMachine( {
+				idle: {
+					enter: () => {
+
+						skinInstance.setAnimation( 'Idle' );
+
+					},
+					update: () => {
+
+						// check if player is near
+						if ( isClose( transform, hitRadius, playerTransform, globals.playerRadius ) ) {
+
+							this.fsm.transition( 'waitForEnd' );
+
+						}
+
+					},
+				},
+				waitForEnd: {
+					enter: () => {
+
+						skinInstance.setAnimation( 'Jump' );
+
+					},
+					update: () => {
+
+						// get the gameObject at the end of the conga line
+						const lastGO = globals.congaLine[ globals.congaLine.length - 1 ];
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						const targetPos = lastGO.transform.position;
+						aimTowardAndGetDistance( transform, targetPos, deltaTurnSpeed );
+						// check if last thing in conga line is near
+						if ( isClose( transform, hitRadius, lastGO.transform, globals.playerRadius ) ) {
+
+							this.fsm.transition( 'goToLast' );
+
+						}
+
+					},
+				},
+				goToLast: {
+					enter: () => {
+
+						// remember who we're following
+						targetNdx = globals.congaLine.length - 1;
+						// add ourselves to the conga line
+						globals.congaLine.push( gameObject );
+						skinInstance.setAnimation( 'Walk' );
+
+					},
+					update: () => {
+
+						addHistory();
+						// walk to the oldest point in the history
+						const targetPos = targetHistory[ 0 ];
+						const maxVelocity = globals.moveSpeed * globals.deltaTime;
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						const distance = aimTowardAndGetDistance( transform, targetPos, deltaTurnSpeed );
+						const velocity = distance;
+						transform.translateOnAxis( kForward, Math.min( velocity, maxVelocity ) );
+						if ( distance <= maxVelocity ) {
+
+							this.fsm.transition( 'follow' );
+
+						}
+
+					},
+				},
+				follow: {
+					update: () => {
+
+						addHistory();
+						// remove the oldest history and just put ourselves there.
+						const targetPos = targetHistory.shift();
+						transform.position.copy( targetPos );
+						const deltaTurnSpeed = maxTurnSpeed * globals.deltaTime;
+						aimTowardAndGetDistance( transform, targetHistory[ 0 ], deltaTurnSpeed );
+
+					},
+				},
+			}, 'idle' );
+
+		}
+		update() {
+
+			this.fsm.update();
+			const dir = THREE.MathUtils.radToDeg( this.gameObject.transform.rotation.y );
+			this.helper.setState( `${this.fsm.state}:${dir.toFixed( 0 )}` );
+
+		}
+
+	}
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( camera, 'camera' );
+			globals.cameraInfo = gameObject.addComponent( CameraInfo );
+
+		}
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( scene, 'player' );
+			globals.player = gameObject.addComponent( Player );
+			globals.congaLine = [ gameObject ];
+
+		}
+
+		const animalModelNames = [
+			'pig',
+			'cow',
+			'llama',
+			'pug',
+			'sheep',
+			'zebra',
+			'horse',
+		];
+		animalModelNames.forEach( ( name, ndx ) => {
+
+			const gameObject = gameObjectManager.createGameObject( scene, name );
+			gameObject.addComponent( Animal, models[ name ] );
+			gameObject.transform.position.x = ( ndx + 1 ) * 5;
+
+		} );
+
+	}
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		// convert to seconds
+		globals.time = now * 0.001;
+		// make sure delta time isn't too big.
+		globals.deltaTime = Math.min( globals.time - then, 1 / 20 );
+		then = globals.time;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		gameObjectManager.update();
+		inputManager.update();
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 348 - 241
manual/examples/game-just-player.html

@@ -94,267 +94,374 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 40, 80);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    Object.values(models).forEach(model => {
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-        // Should really fix this in .blend file
-        if (clip.name === 'Walk') {
-          clip.duration /= 2;
-        }
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function removeArrayElement(array, element) {
-    const ndx = array.indexOf(element);
-    if (ndx >= 0) {
-      array.splice(ndx, 1);
-    }
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 40, 80 );
 
 
-  class SafeArray {
-    constructor() {
-      this.array = [];
-      this.addQueue = [];
-      this.removeQueue = new Set();
-    }
-    get isEmpty() {
-      return this.addQueue.length + this.array.length > 0;
-    }
-    add(element) {
-      this.addQueue.push(element);
-    }
-    remove(element) {
-      this.removeQueue.add(element);
-    }
-    forEach(fn) {
-      this._addQueued();
-      this._removeQueued();
-      for (const element of this.array) {
-        if (this.removeQueue.has(element)) {
-          continue;
-        }
-        fn(element);
-      }
-      this._removeQueued();
-    }
-    _addQueued() {
-      if (this.addQueue.length) {
-        this.array.splice(this.array.length, 0, ...this.addQueue);
-        this.addQueue = [];
-      }
-    }
-    _removeQueued() {
-      if (this.removeQueue.size) {
-        this.array = this.array.filter(element => !this.removeQueue.has(element));
-        this.removeQueue.clear();
-      }
-    }
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  class GameObjectManager {
-    constructor() {
-      this.gameObjects = new SafeArray();
-    }
-    createGameObject(parent, name) {
-      const gameObject = new GameObject(parent, name);
-      this.gameObjects.add(gameObject);
-      return gameObject;
-    }
-    removeGameObject(gameObject) {
-      this.gameObjects.remove(gameObject);
-    }
-    update() {
-      this.gameObjects.forEach(gameObject => gameObject.update());
-    }
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-  const globals = {
-    time: 0,
-    deltaTime: 0,
-  };
-  const gameObjectManager = new GameObjectManager();
-
-  class GameObject {
-    constructor(parent, name) {
-      this.name = name;
-      this.components = [];
-      this.transform = new THREE.Object3D();
-      parent.add(this.transform);
-    }
-    addComponent(ComponentType, ...args) {
-      const component = new ComponentType(this, ...args);
-      this.components.push(component);
-      return component;
-    }
-    removeComponent(component) {
-      removeArrayElement(this.components, component);
-    }
-    getComponent(ComponentType) {
-      return this.components.find(c => c instanceof ComponentType);
-    }
-    update() {
-      for (const component of this.components) {
-        component.update();
-      }
-    }
-  }
+	function addLight( ...pos ) {
 
 
-  // Base for all components
-  class Component {
-    constructor(gameObject) {
-      this.gameObject = gameObject;
-    }
-    update() {
-    }
-  }
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
 
 
-  class SkinInstance extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.model = model;
-      this.animRoot = SkeletonUtils.clone(this.model.gltf.scene);
-      this.mixer = new THREE.AnimationMixer(this.animRoot);
-      gameObject.transform.add(this.animRoot);
-      this.actions = {};
-    }
-    setAnimation(animName) {
-      const clip = this.model.animations[animName];
-      // turn off all current actions
-      for (const action of Object.values(this.actions)) {
-        action.enabled = false;
-      }
-      // get or create existing action for clip
-      const action = this.mixer.clipAction(clip);
-      action.enabled = true;
-      action.reset();
-      action.play();
-      this.actions[animName] = action;
-    }
-    update() {
-      this.mixer.update(globals.deltaTime);
-    }
-  }
+	}
 
 
-  class Player extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      const model = models.knight;
-      this.skinInstance = gameObject.addComponent(SkinInstance, model);
-      this.skinInstance.setAnimation('Run');
-    }
-  }
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
 
 
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
 
 
-    prepModelsAndAnimations();
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(scene, 'player');
-      gameObject.addComponent(Player);
-    }
-  }
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
 
 
-  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;
-  }
+	};
 
 
-  let then = 0;
-  function render(now) {
-    // convert to seconds
-    globals.time = now * 0.001;
-    // make sure delta time isn't too big.
-    globals.deltaTime = Math.min(globals.time - then, 1 / 20);
-    then = globals.time;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
 
 
-    gameObjectManager.update();
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
 
 
-    renderer.render(scene, camera);
+			gltfLoader.load( model.url, ( gltf ) => {
 
 
-    requestAnimationFrame(render);
-  }
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		Object.values( models ).forEach( model => {
+
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+				// Should really fix this in .blend file
+				if ( clip.name === 'Walk' ) {
+
+					clip.duration /= 2;
+
+				}
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	function removeArrayElement( array, element ) {
+
+		const ndx = array.indexOf( element );
+		if ( ndx >= 0 ) {
+
+			array.splice( ndx, 1 );
+
+		}
+
+	}
+
+	class SafeArray {
+
+		constructor() {
+
+			this.array = [];
+			this.addQueue = [];
+			this.removeQueue = new Set();
+
+		}
+		get isEmpty() {
+
+			return this.addQueue.length + this.array.length > 0;
+
+		}
+		add( element ) {
+
+			this.addQueue.push( element );
+
+		}
+		remove( element ) {
+
+			this.removeQueue.add( element );
+
+		}
+		forEach( fn ) {
+
+			this._addQueued();
+			this._removeQueued();
+			for ( const element of this.array ) {
+
+				if ( this.removeQueue.has( element ) ) {
+
+					continue;
+
+				}
+
+				fn( element );
+
+			}
+
+			this._removeQueued();
+
+		}
+		_addQueued() {
+
+			if ( this.addQueue.length ) {
+
+				this.array.splice( this.array.length, 0, ...this.addQueue );
+				this.addQueue = [];
+
+			}
+
+		}
+		_removeQueued() {
+
+			if ( this.removeQueue.size ) {
+
+				this.array = this.array.filter( element => ! this.removeQueue.has( element ) );
+				this.removeQueue.clear();
+
+			}
+
+		}
+
+	}
+
+	class GameObjectManager {
+
+		constructor() {
+
+			this.gameObjects = new SafeArray();
+
+		}
+		createGameObject( parent, name ) {
+
+			const gameObject = new GameObject( parent, name );
+			this.gameObjects.add( gameObject );
+			return gameObject;
+
+		}
+		removeGameObject( gameObject ) {
+
+			this.gameObjects.remove( gameObject );
+
+		}
+		update() {
+
+			this.gameObjects.forEach( gameObject => gameObject.update() );
+
+		}
+
+	}
+
+	const globals = {
+		time: 0,
+		deltaTime: 0,
+	};
+	const gameObjectManager = new GameObjectManager();
+
+	class GameObject {
+
+		constructor( parent, name ) {
+
+			this.name = name;
+			this.components = [];
+			this.transform = new THREE.Object3D();
+			parent.add( this.transform );
+
+		}
+		addComponent( ComponentType, ...args ) {
+
+			const component = new ComponentType( this, ...args );
+			this.components.push( component );
+			return component;
+
+		}
+		removeComponent( component ) {
+
+			removeArrayElement( this.components, component );
+
+		}
+		getComponent( ComponentType ) {
+
+			return this.components.find( c => c instanceof ComponentType );
+
+		}
+		update() {
+
+			for ( const component of this.components ) {
+
+				component.update();
+
+			}
+
+		}
+
+	}
+
+	// Base for all components
+	class Component {
+
+		constructor( gameObject ) {
+
+			this.gameObject = gameObject;
+
+		}
+		update() {
+		}
+
+	}
+
+	class SkinInstance extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.model = model;
+			this.animRoot = SkeletonUtils.clone( this.model.gltf.scene );
+			this.mixer = new THREE.AnimationMixer( this.animRoot );
+			gameObject.transform.add( this.animRoot );
+			this.actions = {};
+
+		}
+		setAnimation( animName ) {
+
+			const clip = this.model.animations[ animName ];
+			// turn off all current actions
+			for ( const action of Object.values( this.actions ) ) {
+
+				action.enabled = false;
+
+			}
+
+			// get or create existing action for clip
+			const action = this.mixer.clipAction( clip );
+			action.enabled = true;
+			action.reset();
+			action.play();
+			this.actions[ animName ] = action;
+
+		}
+		update() {
+
+			this.mixer.update( globals.deltaTime );
+
+		}
+
+	}
+
+	class Player extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			const model = models.knight;
+			this.skinInstance = gameObject.addComponent( SkinInstance, model );
+			this.skinInstance.setAnimation( 'Run' );
+
+		}
+
+	}
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( scene, 'player' );
+			gameObject.addComponent( Player );
+
+		}
+
+	}
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		// convert to seconds
+		globals.time = now * 0.001;
+		// make sure delta time isn't too big.
+		globals.deltaTime = Math.min( globals.time - then, 1 / 20 );
+		then = globals.time;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		gameObjectManager.update();
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 146 - 112
manual/examples/game-load-models.html

@@ -88,134 +88,168 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 20, 40);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    Object.values(models).forEach(model => {
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  const mixers = [];
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 20, 40 );
 
 
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-    prepModelsAndAnimations();
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-    Object.values(models).forEach((model, ndx) => {
-      const clonedScene = SkeletonUtils.clone(model.gltf.scene);
-      const root = new THREE.Object3D();
-      root.add(clonedScene);
-      scene.add(root);
-      root.position.x = (ndx - 3) * 3;
+	function addLight( ...pos ) {
 
 
-      const mixer = new THREE.AnimationMixer(clonedScene);
-      const firstClip = Object.values(model.animations)[0];
-      const action = mixer.clipAction(firstClip);
-      action.play();
-      mixers.push(mixer);
-    });
-  }
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
 
 
-  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;
-  }
+	}
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const deltaTime = now - then;
-    then = now;
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
 
 
-    for (const mixer of mixers) {
-      mixer.update(deltaTime);
-    }
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
 
 
-    renderer.render(scene, camera);
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
 
 
-    requestAnimationFrame(render);
-  }
+	};
+
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
+
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
+
+			gltfLoader.load( model.url, ( gltf ) => {
+
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		Object.values( models ).forEach( model => {
+
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	const mixers = [];
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		Object.values( models ).forEach( ( model, ndx ) => {
+
+			const clonedScene = SkeletonUtils.clone( model.gltf.scene );
+			const root = new THREE.Object3D();
+			root.add( clonedScene );
+			scene.add( root );
+			root.position.x = ( ndx - 3 ) * 3;
+
+			const mixer = new THREE.AnimationMixer( clonedScene );
+			const firstClip = Object.values( model.animations )[ 0 ];
+			const action = mixer.clipAction( firstClip );
+			action.play();
+			mixers.push( mixer );
+
+		} );
+
+	}
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const deltaTime = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		for ( const mixer of mixers ) {
+
+			mixer.update( deltaTime );
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 557 - 396
manual/examples/game-player-input.html

@@ -134,429 +134,590 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 40, 80);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  function addLight(...pos) {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(...pos);
-    scene.add(light);
-    scene.add(light.target);
-  }
-  addLight(5, 5, 2);
-  addLight(-5, 5, 5);
-
-  const manager = new THREE.LoadingManager();
-  manager.onLoad = init;
-
-  const progressbarElem = document.querySelector('#progressbar');
-  manager.onProgress = (url, itemsLoaded, itemsTotal) => {
-    progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
-  };
-
-  const models = {
-    pig:    { url: 'resources/models/animals/Pig.gltf' },
-    cow:    { url: 'resources/models/animals/Cow.gltf' },
-    llama:  { url: 'resources/models/animals/Llama.gltf' },
-    pug:    { url: 'resources/models/animals/Pug.gltf' },
-    sheep:  { url: 'resources/models/animals/Sheep.gltf' },
-    zebra:  { url: 'resources/models/animals/Zebra.gltf' },
-    horse:  { url: 'resources/models/animals/Horse.gltf' },
-    knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
-  };
-  {
-    const gltfLoader = new GLTFLoader(manager);
-    for (const model of Object.values(models)) {
-      gltfLoader.load(model.url, (gltf) => {
-        model.gltf = gltf;
-      });
-    }
-  }
 
 
-  function prepModelsAndAnimations() {
-    Object.values(models).forEach(model => {
-      const animsByName = {};
-      model.gltf.animations.forEach((clip) => {
-        animsByName[clip.name] = clip;
-        // Should really fix this in .blend file
-        if (clip.name === 'Walk') {
-          clip.duration /= 2;
-        }
-      });
-      model.animations = animsByName;
-    });
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  // Keeps the state of keys/buttons
-  //
-  // You can check
-  //
-  //   inputManager.keys.left.down
-  //
-  // to see if the left key is currently held down
-  // and you can check
-  //
-  //   inputManager.keys.left.justPressed
-  //
-  // To see if the left key was pressed this frame
-  //
-  // Keys are 'left', 'right', 'a', 'b', 'up', 'down'
-  class InputManager {
-    constructor() {
-      this.keys = {};
-      const keyMap = new Map();
-
-      const setKey = (keyName, pressed) => {
-        const keyState = this.keys[keyName];
-        keyState.justPressed = pressed && !keyState.down;
-        keyState.down = pressed;
-      };
-
-      const addKey = (keyCode, name) => {
-        this.keys[name] = { down: false, justPressed: false };
-        keyMap.set(keyCode, name);
-      };
-
-      const setKeyFromKeyCode = (keyCode, pressed) => {
-        const keyName = keyMap.get(keyCode);
-        if (!keyName) {
-          return;
-        }
-        setKey(keyName, pressed);
-      };
-
-      addKey(37, 'left');
-      addKey(39, 'right');
-      addKey(38, 'up');
-      addKey(40, 'down');
-      addKey(90, 'a');
-      addKey(88, 'b');
-
-      window.addEventListener('keydown', (e) => {
-        setKeyFromKeyCode(e.keyCode, true);
-      });
-      window.addEventListener('keyup', (e) => {
-        setKeyFromKeyCode(e.keyCode, false);
-      });
-
-      const sides = [
-        { elem: document.querySelector('#left'),  key: 'left'  },
-        { elem: document.querySelector('#right'), key: 'right' },
-      ];
-
-      // note: not a good design?
-      // The last direction the user presses should take
-      // precedence. Example: User presses L, without letting go of
-      // L user presses R. Input should now be R. User lets off R
-      // Input should now be L.
-      // With this code if user pressed both L and R result is nothing
-
-      const clearKeys = () => {
-        for (const {key} of sides) {
-            setKey(key, false);
-        }
-      };
-
-      const handleMouseMove = (e) => {
-        e.preventDefault();
-        // this is needed because we call preventDefault();
-        // we also gave the canvas a tabindex so it can
-        // become the focus
-        canvas.focus();
-        window.addEventListener('pointermove', handleMouseMove);
-        window.addEventListener('pointerup', handleMouseUp);
-
-        for (const {elem, key} of sides) {
-          let pressed = false;
-          const rect = elem.getBoundingClientRect();
-          const x = e.clientX;
-          const y = e.clientY;
-          const inRect = x >= rect.left && x < rect.right &&
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 40, 80 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
+
+	function addLight( ...pos ) {
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( ...pos );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	addLight( 5, 5, 2 );
+	addLight( - 5, 5, 5 );
+
+	const manager = new THREE.LoadingManager();
+	manager.onLoad = init;
+
+	const progressbarElem = document.querySelector( '#progressbar' );
+	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {
+
+		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;
+
+	};
+
+	const models = {
+		pig: { url: 'resources/models/animals/Pig.gltf' },
+		cow: { url: 'resources/models/animals/Cow.gltf' },
+		llama: { url: 'resources/models/animals/Llama.gltf' },
+		pug: { url: 'resources/models/animals/Pug.gltf' },
+		sheep: { url: 'resources/models/animals/Sheep.gltf' },
+		zebra: { url: 'resources/models/animals/Zebra.gltf' },
+		horse: { url: 'resources/models/animals/Horse.gltf' },
+		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
+	};
+	{
+
+		const gltfLoader = new GLTFLoader( manager );
+		for ( const model of Object.values( models ) ) {
+
+			gltfLoader.load( model.url, ( gltf ) => {
+
+				model.gltf = gltf;
+
+			} );
+
+		}
+
+	}
+
+	function prepModelsAndAnimations() {
+
+		Object.values( models ).forEach( model => {
+
+			const animsByName = {};
+			model.gltf.animations.forEach( ( clip ) => {
+
+				animsByName[ clip.name ] = clip;
+				// Should really fix this in .blend file
+				if ( clip.name === 'Walk' ) {
+
+					clip.duration /= 2;
+
+				}
+
+			} );
+			model.animations = animsByName;
+
+		} );
+
+	}
+
+	// Keeps the state of keys/buttons
+	//
+	// You can check
+	//
+	//   inputManager.keys.left.down
+	//
+	// to see if the left key is currently held down
+	// and you can check
+	//
+	//   inputManager.keys.left.justPressed
+	//
+	// To see if the left key was pressed this frame
+	//
+	// Keys are 'left', 'right', 'a', 'b', 'up', 'down'
+	class InputManager {
+
+		constructor() {
+
+			this.keys = {};
+			const keyMap = new Map();
+
+			const setKey = ( keyName, pressed ) => {
+
+				const keyState = this.keys[ keyName ];
+				keyState.justPressed = pressed && ! keyState.down;
+				keyState.down = pressed;
+
+			};
+
+			const addKey = ( keyCode, name ) => {
+
+				this.keys[ name ] = { down: false, justPressed: false };
+				keyMap.set( keyCode, name );
+
+			};
+
+			const setKeyFromKeyCode = ( keyCode, pressed ) => {
+
+				const keyName = keyMap.get( keyCode );
+				if ( ! keyName ) {
+
+					return;
+
+				}
+
+				setKey( keyName, pressed );
+
+			};
+
+			addKey( 37, 'left' );
+			addKey( 39, 'right' );
+			addKey( 38, 'up' );
+			addKey( 40, 'down' );
+			addKey( 90, 'a' );
+			addKey( 88, 'b' );
+
+			window.addEventListener( 'keydown', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, true );
+
+			} );
+			window.addEventListener( 'keyup', ( e ) => {
+
+				setKeyFromKeyCode( e.keyCode, false );
+
+			} );
+
+			const sides = [
+				{ elem: document.querySelector( '#left' ), key: 'left' },
+				{ elem: document.querySelector( '#right' ), key: 'right' },
+			];
+
+			// note: not a good design?
+			// The last direction the user presses should take
+			// precedence. Example: User presses L, without letting go of
+			// L user presses R. Input should now be R. User lets off R
+			// Input should now be L.
+			// With this code if user pressed both L and R result is nothing
+
+			const clearKeys = () => {
+
+				for ( const { key } of sides ) {
+
+					setKey( key, false );
+
+				}
+
+			};
+
+			const handleMouseMove = ( e ) => {
+
+				e.preventDefault();
+				// this is needed because we call preventDefault();
+				// we also gave the canvas a tabindex so it can
+				// become the focus
+				canvas.focus();
+				window.addEventListener( 'pointermove', handleMouseMove );
+				window.addEventListener( 'pointerup', handleMouseUp );
+
+				for ( const { elem, key } of sides ) {
+
+					let pressed = false;
+					const rect = elem.getBoundingClientRect();
+					const x = e.clientX;
+					const y = e.clientY;
+					const inRect = x >= rect.left && x < rect.right &&
                          y >= rect.top && y < rect.bottom;
                          y >= rect.top && y < rect.bottom;
-          if (inRect) {
-            pressed = true;
-          }
-          setKey(key, pressed);
-        }
-      };
+					if ( inRect ) {
 
 
-      function handleMouseUp() {
-        clearKeys();
-        window.removeEventListener('pointermove', handleMouseMove, {passive: false});
-        window.removeEventListener('pointerup', handleMouseUp);
-      }
+						pressed = true;
 
 
-      const uiElem = document.querySelector('#ui');
-      uiElem.addEventListener('pointerdown', handleMouseMove, {passive: false});
+					}
 
 
-      uiElem.addEventListener('touchstart', (e) => {
-        // prevent scrolling
-        e.preventDefault();
-      }, {passive: false});
-    }
-    update() {
-      for (const keyState of Object.values(this.keys)) {
-        if (keyState.justPressed) {
-          keyState.justPressed = false;
-        }
-      }
-    }
-  }
+					setKey( key, pressed );
 
 
-  function removeArrayElement(array, element) {
-    const ndx = array.indexOf(element);
-    if (ndx >= 0) {
-      array.splice(ndx, 1);
-    }
-  }
+				}
 
 
-  class SafeArray {
-    constructor() {
-      this.array = [];
-      this.addQueue = [];
-      this.removeQueue = new Set();
-    }
-    get isEmpty() {
-      return this.addQueue.length + this.array.length > 0;
-    }
-    add(element) {
-      this.addQueue.push(element);
-    }
-    remove(element) {
-      this.removeQueue.add(element);
-    }
-    forEach(fn) {
-      this._addQueued();
-      this._removeQueued();
-      for (const element of this.array) {
-        if (this.removeQueue.has(element)) {
-          continue;
-        }
-        fn(element);
-      }
-      this._removeQueued();
-    }
-    _addQueued() {
-      if (this.addQueue.length) {
-        this.array.splice(this.array.length, 0, ...this.addQueue);
-        this.addQueue = [];
-      }
-    }
-    _removeQueued() {
-      if (this.removeQueue.size) {
-        this.array = this.array.filter(element => !this.removeQueue.has(element));
-        this.removeQueue.clear();
-      }
-    }
-  }
+			};
 
 
-  class GameObjectManager {
-    constructor() {
-      this.gameObjects = new SafeArray();
-    }
-    createGameObject(parent, name) {
-      const gameObject = new GameObject(parent, name);
-      this.gameObjects.add(gameObject);
-      return gameObject;
-    }
-    removeGameObject(gameObject) {
-      this.gameObjects.remove(gameObject);
-    }
-    update() {
-      this.gameObjects.forEach(gameObject => gameObject.update());
-    }
-  }
+			function handleMouseUp() {
 
 
-  const kForward = new THREE.Vector3(0, 0, 1);
-  const globals = {
-    time: 0,
-    deltaTime: 0,
-    moveSpeed: 16,
-    camera,
-  };
-  const gameObjectManager = new GameObjectManager();
-  const inputManager = new InputManager();
-
-  class GameObject {
-    constructor(parent, name) {
-      this.name = name;
-      this.components = [];
-      this.transform = new THREE.Object3D();
-      parent.add(this.transform);
-    }
-    addComponent(ComponentType, ...args) {
-      const component = new ComponentType(this, ...args);
-      this.components.push(component);
-      return component;
-    }
-    removeComponent(component) {
-      removeArrayElement(this.components, component);
-    }
-    getComponent(ComponentType) {
-      return this.components.find(c => c instanceof ComponentType);
-    }
-    update() {
-      for (const component of this.components) {
-        component.update();
-      }
-    }
-  }
+				clearKeys();
+				window.removeEventListener( 'pointermove', handleMouseMove, { passive: false } );
+				window.removeEventListener( 'pointerup', handleMouseUp );
 
 
-  // Base for all components
-  class Component {
-    constructor(gameObject) {
-      this.gameObject = gameObject;
-    }
-    update() {
-    }
-  }
+			}
 
 
-  class CameraInfo extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      this.projScreenMatrix = new THREE.Matrix4();
-      this.frustum = new THREE.Frustum();
-    }
-    update() {
-      const {camera} = globals;
-      this.projScreenMatrix.multiplyMatrices(
-          camera.projectionMatrix,
-          camera.matrixWorldInverse);
-      this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
-    }
-  }
+			const uiElem = document.querySelector( '#ui' );
+			uiElem.addEventListener( 'pointerdown', handleMouseMove, { passive: false } );
 
 
-  class SkinInstance extends Component {
-    constructor(gameObject, model) {
-      super(gameObject);
-      this.model = model;
-      this.animRoot = SkeletonUtils.clone(this.model.gltf.scene);
-      this.mixer = new THREE.AnimationMixer(this.animRoot);
-      gameObject.transform.add(this.animRoot);
-      this.actions = {};
-    }
-    setAnimation(animName) {
-      const clip = this.model.animations[animName];
-      // turn off all current actions
-      for (const action of Object.values(this.actions)) {
-        action.enabled = false;
-      }
-      // get or create existing action for clip
-      const action = this.mixer.clipAction(clip);
-      action.enabled = true;
-      action.reset();
-      action.play();
-      this.actions[animName] = action;
-    }
-    update() {
-      this.mixer.update(globals.deltaTime);
-    }
-  }
+			uiElem.addEventListener( 'touchstart', ( e ) => {
 
 
-  class Player extends Component {
-    constructor(gameObject) {
-      super(gameObject);
-      const model = models.knight;
-      this.skinInstance = gameObject.addComponent(SkinInstance, model);
-      this.skinInstance.setAnimation('Run');
-      this.turnSpeed = globals.moveSpeed / 4;
-      this.offscreenTimer = 0;
-      this.maxTimeOffScreen = 3;
-    }
-    update() {
-      const {deltaTime, moveSpeed} = globals;
-      const {transform} = this.gameObject;
-      const delta = (inputManager.keys.left.down  ?  1 : 0) +
-                    (inputManager.keys.right.down ? -1 : 0);
-      transform.rotation.y += this.turnSpeed * delta * deltaTime;
-      transform.translateOnAxis(kForward, moveSpeed * deltaTime);
-
-      const {frustum} = globals.cameraInfo;
-      if (frustum.containsPoint(transform.position)) {
-        this.offscreenTimer = 0;
-      } else {
-        this.offscreenTimer += deltaTime;
-        if (this.offscreenTimer >= this.maxTimeOffScreen) {
-          transform.position.set(0, 0, 0);
-        }
-      }
-    }
-  }
+				// prevent scrolling
+				e.preventDefault();
 
 
-  function init() {
-    // hide the loading bar
-    const loadingElem = document.querySelector('#loading');
-    loadingElem.style.display = 'none';
+			}, { passive: false } );
 
 
-    prepModelsAndAnimations();
+		}
+		update() {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(camera, 'camera');
-      globals.cameraInfo = gameObject.addComponent(CameraInfo);
-    }
+			for ( const keyState of Object.values( this.keys ) ) {
 
 
-    {
-      const gameObject = gameObjectManager.createGameObject(scene, 'player');
-      gameObject.addComponent(Player);
-    }
-  }
+				if ( keyState.justPressed ) {
 
 
-  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;
-  }
+					keyState.justPressed = false;
 
 
-  let then = 0;
-  function render(now) {
-    // convert to seconds
-    globals.time = now * 0.001;
-    // make sure delta time isn't too big.
-    globals.deltaTime = Math.min(globals.time - then, 1 / 20);
-    then = globals.time;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+				}
 
 
-    gameObjectManager.update();
-    inputManager.update();
+			}
 
 
-    renderer.render(scene, camera);
+		}
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	function removeArrayElement( array, element ) {
+
+		const ndx = array.indexOf( element );
+		if ( ndx >= 0 ) {
+
+			array.splice( ndx, 1 );
+
+		}
+
+	}
+
+	class SafeArray {
+
+		constructor() {
+
+			this.array = [];
+			this.addQueue = [];
+			this.removeQueue = new Set();
+
+		}
+		get isEmpty() {
+
+			return this.addQueue.length + this.array.length > 0;
+
+		}
+		add( element ) {
+
+			this.addQueue.push( element );
+
+		}
+		remove( element ) {
+
+			this.removeQueue.add( element );
+
+		}
+		forEach( fn ) {
+
+			this._addQueued();
+			this._removeQueued();
+			for ( const element of this.array ) {
+
+				if ( this.removeQueue.has( element ) ) {
+
+					continue;
+
+				}
+
+				fn( element );
+
+			}
+
+			this._removeQueued();
+
+		}
+		_addQueued() {
+
+			if ( this.addQueue.length ) {
+
+				this.array.splice( this.array.length, 0, ...this.addQueue );
+				this.addQueue = [];
+
+			}
+
+		}
+		_removeQueued() {
+
+			if ( this.removeQueue.size ) {
+
+				this.array = this.array.filter( element => ! this.removeQueue.has( element ) );
+				this.removeQueue.clear();
+
+			}
+
+		}
+
+	}
+
+	class GameObjectManager {
+
+		constructor() {
+
+			this.gameObjects = new SafeArray();
+
+		}
+		createGameObject( parent, name ) {
+
+			const gameObject = new GameObject( parent, name );
+			this.gameObjects.add( gameObject );
+			return gameObject;
+
+		}
+		removeGameObject( gameObject ) {
+
+			this.gameObjects.remove( gameObject );
+
+		}
+		update() {
+
+			this.gameObjects.forEach( gameObject => gameObject.update() );
+
+		}
+
+	}
+
+	const kForward = new THREE.Vector3( 0, 0, 1 );
+	const globals = {
+		time: 0,
+		deltaTime: 0,
+		moveSpeed: 16,
+		camera,
+	};
+	const gameObjectManager = new GameObjectManager();
+	const inputManager = new InputManager();
+
+	class GameObject {
+
+		constructor( parent, name ) {
+
+			this.name = name;
+			this.components = [];
+			this.transform = new THREE.Object3D();
+			parent.add( this.transform );
+
+		}
+		addComponent( ComponentType, ...args ) {
+
+			const component = new ComponentType( this, ...args );
+			this.components.push( component );
+			return component;
+
+		}
+		removeComponent( component ) {
+
+			removeArrayElement( this.components, component );
+
+		}
+		getComponent( ComponentType ) {
+
+			return this.components.find( c => c instanceof ComponentType );
+
+		}
+		update() {
+
+			for ( const component of this.components ) {
+
+				component.update();
+
+			}
+
+		}
+
+	}
+
+	// Base for all components
+	class Component {
+
+		constructor( gameObject ) {
+
+			this.gameObject = gameObject;
+
+		}
+		update() {
+		}
+
+	}
+
+	class CameraInfo extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			this.projScreenMatrix = new THREE.Matrix4();
+			this.frustum = new THREE.Frustum();
+
+		}
+		update() {
+
+			const { camera } = globals;
+			this.projScreenMatrix.multiplyMatrices(
+				camera.projectionMatrix,
+				camera.matrixWorldInverse );
+			this.frustum.setFromProjectionMatrix( this.projScreenMatrix );
+
+		}
+
+	}
+
+	class SkinInstance extends Component {
+
+		constructor( gameObject, model ) {
+
+			super( gameObject );
+			this.model = model;
+			this.animRoot = SkeletonUtils.clone( this.model.gltf.scene );
+			this.mixer = new THREE.AnimationMixer( this.animRoot );
+			gameObject.transform.add( this.animRoot );
+			this.actions = {};
+
+		}
+		setAnimation( animName ) {
+
+			const clip = this.model.animations[ animName ];
+			// turn off all current actions
+			for ( const action of Object.values( this.actions ) ) {
+
+				action.enabled = false;
+
+			}
+
+			// get or create existing action for clip
+			const action = this.mixer.clipAction( clip );
+			action.enabled = true;
+			action.reset();
+			action.play();
+			this.actions[ animName ] = action;
+
+		}
+		update() {
+
+			this.mixer.update( globals.deltaTime );
+
+		}
+
+	}
+
+	class Player extends Component {
+
+		constructor( gameObject ) {
+
+			super( gameObject );
+			const model = models.knight;
+			this.skinInstance = gameObject.addComponent( SkinInstance, model );
+			this.skinInstance.setAnimation( 'Run' );
+			this.turnSpeed = globals.moveSpeed / 4;
+			this.offscreenTimer = 0;
+			this.maxTimeOffScreen = 3;
+
+		}
+		update() {
+
+			const { deltaTime, moveSpeed } = globals;
+			const { transform } = this.gameObject;
+			const delta = ( inputManager.keys.left.down ? 1 : 0 ) +
+                    ( inputManager.keys.right.down ? - 1 : 0 );
+			transform.rotation.y += this.turnSpeed * delta * deltaTime;
+			transform.translateOnAxis( kForward, moveSpeed * deltaTime );
+
+			const { frustum } = globals.cameraInfo;
+			if ( frustum.containsPoint( transform.position ) ) {
+
+				this.offscreenTimer = 0;
+
+			} else {
+
+				this.offscreenTimer += deltaTime;
+				if ( this.offscreenTimer >= this.maxTimeOffScreen ) {
+
+					transform.position.set( 0, 0, 0 );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	function init() {
+
+		// hide the loading bar
+		const loadingElem = document.querySelector( '#loading' );
+		loadingElem.style.display = 'none';
+
+		prepModelsAndAnimations();
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( camera, 'camera' );
+			globals.cameraInfo = gameObject.addComponent( CameraInfo );
+
+		}
+
+		{
+
+			const gameObject = gameObjectManager.createGameObject( scene, 'player' );
+			gameObject.addComponent( Player );
+
+		}
+
+	}
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		// convert to seconds
+		globals.time = now * 0.001;
+		// make sure delta time isn't too big.
+		globals.deltaTime = Math.min( globals.time - then, 1 / 20 );
+		then = globals.time;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		gameObjectManager.update();
+		inputManager.update();
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 91 - 63
manual/examples/gpw-data-viewer.html

@@ -14,79 +14,107 @@ canvas {
 </body>
 </body>
 <script type="module">
 <script type="module">
 
 
-async function loadFile(url) {
-  const req = await fetch(url);
-  return req.text();
+async function loadFile( url ) {
+
+	const req = await fetch( url );
+	return req.text();
+
 }
 }
 
 
-function parseData(text) {
-  const data = [];
-  const settings = {data};
-  let max;
-  let min;
-  // split into lines
-  text.split('\n').forEach((line) => {
-    // split the line by whitespace
-    const parts = line.trim().split(/\s+/);
-    if (parts.length === 2) {
-      // only 2 parts, must be a key/value pair
-      settings[parts[0]] = parseFloat(parts[1]);
-    } else if (parts.length > 2) {
-      // more than 2 parts, must be data
-      const values = parts.map((v) => {
-        const value = parseFloat(v);
-        if (value === settings.NODATA_value) {
-          return undefined;
-        }
-        max = Math.max(max === undefined ? value : max, value);
-        min = Math.min(min === undefined ? value : min, value);
-        return value;
-      });
-      data.push(values);
-    }
-  });
-  return Object.assign(settings, {min, max});
+function parseData( text ) {
+
+	const data = [];
+	const settings = { data };
+	let max;
+	let min;
+	// split into lines
+	text.split( '\n' ).forEach( ( line ) => {
+
+		// split the line by whitespace
+		const parts = line.trim().split( /\s+/ );
+		if ( parts.length === 2 ) {
+
+			// only 2 parts, must be a key/value pair
+			settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
+
+		} else if ( parts.length > 2 ) {
+
+			// more than 2 parts, must be data
+			const values = parts.map( ( v ) => {
+
+				const value = parseFloat( v );
+				if ( value === settings.NODATA_value ) {
+
+					return undefined;
+
+				}
+
+				max = Math.max( max === undefined ? value : max, value );
+				min = Math.min( min === undefined ? value : min, value );
+				return value;
+
+			} );
+			data.push( values );
+
+		}
+
+	} );
+	return Object.assign( settings, { min, max } );
+
 }
 }
 
 
-function drawData(file) {
-  const {min, max, ncols, nrows, data} = file;
-  const range = max - min;
-  const ctx = document.querySelector('canvas').getContext('2d');
-  // make the canvas the same size as the data
-  ctx.canvas.width = ncols;
-  ctx.canvas.height = nrows;
-  // but display it double size so it's not too small
-  ctx.canvas.style.width = px(ncols * 2);
-  // fill the canvas to dark gray
-  ctx.fillStyle = '#444';
-  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-  // draw each data point
-  data.forEach((row, latNdx) => {
-    row.forEach((value, lonNdx) => {
-      if (value === undefined) {
-        return;
-      }
-      const amount = (value - min) / range;
-      const hue = 1;
-      const saturation = 1;
-      const lightness = amount;
-      ctx.fillStyle = hsl(hue, saturation, lightness);
-      ctx.fillRect(lonNdx, latNdx, 1, 1);
-    });
-  });
+function drawData( file ) {
+
+	const { min, max, ncols, nrows, data } = file;
+	const range = max - min;
+	const ctx = document.querySelector( 'canvas' ).getContext( '2d' );
+	// make the canvas the same size as the data
+	ctx.canvas.width = ncols;
+	ctx.canvas.height = nrows;
+	// but display it double size so it's not too small
+	ctx.canvas.style.width = px( ncols * 2 );
+	// fill the canvas to dark gray
+	ctx.fillStyle = '#444';
+	ctx.fillRect( 0, 0, ctx.canvas.width, ctx.canvas.height );
+	// draw each data point
+	data.forEach( ( row, latNdx ) => {
+
+		row.forEach( ( value, lonNdx ) => {
+
+			if ( value === undefined ) {
+
+				return;
+
+			}
+
+			const amount = ( value - min ) / range;
+			const hue = 1;
+			const saturation = 1;
+			const lightness = amount;
+			ctx.fillStyle = hsl( hue, saturation, lightness );
+			ctx.fillRect( lonNdx, latNdx, 1, 1 );
+
+		} );
+
+	} );
+
 }
 }
 
 
-function px(v) {
-  return `${v | 0}px`;
+function px( v ) {
+
+	return `${v | 0}px`;
+
 }
 }
 
 
-function hsl(h, s, l) {
-  return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+function hsl( h, s, l ) {
+
+	return `hsl(${h * 360 | 0},${s * 100 | 0}%,${l * 100 | 0}%)`;
+
 }
 }
 
 
-loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
-  .then(parseData)
-  .then(drawData);
+loadFile( 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' )
+	.then( parseData )
+	.then( drawData );
 
 
 </script>
 </script>
 </html>
 </html>

+ 398 - 324
manual/examples/indexed-textures-picking-and-highlighting.html

@@ -73,96 +73,107 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('#246');
-
-  const pickingScene = new THREE.Scene();
-  pickingScene.background = new THREE.Color(0);
-
-  const tempColor = new THREE.Color();
-	function get255BasedColor(color) {
-    tempColor.set(color);
-    const base = tempColor.toArray().map(v => v * 255);
-    base.push(255); // alpha
-    return base;
-  }
 
 
-  const maxNumCountries = 512;
-  const paletteTextureWidth = maxNumCountries;
-  const paletteTextureHeight = 1;
-  const palette = new Uint8Array(paletteTextureWidth * 4);
-  const paletteTexture = new THREE.DataTexture(
-      palette, paletteTextureWidth, paletteTextureHeight);
-  paletteTexture.minFilter = THREE.NearestFilter;
-  paletteTexture.magFilter = THREE.NearestFilter;
-
-  const selectedColor = get255BasedColor('red');
-  const unselectedColor = get255BasedColor('#444');
-  const oceanColor = get255BasedColor('rgb(100,200,255)');
-  resetPalette();
-
-  function setPaletteColor(index, color) {
-    palette.set(color, index * 4);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function resetPalette() {
-    // make all colors the unselected color
-    for (let i = 1; i < maxNumCountries; ++i) {
-      setPaletteColor(i, unselectedColor);
-    }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-    // set the ocean color (index #0)
-    setPaletteColor(0, oceanColor);
-    paletteTexture.needsUpdate = true;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( '#246' );
+
+	const pickingScene = new THREE.Scene();
+	pickingScene.background = new THREE.Color( 0 );
+
+	const tempColor = new THREE.Color();
+	function get255BasedColor( color ) {
+
+		tempColor.set( color );
+		const base = tempColor.toArray().map( v => v * 255 );
+		base.push( 255 ); // alpha
+		return base;
+
+	}
+
+	const maxNumCountries = 512;
+	const paletteTextureWidth = maxNumCountries;
+	const paletteTextureHeight = 1;
+	const palette = new Uint8Array( paletteTextureWidth * 4 );
+	const paletteTexture = new THREE.DataTexture( palette, paletteTextureWidth, paletteTextureHeight );
+	paletteTexture.minFilter = THREE.NearestFilter;
+	paletteTexture.magFilter = THREE.NearestFilter;
+	paletteTexture.colorSpace = THREE.SRGBColorSpace;
+
+	const selectedColor = get255BasedColor( 'red' );
+	const unselectedColor = get255BasedColor( '#444' );
+	const oceanColor = get255BasedColor( 'rgb(100,200,255)' );
+	resetPalette();
+
+	function setPaletteColor( index, color ) {
+
+		palette.set( color, index * 4 );
+
+	}
+
+	function resetPalette() {
+
+		// make all colors the unselected color
+		for ( let i = 1; i < maxNumCountries; ++ i ) {
 
 
-  {
-    const loader = new THREE.TextureLoader();
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
+			setPaletteColor( i, unselectedColor );
 
 
-    const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
-    indexTexture.minFilter = THREE.NearestFilter;
-    indexTexture.magFilter = THREE.NearestFilter;
+		}
 
 
-    const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
-    pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
+		// set the ocean color (index #0)
+		setPaletteColor( 0, oceanColor );
+		paletteTexture.needsUpdate = true;
 
 
-    const fragmentShaderReplacements = [
-      {
-        from: '#include <common>',
-        to: `
+	}
+
+	{
+
+		const loader = new THREE.TextureLoader();
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+
+		const indexTexture = loader.load( 'resources/data/world/country-index-texture.png', render );
+		indexTexture.minFilter = THREE.NearestFilter;
+		indexTexture.magFilter = THREE.NearestFilter;
+
+		const pickingMaterial = new THREE.MeshBasicMaterial( { map: indexTexture } );
+		pickingScene.add( new THREE.Mesh( geometry, pickingMaterial ) );
+
+		const fragmentShaderReplacements = [
+			{
+				from: '#include <common>',
+				to: `
           #include <common>
           #include <common>
           uniform sampler2D indexTexture;
           uniform sampler2D indexTexture;
           uniform sampler2D paletteTexture;
           uniform sampler2D paletteTexture;
           uniform float paletteTextureWidth;
           uniform float paletteTextureWidth;
         `,
         `,
-      },
-      {
-        from: '#include <color_fragment>',
-        to: `
+			},
+			{
+				from: '#include <color_fragment>',
+				to: `
           #include <color_fragment>
           #include <color_fragment>
           {
           {
-            vec4 indexColor = texture2D(indexTexture, vUv);
+            vec4 indexColor = texture2D(indexTexture, vMapUv);
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
@@ -170,275 +181,338 @@ function main() {
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
           }
           }
         `,
         `,
-      },
-    ];
-
-    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    material.onBeforeCompile = function(shader) {
-      fragmentShaderReplacements.forEach((rep) => {
-        shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
-      });
-      shader.uniforms.paletteTexture = {value: paletteTexture};
-      shader.uniforms.indexTexture = {value: indexTexture};
-      shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
-    };
-    scene.add(new THREE.Mesh(geometry, material));
-  }
+			},
+		];
 
 
-  async function loadJSON(url) {
-    const req = await fetch(url);
-    return req.json();
-  }
+		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		material.onBeforeCompile = function ( shader ) {
 
 
-  let numCountriesSelected = 0;
-  let countryInfos;
-  async function loadCountryData() {
-    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejs.org: url */
-
-    const lonFudge = Math.PI * 1.5;
-    const latFudge = Math.PI;
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-
-    const labelParentElem = document.querySelector('#labels');
-    for (const countryInfo of countryInfos) {
-      const {lat, lon, min, max, name} = countryInfo;
-
-      // adjust the helpers to point to the latitude and longitude
-      lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
-      latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
-
-      // get the position of the lat/lon
-      positionHelper.updateWorldMatrix(true, false);
-      const position = new THREE.Vector3();
-      positionHelper.getWorldPosition(position);
-      countryInfo.position = position;
-
-      // compute the area for each country
-      const width = max[0] - min[0];
-      const height = max[1] - min[1];
-      const area = width * height;
-      countryInfo.area = area;
-
-      // add an element for each country
-      const elem = document.createElement('div');
-      elem.textContent = name;
-      labelParentElem.appendChild(elem);
-      countryInfo.elem = elem;
-    }
-    requestRenderIfNotRequested();
-  }
-  loadCountryData();
-
-  const tempV = new THREE.Vector3();
-  const cameraToPoint = new THREE.Vector3();
-  const cameraPosition = new THREE.Vector3();
-  const normalMatrix = new THREE.Matrix3();
-
-  const settings = {
-    minArea: 20,
-    maxVisibleDot: -0.2,
-  };
-
-  function updateLabels() {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+			fragmentShaderReplacements.forEach( ( rep ) => {
 
 
-    const large = settings.minArea * settings.minArea;
-    // get a matrix that represents a relative orientation of the camera
-    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
-    // get the camera's position
-    camera.getWorldPosition(cameraPosition);
-    for (const countryInfo of countryInfos) {
-      const {position, elem, area, selected} = countryInfo;
-      const largeEnough = area >= large;
-      const show = selected || (numCountriesSelected === 0 && largeEnough);
-      if (!show) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // Orient the position based on the camera's orientation.
-      // Since the sphere is at the origin and the sphere is a unit sphere
-      // this gives us a camera relative direction vector for the position.
-      tempV.copy(position);
-      tempV.applyMatrix3(normalMatrix);
-
-      // compute the direction to this position from the camera
-      cameraToPoint.copy(position);
-      cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
-
-      // get the dot product of camera relative direction to this position
-      // on the globe with the direction from the camera to that point.
-      // -1 = facing directly towards the camera
-      // 0 = exactly on tangent of the sphere from the camera
-      // > 0 = facing away
-      const dot = tempV.dot(cameraToPoint);
-
-      // if the orientation is not facing us hide it.
-      if (dot > settings.maxVisibleDot) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // restore the element to its default display style
-      elem.style.display = '';
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      tempV.copy(position);
-      tempV.project(camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
-      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-
-      // set the zIndex for sorting
-      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
-    }
-  }
+				shader.fragmentShader = shader.fragmentShader.replace( rep.from, rep.to );
 
 
-  class GPUPickHelper {
-    constructor() {
-      // create a 1x1 pixel render target
-      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
-      this.pixelBuffer = new Uint8Array(4);
-    }
-    pick(cssPosition, scene, camera) {
-      const {pickingTexture, pixelBuffer} = this;
-
-      // set the view offset to represent just a single pixel under the mouse
-      const pixelRatio = renderer.getPixelRatio();
-      camera.setViewOffset(
-          renderer.getContext().drawingBufferWidth,   // full width
-          renderer.getContext().drawingBufferHeight,  // full top
-          cssPosition.x * pixelRatio | 0,             // rect x
-          cssPosition.y * pixelRatio | 0,             // rect y
-          1,                                          // rect width
-          1,                                          // rect height
-      );
-      // render the scene
-      renderer.setRenderTarget(pickingTexture);
-      renderer.render(scene, camera);
-      renderer.setRenderTarget(null);
-      // clear the view offset so rendering returns to normal
-      camera.clearViewOffset();
-      //read the pixel
-      renderer.readRenderTargetPixels(
-          pickingTexture,
-          0,   // x
-          0,   // y
-          1,   // width
-          1,   // height
-          pixelBuffer);
-
-      const id =
-          (pixelBuffer[0] <<  0) |
-          (pixelBuffer[1] <<  8) |
-          (pixelBuffer[2] << 16);
-
-      return id;
-    }
-  }
+			} );
+			shader.uniforms.paletteTexture = { value: paletteTexture };
+			shader.uniforms.indexTexture = { value: indexTexture };
+			shader.uniforms.paletteTextureWidth = { value: paletteTextureWidth };
 
 
-  const pickHelper = new GPUPickHelper();
+		};
 
 
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-  function pickCountry(event) {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+	}
 
 
-    const position = getCanvasRelativePosition(event);
-    const id = pickHelper.pick(position, pickingScene, camera);
-    if (id > 0) {
-      const countryInfo = countryInfos[id - 1];
-      const selected = !countryInfo.selected;
-      if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
-        unselectAllCountries();
-      }
-      numCountriesSelected += selected ? 1 : -1;
-      countryInfo.selected = selected;
-      setPaletteColor(id, selected ? selectedColor : unselectedColor);
-      paletteTexture.needsUpdate = true;
-    } else if (numCountriesSelected) {
-      unselectAllCountries();
-    }
-    requestRenderIfNotRequested();
-  }
+	async function loadJSON( url ) {
 
 
-  function unselectAllCountries() {
-    numCountriesSelected = 0;
-    countryInfos.forEach((countryInfo) => {
-      countryInfo.selected = false;
-    });
-    resetPalette();
-  }
+		const req = await fetch( url );
+		return req.json();
 
 
-  canvas.addEventListener('pointerup', pickCountry);
+	}
 
 
-  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;
-  }
+	let numCountriesSelected = 0;
+	let countryInfos;
+	async function loadCountryData() {
 
 
-  let renderRequested = false;
+		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */
 
 
-  function render() {
-    renderRequested = undefined;
+		const lonFudge = Math.PI * 1.5;
+		const latFudge = Math.PI;
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const labelParentElem = document.querySelector( '#labels' );
+		for ( const countryInfo of countryInfos ) {
 
 
-    controls.update();
+			const { lat, lon, min, max, name } = countryInfo;
 
 
-    updateLabels();
+			// adjust the helpers to point to the latitude and longitude
+			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
+			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;
 
 
-    renderer.render(scene, camera);
-  }
-  render();
+			// get the position of the lat/lon
+			positionHelper.updateWorldMatrix( true, false );
+			const position = new THREE.Vector3();
+			positionHelper.getWorldPosition( position );
+			countryInfo.position = position;
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+			// compute the area for each country
+			const width = max[ 0 ] - min[ 0 ];
+			const height = max[ 1 ] - min[ 1 ];
+			const area = width * height;
+			countryInfo.area = area;
+
+			// add an element for each country
+			const elem = document.createElement( 'div' );
+			elem.textContent = name;
+			labelParentElem.appendChild( elem );
+			countryInfo.elem = elem;
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	loadCountryData();
+
+	const tempV = new THREE.Vector3();
+	const cameraToPoint = new THREE.Vector3();
+	const cameraPosition = new THREE.Vector3();
+	const normalMatrix = new THREE.Matrix3();
+
+	const settings = {
+		minArea: 20,
+		maxVisibleDot: - 0.2,
+	};
+
+	function updateLabels() {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const large = settings.minArea * settings.minArea;
+		// get a matrix that represents a relative orientation of the camera
+		normalMatrix.getNormalMatrix( camera.matrixWorldInverse );
+		// get the camera's position
+		camera.getWorldPosition( cameraPosition );
+		for ( const countryInfo of countryInfos ) {
+
+			const { position, elem, area, selected } = countryInfo;
+			const largeEnough = area >= large;
+			const show = selected || ( numCountriesSelected === 0 && largeEnough );
+			if ( ! show ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// Orient the position based on the camera's orientation.
+			// Since the sphere is at the origin and the sphere is a unit sphere
+			// this gives us a camera relative direction vector for the position.
+			tempV.copy( position );
+			tempV.applyMatrix3( normalMatrix );
+
+			// compute the direction to this position from the camera
+			cameraToPoint.copy( position );
+			cameraToPoint.applyMatrix4( camera.matrixWorldInverse ).normalize();
+
+			// get the dot product of camera relative direction to this position
+			// on the globe with the direction from the camera to that point.
+			// -1 = facing directly towards the camera
+			// 0 = exactly on tangent of the sphere from the camera
+			// > 0 = facing away
+			const dot = tempV.dot( cameraToPoint );
+
+			// if the orientation is not facing us hide it.
+			if ( dot > settings.maxVisibleDot ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// restore the element to its default display style
+			elem.style.display = '';
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			tempV.copy( position );
+			tempV.project( camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+			// set the zIndex for sorting
+			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;
+
+		}
+
+	}
+
+	class GPUPickHelper {
+
+		constructor() {
+
+			// create a 1x1 pixel render target
+			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
+			this.pixelBuffer = new Uint8Array( 4 );
+
+		}
+		pick( cssPosition, scene, camera ) {
+
+			const { pickingTexture, pixelBuffer } = this;
+
+			// set the view offset to represent just a single pixel under the mouse
+			const pixelRatio = renderer.getPixelRatio();
+			camera.setViewOffset(
+				renderer.getContext().drawingBufferWidth, // full width
+				renderer.getContext().drawingBufferHeight, // full top
+				cssPosition.x * pixelRatio | 0, // rect x
+				cssPosition.y * pixelRatio | 0, // rect y
+				1, // rect width
+				1, // rect height
+			);
+			// render the scene
+			renderer.setRenderTarget( pickingTexture );
+			renderer.render( scene, camera );
+			renderer.setRenderTarget( null );
+			// clear the view offset so rendering returns to normal
+			camera.clearViewOffset();
+			//read the pixel
+			renderer.readRenderTargetPixels(
+				pickingTexture,
+				0, // x
+				0, // y
+				1, // width
+				1, // height
+				pixelBuffer );
+
+			const id =
+          ( pixelBuffer[ 0 ] << 0 ) |
+          ( pixelBuffer[ 1 ] << 8 ) |
+          ( pixelBuffer[ 2 ] << 16 );
+
+			return id;
+
+		}
+
+	}
+
+	const pickHelper = new GPUPickHelper();
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function pickCountry( event ) {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const position = getCanvasRelativePosition( event );
+		const id = pickHelper.pick( position, pickingScene, camera );
+		if ( id > 0 ) {
+
+			const countryInfo = countryInfos[ id - 1 ];
+			const selected = ! countryInfo.selected;
+			if ( selected && ! event.shiftKey && ! event.ctrlKey && ! event.metaKey ) {
+
+				unselectAllCountries();
+
+			}
+
+			numCountriesSelected += selected ? 1 : - 1;
+			countryInfo.selected = selected;
+			setPaletteColor( id, selected ? selectedColor : unselectedColor );
+			paletteTexture.needsUpdate = true;
+
+		} else if ( numCountriesSelected ) {
+
+			unselectAllCountries();
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	function unselectAllCountries() {
+
+		numCountriesSelected = 0;
+		countryInfos.forEach( ( countryInfo ) => {
+
+			countryInfo.selected = false;
+
+		} );
+		resetPalette();
+
+	}
+
+	canvas.addEventListener( 'pointerup', pickCountry );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+
+		updateLabels();
+
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 427 - 346
manual/examples/indexed-textures-picking-debounced.html

@@ -74,95 +74,107 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('#246');
-
-  const pickingScene = new THREE.Scene();
-  pickingScene.background = new THREE.Color(0);
-
-  const tempColor = new THREE.Color();
-  function get255BasedColor(color) {
-    tempColor.set(color);
-    const base = tempColor.toArray().map(v => v * 255);
-    base.push(255); // alpha
-    return base;
-  }
 
 
-  const maxNumCountries = 512;
-  const paletteTextureWidth = maxNumCountries;
-  const paletteTextureHeight = 1;
-  const palette = new Uint8Array(paletteTextureWidth * 4);
-  const paletteTexture = new THREE.DataTexture(palette, paletteTextureWidth, paletteTextureHeight);
-  paletteTexture.minFilter = THREE.NearestFilter;
-  paletteTexture.magFilter = THREE.NearestFilter;
-
-  const selectedColor = get255BasedColor('red');
-  const unselectedColor = get255BasedColor('#444');
-  const oceanColor = get255BasedColor('rgb(100,200,255)');
-  resetPalette();
-
-  function setPaletteColor(index, color) {
-    palette.set(color, index * 4);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function resetPalette() {
-    // make all colors the unselected color
-    for (let i = 1; i < maxNumCountries; ++i) {
-      setPaletteColor(i, unselectedColor);
-    }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-    // set the ocean color (index #0)
-    setPaletteColor(0, oceanColor);
-    paletteTexture.needsUpdate = true;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( '#246' );
+
+	const pickingScene = new THREE.Scene();
+	pickingScene.background = new THREE.Color( 0 );
+
+	const tempColor = new THREE.Color();
+	function get255BasedColor( color ) {
+
+		tempColor.set( color );
+		const base = tempColor.toArray().map( v => v * 255 );
+		base.push( 255 ); // alpha
+		return base;
+
+	}
+
+	const maxNumCountries = 512;
+	const paletteTextureWidth = maxNumCountries;
+	const paletteTextureHeight = 1;
+	const palette = new Uint8Array( paletteTextureWidth * 4 );
+	const paletteTexture = new THREE.DataTexture( palette, paletteTextureWidth, paletteTextureHeight );
+	paletteTexture.minFilter = THREE.NearestFilter;
+	paletteTexture.magFilter = THREE.NearestFilter;
+	paletteTexture.colorSpace = THREE.SRGBColorSpace;
+
+	const selectedColor = get255BasedColor( 'red' );
+	const unselectedColor = get255BasedColor( '#444' );
+	const oceanColor = get255BasedColor( 'rgb(100,200,255)' );
+	resetPalette();
+
+	function setPaletteColor( index, color ) {
+
+		palette.set( color, index * 4 );
+
+	}
+
+	function resetPalette() {
+
+		// make all colors the unselected color
+		for ( let i = 1; i < maxNumCountries; ++ i ) {
+
+			setPaletteColor( i, unselectedColor );
 
 
-  {
-    const loader = new THREE.TextureLoader();
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
+		}
 
 
-    const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
-    indexTexture.minFilter = THREE.NearestFilter;
-    indexTexture.magFilter = THREE.NearestFilter;
+		// set the ocean color (index #0)
+		setPaletteColor( 0, oceanColor );
+		paletteTexture.needsUpdate = true;
 
 
-    const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
-    pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
+	}
 
 
-    const fragmentShaderReplacements = [
-      {
-        from: '#include <common>',
-        to: `
+	{
+
+		const loader = new THREE.TextureLoader();
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+
+		const indexTexture = loader.load( 'resources/data/world/country-index-texture.png', render );
+		indexTexture.minFilter = THREE.NearestFilter;
+		indexTexture.magFilter = THREE.NearestFilter;
+
+		const pickingMaterial = new THREE.MeshBasicMaterial( { map: indexTexture } );
+		pickingScene.add( new THREE.Mesh( geometry, pickingMaterial ) );
+
+		const fragmentShaderReplacements = [
+			{
+				from: '#include <common>',
+				to: `
           #include <common>
           #include <common>
           uniform sampler2D indexTexture;
           uniform sampler2D indexTexture;
           uniform sampler2D paletteTexture;
           uniform sampler2D paletteTexture;
           uniform float paletteTextureWidth;
           uniform float paletteTextureWidth;
         `,
         `,
-      },
-      {
-        from: '#include <color_fragment>',
-        to: `
+			},
+			{
+				from: '#include <color_fragment>',
+				to: `
           #include <color_fragment>
           #include <color_fragment>
           {
           {
-            vec4 indexColor = texture2D(indexTexture, vUv);
+            vec4 indexColor = texture2D(indexTexture, vMapUv);
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
@@ -170,302 +182,371 @@ function main() {
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
           }
           }
         `,
         `,
-      },
-    ];
-
-    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    material.onBeforeCompile = function(shader) {
-      fragmentShaderReplacements.forEach((rep) => {
-        shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
-      });
-      shader.uniforms.paletteTexture = {value: paletteTexture};
-      shader.uniforms.indexTexture = {value: indexTexture};
-      shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
-    };
-    scene.add(new THREE.Mesh(geometry, material));
-  }
+			},
+		];
 
 
-  async function loadJSON(url) {
-    const req = await fetch(url);
-    return req.json();
-  }
+		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		material.onBeforeCompile = function ( shader ) {
 
 
-  let numCountriesSelected = 0;
-  let countryInfos;
-  async function loadCountryData() {
-    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejs.org: url */
-
-    const lonFudge = Math.PI * 1.5;
-    const latFudge = Math.PI;
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-
-    const labelParentElem = document.querySelector('#labels');
-    for (const countryInfo of countryInfos) {
-      const {lat, lon, min, max, name} = countryInfo;
-
-      // adjust the helpers to point to the latitude and longitude
-      lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
-      latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
-
-      // get the position of the lat/lon
-      positionHelper.updateWorldMatrix(true, false);
-      const position = new THREE.Vector3();
-      positionHelper.getWorldPosition(position);
-      countryInfo.position = position;
-
-      // compute the area for each country
-      const width = max[0] - min[0];
-      const height = max[1] - min[1];
-      const area = width * height;
-      countryInfo.area = area;
-
-      // add an element for each country
-      const elem = document.createElement('div');
-      elem.textContent = name;
-      labelParentElem.appendChild(elem);
-      countryInfo.elem = elem;
-    }
-    requestRenderIfNotRequested();
-  }
-  loadCountryData();
-
-  const tempV = new THREE.Vector3();
-  const cameraToPoint = new THREE.Vector3();
-  const cameraPosition = new THREE.Vector3();
-  const normalMatrix = new THREE.Matrix3();
-
-  const settings = {
-    minArea: 20,
-    maxVisibleDot: -0.2,
-  };
-
-  function updateLabels() {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+			fragmentShaderReplacements.forEach( ( rep ) => {
 
 
-    const large = settings.minArea * settings.minArea;
-    // get a matrix that represents a relative orientation of the camera
-    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
-    // get the camera's position
-    camera.getWorldPosition(cameraPosition);
-    for (const countryInfo of countryInfos) {
-      const {position, elem, area, selected} = countryInfo;
-      const largeEnough = area >= large;
-      const show = selected || (numCountriesSelected === 0 && largeEnough);
-      if (!show) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // Orient the position based on the camera's orientation.
-      // Since the sphere is at the origin and the sphere is a unit sphere
-      // this gives us a camera relative direction vector for the position.
-      tempV.copy(position);
-      tempV.applyMatrix3(normalMatrix);
-
-      // compute the direction to this position from the camera
-      cameraToPoint.copy(position);
-      cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
-
-      // get the dot product of camera relative direction to this position
-      // on the globe with the direction from the camera to that point.
-      // -1 = facing directly towards the camera
-      // 0 = exactly on tangent of the sphere from the camera
-      // > 0 = facing away
-      const dot = tempV.dot(cameraToPoint);
-
-      // if the orientation is not facing us hide it.
-      if (dot > settings.maxVisibleDot) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // restore the element to its default display style
-      elem.style.display = '';
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      tempV.copy(position);
-      tempV.project(camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
-      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-
-      // set the zIndex for sorting
-      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
-    }
-  }
+				shader.fragmentShader = shader.fragmentShader.replace( rep.from, rep.to );
 
 
-  class GPUPickHelper {
-    constructor() {
-      // create a 1x1 pixel render target
-      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
-      this.pixelBuffer = new Uint8Array(4);
-    }
-    pick(cssPosition, scene, camera) {
-      const {pickingTexture, pixelBuffer} = this;
-
-      // set the view offset to represent just a single pixel under the mouse
-      const pixelRatio = renderer.getPixelRatio();
-      camera.setViewOffset(
-          renderer.getContext().drawingBufferWidth,   // full width
-          renderer.getContext().drawingBufferHeight,  // full top
-          cssPosition.x * pixelRatio | 0,             // rect x
-          cssPosition.y * pixelRatio | 0,             // rect y
-          1,                                          // rect width
-          1,                                          // rect height
-      );
-      // render the scene
-      renderer.setRenderTarget(pickingTexture);
-      renderer.render(scene, camera);
-      renderer.setRenderTarget(null);
-      // clear the view offset so rendering returns to normal
-      camera.clearViewOffset();
-      //read the pixel
-      renderer.readRenderTargetPixels(
-          pickingTexture,
-          0,   // x
-          0,   // y
-          1,   // width
-          1,   // height
-          pixelBuffer);
-
-      const id =
-          (pixelBuffer[0] <<  0) |
-          (pixelBuffer[1] <<  8) |
-          (pixelBuffer[2] << 16);
-
-      return id;
-    }
-  }
+			} );
+			shader.uniforms.paletteTexture = { value: paletteTexture };
+			shader.uniforms.indexTexture = { value: indexTexture };
+			shader.uniforms.paletteTextureWidth = { value: paletteTextureWidth };
 
 
-  const pickHelper = new GPUPickHelper();
+		};
 
 
-  const maxClickTimeMs = 200;
-  const maxMoveDeltaSq = 5 * 5;
-  const startPosition = {};
-  let startTimeMs;
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+	}
 
 
-  function recordStartTimeAndPosition(event) {
-    startTimeMs = performance.now();
-    const pos = getCanvasRelativePosition(event);
-    startPosition.x = pos.x;
-    startPosition.y = pos.y;
-  }
+	async function loadJSON( url ) {
 
 
-  function pickCountry(event) {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+		const req = await fetch( url );
+		return req.json();
 
 
-    // if it's been a moment since the user started
-    // then assume it was a drag action, not a select action
-    const clickTimeMs = performance.now() - startTimeMs;
-    if (clickTimeMs > maxClickTimeMs) {
-      return;
-    }
+	}
 
 
-    // if they moved assume it was a drag action
-    const position = getCanvasRelativePosition(event);
-    const moveDeltaSq = (startPosition.x - position.x) ** 2 +
-                        (startPosition.y - position.y) ** 2;
-    if (moveDeltaSq > maxMoveDeltaSq) {
-      return;
-    }
+	let numCountriesSelected = 0;
+	let countryInfos;
+	async function loadCountryData() {
 
 
-    const id = pickHelper.pick(position, pickingScene, camera);
-    if (id > 0) {
-      const countryInfo = countryInfos[id - 1];
-      const selected = !countryInfo.selected;
-      if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
-        unselectAllCountries();
-      }
-      numCountriesSelected += selected ? 1 : -1;
-      countryInfo.selected = selected;
-      setPaletteColor(id, selected ? selectedColor : unselectedColor);
-      paletteTexture.needsUpdate = true;
-    } else if (numCountriesSelected) {
-      unselectAllCountries();
-    }
-    requestRenderIfNotRequested();
-  }
+		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */
 
 
-  function unselectAllCountries() {
-    numCountriesSelected = 0;
-    countryInfos.forEach((countryInfo) => {
-      countryInfo.selected = false;
-    });
-    resetPalette();
-  }
+		const lonFudge = Math.PI * 1.5;
+		const latFudge = Math.PI;
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
 
 
-  canvas.addEventListener('pointerdown', recordStartTimeAndPosition);
-  canvas.addEventListener('pointerup', pickCountry);
+		const labelParentElem = document.querySelector( '#labels' );
+		for ( const countryInfo of countryInfos ) {
 
 
-  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;
-  }
+			const { lat, lon, min, max, name } = countryInfo;
 
 
-  let renderRequested = false;
+			// adjust the helpers to point to the latitude and longitude
+			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
+			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;
 
 
-  function render() {
-    renderRequested = undefined;
+			// get the position of the lat/lon
+			positionHelper.updateWorldMatrix( true, false );
+			const position = new THREE.Vector3();
+			positionHelper.getWorldPosition( position );
+			countryInfo.position = position;
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			// compute the area for each country
+			const width = max[ 0 ] - min[ 0 ];
+			const height = max[ 1 ] - min[ 1 ];
+			const area = width * height;
+			countryInfo.area = area;
 
 
-    controls.update();
+			// add an element for each country
+			const elem = document.createElement( 'div' );
+			elem.textContent = name;
+			labelParentElem.appendChild( elem );
+			countryInfo.elem = elem;
 
 
-    updateLabels();
+		}
 
 
-    renderer.render(scene, camera);
-  }
-  render();
+		requestRenderIfNotRequested();
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+	}
+
+	loadCountryData();
+
+	const tempV = new THREE.Vector3();
+	const cameraToPoint = new THREE.Vector3();
+	const cameraPosition = new THREE.Vector3();
+	const normalMatrix = new THREE.Matrix3();
+
+	const settings = {
+		minArea: 20,
+		maxVisibleDot: - 0.2,
+	};
+
+	function updateLabels() {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const large = settings.minArea * settings.minArea;
+		// get a matrix that represents a relative orientation of the camera
+		normalMatrix.getNormalMatrix( camera.matrixWorldInverse );
+		// get the camera's position
+		camera.getWorldPosition( cameraPosition );
+		for ( const countryInfo of countryInfos ) {
+
+			const { position, elem, area, selected } = countryInfo;
+			const largeEnough = area >= large;
+			const show = selected || ( numCountriesSelected === 0 && largeEnough );
+			if ( ! show ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// Orient the position based on the camera's orientation.
+			// Since the sphere is at the origin and the sphere is a unit sphere
+			// this gives us a camera relative direction vector for the position.
+			tempV.copy( position );
+			tempV.applyMatrix3( normalMatrix );
+
+			// compute the direction to this position from the camera
+			cameraToPoint.copy( position );
+			cameraToPoint.applyMatrix4( camera.matrixWorldInverse ).normalize();
+
+			// get the dot product of camera relative direction to this position
+			// on the globe with the direction from the camera to that point.
+			// -1 = facing directly towards the camera
+			// 0 = exactly on tangent of the sphere from the camera
+			// > 0 = facing away
+			const dot = tempV.dot( cameraToPoint );
+
+			// if the orientation is not facing us hide it.
+			if ( dot > settings.maxVisibleDot ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// restore the element to its default display style
+			elem.style.display = '';
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			tempV.copy( position );
+			tempV.project( camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+			// set the zIndex for sorting
+			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;
+
+		}
+
+	}
+
+	class GPUPickHelper {
+
+		constructor() {
+
+			// create a 1x1 pixel render target
+			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
+			this.pixelBuffer = new Uint8Array( 4 );
+
+		}
+		pick( cssPosition, scene, camera ) {
+
+			const { pickingTexture, pixelBuffer } = this;
+
+			// set the view offset to represent just a single pixel under the mouse
+			const pixelRatio = renderer.getPixelRatio();
+			camera.setViewOffset(
+				renderer.getContext().drawingBufferWidth, // full width
+				renderer.getContext().drawingBufferHeight, // full top
+				cssPosition.x * pixelRatio | 0, // rect x
+				cssPosition.y * pixelRatio | 0, // rect y
+				1, // rect width
+				1, // rect height
+			);
+			// render the scene
+			renderer.setRenderTarget( pickingTexture );
+			renderer.render( scene, camera );
+			renderer.setRenderTarget( null );
+			// clear the view offset so rendering returns to normal
+			camera.clearViewOffset();
+			//read the pixel
+			renderer.readRenderTargetPixels(
+				pickingTexture,
+				0, // x
+				0, // y
+				1, // width
+				1, // height
+				pixelBuffer );
+
+			const id =
+          ( pixelBuffer[ 0 ] << 0 ) |
+          ( pixelBuffer[ 1 ] << 8 ) |
+          ( pixelBuffer[ 2 ] << 16 );
+
+			return id;
+
+		}
+
+	}
+
+	const pickHelper = new GPUPickHelper();
+
+	const maxClickTimeMs = 200;
+	const maxMoveDeltaSq = 5 * 5;
+	const startPosition = {};
+	let startTimeMs;
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function recordStartTimeAndPosition( event ) {
+
+		startTimeMs = performance.now();
+		const pos = getCanvasRelativePosition( event );
+		startPosition.x = pos.x;
+		startPosition.y = pos.y;
+
+	}
+
+	function pickCountry( event ) {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		// if it's been a moment since the user started
+		// then assume it was a drag action, not a select action
+		const clickTimeMs = performance.now() - startTimeMs;
+		if ( clickTimeMs > maxClickTimeMs ) {
+
+			return;
+
+		}
+
+		// if they moved assume it was a drag action
+		const position = getCanvasRelativePosition( event );
+		const moveDeltaSq = ( startPosition.x - position.x ) ** 2 +
+                        ( startPosition.y - position.y ) ** 2;
+		if ( moveDeltaSq > maxMoveDeltaSq ) {
+
+			return;
+
+		}
+
+		const id = pickHelper.pick( position, pickingScene, camera );
+		if ( id > 0 ) {
+
+			const countryInfo = countryInfos[ id - 1 ];
+			const selected = ! countryInfo.selected;
+			if ( selected && ! event.shiftKey && ! event.ctrlKey && ! event.metaKey ) {
+
+				unselectAllCountries();
+
+			}
+
+			numCountriesSelected += selected ? 1 : - 1;
+			countryInfo.selected = selected;
+			setPaletteColor( id, selected ? selectedColor : unselectedColor );
+			paletteTexture.needsUpdate = true;
+
+		} else if ( numCountriesSelected ) {
+
+			unselectAllCountries();
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	function unselectAllCountries() {
+
+		numCountriesSelected = 0;
+		countryInfos.forEach( ( countryInfo ) => {
+
+			countryInfo.selected = false;
+
+		} );
+		resetPalette();
+
+	}
+
+	canvas.addEventListener( 'pointerdown', recordStartTimeAndPosition );
+	canvas.addEventListener( 'pointerup', pickCountry );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+
+		updateLabels();
+
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 335 - 274
manual/examples/indexed-textures-picking.html

@@ -73,301 +73,362 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('#246');
-
-  const pickingScene = new THREE.Scene();
-  pickingScene.background = new THREE.Color(0);
-
-  {
-    const loader = new THREE.TextureLoader();
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-
-    const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
-    indexTexture.minFilter = THREE.NearestFilter;
-    indexTexture.magFilter = THREE.NearestFilter;
-
-    const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
-    pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
-
-    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadJSON(url) {
-    const req = await fetch(url);
-    return req.json();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  let numCountriesSelected = 0;
-  let countryInfos;
-  async function loadCountryData() {
-    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejs.org: url */
-
-    const lonFudge = Math.PI * 1.5;
-    const latFudge = Math.PI;
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-
-    const labelParentElem = document.querySelector('#labels');
-    for (const countryInfo of countryInfos) {
-      const {lat, lon, min, max, name} = countryInfo;
-
-      // adjust the helpers to point to the latitude and longitude
-      lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
-      latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
-
-      // get the position of the lat/lon
-      positionHelper.updateWorldMatrix(true, false);
-      const position = new THREE.Vector3();
-      positionHelper.getWorldPosition(position);
-      countryInfo.position = position;
-
-      // compute the area for each country
-      const width = max[0] - min[0];
-      const height = max[1] - min[1];
-      const area = width * height;
-      countryInfo.area = area;
-
-      // add an element for each country
-      const elem = document.createElement('div');
-      elem.textContent = name;
-      labelParentElem.appendChild(elem);
-      countryInfo.elem = elem;
-    }
-    requestRenderIfNotRequested();
-  }
-  loadCountryData();
-
-  const tempV = new THREE.Vector3();
-  const cameraToPoint = new THREE.Vector3();
-  const cameraPosition = new THREE.Vector3();
-  const normalMatrix = new THREE.Matrix3();
-
-  const settings = {
-    minArea: 20,
-    maxVisibleDot: -0.2,
-  };
-
-  function updateLabels() {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-    const large = settings.minArea * settings.minArea;
-    // get a matrix that represents a relative orientation of the camera
-    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
-    // get the camera's position
-    camera.getWorldPosition(cameraPosition);
-    for (const countryInfo of countryInfos) {
-      const {position, elem, area, selected} = countryInfo;
-      const largeEnough = area >= large;
-      const show = selected || (numCountriesSelected === 0 && largeEnough);
-      if (!show) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // Orient the position based on the camera's orientation.
-      // Since the sphere is at the origin and the sphere is a unit sphere
-      // this gives us a camera relative direction vector for the position.
-      tempV.copy(position);
-      tempV.applyMatrix3(normalMatrix);
-
-      // compute the direction to this position from the camera
-      cameraToPoint.copy(position);
-      cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
-
-      // get the dot product of camera relative direction to this position
-      // on the globe with the direction from the camera to that point.
-      // 1 = facing directly towards the camera
-      // 0 = exactly on tangent of the sphere from the camera
-      // < 0 = facing away
-      const dot = tempV.dot(cameraToPoint);
-
-      // if the orientation is not facing us hide it.
-      if (dot > settings.maxVisibleDot) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // restore the element to its default display style
-      elem.style.display = '';
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      tempV.copy(position);
-      tempV.project(camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
-      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-
-      // set the zIndex for sorting
-      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
-    }
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  class GPUPickHelper {
-    constructor() {
-      // create a 1x1 pixel render target
-      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
-      this.pixelBuffer = new Uint8Array(4);
-    }
-    pick(cssPosition, scene, camera) {
-      const {pickingTexture, pixelBuffer} = this;
-
-      // set the view offset to represent just a single pixel under the mouse
-      const pixelRatio = renderer.getPixelRatio();
-      camera.setViewOffset(
-          renderer.getContext().drawingBufferWidth,   // full width
-          renderer.getContext().drawingBufferHeight,  // full top
-          cssPosition.x * pixelRatio | 0,             // rect x
-          cssPosition.y * pixelRatio | 0,             // rect y
-          1,                                          // rect width
-          1,                                          // rect height
-      );
-      // render the scene
-      renderer.setRenderTarget(pickingTexture);
-      renderer.render(scene, camera);
-      renderer.setRenderTarget(null);
-      // clear the view offset so rendering returns to normal
-      camera.clearViewOffset();
-      //read the pixel
-      renderer.readRenderTargetPixels(
-          pickingTexture,
-          0,   // x
-          0,   // y
-          1,   // width
-          1,   // height
-          pixelBuffer);
-
-      const id =
-          (pixelBuffer[0] <<  0) |
-          (pixelBuffer[1] <<  8) |
-          (pixelBuffer[2] << 16);
-
-      return id;
-    }
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( '#246' );
 
 
-  const pickHelper = new GPUPickHelper();
+	const pickingScene = new THREE.Scene();
+	pickingScene.background = new THREE.Color( 0 );
 
 
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+	{
 
 
-  function pickCountry(event) {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+		const loader = new THREE.TextureLoader();
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
 
 
-    const position = getCanvasRelativePosition(event);
-    const id = pickHelper.pick(position, pickingScene, camera);
-    if (id > 0) {
-      // we clicked a country. Toggle its 'selected' property
-      const countryInfo = countryInfos[id - 1];
-      const selected = !countryInfo.selected;
-      // if we're selecting this country and modifiers are not
-      // pressed unselect everything else.
-      if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
-        unselectAllCountries();
-      }
-      numCountriesSelected += selected ? 1 : -1;
-      countryInfo.selected = selected;
-    } else if (numCountriesSelected) {
-      unselectAllCountries();
-    }
-    requestRenderIfNotRequested();
-  }
+		const indexTexture = loader.load( 'resources/data/world/country-index-texture.png', render );
+		indexTexture.minFilter = THREE.NearestFilter;
+		indexTexture.magFilter = THREE.NearestFilter;
 
 
-  function unselectAllCountries() {
-    numCountriesSelected = 0;
-    countryInfos.forEach((countryInfo) => {
-      countryInfo.selected = false;
-    });
-  }
+		const pickingMaterial = new THREE.MeshBasicMaterial( { map: indexTexture } );
+		pickingScene.add( new THREE.Mesh( geometry, pickingMaterial ) );
 
 
-  canvas.addEventListener('pointerup', pickCountry);
+		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-  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;
-  }
+	}
 
 
-  let renderRequested = false;
+	async function loadJSON( url ) {
 
 
-  function render() {
-    renderRequested = undefined;
+		const req = await fetch( url );
+		return req.json();
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	}
 
 
-    controls.update();
+	let numCountriesSelected = 0;
+	let countryInfos;
+	async function loadCountryData() {
 
 
-    updateLabels();
+		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */
 
 
-    renderer.render(scene, camera);
-  }
-  render();
+		const lonFudge = Math.PI * 1.5;
+		const latFudge = Math.PI;
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+		const labelParentElem = document.querySelector( '#labels' );
+		for ( const countryInfo of countryInfos ) {
+
+			const { lat, lon, min, max, name } = countryInfo;
+
+			// adjust the helpers to point to the latitude and longitude
+			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
+			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;
+
+			// get the position of the lat/lon
+			positionHelper.updateWorldMatrix( true, false );
+			const position = new THREE.Vector3();
+			positionHelper.getWorldPosition( position );
+			countryInfo.position = position;
+
+			// compute the area for each country
+			const width = max[ 0 ] - min[ 0 ];
+			const height = max[ 1 ] - min[ 1 ];
+			const area = width * height;
+			countryInfo.area = area;
+
+			// add an element for each country
+			const elem = document.createElement( 'div' );
+			elem.textContent = name;
+			labelParentElem.appendChild( elem );
+			countryInfo.elem = elem;
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	loadCountryData();
+
+	const tempV = new THREE.Vector3();
+	const cameraToPoint = new THREE.Vector3();
+	const cameraPosition = new THREE.Vector3();
+	const normalMatrix = new THREE.Matrix3();
+
+	const settings = {
+		minArea: 20,
+		maxVisibleDot: - 0.2,
+	};
+
+	function updateLabels() {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const large = settings.minArea * settings.minArea;
+		// get a matrix that represents a relative orientation of the camera
+		normalMatrix.getNormalMatrix( camera.matrixWorldInverse );
+		// get the camera's position
+		camera.getWorldPosition( cameraPosition );
+		for ( const countryInfo of countryInfos ) {
+
+			const { position, elem, area, selected } = countryInfo;
+			const largeEnough = area >= large;
+			const show = selected || ( numCountriesSelected === 0 && largeEnough );
+			if ( ! show ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// Orient the position based on the camera's orientation.
+			// Since the sphere is at the origin and the sphere is a unit sphere
+			// this gives us a camera relative direction vector for the position.
+			tempV.copy( position );
+			tempV.applyMatrix3( normalMatrix );
+
+			// compute the direction to this position from the camera
+			cameraToPoint.copy( position );
+			cameraToPoint.applyMatrix4( camera.matrixWorldInverse ).normalize();
+
+			// get the dot product of camera relative direction to this position
+			// on the globe with the direction from the camera to that point.
+			// 1 = facing directly towards the camera
+			// 0 = exactly on tangent of the sphere from the camera
+			// < 0 = facing away
+			const dot = tempV.dot( cameraToPoint );
+
+			// if the orientation is not facing us hide it.
+			if ( dot > settings.maxVisibleDot ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// restore the element to its default display style
+			elem.style.display = '';
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			tempV.copy( position );
+			tempV.project( camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+			// set the zIndex for sorting
+			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;
+
+		}
+
+	}
+
+	class GPUPickHelper {
+
+		constructor() {
+
+			// create a 1x1 pixel render target
+			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
+			this.pixelBuffer = new Uint8Array( 4 );
+
+		}
+		pick( cssPosition, scene, camera ) {
+
+			const { pickingTexture, pixelBuffer } = this;
+
+			// set the view offset to represent just a single pixel under the mouse
+			const pixelRatio = renderer.getPixelRatio();
+			camera.setViewOffset(
+				renderer.getContext().drawingBufferWidth, // full width
+				renderer.getContext().drawingBufferHeight, // full top
+				cssPosition.x * pixelRatio | 0, // rect x
+				cssPosition.y * pixelRatio | 0, // rect y
+				1, // rect width
+				1, // rect height
+			);
+			// render the scene
+			renderer.setRenderTarget( pickingTexture );
+			renderer.render( scene, camera );
+			renderer.setRenderTarget( null );
+			// clear the view offset so rendering returns to normal
+			camera.clearViewOffset();
+			//read the pixel
+			renderer.readRenderTargetPixels(
+				pickingTexture,
+				0, // x
+				0, // y
+				1, // width
+				1, // height
+				pixelBuffer );
+
+			const id =
+          ( pixelBuffer[ 0 ] << 0 ) |
+          ( pixelBuffer[ 1 ] << 8 ) |
+          ( pixelBuffer[ 2 ] << 16 );
+
+			return id;
+
+		}
+
+	}
+
+	const pickHelper = new GPUPickHelper();
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function pickCountry( event ) {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const position = getCanvasRelativePosition( event );
+		const id = pickHelper.pick( position, pickingScene, camera );
+		if ( id > 0 ) {
+
+			// we clicked a country. Toggle its 'selected' property
+			const countryInfo = countryInfos[ id - 1 ];
+			const selected = ! countryInfo.selected;
+			// if we're selecting this country and modifiers are not
+			// pressed unselect everything else.
+			if ( selected && ! event.shiftKey && ! event.ctrlKey && ! event.metaKey ) {
+
+				unselectAllCountries();
+
+			}
+
+			numCountriesSelected += selected ? 1 : - 1;
+			countryInfo.selected = selected;
+
+		} else if ( numCountriesSelected ) {
+
+			unselectAllCountries();
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	function unselectAllCountries() {
+
+		numCountriesSelected = 0;
+		countryInfos.forEach( ( countryInfo ) => {
+
+			countryInfo.selected = false;
+
+		} );
+
+	}
+
+	canvas.addEventListener( 'pointerup', pickCountry );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+
+		updateLabels();
+
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 371 - 301
manual/examples/indexed-textures-random-colors.html

@@ -73,73 +73,80 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('#246');
-
-  const pickingScene = new THREE.Scene();
-  pickingScene.background = new THREE.Color(0);
-
-  const maxNumCountries = 512;
-  const paletteTextureWidth = maxNumCountries;
-  const paletteTextureHeight = 1;
-  const palette = new Uint8Array(paletteTextureWidth * 4);
-  const paletteTexture = new THREE.DataTexture(palette, paletteTextureWidth, paletteTextureHeight);
-  paletteTexture.minFilter = THREE.NearestFilter;
-  paletteTexture.magFilter = THREE.NearestFilter;
-  for (let i = 1; i < palette.length; ++i) {
-    palette[i] = Math.random() * 256;
-  }
-  // set the ocean color (index #0)
-  palette.set([100, 200, 255, 255], 0);
-  paletteTexture.needsUpdate = true;
 
 
-  {
-    const loader = new THREE.TextureLoader();
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( '#246' );
+
+	const pickingScene = new THREE.Scene();
+	pickingScene.background = new THREE.Color( 0 );
+
+	const maxNumCountries = 512;
+	const paletteTextureWidth = maxNumCountries;
+	const paletteTextureHeight = 1;
+	const palette = new Uint8Array( paletteTextureWidth * 4 );
+	const paletteTexture = new THREE.DataTexture( palette, paletteTextureWidth, paletteTextureHeight );
+	paletteTexture.minFilter = THREE.NearestFilter;
+	paletteTexture.magFilter = THREE.NearestFilter;
+	paletteTexture.colorSpace = THREE.SRGBColorSpace;
+	for ( let i = 1; i < palette.length; ++ i ) {
+
+		palette[ i ] = Math.random() * 256;
+
+	}
 
 
-    const indexTexture = loader.load('resources/data/world/country-index-texture.png', render);
-    indexTexture.minFilter = THREE.NearestFilter;
-    indexTexture.magFilter = THREE.NearestFilter;
+	// set the ocean color (index #0)
+	palette.set( [ 100, 200, 255, 255 ], 0 );
+	paletteTexture.needsUpdate = true;
 
 
-    const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
-    pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));
+	{
 
 
-    const fragmentShaderReplacements = [
-      {
-        from: '#include <common>',
-        to: `
+		const loader = new THREE.TextureLoader();
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+
+		const indexTexture = loader.load( 'resources/data/world/country-index-texture.png', render );
+		indexTexture.minFilter = THREE.NearestFilter;
+		indexTexture.magFilter = THREE.NearestFilter;
+
+		const pickingMaterial = new THREE.MeshBasicMaterial( { map: indexTexture } );
+		pickingScene.add( new THREE.Mesh( geometry, pickingMaterial ) );
+
+		const fragmentShaderReplacements = [
+			{
+				from: '#include <common>',
+				to: `
           #include <common>
           #include <common>
           uniform sampler2D indexTexture;
           uniform sampler2D indexTexture;
           uniform sampler2D paletteTexture;
           uniform sampler2D paletteTexture;
           uniform float paletteTextureWidth;
           uniform float paletteTextureWidth;
         `,
         `,
-      },
-      {
-        from: '#include <color_fragment>',
-        to: `
+			},
+			{
+				from: '#include <color_fragment>',
+				to: `
           #include <color_fragment>
           #include <color_fragment>
           {
           {
-            vec4 indexColor = texture2D(indexTexture, vUv);
+            vec4 indexColor = texture2D(indexTexture, vMapUv);
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
             vec4 paletteColor = texture2D(paletteTexture, paletteUV);
@@ -147,272 +154,335 @@ function main() {
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
             diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
           }
           }
         `,
         `,
-      },
-    ];
-
-    const texture = loader.load('resources/data/world/country-outlines-4k.png', render);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    material.onBeforeCompile = function(shader) {
-      fragmentShaderReplacements.forEach((rep) => {
-        shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
-      });
-      shader.uniforms.paletteTexture = {value: paletteTexture};
-      shader.uniforms.indexTexture = {value: indexTexture};
-      shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
-    };
-    scene.add(new THREE.Mesh(geometry, material));
-  }
+			},
+		];
 
 
-  async function loadJSON(url) {
-    const req = await fetch(url);
-    return req.json();
-  }
+		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		material.onBeforeCompile = function ( shader ) {
 
 
-  let numCountriesSelected = 0;
-  let countryInfos;
-  async function loadCountryData() {
-    countryInfos = await loadJSON('resources/data/world/country-info.json');  /* threejs.org: url */
-
-    const lonFudge = Math.PI * 1.5;
-    const latFudge = Math.PI;
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-
-    const labelParentElem = document.querySelector('#labels');
-    for (const countryInfo of countryInfos) {
-      const {lat, lon, min, max, name} = countryInfo;
-
-      // adjust the helpers to point to the latitude and longitude
-      lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge;
-      latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge;
-
-      // get the position of the lat/lon
-      positionHelper.updateWorldMatrix(true, false);
-      const position = new THREE.Vector3();
-      positionHelper.getWorldPosition(position);
-      countryInfo.position = position;
-
-      // compute the area for each country
-      const width = max[0] - min[0];
-      const height = max[1] - min[1];
-      const area = width * height;
-      countryInfo.area = area;
-
-      // add an element for each country
-      const elem = document.createElement('div');
-      elem.textContent = name;
-      labelParentElem.appendChild(elem);
-      countryInfo.elem = elem;
-    }
-    requestRenderIfNotRequested();
-  }
-  loadCountryData();
-
-  const tempV = new THREE.Vector3();
-  const cameraToPoint = new THREE.Vector3();
-  const cameraPosition = new THREE.Vector3();
-  const normalMatrix = new THREE.Matrix3();
-
-  const settings = {
-    minArea: 20,
-    maxVisibleDot: -0.2,
-  };
-
-  function updateLabels() {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+			fragmentShaderReplacements.forEach( ( rep ) => {
 
 
-    const large = settings.minArea * settings.minArea;
-    // get a matrix that represents a relative orientation of the camera
-    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
-    // get the camera's position
-    camera.getWorldPosition(cameraPosition);
-    for (const countryInfo of countryInfos) {
-      const {position, elem, area, selected} = countryInfo;
-      const largeEnough = area >= large;
-      const show = selected || (numCountriesSelected === 0 && largeEnough);
-      if (!show) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // Orient the position based on the camera's orientation.
-      // Since the sphere is at the origin and the sphere is a unit sphere
-      // this gives us a camera relative direction vector for the position.
-      tempV.copy(position);
-      tempV.applyMatrix3(normalMatrix);
-
-      // compute the direction to this position from the camera
-      cameraToPoint.copy(position);
-      cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();
-
-      // get the dot product of camera relative direction to this position
-      // on the globe with the direction from the camera to that point.
-      // -1 = facing directly towards the camera
-      // 0 = exactly on tangent of the sphere from the camera
-      // > 0 = facing away
-      const dot = tempV.dot(cameraToPoint);
-
-      // if the orientation is not facing us hide it.
-      if (dot > settings.maxVisibleDot) {
-        elem.style.display = 'none';
-        continue;
-      }
-
-      // restore the element to its default display style
-      elem.style.display = '';
-
-      // get the normalized screen coordinate of that position
-      // x and y will be in the -1 to +1 range with x = -1 being
-      // on the left and y = -1 being on the bottom
-      tempV.copy(position);
-      tempV.project(camera);
-
-      // convert the normalized position to CSS coordinates
-      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
-      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;
-
-      // move the elem to that position
-      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
-
-      // set the zIndex for sorting
-      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
-    }
-  }
+				shader.fragmentShader = shader.fragmentShader.replace( rep.from, rep.to );
 
 
-  class GPUPickHelper {
-    constructor() {
-      // create a 1x1 pixel render target
-      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
-      this.pixelBuffer = new Uint8Array(4);
-    }
-    pick(cssPosition, scene, camera) {
-      const {pickingTexture, pixelBuffer} = this;
-
-      // set the view offset to represent just a single pixel under the mouse
-      const pixelRatio = renderer.getPixelRatio();
-      camera.setViewOffset(
-          renderer.getContext().drawingBufferWidth,   // full width
-          renderer.getContext().drawingBufferHeight,  // full top
-          cssPosition.x * pixelRatio | 0,             // rect x
-          cssPosition.y * pixelRatio | 0,             // rect y
-          1,                                          // rect width
-          1,                                          // rect height
-      );
-      // render the scene
-      renderer.setRenderTarget(pickingTexture);
-      renderer.render(scene, camera);
-      renderer.setRenderTarget(null);
-      // clear the view offset so rendering returns to normal
-      camera.clearViewOffset();
-      //read the pixel
-      renderer.readRenderTargetPixels(
-          pickingTexture,
-          0,   // x
-          0,   // y
-          1,   // width
-          1,   // height
-          pixelBuffer);
-
-      const id =
-          (pixelBuffer[0] <<  0) |
-          (pixelBuffer[1] <<  8) |
-          (pixelBuffer[2] << 16);
-
-      return id;
-    }
-  }
+			} );
+			shader.uniforms.paletteTexture = { value: paletteTexture };
+			shader.uniforms.indexTexture = { value: indexTexture };
+			shader.uniforms.paletteTextureWidth = { value: paletteTextureWidth };
 
 
-  const pickHelper = new GPUPickHelper();
+		};
 
 
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-  function pickCountry(event) {
-    // exit if we have not loaded the data yet
-    if (!countryInfos) {
-      return;
-    }
+	}
 
 
-    const position = getCanvasRelativePosition(event);
-    const id = pickHelper.pick(position, pickingScene, camera);
-    if (id > 0) {
-      const countryInfo = countryInfos[id - 1];
-      const selected = !countryInfo.selected;
-      if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
-        unselectAllCountries();
-      }
-      numCountriesSelected += selected ? 1 : -1;
-      countryInfo.selected = selected;
-    } else if (numCountriesSelected) {
-      unselectAllCountries();
-    }
-    requestRenderIfNotRequested();
-  }
+	async function loadJSON( url ) {
 
 
-  function unselectAllCountries() {
-    numCountriesSelected = 0;
-    countryInfos.forEach((countryInfo) => {
-      countryInfo.selected = false;
-    });
-  }
+		const req = await fetch( url );
+		return req.json();
 
 
-  canvas.addEventListener('pointerup', pickCountry);
+	}
 
 
-  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;
-  }
+	let numCountriesSelected = 0;
+	let countryInfos;
+	async function loadCountryData() {
 
 
-  let renderRequested = false;
+		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */
 
 
-  function render() {
-    renderRequested = undefined;
+		const lonFudge = Math.PI * 1.5;
+		const latFudge = Math.PI;
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const labelParentElem = document.querySelector( '#labels' );
+		for ( const countryInfo of countryInfos ) {
 
 
-    controls.update();
+			const { lat, lon, min, max, name } = countryInfo;
 
 
-    updateLabels();
+			// adjust the helpers to point to the latitude and longitude
+			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
+			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;
 
 
-    renderer.render(scene, camera);
-  }
-  render();
+			// get the position of the lat/lon
+			positionHelper.updateWorldMatrix( true, false );
+			const position = new THREE.Vector3();
+			positionHelper.getWorldPosition( position );
+			countryInfo.position = position;
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+			// compute the area for each country
+			const width = max[ 0 ] - min[ 0 ];
+			const height = max[ 1 ] - min[ 1 ];
+			const area = width * height;
+			countryInfo.area = area;
+
+			// add an element for each country
+			const elem = document.createElement( 'div' );
+			elem.textContent = name;
+			labelParentElem.appendChild( elem );
+			countryInfo.elem = elem;
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	loadCountryData();
+
+	const tempV = new THREE.Vector3();
+	const cameraToPoint = new THREE.Vector3();
+	const cameraPosition = new THREE.Vector3();
+	const normalMatrix = new THREE.Matrix3();
+
+	const settings = {
+		minArea: 20,
+		maxVisibleDot: - 0.2,
+	};
+
+	function updateLabels() {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const large = settings.minArea * settings.minArea;
+		// get a matrix that represents a relative orientation of the camera
+		normalMatrix.getNormalMatrix( camera.matrixWorldInverse );
+		// get the camera's position
+		camera.getWorldPosition( cameraPosition );
+		for ( const countryInfo of countryInfos ) {
+
+			const { position, elem, area, selected } = countryInfo;
+			const largeEnough = area >= large;
+			const show = selected || ( numCountriesSelected === 0 && largeEnough );
+			if ( ! show ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// Orient the position based on the camera's orientation.
+			// Since the sphere is at the origin and the sphere is a unit sphere
+			// this gives us a camera relative direction vector for the position.
+			tempV.copy( position );
+			tempV.applyMatrix3( normalMatrix );
+
+			// compute the direction to this position from the camera
+			cameraToPoint.copy( position );
+			cameraToPoint.applyMatrix4( camera.matrixWorldInverse ).normalize();
+
+			// get the dot product of camera relative direction to this position
+			// on the globe with the direction from the camera to that point.
+			// -1 = facing directly towards the camera
+			// 0 = exactly on tangent of the sphere from the camera
+			// > 0 = facing away
+			const dot = tempV.dot( cameraToPoint );
+
+			// if the orientation is not facing us hide it.
+			if ( dot > settings.maxVisibleDot ) {
+
+				elem.style.display = 'none';
+				continue;
+
+			}
+
+			// restore the element to its default display style
+			elem.style.display = '';
+
+			// get the normalized screen coordinate of that position
+			// x and y will be in the -1 to +1 range with x = -1 being
+			// on the left and y = -1 being on the bottom
+			tempV.copy( position );
+			tempV.project( camera );
+
+			// convert the normalized position to CSS coordinates
+			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
+			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;
+
+			// move the elem to that position
+			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;
+
+			// set the zIndex for sorting
+			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;
+
+		}
+
+	}
+
+	class GPUPickHelper {
+
+		constructor() {
+
+			// create a 1x1 pixel render target
+			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
+			this.pixelBuffer = new Uint8Array( 4 );
+
+		}
+		pick( cssPosition, scene, camera ) {
+
+			const { pickingTexture, pixelBuffer } = this;
+
+			// set the view offset to represent just a single pixel under the mouse
+			const pixelRatio = renderer.getPixelRatio();
+			camera.setViewOffset(
+				renderer.getContext().drawingBufferWidth, // full width
+				renderer.getContext().drawingBufferHeight, // full top
+				cssPosition.x * pixelRatio | 0, // rect x
+				cssPosition.y * pixelRatio | 0, // rect y
+				1, // rect width
+				1, // rect height
+			);
+			// render the scene
+			renderer.setRenderTarget( pickingTexture );
+			renderer.render( scene, camera );
+			renderer.setRenderTarget( null );
+			// clear the view offset so rendering returns to normal
+			camera.clearViewOffset();
+			//read the pixel
+			renderer.readRenderTargetPixels(
+				pickingTexture,
+				0, // x
+				0, // y
+				1, // width
+				1, // height
+				pixelBuffer );
+
+			const id =
+          ( pixelBuffer[ 0 ] << 0 ) |
+          ( pixelBuffer[ 1 ] << 8 ) |
+          ( pixelBuffer[ 2 ] << 16 );
+
+			return id;
+
+		}
+
+	}
+
+	const pickHelper = new GPUPickHelper();
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function pickCountry( event ) {
+
+		// exit if we have not loaded the data yet
+		if ( ! countryInfos ) {
+
+			return;
+
+		}
+
+		const position = getCanvasRelativePosition( event );
+		const id = pickHelper.pick( position, pickingScene, camera );
+		if ( id > 0 ) {
+
+			const countryInfo = countryInfos[ id - 1 ];
+			const selected = ! countryInfo.selected;
+			if ( selected && ! event.shiftKey && ! event.ctrlKey && ! event.metaKey ) {
+
+				unselectAllCountries();
+
+			}
+
+			numCountriesSelected += selected ? 1 : - 1;
+			countryInfo.selected = selected;
+
+		} else if ( numCountriesSelected ) {
+
+			unselectAllCountries();
+
+		}
+
+		requestRenderIfNotRequested();
+
+	}
+
+	function unselectAllCountries() {
+
+		numCountriesSelected = 0;
+		countryInfos.forEach( ( countryInfo ) => {
+
+			countryInfo.selected = false;
+
+		} );
+
+	}
+
+	canvas.addEventListener( 'pointerup', pickCountry );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+
+		updateLabels();
+
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 128 - 98
manual/examples/lights-ambient.html

@@ -35,115 +35,145 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.AmbientLight(color, intensity);
-    scene.add(light);
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 2, 0.01);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function render() {
+	{
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeSize = 40;
 
 
-    renderer.render(scene, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    requestAnimationFrame(render);
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 );
+
+		}
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.AmbientLight( color, intensity );
+		scene.add( light );
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 5, 0.01 );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 154 - 119
manual/examples/lights-directional-w-helper.html

@@ -35,138 +35,173 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function makeXYZGUI(gui, vector3, name, onChangeFn) {
-    const folder = gui.addFolder(name);
-    folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
-    folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
-    folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
-    folder.open();
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(0, 10, 0);
-    light.target.position.set(-5, 0, 0);
-    scene.add(light);
-    scene.add(light.target);
-
-    const helper = new THREE.DirectionalLightHelper(light);
-    scene.add(helper);
-
-    function updateLight() {
-      light.target.updateMatrixWorld();
-      helper.update();
-    }
-    updateLight();
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 2, 0.01);
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    makeXYZGUI(gui, light.position, 'position', updateLight);
-    makeXYZGUI(gui, light.target.position, 'target', updateLight);
-  }
+	{
 
 
-  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;
-  }
+		const planeSize = 40;
 
 
-  function render() {
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	{
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 makeXYZGUI( gui, vector3, name, onChangeFn ) {
+
+		const folder = gui.addFolder( name );
+		folder.add( vector3, 'x', - 10, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'y', 0, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'z', - 10, 10 ).onChange( onChangeFn );
+		folder.open();
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		light.target.position.set( - 5, 0, 0 );
+		scene.add( light );
+		scene.add( light.target );
+
+		const helper = new THREE.DirectionalLightHelper( light );
+		scene.add( helper );
+
+		function updateLight() {
+
+			light.target.updateMatrixWorld();
+			helper.update();
+
+		}
+
+		updateLight();
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 5, 0.01 );
+
+		makeXYZGUI( gui, light.position, 'position', updateLight );
+		makeXYZGUI( gui, light.target.position, 'target', updateLight );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 135 - 105
manual/examples/lights-directional.html

@@ -35,121 +35,151 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(0, 10, 0);
-    light.target.position.set(-5, 0, 0);
-    scene.add(light);
-    scene.add(light.target);
-
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 2, 0.01);
-    gui.add(light.target.position, 'x', -10, 10, .01);
-    gui.add(light.target.position, 'z', -10, 10, .01);
-    gui.add(light.target.position, 'y', 0, 10, .01);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  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;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  function render() {
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	{
 
 
-    renderer.render(scene, camera);
+		const planeSize = 40;
 
 
-    requestAnimationFrame(render);
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 );
+
+		}
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		light.target.position.set( - 5, 0, 0 );
+		scene.add( light );
+		scene.add( light.target );
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 5, 0.01 );
+		gui.add( light.target.position, 'x', - 10, 10, .01 );
+		gui.add( light.target.position, 'z', - 10, 10, .01 );
+		gui.add( light.target.position, 'y', 0, 10, .01 );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 131 - 101
manual/examples/lights-hemisphere.html

@@ -35,117 +35,147 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 1;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('skyColor');
-    gui.addColor(new ColorGUIHelper(light, 'groundColor'), 'value').name('groundColor');
-    gui.add(light, 'intensity', 0, 2, 0.01);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  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;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  function render() {
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	{
 
 
-    renderer.render(scene, camera);
+		const planeSize = 40;
 
 
-    requestAnimationFrame(render);
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 );
+
+		}
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 1;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'skyColor' );
+		gui.addColor( new ColorGUIHelper( light, 'groundColor' ), 'value' ).name( 'groundColor' );
+		gui.add( light, 'intensity', 0, 5, 0.01 );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 0 - 171
manual/examples/lights-point-physically-correct.html

@@ -1,171 +0,0 @@
-<!-- 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 - Lights - Physically Correct Lights</title>
-    <style>
-    html, body {
-        margin: 0;
-        height: 100%;
-    }
-    #c {
-        width: 100%;
-        height: 100%;
-        display: block;
-    }
-    </style>
-  </head>
-  <body>
-    <canvas id="c"></canvas>
-  </body>
-<!-- Import maps polyfill -->
-<!-- Remove this when import maps will be widely supported -->
-<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
-
-<script type="importmap">
-{
-  "imports": {
-    "three": "../../build/three.module.js",
-    "three/addons/": "../../examples/jsm/"
-  }
-}
-</script>
-
-<script type="module">
-import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
-
-function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.physicallyCorrectLights = true;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
-
-  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 makeXYZGUI(gui, vector3, name, onChangeFn) {
-    const folder = gui.addFolder(name);
-    folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
-    folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
-    folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
-    folder.open();
-  }
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.PointLight(color, intensity);
-    light.power = 800;
-    light.decay = 2;
-    light.distance = Infinity;
-    light.position.set(0, 10, 0);
-    scene.add(light);
-
-    const helper = new THREE.PointLightHelper(light);
-    scene.add(helper);
-
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'decay', 0, 4, 0.01);
-    gui.add(light, 'power', 0, 1220);
-
-    makeXYZGUI(gui, light.position, 'position');
-  }
-
-  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);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
-}
-
-main();
-</script>
-</html>
-

+ 147 - 113
manual/examples/lights-point.html

@@ -35,134 +35,168 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function makeXYZGUI(gui, vector3, name, onChangeFn) {
-    const folder = gui.addFolder(name);
-    folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
-    folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
-    folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
-    folder.open();
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.PointLight(color, intensity);
-    light.position.set(0, 10, 0);
-    scene.add(light);
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-    const helper = new THREE.PointLightHelper(light);
-    scene.add(helper);
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    function updateLight() {
-      helper.update();
-    }
+	{
 
 
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 2, 0.01);
-    gui.add(light, 'distance', 0, 40).onChange(updateLight);
+		const planeSize = 40;
 
 
-    makeXYZGUI(gui, light.position, 'position');
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-  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;
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-  function render() {
+	}
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	{
 
 
-    renderer.render(scene, camera);
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 makeXYZGUI( gui, vector3, name, onChangeFn ) {
+
+		const folder = gui.addFolder( name );
+		folder.add( vector3, 'x', - 10, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'y', 0, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'z', - 10, 10 ).onChange( onChangeFn );
+		folder.open();
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 150;
+		const light = new THREE.PointLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		scene.add( light );
+
+		const helper = new THREE.PointLightHelper( light );
+		scene.add( helper );
+
+		function updateLight() {
+
+			helper.update();
+
+		}
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 250, 1 );
+		gui.add( light, 'distance', 0, 40 ).onChange( updateLight );
+
+		makeXYZGUI( gui, light.position, 'position' );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 175 - 135
manual/examples/lights-rectarea.html

@@ -35,153 +35,193 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {RectAreaLightUniformsLib} from 'three/addons/lights/RectAreaLightUniformsLib.js';
-import {RectAreaLightHelper} from 'three/addons/helpers/RectAreaLightHelper.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
+import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  RectAreaLightUniformsLib.init();
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshStandardMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshStandardMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshStandardMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+	RectAreaLightUniformsLib.init();
 
 
-  class DegRadHelper {
-    constructor(obj, prop) {
-      this.obj = obj;
-      this.prop = prop;
-    }
-    get value() {
-      return THREE.MathUtils.radToDeg(this.obj[this.prop]);
-    }
-    set value(v) {
-      this.obj[this.prop] = THREE.MathUtils.degToRad(v);
-    }
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function makeXYZGUI(gui, vector3, name, onChangeFn) {
-    const folder = gui.addFolder(name);
-    folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
-    folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
-    folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
-    folder.open();
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 5;
-    const width = 12;
-    const height = 4;
-    const light = new THREE.RectAreaLight(color, intensity, width, height);
-    light.position.set(0, 10, 0);
-    light.rotation.x = THREE.MathUtils.degToRad(-90);
-    scene.add(light);
-
-    const helper = new RectAreaLightHelper(light);
-    light.add(helper);
-
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 10, 0.01);
-    gui.add(light, 'width', 0, 20);
-    gui.add(light, 'height', 0, 20);
-    gui.add(new DegRadHelper(light.rotation, 'x'), 'value', -180, 180).name('x rotation');
-    gui.add(new DegRadHelper(light.rotation, 'y'), 'value', -180, 180).name('y rotation');
-    gui.add(new DegRadHelper(light.rotation, 'z'), 'value', -180, 180).name('z rotation');
-
-    makeXYZGUI(gui, light.position, 'position');
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  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() {
+		const planeSize = 40;
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    renderer.render(scene, camera);
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshStandardMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	{
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshStandardMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshStandardMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 );
+
+		}
+
+	}
+
+	class DegRadHelper {
+
+		constructor( obj, prop ) {
+
+			this.obj = obj;
+			this.prop = prop;
+
+		}
+		get value() {
+
+			return THREE.MathUtils.radToDeg( this.obj[ this.prop ] );
+
+		}
+		set value( v ) {
+
+			this.obj[ this.prop ] = THREE.MathUtils.degToRad( v );
+
+		}
+
+	}
+
+	function makeXYZGUI( gui, vector3, name, onChangeFn ) {
+
+		const folder = gui.addFolder( name );
+		folder.add( vector3, 'x', - 10, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'y', 0, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'z', - 10, 10 ).onChange( onChangeFn );
+		folder.open();
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 5;
+		const width = 12;
+		const height = 4;
+		const light = new THREE.RectAreaLight( color, intensity, width, height );
+		light.position.set( 0, 10, 0 );
+		light.rotation.x = THREE.MathUtils.degToRad( - 90 );
+		scene.add( light );
+
+		const helper = new RectAreaLightHelper( light );
+		light.add( helper );
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 10, 0.01 );
+		gui.add( light, 'width', 0, 20 );
+		gui.add( light, 'height', 0, 20 );
+		gui.add( new DegRadHelper( light.rotation, 'x' ), 'value', - 180, 180 ).name( 'x rotation' );
+		gui.add( new DegRadHelper( light.rotation, 'y' ), 'value', - 180, 180 ).name( 'y rotation' );
+		gui.add( new DegRadHelper( light.rotation, 'z' ), 'value', - 180, 180 ).name( 'z rotation' );
+
+		makeXYZGUI( gui, light.position, 'position' );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 177 - 134
manual/examples/lights-spot-w-helper.html

@@ -35,154 +35,197 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
-  {
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
-    scene.add(mesh);
-  }
-  {
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
-    scene.add(mesh);
-  }
 
 
-  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);
-    }
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  class DegRadHelper {
-    constructor(obj, prop) {
-      this.obj = obj;
-      this.prop = prop;
-    }
-    get value() {
-      return THREE.MathUtils.radToDeg(this.obj[this.prop]);
-    }
-    set value(v) {
-      this.obj[this.prop] = THREE.MathUtils.degToRad(v);
-    }
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function makeXYZGUI(gui, vector3, name, onChangeFn) {
-    const folder = gui.addFolder(name);
-    folder.add(vector3, 'x', -10, 10).onChange(onChangeFn);
-    folder.add(vector3, 'y', 0, 10).onChange(onChangeFn);
-    folder.add(vector3, 'z', -10, 10).onChange(onChangeFn);
-    folder.open();
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.SpotLight(color, intensity);
-    light.position.set(0, 10, 0);
-    light.target.position.set(-5, 0, 0);
-    scene.add(light);
-    scene.add(light.target);
-
-    const helper = new THREE.SpotLightHelper(light);
-    scene.add(helper);
-
-    function updateLight() {
-      light.target.updateMatrixWorld();
-      helper.update();
-    }
-    updateLight();
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    const gui = new GUI();
-    gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-    gui.add(light, 'intensity', 0, 2, 0.01);
-    gui.add(light, 'distance', 0, 40).onChange(updateLight);
-    gui.add(new DegRadHelper(light, 'angle'), 'value', 0, 90).name('angle').onChange(updateLight);
-    gui.add(light, 'penumbra', 0, 1, 0.01);
+	{
 
 
-    makeXYZGUI(gui, light.position, 'position', updateLight);
-    makeXYZGUI(gui, light.target.position, 'target', updateLight);
-  }
+		const planeSize = 40;
 
 
-  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;
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-  function render() {
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	}
 
 
-    renderer.render(scene, camera);
+	{
 
 
-    requestAnimationFrame(render);
-  }
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+		const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+		mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+		const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+		mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
+		scene.add( mesh );
+
+	}
+
+	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 );
+
+		}
+
+	}
+
+	class DegRadHelper {
+
+		constructor( obj, prop ) {
+
+			this.obj = obj;
+			this.prop = prop;
+
+		}
+		get value() {
+
+			return THREE.MathUtils.radToDeg( this.obj[ this.prop ] );
+
+		}
+		set value( v ) {
+
+			this.obj[ this.prop ] = THREE.MathUtils.degToRad( v );
+
+		}
+
+	}
+
+	function makeXYZGUI( gui, vector3, name, onChangeFn ) {
+
+		const folder = gui.addFolder( name );
+		folder.add( vector3, 'x', - 10, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'y', 0, 10 ).onChange( onChangeFn );
+		folder.add( vector3, 'z', - 10, 10 ).onChange( onChangeFn );
+		folder.open();
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 150;
+		const light = new THREE.SpotLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		light.target.position.set( - 5, 0, 0 );
+		scene.add( light );
+		scene.add( light.target );
+
+		const helper = new THREE.SpotLightHelper( light );
+		scene.add( helper );
+
+		function updateLight() {
+
+			light.target.updateMatrixWorld();
+			helper.update();
+
+		}
+
+		updateLight();
+
+		const gui = new GUI();
+		gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
+		gui.add( light, 'intensity', 0, 250, 1 );
+		gui.add( light, 'distance', 0, 40 ).onChange( updateLight );
+		gui.add( new DegRadHelper( light, 'angle' ), 'value', 0, 90 ).name( 'angle' ).onChange( updateLight );
+		gui.add( light, 'penumbra', 0, 1, 0.01 );
+
+		makeXYZGUI( gui, light.position, 'position', updateLight );
+		makeXYZGUI( gui, light.target.position, 'target', updateLight );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 258 - 222
manual/examples/load-gltf-animated-cars.html

@@ -35,243 +35,279 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
+
+	{
+
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	let curve;
+	let curveObject;
+	{
+
+		const controlPoints = [
+			[ 1.118281, 5.115846, - 3.681386 ],
+			[ 3.948875, 5.115846, - 3.641834 ],
+			[ 3.960072, 5.115846, - 0.240352 ],
+			[ 3.985447, 5.115846, 4.585005 ],
+			[ - 3.793631, 5.115846, 4.585006 ],
+			[ - 3.826839, 5.115846, - 14.736200 ],
+			[ - 14.542292, 5.115846, - 14.765865 ],
+			[ - 14.520929, 5.115846, - 3.627002 ],
+			[ - 5.452815, 5.115846, - 3.634418 ],
+			[ - 5.467251, 5.115846, 4.549161 ],
+			[ - 13.266233, 5.115846, 4.567083 ],
+			[ - 13.250067, 5.115846, - 13.499271 ],
+			[ 4.081842, 5.115846, - 13.435463 ],
+			[ 4.125436, 5.115846, - 5.334928 ],
+			[ - 14.521364, 5.115846, - 5.239871 ],
+			[ - 14.510466, 5.115846, 5.486727 ],
+			[ 5.745666, 5.115846, 5.510492 ],
+			[ 5.787942, 5.115846, - 14.728308 ],
+			[ - 5.423720, 5.115846, - 14.761919 ],
+			[ - 5.373599, 5.115846, - 3.704133 ],
+			[ 1.004861, 5.115846, - 3.641834 ],
+		];
+		const p0 = new THREE.Vector3();
+		const p1 = new THREE.Vector3();
+		curve = new THREE.CatmullRomCurve3(
+			controlPoints.map( ( p, ndx ) => {
+
+				p0.set( ...p );
+				p1.set( ...controlPoints[ ( ndx + 1 ) % controlPoints.length ] );
+				return [
+					( new THREE.Vector3() ).copy( p0 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.1 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.9 ),
+				];
+
+			} ).flat(),
+			true,
+		);
+		{
+
+			const points = curve.getPoints( 250 );
+			const geometry = new THREE.BufferGeometry().setFromPoints( points );
+			const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
+			curveObject = new THREE.Line( geometry, material );
+			curveObject.scale.set( 100, 100, 100 );
+			curveObject.position.y = - 621;
+			curveObject.visible = false;
+			material.depthTest = false;
+			curveObject.renderOrder = 1;
+			scene.add( curveObject );
+
+		}
+
+	}
+
+	const cars = [];
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+			const loadedCars = root.getObjectByName( 'Cars' );
+			const fixes = [
+				{ prefix: 'Car_08', y: 0, rot: [ Math.PI * .5, 0, Math.PI * .5 ], },
+				{ prefix: 'CAR_03', y: 33, rot: [ 0, Math.PI, 0 ], },
+				{ prefix: 'Car_04', y: 40, rot: [ 0, Math.PI, 0 ], },
+			];
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+			root.updateMatrixWorld();
+			for ( const car of loadedCars.children.slice() ) {
 
 
-  let curve;
-  let curveObject;
-  {
-    const controlPoints = [
-      [1.118281, 5.115846, -3.681386],
-      [3.948875, 5.115846, -3.641834],
-      [3.960072, 5.115846, -0.240352],
-      [3.985447, 5.115846, 4.585005],
-      [-3.793631, 5.115846, 4.585006],
-      [-3.826839, 5.115846, -14.736200],
-      [-14.542292, 5.115846, -14.765865],
-      [-14.520929, 5.115846, -3.627002],
-      [-5.452815, 5.115846, -3.634418],
-      [-5.467251, 5.115846, 4.549161],
-      [-13.266233, 5.115846, 4.567083],
-      [-13.250067, 5.115846, -13.499271],
-      [4.081842, 5.115846, -13.435463],
-      [4.125436, 5.115846, -5.334928],
-      [-14.521364, 5.115846, -5.239871],
-      [-14.510466, 5.115846, 5.486727],
-      [5.745666, 5.115846, 5.510492],
-      [5.787942, 5.115846, -14.728308],
-      [-5.423720, 5.115846, -14.761919],
-      [-5.373599, 5.115846, -3.704133],
-      [1.004861, 5.115846, -3.641834],
-    ];
-    const p0 = new THREE.Vector3();
-    const p1 = new THREE.Vector3();
-    curve = new THREE.CatmullRomCurve3(
-      controlPoints.map((p, ndx) => {
-        p0.set(...p);
-        p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
-        return [
-          (new THREE.Vector3()).copy(p0),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
-        ];
-      }).flat(),
-      true,
-    );
-    {
-      const points = curve.getPoints(250);
-      const geometry = new THREE.BufferGeometry().setFromPoints(points);
-      const material = new THREE.LineBasicMaterial({color: 0xff0000});
-      curveObject = new THREE.Line(geometry, material);
-      curveObject.scale.set(100, 100, 100);
-      curveObject.position.y = -621;
-      curveObject.visible = false;
-      material.depthTest = false;
-      curveObject.renderOrder = 1;
-      scene.add(curveObject);
-    }
-  }
+				const fix = fixes.find( fix => car.name.startsWith( fix.prefix ) );
+				const obj = new THREE.Object3D();
+				car.position.set( 0, fix.y, 0 );
+				car.rotation.set( ...fix.rot );
+				obj.add( car );
+				scene.add( obj );
+				cars.push( obj );
 
 
-  const cars = [];
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      const loadedCars = root.getObjectByName('Cars');
-      const fixes = [
-        { prefix: 'Car_08', y: 0,  rot: [Math.PI * .5, 0, Math.PI * .5], },
-        { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], },
-        { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], },
-      ];
-
-      root.updateMatrixWorld();
-      for (const car of loadedCars.children.slice()) {
-        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
-        const obj = new THREE.Object3D();
-        car.position.set(0, fix.y, 0);
-        car.rotation.set(...fix.rot);
-        obj.add(car);
-        scene.add(obj);
-        cars.push(obj);
-      }
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+			}
 
 
-  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;
-  }
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
 
 
-  // create 2 Vector3s we can use for path calculations
-  const carPosition = new THREE.Vector3();
-  const carTarget = new THREE.Vector3();
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
 
 
-    {
-      const pathTime = time * .01;
-      const targetOffset = 0.01;
-      cars.forEach((car, ndx) => {
-        // a number between 0 and 1 to evenly space the cars
-        const u = pathTime + ndx / cars.length;
-
-        // get the first point
-        curve.getPointAt(u % 1, carPosition);
-        carPosition.applyMatrix4(curveObject.matrixWorld);
-
-        // get a second point slightly further down the curve
-        curve.getPointAt((u + targetOffset) % 1, carTarget);
-        carTarget.applyMatrix4(curveObject.matrixWorld);
-
-        // put the car at the first point (temporarily)
-        car.position.copy(carPosition);
-        // point the car the second point
-        car.lookAt(carTarget);
-
-        // put the car between the 2 points
-        car.position.lerpVectors(carPosition, carTarget, 0.5);
-      });
-    }
+		} );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	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;
+
+	}
+
+	// create 2 Vector3s we can use for path calculations
+	const carPosition = new THREE.Vector3();
+	const carTarget = new THREE.Vector3();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		{
+
+			const pathTime = time * .01;
+			const targetOffset = 0.01;
+			cars.forEach( ( car, ndx ) => {
+
+				// a number between 0 and 1 to evenly space the cars
+				const u = pathTime + ndx / cars.length;
+
+				// get the first point
+				curve.getPointAt( u % 1, carPosition );
+				carPosition.applyMatrix4( curveObject.matrixWorld );
+
+				// get a second point slightly further down the curve
+				curve.getPointAt( ( u + targetOffset ) % 1, carTarget );
+				carTarget.applyMatrix4( curveObject.matrixWorld );
+
+				// put the car at the first point (temporarily)
+				car.position.copy( carPosition );
+				// point the car the second point
+				car.lookAt( carTarget );
+
+				// put the car between the 2 points
+				car.position.lerpVectors( carPosition, carTarget, 0.5 );
+
+			} );
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 233 - 199
manual/examples/load-gltf-car-path-fixed.html

@@ -35,219 +35,253 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
+
+	{
+
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	let curve;
+	let curveObject;
+	{
+
+		const controlPoints = [
+			[ 1.118281, 5.115846, - 3.681386 ],
+			[ 3.948875, 5.115846, - 3.641834 ],
+			[ 3.960072, 5.115846, - 0.240352 ],
+			[ 3.985447, 5.115846, 4.585005 ],
+			[ - 3.793631, 5.115846, 4.585006 ],
+			[ - 3.826839, 5.115846, - 14.736200 ],
+			[ - 14.542292, 5.115846, - 14.765865 ],
+			[ - 14.520929, 5.115846, - 3.627002 ],
+			[ - 5.452815, 5.115846, - 3.634418 ],
+			[ - 5.467251, 5.115846, 4.549161 ],
+			[ - 13.266233, 5.115846, 4.567083 ],
+			[ - 13.250067, 5.115846, - 13.499271 ],
+			[ 4.081842, 5.115846, - 13.435463 ],
+			[ 4.125436, 5.115846, - 5.334928 ],
+			[ - 14.521364, 5.115846, - 5.239871 ],
+			[ - 14.510466, 5.115846, 5.486727 ],
+			[ 5.745666, 5.115846, 5.510492 ],
+			[ 5.787942, 5.115846, - 14.728308 ],
+			[ - 5.423720, 5.115846, - 14.761919 ],
+			[ - 5.373599, 5.115846, - 3.704133 ],
+			[ 1.004861, 5.115846, - 3.641834 ],
+		];
+		const p0 = new THREE.Vector3();
+		const p1 = new THREE.Vector3();
+		curve = new THREE.CatmullRomCurve3(
+			controlPoints.map( ( p, ndx ) => {
+
+				p0.set( ...p );
+				p1.set( ...controlPoints[ ( ndx + 1 ) % controlPoints.length ] );
+				return [
+					( new THREE.Vector3() ).copy( p0 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.1 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.9 ),
+				];
+
+			} ).flat(),
+			true,
+		);
+		{
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+			const points = curve.getPoints( 250 );
+			const geometry = new THREE.BufferGeometry().setFromPoints( points );
+			const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
+			curveObject = new THREE.Line( geometry, material );
+			curveObject.scale.set( 100, 100, 100 );
+			curveObject.position.y = - 621;
+			//curveObject.visible = false;
+			material.depthTest = false;
+			curveObject.renderOrder = 1;
+			scene.add( curveObject );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+		}
 
 
-  let curve;
-  let curveObject;
-  {
-    const controlPoints = [
-      [1.118281, 5.115846, -3.681386],
-      [3.948875, 5.115846, -3.641834],
-      [3.960072, 5.115846, -0.240352],
-      [3.985447, 5.115846, 4.585005],
-      [-3.793631, 5.115846, 4.585006],
-      [-3.826839, 5.115846, -14.736200],
-      [-14.542292, 5.115846, -14.765865],
-      [-14.520929, 5.115846, -3.627002],
-      [-5.452815, 5.115846, -3.634418],
-      [-5.467251, 5.115846, 4.549161],
-      [-13.266233, 5.115846, 4.567083],
-      [-13.250067, 5.115846, -13.499271],
-      [4.081842, 5.115846, -13.435463],
-      [4.125436, 5.115846, -5.334928],
-      [-14.521364, 5.115846, -5.239871],
-      [-14.510466, 5.115846, 5.486727],
-      [5.745666, 5.115846, 5.510492],
-      [5.787942, 5.115846, -14.728308],
-      [-5.423720, 5.115846, -14.761919],
-      [-5.373599, 5.115846, -3.704133],
-      [1.004861, 5.115846, -3.641834],
-    ];
-    const p0 = new THREE.Vector3();
-    const p1 = new THREE.Vector3();
-    curve = new THREE.CatmullRomCurve3(
-      controlPoints.map((p, ndx) => {
-        p0.set(...p);
-        p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
-        return [
-          (new THREE.Vector3()).copy(p0),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
-        ];
-      }).flat(),
-      true,
-    );
-    {
-      const points = curve.getPoints(250);
-      const geometry = new THREE.BufferGeometry().setFromPoints(points);
-      const material = new THREE.LineBasicMaterial({color: 0xff0000});
-      curveObject = new THREE.Line(geometry, material);
-      curveObject.scale.set(100, 100, 100);
-      curveObject.position.y = -621;
-      //curveObject.visible = false;
-      material.depthTest = false;
-      curveObject.renderOrder = 1;
-      scene.add(curveObject);
-    }
-  }
+	}
 
 
-  const cars = [];
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      const loadedCars = root.getObjectByName('Cars');
-      const fixes = [
-        { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
-        { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
-        { prefix: 'Car_04', rot: [0, Math.PI, 0], },
-      ];
-
-      root.updateMatrixWorld();
-      for (const car of loadedCars.children.slice()) {
-        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
-        const obj = new THREE.Object3D();
-        car.getWorldPosition(obj.position);
-        car.position.set(0, 0, 0);
-        car.rotation.set(...fix.rot);
-        obj.add(car);
-        scene.add(obj);
-        cars.push(obj);
-      }
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+	const cars = [];
+	{
 
 
-  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;
-  }
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds
+			const root = gltf.scene;
+			scene.add( root );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			const loadedCars = root.getObjectByName( 'Cars' );
+			const fixes = [
+				{ prefix: 'Car_08', rot: [ Math.PI * .5, 0, Math.PI * .5 ], },
+				{ prefix: 'CAR_03', rot: [ 0, Math.PI, 0 ], },
+				{ prefix: 'Car_04', rot: [ 0, Math.PI, 0 ], },
+			];
 
 
-    for (const car of cars) {
-      car.rotation.y = time;
-    }
+			root.updateMatrixWorld();
+			for ( const car of loadedCars.children.slice() ) {
 
 
-    renderer.render(scene, camera);
+				const fix = fixes.find( fix => car.name.startsWith( fix.prefix ) );
+				const obj = new THREE.Object3D();
+				car.getWorldPosition( obj.position );
+				car.position.set( 0, 0, 0 );
+				car.rotation.set( ...fix.rot );
+				obj.add( car );
+				scene.add( obj );
+				cars.push( obj );
 
 
-    requestAnimationFrame(render);
-  }
+			}
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		for ( const car of cars ) {
+
+			car.rotation.y = time;
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 230 - 196
manual/examples/load-gltf-car-path.html

@@ -35,216 +35,250 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
+
+	{
+
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	let curve;
+	let curveObject;
+	{
+
+		const controlPoints = [
+			[ 1.118281, 5.115846, - 3.681386 ],
+			[ 3.948875, 5.115846, - 3.641834 ],
+			[ 3.960072, 5.115846, - 0.240352 ],
+			[ 3.985447, 5.115846, 4.585005 ],
+			[ - 3.793631, 5.115846, 4.585006 ],
+			[ - 3.826839, 5.115846, - 14.736200 ],
+			[ - 14.542292, 5.115846, - 14.765865 ],
+			[ - 14.520929, 5.115846, - 3.627002 ],
+			[ - 5.452815, 5.115846, - 3.634418 ],
+			[ - 5.467251, 5.115846, 4.549161 ],
+			[ - 13.266233, 5.115846, 4.567083 ],
+			[ - 13.250067, 5.115846, - 13.499271 ],
+			[ 4.081842, 5.115846, - 13.435463 ],
+			[ 4.125436, 5.115846, - 5.334928 ],
+			[ - 14.521364, 5.115846, - 5.239871 ],
+			[ - 14.510466, 5.115846, 5.486727 ],
+			[ 5.745666, 5.115846, 5.510492 ],
+			[ 5.787942, 5.115846, - 14.728308 ],
+			[ - 5.423720, 5.115846, - 14.761919 ],
+			[ - 5.373599, 5.115846, - 3.704133 ],
+			[ 1.004861, 5.115846, - 3.641834 ],
+		];
+		const p0 = new THREE.Vector3();
+		const p1 = new THREE.Vector3();
+		curve = new THREE.CatmullRomCurve3(
+			controlPoints.map( ( p, ndx ) => {
+
+				p0.set( ...p );
+				p1.set( ...controlPoints[ ( ndx + 1 ) % controlPoints.length ] );
+				return [
+					( new THREE.Vector3() ).copy( p0 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.1 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.9 ),
+				];
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+			} ).flat(),
+			true,
+		);
+		{
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+			const points = curve.getPoints( 250 );
+			const geometry = new THREE.BufferGeometry().setFromPoints( points );
+			const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
+			curveObject = new THREE.Line( geometry, material );
+			material.depthTest = false;
+			curveObject.renderOrder = 1;
+			scene.add( curveObject );
 
 
-  let curve;
-  let curveObject;
-  {
-    const controlPoints = [
-      [1.118281, 5.115846, -3.681386],
-      [3.948875, 5.115846, -3.641834],
-      [3.960072, 5.115846, -0.240352],
-      [3.985447, 5.115846, 4.585005],
-      [-3.793631, 5.115846, 4.585006],
-      [-3.826839, 5.115846, -14.736200],
-      [-14.542292, 5.115846, -14.765865],
-      [-14.520929, 5.115846, -3.627002],
-      [-5.452815, 5.115846, -3.634418],
-      [-5.467251, 5.115846, 4.549161],
-      [-13.266233, 5.115846, 4.567083],
-      [-13.250067, 5.115846, -13.499271],
-      [4.081842, 5.115846, -13.435463],
-      [4.125436, 5.115846, -5.334928],
-      [-14.521364, 5.115846, -5.239871],
-      [-14.510466, 5.115846, 5.486727],
-      [5.745666, 5.115846, 5.510492],
-      [5.787942, 5.115846, -14.728308],
-      [-5.423720, 5.115846, -14.761919],
-      [-5.373599, 5.115846, -3.704133],
-      [1.004861, 5.115846, -3.641834],
-    ];
-    const p0 = new THREE.Vector3();
-    const p1 = new THREE.Vector3();
-    curve = new THREE.CatmullRomCurve3(
-      controlPoints.map((p, ndx) => {
-        p0.set(...p);
-        p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
-        return [
-          (new THREE.Vector3()).copy(p0),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
-        ];
-      }).flat(),
-      true,
-    );
-    {
-      const points = curve.getPoints(250);
-      const geometry = new THREE.BufferGeometry().setFromPoints(points);
-      const material = new THREE.LineBasicMaterial({color: 0xff0000});
-      curveObject = new THREE.Line(geometry, material);
-      material.depthTest = false;
-      curveObject.renderOrder = 1;
-      scene.add(curveObject);
-    }
-  }
+		}
 
 
-  const cars = [];
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      const loadedCars = root.getObjectByName('Cars');
-      const fixes = [
-        { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
-        { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
-        { prefix: 'Car_04', rot: [0, Math.PI, 0], },
-      ];
-
-      root.updateMatrixWorld();
-      for (const car of loadedCars.children.slice()) {
-        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
-        const obj = new THREE.Object3D();
-        car.getWorldPosition(obj.position);
-        car.position.set(0, 0, 0);
-        car.rotation.set(...fix.rot);
-        obj.add(car);
-        scene.add(obj);
-        cars.push(obj);
-      }
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+	}
 
 
-  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;
-  }
+	const cars = [];
+	{
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			const root = gltf.scene;
+			scene.add( root );
 
 
-    for (const car of cars) {
-      car.rotation.y = time;
-    }
+			const loadedCars = root.getObjectByName( 'Cars' );
+			const fixes = [
+				{ prefix: 'Car_08', rot: [ Math.PI * .5, 0, Math.PI * .5 ], },
+				{ prefix: 'CAR_03', rot: [ 0, Math.PI, 0 ], },
+				{ prefix: 'Car_04', rot: [ 0, Math.PI, 0 ], },
+			];
 
 
-    renderer.render(scene, camera);
+			root.updateMatrixWorld();
+			for ( const car of loadedCars.children.slice() ) {
 
 
-    requestAnimationFrame(render);
-  }
+				const fix = fixes.find( fix => car.name.startsWith( fix.prefix ) );
+				const obj = new THREE.Object3D();
+				car.getWorldPosition( obj.position );
+				car.position.set( 0, 0, 0 );
+				car.rotation.set( ...fix.rot );
+				obj.add( car );
+				scene.add( obj );
+				cars.push( obj );
+
+			}
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		for ( const car of cars ) {
+
+			car.rotation.y = time;
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 167 - 138
manual/examples/load-gltf-dump-scenegraph-extra.html

@@ -35,163 +35,192 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  function dumpVec3(v3, precision = 3) {
-    return `${v3.x.toFixed(precision)}, ${v3.y.toFixed(precision)}, ${v3.z.toFixed(precision)}`;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function dumpObject(obj, lines = [], isLast = true, prefix = '') {
-    const localPrefix = isLast ? '└─' : '├─';
-    lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
-    const dataPrefix = obj.children.length
-       ? (isLast ? '  │ ' : '│ │ ')
-       : (isLast ? '    ' : '│   ');
-    lines.push(`${prefix}${dataPrefix}  pos: ${dumpVec3(obj.position)}`);
-    lines.push(`${prefix}${dataPrefix}  rot: ${dumpVec3(obj.rotation)}`);
-    lines.push(`${prefix}${dataPrefix}  scl: ${dumpVec3(obj.scale)}`);
-    const newPrefix = prefix + (isLast ? '  ' : '│ ');
-    const lastNdx = obj.children.length - 1;
-    obj.children.forEach((child, ndx) => {
-      const isLast = ndx === lastNdx;
-      dumpObject(child, lines, isLast, newPrefix);
-    });
-    return lines;
-  }
+	{
+
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	function dumpVec3( v3, precision = 3 ) {
+
+		return `${v3.x.toFixed( precision )}, ${v3.y.toFixed( precision )}, ${v3.z.toFixed( precision )}`;
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
+	}
+
+	function dumpObject( obj, lines = [], isLast = true, prefix = '' ) {
+
+		const localPrefix = isLast ? '└─' : '├─';
+		lines.push( `${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]` );
+		const dataPrefix = obj.children.length
+			? ( isLast ? '  │ ' : '│ │ ' )
+			: ( isLast ? '    ' : '│   ' );
+		lines.push( `${prefix}${dataPrefix}  pos: ${dumpVec3( obj.position )}` );
+		lines.push( `${prefix}${dataPrefix}  rot: ${dumpVec3( obj.rotation )}` );
+		lines.push( `${prefix}${dataPrefix}  scl: ${dumpVec3( obj.scale )}` );
+		const newPrefix = prefix + ( isLast ? '  ' : '│ ' );
+		const lastNdx = obj.children.length - 1;
+		obj.children.forEach( ( child, ndx ) => {
+
+			const isLast = ndx === lastNdx;
+			dumpObject( child, lines, isLast, newPrefix );
+
+		} );
+		return lines;
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
       console.log(dumpObject(root).join('\n'));  // eslint-disable-line
       console.log(dumpObject(root).join('\n'));  // eslint-disable-line
 
 
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
 
 
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
 
 
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
 
 
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
 
 
-  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();
-    }
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    renderer.render(scene, camera);
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth;
+		const height = canvas.clientHeight;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
 
 
-    requestAnimationFrame(render);
-  }
+			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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 156 - 129
manual/examples/load-gltf-dump-scenegraph.html

@@ -35,153 +35,180 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  function dumpObject(obj, lines = [], isLast = true, prefix = '') {
-    const localPrefix = isLast ? '└─' : '├─';
-    lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
-    const newPrefix = prefix + (isLast ? '  ' : '│ ');
-    const lastNdx = obj.children.length - 1;
-    obj.children.forEach((child, ndx) => {
-      const isLast = ndx === lastNdx;
-      dumpObject(child, lines, isLast, newPrefix);
-    });
-    return lines;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
+
+	{
+
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
+
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
+	function dumpObject( obj, lines = [], isLast = true, prefix = '' ) {
+
+		const localPrefix = isLast ? '└─' : '├─';
+		lines.push( `${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]` );
+		const newPrefix = prefix + ( isLast ? '  ' : '│ ' );
+		const lastNdx = obj.children.length - 1;
+		obj.children.forEach( ( child, ndx ) => {
+
+			const isLast = ndx === lastNdx;
+			dumpObject( child, lines, isLast, newPrefix );
+
+		} );
+		return lines;
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
       console.log(dumpObject(root).join('\n'));  // eslint-disable-line
       console.log(dumpObject(root).join('\n'));  // eslint-disable-line
 
 
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
 
 
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
 
 
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
 
 
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
 
 
-  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();
-    }
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    renderer.render(scene, camera);
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth;
+		const height = canvas.clientHeight;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
 
 
-    requestAnimationFrame(render);
-  }
+			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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 174 - 146
manual/examples/load-gltf-rotate-cars-fixed.html

@@ -35,165 +35,193 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
+
+	{
+
+		const planeSize = 40;
 
 
-  const cars = [];
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      const loadedCars = root.getObjectByName('Cars');
-      const fixes = [
-        { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
-        { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
-        { prefix: 'Car_04', rot: [0, Math.PI, 0], },
-      ];
-
-      root.updateMatrixWorld();
-      for (const car of loadedCars.children.slice()) {
-        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
-        const obj = new THREE.Object3D();
-        car.getWorldPosition(obj.position);
-        car.position.set(0, 0, 0);
-        car.rotation.set(...fix.rot);
-        obj.add(car);
-        scene.add(obj);
-        cars.push(obj);
-      }
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-  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;
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds
+	}
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	{
 
 
-    for (const car of cars) {
-      car.rotation.y = time;
-    }
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	const cars = [];
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			const loadedCars = root.getObjectByName( 'Cars' );
+			const fixes = [
+				{ prefix: 'Car_08', rot: [ Math.PI * .5, 0, Math.PI * .5 ], },
+				{ prefix: 'CAR_03', rot: [ 0, Math.PI, 0 ], },
+				{ prefix: 'Car_04', rot: [ 0, Math.PI, 0 ], },
+			];
+
+			root.updateMatrixWorld();
+			for ( const car of loadedCars.children.slice() ) {
+
+				const fix = fixes.find( fix => car.name.startsWith( fix.prefix ) );
+				const obj = new THREE.Object3D();
+				car.getWorldPosition( obj.position );
+				car.position.set( 0, 0, 0 );
+				car.rotation.set( ...fix.rot );
+				obj.add( car );
+				scene.add( obj );
+				cars.push( obj );
+
+			}
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		for ( const car of cars ) {
+
+			car.rotation.y = time;
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 158 - 130
manual/examples/load-gltf-rotate-cars.html

@@ -35,149 +35,177 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  let cars;
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-      cars = root.getObjectByName('Cars');
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  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;  // convert to seconds
+		const planeSize = 40;
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    if (cars) {
-      for (const car of cars.children) {
-        car.rotation.y = time;
-      }
-    }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	let cars;
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+			cars = root.getObjectByName( 'Cars' );
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		if ( cars ) {
+
+			for ( const car of cars.children ) {
+
+				car.rotation.y = time;
+
+			}
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 416 - 339
manual/examples/load-gltf-shadows.html

@@ -35,365 +35,442 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-  renderer.shadowMap.enabled = true;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('#DEFEFF');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+	renderer.shadowMap.enabled = true;
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( '#DEFEFF' );
+
+	{
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.castShadow = true;
-    light.position.set(-250, 800, -850);
-    light.target.position.set(-550, 40, -450);
-
-    light.shadow.bias = -0.004;
-    light.shadow.mapSize.width = 2048;
-    light.shadow.mapSize.height = 2048;
-
-    scene.add(light);
-    scene.add(light.target);
-    const cam = light.shadow.camera;
-    cam.near = 1;
-    cam.far = 2000;
-    cam.left = -1500;
-    cam.right = 1500;
-    cam.top = 1500;
-    cam.bottom = -1500;
-
-    const cameraHelper = new THREE.CameraHelper(cam);
-    scene.add(cameraHelper);
-    cameraHelper.visible = false;
-    const helper = new THREE.DirectionalLightHelper(light, 100);
-    scene.add(helper);
-    helper.visible = false;
-
-    function makeXYZGUI(gui, vector3, name, onChangeFn) {
-      const folder = gui.addFolder(name);
-      folder.add(vector3, 'x', vector3.x - 500, vector3.x + 500).onChange(onChangeFn);
-      folder.add(vector3, 'y', vector3.y - 500, vector3.y + 500).onChange(onChangeFn);
-      folder.add(vector3, 'z', vector3.z - 500, vector3.z + 500).onChange(onChangeFn);
-      folder.open();
-    }
+		const planeSize = 40;
+
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    function updateCamera() {
-      // update the light target's matrixWorld because it's needed by the helper
-      light.updateMatrixWorld();
-      light.target.updateMatrixWorld();
-      helper.update();
-      // update the light's shadow camera's projection matrix
-      light.shadow.camera.updateProjectionMatrix();
-      // and now update the camera helper we're using to show the light's shadow camera
-      cameraHelper.update();
-    }
-    updateCamera();
-
-    class DimensionGUIHelper {
-      constructor(obj, minProp, maxProp) {
-        this.obj = obj;
-        this.minProp = minProp;
-        this.maxProp = maxProp;
-      }
-      get value() {
-        return this.obj[this.maxProp] * 2;
-      }
-      set value(v) {
-        this.obj[this.maxProp] = v /  2;
-        this.obj[this.minProp] = v / -2;
-      }
-    }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    class MinMaxGUIHelper {
-      constructor(obj, minProp, maxProp, minDif) {
-        this.obj = obj;
-        this.minProp = minProp;
-        this.maxProp = maxProp;
-        this.minDif = minDif;
-      }
-      get min() {
-        return this.obj[this.minProp];
-      }
-      set min(v) {
-        this.obj[this.minProp] = v;
-        this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
-      }
-      get max() {
-        return this.obj[this.maxProp];
-      }
-      set max(v) {
-        this.obj[this.maxProp] = v;
-        this.min = this.min;  // this will call the min setter
-      }
-    }
+	}
 
 
-    class VisibleGUIHelper {
-      constructor(...objects) {
-        this.objects = [...objects];
-      }
-      get value() {
-        return this.objects[0].visible;
-      }
-      set value(v) {
-        this.objects.forEach((obj) => {
-          obj.visible = v;
-        });
-      }
-    }
+	{
 
 
-    const gui = new GUI();
-    gui.close();
-    gui.add(new VisibleGUIHelper(helper, cameraHelper), 'value').name('show helpers');
-    gui.add(light.shadow, 'bias', -0.1, 0.1, 0.001);
-    {
-      const folder = gui.addFolder('Shadow Camera');
-      folder.open();
-      folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 4000)
-        .name('width')
-        .onChange(updateCamera);
-      folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 4000 )
-        .name('height')
-        .onChange(updateCamera);
-      const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1);
-      folder.add(minMaxGUIHelper, 'min', 1, 1000, 1).name('near').onChange(updateCamera);
-      folder.add(minMaxGUIHelper, 'max', 1, 4000, 1).name('far').onChange(updateCamera);
-      folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera);
-    }
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
 
 
-    makeXYZGUI(gui, light.position, 'position', updateCamera);
-    makeXYZGUI(gui, light.target.position, 'target', updateCamera);
-  }
+	}
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	{
 
 
-  let curve;
-  let curveObject;
-  {
-    const controlPoints = [
-      [1.118281, 5.115846, -3.681386],
-      [3.948875, 5.115846, -3.641834],
-      [3.960072, 5.115846, -0.240352],
-      [3.985447, 5.115846, 4.585005],
-      [-3.793631, 5.115846, 4.585006],
-      [-3.826839, 5.115846, -14.736200],
-      [-14.542292, 5.115846, -14.765865],
-      [-14.520929, 5.115846, -3.627002],
-      [-5.452815, 5.115846, -3.634418],
-      [-5.467251, 5.115846, 4.549161],
-      [-13.266233, 5.115846, 4.567083],
-      [-13.250067, 5.115846, -13.499271],
-      [4.081842, 5.115846, -13.435463],
-      [4.125436, 5.115846, -5.334928],
-      [-14.521364, 5.115846, -5.239871],
-      [-14.510466, 5.115846, 5.486727],
-      [5.745666, 5.115846, 5.510492],
-      [5.787942, 5.115846, -14.728308],
-      [-5.423720, 5.115846, -14.761919],
-      [-5.373599, 5.115846, -3.704133],
-      [1.004861, 5.115846, -3.641834],
-    ];
-    const p0 = new THREE.Vector3();
-    const p1 = new THREE.Vector3();
-    curve = new THREE.CatmullRomCurve3(
-      controlPoints.map((p, ndx) => {
-        p0.set(...p);
-        p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
-        return [
-          (new THREE.Vector3()).copy(p0),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
-          (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
-        ];
-      }).flat(),
-      true,
-    );
-    {
-      const points = curve.getPoints(250);
-      const geometry = new THREE.BufferGeometry().setFromPoints(points);
-      const material = new THREE.LineBasicMaterial({color: 0xff0000});
-      curveObject = new THREE.Line(geometry, material);
-      curveObject.scale.set(100, 100, 100);
-      curveObject.position.y = -621;
-      curveObject.visible = false;
-      scene.add(curveObject);
-    }
-  }
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.castShadow = true;
+		light.position.set( - 250, 800, - 850 );
+		light.target.position.set( - 550, 40, - 450 );
 
 
-  const cars = [];
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      root.traverse((obj) => {
-        if (obj.castShadow !== undefined) {
-          obj.castShadow = true;
-          obj.receiveShadow = true;
-        }
-      });
-
-      const loadedCars = root.getObjectByName('Cars');
-      const fixes = [
-        { prefix: 'Car_08', y: 0,  rot: [Math.PI * .5, 0, Math.PI * .5], },
-        { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], },
-        { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], },
-      ];
-
-      root.updateMatrixWorld();
-      for (const car of loadedCars.children.slice()) {
-        const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
-        const obj = new THREE.Object3D();
-        car.position.set(0, fix.y, 0);
-        car.rotation.set(...fix.rot);
-        obj.add(car);
-        scene.add(obj);
-        cars.push(obj);
-      }
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		light.shadow.bias = - 0.004;
+		light.shadow.mapSize.width = 2048;
+		light.shadow.mapSize.height = 2048;
 
 
-  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;
-  }
+		scene.add( light );
+		scene.add( light.target );
+		const cam = light.shadow.camera;
+		cam.near = 1;
+		cam.far = 2000;
+		cam.left = - 1500;
+		cam.right = 1500;
+		cam.top = 1500;
+		cam.bottom = - 1500;
 
 
-  // create 2 Vector3s we can use for path calculations
-  const carPosition = new THREE.Vector3();
-  const carTarget = new THREE.Vector3();
+		const cameraHelper = new THREE.CameraHelper( cam );
+		scene.add( cameraHelper );
+		cameraHelper.visible = false;
+		const helper = new THREE.DirectionalLightHelper( light, 100 );
+		scene.add( helper );
+		helper.visible = false;
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds
+		function makeXYZGUI( gui, vector3, name, onChangeFn ) {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			const folder = gui.addFolder( name );
+			folder.add( vector3, 'x', vector3.x - 500, vector3.x + 500 ).onChange( onChangeFn );
+			folder.add( vector3, 'y', vector3.y - 500, vector3.y + 500 ).onChange( onChangeFn );
+			folder.add( vector3, 'z', vector3.z - 500, vector3.z + 500 ).onChange( onChangeFn );
+			folder.open();
 
 
-    {
-      const pathTime = time * .01;
-      const targetOffset = 0.01;
-      cars.forEach((car, ndx) => {
-        // a number between 0 and 1 to evenly space the cars
-        const u = pathTime + ndx / cars.length;
-
-        // get the first point
-        curve.getPointAt(u % 1, carPosition);
-        carPosition.applyMatrix4(curveObject.matrixWorld);
-
-        // get a second point slightly further down the curve
-        curve.getPointAt((u + targetOffset) % 1, carTarget);
-        carTarget.applyMatrix4(curveObject.matrixWorld);
-
-        // put the car at the first point (temporarily)
-        car.position.copy(carPosition);
-        // point the car the second point
-        car.lookAt(carTarget);
-
-        // put the car between the 2 points
-        car.position.lerpVectors(carPosition, carTarget, 0.5);
-      });
-    }
+		}
 
 
-    renderer.render(scene, camera);
+		function updateCamera() {
 
 
-    requestAnimationFrame(render);
-  }
+			// update the light target's matrixWorld because it's needed by the helper
+			light.updateMatrixWorld();
+			light.target.updateMatrixWorld();
+			helper.update();
+			// update the light's shadow camera's projection matrix
+			light.shadow.camera.updateProjectionMatrix();
+			// and now update the camera helper we're using to show the light's shadow camera
+			cameraHelper.update();
+
+		}
+
+		updateCamera();
+
+		class DimensionGUIHelper {
+
+			constructor( obj, minProp, maxProp ) {
+
+				this.obj = obj;
+				this.minProp = minProp;
+				this.maxProp = maxProp;
+
+			}
+			get value() {
+
+				return this.obj[ this.maxProp ] * 2;
+
+			}
+			set value( v ) {
+
+				this.obj[ this.maxProp ] = v / 2;
+				this.obj[ this.minProp ] = v / - 2;
+
+			}
+
+		}
+
+		class MinMaxGUIHelper {
+
+			constructor( obj, minProp, maxProp, minDif ) {
+
+				this.obj = obj;
+				this.minProp = minProp;
+				this.maxProp = maxProp;
+				this.minDif = minDif;
+
+			}
+			get min() {
+
+				return this.obj[ this.minProp ];
+
+			}
+			set min( v ) {
+
+				this.obj[ this.minProp ] = v;
+				this.obj[ this.maxProp ] = Math.max( this.obj[ this.maxProp ], v + this.minDif );
+
+			}
+			get max() {
+
+				return this.obj[ this.maxProp ];
+
+			}
+			set max( v ) {
+
+				this.obj[ this.maxProp ] = v;
+				this.min = this.min; // this will call the min setter
+
+			}
+
+		}
+
+		class VisibleGUIHelper {
+
+			constructor( ...objects ) {
+
+				this.objects = [ ...objects ];
+
+			}
+			get value() {
+
+				return this.objects[ 0 ].visible;
+
+			}
+			set value( v ) {
+
+				this.objects.forEach( ( obj ) => {
+
+					obj.visible = v;
+
+				} );
+
+			}
+
+		}
+
+		const gui = new GUI();
+		gui.close();
+		gui.add( new VisibleGUIHelper( helper, cameraHelper ), 'value' ).name( 'show helpers' );
+		gui.add( light.shadow, 'bias', - 0.1, 0.1, 0.001 );
+		{
+
+			const folder = gui.addFolder( 'Shadow Camera' );
+			folder.open();
+			folder.add( new DimensionGUIHelper( light.shadow.camera, 'left', 'right' ), 'value', 1, 4000 )
+				.name( 'width' )
+				.onChange( updateCamera );
+			folder.add( new DimensionGUIHelper( light.shadow.camera, 'bottom', 'top' ), 'value', 1, 4000 )
+				.name( 'height' )
+				.onChange( updateCamera );
+			const minMaxGUIHelper = new MinMaxGUIHelper( light.shadow.camera, 'near', 'far', 0.1 );
+			folder.add( minMaxGUIHelper, 'min', 1, 1000, 1 ).name( 'near' ).onChange( updateCamera );
+			folder.add( minMaxGUIHelper, 'max', 1, 4000, 1 ).name( 'far' ).onChange( updateCamera );
+			folder.add( light.shadow.camera, 'zoom', 0.01, 1.5, 0.01 ).onChange( updateCamera );
+
+		}
+
+		makeXYZGUI( gui, light.position, 'position', updateCamera );
+		makeXYZGUI( gui, light.target.position, 'target', updateCamera );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	let curve;
+	let curveObject;
+	{
+
+		const controlPoints = [
+			[ 1.118281, 5.115846, - 3.681386 ],
+			[ 3.948875, 5.115846, - 3.641834 ],
+			[ 3.960072, 5.115846, - 0.240352 ],
+			[ 3.985447, 5.115846, 4.585005 ],
+			[ - 3.793631, 5.115846, 4.585006 ],
+			[ - 3.826839, 5.115846, - 14.736200 ],
+			[ - 14.542292, 5.115846, - 14.765865 ],
+			[ - 14.520929, 5.115846, - 3.627002 ],
+			[ - 5.452815, 5.115846, - 3.634418 ],
+			[ - 5.467251, 5.115846, 4.549161 ],
+			[ - 13.266233, 5.115846, 4.567083 ],
+			[ - 13.250067, 5.115846, - 13.499271 ],
+			[ 4.081842, 5.115846, - 13.435463 ],
+			[ 4.125436, 5.115846, - 5.334928 ],
+			[ - 14.521364, 5.115846, - 5.239871 ],
+			[ - 14.510466, 5.115846, 5.486727 ],
+			[ 5.745666, 5.115846, 5.510492 ],
+			[ 5.787942, 5.115846, - 14.728308 ],
+			[ - 5.423720, 5.115846, - 14.761919 ],
+			[ - 5.373599, 5.115846, - 3.704133 ],
+			[ 1.004861, 5.115846, - 3.641834 ],
+		];
+		const p0 = new THREE.Vector3();
+		const p1 = new THREE.Vector3();
+		curve = new THREE.CatmullRomCurve3(
+			controlPoints.map( ( p, ndx ) => {
+
+				p0.set( ...p );
+				p1.set( ...controlPoints[ ( ndx + 1 ) % controlPoints.length ] );
+				return [
+					( new THREE.Vector3() ).copy( p0 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.1 ),
+					( new THREE.Vector3() ).lerpVectors( p0, p1, 0.9 ),
+				];
+
+			} ).flat(),
+			true,
+		);
+		{
+
+			const points = curve.getPoints( 250 );
+			const geometry = new THREE.BufferGeometry().setFromPoints( points );
+			const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
+			curveObject = new THREE.Line( geometry, material );
+			curveObject.scale.set( 100, 100, 100 );
+			curveObject.position.y = - 621;
+			curveObject.visible = false;
+			scene.add( curveObject );
+
+		}
+
+	}
+
+	const cars = [];
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			root.traverse( ( obj ) => {
+
+				if ( obj.castShadow !== undefined ) {
+
+					obj.castShadow = true;
+					obj.receiveShadow = true;
+
+				}
+
+			} );
+
+			const loadedCars = root.getObjectByName( 'Cars' );
+			const fixes = [
+				{ prefix: 'Car_08', y: 0, rot: [ Math.PI * .5, 0, Math.PI * .5 ], },
+				{ prefix: 'CAR_03', y: 33, rot: [ 0, Math.PI, 0 ], },
+				{ prefix: 'Car_04', y: 40, rot: [ 0, Math.PI, 0 ], },
+			];
+
+			root.updateMatrixWorld();
+			for ( const car of loadedCars.children.slice() ) {
+
+				const fix = fixes.find( fix => car.name.startsWith( fix.prefix ) );
+				const obj = new THREE.Object3D();
+				car.position.set( 0, fix.y, 0 );
+				car.rotation.set( ...fix.rot );
+				obj.add( car );
+				scene.add( obj );
+				cars.push( obj );
+
+			}
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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;
+
+	}
+
+	// create 2 Vector3s we can use for path calculations
+	const carPosition = new THREE.Vector3();
+	const carTarget = new THREE.Vector3();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		{
+
+			const pathTime = time * .01;
+			const targetOffset = 0.01;
+			cars.forEach( ( car, ndx ) => {
+
+				// a number between 0 and 1 to evenly space the cars
+				const u = pathTime + ndx / cars.length;
+
+				// get the first point
+				curve.getPointAt( u % 1, carPosition );
+				carPosition.applyMatrix4( curveObject.matrixWorld );
+
+				// get a second point slightly further down the curve
+				curve.getPointAt( ( u + targetOffset ) % 1, carTarget );
+				carTarget.applyMatrix4( curveObject.matrixWorld );
+
+				// put the car at the first point (temporarily)
+				car.position.copy( carPosition );
+				// point the car the second point
+				car.lookAt( carTarget );
+
+				// put the car between the 2 points
+				car.position.lerpVectors( carPosition, carTarget, 0.5 );
+
+			} );
+
+		}
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 142 - 118
manual/examples/load-gltf.html

@@ -35,139 +35,163 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
+	{
 
 
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
+		const planeSize = 40;
 
 
-      // set the camera to frame the box
-      frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-  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);
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.5, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 144 - 122
manual/examples/load-obj-auto-camera-xz.html

@@ -35,140 +35,162 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 4000;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 200;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  {
-    const objLoader = new OBJLoader();
-    objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
-      root.updateMatrixWorld();
-      scene.add(root);
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  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() {
+		const planeSize = 4000;
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 200;
+		texture.repeat.set( repeats, repeats );
 
 
-    renderer.render(scene, camera);
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const objLoader = new OBJLoader();
+		objLoader.load( 'resources/models/windmill_2/windmill.obj', ( root ) => {
+
+			root.updateMatrixWorld();
+			scene.add( root );
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 1.2, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 136 - 114
manual/examples/load-obj-auto-camera.html

@@ -35,136 +35,158 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // from the center of the box
-    const direction = (new THREE.Vector3()).subVectors(camera.position, boxCenter).normalize();
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
+	{
 
 
-    camera.updateProjectionMatrix();
+		const planeSize = 40;
 
 
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-  {
-    const objLoader = new OBJLoader();
-    objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
-      scene.add(root);
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-  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();
-    }
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// from the center of the box
+		const direction = ( new THREE.Vector3() ).subVectors( camera.position, boxCenter ).normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const objLoader = new OBJLoader();
+		objLoader.load( 'resources/models/windmill_2/windmill.obj', ( root ) => {
+
+			scene.add( root );
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 1.2, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 111 - 89
manual/examples/load-obj-materials-fixed.html

@@ -35,106 +35,128 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
-import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
+import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 1;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const mtlLoader = new MTLLoader();
-    mtlLoader.load('resources/models/windmill/windmill-fixed.mtl', (mtl) => {
-      mtl.preload();
-      const objLoader = new OBJLoader();
-      mtl.materials.Material.side = THREE.DoubleSide;
-      objLoader.setMaterials(mtl);
-      objLoader.load('resources/models/windmill/windmill.obj', (root) => {
-        scene.add(root);
-      });
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function render() {
+	{
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeSize = 40;
 
 
-    renderer.render(scene, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    requestAnimationFrame(render);
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 3;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	{
+
+		const mtlLoader = new MTLLoader();
+		mtlLoader.load( 'resources/models/windmill/windmill-fixed.mtl', ( mtl ) => {
+
+			mtl.preload();
+			const objLoader = new OBJLoader();
+			mtl.materials.Material.side = THREE.DoubleSide;
+			objLoader.setMaterials( mtl );
+			objLoader.load( 'resources/models/windmill/windmill.obj', ( root ) => {
+
+				scene.add( root );
+
+			} );
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 152 - 128
manual/examples/load-obj-materials-windmill2.html

@@ -35,146 +35,170 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
-import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
+import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 4000;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 200;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 1;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  {
-    const mtlLoader = new MTLLoader();
-    mtlLoader.load('resources/models/windmill_2/windmill-fixed.mtl', (mtl) => {
-      mtl.preload();
-      const objLoader = new OBJLoader();
-      objLoader.setMaterials(mtl);
-      objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
-        scene.add(root);
-
-        // compute the box that contains all the stuff
-        // from root and below
-        const box = new THREE.Box3().setFromObject(root);
-
-        const boxSize = box.getSize(new THREE.Vector3()).length();
-        const boxCenter = box.getCenter(new THREE.Vector3());
-
-        // set the camera to frame the box
-        frameArea(boxSize * 1.2, boxSize, boxCenter, camera);
-
-        // update the Trackball controls to handle the new size
-        controls.maxDistance = boxSize * 10;
-        controls.target.copy(boxCenter);
-        controls.update();
-      });
-    });
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  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() {
+		const planeSize = 4000;
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 200;
+		texture.repeat.set( repeats, repeats );
 
 
-    renderer.render(scene, camera);
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
 
 
-    requestAnimationFrame(render);
-  }
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 3;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const mtlLoader = new MTLLoader();
+		mtlLoader.load( 'resources/models/windmill_2/windmill-fixed.mtl', ( mtl ) => {
+
+			mtl.preload();
+			const objLoader = new OBJLoader();
+			objLoader.setMaterials( mtl );
+			objLoader.load( 'resources/models/windmill_2/windmill.obj', ( root ) => {
+
+				scene.add( root );
+
+				// compute the box that contains all the stuff
+				// from root and below
+				const box = new THREE.Box3().setFromObject( root );
+
+				const boxSize = box.getSize( new THREE.Vector3() ).length();
+				const boxCenter = box.getCenter( new THREE.Vector3() );
+
+				// set the camera to frame the box
+				frameArea( boxSize * 1.2, boxSize, boxCenter, camera );
+
+				// update the Trackball controls to handle the new size
+				controls.maxDistance = boxSize * 10;
+				controls.target.copy( boxCenter );
+				controls.update();
+
+			} );
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 110 - 88
manual/examples/load-obj-materials.html

@@ -35,105 +35,127 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
-import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
+import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 1;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(5, 10, 2);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const mtlLoader = new MTLLoader();
-    mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
-      mtl.preload();
-      const objLoader = new OBJLoader();
-      objLoader.setMaterials(mtl);
-      objLoader.load('resources/models/windmill/windmill.obj', (root) => {
-        scene.add(root);
-      });
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function render() {
+	{
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeSize = 40;
 
 
-    renderer.render(scene, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    requestAnimationFrame(render);
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 3;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 5, 10, 2 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	{
+
+		const mtlLoader = new MTLLoader();
+		mtlLoader.load( 'resources/models/windmill/windmill.mtl', ( mtl ) => {
+
+			mtl.preload();
+			const objLoader = new OBJLoader();
+			objLoader.setMaterials( mtl );
+			objLoader.load( 'resources/models/windmill/windmill.obj', ( root ) => {
+
+				scene.add( root );
+
+			} );
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 103 - 83
manual/examples/load-obj-no-materials.html

@@ -35,100 +35,120 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 0.6;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 0.8;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(0, 10, 0);
-    light.target.position.set(-5, 0, 0);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const objLoader = new OBJLoader();
-    objLoader.load('resources/models/windmill/windmill.obj', (root) => {
-      scene.add(root);
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function render() {
+	{
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeSize = 40;
 
 
-    renderer.render(scene, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    requestAnimationFrame(render);
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 2;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 2.5;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		light.target.position.set( - 5, 0, 0 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	{
+
+		const objLoader = new OBJLoader();
+		objLoader.load( 'resources/models/windmill/windmill.obj', ( root ) => {
+
+			scene.add( root );
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 103 - 83
manual/examples/load-obj-wat.html

@@ -35,100 +35,120 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const planeSize = 40;
-
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/checker.png');
-    texture.colorSpace = THREE.SRGBColorSpace;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.magFilter = THREE.NearestFilter;
-    const repeats = planeSize / 2;
-    texture.repeat.set(repeats, repeats);
-
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: texture,
-      side: THREE.DoubleSide,
-    });
-    const mesh = new THREE.Mesh(planeGeo, planeMat);
-    mesh.rotation.x = Math.PI * -.5;
-    scene.add(mesh);
-  }
 
 
-  {
-    const skyColor = 0xB1E1FF;  // light blue
-    const groundColor = 0xB97A20;  // brownish orange
-    const intensity = 1;
-    const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-    scene.add(light);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(0, 10, 0);
-    light.target.position.set(-5, 0, 0);
-    scene.add(light);
-    scene.add(light.target);
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  {
-    const objLoader = new OBJLoader();
-    objLoader.load('resources/models/windmill_2/windmill.obj', (root) => {
-      scene.add(root);
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  function render() {
+	{
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		const planeSize = 40;
 
 
-    renderer.render(scene, camera);
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/checker.png' );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.magFilter = THREE.NearestFilter;
+		const repeats = planeSize / 2;
+		texture.repeat.set( repeats, repeats );
 
 
-    requestAnimationFrame(render);
-  }
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: texture,
+			side: THREE.DoubleSide,
+		} );
+		const mesh = new THREE.Mesh( planeGeo, planeMat );
+		mesh.rotation.x = Math.PI * - .5;
+		scene.add( mesh );
+
+	}
+
+	{
+
+		const skyColor = 0xB1E1FF; // light blue
+		const groundColor = 0xB97A20; // brownish orange
+		const intensity = 3;
+		const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+		scene.add( light );
+
+	}
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 0, 10, 0 );
+		light.target.position.set( - 5, 0, 0 );
+		scene.add( light );
+		scene.add( light.target );
+
+	}
+
+	{
+
+		const objLoader = new OBJLoader();
+		objLoader.load( 'resources/models/windmill_2/windmill.obj', ( root ) => {
+
+			scene.add( root );
+
+		} );
+
+	}
+
+	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 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 416 - 314
manual/examples/lots-of-objects-animated.html

@@ -57,345 +57,447 @@
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 import TWEEN from 'three/addons/libs/tween.module.js';
 import TWEEN from 'three/addons/libs/tween.module.js';
 
 
 class TweenManger {
 class TweenManger {
-  constructor() {
-    this.numTweensRunning = 0;
-  }
-  _handleComplete() {
-    --this.numTweensRunning;
-    console.assert(this.numTweensRunning >= 0);  /* eslint no-console: off */
-  }
-  createTween(targetObject) {
-    const self = this;
-    ++this.numTweensRunning;
-    let userCompleteFn = () => {};
-    // create a new tween and install our own onComplete callback
-    const tween = new TWEEN.Tween(targetObject).onComplete(function(...args) {
-      self._handleComplete();
-      userCompleteFn.call(this, ...args);
-    });
-    // replace the tween's onComplete function with our own
-    // so we can call the user's callback if they supply one.
-    tween.onComplete = (fn) => {
-      userCompleteFn = fn;
-      return tween;
-    };
-    return tween;
-  }
-  update() {
-    TWEEN.update();
-    return this.numTweensRunning > 0;
-  }
+
+	constructor() {
+
+		this.numTweensRunning = 0;
+
+	}
+	_handleComplete() {
+
+		-- this.numTweensRunning;
+		console.assert( this.numTweensRunning >= 0 ); /* eslint no-console: off */
+
+	}
+	createTween( targetObject ) {
+
+		const self = this;
+		++ this.numTweensRunning;
+		let userCompleteFn = () => {};
+
+		// create a new tween and install our own onComplete callback
+		const tween = new TWEEN.Tween( targetObject ).onComplete( function ( ...args ) {
+
+			self._handleComplete();
+			userCompleteFn.call( this, ...args );
+
+		} );
+		// replace the tween's onComplete function with our own
+		// so we can call the user's callback if they supply one.
+		tween.onComplete = ( fn ) => {
+
+			userCompleteFn = fn;
+			return tween;
+
+		};
+
+		return tween;
+
+	}
+	update() {
+
+		TWEEN.update();
+		return this.numTweensRunning > 0;
+
+	}
+
 }
 }
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  const tweenManager = new TweenManger();
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
+	const tweenManager = new TweenManger();
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function addBoxes(file, hueRange) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-    // Used to move the center of the cube so it scales from the position Z axis
-    const originHelper = new THREE.Object3D();
-    originHelper.position.z = 0.5;
-    positionHelper.add(originHelper);
-
-    const color = new THREE.Color();
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    const geometries = [];
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (value === undefined) {
-          return;
-        }
-        const amount = (value - min) / range;
-
-        const boxWidth = 1;
-        const boxHeight = 1;
-        const boxDepth = 1;
-        const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the origin helper to
-        // position this geometry
-        positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-        originHelper.updateWorldMatrix(true, false);
-        geometry.applyMatrix4(originHelper.matrixWorld);
-
-        // compute a color
-        const hue = THREE.MathUtils.lerp(...hueRange, amount);
-        const saturation = 1;
-        const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
-        color.setHSL(hue, saturation, lightness);
-        // get the colors as an array of values from 0 to 255
-        const rgb = color.toArray().map(v => v * 255);
-
-        // make an array to store colors for each vertex
-        const numVerts = geometry.getAttribute('position').count;
-        const itemSize = 3;  // r, g, b
-        const colors = new Uint8Array(itemSize * numVerts);
-
-        // copy the color into the colors array for each vertex
-        colors.forEach((v, ndx) => {
-          colors[ndx] = rgb[ndx % 3];
-        });
-
-        const normalized = true;
-        const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
-        geometry.setAttribute('color', colorAttrib);
-
-        geometries.push(geometry);
-      });
-    });
-
-    const mergedGeometry = BufferGeometryUtils.mergeGeometries(
-        geometries, false);
-    const material = new THREE.MeshBasicMaterial({
-      vertexColors: true,
-      transparent: true,
-      opacity: 0,
-    });
-    const mesh = new THREE.Mesh(mergedGeometry, material);
-    scene.add(mesh);
-    return mesh;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  async function loadData(info) {
-    const text = await loadFile(info.url);
-    info.file = parseData(text);
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  async function loadAll() {
-    const fileInfos = [
-      {name: 'men',   hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
-      {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
-    ];
+	{
 
 
-    await Promise.all(fileInfos.map(loadData));
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    function mapValues(data, fn) {
-      return data.map((row, rowNdx) => {
-        return row.map((value, colNdx) => {
-          return fn(value, rowNdx, colNdx);
-        });
-      });
-    }
+	}
 
 
-    function makeDiffFile(baseFile, otherFile, compareFn) {
-      let min;
-      let max;
-      const baseData = baseFile.data;
-      const otherData = otherFile.data;
-      const data = mapValues(baseData, (base, rowNdx, colNdx) => {
-        const other = otherData[rowNdx][colNdx];
-          if (base === undefined || other === undefined) {
-            return undefined;
-          }
-          const value = compareFn(base, other);
-          min = Math.min(min === undefined ? value : min, value);
-          max = Math.max(max === undefined ? value : max, value);
-          return value;
-      });
-      // make a copy of baseFile and replace min, max, and data
-      // with the new data
-      return {...baseFile, min, max, data};
-    }
+	async function loadFile( url ) {
 
 
-    // generate a new set of data
-    {
-      const menInfo = fileInfos[0];
-      const womenInfo = fileInfos[1];
-      const menFile = menInfo.file;
-      const womenFile = womenInfo.file;
+		const req = await fetch( url );
+		return req.text();
 
 
-      function amountGreaterThan(a, b) {
-        return Math.max(a - b, 0);
-      }
-      fileInfos.push({
-        name: '>50%men',
-        hueRange: [0.6, 1.1],
-        file: makeDiffFile(menFile, womenFile, (men, women) => {
-          return amountGreaterThan(men, women);
-        }),
-      });
-      fileInfos.push({
-        name: '>50% women',
-        hueRange: [0.0, 0.4],
-        file: makeDiffFile(womenFile, menFile, (women, men) => {
-          return amountGreaterThan(women, men);
-        }),
-      });
-    }
+	}
 
 
-    function showFileInfo(fileInfos, fileInfo) {
-      fileInfos.forEach((info) => {
-        const durationInMs = 1000;
-        const visible = fileInfo === info;
-//        const scale = visible ? 1 : 0.1;
-        const opacity = visible ? 1 : 0;
-        info.elem.className = visible ? 'selected' : '';
-        info.root.visible = visible || info.root.material.opacity > 0;
-        tweenManager.createTween(info.root.material)
-          .to({opacity}, durationInMs)
-          .start()
-          .onComplete(() => {
-            info.root.visible = visible;
-          });
-//        tweenManager.createTween(info.root.material)
-//          .to({depthWrite: visible}, 0)
-//          .delay(durationInMs * .5)
-//          .start();
-//        tweenManager.createTween(info.root)
-//          .to({visible}, 0)
-//          .delay(durationInMs)
-//          .start();
-//        tweenManager.createTween(info.root.scale)
-//          .to({x: scale, y: scale, z: scale}, durationInMs)
-//          .start();
-      });
-      requestRenderIfNotRequested();
-    }
+	function parseData( text ) {
 
 
-    const uiElem = document.querySelector('#ui');
-    fileInfos.forEach((info) => {
-      const boxes = addBoxes(info.file, info.hueRange);
-      info.root = boxes;
-//      boxes.scale.set(0.1, 0.1, 0.1);
-      const div = document.createElement('div');
-      info.elem = div;
-      div.textContent = info.name;
-      uiElem.appendChild(div);
-      function show() {
-        showFileInfo(fileInfos, info);
-      }
-      div.addEventListener('mouseover', show);
-      div.addEventListener('touchstart', show);
-    });
-    // show the first set of data
-    showFileInfo(fileInfos, fileInfos[0]);
-  }
-  loadAll();
-
-  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;
-  }
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
 
 
-  let renderRequested = false;
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
 
 
-  function render() {
-    renderRequested = undefined;
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			} else if ( parts.length > 2 ) {
 
 
-    if (tweenManager.update()) {
-      requestRenderIfNotRequested();
-    }
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function addBoxes( file, hueRange ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+		// Used to move the center of the cube so it scales from the position Z axis
+		const originHelper = new THREE.Object3D();
+		originHelper.position.z = 0.5;
+		positionHelper.add( originHelper );
+
+		const color = new THREE.Color();
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		const geometries = [];
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( value === undefined ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+
+				const boxWidth = 1;
+				const boxHeight = 1;
+				const boxDepth = 1;
+				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the origin helper to
+				// position this geometry
+				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+				originHelper.updateWorldMatrix( true, false );
+				geometry.applyMatrix4( originHelper.matrixWorld );
+
+				// compute a color
+				const hue = THREE.MathUtils.lerp( ...hueRange, amount );
+				const saturation = 1;
+				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
+				color.setHSL( hue, saturation, lightness );
+				// get the colors as an array of values from 0 to 255
+				const rgb = color.toArray().map( v => v * 255 );
+
+				// make an array to store colors for each vertex
+				const numVerts = geometry.getAttribute( 'position' ).count;
+				const itemSize = 3; // r, g, b
+				const colors = new Uint8Array( itemSize * numVerts );
+
+				// copy the color into the colors array for each vertex
+				colors.forEach( ( v, ndx ) => {
+
+					colors[ ndx ] = rgb[ ndx % 3 ];
+
+				} );
+
+				const normalized = true;
+				const colorAttrib = new THREE.BufferAttribute( colors, itemSize, normalized );
+				geometry.setAttribute( 'color', colorAttrib );
+
+				geometries.push( geometry );
+
+			} );
+
+		} );
+
+		const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+			geometries, false );
+		const material = new THREE.MeshBasicMaterial( {
+			vertexColors: true,
+			transparent: true,
+			opacity: 0,
+		} );
+		const mesh = new THREE.Mesh( mergedGeometry, material );
+		scene.add( mesh );
+		return mesh;
+
+	}
+
+	async function loadData( info ) {
+
+		const text = await loadFile( info.url );
+		info.file = parseData( text );
+
+	}
+
+	async function loadAll() {
+
+		const fileInfos = [
+			{ name: 'men', hueRange: [ 0.7, 0.3 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
+			{ name: 'women', hueRange: [ 0.9, 1.1 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
+		];
+
+		await Promise.all( fileInfos.map( loadData ) );
+
+		function mapValues( data, fn ) {
+
+			return data.map( ( row, rowNdx ) => {
+
+				return row.map( ( value, colNdx ) => {
+
+					return fn( value, rowNdx, colNdx );
+
+				} );
+
+			} );
+
+		}
+
+		function makeDiffFile( baseFile, otherFile, compareFn ) {
+
+			let min;
+			let max;
+			const baseData = baseFile.data;
+			const otherData = otherFile.data;
+			const data = mapValues( baseData, ( base, rowNdx, colNdx ) => {
+
+				const other = otherData[ rowNdx ][ colNdx ];
+				if ( base === undefined || other === undefined ) {
+
+					return undefined;
+
+				}
+
+				const value = compareFn( base, other );
+				min = Math.min( min === undefined ? value : min, value );
+				max = Math.max( max === undefined ? value : max, value );
+				return value;
+
+			} );
+			// make a copy of baseFile and replace min, max, and data
+			// with the new data
+			return { ...baseFile, min, max, data };
+
+		}
+
+		// generate a new set of data
+		{
+
+			const menInfo = fileInfos[ 0 ];
+			const womenInfo = fileInfos[ 1 ];
+			const menFile = menInfo.file;
+			const womenFile = womenInfo.file;
+
+			function amountGreaterThan( a, b ) {
+
+				return Math.max( a - b, 0 );
+
+			}
+
+			fileInfos.push( {
+				name: '>50%men',
+				hueRange: [ 0.6, 1.1 ],
+				file: makeDiffFile( menFile, womenFile, ( men, women ) => {
+
+					return amountGreaterThan( men, women );
+
+				} ),
+			} );
+			fileInfos.push( {
+				name: '>50% women',
+				hueRange: [ 0.0, 0.4 ],
+				file: makeDiffFile( womenFile, menFile, ( women, men ) => {
+
+					return amountGreaterThan( women, men );
+
+				} ),
+			} );
+
+		}
+
+		function showFileInfo( fileInfos, fileInfo ) {
+
+			fileInfos.forEach( ( info ) => {
+
+				const durationInMs = 1000;
+				const visible = fileInfo === info;
+				//        const scale = visible ? 1 : 0.1;
+				const opacity = visible ? 1 : 0;
+				info.elem.className = visible ? 'selected' : '';
+				info.root.visible = visible || info.root.material.opacity > 0;
+				tweenManager.createTween( info.root.material )
+					.to( { opacity }, durationInMs )
+					.start()
+					.onComplete( () => {
+
+						info.root.visible = visible;
+
+					} );
+				//        tweenManager.createTween(info.root.material)
+				//          .to({depthWrite: visible}, 0)
+				//          .delay(durationInMs * .5)
+				//          .start();
+				//        tweenManager.createTween(info.root)
+				//          .to({visible}, 0)
+				//          .delay(durationInMs)
+				//          .start();
+				//        tweenManager.createTween(info.root.scale)
+				//          .to({x: scale, y: scale, z: scale}, durationInMs)
+				//          .start();
+
+			} );
+			requestRenderIfNotRequested();
+
+		}
+
+		const uiElem = document.querySelector( '#ui' );
+		fileInfos.forEach( ( info ) => {
+
+			const boxes = addBoxes( info.file, info.hueRange );
+			info.root = boxes;
+			//      boxes.scale.set(0.1, 0.1, 0.1);
+			const div = document.createElement( 'div' );
+			info.elem = div;
+			div.textContent = info.name;
+			uiElem.appendChild( div );
+			function show() {
+
+				showFileInfo( fileInfos, info );
+
+			}
+
+			div.addEventListener( 'mouseover', show );
+			div.addEventListener( 'touchstart', show );
+
+		} );
+		// show the first set of data
+		showFileInfo( fileInfos, fileInfos[ 0 ] );
+
+	}
+
+	loadAll();
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		if ( tweenManager.update() ) {
+
+			requestRenderIfNotRequested();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 222 - 176
manual/examples/lots-of-objects-merged-vertexcolors.html

@@ -36,194 +36,240 @@
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function addBoxes(file) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-    // Used to move the center of the cube so it scales from the position Z axis
-    const originHelper = new THREE.Object3D();
-    originHelper.position.z = 0.5;
-    positionHelper.add(originHelper);
-
-    const color = new THREE.Color();
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    const geometries = [];
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (value === undefined) {
-          return;
-        }
-        const amount = (value - min) / range;
-
-        const boxWidth = 1;
-        const boxHeight = 1;
-        const boxDepth = 1;
-        const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the origin helper to
-        // position this geometry
-        positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-        originHelper.updateWorldMatrix(true, false);
-        geometry.applyMatrix4(originHelper.matrixWorld);
-
-        // compute a color
-        const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
-        const saturation = 1;
-        const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
-        color.setHSL(hue, saturation, lightness);
-        // get the colors as an array of values from 0 to 255
-        const rgb = color.toArray().map(v => v * 255);
-
-        // make an array to store colors for each vertex
-        const numVerts = geometry.getAttribute('position').count;
-        const itemSize = 3;  // r, g, b
-        const colors = new Uint8Array(itemSize * numVerts);
-
-        // copy the color into the colors array for each vertex
-        colors.forEach((v, ndx) => {
-          colors[ndx] = rgb[ndx % 3];
-        });
-
-        const normalized = true;
-        const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
-        geometry.setAttribute('color', colorAttrib);
-
-        geometries.push(geometry);
-      });
-    });
-
-    const mergedGeometry = BufferGeometryUtils.mergeGeometries(
-        geometries, false);
-    const material = new THREE.MeshBasicMaterial({
-      vertexColors: true,
-    });
-    const mesh = new THREE.Mesh(mergedGeometry, material);
-    scene.add(mesh);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
-    .then(parseData)
-    .then(addBoxes)
-    .then(render);
-
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  let renderRequested = false;
+	{
 
 
-  function render() {
-    renderRequested = undefined;
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	}
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+	async function loadFile( url ) {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+		const req = await fetch( url );
+		return req.text();
+
+	}
+
+	function parseData( text ) {
+
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
+
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
+
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
+
+			} else if ( parts.length > 2 ) {
+
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
+
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
+
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function addBoxes( file ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+		// Used to move the center of the cube so it scales from the position Z axis
+		const originHelper = new THREE.Object3D();
+		originHelper.position.z = 0.5;
+		positionHelper.add( originHelper );
+
+		const color = new THREE.Color();
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		const geometries = [];
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( value === undefined ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+
+				const boxWidth = 1;
+				const boxHeight = 1;
+				const boxDepth = 1;
+				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the origin helper to
+				// position this geometry
+				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+				originHelper.updateWorldMatrix( true, false );
+				geometry.applyMatrix4( originHelper.matrixWorld );
+
+				// compute a color
+				const hue = THREE.MathUtils.lerp( 0.7, 0.3, amount );
+				const saturation = 1;
+				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
+				color.setHSL( hue, saturation, lightness );
+				// get the colors as an array of values from 0 to 255
+				const rgb = color.toArray().map( v => v * 255 );
+
+				// make an array to store colors for each vertex
+				const numVerts = geometry.getAttribute( 'position' ).count;
+				const itemSize = 3; // r, g, b
+				const colors = new Uint8Array( itemSize * numVerts );
+
+				// copy the color into the colors array for each vertex
+				colors.forEach( ( v, ndx ) => {
+
+					colors[ ndx ] = rgb[ ndx % 3 ];
+
+				} );
+
+				const normalized = true;
+				const colorAttrib = new THREE.BufferAttribute( colors, itemSize, normalized );
+				geometry.setAttribute( 'color', colorAttrib );
+
+				geometries.push( geometry );
+
+			} );
+
+		} );
+
+		const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+			geometries, false );
+		const material = new THREE.MeshBasicMaterial( {
+			vertexColors: true,
+		} );
+		const mesh = new THREE.Mesh( mergedGeometry, material );
+		scene.add( mesh );
+
+	}
+
+	loadFile( 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' )
+		.then( parseData )
+		.then( addBoxes )
+		.then( render );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 194 - 150
manual/examples/lots-of-objects-merged.html

@@ -36,168 +36,212 @@
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function addBoxes(file) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-    // Used to move the center of the cube so it scales from the position Z axis
-    const originHelper = new THREE.Object3D();
-    originHelper.position.z = 0.5;
-    positionHelper.add(originHelper);
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    const geometries = [];
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (value === undefined) {
-          return;
-        }
-        const amount = (value - min) / range;
-
-        const boxWidth = 1;
-        const boxHeight = 1;
-        const boxDepth = 1;
-        const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the origin helper to
-        // position this geometry
-        positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-        originHelper.updateWorldMatrix(true, false);
-        geometry.applyMatrix4(originHelper.matrixWorld);
-
-        geometries.push(geometry);
-      });
-    });
-
-    const mergedGeometry = BufferGeometryUtils.mergeGeometries(
-        geometries, false);
-    const material = new THREE.MeshBasicMaterial({color:'red'});
-    const mesh = new THREE.Mesh(mergedGeometry, material);
-    scene.add(mesh);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
-    .then(parseData)
-    .then(addBoxes)
-    .then(render);
-
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  let renderRequested = false;
+	{
 
 
-  function render() {
-    renderRequested = undefined;
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	}
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+	async function loadFile( url ) {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+		const req = await fetch( url );
+		return req.text();
+
+	}
+
+	function parseData( text ) {
+
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
+
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
+
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
+
+			} else if ( parts.length > 2 ) {
+
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
+
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
+
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function addBoxes( file ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+		// Used to move the center of the cube so it scales from the position Z axis
+		const originHelper = new THREE.Object3D();
+		originHelper.position.z = 0.5;
+		positionHelper.add( originHelper );
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		const geometries = [];
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( value === undefined ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+
+				const boxWidth = 1;
+				const boxHeight = 1;
+				const boxDepth = 1;
+				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the origin helper to
+				// position this geometry
+				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+				originHelper.updateWorldMatrix( true, false );
+				geometry.applyMatrix4( originHelper.matrixWorld );
+
+				geometries.push( geometry );
+
+			} );
+
+		} );
+
+		const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+			geometries, false );
+		const material = new THREE.MeshBasicMaterial( { color: 'red' } );
+		const mesh = new THREE.Mesh( mergedGeometry, material );
+		scene.add( mesh );
+
+	}
+
+	loadFile( 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' )
+		.then( parseData )
+		.then( addBoxes )
+		.then( render );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 437 - 323
manual/examples/lots-of-objects-morphtargets.html

@@ -57,355 +57,469 @@
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 import TWEEN from 'three/addons/libs/tween.module.js';
 import TWEEN from 'three/addons/libs/tween.module.js';
 
 
 class TweenManger {
 class TweenManger {
-  constructor() {
-    this.numTweensRunning = 0;
-  }
-  _handleComplete() {
-    --this.numTweensRunning;
-    console.assert(this.numTweensRunning >= 0);  /* eslint no-console: off */
-  }
-  createTween(targetObject) {
-    const self = this;
-    ++this.numTweensRunning;
-    let userCompleteFn = () => {};
-    // create a new tween and install our own onComplete callback
-    const tween = new TWEEN.Tween(targetObject).onComplete(function(...args) {
-      self._handleComplete();
-      userCompleteFn.call(this, ...args);
-    });
-    // replace the tween's onComplete function with our own
-    // so we can call the user's callback if they supply one.
-    tween.onComplete = (fn) => {
-      userCompleteFn = fn;
-      return tween;
-    };
-    return tween;
-  }
-  update() {
-    TWEEN.update();
-    return this.numTweensRunning > 0;
-  }
+
+	constructor() {
+
+		this.numTweensRunning = 0;
+
+	}
+	_handleComplete() {
+
+		-- this.numTweensRunning;
+		console.assert( this.numTweensRunning >= 0 ); /* eslint no-console: off */
+
+	}
+	createTween( targetObject ) {
+
+		const self = this;
+		++ this.numTweensRunning;
+		let userCompleteFn = () => {};
+
+		// create a new tween and install our own onComplete callback
+		const tween = new TWEEN.Tween( targetObject ).onComplete( function ( ...args ) {
+
+			self._handleComplete();
+			userCompleteFn.call( this, ...args );
+
+		} );
+		// replace the tween's onComplete function with our own
+		// so we can call the user's callback if they supply one.
+		tween.onComplete = ( fn ) => {
+
+			userCompleteFn = fn;
+			return tween;
+
+		};
+
+		return tween;
+
+	}
+	update() {
+
+		TWEEN.update();
+		return this.numTweensRunning > 0;
+
+	}
+
 }
 }
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  const tweenManager = new TweenManger();
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const tweenManager = new TweenManger();
 
 
-  function dataMissingInAnySet(fileInfos, latNdx, lonNdx) {
-    for (const fileInfo of fileInfos) {
-      if (fileInfo.file.data[latNdx][lonNdx] === undefined) {
-        return true;
-      }
-    }
-    return false;
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function makeBoxes(file, hueRange, fileInfos) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-    // Used to move the center of the cube so it scales from the position Z axis
-    const originHelper = new THREE.Object3D();
-    originHelper.position.z = 0.5;
-    positionHelper.add(originHelper);
-
-    const color = new THREE.Color();
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    const geometries = [];
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (dataMissingInAnySet(fileInfos, latNdx, lonNdx)) {
-          return;
-        }
-        const amount = (value - min) / range;
-
-        const boxWidth = 1;
-        const boxHeight = 1;
-        const boxDepth = 1;
-        const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the origin helper to
-        // position this geometry
-        positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-        originHelper.updateWorldMatrix(true, false);
-        geometry.applyMatrix4(originHelper.matrixWorld);
-
-        // compute a color
-        const hue = THREE.MathUtils.lerp(...hueRange, amount);
-        const saturation = 1;
-        const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
-        color.setHSL(hue, saturation, lightness);
-        // get the colors as an array of values from 0 to 255
-        const rgb = color.toArray().map(v => v * 255);
-
-        // make an array to store colors for each vertex
-        const numVerts = geometry.getAttribute('position').count;
-        const itemSize = 3;  // r, g, b
-        const colors = new Uint8Array(itemSize * numVerts);
-
-        // copy the color into the colors array for each vertex
-        colors.forEach((v, ndx) => {
-          colors[ndx] = rgb[ndx % 3];
-        });
-
-        const normalized = true;
-        const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
-        geometry.setAttribute('color', colorAttrib);
-
-        geometries.push(geometry);
-      });
-    });
-
-    return BufferGeometryUtils.mergeGeometries(
-       geometries, false);
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  async function loadData(info) {
-    const text = await loadFile(info.url);
-    info.file = parseData(text);
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  async function loadAll() {
-    const fileInfos = [
-      {name: 'men',   hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
-      {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
-    ];
+	{
 
 
-    await Promise.all(fileInfos.map(loadData));
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    function mapValues(data, fn) {
-      return data.map((row, rowNdx) => {
-        return row.map((value, colNdx) => {
-          return fn(value, rowNdx, colNdx);
-        });
-      });
-    }
+	}
 
 
-    function makeDiffFile(baseFile, otherFile, compareFn) {
-      let min;
-      let max;
-      const baseData = baseFile.data;
-      const otherData = otherFile.data;
-      const data = mapValues(baseData, (base, rowNdx, colNdx) => {
-        const other = otherData[rowNdx][colNdx];
-          if (base === undefined || other === undefined) {
-            return undefined;
-          }
-          const value = compareFn(base, other);
-          min = Math.min(min === undefined ? value : min, value);
-          max = Math.max(max === undefined ? value : max, value);
-          return value;
-      });
-      // make a copy of baseFile and replace min, max, and data
-      // with the new data
-      return {...baseFile, min, max, data};
-    }
+	async function loadFile( url ) {
 
 
-    // generate a new set of data
-    {
-      const menInfo = fileInfos[0];
-      const womenInfo = fileInfos[1];
-      const menFile = menInfo.file;
-      const womenFile = womenInfo.file;
+		const req = await fetch( url );
+		return req.text();
 
 
-      function amountGreaterThan(a, b) {
-        return Math.max(a - b, 0);
-      }
-      fileInfos.push({
-        name: '>50%men',
-        hueRange: [0.6, 1.1],
-        file: makeDiffFile(menFile, womenFile, (men, women) => {
-          return amountGreaterThan(men, women);
-        }),
-      });
-      fileInfos.push({
-        name: '>50% women',
-        hueRange: [0.0, 0.4],
-        file: makeDiffFile(womenFile, menFile, (women, men) => {
-          return amountGreaterThan(women, men);
-        }),
-      });
-    }
+	}
 
 
-    // make geometry for each data set
-    const geometries = fileInfos.map((info) => {
-      return makeBoxes(info.file, info.hueRange, fileInfos);
-    });
-
-    // use the first geometry as the base
-    // and add all the geometries as morphtargets
-    const baseGeometry = geometries[0];
-    baseGeometry.morphAttributes.position = geometries.map((geometry, ndx) => {
-      const attribute = geometry.getAttribute('position');
-      const name = `target${ndx}`;
-      attribute.name = name;
-      return attribute;
-    });
-    baseGeometry.morphAttributes.color = geometries.map((geometry, ndx) => {
-      const attribute = geometry.getAttribute('color');
-      const name = `target${ndx}`;
-      attribute.name = name;
-      return attribute;
-    });
-    const material = new THREE.MeshBasicMaterial({
-      vertexColors: true,
-    });
-    const mesh = new THREE.Mesh(baseGeometry, material);
-    scene.add(mesh);
-
-    // show the selected data, hide the rest
-    function showFileInfo(fileInfos, fileInfo) {
-      const targets = {};
-      fileInfos.forEach((info, i) => {
-        const visible = fileInfo === info;
-        info.elem.className = visible ? 'selected' : '';
-        targets[i] = visible ? 1 : 0;
-      });
-      const durationInMs = 1000;
-      tweenManager.createTween(mesh.morphTargetInfluences)
-        .to(targets, durationInMs)
-        .start();
-      requestRenderIfNotRequested();
-    }
+	function parseData( text ) {
 
 
-    const uiElem = document.querySelector('#ui');
-    fileInfos.forEach((info) => {
-      const div = document.createElement('div');
-      info.elem = div;
-      div.textContent = info.name;
-      uiElem.appendChild(div);
-      function show() {
-        showFileInfo(fileInfos, info);
-      }
-      div.addEventListener('mouseover', show);
-      div.addEventListener('touchstart', show);
-    });
-    // show the first set of data
-    showFileInfo(fileInfos, fileInfos[0]);
-  }
-  loadAll();
-
-  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;
-  }
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
 
 
-  let renderRequested = false;
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
 
 
-  function render() {
-    renderRequested = undefined;
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			} else if ( parts.length > 2 ) {
 
 
-    if (tweenManager.update()) {
-      requestRenderIfNotRequested();
-    }
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function dataMissingInAnySet( fileInfos, latNdx, lonNdx ) {
+
+		for ( const fileInfo of fileInfos ) {
+
+			if ( fileInfo.file.data[ latNdx ][ lonNdx ] === undefined ) {
+
+				return true;
+
+			}
+
+		}
+
+		return false;
+
+	}
+
+	function makeBoxes( file, hueRange, fileInfos ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+		// Used to move the center of the cube so it scales from the position Z axis
+		const originHelper = new THREE.Object3D();
+		originHelper.position.z = 0.5;
+		positionHelper.add( originHelper );
+
+		const color = new THREE.Color();
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		const geometries = [];
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( dataMissingInAnySet( fileInfos, latNdx, lonNdx ) ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+
+				const boxWidth = 1;
+				const boxHeight = 1;
+				const boxDepth = 1;
+				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the origin helper to
+				// position this geometry
+				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+				originHelper.updateWorldMatrix( true, false );
+				geometry.applyMatrix4( originHelper.matrixWorld );
+
+				// compute a color
+				const hue = THREE.MathUtils.lerp( ...hueRange, amount );
+				const saturation = 1;
+				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
+				color.setHSL( hue, saturation, lightness );
+				// get the colors as an array of values from 0 to 255
+				const rgb = color.toArray().map( v => v * 255 );
+
+				// make an array to store colors for each vertex
+				const numVerts = geometry.getAttribute( 'position' ).count;
+				const itemSize = 3; // r, g, b
+				const colors = new Uint8Array( itemSize * numVerts );
+
+				// copy the color into the colors array for each vertex
+				colors.forEach( ( v, ndx ) => {
+
+					colors[ ndx ] = rgb[ ndx % 3 ];
+
+				} );
+
+				const normalized = true;
+				const colorAttrib = new THREE.BufferAttribute( colors, itemSize, normalized );
+				geometry.setAttribute( 'color', colorAttrib );
+
+				geometries.push( geometry );
+
+			} );
+
+		} );
+
+		return BufferGeometryUtils.mergeGeometries(
+			geometries, false );
+
+	}
+
+	async function loadData( info ) {
+
+		const text = await loadFile( info.url );
+		info.file = parseData( text );
+
+	}
+
+	async function loadAll() {
+
+		const fileInfos = [
+			{ name: 'men', hueRange: [ 0.7, 0.3 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
+			{ name: 'women', hueRange: [ 0.9, 1.1 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
+		];
+
+		await Promise.all( fileInfos.map( loadData ) );
+
+		function mapValues( data, fn ) {
+
+			return data.map( ( row, rowNdx ) => {
+
+				return row.map( ( value, colNdx ) => {
+
+					return fn( value, rowNdx, colNdx );
+
+				} );
+
+			} );
+
+		}
+
+		function makeDiffFile( baseFile, otherFile, compareFn ) {
+
+			let min;
+			let max;
+			const baseData = baseFile.data;
+			const otherData = otherFile.data;
+			const data = mapValues( baseData, ( base, rowNdx, colNdx ) => {
+
+				const other = otherData[ rowNdx ][ colNdx ];
+				if ( base === undefined || other === undefined ) {
+
+					return undefined;
+
+				}
+
+				const value = compareFn( base, other );
+				min = Math.min( min === undefined ? value : min, value );
+				max = Math.max( max === undefined ? value : max, value );
+				return value;
+
+			} );
+			// make a copy of baseFile and replace min, max, and data
+			// with the new data
+			return { ...baseFile, min, max, data };
+
+		}
+
+		// generate a new set of data
+		{
+
+			const menInfo = fileInfos[ 0 ];
+			const womenInfo = fileInfos[ 1 ];
+			const menFile = menInfo.file;
+			const womenFile = womenInfo.file;
+
+			function amountGreaterThan( a, b ) {
+
+				return Math.max( a - b, 0 );
+
+			}
+
+			fileInfos.push( {
+				name: '>50%men',
+				hueRange: [ 0.6, 1.1 ],
+				file: makeDiffFile( menFile, womenFile, ( men, women ) => {
+
+					return amountGreaterThan( men, women );
+
+				} ),
+			} );
+			fileInfos.push( {
+				name: '>50% women',
+				hueRange: [ 0.0, 0.4 ],
+				file: makeDiffFile( womenFile, menFile, ( women, men ) => {
+
+					return amountGreaterThan( women, men );
+
+				} ),
+			} );
+
+		}
+
+		// make geometry for each data set
+		const geometries = fileInfos.map( ( info ) => {
+
+			return makeBoxes( info.file, info.hueRange, fileInfos );
+
+		} );
+
+		// use the first geometry as the base
+		// and add all the geometries as morphtargets
+		const baseGeometry = geometries[ 0 ];
+		baseGeometry.morphAttributes.position = geometries.map( ( geometry, ndx ) => {
+
+			const attribute = geometry.getAttribute( 'position' );
+			const name = `target${ndx}`;
+			attribute.name = name;
+			return attribute;
+
+		} );
+		baseGeometry.morphAttributes.color = geometries.map( ( geometry, ndx ) => {
+
+			const attribute = geometry.getAttribute( 'color' );
+			const name = `target${ndx}`;
+			attribute.name = name;
+			return attribute;
+
+		} );
+		const material = new THREE.MeshBasicMaterial( {
+			vertexColors: true,
+		} );
+		const mesh = new THREE.Mesh( baseGeometry, material );
+		scene.add( mesh );
+
+		// show the selected data, hide the rest
+		function showFileInfo( fileInfos, fileInfo ) {
+
+			const targets = {};
+			fileInfos.forEach( ( info, i ) => {
+
+				const visible = fileInfo === info;
+				info.elem.className = visible ? 'selected' : '';
+				targets[ i ] = visible ? 1 : 0;
+
+			} );
+			const durationInMs = 1000;
+			tweenManager.createTween( mesh.morphTargetInfluences )
+				.to( targets, durationInMs )
+				.start();
+			requestRenderIfNotRequested();
+
+		}
+
+		const uiElem = document.querySelector( '#ui' );
+		fileInfos.forEach( ( info ) => {
+
+			const div = document.createElement( 'div' );
+			info.elem = div;
+			div.textContent = info.name;
+			uiElem.appendChild( div );
+			function show() {
+
+				showFileInfo( fileInfos, info );
+
+			}
+
+			div.addEventListener( 'mouseover', show );
+			div.addEventListener( 'touchstart', show );
+
+		} );
+		// show the first set of data
+		showFileInfo( fileInfos, fileInfos[ 0 ] );
+
+	}
+
+	loadAll();
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		if ( tweenManager.update() ) {
+
+			requestRenderIfNotRequested();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 342 - 260
manual/examples/lots-of-objects-multiple-data-sets.html

@@ -57,286 +57,368 @@
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function addBoxes(file, hueRange) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-    // Used to move the center of the cube so it scales from the position Z axis
-    const originHelper = new THREE.Object3D();
-    originHelper.position.z = 0.5;
-    positionHelper.add(originHelper);
-
-    const color = new THREE.Color();
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    const geometries = [];
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (value === undefined) {
-          return;
-        }
-        const amount = (value - min) / range;
-
-        const boxWidth = 1;
-        const boxHeight = 1;
-        const boxDepth = 1;
-        const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the origin helper to
-        // position this geometry
-        positionHelper.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-        originHelper.updateWorldMatrix(true, false);
-        geometry.applyMatrix4(originHelper.matrixWorld);
-
-        // compute a color
-        const hue = THREE.MathUtils.lerp(...hueRange, amount);
-        const saturation = 1;
-        const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
-        color.setHSL(hue, saturation, lightness);
-        // get the colors as an array of values from 0 to 255
-        const rgb = color.toArray().map(v => v * 255);
-
-        // make an array to store colors for each vertex
-        const numVerts = geometry.getAttribute('position').count;
-        const itemSize = 3;  // r, g, b
-        const colors = new Uint8Array(itemSize * numVerts);
-
-        // copy the color into the colors array for each vertex
-        colors.forEach((v, ndx) => {
-          colors[ndx] = rgb[ndx % 3];
-        });
-
-        const normalized = true;
-        const colorAttrib = new THREE.BufferAttribute(colors, itemSize, normalized);
-        geometry.setAttribute('color', colorAttrib);
-
-        geometries.push(geometry);
-      });
-    });
-
-    const mergedGeometry = BufferGeometryUtils.mergeGeometries(
-        geometries, false);
-    const material = new THREE.MeshBasicMaterial({
-      vertexColors: true,
-    });
-    const mesh = new THREE.Mesh(mergedGeometry, material);
-    scene.add(mesh);
-    return mesh;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  async function loadData(info) {
-    const text = await loadFile(info.url);
-    info.file = parseData(text);
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  async function loadAll() {
-    const fileInfos = [
-      {name: 'men',   hueRange: [0.7, 0.3], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
-      {name: 'women', hueRange: [0.9, 1.1], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
-    ];
+	{
 
 
-    await Promise.all(fileInfos.map(loadData));
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    function mapValues(data, fn) {
-      return data.map((row, rowNdx) => {
-        return row.map((value, colNdx) => {
-          return fn(value, rowNdx, colNdx);
-        });
-      });
-    }
+	}
 
 
-    function makeDiffFile(baseFile, otherFile, compareFn) {
-      let min;
-      let max;
-      const baseData = baseFile.data;
-      const otherData = otherFile.data;
-      const data = mapValues(baseData, (base, rowNdx, colNdx) => {
-        const other = otherData[rowNdx][colNdx];
-          if (base === undefined || other === undefined) {
-            return undefined;
-          }
-          const value = compareFn(base, other);
-          min = Math.min(min === undefined ? value : min, value);
-          max = Math.max(max === undefined ? value : max, value);
-          return value;
-      });
-      // make a copy of baseFile and replace min, max, and data
-      // with the new data
-      return {...baseFile, min, max, data};
-    }
+	async function loadFile( url ) {
 
 
-    // generate a new set of data
-    {
-      const menInfo = fileInfos[0];
-      const womenInfo = fileInfos[1];
-      const menFile = menInfo.file;
-      const womenFile = womenInfo.file;
+		const req = await fetch( url );
+		return req.text();
 
 
-      function amountGreaterThan(a, b) {
-        return Math.max(a - b, 0);
-      }
-      fileInfos.push({
-        name: '>50%men',
-        hueRange: [0.6, 1.1],
-        file: makeDiffFile(menFile, womenFile, (men, women) => {
-          return amountGreaterThan(men, women);
-        }),
-      });
-      fileInfos.push({
-        name: '>50% women',
-        hueRange: [0.0, 0.4],
-        file: makeDiffFile(womenFile, menFile, (women, men) => {
-          return amountGreaterThan(women, men);
-        }),
-      });
-    }
+	}
 
 
-    // show the selected data, hide the rest
-    function showFileInfo(fileInfos, fileInfo) {
-      fileInfos.forEach((info) => {
-        const visible = fileInfo === info;
-        info.root.visible = visible;
-        info.elem.className = visible ? 'selected' : '';
-      });
-      requestRenderIfNotRequested();
-    }
+	function parseData( text ) {
 
 
-    const uiElem = document.querySelector('#ui');
-    fileInfos.forEach((info) => {
-      const boxes = addBoxes(info.file, info.hueRange);
-      info.root = boxes;
-      const div = document.createElement('div');
-      info.elem = div;
-      div.textContent = info.name;
-      uiElem.appendChild(div);
-      function show() {
-        showFileInfo(fileInfos, info);
-      }
-      div.addEventListener('mouseover', show);
-      div.addEventListener('touchstart', show);
-    });
-    // show the first set of data
-    showFileInfo(fileInfos, fileInfos[0]);
-  }
-  loadAll();
-
-  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;
-  }
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
 
 
-  let renderRequested = false;
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
 
 
-  function render() {
-    renderRequested = undefined;
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			} else if ( parts.length > 2 ) {
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
+
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function addBoxes( file, hueRange ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+		// Used to move the center of the cube so it scales from the position Z axis
+		const originHelper = new THREE.Object3D();
+		originHelper.position.z = 0.5;
+		positionHelper.add( originHelper );
+
+		const color = new THREE.Color();
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		const geometries = [];
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( value === undefined ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+
+				const boxWidth = 1;
+				const boxHeight = 1;
+				const boxDepth = 1;
+				const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the origin helper to
+				// position this geometry
+				positionHelper.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+				originHelper.updateWorldMatrix( true, false );
+				geometry.applyMatrix4( originHelper.matrixWorld );
+
+				// compute a color
+				const hue = THREE.MathUtils.lerp( ...hueRange, amount );
+				const saturation = 1;
+				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
+				color.setHSL( hue, saturation, lightness );
+				// get the colors as an array of values from 0 to 255
+				const rgb = color.toArray().map( v => v * 255 );
+
+				// make an array to store colors for each vertex
+				const numVerts = geometry.getAttribute( 'position' ).count;
+				const itemSize = 3; // r, g, b
+				const colors = new Uint8Array( itemSize * numVerts );
+
+				// copy the color into the colors array for each vertex
+				colors.forEach( ( v, ndx ) => {
+
+					colors[ ndx ] = rgb[ ndx % 3 ];
+
+				} );
+
+				const normalized = true;
+				const colorAttrib = new THREE.BufferAttribute( colors, itemSize, normalized );
+				geometry.setAttribute( 'color', colorAttrib );
+
+				geometries.push( geometry );
+
+			} );
+
+		} );
+
+		const mergedGeometry = BufferGeometryUtils.mergeGeometries(
+			geometries, false );
+		const material = new THREE.MeshBasicMaterial( {
+			vertexColors: true,
+		} );
+		const mesh = new THREE.Mesh( mergedGeometry, material );
+		scene.add( mesh );
+		return mesh;
+
+	}
+
+	async function loadData( info ) {
+
+		const text = await loadFile( info.url );
+		info.file = parseData( text );
+
+	}
+
+	async function loadAll() {
+
+		const fileInfos = [
+			{ name: 'men', hueRange: [ 0.7, 0.3 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' },
+			{ name: 'women', hueRange: [ 0.9, 1.1 ], url: 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014ft_2010_cntm_1_deg.asc' },
+		];
+
+		await Promise.all( fileInfos.map( loadData ) );
+
+		function mapValues( data, fn ) {
+
+			return data.map( ( row, rowNdx ) => {
+
+				return row.map( ( value, colNdx ) => {
+
+					return fn( value, rowNdx, colNdx );
+
+				} );
+
+			} );
+
+		}
+
+		function makeDiffFile( baseFile, otherFile, compareFn ) {
+
+			let min;
+			let max;
+			const baseData = baseFile.data;
+			const otherData = otherFile.data;
+			const data = mapValues( baseData, ( base, rowNdx, colNdx ) => {
+
+				const other = otherData[ rowNdx ][ colNdx ];
+				if ( base === undefined || other === undefined ) {
+
+					return undefined;
+
+				}
+
+				const value = compareFn( base, other );
+				min = Math.min( min === undefined ? value : min, value );
+				max = Math.max( max === undefined ? value : max, value );
+				return value;
+
+			} );
+			// make a copy of baseFile and replace min, max, and data
+			// with the new data
+			return { ...baseFile, min, max, data };
+
+		}
+
+		// generate a new set of data
+		{
+
+			const menInfo = fileInfos[ 0 ];
+			const womenInfo = fileInfos[ 1 ];
+			const menFile = menInfo.file;
+			const womenFile = womenInfo.file;
+
+			function amountGreaterThan( a, b ) {
+
+				return Math.max( a - b, 0 );
+
+			}
+
+			fileInfos.push( {
+				name: '>50%men',
+				hueRange: [ 0.6, 1.1 ],
+				file: makeDiffFile( menFile, womenFile, ( men, women ) => {
+
+					return amountGreaterThan( men, women );
+
+				} ),
+			} );
+			fileInfos.push( {
+				name: '>50% women',
+				hueRange: [ 0.0, 0.4 ],
+				file: makeDiffFile( womenFile, menFile, ( women, men ) => {
+
+					return amountGreaterThan( women, men );
+
+				} ),
+			} );
+
+		}
+
+		// show the selected data, hide the rest
+		function showFileInfo( fileInfos, fileInfo ) {
+
+			fileInfos.forEach( ( info ) => {
+
+				const visible = fileInfo === info;
+				info.root.visible = visible;
+				info.elem.className = visible ? 'selected' : '';
+
+			} );
+			requestRenderIfNotRequested();
+
+		}
+
+		const uiElem = document.querySelector( '#ui' );
+		fileInfos.forEach( ( info ) => {
+
+			const boxes = addBoxes( info.file, info.hueRange );
+			info.root = boxes;
+			const div = document.createElement( 'div' );
+			info.elem = div;
+			div.textContent = info.name;
+			uiElem.appendChild( div );
+			function show() {
+
+				showFileInfo( fileInfos, info );
+
+			}
+
+			div.addEventListener( 'mouseover', show );
+			div.addEventListener( 'touchstart', show );
+
+		} );
+		// show the first set of data
+		showFileInfo( fileInfos, fileInfos[ 0 ] );
+
+	}
+
+	loadAll();
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 192 - 148
manual/examples/lots-of-objects-slow.html

@@ -35,166 +35,210 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 10;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2.5;
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.enableDamping = true;
-  controls.enablePan = false;
-  controls.minDistance = 1.2;
-  controls.maxDistance = 4;
-  controls.update();
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('black');
-
-  {
-    const loader = new THREE.TextureLoader();
-    const texture = loader.load('resources/images/world.jpg', render);
-    const geometry = new THREE.SphereGeometry(1, 64, 32);
-    const material = new THREE.MeshBasicMaterial({map: texture});
-    scene.add(new THREE.Mesh(geometry, material));
-  }
 
 
-  async function loadFile(url) {
-    const req = await fetch(url);
-    return req.text();
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function parseData(text) {
-    const data = [];
-    const settings = {data};
-    let max;
-    let min;
-    // split into lines
-    text.split('\n').forEach((line) => {
-      // split the line by whitespace
-      const parts = line.trim().split(/\s+/);
-      if (parts.length === 2) {
-        // only 2 parts, must be a key/value pair
-        settings[parts[0]] = parseFloat(parts[1]);
-      } else if (parts.length > 2) {
-        // more than 2 parts, must be data
-        const values = parts.map((v) => {
-          const value = parseFloat(v);
-          if (value === settings.NODATA_value) {
-            return undefined;
-          }
-          max = Math.max(max === undefined ? value : max, value);
-          min = Math.min(min === undefined ? value : min, value);
-          return value;
-        });
-        data.push(values);
-      }
-    });
-    return Object.assign(settings, {min, max});
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 10;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2.5;
 
 
-  function addBoxes(file) {
-    const {min, max, data} = file;
-    const range = max - min;
-
-    // make one box geometry
-    const boxWidth = 1;
-    const boxHeight = 1;
-    const boxDepth = 1;
-    const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-    // make it so it scales away from the positive Z axis
-    geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));
-
-    // these helpers will make it easy to position the boxes
-    // We can rotate the lon helper on its Y axis to the longitude
-    const lonHelper = new THREE.Object3D();
-    scene.add(lonHelper);
-    // We rotate the latHelper on its X axis to the latitude
-    const latHelper = new THREE.Object3D();
-    lonHelper.add(latHelper);
-    // The position helper moves the object to the edge of the sphere
-    const positionHelper = new THREE.Object3D();
-    positionHelper.position.z = 1;
-    latHelper.add(positionHelper);
-
-    const lonFudge = Math.PI * .5;
-    const latFudge = Math.PI * -0.135;
-    data.forEach((row, latNdx) => {
-      row.forEach((value, lonNdx) => {
-        if (value === undefined) {
-          return;
-        }
-        const amount = (value - min) / range;
-        const material = new THREE.MeshBasicMaterial();
-        const hue = THREE.MathUtils.lerp(0.7, 0.3, amount);
-        const saturation = 1;
-        const lightness = THREE.MathUtils.lerp(0.4, 1.0, amount);
-        material.color.setHSL(hue, saturation, lightness);
-        const mesh = new THREE.Mesh(geometry, material);
-        scene.add(mesh);
-
-        // adjust the helpers to point to the latitude and longitude
-        lonHelper.rotation.y = THREE.MathUtils.degToRad(lonNdx + file.xllcorner) + lonFudge;
-        latHelper.rotation.x = THREE.MathUtils.degToRad(latNdx + file.yllcorner) + latFudge;
-
-        // use the world matrix of the position helper to
-        // position this mesh.
-        positionHelper.updateWorldMatrix(true, false);
-        mesh.applyMatrix4(positionHelper.matrixWorld);
-
-        mesh.scale.set(0.005, 0.005, THREE.MathUtils.lerp(0.01, 0.5, amount));
-      });
-    });
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.enableDamping = true;
+	controls.enablePan = false;
+	controls.minDistance = 1.2;
+	controls.maxDistance = 4;
+	controls.update();
 
 
-  loadFile('resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc')
-    .then(parseData)
-    .then(addBoxes)
-    .then(render);
-
-  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;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'black' );
 
 
-  let renderRequested = false;
+	{
 
 
-  function render() {
-    renderRequested = undefined;
+		const loader = new THREE.TextureLoader();
+		const texture = loader.load( 'resources/images/world.jpg', render );
+		texture.colorSpace = THREE.SRGBColorSpace;
+		const geometry = new THREE.SphereGeometry( 1, 64, 32 );
+		const material = new THREE.MeshBasicMaterial( { map: texture } );
+		scene.add( new THREE.Mesh( geometry, material ) );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	}
 
 
-    controls.update();
-    renderer.render(scene, camera);
-  }
-  render();
+	async function loadFile( url ) {
 
 
-  function requestRenderIfNotRequested() {
-    if (!renderRequested) {
-      renderRequested = true;
-      requestAnimationFrame(render);
-    }
-  }
+		const req = await fetch( url );
+		return req.text();
+
+	}
+
+	function parseData( text ) {
+
+		const data = [];
+		const settings = { data };
+		let max;
+		let min;
+		// split into lines
+		text.split( '\n' ).forEach( ( line ) => {
+
+			// split the line by whitespace
+			const parts = line.trim().split( /\s+/ );
+			if ( parts.length === 2 ) {
+
+				// only 2 parts, must be a key/value pair
+				settings[ parts[ 0 ] ] = parseFloat( parts[ 1 ] );
+
+			} else if ( parts.length > 2 ) {
+
+				// more than 2 parts, must be data
+				const values = parts.map( ( v ) => {
+
+					const value = parseFloat( v );
+					if ( value === settings.NODATA_value ) {
+
+						return undefined;
+
+					}
+
+					max = Math.max( max === undefined ? value : max, value );
+					min = Math.min( min === undefined ? value : min, value );
+					return value;
+
+				} );
+				data.push( values );
+
+			}
+
+		} );
+		return Object.assign( settings, { min, max } );
+
+	}
+
+	function addBoxes( file ) {
+
+		const { min, max, data } = file;
+		const range = max - min;
+
+		// make one box geometry
+		const boxWidth = 1;
+		const boxHeight = 1;
+		const boxDepth = 1;
+		const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+		// make it so it scales away from the positive Z axis
+		geometry.applyMatrix4( new THREE.Matrix4().makeTranslation( 0, 0, 0.5 ) );
+
+		// these helpers will make it easy to position the boxes
+		// We can rotate the lon helper on its Y axis to the longitude
+		const lonHelper = new THREE.Object3D();
+		scene.add( lonHelper );
+		// We rotate the latHelper on its X axis to the latitude
+		const latHelper = new THREE.Object3D();
+		lonHelper.add( latHelper );
+		// The position helper moves the object to the edge of the sphere
+		const positionHelper = new THREE.Object3D();
+		positionHelper.position.z = 1;
+		latHelper.add( positionHelper );
+
+		const lonFudge = Math.PI * .5;
+		const latFudge = Math.PI * - 0.135;
+		data.forEach( ( row, latNdx ) => {
+
+			row.forEach( ( value, lonNdx ) => {
+
+				if ( value === undefined ) {
+
+					return;
+
+				}
+
+				const amount = ( value - min ) / range;
+				const material = new THREE.MeshBasicMaterial();
+				const hue = THREE.MathUtils.lerp( 0.7, 0.3, amount );
+				const saturation = 1;
+				const lightness = THREE.MathUtils.lerp( 0.4, 1.0, amount );
+				material.color.setHSL( hue, saturation, lightness );
+				const mesh = new THREE.Mesh( geometry, material );
+				scene.add( mesh );
+
+				// adjust the helpers to point to the latitude and longitude
+				lonHelper.rotation.y = THREE.MathUtils.degToRad( lonNdx + file.xllcorner ) + lonFudge;
+				latHelper.rotation.x = THREE.MathUtils.degToRad( latNdx + file.yllcorner ) + latFudge;
+
+				// use the world matrix of the position helper to
+				// position this mesh.
+				positionHelper.updateWorldMatrix( true, false );
+				mesh.applyMatrix4( positionHelper.matrixWorld );
+
+				mesh.scale.set( 0.005, 0.005, THREE.MathUtils.lerp( 0.01, 0.5, amount ) );
+
+			} );
+
+		} );
+
+	}
+
+	loadFile( 'resources/data/gpw/gpw_v4_basic_demographic_characteristics_rev10_a000_014mt_2010_cntm_1_deg.asc' )
+		.then( parseData )
+		.then( addBoxes )
+		.then( render );
+
+	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;
+
+	}
+
+	let renderRequested = false;
+
+	function render() {
+
+		renderRequested = undefined;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		controls.update();
+		renderer.render( scene, camera );
+
+	}
+
+	render();
+
+	function requestRenderIfNotRequested() {
+
+		if ( ! renderRequested ) {
+
+			renderRequested = true;
+			requestAnimationFrame( render );
+
+		}
+
+	}
+
+	controls.addEventListener( 'change', requestRenderIfNotRequested );
+	window.addEventListener( 'resize', requestRenderIfNotRequested );
 
 
-  controls.addEventListener('change', requestRenderIfNotRequested);
-  window.addEventListener('resize', requestRenderIfNotRequested);
 }
 }
 
 
 main();
 main();

+ 143 - 113
manual/examples/multiple-scenes-controls.html

@@ -63,139 +63,169 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {TrackballControls} from 'three/addons/controls/TrackballControls.js';
+import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
 
 
-  const sceneElements = [];
-  function addScene(elem, fn) {
-    sceneElements.push({elem, fn});
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function makeScene(elem) {
-    const scene = new THREE.Scene();
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-    scene.add(camera);
-
-    const controls = new TrackballControls(camera, elem);
-    controls.noZoom = true;
-    controls.noPan = true;
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      camera.add(light);
-    }
+	const sceneElements = [];
+	function addScene( elem, fn ) {
 
 
-    return {scene, camera, controls};
-  }
+		sceneElements.push( { elem, fn } );
 
 
-  const sceneInitFunctionsByName = {
-    'box': (elem) => {
-      const {scene, camera, controls} = makeScene(elem);
-      const geometry = new THREE.BoxGeometry(1, 1, 1);
-      const material = new THREE.MeshPhongMaterial({color: 'red'});
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        controls.handleResize();
-        controls.update();
-        renderer.render(scene, camera);
-      };
-    },
-    'pyramid': (elem) => {
-      const {scene, camera, controls} = makeScene(elem);
-      const radius = .8;
-      const widthSegments = 4;
-      const heightSegments = 2;
-      const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-      const material = new THREE.MeshPhongMaterial({
-        color: 'blue',
-        flatShading: true,
-      });
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        controls.handleResize();
-        controls.update();
-        renderer.render(scene, camera);
-      };
-    },
-  };
-
-  document.querySelectorAll('[data-diagram]').forEach((elem) => {
-    const sceneName = elem.dataset.diagram;
-    const sceneInitFunction = sceneInitFunctionsByName[sceneName];
-    const sceneRenderFunction = sceneInitFunction(elem);
-    addScene(elem, sceneRenderFunction);
-  });
-
-  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 makeScene( elem ) {
+
+		const scene = new THREE.Scene();
+
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
+		scene.add( camera );
+
+		const controls = new TrackballControls( camera, elem );
+		controls.noZoom = true;
+		controls.noPan = true;
+
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			camera.add( light );
+
+		}
+
+		return { scene, camera, controls };
+
+	}
+
+	const sceneInitFunctionsByName = {
+		'box': ( elem ) => {
+
+			const { scene, camera, controls } = makeScene( elem );
+			const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+			const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				controls.handleResize();
+				controls.update();
+				renderer.render( scene, camera );
+
+			};
+
+		},
+		'pyramid': ( elem ) => {
+
+			const { scene, camera, controls } = makeScene( elem );
+			const radius = .8;
+			const widthSegments = 4;
+			const heightSegments = 2;
+			const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+			const material = new THREE.MeshPhongMaterial( {
+				color: 'blue',
+				flatShading: true,
+			} );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				controls.handleResize();
+				controls.update();
+				renderer.render( scene, camera );
 
 
-  const clearColor = new THREE.Color('#000');
-  function render(time) {
-    time *= 0.001;
+			};
 
 
-    resizeRendererToDisplaySize(renderer);
+		},
+	};
 
 
-    renderer.setScissorTest(false);
-    renderer.setClearColor(clearColor, 0);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+	document.querySelectorAll( '[data-diagram]' ).forEach( ( elem ) => {
 
 
-    const transform = `translateY(${window.scrollY}px)`;
-    renderer.domElement.style.transform = transform;
+		const sceneName = elem.dataset.diagram;
+		const sceneInitFunction = sceneInitFunctionsByName[ sceneName ];
+		const sceneRenderFunction = sceneInitFunction( elem );
+		addScene( elem, sceneRenderFunction );
 
 
-    for (const {elem, fn} of sceneElements) {
-      // get the viewport relative position of this element
-      const rect = elem.getBoundingClientRect();
-      const {left, right, top, bottom, width, height} = rect;
+	} );
 
 
-      const isOffscreen =
+	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;
+
+	}
+
+	const clearColor = new THREE.Color( '#000' );
+	function render( time ) {
+
+		time *= 0.001;
+
+		resizeRendererToDisplaySize( renderer );
+
+		renderer.setScissorTest( false );
+		renderer.setClearColor( clearColor, 0 );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		const transform = `translateY(${window.scrollY}px)`;
+		renderer.domElement.style.transform = transform;
+
+		for ( const { elem, fn } of sceneElements ) {
+
+			// get the viewport relative position of this element
+			const rect = elem.getBoundingClientRect();
+			const { left, right, top, bottom, width, height } = rect;
+
+			const isOffscreen =
           bottom < 0 ||
           bottom < 0 ||
           top > renderer.domElement.clientHeight ||
           top > renderer.domElement.clientHeight ||
           right < 0 ||
           right < 0 ||
           left > renderer.domElement.clientWidth;
           left > renderer.domElement.clientWidth;
 
 
-      if (!isOffscreen) {
-        const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-        renderer.setScissor(left, positiveYUpBottom, width, height);
-        renderer.setViewport(left, positiveYUpBottom, width, height);
+			if ( ! isOffscreen ) {
 
 
-        fn(time, rect);
-      }
-    }
+				const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+				renderer.setScissor( left, positiveYUpBottom, width, height );
+				renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    requestAnimationFrame(render);
-  }
+				fn( time, rect );
+
+			}
+
+		}
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 150 - 120
manual/examples/multiple-scenes-copy-canvas.html

@@ -58,138 +58,168 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {TrackballControls} from 'three/addons/controls/TrackballControls.js';
+import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
 
 
 function main() {
 function main() {
-  const canvas = document.createElement('canvas');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
-  renderer.setScissorTest(true);
-
-  const sceneElements = [];
-  function addScene(elem, fn) {
-    const ctx = document.createElement('canvas').getContext('2d');
-    elem.appendChild(ctx.canvas);
-    sceneElements.push({elem, ctx, fn});
-  }
 
 
-  function makeScene(elem) {
-    const scene = new THREE.Scene();
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-    scene.add(camera);
-
-    const controls = new TrackballControls(camera, elem);
-    controls.noZoom = true;
-    controls.noPan = true;
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      camera.add(light);
-    }
+	const canvas = document.createElement( 'canvas' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-    return {scene, camera, controls};
-  }
+	renderer.setScissorTest( true );
+
+	const sceneElements = [];
+	function addScene( elem, fn ) {
+
+		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+		elem.appendChild( ctx.canvas );
+		sceneElements.push( { elem, ctx, fn } );
+
+	}
+
+	function makeScene( elem ) {
+
+		const scene = new THREE.Scene();
+
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
+		scene.add( camera );
+
+		const controls = new TrackballControls( camera, elem );
+		controls.noZoom = true;
+		controls.noPan = true;
+
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			camera.add( light );
+
+		}
+
+		return { scene, camera, controls };
+
+	}
+
+	const sceneInitFunctionsByName = {
+		'box': ( elem ) => {
+
+			const { scene, camera, controls } = makeScene( elem );
+			const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+			const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				controls.handleResize();
+				controls.update();
+				renderer.render( scene, camera );
+
+			};
+
+		},
+		'pyramid': ( elem ) => {
+
+			const { scene, camera, controls } = makeScene( elem );
+			const radius = .8;
+			const widthSegments = 4;
+			const heightSegments = 2;
+			const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+			const material = new THREE.MeshPhongMaterial( {
+				color: 'blue',
+				flatShading: true,
+			} );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				controls.handleResize();
+				controls.update();
+				renderer.render( scene, camera );
+
+			};
 
 
-  const sceneInitFunctionsByName = {
-    'box': (elem) => {
-      const {scene, camera, controls} = makeScene(elem);
-      const geometry = new THREE.BoxGeometry(1, 1, 1);
-      const material = new THREE.MeshPhongMaterial({color: 'red'});
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        controls.handleResize();
-        controls.update();
-        renderer.render(scene, camera);
-      };
-    },
-    'pyramid': (elem) => {
-      const {scene, camera, controls} = makeScene(elem);
-      const radius = .8;
-      const widthSegments = 4;
-      const heightSegments = 2;
-      const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-      const material = new THREE.MeshPhongMaterial({
-        color: 'blue',
-        flatShading: true,
-      });
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        controls.handleResize();
-        controls.update();
-        renderer.render(scene, camera);
-      };
-    },
-  };
-
-  document.querySelectorAll('[data-diagram]').forEach((elem) => {
-    const sceneName = elem.dataset.diagram;
-    const sceneInitFunction = sceneInitFunctionsByName[sceneName];
-    const sceneRenderFunction = sceneInitFunction(elem);
-    addScene(elem, sceneRenderFunction);
-  });
-
-  function render(time) {
-    time *= 0.001;
-
-    for (const {elem, fn, ctx} of sceneElements) {
-      // get the viewport relative position of this element
-      const rect = elem.getBoundingClientRect();
-      const {left, right, top, bottom, width, height} = rect;
-      const rendererCanvas = renderer.domElement;
-
-      const isOffscreen =
+		},
+	};
+
+	document.querySelectorAll( '[data-diagram]' ).forEach( ( elem ) => {
+
+		const sceneName = elem.dataset.diagram;
+		const sceneInitFunction = sceneInitFunctionsByName[ sceneName ];
+		const sceneRenderFunction = sceneInitFunction( elem );
+		addScene( elem, sceneRenderFunction );
+
+	} );
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		for ( const { elem, fn, ctx } of sceneElements ) {
+
+			// get the viewport relative position of this element
+			const rect = elem.getBoundingClientRect();
+			const { left, right, top, bottom, width, height } = rect;
+			const rendererCanvas = renderer.domElement;
+
+			const isOffscreen =
           bottom < 0 ||
           bottom < 0 ||
           top > window.innerHeight ||
           top > window.innerHeight ||
           right < 0 ||
           right < 0 ||
           left > window.innerWidth;
           left > window.innerWidth;
 
 
-      if (!isOffscreen) {
-        // make sure the renderer's canvas is big enough
-        if (rendererCanvas.width < width || rendererCanvas.height < height) {
-          renderer.setSize(width, height, false);
-        }
-
-        // make sure the canvas for this area is the same size as the area
-        if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
-          ctx.canvas.width = width;
-          ctx.canvas.height = height;
-        }
-
-        renderer.setScissor(0, 0, width, height);
-        renderer.setViewport(0, 0, width, height);
-
-        fn(time, rect);
-
-        // copy the rendered scene to this element's canvas
-        ctx.globalCompositeOperation = 'copy';
-        ctx.drawImage(
-            rendererCanvas,
-            0, rendererCanvas.height - height, width, height,  // src rect
-            0, 0, width, height);                              // dst rect
-      }
-    }
+			if ( ! isOffscreen ) {
 
 
-    requestAnimationFrame(render);
-  }
+				// make sure the renderer's canvas is big enough
+				if ( rendererCanvas.width < width || rendererCanvas.height < height ) {
+
+					renderer.setSize( width, height, false );
+
+				}
+
+				// make sure the canvas for this area is the same size as the area
+				if ( ctx.canvas.width !== width || ctx.canvas.height !== height ) {
+
+					ctx.canvas.width = width;
+					ctx.canvas.height = height;
+
+				}
+
+				renderer.setScissor( 0, 0, width, height );
+				renderer.setViewport( 0, 0, width, height );
+
+				fn( time, rect );
+
+				// copy the rendered scene to this element's canvas
+				ctx.globalCompositeOperation = 'copy';
+				ctx.drawImage(
+					rendererCanvas,
+					0, rendererCanvas.height - height, width, height, // src rect
+					0, 0, width, height ); // dst rect
+
+			}
+
+		}
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 123 - 95
manual/examples/multiple-scenes-generic.html

@@ -64,121 +64,149 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
 
 
-  const sceneElements = [];
-  function addScene(elem, fn) {
-    sceneElements.push({elem, fn});
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function makeScene() {
-    const scene = new THREE.Scene();
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      scene.add(light);
-    }
+	const sceneElements = [];
+	function addScene( elem, fn ) {
 
 
-    return {scene, camera};
-  }
+		sceneElements.push( { elem, fn } );
 
 
-  {
-    const elem = document.querySelector('#box');
-    const {scene, camera} = makeScene();
-    const geometry = new THREE.BoxGeometry(1, 1, 1);
-    const material = new THREE.MeshPhongMaterial({color: 'red'});
-    const mesh = new THREE.Mesh(geometry, material);
-    scene.add(mesh);
-    addScene(elem, (time, rect) => {
-      camera.aspect = rect.width / rect.height;
-      camera.updateProjectionMatrix();
-      mesh.rotation.y = time * .1;
-      renderer.render(scene, camera);
-    });
-  }
+	}
 
 
-  {
-    const elem = document.querySelector('#pyramid');
-    const {scene, camera} = makeScene();
-    const radius = .8;
-    const widthSegments = 4;
-    const heightSegments = 2;
-    const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-    const material = new THREE.MeshPhongMaterial({
-      color: 'blue',
-      flatShading: true,
-    });
-    const mesh = new THREE.Mesh(geometry, material);
-    scene.add(mesh);
-    addScene(elem, (time, rect) => {
-      camera.aspect = rect.width / rect.height;
-      camera.updateProjectionMatrix();
-      mesh.rotation.y = time * .1;
-      renderer.render(scene, camera);
-    });
-  }
+	function makeScene() {
 
 
-  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;
-  }
+		const scene = new THREE.Scene();
+
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
+
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			scene.add( light );
+
+		}
+
+		return { scene, camera };
+
+	}
+
+	{
+
+		const elem = document.querySelector( '#box' );
+		const { scene, camera } = makeScene();
+		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+		const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+		const mesh = new THREE.Mesh( geometry, material );
+		scene.add( mesh );
+		addScene( elem, ( time, rect ) => {
+
+			camera.aspect = rect.width / rect.height;
+			camera.updateProjectionMatrix();
+			mesh.rotation.y = time * .1;
+			renderer.render( scene, camera );
+
+		} );
+
+	}
+
+	{
 
 
-  const clearColor = new THREE.Color('#000');
-  function render(time) {
-    time *= 0.001;
+		const elem = document.querySelector( '#pyramid' );
+		const { scene, camera } = makeScene();
+		const radius = .8;
+		const widthSegments = 4;
+		const heightSegments = 2;
+		const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+		const material = new THREE.MeshPhongMaterial( {
+			color: 'blue',
+			flatShading: true,
+		} );
+		const mesh = new THREE.Mesh( geometry, material );
+		scene.add( mesh );
+		addScene( elem, ( time, rect ) => {
 
 
-    resizeRendererToDisplaySize(renderer);
+			camera.aspect = rect.width / rect.height;
+			camera.updateProjectionMatrix();
+			mesh.rotation.y = time * .1;
+			renderer.render( scene, camera );
 
 
-    renderer.setScissorTest(false);
-    renderer.setClearColor(clearColor, 0);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+		} );
 
 
-    const transform = `translateY(${window.scrollY}px)`;
-    renderer.domElement.style.transform = transform;
+	}
 
 
-    for (const {elem, fn} of sceneElements) {
-      // get the viewport relative position of this element
-      const rect = elem.getBoundingClientRect();
-      const {left, right, top, bottom, width, height} = rect;
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-      const isOffscreen =
+		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;
+
+	}
+
+	const clearColor = new THREE.Color( '#000' );
+	function render( time ) {
+
+		time *= 0.001;
+
+		resizeRendererToDisplaySize( renderer );
+
+		renderer.setScissorTest( false );
+		renderer.setClearColor( clearColor, 0 );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		const transform = `translateY(${window.scrollY}px)`;
+		renderer.domElement.style.transform = transform;
+
+		for ( const { elem, fn } of sceneElements ) {
+
+			// get the viewport relative position of this element
+			const rect = elem.getBoundingClientRect();
+			const { left, right, top, bottom, width, height } = rect;
+
+			const isOffscreen =
           bottom < 0 ||
           bottom < 0 ||
           top > renderer.domElement.clientHeight ||
           top > renderer.domElement.clientHeight ||
           right < 0 ||
           right < 0 ||
           left > renderer.domElement.clientWidth;
           left > renderer.domElement.clientWidth;
 
 
-      if (!isOffscreen) {
-        const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-        renderer.setScissor(left, positiveYUpBottom, width, height);
-        renderer.setViewport(left, positiveYUpBottom, width, height);
+			if ( ! isOffscreen ) {
 
 
-        fn(time, rect);
-      }
-    }
+				const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+				renderer.setScissor( left, positiveYUpBottom, width, height );
+				renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    requestAnimationFrame(render);
-  }
+				fn( time, rect );
+
+			}
+
+		}
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 133 - 103
manual/examples/multiple-scenes-selector.html

@@ -64,127 +64,157 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
 
 
-  const sceneElements = [];
-  function addScene(elem, fn) {
-    sceneElements.push({elem, fn});
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function makeScene() {
-    const scene = new THREE.Scene();
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      scene.add(light);
-    }
+	const sceneElements = [];
+	function addScene( elem, fn ) {
 
 
-    return {scene, camera};
-  }
+		sceneElements.push( { elem, fn } );
 
 
-  const sceneInitFunctionsByName = {
-    'box': () => {
-      const {scene, camera} = makeScene();
-      const geometry = new THREE.BoxGeometry(1, 1, 1);
-      const material = new THREE.MeshPhongMaterial({color: 'red'});
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        renderer.render(scene, camera);
-      };
-    },
-    'pyramid': () => {
-      const {scene, camera} = makeScene();
-      const radius = .8;
-      const widthSegments = 4;
-      const heightSegments = 2;
-      const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-      const material = new THREE.MeshPhongMaterial({
-        color: 'blue',
-        flatShading: true,
-      });
-      const mesh = new THREE.Mesh(geometry, material);
-      scene.add(mesh);
-      return (time, rect) => {
-        mesh.rotation.y = time * .1;
-        camera.aspect = rect.width / rect.height;
-        camera.updateProjectionMatrix();
-        renderer.render(scene, camera);
-      };
-    },
-  };
-
-  document.querySelectorAll('[data-diagram]').forEach((elem) => {
-    const sceneName = elem.dataset.diagram;
-    const sceneInitFunction = sceneInitFunctionsByName[sceneName];
-    const sceneRenderFunction = sceneInitFunction(elem);
-    addScene(elem, sceneRenderFunction);
-  });
-
-  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 makeScene() {
+
+		const scene = new THREE.Scene();
+
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
+
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			scene.add( light );
+
+		}
+
+		return { scene, camera };
+
+	}
+
+	const sceneInitFunctionsByName = {
+		'box': () => {
+
+			const { scene, camera } = makeScene();
+			const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+			const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				renderer.render( scene, camera );
+
+			};
+
+		},
+		'pyramid': () => {
+
+			const { scene, camera } = makeScene();
+			const radius = .8;
+			const widthSegments = 4;
+			const heightSegments = 2;
+			const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+			const material = new THREE.MeshPhongMaterial( {
+				color: 'blue',
+				flatShading: true,
+			} );
+			const mesh = new THREE.Mesh( geometry, material );
+			scene.add( mesh );
+			return ( time, rect ) => {
+
+				mesh.rotation.y = time * .1;
+				camera.aspect = rect.width / rect.height;
+				camera.updateProjectionMatrix();
+				renderer.render( scene, camera );
 
 
-  const clearColor = new THREE.Color('#000');
-  function render(time) {
-    time *= 0.001;
+			};
 
 
-    resizeRendererToDisplaySize(renderer);
+		},
+	};
 
 
-    renderer.setScissorTest(false);
-    renderer.setClearColor(clearColor, 0);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+	document.querySelectorAll( '[data-diagram]' ).forEach( ( elem ) => {
 
 
-    const transform = `translateY(${window.scrollY}px)`;
-    renderer.domElement.style.transform = transform;
+		const sceneName = elem.dataset.diagram;
+		const sceneInitFunction = sceneInitFunctionsByName[ sceneName ];
+		const sceneRenderFunction = sceneInitFunction( elem );
+		addScene( elem, sceneRenderFunction );
 
 
-    for (const {elem, fn} of sceneElements) {
-      // get the viewport relative position of this element
-      const rect = elem.getBoundingClientRect();
-      const {left, right, top, bottom, width, height} = rect;
+	} );
 
 
-      const isOffscreen =
+	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;
+
+	}
+
+	const clearColor = new THREE.Color( '#000' );
+	function render( time ) {
+
+		time *= 0.001;
+
+		resizeRendererToDisplaySize( renderer );
+
+		renderer.setScissorTest( false );
+		renderer.setClearColor( clearColor, 0 );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		const transform = `translateY(${window.scrollY}px)`;
+		renderer.domElement.style.transform = transform;
+
+		for ( const { elem, fn } of sceneElements ) {
+
+			// get the viewport relative position of this element
+			const rect = elem.getBoundingClientRect();
+			const { left, right, top, bottom, width, height } = rect;
+
+			const isOffscreen =
           bottom < 0 ||
           bottom < 0 ||
           top > renderer.domElement.clientHeight ||
           top > renderer.domElement.clientHeight ||
           right < 0 ||
           right < 0 ||
           left > renderer.domElement.clientWidth;
           left > renderer.domElement.clientWidth;
 
 
-      if (!isOffscreen) {
-        const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-        renderer.setScissor(left, positiveYUpBottom, width, height);
-        renderer.setViewport(left, positiveYUpBottom, width, height);
+			if ( ! isOffscreen ) {
 
 
-        fn(time, rect);
-      }
-    }
+				const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+				renderer.setScissor( left, positiveYUpBottom, width, height );
+				renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    requestAnimationFrame(render);
-  }
+				fn( time, rect );
+
+			}
+
+		}
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 109 - 87
manual/examples/multiple-scenes-v1.html

@@ -64,117 +64,139 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
-
-  function makeScene(elem) {
-    const scene = new THREE.Scene();
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      scene.add(light);
-    }
 
 
-    return {scene, camera, elem};
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function setupScene1() {
-    const sceneInfo = makeScene(document.querySelector('#box'));
-    const geometry = new THREE.BoxGeometry(1, 1, 1);
-    const material = new THREE.MeshPhongMaterial({color: 'red'});
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+	function makeScene( elem ) {
 
 
-  function setupScene2() {
-    const sceneInfo = makeScene(document.querySelector('#pyramid'));
-    const radius = .8;
-    const widthSegments = 4;
-    const heightSegments = 2;
-    const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-    const material = new THREE.MeshPhongMaterial({
-      color: 'blue',
-      flatShading: true,
-    });
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+		const scene = new THREE.Scene();
 
 
-  const sceneInfo1 = setupScene1();
-  const sceneInfo2 = setupScene2();
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
 
 
-  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;
-  }
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			scene.add( light );
+
+		}
+
+		return { scene, camera, elem };
+
+	}
+
+	function setupScene1() {
+
+		const sceneInfo = makeScene( document.querySelector( '#box' ) );
+		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+		const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	function setupScene2() {
+
+		const sceneInfo = makeScene( document.querySelector( '#pyramid' ) );
+		const radius = .8;
+		const widthSegments = 4;
+		const heightSegments = 2;
+		const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+		const material = new THREE.MeshPhongMaterial( {
+			color: 'blue',
+			flatShading: true,
+		} );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	const sceneInfo1 = setupScene1();
+	const sceneInfo2 = setupScene2();
 
 
-  function renderSceneInfo(sceneInfo) {
-    const {scene, camera, elem} = sceneInfo;
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    // get the viewport relative position of this element
-    const {left, right, top, bottom, width, height} =
+		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 renderSceneInfo( sceneInfo ) {
+
+		const { scene, camera, elem } = sceneInfo;
+
+		// get the viewport relative position of this element
+		const { left, right, top, bottom, width, height } =
         elem.getBoundingClientRect();
         elem.getBoundingClientRect();
 
 
-    const isOffscreen =
+		const isOffscreen =
         bottom < 0 ||
         bottom < 0 ||
         top > renderer.domElement.clientHeight ||
         top > renderer.domElement.clientHeight ||
         right < 0 ||
         right < 0 ||
         left > renderer.domElement.clientWidth;
         left > renderer.domElement.clientWidth;
 
 
-    if (isOffscreen) {
-      return;
-    }
+		if ( isOffscreen ) {
 
 
-    camera.aspect = width / height;
-    camera.updateProjectionMatrix();
+			return;
 
 
-    const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-    renderer.setScissor(left, positiveYUpBottom, width, height);
-    renderer.setViewport(left, positiveYUpBottom, width, height);
+		}
 
 
-    renderer.render(scene, camera);
-  }
+		camera.aspect = width / height;
+		camera.updateProjectionMatrix();
 
 
-  function render(time) {
-    time *= 0.001;
+		const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+		renderer.setScissor( left, positiveYUpBottom, width, height );
+		renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    resizeRendererToDisplaySize(renderer);
+		renderer.render( scene, camera );
 
 
-    renderer.setScissorTest(false);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+	}
 
 
-    sceneInfo1.mesh.rotation.y = time * .1;
-    sceneInfo2.mesh.rotation.y = time * .1;
+	function render( time ) {
 
 
-    renderSceneInfo(sceneInfo1);
-    renderSceneInfo(sceneInfo2);
+		time *= 0.001;
 
 
-    requestAnimationFrame(render);
-  }
+		resizeRendererToDisplaySize( renderer );
+
+		renderer.setScissorTest( false );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		sceneInfo1.mesh.rotation.y = time * .1;
+		sceneInfo2.mesh.rotation.y = time * .1;
+
+		renderSceneInfo( sceneInfo1 );
+		renderSceneInfo( sceneInfo2 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 112 - 90
manual/examples/multiple-scenes-v2.html

@@ -65,120 +65,142 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
-
-  function makeScene(elem) {
-    const scene = new THREE.Scene();
-    scene.background = new THREE.Color('#0F0');
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      scene.add(light);
-    }
 
 
-    return {scene, camera, elem};
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function setupScene1() {
-    const sceneInfo = makeScene(document.querySelector('#box'));
-    const geometry = new THREE.BoxGeometry(1, 1, 1);
-    const material = new THREE.MeshPhongMaterial({color: 'red'});
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+	function makeScene( elem ) {
 
 
-  function setupScene2() {
-    const sceneInfo = makeScene(document.querySelector('#pyramid'));
-    const radius = .8;
-    const widthSegments = 4;
-    const heightSegments = 2;
-    const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-    const material = new THREE.MeshPhongMaterial({
-      color: 'blue',
-      flatShading: true,
-    });
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+		const scene = new THREE.Scene();
+		scene.background = new THREE.Color( '#0F0' );
 
 
-  const sceneInfo1 = setupScene1();
-  const sceneInfo2 = setupScene2();
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
 
 
-  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;
-  }
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			scene.add( light );
+
+		}
+
+		return { scene, camera, elem };
+
+	}
+
+	function setupScene1() {
+
+		const sceneInfo = makeScene( document.querySelector( '#box' ) );
+		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+		const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	function setupScene2() {
+
+		const sceneInfo = makeScene( document.querySelector( '#pyramid' ) );
+		const radius = .8;
+		const widthSegments = 4;
+		const heightSegments = 2;
+		const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+		const material = new THREE.MeshPhongMaterial( {
+			color: 'blue',
+			flatShading: true,
+		} );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	const sceneInfo1 = setupScene1();
+	const sceneInfo2 = setupScene2();
 
 
-  function renderSceneInfo(sceneInfo) {
-    const {scene, camera, elem} = sceneInfo;
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    // get the viewport relative position of this element
-    const {left, right, top, bottom, width, height} =
+		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 renderSceneInfo( sceneInfo ) {
+
+		const { scene, camera, elem } = sceneInfo;
+
+		// get the viewport relative position of this element
+		const { left, right, top, bottom, width, height } =
         elem.getBoundingClientRect();
         elem.getBoundingClientRect();
 
 
-    const isOffscreen =
+		const isOffscreen =
         bottom < 0 ||
         bottom < 0 ||
         top > renderer.domElement.clientHeight ||
         top > renderer.domElement.clientHeight ||
         right < 0 ||
         right < 0 ||
         left > renderer.domElement.clientWidth;
         left > renderer.domElement.clientWidth;
 
 
-    if (isOffscreen) {
-      return;
-    }
+		if ( isOffscreen ) {
 
 
-    camera.aspect = width / height;
-    camera.updateProjectionMatrix();
+			return;
 
 
-    const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-    renderer.setScissor(left, positiveYUpBottom, width, height);
-    renderer.setViewport(left, positiveYUpBottom, width, height);
+		}
 
 
-    renderer.render(scene, camera);
-  }
+		camera.aspect = width / height;
+		camera.updateProjectionMatrix();
 
 
-  const clearColor = new THREE.Color('#000');
-  function render(time) {
-    time *= 0.001;
+		const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+		renderer.setScissor( left, positiveYUpBottom, width, height );
+		renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    resizeRendererToDisplaySize(renderer);
+		renderer.render( scene, camera );
 
 
-    renderer.setScissorTest(false);
-    renderer.setClearColor(clearColor, 0);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+	}
 
 
-    sceneInfo1.mesh.rotation.y = time * .1;
-    sceneInfo2.mesh.rotation.y = time * .1;
+	const clearColor = new THREE.Color( '#000' );
+	function render( time ) {
 
 
-    renderSceneInfo(sceneInfo1);
-    renderSceneInfo(sceneInfo2);
+		time *= 0.001;
 
 
-    requestAnimationFrame(render);
-  }
+		resizeRendererToDisplaySize( renderer );
+
+		renderer.setScissorTest( false );
+		renderer.setClearColor( clearColor, 0 );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		sceneInfo1.mesh.rotation.y = time * .1;
+		sceneInfo2.mesh.rotation.y = time * .1;
+
+		renderSceneInfo( sceneInfo1 );
+		renderSceneInfo( sceneInfo2 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 114 - 92
manual/examples/multiple-scenes-v3.html

@@ -65,123 +65,145 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
-
-  function makeScene(elem) {
-    const scene = new THREE.Scene();
-    scene.background = new THREE.Color('#0F0');
-
-    const fov = 45;
-    const aspect = 2;  // the canvas default
-    const near = 0.1;
-    const far = 5;
-    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-    camera.position.set(0, 1, 2);
-    camera.lookAt(0, 0, 0);
-
-    {
-      const color = 0xFFFFFF;
-      const intensity = 1;
-      const light = new THREE.DirectionalLight(color, intensity);
-      light.position.set(-1, 2, 4);
-      scene.add(light);
-    }
 
 
-    return {scene, camera, elem};
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas, alpha: true } );
+	renderer.useLegacyLights = false;
 
 
-  function setupScene1() {
-    const sceneInfo = makeScene(document.querySelector('#box'));
-    const geometry = new THREE.BoxGeometry(1, 1, 1);
-    const material = new THREE.MeshPhongMaterial({color: 'red'});
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+	function makeScene( elem ) {
 
 
-  function setupScene2() {
-    const sceneInfo = makeScene(document.querySelector('#pyramid'));
-    const radius = .8;
-    const widthSegments = 4;
-    const heightSegments = 2;
-    const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
-    const material = new THREE.MeshPhongMaterial({
-      color: 'blue',
-      flatShading: true,
-    });
-    const mesh = new THREE.Mesh(geometry, material);
-    sceneInfo.scene.add(mesh);
-    sceneInfo.mesh = mesh;
-    return sceneInfo;
-  }
+		const scene = new THREE.Scene();
+		scene.background = new THREE.Color( '#0F0' );
 
 
-  const sceneInfo1 = setupScene1();
-  const sceneInfo2 = setupScene2();
+		const fov = 45;
+		const aspect = 2; // the canvas default
+		const near = 0.1;
+		const far = 5;
+		const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+		camera.position.set( 0, 1, 2 );
+		camera.lookAt( 0, 0, 0 );
 
 
-  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;
-  }
+		{
+
+			const color = 0xFFFFFF;
+			const intensity = 3;
+			const light = new THREE.DirectionalLight( color, intensity );
+			light.position.set( - 1, 2, 4 );
+			scene.add( light );
+
+		}
+
+		return { scene, camera, elem };
+
+	}
+
+	function setupScene1() {
+
+		const sceneInfo = makeScene( document.querySelector( '#box' ) );
+		const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+		const material = new THREE.MeshPhongMaterial( { color: 'red' } );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	function setupScene2() {
+
+		const sceneInfo = makeScene( document.querySelector( '#pyramid' ) );
+		const radius = .8;
+		const widthSegments = 4;
+		const heightSegments = 2;
+		const geometry = new THREE.SphereGeometry( radius, widthSegments, heightSegments );
+		const material = new THREE.MeshPhongMaterial( {
+			color: 'blue',
+			flatShading: true,
+		} );
+		const mesh = new THREE.Mesh( geometry, material );
+		sceneInfo.scene.add( mesh );
+		sceneInfo.mesh = mesh;
+		return sceneInfo;
+
+	}
+
+	const sceneInfo1 = setupScene1();
+	const sceneInfo2 = setupScene2();
 
 
-  function renderSceneInfo(sceneInfo) {
-    const {scene, camera, elem} = sceneInfo;
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    // get the viewport relative position of this element
-    const {left, right, top, bottom, width, height} =
+		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 renderSceneInfo( sceneInfo ) {
+
+		const { scene, camera, elem } = sceneInfo;
+
+		// get the viewport relative position of this element
+		const { left, right, top, bottom, width, height } =
         elem.getBoundingClientRect();
         elem.getBoundingClientRect();
 
 
-    const isOffscreen =
+		const isOffscreen =
         bottom < 0 ||
         bottom < 0 ||
         top > renderer.domElement.clientHeight ||
         top > renderer.domElement.clientHeight ||
         right < 0 ||
         right < 0 ||
         left > renderer.domElement.clientWidth;
         left > renderer.domElement.clientWidth;
 
 
-    if (isOffscreen) {
-      return;
-    }
+		if ( isOffscreen ) {
 
 
-    camera.aspect = width / height;
-    camera.updateProjectionMatrix();
+			return;
 
 
-    const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
-    renderer.setScissor(left, positiveYUpBottom, width, height);
-    renderer.setViewport(left, positiveYUpBottom, width, height);
+		}
 
 
-    renderer.render(scene, camera);
-  }
+		camera.aspect = width / height;
+		camera.updateProjectionMatrix();
 
 
-  const clearColor = new THREE.Color('#000');
-  function render(time) {
-    time *= 0.001;
+		const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
+		renderer.setScissor( left, positiveYUpBottom, width, height );
+		renderer.setViewport( left, positiveYUpBottom, width, height );
 
 
-    resizeRendererToDisplaySize(renderer);
+		renderer.render( scene, camera );
 
 
-    renderer.setScissorTest(false);
-    renderer.setClearColor(clearColor, 0);
-    renderer.clear(true, true);
-    renderer.setScissorTest(true);
+	}
 
 
-    const transform = `translateY(${window.scrollY}px)`;
-    renderer.domElement.style.transform = transform;
+	const clearColor = new THREE.Color( '#000' );
+	function render( time ) {
 
 
-    sceneInfo1.mesh.rotation.y = time * .1;
-    sceneInfo2.mesh.rotation.y = time * .1;
+		time *= 0.001;
 
 
-    renderSceneInfo(sceneInfo1);
-    renderSceneInfo(sceneInfo2);
+		resizeRendererToDisplaySize( renderer );
 
 
-    requestAnimationFrame(render);
-  }
+		renderer.setScissorTest( false );
+		renderer.setClearColor( clearColor, 0 );
+		renderer.clear( true, true );
+		renderer.setScissorTest( true );
+
+		const transform = `translateY(${window.scrollY}px)`;
+		renderer.domElement.style.transform = transform;
+
+		sceneInfo1.mesh.rotation.y = time * .1;
+		sceneInfo2.mesh.rotation.y = time * .1;
+
+		renderSceneInfo( sceneInfo1 );
+		renderSceneInfo( sceneInfo2 );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 46 - 31
manual/examples/offscreencanvas-w-fallback.html

@@ -21,47 +21,62 @@
     <canvas id="c"></canvas>
     <canvas id="c"></canvas>
   </body>
   </body>
 <script type="module">
 <script type="module">
-import {init, state} from './shared-cubes.js';
+import { init, state } from './shared-cubes.js';
 
 
-function startWorker(canvas) {
-  const offscreen = canvas.transferControlToOffscreen();
-  const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
-  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
+function startWorker( canvas ) {
 
 
-  function sendSize() {
-    worker.postMessage({
-      type: 'size',
-      width: canvas.clientWidth,
-      height: canvas.clientHeight,
-    });
-  }
+	const offscreen = canvas.transferControlToOffscreen();
+	const worker = new Worker( 'offscreencanvas-worker-cubes.js', { type: 'module' } );
+	worker.postMessage( { type: 'init', canvas: offscreen }, [ offscreen ] );
 
 
-  window.addEventListener('resize', sendSize);
-  sendSize();
+	function sendSize() {
+
+		worker.postMessage( {
+			type: 'size',
+			width: canvas.clientWidth,
+			height: canvas.clientHeight,
+		} );
+
+	}
+
+	window.addEventListener( 'resize', sendSize );
+	sendSize();
+
+	console.log( 'using OffscreenCanvas' ); /* eslint-disable-line no-console */
 
 
-  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
 }
 }
 
 
-function startMainPage(canvas) {
-  init({canvas});
+function startMainPage( canvas ) {
+
+	init( { canvas } );
+
+	function sendSize() {
+
+		state.width = canvas.clientWidth;
+		state.height = canvas.clientHeight;
+
+	}
 
 
-  function sendSize() {
-    state.width = canvas.clientWidth;
-    state.height = canvas.clientHeight;
-  }
-  window.addEventListener('resize', sendSize);
-  sendSize();
+	window.addEventListener( 'resize', sendSize );
+	sendSize();
+
+	console.log( 'using regular canvas' ); /* eslint-disable-line no-console */
 
 
-  console.log('using regular canvas');  /* eslint-disable-line no-console */
 }
 }
 
 
-function main() {  /* eslint consistent-return: 0 */
-  const canvas = document.querySelector('#c');
-  if (canvas.transferControlToOffscreen) {
-    startWorker(canvas);
-  } else {
-    startMainPage(canvas);
-  }
+function main() { /* eslint consistent-return: 0 */
+
+	const canvas = document.querySelector( '#c' );
+	if ( canvas.transferControlToOffscreen ) {
+
+		startWorker( canvas );
+
+	} else {
+
+		startMainPage( canvas );
+
+	}
+
 }
 }
 
 
 main();
 main();

+ 174 - 131
manual/examples/offscreencanvas-w-orbitcontrols.html

@@ -24,162 +24,205 @@
     <canvas id="c" tabindex="1"></canvas>
     <canvas id="c" tabindex="1"></canvas>
   </body>
   </body>
 <script type="module">
 <script type="module">
-import {init} from './shared-orbitcontrols.js';
-
-const mouseEventHandler = makeSendPropertiesHandler([
-  'ctrlKey',
-  'metaKey',
-  'shiftKey',
-  'button',
-  'pointerType',
-  'clientX',
-  'clientY',
-  'pageX',
-  'pageY',
-]);
-const wheelEventHandlerImpl = makeSendPropertiesHandler([
-  'deltaX',
-  'deltaY',
-]);
-const keydownEventHandler = makeSendPropertiesHandler([
-  'ctrlKey',
-  'metaKey',
-  'shiftKey',
-  'keyCode',
-]);
-
-function wheelEventHandler(event, sendFn) {
-  event.preventDefault();
-  wheelEventHandlerImpl(event, sendFn);
+import { init } from './shared-orbitcontrols.js';
+
+const mouseEventHandler = makeSendPropertiesHandler( [
+	'ctrlKey',
+	'metaKey',
+	'shiftKey',
+	'button',
+	'pointerType',
+	'clientX',
+	'clientY',
+	'pageX',
+	'pageY',
+] );
+const wheelEventHandlerImpl = makeSendPropertiesHandler( [
+	'deltaX',
+	'deltaY',
+] );
+const keydownEventHandler = makeSendPropertiesHandler( [
+	'ctrlKey',
+	'metaKey',
+	'shiftKey',
+	'keyCode',
+] );
+
+function wheelEventHandler( event, sendFn ) {
+
+	event.preventDefault();
+	wheelEventHandlerImpl( event, sendFn );
+
 }
 }
 
 
-function preventDefaultHandler(event) {
-  event.preventDefault();
+function preventDefaultHandler( event ) {
+
+	event.preventDefault();
+
 }
 }
 
 
-function copyProperties(src, properties, dst) {
-  for (const name of properties) {
-      dst[name] = src[name];
-  }
+function copyProperties( src, properties, dst ) {
+
+	for ( const name of properties ) {
+
+		dst[ name ] = src[ name ];
+
+	}
+
 }
 }
 
 
-function makeSendPropertiesHandler(properties) {
-  return function sendProperties(event, sendFn) {
-    const data = {type: event.type};
-    copyProperties(event, properties, data);
-    sendFn(data);
-  };
+function makeSendPropertiesHandler( properties ) {
+
+	return function sendProperties( event, sendFn ) {
+
+		const data = { type: event.type };
+		copyProperties( event, properties, data );
+		sendFn( data );
+
+	};
+
 }
 }
 
 
-function touchEventHandler(event, sendFn) {
-  const touches = [];
-  const data = {type: event.type, touches};
-  for (let i = 0; i < event.touches.length; ++i) {
-    const touch = event.touches[i];
-    touches.push({
-      pageX: touch.pageX,
-      pageY: touch.pageY,
-    });
-  }
-  sendFn(data);
+function touchEventHandler( event, sendFn ) {
+
+	const touches = [];
+	const data = { type: event.type, touches };
+	for ( let i = 0; i < event.touches.length; ++ i ) {
+
+		const touch = event.touches[ i ];
+		touches.push( {
+			pageX: touch.pageX,
+			pageY: touch.pageY,
+		} );
+
+	}
+
+	sendFn( data );
+
 }
 }
 
 
 // The four arrow keys
 // The four arrow keys
 const orbitKeys = {
 const orbitKeys = {
-  '37': true,  // left
-  '38': true,  // up
-  '39': true,  // right
-  '40': true,  // down
+	'37': true, // left
+	'38': true, // up
+	'39': true, // right
+	'40': true, // down
 };
 };
-function filteredKeydownEventHandler(event, sendFn) {
-  const {keyCode} = event;
-  if (orbitKeys[keyCode]) {
-    event.preventDefault();
-    keydownEventHandler(event, sendFn);
-  }
+function filteredKeydownEventHandler( event, sendFn ) {
+
+	const { keyCode } = event;
+	if ( orbitKeys[ keyCode ] ) {
+
+		event.preventDefault();
+		keydownEventHandler( event, sendFn );
+
+	}
+
 }
 }
 
 
 let nextProxyId = 0;
 let nextProxyId = 0;
 class ElementProxy {
 class ElementProxy {
-  constructor(element, worker, eventHandlers) {
-    this.id = nextProxyId++;
-    this.worker = worker;
-    const sendEvent = (data) => {
-      this.worker.postMessage({
-        type: 'event',
-        id: this.id,
-        data,
-      });
-    };
-
-    // register an id
-    worker.postMessage({
-      type: 'makeProxy',
-      id: this.id,
-    });
-    sendSize();
-    for (const [eventName, handler] of Object.entries(eventHandlers)) {
-      element.addEventListener(eventName, function(event) {
-        handler(event, sendEvent);
-      });
-    }
 
 
-    function sendSize() {
-      const rect = element.getBoundingClientRect();
-      sendEvent({
-        type: 'size',
-        left: rect.left,
-        top: rect.top,
-        width: element.clientWidth,
-        height: element.clientHeight,
-      });
-    }
+	constructor( element, worker, eventHandlers ) {
+
+		this.id = nextProxyId ++;
+		this.worker = worker;
+		const sendEvent = ( data ) => {
+
+			this.worker.postMessage( {
+				type: 'event',
+				id: this.id,
+				data,
+			} );
+
+		};
+
+		// register an id
+		worker.postMessage( {
+			type: 'makeProxy',
+			id: this.id,
+		} );
+		sendSize();
+		for ( const [ eventName, handler ] of Object.entries( eventHandlers ) ) {
+
+			element.addEventListener( eventName, function ( event ) {
+
+				handler( event, sendEvent );
+
+			} );
+
+		}
+
+		function sendSize() {
+
+			const rect = element.getBoundingClientRect();
+			sendEvent( {
+				type: 'size',
+				left: rect.left,
+				top: rect.top,
+				width: element.clientWidth,
+				height: element.clientHeight,
+			} );
+
+		}
+
+		// really need to use ResizeObserver
+		window.addEventListener( 'resize', sendSize );
+
+	}
 
 
-    // really need to use ResizeObserver
-    window.addEventListener('resize', sendSize);
-  }
 }
 }
 
 
-function startWorker(canvas) {
-  canvas.focus();
-  const offscreen = canvas.transferControlToOffscreen();
-  const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
-
-  const eventHandlers = {
-    contextmenu: preventDefaultHandler,
-    mousedown: mouseEventHandler,
-    mousemove: mouseEventHandler,
-    mouseup: mouseEventHandler,
-    pointerdown: mouseEventHandler,
-    pointermove: mouseEventHandler,
-    pointerup: mouseEventHandler,
-    touchstart: touchEventHandler,
-    touchmove: touchEventHandler,
-    touchend: touchEventHandler,
-    wheel: wheelEventHandler,
-    keydown: filteredKeydownEventHandler,
-  };
-  const proxy = new ElementProxy(canvas, worker, eventHandlers);
-  worker.postMessage({
-    type: 'start',
-    canvas: offscreen,
-    canvasId: proxy.id,
-  }, [offscreen]);
-  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
+function startWorker( canvas ) {
+
+	canvas.focus();
+	const offscreen = canvas.transferControlToOffscreen();
+	const worker = new Worker( 'offscreencanvas-worker-orbitcontrols.js', { type: 'module' } );
+
+	const eventHandlers = {
+		contextmenu: preventDefaultHandler,
+		mousedown: mouseEventHandler,
+		mousemove: mouseEventHandler,
+		mouseup: mouseEventHandler,
+		pointerdown: mouseEventHandler,
+		pointermove: mouseEventHandler,
+		pointerup: mouseEventHandler,
+		touchstart: touchEventHandler,
+		touchmove: touchEventHandler,
+		touchend: touchEventHandler,
+		wheel: wheelEventHandler,
+		keydown: filteredKeydownEventHandler,
+	};
+	const proxy = new ElementProxy( canvas, worker, eventHandlers );
+	worker.postMessage( {
+		type: 'start',
+		canvas: offscreen,
+		canvasId: proxy.id,
+	}, [ offscreen ] );
+	console.log( 'using OffscreenCanvas' ); /* eslint-disable-line no-console */
+
 }
 }
 
 
-function startMainPage(canvas) {
-  init({canvas, inputElement: canvas});
-  console.log('using regular canvas');  /* eslint-disable-line no-console */
+function startMainPage( canvas ) {
+
+	init( { canvas, inputElement: canvas } );
+	console.log( 'using regular canvas' ); /* eslint-disable-line no-console */
+
 }
 }
 
 
-function main() {  /* eslint consistent-return: 0 */
-  const canvas = document.querySelector('#c');
-  if (canvas.transferControlToOffscreen) {
-    startWorker(canvas);
-  } else {
-    startMainPage(canvas);
-  }
+function main() { /* eslint consistent-return: 0 */
+
+	const canvas = document.querySelector( '#c' );
+	if ( canvas.transferControlToOffscreen ) {
+
+		startWorker( canvas );
+
+	} else {
+
+		startMainPage( canvas );
+
+	}
+
 }
 }
 
 
 main();
 main();

+ 115 - 84
manual/examples/offscreencanvas-w-picking.html

@@ -21,101 +21,132 @@
     <canvas id="c"></canvas>
     <canvas id="c"></canvas>
   </body>
   </body>
 <script type="module">
 <script type="module">
-import {state, init, pickPosition} from './shared-picking.js';
+import { state, init, pickPosition } from './shared-picking.js';
 
 
 let sendMouse;
 let sendMouse;
 
 
-function startWorker(canvas) {
-  const offscreen = canvas.transferControlToOffscreen();
-  const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
-  worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
-
-  sendMouse = (x, y) => {
-    worker.postMessage({
-      type: 'mouse',
-      x,
-      y,
-    });
-  };
-
-  function sendSize() {
-    worker.postMessage({
-      type: 'size',
-      width: canvas.clientWidth,
-      height: canvas.clientHeight,
-    });
-  }
-
-  window.addEventListener('resize', sendSize);
-  sendSize();
-
-  console.log('using OffscreenCanvas');  /* eslint-disable-line no-console */
+function startWorker( canvas ) {
+
+	const offscreen = canvas.transferControlToOffscreen();
+	const worker = new Worker( 'offscreencanvas-worker-picking.js', { type: 'module' } );
+	worker.postMessage( { type: 'init', canvas: offscreen }, [ offscreen ] );
+
+	sendMouse = ( x, y ) => {
+
+		worker.postMessage( {
+			type: 'mouse',
+			x,
+			y,
+		} );
+
+	};
+
+	function sendSize() {
+
+		worker.postMessage( {
+			type: 'size',
+			width: canvas.clientWidth,
+			height: canvas.clientHeight,
+		} );
+
+	}
+
+	window.addEventListener( 'resize', sendSize );
+	sendSize();
+
+	console.log( 'using OffscreenCanvas' ); /* eslint-disable-line no-console */
+
 }
 }
 
 
-function startMainPage(canvas) {
-  init({canvas});
+function startMainPage( canvas ) {
+
+	init( { canvas } );
 
 
-  sendMouse = (x, y) => {
-    pickPosition.x = x;
-    pickPosition.y = y;
-  };
+	sendMouse = ( x, y ) => {
 
 
-  function sendSize() {
-    state.width = canvas.clientWidth;
-    state.height = canvas.clientHeight;
-  }
-  window.addEventListener('resize', sendSize);
-  sendSize();
+		pickPosition.x = x;
+		pickPosition.y = y;
+
+	};
+
+	function sendSize() {
+
+		state.width = canvas.clientWidth;
+		state.height = canvas.clientHeight;
+
+	}
+
+	window.addEventListener( 'resize', sendSize );
+	sendSize();
+
+	console.log( 'using regular canvas' ); /* eslint-disable-line no-console */
 
 
-  console.log('using regular canvas');  /* eslint-disable-line no-console */
 }
 }
 
 
-function main() {  /* eslint consistent-return: 0 */
-  const canvas = document.querySelector('#c');
-  if (canvas.transferControlToOffscreen) {
-    startWorker(canvas);
-  } else {
-    startMainPage(canvas);
-  }
-
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-   };
-  }
-
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    sendMouse(
-        (pos.x / canvas.width ) *  2 - 1,
-        (pos.y / canvas.height) * -2 + 1);  // note we flip Y
-  }
-
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    sendMouse(-100000, -100000);
-  }
-  window.addEventListener('mousemove', setPickPosition);
-  window.addEventListener('mouseout', clearPickPosition);
-  window.addEventListener('mouseleave', clearPickPosition);
-
-  window.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
-
-  window.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
-
-  window.addEventListener('touchend', clearPickPosition);
+function main() { /* eslint consistent-return: 0 */
+
+	const canvas = document.querySelector( '#c' );
+	if ( canvas.transferControlToOffscreen ) {
+
+		startWorker( canvas );
+
+	} else {
+
+		startMainPage( canvas );
+
+	}
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		sendMouse(
+			( pos.x / canvas.width ) * 2 - 1,
+			( pos.y / canvas.height ) * - 2 + 1 ); // note we flip Y
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		sendMouse( - 100000, - 100000 );
+
+	}
+
+	window.addEventListener( 'mousemove', setPickPosition );
+	window.addEventListener( 'mouseout', clearPickPosition );
+	window.addEventListener( 'mouseleave', clearPickPosition );
+
+	window.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	window.addEventListener( 'touchmove', ( event ) => {
+
+		setPickPosition( event.touches[ 0 ] );
+
+	} );
+
+	window.addEventListener( 'touchend', clearPickPosition );
+
 }
 }
+
 main();
 main();
 
 
 </script>
 </script>

+ 29 - 21
manual/examples/offscreencanvas.html

@@ -33,28 +33,36 @@
     </div>
     </div>
   </body>
   </body>
 <script type="module">
 <script type="module">
-function main() {  /* eslint consistent-return: 0 */
-  const canvas = document.querySelector('#c');
-  if (!canvas.transferControlToOffscreen) {
-    canvas.style.display = 'none';
-    document.querySelector('#noOffscreenCanvas').style.display = '';
-    return;
-  }
-  const offscreen = canvas.transferControlToOffscreen();
-  const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
-  worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
-
-  function sendSize() {
-    worker.postMessage({
-      type: 'size',
-      width: canvas.clientWidth,
-      height: canvas.clientHeight,
-    });
-  }
-
-  window.addEventListener('resize', sendSize);
-  sendSize();
+function main() { /* eslint consistent-return: 0 */
+
+	const canvas = document.querySelector( '#c' );
+	if ( ! canvas.transferControlToOffscreen ) {
+
+		canvas.style.display = 'none';
+		document.querySelector( '#noOffscreenCanvas' ).style.display = '';
+		return;
+
+	}
+
+	const offscreen = canvas.transferControlToOffscreen();
+	const worker = new Worker( 'offscreencanvas-cubes.js', { type: 'module' } );
+	worker.postMessage( { type: 'main', canvas: offscreen }, [ offscreen ] );
+
+	function sendSize() {
+
+		worker.postMessage( {
+			type: 'size',
+			width: canvas.clientWidth,
+			height: canvas.clientHeight,
+		} );
+
+	}
+
+	window.addEventListener( 'resize', sendSize );
+	sendSize();
+
 }
 }
+
 main();
 main();
 
 
 </script>
 </script>

+ 242 - 198
manual/examples/picking-gpu.html

@@ -36,223 +36,267 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 200;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 30;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-  const pickingScene = new THREE.Scene();
-  pickingScene.background = new THREE.Color(0);
-
-  // put the camera on a pole (parent it to an object)
-  // so we can spin the pole to move the camera around the scene
-  const cameraPole = new THREE.Object3D();
-  scene.add(cameraPole);
-  cameraPole.add(camera);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    camera.add(light);
-  }
 
 
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function rand(min, max) {
-    if (max === undefined) {
-      max = min;
-      min = 0;
-    }
-    return min + (max - min) * Math.random();
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 200;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 30;
 
 
-  function randomColor() {
-    return `hsl(${rand(360) | 0}, ${rand(50, 100) | 0}%, 50%)`;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
+	const pickingScene = new THREE.Scene();
+	pickingScene.background = new THREE.Color( 0 );
 
 
-  const loader = new THREE.TextureLoader();
-  const texture = loader.load('resources/images/frame.png');
-
-  const idToObject = {};
-  const numObjects = 100;
-  for (let i = 0; i < numObjects; ++i) {
-    const id = i + 1;
-    const material = new THREE.MeshPhongMaterial({
-      color: randomColor(),
-      map: texture,
-      transparent: true,
-      side: THREE.DoubleSide,
-      alphaTest: 0.5,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-    idToObject[id] = cube;
-
-    cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
-    cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
-    cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
-
-    const pickingMaterial = new THREE.MeshPhongMaterial({
-      emissive: new THREE.Color(id),
-      color: new THREE.Color(0, 0, 0),
-      specular: new THREE.Color(0, 0, 0),
-      map: texture,
-      transparent: true,
-      side: THREE.DoubleSide,
-      alphaTest: 0.5,
-      blending: THREE.NoBlending,
-    });
-    const pickingCube = new THREE.Mesh(geometry, pickingMaterial);
-    pickingScene.add(pickingCube);
-    pickingCube.position.copy(cube.position);
-    pickingCube.rotation.copy(cube.rotation);
-    pickingCube.scale.copy(cube.scale);
-  }
+	// put the camera on a pole (parent it to an object)
+	// so we can spin the pole to move the camera around the scene
+	const cameraPole = new THREE.Object3D();
+	scene.add( cameraPole );
+	cameraPole.add( camera );
 
 
-  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;
-  }
+	{
 
 
-  class GPUPickHelper {
-    constructor() {
-      // create a 1x1 pixel render target
-      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
-      this.pixelBuffer = new Uint8Array(4);
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(cssPosition, scene, camera, time) {
-      const {pickingTexture, pixelBuffer} = this;
-
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      // set the view offset to represent just a single pixel under the mouse
-      const pixelRatio = renderer.getPixelRatio();
-      camera.setViewOffset(
-          renderer.getContext().drawingBufferWidth,   // full width
-          renderer.getContext().drawingBufferHeight,  // full top
-          cssPosition.x * pixelRatio | 0,             // rect x
-          cssPosition.y * pixelRatio | 0,             // rect y
-          1,                                          // rect width
-          1,                                          // rect height
-      );
-      // render the scene
-      renderer.setRenderTarget(pickingTexture);
-      renderer.render(scene, camera);
-      renderer.setRenderTarget(null);
-      // clear the view offset so rendering returns to normal
-      camera.clearViewOffset();
-      //read the pixel
-      renderer.readRenderTargetPixels(
-          pickingTexture,
-          0,   // x
-          0,   // y
-          1,   // width
-          1,   // height
-          pixelBuffer);
-
-      const id =
-          (pixelBuffer[0] << 16) |
-          (pixelBuffer[1] <<  8) |
-          (pixelBuffer[2]      );
-
-      const intersectedObject = idToObject[id];
-      if (intersectedObject) {
-        // pick the first object. It's the closest one
-        this.pickedObject = intersectedObject;
-        // save its color
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        // set its emissive color to flashing red/yellow
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		camera.add( light );
 
 
-  const pickPosition = {x: 0, y: 0};
-  const pickHelper = new GPUPickHelper();
-  clearPickPosition();
+	}
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds;
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	function rand( min, max ) {
 
 
-    cameraPole.rotation.y = time * .1;
+		if ( max === undefined ) {
 
 
-    pickHelper.pick(pickPosition, pickingScene, camera, time);
+			max = min;
+			min = 0;
 
 
-    renderer.render(scene, camera);
+		}
 
 
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+		return min + ( max - min ) * Math.random();
 
 
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    pickPosition.x = pos.x;
-    pickPosition.y = pos.y;
-  }
+	}
 
 
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    pickPosition.x = -100000;
-    pickPosition.y = -100000;
-  }
+	function randomColor() {
+
+		return `hsl(${rand( 360 ) | 0}, ${rand( 50, 100 ) | 0}%, 50%)`;
+
+	}
+
+	const loader = new THREE.TextureLoader();
+	const texture = loader.load( 'resources/images/frame.png' );
+
+	const idToObject = {};
+	const numObjects = 100;
+	for ( let i = 0; i < numObjects; ++ i ) {
+
+		const id = i + 1;
+		const material = new THREE.MeshPhongMaterial( {
+			color: randomColor(),
+			map: texture,
+			transparent: true,
+			side: THREE.DoubleSide,
+			alphaTest: 0.5,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+		idToObject[ id ] = cube;
+
+		cube.position.set( rand( - 20, 20 ), rand( - 20, 20 ), rand( - 20, 20 ) );
+		cube.rotation.set( rand( Math.PI ), rand( Math.PI ), 0 );
+		cube.scale.set( rand( 3, 6 ), rand( 3, 6 ), rand( 3, 6 ) );
+
+		const pickingMaterial = new THREE.MeshPhongMaterial( {
+			emissive: new THREE.Color( id ),
+			color: new THREE.Color( 0, 0, 0 ),
+			specular: new THREE.Color( 0, 0, 0 ),
+			map: texture,
+			transparent: true,
+			side: THREE.DoubleSide,
+			alphaTest: 0.5,
+			blending: THREE.NoBlending,
+		} );
+		const pickingCube = new THREE.Mesh( geometry, pickingMaterial );
+		pickingScene.add( pickingCube );
+		pickingCube.position.copy( cube.position );
+		pickingCube.rotation.copy( cube.rotation );
+		pickingCube.scale.copy( cube.scale );
+
+	}
+
+	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;
+
+	}
+
+	class GPUPickHelper {
+
+		constructor() {
+
+			// create a 1x1 pixel render target
+			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
+			this.pixelBuffer = new Uint8Array( 4 );
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( cssPosition, scene, camera, time ) {
+
+			const { pickingTexture, pixelBuffer } = this;
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			// set the view offset to represent just a single pixel under the mouse
+			const pixelRatio = renderer.getPixelRatio();
+			camera.setViewOffset(
+				renderer.getContext().drawingBufferWidth, // full width
+				renderer.getContext().drawingBufferHeight, // full top
+				cssPosition.x * pixelRatio | 0, // rect x
+				cssPosition.y * pixelRatio | 0, // rect y
+				1, // rect width
+				1, // rect height
+			);
+			// render the scene
+			renderer.setRenderTarget( pickingTexture );
+			renderer.render( scene, camera );
+			renderer.setRenderTarget( null );
+			// clear the view offset so rendering returns to normal
+			camera.clearViewOffset();
+			//read the pixel
+			renderer.readRenderTargetPixels(
+				pickingTexture,
+				0, // x
+				0, // y
+				1, // width
+				1, // height
+				pixelBuffer );
+
+			const id =
+          ( pixelBuffer[ 0 ] << 16 ) |
+          ( pixelBuffer[ 1 ] << 8 ) |
+          ( pixelBuffer[ 2 ] );
+
+			const intersectedObject = idToObject[ id ];
+			if ( intersectedObject ) {
+
+				// pick the first object. It's the closest one
+				this.pickedObject = intersectedObject;
+				// save its color
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				// set its emissive color to flashing red/yellow
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickPosition = { x: 0, y: 0 };
+	const pickHelper = new GPUPickHelper();
+	clearPickPosition();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cameraPole.rotation.y = time * .1;
+
+		pickHelper.pick( pickPosition, pickingScene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		pickPosition.x = pos.x;
+		pickPosition.y = pos.y;
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		pickPosition.x = - 100000;
+		pickPosition.y = - 100000;
+
+	}
+
+	window.addEventListener( 'mousemove', setPickPosition );
+	window.addEventListener( 'mouseout', clearPickPosition );
+	window.addEventListener( 'mouseleave', clearPickPosition );
+
+	window.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	window.addEventListener( 'touchmove', ( event ) => {
 
 
-  window.addEventListener('mousemove', setPickPosition);
-  window.addEventListener('mouseout', clearPickPosition);
-  window.addEventListener('mouseleave', clearPickPosition);
+		setPickPosition( event.touches[ 0 ] );
 
 
-  window.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
+	} );
 
 
-  window.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
+	window.addEventListener( 'touchend', clearPickPosition );
 
 
-  window.addEventListener('touchend', clearPickPosition);
 }
 }
 
 
 main();
 main();

+ 171 - 127
manual/examples/picking-raycaster-complex-geo.html

@@ -36,154 +36,198 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 200;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 30;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  // put the camera on a pole (parent it to an object)
-  // so we can spin the pole to move the camera around the scene
-  const cameraPole = new THREE.Object3D();
-  scene.add(cameraPole);
-  cameraPole.add(camera);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    camera.add(light);
-  }
 
 
-  const geometry = new THREE.SphereGeometry(.6, 100, 100);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function rand(min, max) {
-    if (max === undefined) {
-      max = min;
-      min = 0;
-    }
-    return min + (max - min) * Math.random();
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 200;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 30;
 
 
-  function randomColor() {
-    return `hsl(${rand(360) | 0}, ${rand(50, 100) | 0}%, 50%)`;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-  const numObjects = 100;
-  for (let i = 0; i < numObjects; ++i) {
-    const material = new THREE.MeshPhongMaterial({color: randomColor()});
+	// put the camera on a pole (parent it to an object)
+	// so we can spin the pole to move the camera around the scene
+	const cameraPole = new THREE.Object3D();
+	scene.add( cameraPole );
+	cameraPole.add( camera );
 
 
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
+	{
 
 
-    cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
-    cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
-    cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
-  }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		camera.add( light );
 
 
-  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;
-  }
+	}
 
 
-  class PickHelper {
-    constructor() {
-      this.raycaster = new THREE.Raycaster();
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(normalizedPosition, scene, camera, time) {
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      this.raycaster.setFromCamera(normalizedPosition, camera);
-      const intersectedObjects = this.raycaster.intersectObjects(scene.children);
-      if (intersectedObjects.length) {
-        this.pickedObject = intersectedObjects[0].object;
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
+	const geometry = new THREE.SphereGeometry( .6, 100, 100 );
 
 
-  const pickHelper = new PickHelper();
-  const pickPosition = {x: -1, y: -1};
-  clearPickPosition();
+	function rand( min, max ) {
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds;
+		if ( max === undefined ) {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			max = min;
+			min = 0;
 
 
-    cameraPole.rotation.y = time * .1;
+		}
 
 
-    pickHelper.pick(pickPosition, scene, camera, time);
+		return min + ( max - min ) * Math.random();
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+	function randomColor() {
 
 
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    pickPosition.x = pos.x;
-    pickPosition.y = pos.y;
-  }
+		return `hsl(${rand( 360 ) | 0}, ${rand( 50, 100 ) | 0}%, 50%)`;
 
 
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    pickPosition.x = -100000;
-    pickPosition.y = -100000;
-  }
+	}
+
+	const numObjects = 100;
+	for ( let i = 0; i < numObjects; ++ i ) {
+
+		const material = new THREE.MeshPhongMaterial( { color: randomColor() } );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.set( rand( - 20, 20 ), rand( - 20, 20 ), rand( - 20, 20 ) );
+		cube.rotation.set( rand( Math.PI ), rand( Math.PI ), 0 );
+		cube.scale.set( rand( 3, 6 ), rand( 3, 6 ), rand( 3, 6 ) );
+
+	}
+
+	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;
+
+	}
+
+	class PickHelper {
+
+		constructor() {
+
+			this.raycaster = new THREE.Raycaster();
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( normalizedPosition, scene, camera, time ) {
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			this.raycaster.setFromCamera( normalizedPosition, camera );
+			const intersectedObjects = this.raycaster.intersectObjects( scene.children );
+			if ( intersectedObjects.length ) {
+
+				this.pickedObject = intersectedObjects[ 0 ].object;
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickHelper = new PickHelper();
+	const pickPosition = { x: - 1, y: - 1 };
+	clearPickPosition();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cameraPole.rotation.y = time * .1;
+
+		pickHelper.pick( pickPosition, scene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		pickPosition.x = pos.x;
+		pickPosition.y = pos.y;
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		pickPosition.x = - 100000;
+		pickPosition.y = - 100000;
+
+	}
+
+	window.addEventListener( 'mousemove', setPickPosition );
+	window.addEventListener( 'mouseout', clearPickPosition );
+	window.addEventListener( 'mouseleave', clearPickPosition );
+
+	window.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	window.addEventListener( 'touchmove', ( event ) => {
 
 
-  window.addEventListener('mousemove', setPickPosition);
-  window.addEventListener('mouseout', clearPickPosition);
-  window.addEventListener('mouseleave', clearPickPosition);
+		setPickPosition( event.touches[ 0 ] );
 
 
-  window.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
+	} );
 
 
-  window.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
+	window.addEventListener( 'touchend', clearPickPosition );
 
 
-  window.addEventListener('touchend', clearPickPosition);
 }
 }
 
 
 main();
 main();

+ 190 - 146
manual/examples/picking-raycaster-transparency.html

@@ -36,171 +36,215 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 200;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 30;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  // put the camera on a pole (parent it to an object)
-  // so we can spin the pole to move the camera around the scene
-  const cameraPole = new THREE.Object3D();
-  scene.add(cameraPole);
-  cameraPole.add(camera);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    camera.add(light);
-  }
 
 
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function rand(min, max) {
-    if (max === undefined) {
-      max = min;
-      min = 0;
-    }
-    return min + (max - min) * Math.random();
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 200;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 30;
 
 
-  function randomColor() {
-    return `hsl(${rand(360) | 0}, ${rand(50, 100) | 0}%, 50%)`;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-  const loader = new THREE.TextureLoader();
-  const texture = loader.load('resources/images/frame.png');
-
-  const numObjects = 100;
-  for (let i = 0; i < numObjects; ++i) {
-    const material = new THREE.MeshPhongMaterial({
-      color: randomColor(),
-      map: texture,
-      transparent: true,
-      side: THREE.DoubleSide,
-      alphaTest: 0.1,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
-    cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
-    cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
-  }
+	// put the camera on a pole (parent it to an object)
+	// so we can spin the pole to move the camera around the scene
+	const cameraPole = new THREE.Object3D();
+	scene.add( cameraPole );
+	cameraPole.add( camera );
 
 
-  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;
-  }
+	{
 
 
-  class PickHelper {
-    constructor() {
-      this.raycaster = new THREE.Raycaster();
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(normalizedPosition, scene, camera, time) {
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      // cast a ray through the frustum
-      this.raycaster.setFromCamera(normalizedPosition, camera);
-      // get the list of objects the ray intersected
-      const intersectedObjects = this.raycaster.intersectObjects(scene.children);
-      if (intersectedObjects.length) {
-        // pick the first object. It's the closest one
-        this.pickedObject = intersectedObjects[0].object;
-        // save its color
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        // set its emissive color to flashing red/yellow
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		camera.add( light );
 
 
-  const pickPosition = {x: 0, y: 0};
-  const pickHelper = new PickHelper();
-  clearPickPosition();
+	}
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds;
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+	function rand( min, max ) {
 
 
-    cameraPole.rotation.y = time * .1;
+		if ( max === undefined ) {
 
 
-    pickHelper.pick(pickPosition, scene, camera, time);
+			max = min;
+			min = 0;
 
 
-    renderer.render(scene, camera);
+		}
 
 
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+		return min + ( max - min ) * Math.random();
 
 
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    pickPosition.x = (pos.x / canvas.width ) *  2 - 1;
-    pickPosition.y = (pos.y / canvas.height) * -2 + 1;  // note we flip Y
-  }
+	}
 
 
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    pickPosition.x = -100000;
-    pickPosition.y = -100000;
-  }
+	function randomColor() {
+
+		return `hsl(${rand( 360 ) | 0}, ${rand( 50, 100 ) | 0}%, 50%)`;
+
+	}
+
+	const loader = new THREE.TextureLoader();
+	const texture = loader.load( 'resources/images/frame.png' );
+
+	const numObjects = 100;
+	for ( let i = 0; i < numObjects; ++ i ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color: randomColor(),
+			map: texture,
+			transparent: true,
+			side: THREE.DoubleSide,
+			alphaTest: 0.1,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.set( rand( - 20, 20 ), rand( - 20, 20 ), rand( - 20, 20 ) );
+		cube.rotation.set( rand( Math.PI ), rand( Math.PI ), 0 );
+		cube.scale.set( rand( 3, 6 ), rand( 3, 6 ), rand( 3, 6 ) );
+
+	}
+
+	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;
+
+	}
+
+	class PickHelper {
+
+		constructor() {
+
+			this.raycaster = new THREE.Raycaster();
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( normalizedPosition, scene, camera, time ) {
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			// cast a ray through the frustum
+			this.raycaster.setFromCamera( normalizedPosition, camera );
+			// get the list of objects the ray intersected
+			const intersectedObjects = this.raycaster.intersectObjects( scene.children );
+			if ( intersectedObjects.length ) {
+
+				// pick the first object. It's the closest one
+				this.pickedObject = intersectedObjects[ 0 ].object;
+				// save its color
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				// set its emissive color to flashing red/yellow
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickPosition = { x: 0, y: 0 };
+	const pickHelper = new PickHelper();
+	clearPickPosition();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cameraPole.rotation.y = time * .1;
+
+		pickHelper.pick( pickPosition, scene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		pickPosition.x = ( pos.x / canvas.width ) * 2 - 1;
+		pickPosition.y = ( pos.y / canvas.height ) * - 2 + 1; // note we flip Y
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		pickPosition.x = - 100000;
+		pickPosition.y = - 100000;
+
+	}
+
+	window.addEventListener( 'mousemove', setPickPosition );
+	window.addEventListener( 'mouseout', clearPickPosition );
+	window.addEventListener( 'mouseleave', clearPickPosition );
+
+	window.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	window.addEventListener( 'touchmove', ( event ) => {
 
 
-  window.addEventListener('mousemove', setPickPosition);
-  window.addEventListener('mouseout', clearPickPosition);
-  window.addEventListener('mouseleave', clearPickPosition);
+		setPickPosition( event.touches[ 0 ] );
 
 
-  window.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
+	} );
 
 
-  window.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
+	window.addEventListener( 'touchend', clearPickPosition );
 
 
-  window.addEventListener('touchend', clearPickPosition);
 }
 }
 
 
 main();
 main();

+ 182 - 137
manual/examples/picking-raycaster.html

@@ -36,163 +36,208 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 60;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 200;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 30;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color('white');
-
-  // put the camera on a pole (parent it to an object)
-  // so we can spin the pole to move the camera around the scene
-  const cameraPole = new THREE.Object3D();
-  scene.add(cameraPole);
-  cameraPole.add(camera);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    camera.add(light);
-  }
 
 
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function rand(min, max) {
-    if (max === undefined) {
-      max = min;
-      min = 0;
-    }
-    return min + (max - min) * Math.random();
-  }
+	const fov = 60;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 200;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 30;
 
 
-  function randomColor() {
-    return `hsl(${rand(360) | 0}, ${rand(50, 100) | 0}%, 50%)`;
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 'white' );
 
 
-  const numObjects = 100;
-  for (let i = 0; i < numObjects; ++i) {
-    const material = new THREE.MeshPhongMaterial({
-      color: randomColor(),
-    });
+	// put the camera on a pole (parent it to an object)
+	// so we can spin the pole to move the camera around the scene
+	const cameraPole = new THREE.Object3D();
+	scene.add( cameraPole );
+	cameraPole.add( camera );
 
 
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
+	{
 
 
-    cube.position.set(rand(-20, 20), rand(-20, 20), rand(-20, 20));
-    cube.rotation.set(rand(Math.PI), rand(Math.PI), 0);
-    cube.scale.set(rand(3, 6), rand(3, 6), rand(3, 6));
-  }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		camera.add( light );
 
 
-  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;
-  }
+	}
 
 
-  class PickHelper {
-    constructor() {
-      this.raycaster = new THREE.Raycaster();
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(normalizedPosition, scene, camera, time) {
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      // cast a ray through the frustum
-      this.raycaster.setFromCamera(normalizedPosition, camera);
-      // get the list of objects the ray intersected
-      const intersectedObjects = this.raycaster.intersectObjects(scene.children);
-      if (intersectedObjects.length) {
-        // pick the first object. It's the closest one
-        this.pickedObject = intersectedObjects[0].object;
-        // save its color
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        // set its emissive color to flashing red/yellow
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
 
 
-  const pickPosition = {x: 0, y: 0};
-  const pickHelper = new PickHelper();
-  clearPickPosition();
+	function rand( min, max ) {
 
 
-  function render(time) {
-    time *= 0.001;  // convert to seconds;
+		if ( max === undefined ) {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+			max = min;
+			min = 0;
 
 
-    cameraPole.rotation.y = time * .1;
+		}
 
 
-    pickHelper.pick(pickPosition, scene, camera, time);
+		return min + ( max - min ) * Math.random();
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
-  requestAnimationFrame(render);
-
-  function getCanvasRelativePosition(event) {
-    const rect = canvas.getBoundingClientRect();
-    return {
-      x: (event.clientX - rect.left) * canvas.width  / rect.width,
-      y: (event.clientY - rect.top ) * canvas.height / rect.height,
-    };
-  }
+	function randomColor() {
 
 
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    pickPosition.x = (pos.x / canvas.width ) *  2 - 1;
-    pickPosition.y = (pos.y / canvas.height) * -2 + 1;  // note we flip Y
-  }
+		return `hsl(${rand( 360 ) | 0}, ${rand( 50, 100 ) | 0}%, 50%)`;
 
 
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    pickPosition.x = -100000;
-    pickPosition.y = -100000;
-  }
-  window.addEventListener('mousemove', setPickPosition);
-  window.addEventListener('mouseout', clearPickPosition);
-  window.addEventListener('mouseleave', clearPickPosition);
+	}
+
+	const numObjects = 100;
+	for ( let i = 0; i < numObjects; ++ i ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color: randomColor(),
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.set( rand( - 20, 20 ), rand( - 20, 20 ), rand( - 20, 20 ) );
+		cube.rotation.set( rand( Math.PI ), rand( Math.PI ), 0 );
+		cube.scale.set( rand( 3, 6 ), rand( 3, 6 ), rand( 3, 6 ) );
+
+	}
+
+	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;
+
+	}
+
+	class PickHelper {
+
+		constructor() {
+
+			this.raycaster = new THREE.Raycaster();
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( normalizedPosition, scene, camera, time ) {
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			// cast a ray through the frustum
+			this.raycaster.setFromCamera( normalizedPosition, camera );
+			// get the list of objects the ray intersected
+			const intersectedObjects = this.raycaster.intersectObjects( scene.children );
+			if ( intersectedObjects.length ) {
+
+				// pick the first object. It's the closest one
+				this.pickedObject = intersectedObjects[ 0 ].object;
+				// save its color
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				// set its emissive color to flashing red/yellow
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickPosition = { x: 0, y: 0 };
+	const pickHelper = new PickHelper();
+	clearPickPosition();
+
+	function render( time ) {
+
+		time *= 0.001; // convert to seconds;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cameraPole.rotation.y = time * .1;
+
+		pickHelper.pick( pickPosition, scene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = canvas.getBoundingClientRect();
+		return {
+			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
+			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		pickPosition.x = ( pos.x / canvas.width ) * 2 - 1;
+		pickPosition.y = ( pos.y / canvas.height ) * - 2 + 1; // note we flip Y
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		pickPosition.x = - 100000;
+		pickPosition.y = - 100000;
+
+	}
+
+	window.addEventListener( 'mousemove', setPickPosition );
+	window.addEventListener( 'mouseout', clearPickPosition );
+	window.addEventListener( 'mouseleave', clearPickPosition );
+
+	window.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	window.addEventListener( 'touchmove', ( event ) => {
+
+		setPickPosition( event.touches[ 0 ] );
 
 
-  window.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
+	} );
 
 
-  window.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
+	window.addEventListener( 'touchend', clearPickPosition );
 
 
-  window.addEventListener('touchend', clearPickPosition);
 }
 }
 
 
 main();
 main();

+ 245 - 216
manual/examples/postprocessing-3dlut-identity.html

@@ -34,166 +34,185 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
-import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const makeIdentityLutTexture = function() {
-    const identityLUT = new Uint8Array([
-        0,   0,   0, 255,  // black
-      255,   0,   0, 255,  // red
-        0,   0, 255, 255,  // blue
-      255,   0, 255, 255,  // magenta
-        0, 255,   0, 255,  // green
-      255, 255,   0, 255,  // yellow
-        0, 255, 255, 255,  // cyan
-      255, 255, 255, 255,  // white
-    ]);
-
-    return function(filter) {
-      const texture = new THREE.DataTexture(identityLUT, 4, 2);
-      texture.minFilter = filter;
-      texture.magFilter = filter;
-      texture.needsUpdate = true;
-      texture.flipY = false;
-      return texture;
-    };
-  }();
-
-  const lutTextures = [
-    {
-      name: 'identity',
-      size: 2,
-      filter: true,
-      texture: makeIdentityLutTexture(THREE.LinearFilter),
-    },
-    {
-      name: 'identity not filtered',
-      size: 2,
-      filter: false,
-      texture: makeIdentityLutTexture(THREE.NearestFilter),
-    },
-  ];
-
-  const lutNameIndexMap = {};
-  lutTextures.forEach((info, ndx) => {
-    lutNameIndexMap[info.name] = ndx;
-  });
-
-  const lutSettings = {
-    lut: lutNameIndexMap.identity,
-  };
-  const gui = new GUI({ width: 300 });
-  gui.add(lutSettings, 'lut', lutNameIndexMap);
-
-  const scene = new THREE.Scene();
-
-  const sceneBG = new THREE.Scene();
-  const cameraBG = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
-
-  let bgMesh;
-  let bgTexture;
-  {
-    const loader = new THREE.TextureLoader();
-    bgTexture = loader.load('resources/images/beach.jpg');
-    bgTexture.colorSpace = THREE.SRGBColorSpace;
-    const planeGeo = new THREE.PlaneGeometry(2, 2);
-    const planeMat = new THREE.MeshBasicMaterial({
-      map: bgTexture,
-      depthTest: false,
-    });
-    bgMesh = new THREE.Mesh(planeGeo, planeMat);
-    sceneBG.add(bgMesh);
-  }
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const makeIdentityLutTexture = function () {
+
+		const identityLUT = new Uint8Array( [
+			0, 0, 0, 255, // black
+			255, 0, 0, 255, // red
+			0, 0, 255, 255, // blue
+			255, 0, 255, 255, // magenta
+			0, 255, 0, 255, // green
+			255, 255, 0, 255, // yellow
+			0, 255, 255, 255, // cyan
+			255, 255, 255, 255, // white
+		] );
+
+		return function ( filter ) {
+
+			const texture = new THREE.DataTexture( identityLUT, 4, 2 );
+			texture.minFilter = filter;
+			texture.magFilter = filter;
+			texture.needsUpdate = true;
+			texture.flipY = false;
+			return texture;
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/3dbustchallange_submission/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      // fix materials from r114
-      root.traverse(({material}) => {
-        if (material) {
-          material.depthWrite = true;
-        }
-      });
-
-      root.updateMatrixWorld();
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-      frameArea(boxSize * 0.4, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		};
+
+	}();
+
+	const lutTextures = [
+		{
+			name: 'identity',
+			size: 2,
+			filter: true,
+			texture: makeIdentityLutTexture( THREE.LinearFilter ),
+		},
+		{
+			name: 'identity not filtered',
+			size: 2,
+			filter: false,
+			texture: makeIdentityLutTexture( THREE.NearestFilter ),
+		},
+	];
+
+	const lutNameIndexMap = {};
+	lutTextures.forEach( ( info, ndx ) => {
+
+		lutNameIndexMap[ info.name ] = ndx;
+
+	} );
+
+	const lutSettings = {
+		lut: lutNameIndexMap.identity,
+	};
+	const gui = new GUI( { width: 300 } );
+	gui.add( lutSettings, 'lut', lutNameIndexMap );
+
+	const scene = new THREE.Scene();
+
+	const sceneBG = new THREE.Scene();
+	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );
+
+	let bgMesh;
+	let bgTexture;
+	{
+
+		const loader = new THREE.TextureLoader();
+		bgTexture = loader.load( 'resources/images/beach.jpg' );
+		bgTexture.colorSpace = THREE.SRGBColorSpace;
+		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
+		const planeMat = new THREE.MeshBasicMaterial( {
+			map: bgTexture,
+			depthTest: false,
+		} );
+		bgMesh = new THREE.Mesh( planeGeo, planeMat );
+		sceneBG.add( bgMesh );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/3dbustchallange_submission/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			// fix materials from r114
+			root.traverse( ( { material } ) => {
+
+				if ( material ) {
+
+					material.depthWrite = true;
+
+				}
+
+			} );
+
+			root.updateMatrixWorld();
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+			frameArea( boxSize * 0.4, boxSize, boxCenter, camera );
 
 
-  const lutShader = {
-    uniforms: {
-      tDiffuse: { value: null },  // the previous pass's result
-      lutMap:  { value: null },
-      lutMapSize: { value: 1, },
-    },
-    vertexShader: `
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	const lutShader = {
+		uniforms: {
+			tDiffuse: { value: null }, // the previous pass's result
+			lutMap: { value: null },
+			lutMapSize: { value: 1, },
+		},
+		vertexShader: `
       varying vec2 vUv;
       varying vec2 vUv;
       void main() {
       void main() {
         vUv = uv;
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
       }
       }
     `,
     `,
-    fragmentShader: `
+		fragmentShader: `
       #include <common>
       #include <common>
 
 
       #define FILTER_LUT true
       #define FILTER_LUT true
@@ -235,80 +254,90 @@ function main() {
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
       }
       }
     `,
     `,
-  };
-
-  const lutNearestShader = {
-    uniforms: {...lutShader.uniforms},
-    vertexShader: lutShader.vertexShader,
-    fragmentShader: lutShader.fragmentShader.replace('#define FILTER_LUT', '//'),
-  };
-
-  const effectLUT = new ShaderPass(lutShader);
-  const effectLUTNearest = new ShaderPass(lutNearestShader);
-
-  const renderModel = new RenderPass(scene, camera);
-  renderModel.clear = false;  // so we don't clear out the background
-  const renderBG = new RenderPass(sceneBG, cameraBG);
-  const outputPass = new OutputPass();
-
-  const composer = new EffectComposer(renderer);
-
-  composer.addPass(renderBG);
-  composer.addPass(renderModel);
-  composer.addPass(effectLUT);
-  composer.addPass(effectLUTNearest);
-  composer.addPass(outputPass);
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = canvas.clientWidth * window.devicePixelRatio | 0;
-    const height = canvas.clientHeight * window.devicePixelRatio | 0;
-
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
+	};
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const delta = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      const canvasAspect = canvas.clientWidth / canvas.clientHeight;
-      camera.aspect = canvasAspect;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-
-      // scale the background plane to keep the image's
-      // aspect correct.
-      // Note the image may not have loaded yet.
-      const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
-      const aspect = imageAspect / canvasAspect;
-      bgMesh.scale.x = aspect > 1 ? aspect : 1;
-      bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
-    }
+	const lutNearestShader = {
+		uniforms: { ...lutShader.uniforms },
+		vertexShader: lutShader.vertexShader,
+		fragmentShader: lutShader.fragmentShader.replace( '#define FILTER_LUT', '//' ),
+	};
 
 
-    const lutInfo = lutTextures[lutSettings.lut];
+	const effectLUT = new ShaderPass( lutShader );
+	const effectLUTNearest = new ShaderPass( lutNearestShader );
 
 
-    const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
-    effectLUT.enabled = lutInfo.filter;
-    effectLUTNearest.enabled = !lutInfo.filter;
+	const renderModel = new RenderPass( scene, camera );
+	renderModel.clear = false; // so we don't clear out the background
+	const renderBG = new RenderPass( sceneBG, cameraBG );
+	const outputPass = new OutputPass();
 
 
-    const lutTexture = lutInfo.texture;
-    effect.uniforms.lutMap.value = lutTexture;
-    effect.uniforms.lutMapSize.value = lutInfo.size;
+	const composer = new EffectComposer( renderer );
 
 
-    composer.render(delta);
+	composer.addPass( renderBG );
+	composer.addPass( renderModel );
+	composer.addPass( effectLUT );
+	composer.addPass( effectLUTNearest );
+	composer.addPass( outputPass );
 
 
-    requestAnimationFrame(render);
-  }
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth * window.devicePixelRatio | 0;
+		const height = canvas.clientHeight * window.devicePixelRatio | 0;
+
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const delta = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			const canvasAspect = canvas.clientWidth / canvas.clientHeight;
+			camera.aspect = canvasAspect;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+			// scale the background plane to keep the image's
+			// aspect correct.
+			// Note the image may not have loaded yet.
+			const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
+			const aspect = imageAspect / canvasAspect;
+			bgMesh.scale.x = aspect > 1 ? aspect : 1;
+			bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
+
+		}
+
+		const lutInfo = lutTextures[ lutSettings.lut ];
+
+		const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
+		effectLUT.enabled = lutInfo.filter;
+		effectLUTNearest.enabled = ! lutInfo.filter;
+
+		const lutTexture = lutInfo.texture;
+		effect.uniforms.lutMap.value = lutTexture;
+		effect.uniforms.lutMapSize.value = lutInfo.size;
+
+		composer.render( delta );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 144 - 121
manual/examples/postprocessing-3dlut-prep.html

@@ -35,136 +35,159 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-  renderer.outputColorSpace = THREE.SRGBColorSpace;
-  renderer.autoClearColor = false;
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-
-  const sceneBG = new THREE.Scene();
-  const cameraBG = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
-
-  let bgMesh;
-  let bgTexture;
-  {
-    const loader = new THREE.TextureLoader();
-    bgTexture = loader.load('resources/images/beach.jpg');
-    bgTexture.colorSpace = THREE.SRGBColorSpace;
-    const planeGeo = new THREE.PlaneGeometry(2, 2);
-    const planeMat = new THREE.MeshBasicMaterial({
-      map: bgTexture,
-      depthTest: false,
-    });
-    bgMesh = new THREE.Mesh(planeGeo, planeMat);
-    sceneBG.add(bgMesh);
-  }
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.outputColorSpace = THREE.SRGBColorSpace;
+	renderer.autoClearColor = false;
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/3dbustchallange_submission/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      // fix materials from r114
-      root.traverse(({material}) => {
-        if (material) {
-          material.depthWrite = true;
-        }
-      });
-
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-
-      // set the camera to frame the box
-      frameArea(boxSize * 0.4, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
 
 
-  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;
-  }
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
 
 
-  function render() {
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      const canvasAspect = canvas.clientWidth / canvas.clientHeight;
-      camera.aspect = canvasAspect;
-      camera.updateProjectionMatrix();
-
-      // scale the background plane to keep the image's
-      // aspect correct.
-      // Note the image may not have loaded yet.
-      const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
-      const aspect = imageAspect / canvasAspect;
-      bgMesh.scale.x = aspect > 1 ? aspect : 1;
-      bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
-    }
+	const scene = new THREE.Scene();
 
 
-    renderer.render(sceneBG, cameraBG);
-    renderer.render(scene, camera);
+	const sceneBG = new THREE.Scene();
+	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );
 
 
-    requestAnimationFrame(render);
-  }
+	let bgMesh;
+	let bgTexture;
+	{
+
+		const loader = new THREE.TextureLoader();
+		bgTexture = loader.load( 'resources/images/beach.jpg' );
+		bgTexture.colorSpace = THREE.SRGBColorSpace;
+		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
+		const planeMat = new THREE.MeshBasicMaterial( {
+			map: bgTexture,
+			depthTest: false,
+		} );
+		bgMesh = new THREE.Mesh( planeGeo, planeMat );
+		sceneBG.add( bgMesh );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/3dbustchallange_submission/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			// fix materials from r114
+			root.traverse( ( { material } ) => {
+
+				if ( material ) {
+
+					material.depthWrite = true;
+
+				}
+
+			} );
+
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+
+			// set the camera to frame the box
+			frameArea( boxSize * 0.4, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	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;
+			const canvasAspect = canvas.clientWidth / canvas.clientHeight;
+			camera.aspect = canvasAspect;
+			camera.updateProjectionMatrix();
+
+			// scale the background plane to keep the image's
+			// aspect correct.
+			// Note the image may not have loaded yet.
+			const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
+			const aspect = imageAspect / canvasAspect;
+			bgMesh.scale.x = aspect > 1 ? aspect : 1;
+			bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
+
+		}
+
+		renderer.render( sceneBG, cameraBG );
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 363 - 313
manual/examples/postprocessing-3dlut-w-loader.html

@@ -32,232 +32,267 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as lutParser from './resources/lut-reader.js';
 import * as lutParser from './resources/lut-reader.js';
 import * as dragAndDrop from './resources/drag-and-drop.js';
 import * as dragAndDrop from './resources/drag-and-drop.js';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
-import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const lutTextures = [
-    { name: 'identity',           size: 2, filter: true , },
-    { name: 'identity no filter', size: 2, filter: false, },
-    { name: 'custom',          url: 'resources/images/lut/3dlut-red-only-s16.png' },
-    { name: 'monochrome',      url: 'resources/images/lut/monochrome-s8.png' },
-    { name: 'sepia',           url: 'resources/images/lut/sepia-s8.png' },
-    { name: 'saturated',       url: 'resources/images/lut/saturated-s8.png', },
-    { name: 'posterize',       url: 'resources/images/lut/posterize-s8n.png', },
-    { name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
-    { name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
-    { name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
-    { name: 'posterize-more',  url: 'resources/images/lut/posterize-more-s8n.png', },
-    { name: 'inverse',         url: 'resources/images/lut/inverse-s8.png', },
-    { name: 'color negative',  url: 'resources/images/lut/color-negative-s8.png', },
-    { name: 'high contrast',   url: 'resources/images/lut/high-contrast-bw-s8.png', },
-    { name: 'funky contrast',  url: 'resources/images/lut/funky-contrast-s8.png', },
-    { name: 'nightvision',     url: 'resources/images/lut/nightvision-s8.png', },
-    { name: 'thermal',         url: 'resources/images/lut/thermal-s8.png', },
-    { name: 'b/w',             url: 'resources/images/lut/black-white-s8n.png', },
-    { name: 'hue +60',         url: 'resources/images/lut/hue-plus-60-s8.png', },
-    { name: 'hue +180',        url: 'resources/images/lut/hue-plus-180-s8.png', },
-    { name: 'hue -60',         url: 'resources/images/lut/hue-minus-60-s8.png', },
-    { name: 'red to cyan',     url: 'resources/images/lut/red-to-cyan-s8.png' },
-    { name: 'blues',           url: 'resources/images/lut/blues-s8.png' },
-    { name: 'infrared',        url: 'resources/images/lut/infrared-s8.png' },
-    { name: 'radioactive',     url: 'resources/images/lut/radioactive-s8.png' },
-    { name: 'goolgey',         url: 'resources/images/lut/googley-s8.png' },
-    { name: 'bgy',             url: 'resources/images/lut/bgy-s8.png' },
-  ];
-
-  const makeIdentityLutTexture = function() {
-    const identityLUT = new Uint8Array([
-        0,   0,   0, 255,  // black
-      255,   0,   0, 255,  // red
-        0,   0, 255, 255,  // blue
-      255,   0, 255, 255,  // magenta
-        0, 255,   0, 255,  // green
-      255, 255,   0, 255,  // yellow
-        0, 255, 255, 255,  // cyan
-      255, 255, 255, 255,  // white
-    ]);
-
-    return function(filter) {
-      const texture = new THREE.DataTexture(identityLUT, 4, 2);
-      texture.minFilter = texture.magFilter = filter ? THREE.LinearFilter : THREE.NearestFilter;
-      texture.needsUpdate = true;
-      texture.flipY = false;
-      return texture;
-    };
-  }();
-
-  const makeLUTTexture = function() {
-    const imgLoader = new THREE.ImageLoader();
-    const ctx = document.createElement('canvas').getContext('2d');
-
-    return function(info) {
-      const lutSize = info.size;
-      const width = lutSize * lutSize;
-      const height = lutSize;
-      const texture = new THREE.DataTexture(new Uint8Array(width * height), width, height);
-      texture.minFilter = texture.magFilter = info.filter ? THREE.LinearFilter : THREE.NearestFilter;
-      texture.flipY = false;
-
-      if (info.url) {
-
-        imgLoader.load(info.url, function(image) {
-          ctx.canvas.width = width;
-          ctx.canvas.height = height;
-          ctx.drawImage(image, 0, 0);
-          const imageData = ctx.getImageData(0, 0, width, height);
-
-          texture.image.data = new Uint8Array(imageData.data.buffer);
-          texture.image.width = width;
-          texture.image.height = height;
-          texture.needsUpdate = true;
-        });
-      }
 
 
-      return texture;
-    };
-  }();
-
-  lutTextures.forEach((info) => {
-    // if not size set get it from the filename
-    if (!info.size) {
-      // assumes filename ends in '-s<num>[n]'
-      // where <num> is the size of the 3DLUT cube
-      // and [n] means 'no filtering' or 'nearest'
-      //
-      // examples:
-      //    'foo-s16.png' = size:16, filter: true
-      //    'bar-s8n.png' = size:8, filter: false
-      const m = /-s(\d+)(n*)\.[^.]+$/.exec(info.url);
-      if (m) {
-        info.size = parseInt(m[1]);
-        info.filter = info.filter === undefined ? m[2] !== 'n' : info.filter;
-      }
-      info.texture = makeLUTTexture(info);
-    } else {
-      info.texture = makeIdentityLutTexture(info.filter);
-    }
-  });
-
-  const lutNameIndexMap = {};
-  lutTextures.forEach((info, ndx) => {
-    lutNameIndexMap[info.name] = ndx;
-  });
-
-  const lutSettings = {
-    lut: lutNameIndexMap.custom,
-  };
-  const gui = new GUI({ width: 300 });
-  gui.add(lutSettings, 'lut', lutNameIndexMap);
-
-  const scene = new THREE.Scene();
-
-  const sceneBG = new THREE.Scene();
-  const cameraBG = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
-
-  let bgMesh;
-  let bgTexture;
-  {
-    const loader = new THREE.TextureLoader();
-    bgTexture = loader.load('resources/images/beach.jpg');
-    bgTexture.colorSpace = THREE.SRGBColorSpace;
-    const planeGeo = new THREE.PlaneGeometry(2, 2);
-    const planeMat = new THREE.MeshBasicMaterial({
-      map: bgTexture,
-      depthTest: false,
-    });
-    bgMesh = new THREE.Mesh(planeGeo, planeMat);
-    sceneBG.add(bgMesh);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const lutTextures = [
+		{ name: 'identity', size: 2, filter: true, },
+		{ name: 'identity no filter', size: 2, filter: false, },
+		{ name: 'custom', url: 'resources/images/lut/3dlut-red-only-s16.png' },
+		{ name: 'monochrome', url: 'resources/images/lut/monochrome-s8.png' },
+		{ name: 'sepia', url: 'resources/images/lut/sepia-s8.png' },
+		{ name: 'saturated', url: 'resources/images/lut/saturated-s8.png', },
+		{ name: 'posterize', url: 'resources/images/lut/posterize-s8n.png', },
+		{ name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
+		{ name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
+		{ name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
+		{ name: 'posterize-more', url: 'resources/images/lut/posterize-more-s8n.png', },
+		{ name: 'inverse', url: 'resources/images/lut/inverse-s8.png', },
+		{ name: 'color negative', url: 'resources/images/lut/color-negative-s8.png', },
+		{ name: 'high contrast', url: 'resources/images/lut/high-contrast-bw-s8.png', },
+		{ name: 'funky contrast', url: 'resources/images/lut/funky-contrast-s8.png', },
+		{ name: 'nightvision', url: 'resources/images/lut/nightvision-s8.png', },
+		{ name: 'thermal', url: 'resources/images/lut/thermal-s8.png', },
+		{ name: 'b/w', url: 'resources/images/lut/black-white-s8n.png', },
+		{ name: 'hue +60', url: 'resources/images/lut/hue-plus-60-s8.png', },
+		{ name: 'hue +180', url: 'resources/images/lut/hue-plus-180-s8.png', },
+		{ name: 'hue -60', url: 'resources/images/lut/hue-minus-60-s8.png', },
+		{ name: 'red to cyan', url: 'resources/images/lut/red-to-cyan-s8.png' },
+		{ name: 'blues', url: 'resources/images/lut/blues-s8.png' },
+		{ name: 'infrared', url: 'resources/images/lut/infrared-s8.png' },
+		{ name: 'radioactive', url: 'resources/images/lut/radioactive-s8.png' },
+		{ name: 'goolgey', url: 'resources/images/lut/googley-s8.png' },
+		{ name: 'bgy', url: 'resources/images/lut/bgy-s8.png' },
+	];
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const makeIdentityLutTexture = function () {
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/3dbustchallange_submission/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      // fix materials from r114
-      root.traverse(({material}) => {
-        if (material) {
-          material.depthWrite = true;
-        }
-      });
-
-      root.updateMatrixWorld();
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-      frameArea(boxSize * 0.4, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		const identityLUT = new Uint8Array( [
+			0, 0, 0, 255, // black
+			255, 0, 0, 255, // red
+			0, 0, 255, 255, // blue
+			255, 0, 255, 255, // magenta
+			0, 255, 0, 255, // green
+			255, 255, 0, 255, // yellow
+			0, 255, 255, 255, // cyan
+			255, 255, 255, 255, // white
+		] );
+
+		return function ( filter ) {
+
+			const texture = new THREE.DataTexture( identityLUT, 4, 2 );
+			texture.minFilter = texture.magFilter = filter ? THREE.LinearFilter : THREE.NearestFilter;
+			texture.needsUpdate = true;
+			texture.flipY = false;
+			return texture;
+
+		};
+
+	}();
+
+	const makeLUTTexture = function () {
+
+		const imgLoader = new THREE.ImageLoader();
+		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+
+		return function ( info ) {
+
+			const lutSize = info.size;
+			const width = lutSize * lutSize;
+			const height = lutSize;
+			const texture = new THREE.DataTexture( new Uint8Array( width * height ), width, height );
+			texture.minFilter = texture.magFilter = info.filter ? THREE.LinearFilter : THREE.NearestFilter;
+			texture.flipY = false;
+
+			if ( info.url ) {
+
+				imgLoader.load( info.url, function ( image ) {
+
+					ctx.canvas.width = width;
+					ctx.canvas.height = height;
+					ctx.drawImage( image, 0, 0 );
+					const imageData = ctx.getImageData( 0, 0, width, height );
+
+					texture.image.data = new Uint8Array( imageData.data.buffer );
+					texture.image.width = width;
+					texture.image.height = height;
+					texture.needsUpdate = true;
+
+				} );
+
+			}
+
+			return texture;
+
+		};
+
+	}();
+
+	lutTextures.forEach( ( info ) => {
+
+		// if not size set get it from the filename
+		if ( ! info.size ) {
+
+			// assumes filename ends in '-s<num>[n]'
+			// where <num> is the size of the 3DLUT cube
+			// and [n] means 'no filtering' or 'nearest'
+			//
+			// examples:
+			//    'foo-s16.png' = size:16, filter: true
+			//    'bar-s8n.png' = size:8, filter: false
+			const m = /-s(\d+)(n*)\.[^.]+$/.exec( info.url );
+			if ( m ) {
+
+				info.size = parseInt( m[ 1 ] );
+				info.filter = info.filter === undefined ? m[ 2 ] !== 'n' : info.filter;
+
+			}
+
+			info.texture = makeLUTTexture( info );
+
+		} else {
+
+			info.texture = makeIdentityLutTexture( info.filter );
+
+		}
+
+	} );
+
+	const lutNameIndexMap = {};
+	lutTextures.forEach( ( info, ndx ) => {
+
+		lutNameIndexMap[ info.name ] = ndx;
+
+	} );
+
+	const lutSettings = {
+		lut: lutNameIndexMap.custom,
+	};
+	const gui = new GUI( { width: 300 } );
+	gui.add( lutSettings, 'lut', lutNameIndexMap );
+
+	const scene = new THREE.Scene();
+
+	const sceneBG = new THREE.Scene();
+	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );
+
+	let bgMesh;
+	let bgTexture;
+	{
+
+		const loader = new THREE.TextureLoader();
+		bgTexture = loader.load( 'resources/images/beach.jpg' );
+		bgTexture.colorSpace = THREE.SRGBColorSpace;
+		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
+		const planeMat = new THREE.MeshBasicMaterial( {
+			map: bgTexture,
+			depthTest: false,
+		} );
+		bgMesh = new THREE.Mesh( planeGeo, planeMat );
+		sceneBG.add( bgMesh );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
+
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/3dbustchallange_submission/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
 
 
-  const lutShader = {
-    uniforms: {
-      tDiffuse: { value: null },
-      lutMap:  { value: null },
-      lutMapSize: { value: 1, },
-    },
-    vertexShader: `
+			// fix materials from r114
+			root.traverse( ( { material } ) => {
+
+				if ( material ) {
+
+					material.depthWrite = true;
+
+				}
+
+			} );
+
+			root.updateMatrixWorld();
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+			frameArea( boxSize * 0.4, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	const lutShader = {
+		uniforms: {
+			tDiffuse: { value: null },
+			lutMap: { value: null },
+			lutMapSize: { value: 1, },
+		},
+		vertexShader: `
       varying vec2 vUv;
       varying vec2 vUv;
       void main() {
       void main() {
         vUv = uv;
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
       }
       }
     `,
     `,
-    fragmentShader: `
+		fragmentShader: `
       #include <common>
       #include <common>
 
 
       #define FILTER_LUT true
       #define FILTER_LUT true
@@ -299,115 +334,130 @@ function main() {
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
       }
       }
     `,
     `,
-  };
-
-  const lutNearestShader = {
-    uniforms: {...lutShader.uniforms},
-    vertexShader: lutShader.vertexShader,
-    fragmentShader: lutShader.fragmentShader.replace('#define FILTER_LUT', '//'),
-  };
-
-  const effectLUT = new ShaderPass(lutShader);
-  const effectLUTNearest = new ShaderPass(lutNearestShader);
-
-  const renderModel = new RenderPass(scene, camera);
-  renderModel.clear = false;  // so we don't clear out the background
-  const renderBG = new RenderPass(sceneBG, cameraBG);
-  const outputPass = new OutputPass();
-
-  const composer = new EffectComposer(renderer);
-
-  composer.addPass(renderBG);
-  composer.addPass(renderModel);
-  composer.addPass(effectLUT);
-  composer.addPass(effectLUTNearest);
-  composer.addPass(outputPass);
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = canvas.clientWidth * window.devicePixelRatio | 0;
-    const height = canvas.clientHeight * window.devicePixelRatio | 0;
-
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
+	};
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const delta = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      const canvasAspect = canvas.clientWidth / canvas.clientHeight;
-      camera.aspect = canvasAspect;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-
-      // scale the background plane to keep the image's
-      // aspect correct.
-      // Note the image may not have loaded yet.
-      const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
-      const aspect = imageAspect / canvasAspect;
-      bgMesh.scale.x = aspect > 1 ? aspect : 1;
-      bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
-    }
+	const lutNearestShader = {
+		uniforms: { ...lutShader.uniforms },
+		vertexShader: lutShader.vertexShader,
+		fragmentShader: lutShader.fragmentShader.replace( '#define FILTER_LUT', '//' ),
+	};
 
 
-    const lutInfo = lutTextures[ lutSettings.lut ];
+	const effectLUT = new ShaderPass( lutShader );
+	const effectLUTNearest = new ShaderPass( lutNearestShader );
 
 
-    const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
-    effectLUT.enabled = lutInfo.filter;
-    effectLUTNearest.enabled = !lutInfo.filter;
+	const renderModel = new RenderPass( scene, camera );
+	renderModel.clear = false; // so we don't clear out the background
+	const renderBG = new RenderPass( sceneBG, cameraBG );
+	const outputPass = new OutputPass();
 
 
-    const lutTexture = lutInfo.texture;
-    effect.uniforms.lutMap.value = lutTexture;
-    effect.uniforms.lutMapSize.value = lutInfo.size;
+	const composer = new EffectComposer( renderer );
 
 
-    composer.render(delta);
+	composer.addPass( renderBG );
+	composer.addPass( renderModel );
+	composer.addPass( effectLUT );
+	composer.addPass( effectLUTNearest );
+	composer.addPass( outputPass );
 
 
-    requestAnimationFrame(render);
-  }
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-  requestAnimationFrame(render);
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth * window.devicePixelRatio | 0;
+		const height = canvas.clientHeight * window.devicePixelRatio | 0;
 
 
-  dragAndDrop.setup({msg: 'Drop LUT File here'});
-  dragAndDrop.onDropFile(readLUTFile);
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
 
 
-  function ext(s) {
-    const period = s.lastIndexOf('.');
-    return s.slice(period + 1);
-  }
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const delta = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			const canvasAspect = canvas.clientWidth / canvas.clientHeight;
+			camera.aspect = canvasAspect;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+			// scale the background plane to keep the image's
+			// aspect correct.
+			// Note the image may not have loaded yet.
+			const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
+			const aspect = imageAspect / canvasAspect;
+			bgMesh.scale.x = aspect > 1 ? aspect : 1;
+			bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
+
+		}
+
+		const lutInfo = lutTextures[ lutSettings.lut ];
+
+		const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
+		effectLUT.enabled = lutInfo.filter;
+		effectLUTNearest.enabled = ! lutInfo.filter;
+
+		const lutTexture = lutInfo.texture;
+		effect.uniforms.lutMap.value = lutTexture;
+		effect.uniforms.lutMapSize.value = lutInfo.size;
+
+		composer.render( delta );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	dragAndDrop.setup( { msg: 'Drop LUT File here' } );
+	dragAndDrop.onDropFile( readLUTFile );
+
+	function ext( s ) {
+
+		const period = s.lastIndexOf( '.' );
+		return s.slice( period + 1 );
+
+	}
+
+	function readLUTFile( file ) {
+
+		const reader = new FileReader();
+		reader.onload = ( e ) => {
+
+			const type = ext( file.name );
+			const lut = lutParser.lutTo2D3Drgba8( lutParser.parse( e.target.result, type ) );
+			const { size, data, name } = lut;
+			const texture = new THREE.DataTexture( data, size * size, size );
+			texture.magFilter = THREE.LinearFilter;
+			texture.minFilter = THREE.LinearFilter;
+			texture.needsUpdate = true;
+			texture.flipY = false;
+			const lutTexture = {
+				name: ( name && name.toLowerCase().trim() !== 'untitled' )
+					? name
+					: file.name,
+				size: size,
+				filter: true,
+				texture,
+			};
+			lutTextures.push( lutTexture );
+			lutSettings.lut = lutTextures.length - 1;
+
+		};
+
+		reader.readAsText( file );
+
+	}
 
 
-  function readLUTFile(file) {
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      const type = ext(file.name);
-      const lut = lutParser.lutTo2D3Drgba8(lutParser.parse(e.target.result, type));
-      const {size, data, name} = lut;
-      const texture = new THREE.DataTexture(data, size * size, size);
-      texture.magFilter = THREE.LinearFilter;
-      texture.minFilter = THREE.LinearFilter;
-      texture.needsUpdate = true;
-      texture.flipY = false;
-      const lutTexture = {
-        name: (name && name.toLowerCase().trim() !== 'untitled')
-          ? name
-          : file.name,
-        size: size,
-        filter: true,
-        texture,
-      };
-      lutTextures.push(lutTexture);
-      lutSettings.lut = lutTextures.length - 1;
-
-    };
-
-    reader.readAsText(file);
-  }
 }
 }
 
 
 main();
 main();

+ 326 - 281
manual/examples/postprocessing-3dlut.html

@@ -34,232 +34,267 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
-import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
-import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 45;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.set(0, 10, 20);
-
-  const controls = new OrbitControls(camera, canvas);
-  controls.target.set(0, 5, 0);
-  controls.update();
-
-  const lutTextures = [
-    { name: 'identity',           size: 2, filter: true , },
-    { name: 'identity no filter', size: 2, filter: false, },
-    { name: 'custom',          url: 'resources/images/lut/3dlut-red-only-s16.png' },
-    { name: 'monochrome',      url: 'resources/images/lut/monochrome-s8.png' },
-    { name: 'sepia',           url: 'resources/images/lut/sepia-s8.png' },
-    { name: 'saturated',       url: 'resources/images/lut/saturated-s8.png', },
-    { name: 'posterize',       url: 'resources/images/lut/posterize-s8n.png', },
-    { name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
-    { name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
-    { name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
-    { name: 'posterize-more',  url: 'resources/images/lut/posterize-more-s8n.png', },
-    { name: 'inverse',         url: 'resources/images/lut/inverse-s8.png', },
-    { name: 'color negative',  url: 'resources/images/lut/color-negative-s8.png', },
-    { name: 'high contrast',   url: 'resources/images/lut/high-contrast-bw-s8.png', },
-    { name: 'funky contrast',  url: 'resources/images/lut/funky-contrast-s8.png', },
-    { name: 'nightvision',     url: 'resources/images/lut/nightvision-s8.png', },
-    { name: 'thermal',         url: 'resources/images/lut/thermal-s8.png', },
-    { name: 'b/w',             url: 'resources/images/lut/black-white-s8n.png', },
-    { name: 'hue +60',         url: 'resources/images/lut/hue-plus-60-s8.png', },
-    { name: 'hue +180',        url: 'resources/images/lut/hue-plus-180-s8.png', },
-    { name: 'hue -60',         url: 'resources/images/lut/hue-minus-60-s8.png', },
-    { name: 'red to cyan',     url: 'resources/images/lut/red-to-cyan-s8.png' },
-    { name: 'blues',           url: 'resources/images/lut/blues-s8.png' },
-    { name: 'infrared',        url: 'resources/images/lut/infrared-s8.png' },
-    { name: 'radioactive',     url: 'resources/images/lut/radioactive-s8.png' },
-    { name: 'goolgey',         url: 'resources/images/lut/googley-s8.png' },
-    { name: 'bgy',             url: 'resources/images/lut/bgy-s8.png' },
-  ];
-
-  const makeIdentityLutTexture = function() {
-    const identityLUT = new Uint8Array([
-        0,   0,   0, 255,  // black
-      255,   0,   0, 255,  // red
-        0,   0, 255, 255,  // blue
-      255,   0, 255, 255,  // magenta
-        0, 255,   0, 255,  // green
-      255, 255,   0, 255,  // yellow
-        0, 255, 255, 255,  // cyan
-      255, 255, 255, 255,  // white
-    ]);
-
-    return function(filter) {
-      const texture = new THREE.DataTexture(identityLUT, 4, 2);
-      texture.minFilter = texture.magFilter = filter ? THREE.LinearFilter : THREE.NearestFilter;
-      texture.needsUpdate = true;
-      texture.flipY = false;
-      return texture;
-    };
-  }();
-
-  const makeLUTTexture = function() {
-    const imgLoader = new THREE.ImageLoader();
-    const ctx = document.createElement('canvas').getContext('2d');
-
-    return function(info) {
-      const lutSize = info.size;
-      const width = lutSize * lutSize;
-      const height = lutSize;
-      const texture = new THREE.DataTexture(new Uint8Array(width * height), width, height);
-      texture.minFilter = texture.magFilter = info.filter ? THREE.LinearFilter : THREE.NearestFilter;
-      texture.flipY = false;
-
-      if (info.url) {
-
-        imgLoader.load(info.url, function(image) {
-          ctx.canvas.width = width;
-          ctx.canvas.height = height;
-          ctx.drawImage(image, 0, 0);
-          const imageData = ctx.getImageData(0, 0, width, height);
-
-          texture.image.data = new Uint8Array(imageData.data.buffer);
-          texture.image.width = width;
-          texture.image.height = height;
-          texture.needsUpdate = true;
-        });
-      }
 
 
-      return texture;
-    };
-  }();
-
-  lutTextures.forEach((info) => {
-    // if not size set get it from the filename
-    if (!info.size) {
-      // assumes filename ends in '-s<num>[n]'
-      // where <num> is the size of the 3DLUT cube
-      // and [n] means 'no filtering' or 'nearest'
-      //
-      // examples:
-      //    'foo-s16.png' = size:16, filter: true
-      //    'bar-s8n.png' = size:8, filter: false
-      const m = /-s(\d+)(n*)\.[^.]+$/.exec(info.url);
-      if (m) {
-        info.size = parseInt(m[1]);
-        info.filter = info.filter === undefined ? m[2] !== 'n' : info.filter;
-      }
-      info.texture = makeLUTTexture(info);
-    } else {
-      info.texture = makeIdentityLutTexture(info.filter);
-    }
-  });
-
-  const lutNameIndexMap = {};
-  lutTextures.forEach((info, ndx) => {
-    lutNameIndexMap[info.name] = ndx;
-  });
-
-  const lutSettings = {
-    lut: lutNameIndexMap.custom,
-  };
-  const gui = new GUI({ width: 300 });
-  gui.add(lutSettings, 'lut', lutNameIndexMap);
-
-  const scene = new THREE.Scene();
-
-  const sceneBG = new THREE.Scene();
-  const cameraBG = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
-
-  let bgMesh;
-  let bgTexture;
-  {
-    const loader = new THREE.TextureLoader();
-    bgTexture = loader.load('resources/images/beach.jpg');
-    bgTexture.colorSpace = THREE.SRGBColorSpace;
-    const planeGeo = new THREE.PlaneGeometry(2, 2);
-    const planeMat = new THREE.MeshBasicMaterial({
-      map: bgTexture,
-      depthTest: false,
-    });
-    bgMesh = new THREE.Mesh(planeGeo, planeMat);
-    sceneBG.add(bgMesh);
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+
+	const fov = 45;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.set( 0, 10, 20 );
+
+	const controls = new OrbitControls( camera, canvas );
+	controls.target.set( 0, 5, 0 );
+	controls.update();
+
+	const lutTextures = [
+		{ name: 'identity', size: 2, filter: true, },
+		{ name: 'identity no filter', size: 2, filter: false, },
+		{ name: 'custom', url: 'resources/images/lut/3dlut-red-only-s16.png' },
+		{ name: 'monochrome', url: 'resources/images/lut/monochrome-s8.png' },
+		{ name: 'sepia', url: 'resources/images/lut/sepia-s8.png' },
+		{ name: 'saturated', url: 'resources/images/lut/saturated-s8.png', },
+		{ name: 'posterize', url: 'resources/images/lut/posterize-s8n.png', },
+		{ name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
+		{ name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
+		{ name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
+		{ name: 'posterize-more', url: 'resources/images/lut/posterize-more-s8n.png', },
+		{ name: 'inverse', url: 'resources/images/lut/inverse-s8.png', },
+		{ name: 'color negative', url: 'resources/images/lut/color-negative-s8.png', },
+		{ name: 'high contrast', url: 'resources/images/lut/high-contrast-bw-s8.png', },
+		{ name: 'funky contrast', url: 'resources/images/lut/funky-contrast-s8.png', },
+		{ name: 'nightvision', url: 'resources/images/lut/nightvision-s8.png', },
+		{ name: 'thermal', url: 'resources/images/lut/thermal-s8.png', },
+		{ name: 'b/w', url: 'resources/images/lut/black-white-s8n.png', },
+		{ name: 'hue +60', url: 'resources/images/lut/hue-plus-60-s8.png', },
+		{ name: 'hue +180', url: 'resources/images/lut/hue-plus-180-s8.png', },
+		{ name: 'hue -60', url: 'resources/images/lut/hue-minus-60-s8.png', },
+		{ name: 'red to cyan', url: 'resources/images/lut/red-to-cyan-s8.png' },
+		{ name: 'blues', url: 'resources/images/lut/blues-s8.png' },
+		{ name: 'infrared', url: 'resources/images/lut/infrared-s8.png' },
+		{ name: 'radioactive', url: 'resources/images/lut/radioactive-s8.png' },
+		{ name: 'goolgey', url: 'resources/images/lut/googley-s8.png' },
+		{ name: 'bgy', url: 'resources/images/lut/bgy-s8.png' },
+	];
 
 
-  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
-    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
-    const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
-    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
-    // compute a unit vector that points in the direction the camera is now
-    // in the xz plane from the center of the box
-    const direction = (new THREE.Vector3())
-        .subVectors(camera.position, boxCenter)
-        .multiply(new THREE.Vector3(1, 0, 1))
-        .normalize();
-
-    // move the camera to a position distance units way from the center
-    // in whatever direction the camera was from the center already
-    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
-
-    // pick some near and far values for the frustum that
-    // will contain the box.
-    camera.near = boxSize / 100;
-    camera.far = boxSize * 100;
-
-    camera.updateProjectionMatrix();
-
-    // point the camera to look at the center of the box
-    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
-  }
+	const makeIdentityLutTexture = function () {
 
 
-  {
-    const gltfLoader = new GLTFLoader();
-    gltfLoader.load('resources/models/3dbustchallange_submission/scene.gltf', (gltf) => {
-      const root = gltf.scene;
-      scene.add(root);
-
-      // fix materials from r114
-      root.traverse(({material}) => {
-        if (material) {
-          material.depthWrite = true;
-        }
-      });
-
-      root.updateMatrixWorld();
-      // compute the box that contains all the stuff
-      // from root and below
-      const box = new THREE.Box3().setFromObject(root);
-
-      const boxSize = box.getSize(new THREE.Vector3()).length();
-      const boxCenter = box.getCenter(new THREE.Vector3());
-      frameArea(boxSize * 0.4, boxSize, boxCenter, camera);
-
-      // update the Trackball controls to handle the new size
-      controls.maxDistance = boxSize * 10;
-      controls.target.copy(boxCenter);
-      controls.update();
-    });
-  }
+		const identityLUT = new Uint8Array( [
+			0, 0, 0, 255, // black
+			255, 0, 0, 255, // red
+			0, 0, 255, 255, // blue
+			255, 0, 255, 255, // magenta
+			0, 255, 0, 255, // green
+			255, 255, 0, 255, // yellow
+			0, 255, 255, 255, // cyan
+			255, 255, 255, 255, // white
+		] );
+
+		return function ( filter ) {
+
+			const texture = new THREE.DataTexture( identityLUT, 4, 2 );
+			texture.minFilter = texture.magFilter = filter ? THREE.LinearFilter : THREE.NearestFilter;
+			texture.needsUpdate = true;
+			texture.flipY = false;
+			return texture;
+
+		};
+
+	}();
+
+	const makeLUTTexture = function () {
+
+		const imgLoader = new THREE.ImageLoader();
+		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+
+		return function ( info ) {
+
+			const lutSize = info.size;
+			const width = lutSize * lutSize;
+			const height = lutSize;
+			const texture = new THREE.DataTexture( new Uint8Array( width * height ), width, height );
+			texture.minFilter = texture.magFilter = info.filter ? THREE.LinearFilter : THREE.NearestFilter;
+			texture.flipY = false;
+
+			if ( info.url ) {
+
+				imgLoader.load( info.url, function ( image ) {
+
+					ctx.canvas.width = width;
+					ctx.canvas.height = height;
+					ctx.drawImage( image, 0, 0 );
+					const imageData = ctx.getImageData( 0, 0, width, height );
+
+					texture.image.data = new Uint8Array( imageData.data.buffer );
+					texture.image.width = width;
+					texture.image.height = height;
+					texture.needsUpdate = true;
+
+				} );
+
+			}
+
+			return texture;
+
+		};
+
+	}();
+
+	lutTextures.forEach( ( info ) => {
+
+		// if not size set get it from the filename
+		if ( ! info.size ) {
+
+			// assumes filename ends in '-s<num>[n]'
+			// where <num> is the size of the 3DLUT cube
+			// and [n] means 'no filtering' or 'nearest'
+			//
+			// examples:
+			//    'foo-s16.png' = size:16, filter: true
+			//    'bar-s8n.png' = size:8, filter: false
+			const m = /-s(\d+)(n*)\.[^.]+$/.exec( info.url );
+			if ( m ) {
+
+				info.size = parseInt( m[ 1 ] );
+				info.filter = info.filter === undefined ? m[ 2 ] !== 'n' : info.filter;
+
+			}
+
+			info.texture = makeLUTTexture( info );
+
+		} else {
+
+			info.texture = makeIdentityLutTexture( info.filter );
+
+		}
+
+	} );
+
+	const lutNameIndexMap = {};
+	lutTextures.forEach( ( info, ndx ) => {
+
+		lutNameIndexMap[ info.name ] = ndx;
+
+	} );
+
+	const lutSettings = {
+		lut: lutNameIndexMap.custom,
+	};
+	const gui = new GUI( { width: 300 } );
+	gui.add( lutSettings, 'lut', lutNameIndexMap );
+
+	const scene = new THREE.Scene();
+
+	const sceneBG = new THREE.Scene();
+	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );
+
+	let bgMesh;
+	let bgTexture;
+	{
+
+		const loader = new THREE.TextureLoader();
+		bgTexture = loader.load( 'resources/images/beach.jpg' );
+		bgTexture.colorSpace = THREE.SRGBColorSpace;
+		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
+		const planeMat = new THREE.MeshBasicMaterial( {
+			map: bgTexture,
+			depthTest: false,
+		} );
+		bgMesh = new THREE.Mesh( planeGeo, planeMat );
+		sceneBG.add( bgMesh );
+
+	}
+
+	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {
+
+		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
+		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
+		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
+		// compute a unit vector that points in the direction the camera is now
+		// in the xz plane from the center of the box
+		const direction = ( new THREE.Vector3() )
+			.subVectors( camera.position, boxCenter )
+			.multiply( new THREE.Vector3( 1, 0, 1 ) )
+			.normalize();
+
+		// move the camera to a position distance units way from the center
+		// in whatever direction the camera was from the center already
+		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );
+
+		// pick some near and far values for the frustum that
+		// will contain the box.
+		camera.near = boxSize / 100;
+		camera.far = boxSize * 100;
+
+		camera.updateProjectionMatrix();
+
+		// point the camera to look at the center of the box
+		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );
 
 
-  const lutShader = {
-    uniforms: {
-      tDiffuse: { value: null },
-      lutMap:  { value: null },
-      lutMapSize: { value: 1, },
-    },
-    vertexShader: `
+	}
+
+	{
+
+		const gltfLoader = new GLTFLoader();
+		gltfLoader.load( 'resources/models/3dbustchallange_submission/scene.gltf', ( gltf ) => {
+
+			const root = gltf.scene;
+			scene.add( root );
+
+			// fix materials from r114
+			root.traverse( ( { material } ) => {
+
+				if ( material ) {
+
+					material.depthWrite = true;
+
+				}
+
+			} );
+
+			root.updateMatrixWorld();
+			// compute the box that contains all the stuff
+			// from root and below
+			const box = new THREE.Box3().setFromObject( root );
+
+			const boxSize = box.getSize( new THREE.Vector3() ).length();
+			const boxCenter = box.getCenter( new THREE.Vector3() );
+			frameArea( boxSize * 0.4, boxSize, boxCenter, camera );
+
+			// update the Trackball controls to handle the new size
+			controls.maxDistance = boxSize * 10;
+			controls.target.copy( boxCenter );
+			controls.update();
+
+		} );
+
+	}
+
+	const lutShader = {
+		uniforms: {
+			tDiffuse: { value: null },
+			lutMap: { value: null },
+			lutMapSize: { value: 1, },
+		},
+		vertexShader: `
       varying vec2 vUv;
       varying vec2 vUv;
       void main() {
       void main() {
         vUv = uv;
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
       }
       }
     `,
     `,
-    fragmentShader: `
+		fragmentShader: `
       #include <common>
       #include <common>
 
 
       #define FILTER_LUT true
       #define FILTER_LUT true
@@ -301,80 +336,90 @@ function main() {
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
       }
       }
     `,
     `,
-  };
-
-  const lutNearestShader = {
-    uniforms: {...lutShader.uniforms},
-    vertexShader: lutShader.vertexShader,
-    fragmentShader: lutShader.fragmentShader.replace('#define FILTER_LUT', '//'),
-  };
-
-  const effectLUT = new ShaderPass(lutShader);
-  const effectLUTNearest = new ShaderPass(lutNearestShader);
-
-  const renderModel = new RenderPass(scene, camera);
-  renderModel.clear = false;  // so we don't clear out the background
-  const renderBG = new RenderPass(sceneBG, cameraBG);
-  const outputPass = new OutputPass();
-
-  const composer = new EffectComposer(renderer);
-
-  composer.addPass(renderBG);
-  composer.addPass(renderModel);
-  composer.addPass(effectLUT);
-  composer.addPass(effectLUTNearest);
-  composer.addPass(outputPass);
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = canvas.clientWidth * window.devicePixelRatio | 0;
-    const height = canvas.clientHeight * window.devicePixelRatio | 0;
-
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
+	};
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const delta = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      const canvasAspect = canvas.clientWidth / canvas.clientHeight;
-      camera.aspect = canvasAspect;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-
-      // scale the background plane to keep the image's
-      // aspect correct.
-      // Note the image may not have loaded yet.
-      const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
-      const aspect = imageAspect / canvasAspect;
-      bgMesh.scale.x = aspect > 1 ? aspect : 1;
-      bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
-    }
+	const lutNearestShader = {
+		uniforms: { ...lutShader.uniforms },
+		vertexShader: lutShader.vertexShader,
+		fragmentShader: lutShader.fragmentShader.replace( '#define FILTER_LUT', '//' ),
+	};
 
 
-    const lutInfo = lutTextures[ lutSettings.lut ];
+	const effectLUT = new ShaderPass( lutShader );
+	const effectLUTNearest = new ShaderPass( lutNearestShader );
 
 
-    const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
-    effectLUT.enabled = lutInfo.filter;
-    effectLUTNearest.enabled = !lutInfo.filter;
+	const renderModel = new RenderPass( scene, camera );
+	renderModel.clear = false; // so we don't clear out the background
+	const renderBG = new RenderPass( sceneBG, cameraBG );
+	const outputPass = new OutputPass();
 
 
-    const lutTexture = lutInfo.texture;
-    effect.uniforms.lutMap.value = lutTexture;
-    effect.uniforms.lutMapSize.value = lutInfo.size;
+	const composer = new EffectComposer( renderer );
 
 
-    composer.render(delta);
+	composer.addPass( renderBG );
+	composer.addPass( renderModel );
+	composer.addPass( effectLUT );
+	composer.addPass( effectLUTNearest );
+	composer.addPass( outputPass );
 
 
-    requestAnimationFrame(render);
-  }
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth * window.devicePixelRatio | 0;
+		const height = canvas.clientHeight * window.devicePixelRatio | 0;
+
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const delta = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			const canvasAspect = canvas.clientWidth / canvas.clientHeight;
+			camera.aspect = canvasAspect;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+			// scale the background plane to keep the image's
+			// aspect correct.
+			// Note the image may not have loaded yet.
+			const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
+			const aspect = imageAspect / canvasAspect;
+			bgMesh.scale.x = aspect > 1 ? aspect : 1;
+			bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;
+
+		}
+
+		const lutInfo = lutTextures[ lutSettings.lut ];
+
+		const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
+		effectLUT.enabled = lutInfo.filter;
+		effectLUTNearest.enabled = ! lutInfo.filter;
+
+		const lutTexture = lutInfo.texture;
+		effect.uniforms.lutMap.value = lutTexture;
+		effect.uniforms.lutMapSize.value = lutInfo.size;
+
+		composer.render( delta );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 168 - 135
manual/examples/postprocessing-adobe-lut-to-png-converter.html

@@ -43,85 +43,100 @@
 import * as THREE from 'three';
 import * as THREE from 'three';
 import * as lutParser from './resources/lut-reader.js';
 import * as lutParser from './resources/lut-reader.js';
 import * as dragAndDrop from './resources/drag-and-drop.js';
 import * as dragAndDrop from './resources/drag-and-drop.js';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const makeIdentityLutTexture = function() {
-    const identityLUT = new Uint8Array([
-        0,   0,   0, 255,  // black
-      255,   0,   0, 255,  // red
-        0,   0, 255, 255,  // blue
-      255,   0, 255, 255,  // magenta
-        0, 255,   0, 255,  // green
-      255, 255,   0, 255,  // yellow
-        0, 255, 255, 255,  // cyan
-      255, 255, 255, 255,  // white
-    ]);
-
-    return function(filter) {
-      const texture = new THREE.DataTexture(identityLUT, 4, 2);
-      texture.minFilter = filter;
-      texture.magFilter = filter;
-      texture.needsUpdate = true;
-      texture.flipY = false;
-      return texture;
-    };
-  }();
-
-  const sceneBG = new THREE.Scene();
-  const cameraBG = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
-
-  const ctx = document.createElement('canvas').getContext('2d');
-  function drawColorCubeImage(ctx, size) {
-    const canvas = ctx.canvas;
-    canvas.width = size * size;
-    canvas.height = size;
-
-    for (let zz = 0; zz < size; ++zz) {
-      for (let yy = 0; yy < size; ++yy) {
-        for (let xx = 0; xx < size; ++xx) {
-          const r = Math.floor(xx / (size - 1) * 255);
-          const g = Math.floor(yy / (size - 1) * 255);
-          const b = Math.floor(zz / (size - 1) * 255);
-          ctx.fillStyle = `rgb(${r},${g},${b})`;
-          ctx.fillRect(zz * size + xx, yy, 1, 1);
-        }
-      }
-    }
-  }
 
 
-  const idTexture = new THREE.CanvasTexture(ctx.canvas);
-  idTexture.magFilter = THREE.NearestFilter;
-  idTexture.minFilter = THREE.NearestFilter;
-
-  {
-    const planeGeo = new THREE.PlaneGeometry(2, 2);
-    const planeMat = new THREE.MeshBasicMaterial({
-      map: idTexture,
-      depthTest: false,
-    });
-    sceneBG.add(new THREE.Mesh(planeGeo, planeMat));
-  }
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+
+	const makeIdentityLutTexture = function () {
+
+		const identityLUT = new Uint8Array( [
+			0, 0, 0, 255, // black
+			255, 0, 0, 255, // red
+			0, 0, 255, 255, // blue
+			255, 0, 255, 255, // magenta
+			0, 255, 0, 255, // green
+			255, 255, 0, 255, // yellow
+			0, 255, 255, 255, // cyan
+			255, 255, 255, 255, // white
+		] );
+
+		return function ( filter ) {
+
+			const texture = new THREE.DataTexture( identityLUT, 4, 2 );
+			texture.minFilter = filter;
+			texture.magFilter = filter;
+			texture.needsUpdate = true;
+			texture.flipY = false;
+			return texture;
+
+		};
+
+	}();
+
+	const sceneBG = new THREE.Scene();
+	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );
+
+	const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+	function drawColorCubeImage( ctx, size ) {
+
+		const canvas = ctx.canvas;
+		canvas.width = size * size;
+		canvas.height = size;
+
+		for ( let zz = 0; zz < size; ++ zz ) {
+
+			for ( let yy = 0; yy < size; ++ yy ) {
+
+				for ( let xx = 0; xx < size; ++ xx ) {
+
+					const r = Math.floor( xx / ( size - 1 ) * 255 );
+					const g = Math.floor( yy / ( size - 1 ) * 255 );
+					const b = Math.floor( zz / ( size - 1 ) * 255 );
+					ctx.fillStyle = `rgb(${r},${g},${b})`;
+					ctx.fillRect( zz * size + xx, yy, 1, 1 );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	const idTexture = new THREE.CanvasTexture( ctx.canvas );
+	idTexture.magFilter = THREE.NearestFilter;
+	idTexture.minFilter = THREE.NearestFilter;
+
+	{
+
+		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
+		const planeMat = new THREE.MeshBasicMaterial( {
+			map: idTexture,
+			depthTest: false,
+		} );
+		sceneBG.add( new THREE.Mesh( planeGeo, planeMat ) );
 
 
-  const lutShader = {
-    uniforms: {
-      tDiffuse: { value: null },
-      lutMap:  { value: null },
-      lutMapSize: { value: 1, },
-    },
-    vertexShader: `
+	}
+
+	const lutShader = {
+		uniforms: {
+			tDiffuse: { value: null },
+			lutMap: { value: null },
+			lutMapSize: { value: 1, },
+		},
+		vertexShader: `
       varying vec2 vUv;
       varying vec2 vUv;
       void main() {
       void main() {
         vUv = uv;
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
         gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
       }
       }
     `,
     `,
-    fragmentShader: `
+		fragmentShader: `
       #include <common>
       #include <common>
 
 
       #define FILTER_LUT true
       #define FILTER_LUT true
@@ -163,89 +178,107 @@ function main() {
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
         gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
       }
       }
     `,
     `,
-  };
+	};
 
 
-  const effectLUT = new ShaderPass(lutShader);
-  effectLUT.renderToScreen = true;
+	const effectLUT = new ShaderPass( lutShader );
+	effectLUT.renderToScreen = true;
 
 
-  const renderBG = new RenderPass(sceneBG, cameraBG);
+	const renderBG = new RenderPass( sceneBG, cameraBG );
 
 
-  const rtParameters = {
-    minFilter: THREE.NearestFilter,
-    magFilter: THREE.NearestFilter
-  };
-  const composer = new EffectComposer(renderer, new THREE.WebGLRenderTarget(1, 1, rtParameters));
+	const rtParameters = {
+		minFilter: THREE.NearestFilter,
+		magFilter: THREE.NearestFilter
+	};
+	const composer = new EffectComposer( renderer, new THREE.WebGLRenderTarget( 1, 1, rtParameters ) );
 
 
-  composer.addPass(renderBG);
-  composer.addPass(effectLUT);
+	composer.addPass( renderBG );
+	composer.addPass( effectLUT );
 
 
-  let name = 'identity';
-  const lutTexture = makeIdentityLutTexture(THREE.LinearFilter);
-  effectLUT.uniforms.lutMap.value = lutTexture;
-  effectLUT.uniforms.lutMapSize.value = 2;
+	let name = 'identity';
+	const lutTexture = makeIdentityLutTexture( THREE.LinearFilter );
+	effectLUT.uniforms.lutMap.value = lutTexture;
+	effectLUT.uniforms.lutMapSize.value = 2;
 
 
-  const sizeElem = document.querySelector('#size');
-  sizeElem.addEventListener('change', render);
+	const sizeElem = document.querySelector( '#size' );
+	sizeElem.addEventListener( 'change', render );
 
 
-  function render() {
-    const size = parseInt(sizeElem.value);
-    renderer.setSize(size * size, size, false);
-    composer.setSize(size * size, size);
+	function render() {
 
 
-    drawColorCubeImage(ctx, size);
-    idTexture.needsUpdate = true;
+		const size = parseInt( sizeElem.value );
+		renderer.setSize( size * size, size, false );
+		composer.setSize( size * size, size );
 
 
-    composer.render(0);
-  }
-  render();
+		drawColorCubeImage( ctx, size );
+		idTexture.needsUpdate = true;
 
 
-  dragAndDrop.setup({msg: 'Drop LUT File here'});
-  dragAndDrop.onDropFile(readLUTFile);
+		composer.render( 0 );
 
 
-  function ext(s) {
-    const period = s.lastIndexOf('.');
-    return s.slice(period + 1);
-  }
+	}
 
 
-  function readLUTFile(file) {
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      const type = ext(file.name);
-      const lut = lutParser.lutTo2D3Drgba8(lutParser.parse(e.target.result, type));
+	render();
 
 
-      effectLUT.uniforms.lutMapSize.value = lut.size;
+	dragAndDrop.setup( { msg: 'Drop LUT File here' } );
+	dragAndDrop.onDropFile( readLUTFile );
 
 
-      lutTexture.image.data = lut.data;
-      lutTexture.image.width = lut.size * lut.size;
-      lutTexture.image.height = lut.size;
-      lutTexture.needsUpdate = true;
+	function ext( s ) {
 
 
-      render();
-      name = `${file.name || lut.name || 'untitled'}`;
-      document.querySelector('#result').textContent = `loaded: ${name}`;
-    };
+		const period = s.lastIndexOf( '.' );
+		return s.slice( period + 1 );
 
 
-    reader.readAsText(file);
-  }
+	}
+
+	function readLUTFile( file ) {
+
+		const reader = new FileReader();
+		reader.onload = ( e ) => {
+
+			const type = ext( file.name );
+			const lut = lutParser.lutTo2D3Drgba8( lutParser.parse( e.target.result, type ) );
+
+			effectLUT.uniforms.lutMapSize.value = lut.size;
+
+			lutTexture.image.data = lut.data;
+			lutTexture.image.width = lut.size * lut.size;
+			lutTexture.image.height = lut.size;
+			lutTexture.needsUpdate = true;
+
+			render();
+			name = `${file.name || lut.name || 'untitled'}`;
+			document.querySelector( '#result' ).textContent = `loaded: ${name}`;
+
+		};
+
+		reader.readAsText( file );
+
+	}
+
+	const saveData = ( function () {
+
+		const a = document.createElement( 'a' );
+		document.body.appendChild( a );
+		a.style.display = 'none';
+		return function saveData( blob, fileName ) {
+
+			const url = window.URL.createObjectURL( blob );
+			a.href = url;
+			a.download = fileName;
+			a.click();
+
+		};
+
+	}() );
+
+	document.querySelector( 'button' ).addEventListener( 'click', () => {
+
+		render();
+		renderer.domElement.toBlob( ( blob ) => {
+
+			saveData( blob, `${name}-s${renderer.domElement.height}.png` );
+
+		} );
+
+	} );
 
 
-  const saveData = (function() {
-    const a = document.createElement('a');
-    document.body.appendChild(a);
-    a.style.display = 'none';
-    return function saveData(blob, fileName) {
-      const url = window.URL.createObjectURL(blob);
-      a.href = url;
-      a.download = fileName;
-      a.click();
-    };
-  }());
-
-  document.querySelector('button').addEventListener('click', () => {
-    render();
-    renderer.domElement.toBlob((blob) => {
-      saveData(blob, `${name}-s${renderer.domElement.height}.png`);
-    });
-  });
 }
 }
 
 
 main();
 main();

+ 111 - 93
manual/examples/postprocessing-custom.html

@@ -35,70 +35,76 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, 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 scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 2;
-    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);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({color});
+	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 cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
+	const scene = new THREE.Scene();
 
 
-    cube.position.x = x;
+	{
 
 
-    return cube;
-  }
+		const color = 0xFFFFFF;
+		const intensity = 6;
+		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;
 
 
-  const cubes = [
-    makeInstance(geometry, 0x44aa88,  0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844,  2),
-  ];
-
-  const composer = new EffectComposer(renderer);
-  composer.addPass(new RenderPass(scene, camera));
-
-  const colorShader = {
-    uniforms: {
-      tDiffuse: { value: null },
-      color:    { value: new THREE.Color(0x88CCFF) },
-    },
-    vertexShader: `
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	const composer = new EffectComposer( renderer );
+	composer.addPass( new RenderPass( scene, camera ) );
+
+	const colorShader = {
+		uniforms: {
+			tDiffuse: { value: null },
+			color: { value: new THREE.Color( 0x88CCFF ) },
+		},
+		vertexShader: `
       varying vec2 vUv;
       varying vec2 vUv;
       void main() {
       void main() {
         vUv = uv;
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
         gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
       }
       }
     `,
     `,
-    fragmentShader: `
+		fragmentShader: `
       uniform vec3 color;
       uniform vec3 color;
       uniform sampler2D tDiffuse;
       uniform sampler2D tDiffuse;
       varying vec2 vUv;
       varying vec2 vUv;
@@ -109,54 +115,66 @@ function main() {
             previousPassColor.a);
             previousPassColor.a);
       }
       }
     `,
     `,
-  };
-
-  const colorPass = new ShaderPass(colorShader);
-  colorPass.renderToScreen = true;
-  composer.addPass(colorPass);
-
-  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;
-  }
+	};
 
 
-  const gui = new GUI();
-  gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
-  gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
-  gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');
-
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const deltaTime = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-    }
+	const colorPass = new ShaderPass( colorShader );
+	colorPass.renderToScreen = true;
+	composer.addPass( colorPass );
 
 
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = now * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
+	function resizeRendererToDisplaySize( renderer ) {
 
 
-    composer.render(deltaTime);
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth;
+		const height = canvas.clientHeight;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
 
 
-    requestAnimationFrame(render);
-  }
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	const gui = new GUI();
+	gui.add( colorPass.uniforms.color.value, 'r', 0, 4 ).name( 'red' );
+	gui.add( colorPass.uniforms.color.value, 'g', 0, 4 ).name( 'green' );
+	gui.add( colorPass.uniforms.color.value, 'b', 0, 4 ).name( 'blue' );
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const deltaTime = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = now * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		composer.render( deltaTime );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 133 - 107
manual/examples/postprocessing-gui.html

@@ -35,127 +35,153 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
-import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
-import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
+import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, 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 scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 2;
-    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);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({color});
+	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 cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
+	const scene = new THREE.Scene();
 
 
-    cube.position.x = x;
+	{
 
 
-    return cube;
-  }
+		const color = 0xFFFFFF;
+		const intensity = 6;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
 
 
-  const cubes = [
-    makeInstance(geometry, 0x44aa88,  0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844,  2),
-  ];
-
-  const composer = new EffectComposer(renderer);
-  composer.addPass(new RenderPass(scene, camera));
-
-  const bloomPass = new BloomPass(
-      1,    // strength
-      25,   // kernel size
-      4,    // sigma ?
-      256,  // blur render target resolution
-  );
-  composer.addPass(bloomPass);
-
-  const filmPass = new FilmPass(
-      0.35,   // noise intensity
-      0.025,  // scanline intensity
-      648,    // scanline count
-      false,  // grayscale
-  );
-  filmPass.renderToScreen = true;
-  composer.addPass(filmPass);
-
-  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;
-  }
+	}
 
 
-  const gui = new GUI();
-  {
-    const folder = gui.addFolder('BloomPass');
-    folder.add(bloomPass.combineUniforms.strength, 'value', 0, 2).name('strength');
-    folder.open();
-  }
-  {
-    const folder = gui.addFolder('FilmPass');
-    folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
-    folder.add(filmPass.uniforms.nIntensity, 'value', 0, 1).name('noise intensity');
-    folder.add(filmPass.uniforms.sIntensity, 'value', 0, 1).name('scanline intensity');
-    folder.add(filmPass.uniforms.sCount, 'value', 0, 1000).name('scanline count');
-    folder.open();
-  }
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const deltaTime = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-    }
+	function makeInstance( geometry, color, x ) {
 
 
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = now * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
+		const material = new THREE.MeshPhongMaterial( { color } );
 
 
-    composer.render(deltaTime);
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
 
 
-    requestAnimationFrame(render);
-  }
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	const composer = new EffectComposer( renderer );
+	composer.addPass( new RenderPass( scene, camera ) );
+
+	const bloomPass = new BloomPass(
+		1, // strength
+		25, // kernel size
+		4, // sigma ?
+		256, // blur render target resolution
+	);
+	composer.addPass( bloomPass );
+
+	const filmPass = new FilmPass(
+		0.35, // noise intensity
+		0.025, // scanline intensity
+		648, // scanline count
+		false, // grayscale
+	);
+	composer.addPass( filmPass );
+
+	const outputPass = new OutputPass();
+	composer.addPass( outputPass );
+
+	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;
+
+	}
+
+	const gui = new GUI();
+	{
+
+		const folder = gui.addFolder( 'BloomPass' );
+		folder.add( bloomPass.combineUniforms.strength, 'value', 0, 2 ).name( 'strength' );
+		folder.open();
+
+	}
+
+	{
+
+		const folder = gui.addFolder( 'FilmPass' );
+		folder.add( filmPass.uniforms.grayscale, 'value' ).name( 'grayscale' );
+		folder.add( filmPass.uniforms.nIntensity, 'value', 0, 1 ).name( 'noise intensity' );
+		folder.add( filmPass.uniforms.sIntensity, 'value', 0, 1 ).name( 'scanline intensity' );
+		folder.add( filmPass.uniforms.sCount, 'value', 0, 1000 ).name( 'scanline count' );
+		folder.open();
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const deltaTime = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = now * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		composer.render( deltaTime );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 113 - 92
manual/examples/postprocessing.html

@@ -35,111 +35,132 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
-import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
-import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
-import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
+import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
+import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, 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 scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 2;
-    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);
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({color});
+	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 cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
+	const scene = new THREE.Scene();
 
 
-    cube.position.x = x;
+	{
 
 
-    return cube;
-  }
+		const color = 0xFFFFFF;
+		const intensity = 6;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
 
 
-  const cubes = [
-    makeInstance(geometry, 0x44aa88,  0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844,  2),
-  ];
-
-  const composer = new EffectComposer(renderer);
-  composer.addPass(new RenderPass(scene, camera));
-
-  const bloomPass = new BloomPass(
-      1,    // strength
-      25,   // kernel size
-      4,    // sigma ?
-      256,  // blur render target resolution
-  );
-  composer.addPass(bloomPass);
-
-  const filmPass = new FilmPass(
-      0.35,   // noise intensity
-      0.025,  // scanline intensity
-      648,    // scanline count
-      false,  // grayscale
-  );
-  filmPass.renderToScreen = true;
-  composer.addPass(filmPass);
-
-  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;
-  }
+	}
 
 
-  let then = 0;
-  function render(now) {
-    now *= 0.001;  // convert to seconds
-    const deltaTime = now - then;
-    then = now;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-      composer.setSize(canvas.width, canvas.height);
-    }
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
 
 
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = now * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
+	function makeInstance( geometry, color, x ) {
 
 
-    composer.render(deltaTime);
+		const material = new THREE.MeshPhongMaterial( { color } );
 
 
-    requestAnimationFrame(render);
-  }
+		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 ),
+	];
+
+	const composer = new EffectComposer( renderer );
+	composer.addPass( new RenderPass( scene, camera ) );
+
+	const bloomPass = new BloomPass(
+		1, // strength
+		25, // kernel size
+		4, // sigma ?
+		256, // blur render target resolution
+	);
+	composer.addPass( bloomPass );
+
+	const filmPass = new FilmPass(
+		0.35, // noise intensity
+		0.025, // scanline intensity
+		648, // scanline count
+		false, // grayscale
+	);
+	composer.addPass( filmPass );
+
+	const outputPass = new OutputPass();
+	composer.addPass( outputPass );
+
+	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;
+
+	}
+
+	let then = 0;
+	function render( now ) {
+
+		now *= 0.001; // convert to seconds
+		const deltaTime = now - then;
+		then = now;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+			composer.setSize( canvas.width, canvas.height );
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = now * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		composer.render( deltaTime );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 146 - 112
manual/examples/primitives-text.html

@@ -35,136 +35,170 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {FontLoader} from 'three/addons/loaders/FontLoader.js';
-import {TextGeometry} from 'three/addons/geometries/TextGeometry.js';
+import { FontLoader } from 'three/addons/loaders/FontLoader.js';
+import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 40;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 40;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color(0xAAAAAA);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(1, -2, -4);
-    scene.add(light);
-  }
 
 
-  const objects = [];
-  const spread = 15;
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function addObject(x, y, obj) {
-    obj.position.x = x * spread;
-    obj.position.y = y * spread;
+	const fov = 40;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 40;
 
 
-    scene.add(obj);
-    objects.push(obj);
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 0xAAAAAA );
 
 
-  function createMaterial() {
-    const material = new THREE.MeshPhongMaterial({
-      side: THREE.DoubleSide,
-    });
+	{
 
 
-    const hue = Math.random();
-    const saturation = 1;
-    const luminance = .5;
-    material.color.setHSL(hue, saturation, luminance);
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
 
 
-    return material;
-  }
+	}
 
 
-  function addSolidGeometry(x, y, geometry) {
-    const mesh = new THREE.Mesh(geometry, createMaterial());
-    addObject(x, y, mesh);
-  }
+	{
 
 
-  {
-    const loader = new FontLoader();
-    // promisify font loading
-    function loadFont(url) {
-      return new Promise((resolve, reject) => {
-        loader.load(url, resolve, undefined, reject);
-      });
-    }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 1, - 2, - 4 );
+		scene.add( light );
 
 
-    async function doit() {
-      const font = await loadFont('/examples/fonts/helvetiker_regular.typeface.json');   /* threejs.org: url */
-      const geometry = new TextGeometry('three.js', {
-        font: font,
-        size: 3.0,
-        height: .2,
-        curveSegments: 12,
-        bevelEnabled: true,
-        bevelThickness: 0.15,
-        bevelSize: .3,
-        bevelSegments: 5,
-      });
-
-      addSolidGeometry(-.5, 0, geometry);
-
-      const mesh = new THREE.Mesh(geometry, createMaterial());
-      geometry.computeBoundingBox();
-      geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
-
-      const parent = new THREE.Object3D();
-      parent.add(mesh);
-
-      addObject(.5, 0, parent);
-    }
-    doit();
-  }
+	}
 
 
-  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;
-  }
+	const objects = [];
+	const spread = 15;
 
 
-  function render(time) {
-    time *= 0.001;
+	function addObject( x, y, obj ) {
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		obj.position.x = x * spread;
+		obj.position.y = y * spread;
 
 
-    objects.forEach((obj, ndx) => {
-      const speed = .5 + ndx * .05;
-      const rot = time * speed;
-      obj.rotation.x = rot;
-      obj.rotation.y = rot;
-    });
+		scene.add( obj );
+		objects.push( obj );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	function createMaterial() {
+
+		const material = new THREE.MeshPhongMaterial( {
+			side: THREE.DoubleSide,
+		} );
+
+		const hue = Math.random();
+		const saturation = 1;
+		const luminance = .5;
+		material.color.setHSL( hue, saturation, luminance );
+
+		return material;
+
+	}
+
+	function addSolidGeometry( x, y, geometry ) {
+
+		const mesh = new THREE.Mesh( geometry, createMaterial() );
+		addObject( x, y, mesh );
+
+	}
+
+	{
+
+		const loader = new FontLoader();
+		// promisify font loading
+		function loadFont( url ) {
+
+			return new Promise( ( resolve, reject ) => {
+
+				loader.load( url, resolve, undefined, reject );
+
+			} );
+
+		}
+
+		async function doit() {
+
+			const font = await loadFont( '/examples/fonts/helvetiker_regular.typeface.json' ); /* threejs.org: url */
+			const geometry = new TextGeometry( 'three.js', {
+				font: font,
+				size: 3.0,
+				height: .2,
+				curveSegments: 12,
+				bevelEnabled: true,
+				bevelThickness: 0.15,
+				bevelSize: .3,
+				bevelSegments: 5,
+			} );
+
+			addSolidGeometry( - .5, 0, geometry );
+
+			const mesh = new THREE.Mesh( geometry, createMaterial() );
+			geometry.computeBoundingBox();
+			geometry.boundingBox.getCenter( mesh.position ).multiplyScalar( - 1 );
+
+			const parent = new THREE.Object3D();
+			parent.add( mesh );
+
+			addObject( .5, 0, parent );
+
+		}
+
+		doit();
+
+	}
+
+	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();
+
+		}
+
+		objects.forEach( ( obj, ndx ) => {
+
+			const speed = .5 + ndx * .05;
+			const rot = time * speed;
+			obj.rotation.x = rot;
+			obj.rotation.y = rot;
+
+		} );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 421 - 307
manual/examples/primitives.html

@@ -35,142 +35,188 @@
 
 
 <script type="module">
 <script type="module">
 import * as THREE from 'three';
 import * as THREE from 'three';
-import {FontLoader} from 'three/addons/loaders/FontLoader.js';
-import {ParametricGeometry} from 'three/addons/geometries/ParametricGeometry.js';
-import {TextGeometry} from 'three/addons/geometries/TextGeometry.js';
+import { FontLoader } from 'three/addons/loaders/FontLoader.js';
+import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js';
+import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
 
 
 function main() {
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-
-  const fov = 40;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 1000;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 120;
-
-  const scene = new THREE.Scene();
-  scene.background = new THREE.Color(0xAAAAAA);
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(1, -2, -4);
-    scene.add(light);
-  }
 
 
-  const objects = [];
-  const spread = 15;
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
+	renderer.useLegacyLights = false;
 
 
-  function addObject(x, y, obj) {
-    obj.position.x = x * spread;
-    obj.position.y = y * spread;
+	const fov = 40;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 1000;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 120;
 
 
-    scene.add(obj);
-    objects.push(obj);
-  }
+	const scene = new THREE.Scene();
+	scene.background = new THREE.Color( 0xAAAAAA );
 
 
-  function createMaterial() {
-    const material = new THREE.MeshPhongMaterial({
-      side: THREE.DoubleSide,
-    });
+	{
 
 
-    const hue = Math.random();
-    const saturation = 1;
-    const luminance = .5;
-    material.color.setHSL(hue, saturation, luminance);
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
 
 
-    return material;
-  }
+	}
 
 
-  function addSolidGeometry(x, y, geometry) {
-    const mesh = new THREE.Mesh(geometry, createMaterial());
-    addObject(x, y, mesh);
-  }
+	{
 
 
-  function addLineGeometry(x, y, geometry) {
-    const material = new THREE.LineBasicMaterial({color: 0x000000});
-    const mesh = new THREE.LineSegments(geometry, material);
-    addObject(x, y, mesh);
-  }
+		const color = 0xFFFFFF;
+		const intensity = 3;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( 1, - 2, - 4 );
+		scene.add( light );
 
 
-  {
-    const width = 8;
-    const height = 8;
-    const depth = 8;
-    addSolidGeometry(-2, 2, new THREE.BoxGeometry(width, height, depth));
-  }
-  {
-    const radius = 7;
-    const segments = 24;
-    addSolidGeometry(-1, 2, new THREE.CircleGeometry(radius, segments));
-  }
-  {
-    const radius = 6;
-    const height = 8;
-    const segments = 16;
-    addSolidGeometry(0, 2, new THREE.ConeGeometry(radius, height, segments));
-  }
-  {
-    const radiusTop = 4;
-    const radiusBottom = 4;
-    const height = 8;
-    const radialSegments = 12;
-    addSolidGeometry(1, 2, new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSegments));
-  }
-  {
-    const radius = 7;
-    addSolidGeometry(2, 2, new THREE.DodecahedronGeometry(radius));
-  }
-  {
-    const shape = new THREE.Shape();
-    const x = -2.5;
-    const y = -5;
-    shape.moveTo(x + 2.5, y + 2.5);
-    shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
-    shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
-    shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
-    shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
-    shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
-    shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
-
-    const extrudeSettings = {
-      steps: 2,
-      depth: 2,
-      bevelEnabled: true,
-      bevelThickness: 1,
-      bevelSize: 1,
-      bevelSegments: 2,
-    };
-
-    addSolidGeometry(-2, 1, new THREE.ExtrudeGeometry(shape, extrudeSettings));
-  }
-  {
-    const radius = 7;
-    addSolidGeometry(-1, 1, new THREE.IcosahedronGeometry(radius));
-  }
-  {
-    const points = [];
-    for (let i = 0; i < 10; ++i) {
-      points.push(new THREE.Vector2(Math.sin(i * 0.2) * 3 + 3, (i - 5) * .8));
-    }
-    addSolidGeometry(0, 1, new THREE.LatheGeometry(points));
-  }
-  {
-    const radius = 7;
-    addSolidGeometry(1, 1, new THREE.OctahedronGeometry(radius));
-  }
-  {
-    /*
+	}
+
+	const objects = [];
+	const spread = 15;
+
+	function addObject( x, y, obj ) {
+
+		obj.position.x = x * spread;
+		obj.position.y = y * spread;
+
+		scene.add( obj );
+		objects.push( obj );
+
+	}
+
+	function createMaterial() {
+
+		const material = new THREE.MeshPhongMaterial( {
+			side: THREE.DoubleSide,
+		} );
+
+		const hue = Math.random();
+		const saturation = 1;
+		const luminance = .5;
+		material.color.setHSL( hue, saturation, luminance );
+
+		return material;
+
+	}
+
+	function addSolidGeometry( x, y, geometry ) {
+
+		const mesh = new THREE.Mesh( geometry, createMaterial() );
+		addObject( x, y, mesh );
+
+	}
+
+	function addLineGeometry( x, y, geometry ) {
+
+		const material = new THREE.LineBasicMaterial( { color: 0x000000 } );
+		const mesh = new THREE.LineSegments( geometry, material );
+		addObject( x, y, mesh );
+
+	}
+
+	{
+
+		const width = 8;
+		const height = 8;
+		const depth = 8;
+		addSolidGeometry( - 2, 2, new THREE.BoxGeometry( width, height, depth ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		const segments = 24;
+		addSolidGeometry( - 1, 2, new THREE.CircleGeometry( radius, segments ) );
+
+	}
+
+	{
+
+		const radius = 6;
+		const height = 8;
+		const segments = 16;
+		addSolidGeometry( 0, 2, new THREE.ConeGeometry( radius, height, segments ) );
+
+	}
+
+	{
+
+		const radiusTop = 4;
+		const radiusBottom = 4;
+		const height = 8;
+		const radialSegments = 12;
+		addSolidGeometry( 1, 2, new THREE.CylinderGeometry( radiusTop, radiusBottom, height, radialSegments ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		addSolidGeometry( 2, 2, new THREE.DodecahedronGeometry( radius ) );
+
+	}
+
+	{
+
+		const shape = new THREE.Shape();
+		const x = - 2.5;
+		const y = - 5;
+		shape.moveTo( x + 2.5, y + 2.5 );
+		shape.bezierCurveTo( x + 2.5, y + 2.5, x + 2, y, x, y );
+		shape.bezierCurveTo( x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5 );
+		shape.bezierCurveTo( x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5 );
+		shape.bezierCurveTo( x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5 );
+		shape.bezierCurveTo( x + 8, y + 3.5, x + 8, y, x + 5, y );
+		shape.bezierCurveTo( x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5 );
+
+		const extrudeSettings = {
+			steps: 2,
+			depth: 2,
+			bevelEnabled: true,
+			bevelThickness: 1,
+			bevelSize: 1,
+			bevelSegments: 2,
+		};
+
+		addSolidGeometry( - 2, 1, new THREE.ExtrudeGeometry( shape, extrudeSettings ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		addSolidGeometry( - 1, 1, new THREE.IcosahedronGeometry( radius ) );
+
+	}
+
+	{
+
+		const points = [];
+		for ( let i = 0; i < 10; ++ i ) {
+
+			points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 3 + 3, ( i - 5 ) * .8 ) );
+
+		}
+
+		addSolidGeometry( 0, 1, new THREE.LatheGeometry( points ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		addSolidGeometry( 1, 1, new THREE.OctahedronGeometry( radius ) );
+
+	}
+
+	{
+
+		/*
     from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js
     from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js
 
 
     The MIT License
     The MIT License
@@ -196,202 +242,270 @@ function main() {
     THE SOFTWARE.
     THE SOFTWARE.
 
 
     */
     */
-    function klein(v, u, target) {
-      u *= Math.PI;
-      v *= 2 * Math.PI;
-      u = u * 2;
+		function klein( v, u, target ) {
 
 
-      let x;
-      let z;
+			u *= Math.PI;
+			v *= 2 * Math.PI;
+			u = u * 2;
 
 
-      if (u < Math.PI) {
-          x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(u) * Math.cos(v);
-          z = -8 * Math.sin(u) - 2 * (1 - Math.cos(u) / 2) * Math.sin(u) * Math.cos(v);
-      } else {
-          x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(v + Math.PI);
-          z = -8 * Math.sin(u);
-      }
+			let x;
+			let z;
 
 
-      const y = -2 * (1 - Math.cos(u) / 2) * Math.sin(v);
+			if ( u < Math.PI ) {
 
 
-      target.set(x, y, z).multiplyScalar(0.75);
-    }
+				x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( u ) * Math.cos( v );
+				z = - 8 * Math.sin( u ) - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( u ) * Math.cos( v );
 
 
-    const slices = 25;
-    const stacks = 25;
-    addSolidGeometry(2, 1, new ParametricGeometry(klein, slices, stacks));
-  }
-  {
-    const width = 9;
-    const height = 9;
-    const widthSegments = 2;
-    const heightSegments = 2;
-    addSolidGeometry(-2, 0, new THREE.PlaneGeometry(width, height, widthSegments, heightSegments));
-  }
-  {
-    const verticesOfCube = [
-        -1, -1, -1,    1, -1, -1,    1,  1, -1,    -1,  1, -1,
-        -1, -1,  1,    1, -1,  1,    1,  1,  1,    -1,  1,  1,
-    ];
-    const indicesOfFaces = [
-        2, 1, 0,    0, 3, 2,
-        0, 4, 7,    7, 3, 0,
-        0, 1, 5,    5, 4, 0,
-        1, 2, 6,    6, 5, 1,
-        2, 3, 7,    7, 6, 2,
-        4, 5, 6,    6, 7, 4,
-    ];
-    const radius = 7;
-    const detail = 2;
-    addSolidGeometry(-1, 0, new THREE.PolyhedronGeometry(verticesOfCube, indicesOfFaces, radius, detail));
-  }
-  {
-    const innerRadius = 2;
-    const outerRadius = 7;
-    const segments = 18;
-    addSolidGeometry(0, 0, new THREE.RingGeometry(innerRadius, outerRadius, segments));
-  }
-  {
-    const shape = new THREE.Shape();
-    const x = -2.5;
-    const y = -5;
-    shape.moveTo(x + 2.5, y + 2.5);
-    shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y);
-    shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5);
-    shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5);
-    shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5);
-    shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y);
-    shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5);
-    addSolidGeometry(1, 0, new THREE.ShapeGeometry(shape));
-  }
-  {
-    const radius = 7;
-    const widthSegments = 12;
-    const heightSegments = 8;
-    addSolidGeometry(2, 0, new THREE.SphereGeometry(radius, widthSegments, heightSegments));
-  }
-  {
-    const radius = 7;
-    addSolidGeometry(-2, -1, new THREE.TetrahedronGeometry(radius));
-  }
-  {
-    const loader = new FontLoader();
-    // promisify font loading
-    function loadFont(url) {
-      return new Promise((resolve, reject) => {
-        loader.load(url, resolve, undefined, reject);
-      });
-    }
+			} else {
 
 
-    async function doit() {
-      const font = await loadFont('/examples/fonts/helvetiker_regular.typeface.json');  /* threejs.org: url */
-      const geometry = new TextGeometry('three.js', {
-        font: font,
-        size: 3.0,
-        height: .2,
-        curveSegments: 12,
-        bevelEnabled: true,
-        bevelThickness: 0.15,
-        bevelSize: .3,
-        bevelSegments: 5,
-      });
-      const mesh = new THREE.Mesh(geometry, createMaterial());
-      geometry.computeBoundingBox();
-      geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
-
-      const parent = new THREE.Object3D();
-      parent.add(mesh);
-
-      addObject(-1, -1, parent);
-    }
-    doit();
-  }
-  {
-    const radius = 5;
-    const tubeRadius = 2;
-    const radialSegments = 8;
-    const tubularSegments = 24;
-    addSolidGeometry(0, -1, new THREE.TorusGeometry(radius, tubeRadius, radialSegments, tubularSegments));
-  }
-  {
-    const radius = 3.5;
-    const tube = 1.5;
-    const radialSegments = 8;
-    const tubularSegments = 64;
-    const p = 2;
-    const q = 3;
-    addSolidGeometry(1, -1, new THREE.TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q));
-  }
-  {
-    class CustomSinCurve extends THREE.Curve {
-      constructor(scale) {
-        super();
-        this.scale = scale;
-      }
-      getPoint(t) {
-        const tx = t * 3 - 1.5;
-        const ty = Math.sin(2 * Math.PI * t);
-        const tz = 0;
-        return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
-      }
-    }
+				x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( v + Math.PI );
+				z = - 8 * Math.sin( u );
 
 
-    const path = new CustomSinCurve(4);
-    const tubularSegments = 20;
-    const radius = 1;
-    const radialSegments = 8;
-    const closed = false;
-    addSolidGeometry(2, -1, new THREE.TubeGeometry(path, tubularSegments, radius, radialSegments, closed));
-  }
-  {
-    const width = 8;
-    const height = 8;
-    const depth = 8;
-    const thresholdAngle = 15;
-    addLineGeometry(-1, -2, new THREE.EdgesGeometry(
-        new THREE.BoxGeometry(width, height, depth),
-        thresholdAngle));
-  }
-  {
-    const width = 8;
-    const height = 8;
-    const depth = 8;
-    addLineGeometry(1, -2, new THREE.WireframeGeometry(new THREE.BoxGeometry(width, height, depth)));
-  }
+			}
 
 
-  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;
-  }
+			const y = - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( v );
 
 
-  function render(time) {
-    time *= 0.001;
+			target.set( x, y, z ).multiplyScalar( 0.75 );
 
 
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
+		}
 
 
-    objects.forEach((obj, ndx) => {
-      const speed = .1 + ndx * .05;
-      const rot = time * speed;
-      obj.rotation.x = rot;
-      obj.rotation.y = rot;
-    });
+		const slices = 25;
+		const stacks = 25;
+		addSolidGeometry( 2, 1, new ParametricGeometry( klein, slices, stacks ) );
 
 
-    renderer.render(scene, camera);
+	}
 
 
-    requestAnimationFrame(render);
-  }
+	{
+
+		const width = 9;
+		const height = 9;
+		const widthSegments = 2;
+		const heightSegments = 2;
+		addSolidGeometry( - 2, 0, new THREE.PlaneGeometry( width, height, widthSegments, heightSegments ) );
+
+	}
+
+	{
+
+		const verticesOfCube = [
+			- 1, - 1, - 1, 1, - 1, - 1, 1, 1, - 1, - 1, 1, - 1,
+			- 1, - 1, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 1,
+		];
+		const indicesOfFaces = [
+			2, 1, 0, 0, 3, 2,
+			0, 4, 7, 7, 3, 0,
+			0, 1, 5, 5, 4, 0,
+			1, 2, 6, 6, 5, 1,
+			2, 3, 7, 7, 6, 2,
+			4, 5, 6, 6, 7, 4,
+		];
+		const radius = 7;
+		const detail = 2;
+		addSolidGeometry( - 1, 0, new THREE.PolyhedronGeometry( verticesOfCube, indicesOfFaces, radius, detail ) );
+
+	}
+
+	{
+
+		const innerRadius = 2;
+		const outerRadius = 7;
+		const segments = 18;
+		addSolidGeometry( 0, 0, new THREE.RingGeometry( innerRadius, outerRadius, segments ) );
+
+	}
+
+	{
+
+		const shape = new THREE.Shape();
+		const x = - 2.5;
+		const y = - 5;
+		shape.moveTo( x + 2.5, y + 2.5 );
+		shape.bezierCurveTo( x + 2.5, y + 2.5, x + 2, y, x, y );
+		shape.bezierCurveTo( x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5 );
+		shape.bezierCurveTo( x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5 );
+		shape.bezierCurveTo( x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5 );
+		shape.bezierCurveTo( x + 8, y + 3.5, x + 8, y, x + 5, y );
+		shape.bezierCurveTo( x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5 );
+		addSolidGeometry( 1, 0, new THREE.ShapeGeometry( shape ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		const widthSegments = 12;
+		const heightSegments = 8;
+		addSolidGeometry( 2, 0, new THREE.SphereGeometry( radius, widthSegments, heightSegments ) );
+
+	}
+
+	{
+
+		const radius = 7;
+		addSolidGeometry( - 2, - 1, new THREE.TetrahedronGeometry( radius ) );
+
+	}
+
+	{
+
+		const loader = new FontLoader();
+		// promisify font loading
+		function loadFont( url ) {
+
+			return new Promise( ( resolve, reject ) => {
+
+				loader.load( url, resolve, undefined, reject );
+
+			} );
+
+		}
+
+		async function doit() {
+
+			const font = await loadFont( '/examples/fonts/helvetiker_regular.typeface.json' ); /* threejs.org: url */
+			const geometry = new TextGeometry( 'three.js', {
+				font: font,
+				size: 3.0,
+				height: .2,
+				curveSegments: 12,
+				bevelEnabled: true,
+				bevelThickness: 0.15,
+				bevelSize: .3,
+				bevelSegments: 5,
+			} );
+			const mesh = new THREE.Mesh( geometry, createMaterial() );
+			geometry.computeBoundingBox();
+			geometry.boundingBox.getCenter( mesh.position ).multiplyScalar( - 1 );
+
+			const parent = new THREE.Object3D();
+			parent.add( mesh );
+
+			addObject( - 1, - 1, parent );
+
+		}
+
+		doit();
+
+	}
+
+	{
+
+		const radius = 5;
+		const tubeRadius = 2;
+		const radialSegments = 8;
+		const tubularSegments = 24;
+		addSolidGeometry( 0, - 1, new THREE.TorusGeometry( radius, tubeRadius, radialSegments, tubularSegments ) );
+
+	}
+
+	{
+
+		const radius = 3.5;
+		const tube = 1.5;
+		const radialSegments = 8;
+		const tubularSegments = 64;
+		const p = 2;
+		const q = 3;
+		addSolidGeometry( 1, - 1, new THREE.TorusKnotGeometry( radius, tube, tubularSegments, radialSegments, p, q ) );
+
+	}
+
+	{
+
+		class CustomSinCurve extends THREE.Curve {
+
+			constructor( scale ) {
+
+				super();
+				this.scale = scale;
+
+			}
+			getPoint( t ) {
+
+				const tx = t * 3 - 1.5;
+				const ty = Math.sin( 2 * Math.PI * t );
+				const tz = 0;
+				return new THREE.Vector3( tx, ty, tz ).multiplyScalar( this.scale );
+
+			}
+
+		}
+
+		const path = new CustomSinCurve( 4 );
+		const tubularSegments = 20;
+		const radius = 1;
+		const radialSegments = 8;
+		const closed = false;
+		addSolidGeometry( 2, - 1, new THREE.TubeGeometry( path, tubularSegments, radius, radialSegments, closed ) );
+
+	}
+
+	{
+
+		const width = 8;
+		const height = 8;
+		const depth = 8;
+		const thresholdAngle = 15;
+		addLineGeometry( - 1, - 2, new THREE.EdgesGeometry(
+			new THREE.BoxGeometry( width, height, depth ),
+			thresholdAngle ) );
+
+	}
+
+	{
+
+		const width = 8;
+		const height = 8;
+		const depth = 8;
+		addLineGeometry( 1, - 2, new THREE.WireframeGeometry( new THREE.BoxGeometry( width, height, depth ) ) );
+
+	}
+
+	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();
+
+		}
+
+		objects.forEach( ( obj, ndx ) => {
+
+			const speed = .1 + ndx * .05;
+			const rot = time * speed;
+			obj.rotation.x = rot;
+			obj.rotation.y = rot;
+
+		} );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
 
 
-  requestAnimationFrame(render);
 }
 }
 
 
 main();
 main();

+ 1 - 27
manual/fr/lights.html

@@ -69,6 +69,7 @@ const texture = loader.load('resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -389,34 +390,7 @@ makeXYZGUI(gui, light.position, 'position');
 </div>
 </div>
 
 
 <p></p>
 <p></p>
-<p>Une chose que nous n'avons pas vu, c'est qu'il existe un paramètre sur le <a href="https://threejs.org/docs/#api/en/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a> appelé <code class="notranslate" translate="no">physicalCorrectLights</code>. Il affecte la façon dont la lumière tombe en tant que distance de la lumière. Cela n'affecte que <a href="https://threejs.org/docs/#api/en/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a> et <a href="https://threejs.org/docs/#api/en/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>. <a href="https://threejs.org/docs/#api/en/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a> le fait automatiquement.</p>
-<p>Pour les lumières, l'idée de base est de ne pas définir de distance pour qu'elles s'éteignent, et vous ne définissez pas <code class="notranslate" translate="no">intensity</code>. Au lieu de cela, vous définissez la <a href="/docs/#api/en/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a> de la lumière en lumens, puis three.js utilisera des calculs physiques comme de vraies lumières. Les unités de three.js dans ce cas sont des mètres et une ampoule de 60w aurait environ 800 lumens. Il y a aussi une propriété <a href="/docs/#api/en/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a>. Elle doit être réglé sur 2 pour une apparence réaliste.</p>
-<p>Testons ça.</p>
-<p>D'abord, définissons <code class="notranslate" translate="no">physicallyCorrectLights</code> sur true</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>Ensuite, réglons la <code class="notranslate" translate="no">power</code> à 800 lumens, la <code class="notranslate" translate="no">decay</code> à 2, et
-la <code class="notranslate" translate="no">distance</code> sur <code class="notranslate" translate="no">Infinity</code>.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>et mettons à jour lil-gui pour pouvoir changer <code class="notranslate" translate="no">power</code> et <code class="notranslate" translate="no">decay</code></p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">Cliquer ici pour ouvrir dans une fenêtre séparée</a>
-</div>
 
 
-<p></p>
 <p>Il est important de noter que chaque lumière que vous ajoutez à la scène ralentit la vitesse à laquelle Three.js rend la scène, vous devez donc toujours essayer d'en utiliser le moins possible pour atteindre vos objectifs.</p>
 <p>Il est important de noter que chaque lumière que vous ajoutez à la scène ralentit la vitesse à laquelle Three.js rend la scène, vous devez donc toujours essayer d'en utiliser le moins possible pour atteindre vos objectifs.</p>
 <p>Passons maintenant à <a href="cameras.html">la gestion des caméras</a>.</p>
 <p>Passons maintenant à <a href="cameras.html">la gestion des caméras</a>.</p>
 <p><canvas id="c"></canvas></p>
 <p><canvas id="c"></canvas></p>

+ 1 - 34
manual/ja/lights.html

@@ -77,6 +77,7 @@ const texture = loader.load('resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -427,41 +428,7 @@ makeXYZGUI(gui, light.position, 'position');
 </div>
 </div>
 
 
 <p></p>
 <p></p>
-<p>説明してない事が1つあり、 <a href="/docs/#api/ja/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a> に <code class="notranslate" translate="no">physicallyCorrectLights</code> を設定します。
-ライトとの距離は、ライトの落ち方に影響を与えます。
-これは <a href="/docs/#api/ja/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a> と <a href="/docs/#api/ja/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a> にのみ影響します。
-<a href="/docs/#api/ja/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a> は自動的にこれを行います。</p>
-<p>ライトの場合の基本的な考え方は、フェードアウトする距離を設定しない、<code class="notranslate" translate="no">intensity</code> を設定しない事です。
-代わりにライトの<a href="/docs/#api/ja/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a>をルーメン単位で設定すると、three.jsは実際のライトのように物理計算を行います。
-この場合の単位はメートルで、60Wの電球であれば800ルーメン程度の明るさになります。
-また<a href="/docs/#api/ja/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a>プロパティもあります。
-現実的な減衰のためには <code class="notranslate" translate="no">2</code> に設定します。</p>
-<p>試してみましょう。</p>
-<p>まずは物理的に正しいライトを作ります。</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>次に <code class="notranslate" translate="no">power</code> を800ルーメンにし、<code class="notranslate" translate="no">decay</code> を2にします。
-<code class="notranslate" translate="no">distance</code> は <code class="notranslate" translate="no">Infinity</code> にします。</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>GUIを追加して <code class="notranslate" translate="no">power</code> と <code class="notranslate" translate="no">decay</code> を変更できるようにします。</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
-</div>
 
 
-<p></p>
 <p>シーンにライトを追加するたびに、Three.jsのレンダリング速度が遅くなる事に注意して下さい。</p>
 <p>シーンにライトを追加するたびに、Three.jsのレンダリング速度が遅くなる事に注意して下さい。</p>
 <p>次は<a href="cameras.html">カメラの扱い方</a>についてです。</p>
 <p>次は<a href="cameras.html">カメラの扱い方</a>についてです。</p>
 <p><canvas id="c"></canvas></p>
 <p><canvas id="c"></canvas></p>

+ 1 - 0
manual/ja/post-processing.html

@@ -99,6 +99,7 @@ composer.addPass(filmPass);
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
+import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
 </pre>
 </pre>
 <p>ほとんどのポストプロセスには <code class="notranslate" translate="no">EffectComposer.js</code> と <code class="notranslate" translate="no">RenderPass.js</code> が必須です。</p>
 <p>ほとんどのポストプロセスには <code class="notranslate" translate="no">EffectComposer.js</code> と <code class="notranslate" translate="no">RenderPass.js</code> が必須です。</p>
 <p>最後に <a href="/docs/#api/ja/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> の代わりに <code class="notranslate" translate="no">EffectComposer.render</code> を使用し、<code class="notranslate" translate="no">EffectComposer</code> にキャンバスのサイズを合わせます。</p>
 <p>最後に <a href="/docs/#api/ja/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> の代わりに <code class="notranslate" translate="no">EffectComposer.render</code> を使用し、<code class="notranslate" translate="no">EffectComposer</code> にキャンバスのサイズを合わせます。</p>

+ 1 - 36
manual/ko/lights.html

@@ -78,6 +78,7 @@ const texture = loader.load('resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -442,43 +443,7 @@ makeXYZGUI(gui, light.position, 'position');
 </div>
 </div>
 
 
 <p></p>
 <p></p>
-<p>하나 설명하지 않은 것이 있습니다. 위 예제에는 <a href="/docs/#api/ko/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a>의 <code class="notranslate" translate="no">physicallyCorrectLights(물리 기반 조명)</code>
-설정이 있습니다. 이는 거리에 따라 빛이 어떻게 떨어질지 결정하는 속성으로,
-<a href="/docs/#api/ko/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>와 <a href="/docs/#api/ko/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>가 이 설정의 영향을 받습니다. <a href="/docs/#api/ko/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a>는
-마찬가지로 설정의 영향도 받고, 기본적으로 이 설정을 사용하죠.</p>
-<p>이 설정을 사용하면 기본적으로 조명의 <code class="notranslate" translate="no">distance</code>나 <code class="notranslate" translate="no">intensity</code> 대신
-<a href="/docs/#api/ko/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a> 속성을 루멘(lumens) 단위로 설정해야 합니다.
-그러면 Three.js는 물리적 계산을 통해 실제 광원을 흉내내죠. 예제의
-거리 단위는 미터(meters)이니, 60w짜리 전구는 약 800루멘 정도일 겁니다.
-그리고 조명의 부서짐(decay) 정도를 설정하는 <a href="/docs/#api/ko/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a>
-속성도 있습니다. 현실적인 조명을 위해서는 <code class="notranslate" translate="no">2</code> 정도가 적당하죠.</p>
-<p>한 번 예제를 만들어 테스트해봅시다.</p>
-<p>먼저 <code class="notranslate" translate="no">renderer</code>의 <code class="notranslate" translate="no">physicallyCorrectLights</code> 속성을 켭니다.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>그리고 <code class="notranslate" translate="no">power</code>를 800루멘으로, <code class="notranslate" translate="no">decay</code> 속성을 2로, <code class="notranslate" translate="no">distance</code>
-속성을 <code class="notranslate" translate="no">Infinity</code>로 설정합니다.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>마지막으로 GUI를 추가해 <code class="notranslate" translate="no">power</code>와 <code class="notranslate" translate="no">decay</code> 속성을 조정할 수 있도록
-해줍니다.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">새 탭에서 보기</a>
-</div>
 
 
-<p></p>
 <p>조명은 <code class="notranslate" translate="no">renderer</code>가 장면을 렌더링하는 속도에 영향을 미칩니다. 그러니
 <p>조명은 <code class="notranslate" translate="no">renderer</code>가 장면을 렌더링하는 속도에 영향을 미칩니다. 그러니
 가능한 적은 조명을 쓰는 게 좋죠.</p>
 가능한 적은 조명을 쓰는 게 좋죠.</p>
 <p>다음 장에서는 <a href="cameras.html">카메라 조작법</a>에 대해 알아보겠습니다.</p>
 <p>다음 장에서는 <a href="cameras.html">카메라 조작법</a>에 대해 알아보겠습니다.</p>

+ 1 - 0
manual/ko/post-processing.html

@@ -82,6 +82,7 @@ composer.addPass(filmPass);
 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
 import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
 import { BloomPass } from 'three/addons/postprocessing/BloomPass.js';
 import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
 import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
+import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
 </pre>
 </pre>
 <p>대부분의 후처리에는 <code class="notranslate" translate="no">EffectComposer.js</code>와 <code class="notranslate" translate="no">RenderPass.js</code>가 필수입니다.</p>
 <p>대부분의 후처리에는 <code class="notranslate" translate="no">EffectComposer.js</code>와 <code class="notranslate" translate="no">RenderPass.js</code>가 필수입니다.</p>
 <p>이제 <a href="/docs/#api/ko/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> 대신 <code class="notranslate" translate="no">EffectComposer.render</code>를 사용<em>하고</em> <code class="notranslate" translate="no">EffectComposer</code>가 결과물을 캔버스의 크기에 맞추도록 해야 합니다.</p>
 <p>이제 <a href="/docs/#api/ko/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> 대신 <code class="notranslate" translate="no">EffectComposer.render</code>를 사용<em>하고</em> <code class="notranslate" translate="no">EffectComposer</code>가 결과물을 캔버스의 크기에 맞추도록 해야 합니다.</p>

+ 1 - 34
manual/ru/lights.html

@@ -84,6 +84,7 @@ const texture = loader.load('../resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -460,41 +461,7 @@ makeXYZGUI(gui, light.position, 'position');
 </div>
 </div>
 
 
 <p></p>
 <p></p>
-<p>Одна вещь, которую мы не охватили, это то, что есть настройка для <a href="/docs/#api/en/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a>
-вызываемого в <code class="notranslate" translate="no">physicallyCorrectLights</code>. Это влияет на то, как свет падает в зависимости 
-от расстояния до предмета. Это влияет только на <a href="/docs/#api/en/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a> и <a href="/docs/#api/en/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>. 
-<a href="/docs/#api/en/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a> делает это автоматически.</p>
-<p>Для источников света, хотя основная идея заключается в том, что вы не устанавливаете 
-расстояние для их затухания и не устанавливаете <code class="notranslate" translate="no">intensity</code>. Вместо этого вы устанавливаете силу
-<a href="/docs/#api/en/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a> света в люменах, а затем three.js будет использовать физические 
-вычисления, как у настоящих источников света. Единицами three.js в этом случае являются метры, 
-а лампочка мощностью 60 Вт будет иметь около 800 люмен. 
-Там также есть распад <a href="/docs/#api/en/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a>, он должен быть установлен в <code class="notranslate" translate="no">2</code> для реализма.</p>
-<p>Давайте проверим это.</p>
-<p>Сначала мы включим физически правильное освещение</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>Затем мы установим <code class="notranslate" translate="no">power</code> = 800 lumens,  <code class="notranslate" translate="no">decay</code> = 2 и <code class="notranslate" translate="no">distance</code> до <code class="notranslate" translate="no">Infinity</code>.</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>и мы добавим графический интерфейс, чтобы мы могли изменить <code class="notranslate" translate="no">power</code> и <code class="notranslate" translate="no">decay</code></p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">нажмите здесь, чтобы открыть в отдельном окне</a>
-</div>
 
 
-<p></p>
 <p>Важно отметить, что каждый источник света, который вы добавляете в сцену, 
 <p>Важно отметить, что каждый источник света, который вы добавляете в сцену, 
 замедляет скорость рендеринга сцены в three.js, поэтому вы всегда должны 
 замедляет скорость рендеринга сцены в three.js, поэтому вы всегда должны 
 стараться использовать как можно меньше для достижения своих целей.</p>
 стараться использовать как можно меньше для достижения своих целей.</p>

+ 1 - 26
manual/zh/lights.html

@@ -67,6 +67,7 @@ const texture = loader.load('resources/images/checker.png');
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapS = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.wrapT = THREE.RepeatWrapping;
 texture.magFilter = THREE.NearestFilter;
 texture.magFilter = THREE.NearestFilter;
+texture.colorSpace = THREE.SRGBColorSpace;
 const repeats = planeSize / 2;
 const repeats = planeSize / 2;
 texture.repeat.set(repeats, repeats);
 texture.repeat.set(repeats, repeats);
 </pre>
 </pre>
@@ -382,33 +383,7 @@ makeXYZGUI(gui, light.position, 'position');
 </div>
 </div>
 
 
 <p></p>
 <p></p>
-<p>关于光照,我们尚未提及的是 <a href="/docs/#api/zh/renderers/WebGLRenderer"><code class="notranslate" translate="no">WebGLRenderer</code></a> 中有一个设置项 <code class="notranslate" translate="no">physicallyCorrectLights</code>。这个设置会影响(随着离光源的距离增加)光照如何减弱。这个设置会影响点光源(<a href="/docs/#api/zh/lights/PointLight"><code class="notranslate" translate="no">PointLight</code></a>)和聚光灯(<a href="/docs/#api/zh/lights/SpotLight"><code class="notranslate" translate="no">SpotLight</code></a>),矩形区域光(<a href="/docs/#api/zh/lights/RectAreaLight"><code class="notranslate" translate="no">RectAreaLight</code></a>)会自动应用这个特性。</p>
-<p>在设置光照时,基本思路是不要设置 <code class="notranslate" translate="no">distance</code> 来表现光照的衰减,也不要设置 <code class="notranslate" translate="no">intensity</code>。而是设置光照的 <a href="/docs/#api/zh/lights/PointLight#power"><code class="notranslate" translate="no">power</code></a> 属性,以流明为单位,three.js 会进行物理计算,从而表现出接近真实的光照效果。在这种情况下 three.js 参与计算的长度单位是米,一个 60瓦 的灯泡大概是 800 流明强度。并且光源有一个 <a href="/docs/#api/zh/lights/PointLight#decay"><code class="notranslate" translate="no">decay</code></a> 属性,为了模拟真实效果,应该被设置为 <code class="notranslate" translate="no">2</code>。</p>
-<p>下面让我们测试看看。</p>
-<p>首先开启 <code class="notranslate" translate="no">physicallyCorrectLights</code> 模式</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
-+renderer.physicallyCorrectLights = true;
-</pre>
-<p>然后我们设置光照的参数,<code class="notranslate" translate="no">power</code> 设置为 800 流明,<code class="notranslate" translate="no">decay</code> 设置为 2,<code class="notranslate" translate="no">distance</code> 设置为 <code class="notranslate" translate="no">Infinity</code>。</p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const color = 0xFFFFFF;
-const intensity = 1;
-const light = new THREE.PointLight(color, intensity);
-light.power = 800;
-light.decay = 2;
-light.distance = Infinity;
-</pre>
-<p>并且添加 gui 控制 <code class="notranslate" translate="no">power</code> 和 <code class="notranslate" translate="no">decay</code></p>
-<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
-gui.addColor(new ColorGUIHelper(light, 'color'), 'value').name('color');
-gui.add(light, 'decay', 0, 4, 0.01);
-gui.add(light, 'power', 0, 2000);
-</pre>
-<p></p><div translate="no" class="threejs_example_container notranslate">
-  <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/lights-point-physically-correct.html"></iframe></div>
-  <a class="threejs_center" href="/manual/examples/lights-point-physically-correct.html" target="_blank">点击此处在新标签页中打开</a>
-</div>
 
 
-<p></p>
 <p>需要注意,每添加一个光源到场景中,都会降低 three.js 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果。</p>
 <p>需要注意,每添加一个光源到场景中,都会降低 three.js 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果。</p>
 <p>接下来我们学习 three.js 中的 <a href="cameras.html">相机</a>。</p>
 <p>接下来我们学习 three.js 中的 <a href="cameras.html">相机</a>。</p>
 <p><canvas id="c"></canvas></p>
 <p><canvas id="c"></canvas></p>

+ 1 - 0
manual/zh/post-processing.html

@@ -81,6 +81,7 @@ composer.addPass(filmPass);
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
 import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
+import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';
 </pre>
 </pre>
 <p>对于几乎所有的后期处理EffectComposer.js,RenderPass.js 都是必需的。</p>
 <p>对于几乎所有的后期处理EffectComposer.js,RenderPass.js 都是必需的。</p>
 <p>们需要做的最后一件事是使用EffectComposer.render 替代 WebGLRenderer.render 并告诉EffectComposer来匹配画布的大小</p>
 <p>们需要做的最后一件事是使用EffectComposer.render 替代 WebGLRenderer.render 并告诉EffectComposer来匹配画布的大小</p>