فهرست منبع

Initial commit.

simondevyoutube 5 سال پیش
والد
کامیت
97ffc7ecb1
17فایلهای تغییر یافته به همراه2143 افزوده شده و 0 حذف شده
  1. 47 0
      base.css
  2. 12 0
      index.html
  3. BIN
      resources/heightmap-hi.png
  4. BIN
      resources/heightmap-simondev.jpg
  5. 266 0
      src/controls.js
  6. 60 0
      src/game.js
  7. 121 0
      src/graphics.js
  8. 87 0
      src/main.js
  9. 38 0
      src/math.js
  10. 47 0
      src/noise.js
  11. 550 0
      src/perlin-noise.js
  12. 187 0
      src/quadtree.js
  13. 76 0
      src/spline.js
  14. 167 0
      src/terrain-chunk.js
  15. 406 0
      src/terrain.js
  16. 58 0
      src/textures.js
  17. 21 0
      src/utils.js

+ 47 - 0
base.css

@@ -0,0 +1,47 @@
+.header {
+  font-size: 3em;
+  color: white;
+  background: #404040;
+  text-align: center;
+  height: 2.5em;
+  text-shadow: 4px 4px 4px black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+#error {
+  font-size: 2em;
+  color: red;
+  height: 50px;
+  text-shadow: 2px 2px 2px black;
+  margin: 2em;
+  display: none;
+}
+
+.container {
+  width: 100% !important;
+  height: 100% !important;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  position: absolute;
+}
+
+.visible {
+  display: block;
+}
+
+#target {
+  width: 100% !important;
+  height: 100% !important;
+  position: absolute;
+}
+
+body {
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Procedural Planet</title>
+  <link rel="stylesheet" type="text/css" href="base.css">
+</head>
+<body>
+  <div id="target"></div>
+  <script src="./src/main.js" type="module">
+  </script>
+</body>
+</html>

BIN
resources/heightmap-hi.png


BIN
resources/heightmap-simondev.jpg


+ 266 - 0
src/controls.js

@@ -0,0 +1,266 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/PointerLockControls.js';
+import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+
+
+export const controls = (function() {
+
+  class _OrbitControls {
+    constructor(params) {
+      this._params = params;
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._controls = new OrbitControls(params.camera, params.domElement);
+      this._controls.target.set(0, 0, 0);
+      this._controls.update();
+    }
+
+    Update() {
+    }
+  }
+
+  // FPSControls was adapted heavily from a threejs example. Movement control
+  // and collision detection was completely rewritten, but credit to original
+  // class for the setup code.
+  class _FPSControls {
+    constructor(params) {
+      this._cells = params.cells;
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._params = params;
+      this._radius = 2;
+      this._enabled = false;
+      this._move = {
+        forward: false,
+        backward: false,
+        left: false,
+        right: false,
+        up: false,
+        down: false,
+      };
+      this._standing = true;
+      this._velocity = new THREE.Vector3(0, 0, 0);
+      this._decceleration = new THREE.Vector3(-10, -10, -10);
+      this._acceleration = new THREE.Vector3(250, 100, 250);
+
+      this._SetupPointerLock();
+
+      this._controls = new PointerLockControls(
+          params.camera, document.body);
+      params.scene.add(this._controls.getObject());
+
+      document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
+      document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
+
+      this._InitGUI();
+    }
+
+    _InitGUI() {
+      this._params.guiParams.camera = {
+        acceleration_x: 250,
+      };
+
+      const rollup = this._params.gui.addFolder('Camera.FPS');
+      rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange(
+        () => {
+          this._acceleration.set(
+            this._params.guiParams.camera.acceleration_x,
+            this._params.guiParams.camera.acceleration_x,
+            this._params.guiParams.camera.acceleration_x);
+        });
+    }
+
+    _onKeyDown(event) {
+      switch (event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this._move.forward = true;
+          break;
+        case 37: // left
+        case 65: // a
+          this._move.left = true;
+          break;
+        case 40: // down
+        case 83: // s
+          this._move.backward = true;
+          break;
+        case 39: // right
+        case 68: // d
+          this._move.right = true;
+          break;
+        case 33: // PG_UP
+          this._move.up = true;
+          break;
+        case 34: // PG_DOWN
+          this._move.down = true;
+          break;
+      }
+    }
+
+    _onKeyUp(event) {
+      switch(event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this._move.forward = false;
+          break;
+        case 37: // left
+        case 65: // a
+          this._move.left = false;
+          break;
+        case 40: // down
+        case 83: // s
+          this._move.backward = false;
+          break;
+        case 39: // right
+        case 68: // d
+          this._move.right = false;
+          break;
+        case 33: // PG_UP
+          this._move.up = false;
+          break;
+        case 34: // PG_DOWN
+          this._move.down = false;
+          break;
+      }
+    }
+
+    _SetupPointerLock() {
+      const hasPointerLock = (
+          'pointerLockElement' in document ||
+          'mozPointerLockElement' in document ||
+          'webkitPointerLockElement' in document);
+      if (hasPointerLock) {
+        const lockChange = (event) => {
+          if (document.pointerLockElement === document.body ||
+              document.mozPointerLockElement === document.body ||
+              document.webkitPointerLockElement === document.body ) {
+            this._enabled = true;
+            this._controls.enabled = true;
+          } else {
+            this._controls.enabled = false;
+          }
+        };
+        const lockError = (event) => {
+          console.log(event);
+        };
+
+        document.addEventListener('pointerlockchange', lockChange, false);
+        document.addEventListener('webkitpointerlockchange', lockChange, false);
+        document.addEventListener('mozpointerlockchange', lockChange, false);
+        document.addEventListener('pointerlockerror', lockError, false);
+        document.addEventListener('mozpointerlockerror', lockError, false);
+        document.addEventListener('webkitpointerlockerror', lockError, false);
+
+        document.getElementById('target').addEventListener('click', (event) => {
+          document.body.requestPointerLock = (
+              document.body.requestPointerLock ||
+              document.body.mozRequestPointerLock ||
+              document.body.webkitRequestPointerLock);
+
+          if (/Firefox/i.test(navigator.userAgent)) {
+            const fullScreenChange = (event) => {
+              if (document.fullscreenElement === document.body ||
+                  document.mozFullscreenElement === document.body ||
+                  document.mozFullScreenElement === document.body) {
+                document.removeEventListener('fullscreenchange', fullScreenChange);
+                document.removeEventListener('mozfullscreenchange', fullScreenChange);
+                document.body.requestPointerLock();
+              }
+            };
+            document.addEventListener(
+                'fullscreenchange', fullScreenChange, false);
+            document.addEventListener(
+                'mozfullscreenchange', fullScreenChange, false);
+            document.body.requestFullscreen = (
+                document.body.requestFullscreen ||
+                document.body.mozRequestFullscreen ||
+                document.body.mozRequestFullScreen ||
+                document.body.webkitRequestFullscreen);
+            document.body.requestFullscreen();
+          } else {
+            document.body.requestPointerLock();
+          }
+        }, false);
+      }
+    }
+
+    _FindIntersections(boxes, position) {
+      const sphere = new THREE.Sphere(position, this._radius);
+
+      const intersections = boxes.filter(b => {
+        return sphere.intersectsBox(b);
+      });
+
+      return intersections;
+    }
+
+    Update(timeInSeconds) {
+      if (!this._enabled) {
+        return;
+      }
+
+      const frameDecceleration = new THREE.Vector3(
+          this._velocity.x * this._decceleration.x,
+          this._velocity.y * this._decceleration.y,
+          this._velocity.z * this._decceleration.z
+      );
+      frameDecceleration.multiplyScalar(timeInSeconds);
+
+      this._velocity.add(frameDecceleration);
+
+      if (this._move.forward) {
+        this._velocity.z -= this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.backward) {
+        this._velocity.z += this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.left) {
+        this._velocity.x -= this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.right) {
+        this._velocity.x += this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.up) {
+        this._velocity.y += this._acceleration.y * timeInSeconds;
+      }
+      if (this._move.down) {
+        this._velocity.y -= this._acceleration.y * timeInSeconds;
+      }
+
+      const controlObject = this._controls.getObject();
+
+      const oldPosition = new THREE.Vector3();
+      oldPosition.copy(controlObject.position);
+
+      const forward = new THREE.Vector3(0, 0, 1);
+      forward.applyQuaternion(controlObject.quaternion);
+      //forward.y = 0;
+      forward.normalize();
+
+      const updown = new THREE.Vector3(0, 1, 0);
+
+      const sideways = new THREE.Vector3(1, 0, 0);
+      sideways.applyQuaternion(controlObject.quaternion);
+      sideways.normalize();
+
+      sideways.multiplyScalar(this._velocity.x * timeInSeconds);
+      updown.multiplyScalar(this._velocity.y * timeInSeconds);
+      forward.multiplyScalar(this._velocity.z * timeInSeconds);
+
+      controlObject.position.add(forward);
+      controlObject.position.add(sideways);
+      controlObject.position.add(updown);
+
+      oldPosition.copy(controlObject.position);
+    }
+  }
+
+  return {
+    FPSControls: _FPSControls,
+    OrbitControls: _OrbitControls,
+  };
+})();

+ 60 - 0
src/game.js

@@ -0,0 +1,60 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {WEBGL} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+import {graphics} from './graphics.js';
+
+
+export const game = (function() {
+  return {
+    Game: class {
+      constructor() {
+        this._Initialize();
+      }
+
+      _Initialize() {
+        this._graphics = new graphics.Graphics(this);
+        if (!this._graphics.Initialize()) {
+          this._DisplayError('WebGL2 is not available.');
+          return;
+        }
+
+        this._previousRAF = null;
+        this._minFrameTime = 1.0 / 10.0;
+        this._entities = {};
+
+        this._OnInitialize();
+        this._RAF();
+      }
+
+      _DisplayError(errorText) {
+        const error = document.getElementById('error');
+        error.innerText = errorText;
+      }
+
+      _RAF() {
+        requestAnimationFrame((t) => {
+          if (this._previousRAF === null) {
+            this._previousRAF = t;
+          }
+          this._Render(t - this._previousRAF);
+          this._previousRAF = t;
+        });
+      }
+
+      _StepEntities(timeInSeconds) {
+        for (let k in this._entities) {
+          this._entities[k].Update(timeInSeconds);
+        }
+      }
+
+      _Render(timeInMS) {
+        const timeInSeconds = Math.min(timeInMS * 0.001, this._minFrameTime);
+
+        this._OnStep(timeInSeconds);
+        this._StepEntities(timeInSeconds);
+        this._graphics.Render(timeInSeconds);
+
+        this._RAF();
+      }
+    }
+  };
+})();

+ 121 - 0
src/graphics.js

@@ -0,0 +1,121 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import Stats from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/stats.module.js';
+import {WEBGL} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+
+
+export const graphics = (function() {
+
+  function _GetImageData(image) {
+    const canvas = document.createElement('canvas');
+    canvas.width = image.width;
+    canvas.height = image.height;
+
+    const context = canvas.getContext( '2d' );
+    context.drawImage(image, 0, 0);
+
+    return context.getImageData(0, 0, image.width, image.height);
+  }
+
+  function _GetPixel(imagedata, x, y) {
+    const position = (x + imagedata.width * y) * 4;
+    const data = imagedata.data;
+    return {
+        r: data[position],
+        g: data[position + 1],
+        b: data[position + 2],
+        a: data[position + 3]
+    };
+  }
+
+  class _Graphics {
+    constructor(game) {
+    }
+
+    Initialize() {
+      if (!WEBGL.isWebGL2Available()) {
+        return false;
+      }
+
+      this._threejs = new THREE.WebGLRenderer({
+          antialias: true,
+      });
+      this._threejs.setPixelRatio(window.devicePixelRatio);
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+
+      const target = document.getElementById('target');
+      target.appendChild(this._threejs.domElement);
+
+      this._stats = new Stats();
+      //target.appendChild(this._stats.dom);
+
+      window.addEventListener('resize', () => {
+        this._OnWindowResize();
+      }, false);
+
+      const fov = 60;
+      const aspect = 1920 / 1080;
+      const near = 1;
+      const far = 100000.0;
+      this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this._camera.position.set(75, 20, 0);
+
+      this._scene = new THREE.Scene();
+      this._scene.background = new THREE.Color(0xaaaaaa);
+
+      this._CreateLights();
+
+      return true;
+    }
+
+    _CreateLights() {
+      let light = new THREE.DirectionalLight(0xFFFFFF, 1, 100);
+      light.position.set(-100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x404040, 1, 100);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x404040, 1, 100);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x101040, 1, 100);
+      light.position.set(100, -100, 100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+    }
+
+    _OnWindowResize() {
+      this._camera.aspect = window.innerWidth / window.innerHeight;
+      this._camera.updateProjectionMatrix();
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+    }
+
+    get Scene() {
+      return this._scene;
+    }
+
+    get Camera() {
+      return this._camera;
+    }
+
+    Render(timeInSeconds) {
+      this._threejs.render(this._scene, this._camera);
+      this._stats.update();
+    }
+  }
+
+  return {
+    Graphics: _Graphics,
+    GetPixel: _GetPixel,
+    GetImageData: _GetImageData,
+  };
+})();

+ 87 - 0
src/main.js

@@ -0,0 +1,87 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {GUI} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/dat.gui.module.js';
+import {controls} from './controls.js';
+import {game} from './game.js';
+import {terrain} from './terrain.js';
+import {textures} from './textures.js';
+
+
+let _APP = null;
+
+
+
+class ProceduralTerrain_Demo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._textures = new textures.TextureAtlas(this);
+    this._textures.onLoad = () => {};
+    this._CreateGUI();
+
+    this._userCamera = new THREE.Object3D();
+    this._userCamera.position.set(4100, 0, 0);
+    this._graphics.Camera.position.set(7000, 7000, 7000);
+
+    this._entities['_terrain'] = new terrain.TerrainChunkManager({
+      camera: this._userCamera,
+      scene: this._graphics.Scene,
+      gui: this._gui,
+      guiParams: this._guiParams,
+    });
+
+    this._entities['_controls'] = new controls.OrbitControls({
+      camera: this._graphics.Camera,
+      scene: this._graphics.Scene,
+      domElement: this._graphics._threejs.domElement,
+      gui: this._gui,
+      guiParams: this._guiParams,
+    });
+
+    this._focusMesh = new THREE.Mesh(
+      new THREE.SphereGeometry(25, 32, 32),
+      new THREE.MeshBasicMaterial({
+          color: 0xFFFFFF
+      }));
+    this._focusMesh.castShadow = true;
+    this._focusMesh.receiveShadow = true;
+    this._graphics.Scene.add(this._focusMesh);
+
+    this._totalTime = 0;
+
+    this._LoadBackground();
+  }
+
+  _CreateGUI() {
+    this._guiParams = {
+      general: {
+      },
+    };
+    this._gui = new GUI();
+
+    const generalRollup = this._gui.addFolder('General');
+    this._gui.close();
+  }
+
+  _LoadBackground() {
+    this._graphics.Scene.background = new THREE.Color(0x000000);
+  }
+
+  _OnStep(timeInSeconds) {
+    this._totalTime += timeInSeconds;
+
+    const x = Math.cos(this._totalTime * 0.025) * 4100;
+    const y = Math.sin(this._totalTime * 0.025) * 4100;
+    this._userCamera.position.set(x, 0, y);
+
+    this._focusMesh.position.copy(this._userCamera.position);
+  }
+}
+
+
+function _Main() {
+  _APP = new ProceduralTerrain_Demo();
+}
+
+_Main();

+ 38 - 0
src/math.js

@@ -0,0 +1,38 @@
+export const math = (function() {
+  return {
+    rand_range: function(a, b) {
+      return Math.random() * (b - a) + a;
+    },
+
+    rand_normalish: function() {
+      const r = Math.random() + Math.random() + Math.random() + Math.random();
+      return (r / 4.0) * 2.0 - 1;
+    },
+
+    rand_int: function(a, b) {
+      return Math.round(Math.random() * (b - a) + a);
+    },
+
+    lerp: function(x, a, b) {
+      return x * (b - a) + a;
+    },
+
+    smoothstep: function(x, a, b) {
+      x = x * x * (3.0 - 2.0 * x);
+      return x * (b - a) + a;
+    },
+
+    smootherstep: function(x, a, b) {
+      x = x * x * x * (x * (x * 6 - 15) + 10);
+      return x * (b - a) + a;
+    },
+
+    clamp: function(x, a, b) {
+      return Math.min(Math.max(x, a), b);
+    },
+
+    sat: function(x) {
+      return Math.min(Math.max(x, 0.0), 1.0);
+    },
+  };
+})();

+ 47 - 0
src/noise.js

@@ -0,0 +1,47 @@
+import 'https://cdn.jsdelivr.net/npm/[email protected]/simplex-noise.js';
+//import perlin from 'https://cdn.jsdelivr.net/gh/mikechambers/es6-perlin-module/perlin.js';
+import perlin from './perlin-noise.js';
+
+import {math} from './math.js';
+
+export const noise = (function() {
+
+  class _NoiseGenerator {
+    constructor(params) {
+      this._params = params;
+      this._Init();
+    }
+
+    _Init() {
+      this._noise = new SimplexNoise(this._params.seed);
+    }
+
+    Get(x, y, z) {
+      const G = 2.0 ** (-this._params.persistence);
+      const xs = x / this._params.scale;
+      const ys = y / this._params.scale;
+      const zs = z / this._params.scale;
+      const noiseFunc = this._noise;
+
+      let amplitude = 1.0;
+      let frequency = 1.0;
+      let normalization = 0;
+      let total = 0;
+      for (let o = 0; o < this._params.octaves; o++) {
+        const noiseValue = noiseFunc.noise3D(
+            xs * frequency, ys * frequency, zs * frequency) * 0.5 + 0.5;
+        total += noiseValue * amplitude;
+        normalization += amplitude;
+        amplitude *= G;
+        frequency *= this._params.lacunarity;
+      }
+      total /= normalization;
+      return Math.pow(
+          total, this._params.exponentiation) * this._params.height;
+    }
+  }
+
+  return {
+    Noise: _NoiseGenerator
+  }
+})();

+ 550 - 0
src/perlin-noise.js

@@ -0,0 +1,550 @@
+// noise1234
+//
+// Author: Stefan Gustavson, 2003-2005
+// Contact: [email protected]
+//
+// This code was GPL licensed until February 2011.
+// As the original author of this code, I hereby
+// release it into the public domain.
+// Please feel free to use it for whatever you want.
+// Credit is appreciated where appropriate, and I also
+// appreciate being told where this code finds any use,
+// but you may do as you like.
+
+//Ported to JavaScript by Mike mikechambers
+//http://www.mikechambers.com
+//
+// Note, all return values are scaled to be between 0 and 1
+//
+//From original C at:
+//https://github.com/stegu/perlin-noise
+//https://github.com/stegu/perlin-noise/blob/master/src/noise1234.c
+
+/*
+ * This implementation is "Improved Noise" as presented by
+ * Ken Perlin at Siggraph 2002. The 3D function is a direct port
+ * of his Java reference code which was once publicly available
+ * on www.noisemachine.com (although I cleaned it up, made it
+ * faster and made the code more readable), but the 1D, 2D and
+ * 4D functions were implemented from scratch by me.
+ *
+ * This is a backport to C of my improved noise class in C++
+ * which was included in the Aqsis renderer project.
+ * It is highly reusable without source code modifications.
+ *
+ */
+
+// This is the new and improved, C(2) continuous interpolant
+function fade(t) {
+	return ( t * t * t * ( t * ( t * 6 - 15 ) + 10 ) );
+}
+
+function lerp(t, a, b) {
+	return ((a) + (t)*((b)-(a)));
+}
+
+
+//---------------------------------------------------------------------
+// Static data
+
+/*
+ * Permutation table. This is just a random jumble of all numbers 0-255,
+ * repeated twice to avoid wrapping the index at 255 for each lookup.
+ * This needs to be exactly the same for all instances on all platforms,
+ * so it's easiest to just keep it as static explicit data.
+ * This also removes the need for any initialisation of this class.
+ *
+ * Note that making this an int[] instead of a char[] might make the
+ * code run faster on platforms with a high penalty for unaligned single
+ * byte addressing. Intel x86 is generally single-byte-friendly, but
+ * some other CPUs are faster with 4-aligned reads.
+ * However, a char[] is smaller, which avoids cache trashing, and that
+ * is probably the most important aspect on most architectures.
+ * This array is accessed a *lot* by the noise functions.
+ * A vector-valued noise over 3D accesses it 96 times, and a
+ * float-valued 4D noise 64 times. We want this to fit in the cache!
+ */
+const perm = [151,160,137,91,90,15,
+  131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+  190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+  88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+  77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+  102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+  135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+  5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+  223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+  129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+  251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+  49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+  138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,
+  151,160,137,91,90,15,
+  131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
+  190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
+  88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
+  77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
+  102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
+  135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
+  5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
+  223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
+  129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
+  251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
+  49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
+  138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
+];
+
+//---------------------------------------------------------------------
+
+/*
+ * Helper functions to compute gradients-dot-residualvectors (1D to 4D)
+ * Note that these generate gradients of more than unit length. To make
+ * a close match with the value range of classic Perlin noise, the final
+ * noise values need to be rescaled. To match the RenderMan noise in a
+ * statistical sense, the approximate scaling values (empirically
+ * determined from test renderings) are:
+ * 1D noise needs rescaling with 0.188
+ * 2D noise needs rescaling with 0.507
+ * 3D noise needs rescaling with 0.936
+ * 4D noise needs rescaling with 0.87
+ */
+
+function grad1( hash, x ) {
+    let h = hash & 15;
+    let grad = 1.0 + (h & 7);  // Gradient value 1.0, 2.0, ..., 8.0
+    if (h&8) grad = -grad;         // and a random sign for the gradient
+    return ( grad * x );           // Multiply the gradient with the distance
+}
+
+function grad2(  hash,  x,  y ) {
+    let h = hash & 7;      // Convert low 3 bits of hash code
+    let u = h<4 ? x : y;  // into 8 simple gradient directions,
+    let v = h<4 ? y : x;  // and compute the dot product with (x,y).
+    return ((h&1)? -u : u) + ((h&2)? -2.0*v : 2.0*v);
+}
+
+function grad3(  hash,  x,  y ,  z ) {
+    let h = hash & 15;     // Convert low 4 bits of hash code into 12 simple
+    let u = h<8 ? x : y; // gradient directions, and compute dot product.
+    let v = h<4 ? y : h==12||h==14 ? x : z; // Fix repeats at h = 12 to 15
+    return ((h&1)? -u : u) + ((h&2)? -v : v);
+}
+
+function grad4(  hash,  x,  y,  z,  t ) {
+    let h = hash & 31;      // Convert low 5 bits of hash code into 32 simple
+    let u = h<24 ? x : y; // gradient directions, and compute dot product.
+    let v = h<16 ? y : z;
+    let w = h<8 ? z : t;
+    return ((h&1)? -u : u) + ((h&2)? -v : v) + ((h&4)? -w : w);
+}
+
+//---------------------------------------------------------------------
+/** 1D float Perlin noise, SL "noise()"
+ */
+export function noise1(  x )
+{
+    let ix0, ix1;
+    let fx0, fx1;
+    let s, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    fx0 = x - ix0;       // Fractional part of x
+    fx1 = fx0 - 1.0;
+    ix1 = ( ix0+1 ) & 0xff;
+    ix0 = ix0 & 0xff;    // Wrap to 0..255
+
+    s = fade( fx0 );
+
+    n0 = grad1( perm[ ix0 ], fx0 );
+    n1 = grad1( perm[ ix1 ], fx1 );
+    return scale(0.188 * ( lerp( s, n0, n1 ) ));
+}
+
+//---------------------------------------------------------------------
+/** 1D float Perlin periodic noise, SL "pnoise()"
+ */
+export function pnoise1(  x,  px )
+{
+    let ix0, ix1;
+    let fx0, fx1;
+    let s, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    fx0 = x - ix0;       // Fractional part of x
+    fx1 = fx0 - 1.0;
+    ix1 = (( ix0 + 1 ) % px) & 0xff; // Wrap to 0..px-1 *and* wrap to 0..255
+    ix0 = ( ix0 % px ) & 0xff;      // (because px might be greater than 256)
+
+    s = fade( fx0 );
+
+    n0 = grad1( perm[ ix0 ], fx0 );
+    n1 = grad1( perm[ ix1 ], fx1 );
+    return scale(0.188 * ( lerp( s, n0, n1 ) ));
+}
+
+
+//---------------------------------------------------------------------
+/** 2D float Perlin noise.
+ */
+export function noise2( x, y )
+{
+    let ix0, iy0, ix1, iy1;
+    let fx0, fy0, fx1, fy1;
+    let s, t, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    ix1 = (ix0 + 1) & 0xff;  // Wrap to 0..255
+    iy1 = (iy0 + 1) & 0xff;
+    ix0 = ix0 & 0xff;
+    iy0 = iy0 & 0xff;
+
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0);
+    nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1);
+    n0 = lerp( t, nx0, nx1 );
+
+    nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0);
+    nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1);
+    n1 = lerp(t, nx0, nx1);
+
+    return scale(0.507 * ( lerp( s, n0, n1 ) ));
+}
+
+//---------------------------------------------------------------------
+/** 2D float Perlin periodic noise.
+ */
+export function pnoise2(  x,  y,  px,  py )
+{
+    let ix0, iy0, ix1, iy1;
+    let fx0, fy0, fx1, fy1;
+    let s, t, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    ix1 = (( ix0 + 1 ) % px) & 0xff;  // Wrap to 0..px-1 and wrap to 0..255
+    iy1 = (( iy0 + 1 ) % py) & 0xff;  // Wrap to 0..py-1 and wrap to 0..255
+    ix0 = ( ix0 % px ) & 0xff;
+    iy0 = ( iy0 % py ) & 0xff;
+
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0);
+    nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1);
+    n0 = lerp( t, nx0, nx1 );
+
+    nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0);
+    nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1);
+    n1 = lerp(t, nx0, nx1);
+
+    return scale(0.507 * ( lerp( s, n0, n1 ) ));
+}
+
+
+//---------------------------------------------------------------------
+/** 3D float Perlin noise.
+ */
+export function noise3(  x,  y,  z )
+{
+    let ix0, iy0, ix1, iy1, iz0, iz1;
+    let fx0, fy0, fz0, fx1, fy1, fz1;
+    let s, t, r;
+    let nxy0, nxy1, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    iz0 = Math.floor( z ); // Integer part of z
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fz0 = z - iz0;        // Fractional part of z
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    fz1 = fz0 - 1.0;
+    ix1 = ( ix0 + 1 ) & 0xff; // Wrap to 0..255
+    iy1 = ( iy0 + 1 ) & 0xff;
+    iz1 = ( iz0 + 1 ) & 0xff;
+    ix0 = ix0 & 0xff;
+    iy0 = iy0 & 0xff;
+    iz0 = iz0 & 0xff;
+
+    r = fade( fz0 );
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0);
+    nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1);
+    nx0 = lerp( r, nxy0, nxy1 );
+
+    nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0);
+    nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1);
+    nx1 = lerp( r, nxy0, nxy1 );
+
+    n0 = lerp( t, nx0, nx1 );
+
+    nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0);
+    nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1);
+    nx0 = lerp( r, nxy0, nxy1 );
+
+    nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0);
+    nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1);
+    nx1 = lerp( r, nxy0, nxy1 );
+
+    n1 = lerp( t, nx0, nx1 );
+
+    return scale(0.936 * ( lerp( s, n0, n1 ) ));
+}
+
+//---------------------------------------------------------------------
+/** 3D float Perlin periodic noise.
+ */
+export function pnoise3(  x,  y,  z,  px,  py,  pz )
+{
+    let ix0, iy0, ix1, iy1, iz0, iz1;
+    let fx0, fy0, fz0, fx1, fy1, fz1;
+    let s, t, r;
+    let nxy0, nxy1, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    iz0 = Math.floor( z ); // Integer part of z
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fz0 = z - iz0;        // Fractional part of z
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    fz1 = fz0 - 1.0;
+    ix1 = (( ix0 + 1 ) % px ) & 0xff; // Wrap to 0..px-1 and wrap to 0..255
+    iy1 = (( iy0 + 1 ) % py ) & 0xff; // Wrap to 0..py-1 and wrap to 0..255
+    iz1 = (( iz0 + 1 ) % pz ) & 0xff; // Wrap to 0..pz-1 and wrap to 0..255
+    ix0 = ( ix0 % px ) & 0xff;
+    iy0 = ( iy0 % py ) & 0xff;
+    iz0 = ( iz0 % pz ) & 0xff;
+
+    r = fade( fz0 );
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0);
+    nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1);
+    nx0 = lerp( r, nxy0, nxy1 );
+
+    nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0);
+    nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1);
+    nx1 = lerp( r, nxy0, nxy1 );
+
+    n0 = lerp( t, nx0, nx1 );
+
+    nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0);
+    nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1);
+    nx0 = lerp( r, nxy0, nxy1 );
+
+    nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0);
+    nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1);
+    nx1 = lerp( r, nxy0, nxy1 );
+
+    n1 = lerp( t, nx0, nx1 );
+
+    return scale(0.936 * ( lerp( s, n0, n1 ) ));
+}
+
+
+//---------------------------------------------------------------------
+/** 4D float Perlin noise.
+ */
+
+export function noise4(  x,  y,  z,  w )
+{
+    let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1;
+    let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1;
+    let s, t, r, q;
+    let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    iz0 = Math.floor( z ); // Integer part of y
+    iw0 = Math.floor( w ); // Integer part of w
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fz0 = z - iz0;        // Fractional part of z
+    fw0 = w - iw0;        // Fractional part of w
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    fz1 = fz0 - 1.0;
+    fw1 = fw0 - 1.0;
+    ix1 = ( ix0 + 1 ) & 0xff;  // Wrap to 0..255
+    iy1 = ( iy0 + 1 ) & 0xff;
+    iz1 = ( iz0 + 1 ) & 0xff;
+    iw1 = ( iw0 + 1 ) & 0xff;
+    ix0 = ix0 & 0xff;
+    iy0 = iy0 & 0xff;
+    iz0 = iz0 & 0xff;
+    iw0 = iw0 & 0xff;
+
+    q = fade( fw0 );
+    r = fade( fz0 );
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx0 = lerp ( r, nxy0, nxy1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx1 = lerp ( r, nxy0, nxy1 );
+
+    n0 = lerp( t, nx0, nx1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx0 = lerp ( r, nxy0, nxy1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx1 = lerp ( r, nxy0, nxy1 );
+
+    n1 = lerp( t, nx0, nx1 );
+
+    return scale(0.87 * ( lerp( s, n0, n1 ) ));
+}
+
+//---------------------------------------------------------------------
+/** 4D float Perlin periodic noise.
+ */
+
+export function pnoise4(  x,  y,  z,  w,
+                             px,  py,  pz,  pw )
+{
+    let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1;
+    let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1;
+    let s, t, r, q;
+    let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1;
+
+    ix0 = Math.floor( x ); // Integer part of x
+    iy0 = Math.floor( y ); // Integer part of y
+    iz0 = Math.floor( z ); // Integer part of y
+    iw0 = Math.floor( w ); // Integer part of w
+    fx0 = x - ix0;        // Fractional part of x
+    fy0 = y - iy0;        // Fractional part of y
+    fz0 = z - iz0;        // Fractional part of z
+    fw0 = w - iw0;        // Fractional part of w
+    fx1 = fx0 - 1.0;
+    fy1 = fy0 - 1.0;
+    fz1 = fz0 - 1.0;
+    fw1 = fw0 - 1.0;
+    ix1 = (( ix0 + 1 ) % px ) & 0xff;  // Wrap to 0..px-1 and wrap to 0..255
+    iy1 = (( iy0 + 1 ) % py ) & 0xff;  // Wrap to 0..py-1 and wrap to 0..255
+    iz1 = (( iz0 + 1 ) % pz ) & 0xff;  // Wrap to 0..pz-1 and wrap to 0..255
+    iw1 = (( iw0 + 1 ) % pw ) & 0xff;  // Wrap to 0..pw-1 and wrap to 0..255
+    ix0 = ( ix0 % px ) & 0xff;
+    iy0 = ( iy0 % py ) & 0xff;
+    iz0 = ( iz0 % pz ) & 0xff;
+    iw0 = ( iw0 % pw ) & 0xff;
+
+    q = fade( fw0 );
+    r = fade( fz0 );
+    t = fade( fy0 );
+    s = fade( fx0 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx0 = lerp ( r, nxy0, nxy1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0);
+    nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx1 = lerp ( r, nxy0, nxy1 );
+
+    n0 = lerp( t, nx0, nx1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx0 = lerp ( r, nxy0, nxy1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1);
+    nxy0 = lerp( q, nxyz0, nxyz1 );
+
+    nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0);
+    nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1);
+    nxy1 = lerp( q, nxyz0, nxyz1 );
+
+    nx1 = lerp ( r, nxy0, nxy1 );
+
+    n1 = lerp( t, nx0, nx1 );
+
+    return scale(0.87 * ( lerp( s, n0, n1 ) ));
+}
+
+function scale(n) {
+	return (1 + n) / 2;
+}
+
+export default function noise(x, y, z, w) {
+
+	switch(arguments.length) {
+		case 1:
+		  return noise1(x); //todo: move these to perlin functions
+		break;
+		case 2:
+		  return noise2(x, y); //todo: move these to perlin functions
+		break;
+		case 3:
+		  return noise3(x, y, z);
+	  case 3:
+		return noise4(x, y, z, w);
+		break;
+	}
+}
+
+//---------------------------------------------------------------------

+ 187 - 0
src/quadtree.js

@@ -0,0 +1,187 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const quadtree = (function() {
+
+  class CubeQuadTree {
+    constructor(params) {
+      this._params = params;
+      this._sides = [];
+
+      const r = params.radius;
+      let m;
+
+      const transforms = [];
+
+      // +Y
+      m = new THREE.Matrix4();
+      m.makeRotationX(-Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, r, 0));
+      transforms.push(m);
+
+      // -Y
+      m = new THREE.Matrix4();
+      m.makeRotationX(Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, -r, 0));
+      transforms.push(m);
+
+      // +X
+      m = new THREE.Matrix4();
+      m.makeRotationY(Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(r, 0, 0));
+      transforms.push(m);
+
+      // -X
+      m = new THREE.Matrix4();
+      m.makeRotationY(-Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(-r, 0, 0));
+      transforms.push(m);
+
+      // +Z
+      m = new THREE.Matrix4();
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r));
+      transforms.push(m);
+      
+      // -Z
+      m = new THREE.Matrix4();
+      m.makeRotationY(Math.PI);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, -r));
+      transforms.push(m);
+
+      for (let t of transforms) {
+        this._sides.push({
+          transform: t.clone(),
+          worldToLocal: t.clone().getInverse(t),
+          quadtree: new QuadTree({
+            size: r,
+            min_node_size: params.min_node_size,
+            localToWorld: t
+          }),
+        });
+      }
+   }
+
+    GetChildren() {
+      const children = [];
+
+      for (let s of this._sides) {
+        const side = {
+          transform: s.transform,
+          children: s.quadtree.GetChildren(),
+        }
+        children.push(side);
+      }
+      return children;
+    }
+
+    Insert(pos) {
+      for (let s of this._sides) {
+        s.quadtree.Insert(pos);
+      }
+    }
+  }
+
+  class QuadTree {
+    constructor(params) {
+      const s = params.size;
+      const b = new THREE.Box3(
+        new THREE.Vector3(-s, -s, 0),
+        new THREE.Vector3(s, s, 0));
+      this._root = {
+        bounds: b,
+        children: [],
+        center: b.getCenter(new THREE.Vector3()),
+        sphereCenter: b.getCenter(new THREE.Vector3()),
+        size: b.getSize(new THREE.Vector3()),
+        root: true,
+      };
+
+      this._params = params;
+      this._root.sphereCenter = this._root.center.clone();
+      this._root.sphereCenter.applyMatrix4(this._params.localToWorld);
+      this._root.sphereCenter.normalize();
+      this._root.sphereCenter.multiplyScalar(this._params.size);
+    }
+
+    GetChildren() {
+      const children = [];
+      this._GetChildren(this._root, children);
+      return children;
+    }
+
+    _GetChildren(node, target) {
+      if (node.children.length == 0) {
+        target.push(node);
+        return;
+      }
+
+      for (let c of node.children) {
+        this._GetChildren(c, target);
+      }
+  }
+
+    Insert(pos) {
+      this._Insert(this._root, pos);
+    }
+
+    _Insert(child, pos) {
+      const distToChild = this._DistanceToChild(child, pos);
+
+      if (distToChild < child.size.x * 1.25 && child.size.x > this._params.min_node_size) {
+        child.children = this._CreateChildren(child);
+
+        for (let c of child.children) {
+          this._Insert(c, pos);
+        }
+      }
+    }
+
+    _DistanceToChild(child, pos) {
+      return child.sphereCenter.distanceTo(pos);
+    }
+
+    _CreateChildren(child) {
+      const midpoint = child.bounds.getCenter(new THREE.Vector3());
+
+      // Bottom left
+      const b1 = new THREE.Box3(child.bounds.min, midpoint);
+
+      // Bottom right
+      const b2 = new THREE.Box3(
+        new THREE.Vector3(midpoint.x, child.bounds.min.y, 0),
+        new THREE.Vector3(child.bounds.max.x, midpoint.y, 0));
+
+      // Top left
+      const b3 = new THREE.Box3(
+        new THREE.Vector3(child.bounds.min.x, midpoint.y, 0),
+        new THREE.Vector3(midpoint.x, child.bounds.max.y, 0));
+
+      // Top right
+      const b4 = new THREE.Box3(midpoint, child.bounds.max);
+
+      const children = [b1, b2, b3, b4].map(
+          b => {
+            return {
+              bounds: b,
+              children: [],
+              center: b.getCenter(new THREE.Vector3()),
+              size: b.getSize(new THREE.Vector3())
+            };
+          });
+
+      for (let c of children) {
+        c.sphereCenter = c.center.clone();
+        c.sphereCenter.applyMatrix4(this._params.localToWorld);
+        c.sphereCenter.normalize()
+        c.sphereCenter.multiplyScalar(this._params.size);
+      }
+
+      return children;
+    }
+  }
+
+  return {
+    QuadTree: QuadTree,
+    CubeQuadTree: CubeQuadTree,
+  }
+})();

+ 76 - 0
src/spline.js

@@ -0,0 +1,76 @@
+export const spline = (function() {
+
+  class _CubicHermiteSpline {
+    constructor(lerp) {
+      this._points = [];
+      this._lerp = lerp;
+    }
+
+    AddPoint(t, d) {
+      this._points.push([t, d]);
+    }
+
+    Get(t) {
+      let p1 = 0;
+
+      for (let i = 0; i < this._points.length; i++) {
+        if (this._points[i][0] >= t) {
+          break;
+        }
+        p1 = i;
+      }
+
+      const p0 = Math.max(0, p1 - 1);
+      const p2 = Math.min(this._points.length - 1, p1 + 1);
+      const p3 = Math.min(this._points.length - 1, p1 + 2);
+
+      if (p1 == p2) {
+        return this._points[p1][1];
+      }
+
+      return this._lerp(
+          (t - this._points[p1][0]) / (
+              this._points[p2][0] - this._points[p1][0]),
+          this._points[p0][1], this._points[p1][1],
+          this._points[p2][1], this._points[p3][1]);
+    }
+  };
+
+  class _LinearSpline {
+    constructor(lerp) {
+      this._points = [];
+      this._lerp = lerp;
+    }
+
+    AddPoint(t, d) {
+      this._points.push([t, d]);
+    }
+
+    Get(t) {
+      let p1 = 0;
+
+      for (let i = 0; i < this._points.length; i++) {
+        if (this._points[i][0] >= t) {
+          break;
+        }
+        p1 = i;
+      }
+
+      const p2 = Math.min(this._points.length - 1, p1 + 1);
+
+      if (p1 == p2) {
+        return this._points[p1][1];
+      }
+
+      return this._lerp(
+          (t - this._points[p1][0]) / (
+              this._points[p2][0] - this._points[p1][0]),
+          this._points[p1][1], this._points[p2][1]);
+    }
+  }
+
+  return {
+    CubicHermiteSpline: _CubicHermiteSpline,
+    LinearSpline: _LinearSpline,
+  };
+})();

+ 167 - 0
src/terrain-chunk.js

@@ -0,0 +1,167 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const terrain_chunk = (function() {
+
+  class TerrainChunk {
+    constructor(params) {
+      this._params = params;
+      this._Init(params);
+    }
+    
+    Destroy() {
+      this._params.group.remove(this._plane);
+    }
+
+    Hide() {
+      this._plane.visible = false;
+    }
+
+    Show() {
+      this._plane.visible = true;
+    }
+
+    _Init(params) {
+      this._geometry = new THREE.BufferGeometry();
+      this._plane = new THREE.Mesh(this._geometry, params.material);
+      this._plane.castShadow = false;
+      this._plane.receiveShadow = true;
+      this._params.group.add(this._plane);
+    }
+
+    _GenerateHeight(v) {
+      return this._params.heightGenerators[0].Get(v.x, v.y, v.z)[0];
+    }
+
+    *_Rebuild() {
+      const _D = new THREE.Vector3();
+      const _D1 = new THREE.Vector3();
+      const _D2 = new THREE.Vector3();
+      const _P = new THREE.Vector3();
+      const _H = new THREE.Vector3();
+      const _W = new THREE.Vector3();
+      const _C = new THREE.Vector3();
+      const _S = new THREE.Vector3();
+
+      const _N = new THREE.Vector3();
+      const _N1 = new THREE.Vector3();
+      const _N2 = new THREE.Vector3();
+      const _N3 = new THREE.Vector3();
+
+      const positions = [];
+      const colors = [];
+      const normals = [];
+      const tangents = [];
+      const uvs = [];
+      const indices = [];
+
+      const localToWorld = this._params.group.matrix;
+      const resolution = this._params.resolution;
+      const radius = this._params.radius;
+      const offset = this._params.offset;
+      const width = this._params.width;
+      const half = width / 2;
+
+      for (let x = 0; x < resolution + 1; x++) {
+        const xp = width * x / resolution;
+        for (let y = 0; y < resolution + 1; y++) {
+          const yp = width * y / resolution;
+
+          // Compute position
+          _P.set(xp - half, yp - half, radius);
+          _P.add(offset);
+          _P.normalize();
+          _D.copy(_P);
+          _P.multiplyScalar(radius);
+          _P.z -= radius;
+
+          // Compute a world space position to sample noise
+          _W.copy(_P);
+          _W.applyMatrix4(localToWorld);
+
+          const height = this._GenerateHeight(_W);
+          const color = this._params.colourGenerator.Get(_W.x, _W.y, height);
+
+          // Purturb height along z-vector
+          _H.copy(_D);
+          _H.multiplyScalar(height);
+          _P.add(_H);
+
+          positions.push(_P.x, _P.y, _P.z);
+          colors.push(color.r, color.g, color.b);
+          normals.push(_D.x, _D.y, _D.z);
+          tangents.push(1, 0, 0, 1);
+          uvs.push(_P.x / 10, _P.y / 10);
+        }
+      }
+      yield;
+
+      for (let i = 0; i < resolution; i++) {
+        for (let j = 0; j < resolution; j++) {
+          indices.push(
+              i * (resolution + 1) + j,
+              (i + 1) * (resolution + 1) + j + 1,
+              i * (resolution + 1) + j + 1);
+          indices.push(
+              (i + 1) * (resolution + 1) + j,
+              (i + 1) * (resolution + 1) + j + 1,
+              i * (resolution + 1) + j);
+        }
+      }
+      yield;
+
+      for (let i = 0, n = indices.length; i < n; i+= 3) {
+        const i1 = indices[i] * 3;
+        const i2 = indices[i+1] * 3;
+        const i3 = indices[i+2] * 3;
+
+        _N1.fromArray(positions, i1);
+        _N2.fromArray(positions, i2);
+        _N3.fromArray(positions, i3);
+
+        _D1.subVectors(_N3, _N2);
+        _D2.subVectors(_N1, _N2);
+        _D1.cross(_D2);
+
+        normals[i1] += _D1.x;
+        normals[i2] += _D1.x;
+        normals[i3] += _D1.x;
+
+        normals[i1+1] += _D1.y;
+        normals[i2+1] += _D1.y;
+        normals[i3+1] += _D1.y;
+
+        normals[i1+2] += _D1.z;
+        normals[i2+2] += _D1.z;
+        normals[i3+2] += _D1.z;
+      }
+      yield;
+
+      for (let i = 0, n = normals.length; i < n; i+=3) {
+        _N.fromArray(normals, i);
+        _N.normalize();
+        normals[i] = _N.x;
+        normals[i+1] = _N.y;
+        normals[i+2] = _N.z;
+      }
+      yield;
+
+      this._geometry.setAttribute(
+          'position', new THREE.Float32BufferAttribute(positions, 3));
+      this._geometry.setAttribute(
+          'color', new THREE.Float32BufferAttribute(colors, 3));
+      this._geometry.setAttribute(
+          'normal', new THREE.Float32BufferAttribute(normals, 3));
+      this._geometry.setAttribute(
+          'tangent', new THREE.Float32BufferAttribute(tangents, 4));
+      this._geometry.setAttribute(
+          'uv', new THREE.Float32BufferAttribute(uvs, 2));
+      this._geometry.setIndex(
+          new THREE.BufferAttribute(new Uint32Array(indices), 1));
+    }
+  }
+
+  return {
+    TerrainChunk: TerrainChunk
+  }
+})();

+ 406 - 0
src/terrain.js

@@ -0,0 +1,406 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {noise} from './noise.js';
+import {quadtree} from './quadtree.js';
+import {spline} from './spline.js';
+import {terrain_chunk} from './terrain-chunk.js';
+import {utils} from './utils.js';
+
+
+export const terrain = (function() {
+
+  const _WHITE = new THREE.Color(0x808080);
+
+  const _DEEP_OCEAN = new THREE.Color(0x20020FF);
+  const _SHALLOW_OCEAN = new THREE.Color(0x8080FF);
+  const _BEACH = new THREE.Color(0xd9d592);
+  const _SNOW = new THREE.Color(0xFFFFFF);
+  const _FOREST_TROPICAL = new THREE.Color(0x4f9f0f);
+  const _FOREST_TEMPERATE = new THREE.Color(0x2b960e);
+  const _FOREST_BOREAL = new THREE.Color(0x29c100);
+  
+  const _GREEN = new THREE.Color(0x80FF80);
+  const _RED = new THREE.Color(0xFF8080);
+  const _BLACK = new THREE.Color(0x000000);
+  
+  const _MIN_CELL_SIZE = 500;
+  const _MIN_CELL_RESOLUTION = 128;
+  const _PLANET_RADIUS = 4000;
+
+
+  class HeightGenerator {
+    constructor(generator, position, minRadius, maxRadius) {
+      this._position = position.clone();
+      this._radius = [minRadius, maxRadius];
+      this._generator = generator;
+    }
+  
+    Get(x, y, z) {
+      return [this._generator.Get(x, y, z), 1];
+    }
+  }
+  
+  
+  class FixedHeightGenerator {
+    constructor() {}
+  
+    Get() {
+      return [50, 1];
+    }
+  }
+   
+  
+  // Cross-blended Hypsometric Tints
+  // http://www.shadedrelief.com/hypso/hypso.html
+  class HyposemetricTints {
+    constructor(params) {
+      const _colourLerp = (t, p0, p1) => {
+        const c = p0.clone();
+  
+        return c.lerp(p1, t);
+      };
+      this._colourSpline = [
+        new spline.LinearSpline(_colourLerp),
+        new spline.LinearSpline(_colourLerp)
+      ];
+
+      // Arid
+      this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d));
+      this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc));
+      this._colourSpline[0].AddPoint(1.0, _SNOW);
+  
+      // Humid
+      this._colourSpline[1].AddPoint(0.0, _FOREST_BOREAL);
+      this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c));
+      this._colourSpline[1].AddPoint(1.0, _SNOW);
+
+      this._oceanSpline = new spline.LinearSpline(_colourLerp);
+      this._oceanSpline.AddPoint(0, _DEEP_OCEAN);
+      this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN);
+      this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN);
+
+      this._params = params;
+    }
+  
+    Get(x, y, z) {
+      const m = this._params.biomeGenerator.Get(x, y, z);
+      const h = z / 100.0;
+  
+      if (h < 0.05) {
+        return this._oceanSpline.Get(h);
+      }
+
+      const c1 = this._colourSpline[0].Get(h);
+      const c2 = this._colourSpline[1].Get(h);
+  
+      return c1.lerp(c2, m);
+    }
+  }
+  
+  
+  class FixedColourGenerator {
+    constructor(params) {
+      this._params = params;
+    }
+  
+    Get() {
+      return this._params.colour;
+    }
+  }
+  
+  
+
+  class TerrainChunkRebuilder {
+    constructor(params) {
+      this._pool = {};
+      this._params = params;
+      this._Reset();
+    }
+
+    AllocateChunk(params) {
+      const w = params.width;
+
+      if (!(w in this._pool)) {
+        this._pool[w] = [];
+      }
+
+      let c = null;
+      if (this._pool[w].length > 0) {
+        c = this._pool[w].pop();
+        c._params = params;
+      } else {
+        c = new terrain_chunk.TerrainChunk(params);
+      }
+
+      c.Hide();
+
+      this._queued.push(c);
+
+      return c;    
+    }
+
+    _RecycleChunks(chunks) {
+      for (let c of chunks) {
+        if (!(c.chunk._params.width in this._pool)) {
+          this._pool[c.chunk._params.width] = [];
+        }
+
+        c.chunk.Destroy();
+      }
+    }
+
+    _Reset() {
+      this._active = null;
+      this._queued = [];
+      this._old = [];
+      this._new = [];
+    }
+
+    get Busy() {
+      return this._active || this._queued.length > 0;
+    }
+
+    Rebuild(chunks) {
+      if (this.Busy) {
+        return;
+      }
+      for (let k in chunks) {
+        this._queued.push(chunks[k].chunk);
+      }
+    }
+
+    Update() {
+      if (this._active) {
+        const r = this._active.next();
+        if (r.done) {
+          this._active = null;
+        }
+      } else {
+        const b = this._queued.pop();
+        if (b) {
+          this._active = b._Rebuild();
+          this._new.push(b);
+        }
+      }
+
+      if (this._active) {
+        return;
+      }
+
+      if (!this._queued.length) {
+        this._RecycleChunks(this._old);
+        for (let b of this._new) {
+          b.Show();
+        }
+        this._Reset();
+      }
+    }
+  }
+
+  class TerrainChunkManager {
+    constructor(params) {
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._params = params;
+
+      this._material = new THREE.MeshStandardMaterial({
+        wireframe: false,
+        wireframeLinewidth: 1,
+        color: 0xFFFFFF,
+        side: THREE.FrontSide,
+        vertexColors: THREE.VertexColors,
+      });
+      this._builder = new TerrainChunkRebuilder();
+
+      this._InitNoise(params);
+      this._InitBiomes(params);
+      this._InitTerrain(params);
+    }
+
+    _InitNoise(params) {
+      params.guiParams.noise = {
+        octaves: 13,
+        persistence: 0.707,
+        lacunarity: 1.8,
+        exponentiation: 4.5,
+        height: 300.0,
+        scale: 1100.0,
+        seed: 1
+      };
+
+      const onNoiseChanged = () => {
+        this._builder.Rebuild(this._chunks);
+      };
+
+      const noiseRollup = params.gui.addFolder('Terrain.Noise');
+      noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "height", 0, 512).onChange(
+          onNoiseChanged);
+
+      this._noise = new noise.Noise(params.guiParams.noise);
+
+      params.guiParams.heightmap = {
+        height: 16,
+      };
+
+      const heightmapRollup = params.gui.addFolder('Terrain.Heightmap');
+      heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange(
+          onNoiseChanged);
+    }
+
+    _InitBiomes(params) {
+      params.guiParams.biomes = {
+        octaves: 2,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 3.9,
+        scale: 2048.0,
+        noiseType: 'simplex',
+        seed: 2,
+        exponentiation: 1,
+        height: 1
+      };
+
+      const onNoiseChanged = () => {
+        this._builder.Rebuild(this._chunks);
+      };
+
+      const noiseRollup = params.gui.addFolder('Terrain.Biomes');
+      noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange(
+          onNoiseChanged);
+
+      this._biomes = new noise.Noise(params.guiParams.biomes);
+    }
+
+    _InitTerrain(params) {
+      params.guiParams.terrain= {
+        wireframe: false,
+      };
+
+      this._groups = [...new Array(6)].map(_ => new THREE.Group());
+      params.scene.add(...this._groups);
+
+      const terrainRollup = params.gui.addFolder('Terrain');
+      terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => {
+        for (let k in this._chunks) {
+          this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe;
+        }
+      });
+
+      this._chunks = {};
+      this._params = params;
+    }
+
+    _CellIndex(p) {
+      const xp = p.x + _MIN_CELL_SIZE * 0.5;
+      const yp = p.z + _MIN_CELL_SIZE * 0.5;
+      const x = Math.floor(xp / _MIN_CELL_SIZE);
+      const z = Math.floor(yp / _MIN_CELL_SIZE);
+      return [x, z];
+    }
+
+    _CreateTerrainChunk(group, offset, width, resolution) {
+      const params = {
+        group: group,
+        material: this._material,
+        width: width,
+        offset: offset,
+        radius: _PLANET_RADIUS,
+        resolution: resolution,
+        biomeGenerator: this._biomes,
+        colourGenerator: new HyposemetricTints({biomeGenerator: this._biomes}),
+        heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)],
+      };
+
+      return this._builder.AllocateChunk(params);
+    }
+
+    Update(_) {
+      this._builder.Update();
+      if (!this._builder.Busy) {
+        this._UpdateVisibleChunks_Quadtree();
+      }
+    }
+
+    _UpdateVisibleChunks_Quadtree() {
+      function _Key(c) {
+        return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']';
+      }
+
+      const q = new quadtree.CubeQuadTree({
+        radius: _PLANET_RADIUS,
+        min_node_size: _MIN_CELL_SIZE,
+      });
+      q.Insert(this._params.camera.position);
+
+      const sides = q.GetChildren();
+
+      let newTerrainChunks = {};
+      const center = new THREE.Vector3();
+      const dimensions = new THREE.Vector3();
+      for (let i = 0; i < sides.length; i++) {
+        this._groups[i].matrix = sides[i].transform;
+        this._groups[i].matrixAutoUpdate = false;
+        for (let c of sides[i].children) {
+          c.bounds.getCenter(center);
+          c.bounds.getSize(dimensions);
+  
+          const child = {
+            index: i,
+            group: this._groups[i],
+            position: [center.x, center.y, center.z],
+            bounds: c.bounds,
+            size: dimensions.x,
+          };
+  
+          const k = _Key(child);
+          newTerrainChunks[k] = child;
+        }
+      }
+
+      const intersection = utils.DictIntersection(this._chunks, newTerrainChunks);
+      const difference = utils.DictDifference(newTerrainChunks, this._chunks);
+      const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks));
+
+      this._builder._old.push(...recycle);
+
+      newTerrainChunks = intersection;
+
+      for (let k in difference) {
+        const [xp, yp, zp] = difference[k].position;
+
+        const offset = new THREE.Vector3(xp, yp, zp);
+        newTerrainChunks[k] = {
+          position: [xp, zp],
+          chunk: this._CreateTerrainChunk(
+              difference[k].group, offset, difference[k].size, _MIN_CELL_RESOLUTION),
+        };
+      }
+
+      this._chunks = newTerrainChunks;
+    }
+  }
+
+  return {
+    TerrainChunkManager: TerrainChunkManager
+  }
+})();

+ 58 - 0
src/textures.js

@@ -0,0 +1,58 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const textures = (function() {
+  return {
+    // Originally I planned to do texture atlasing, then got lazy.
+    TextureAtlas: class {
+      constructor(game) {
+        this._game = game;
+        this._Create(game);
+        this.onLoad = () => {};
+      }
+
+      _Create(game) {
+        this._manager = new THREE.LoadingManager();
+        this._loader = new THREE.TextureLoader(this._manager);
+        this._textures = {};
+
+        this._manager.onLoad = () => {
+          this._OnLoad();
+        };
+
+        this._game = game;
+      }
+
+      get Info() {
+        return this._textures;
+      }
+
+      _OnLoad() {
+        this.onLoad();
+      }
+
+      _LoadType(name, textureNames, offset, colourRange) {
+        this._textures[name] = {
+          colourRange: colourRange,
+          uvOffset: [
+              offset.x,
+              offset.y,
+          ],
+          textures: textureNames.map(n => this._loader.load(n))
+        };
+        if (this._textures[name].textures.length > 1) {
+        } else {
+          const caps = this._game._graphics._threejs.capabilities;
+          const aniso = caps.getMaxAnisotropy();
+
+          this._textures[name].texture = this._textures[name].textures[0];
+          this._textures[name].texture.minFilter = THREE.LinearMipMapLinearFilter;
+          this._textures[name].texture.magFilter = THREE.NearestFilter;
+          this._textures[name].texture.wrapS = THREE.RepeatWrapping;
+          this._textures[name].texture.wrapT = THREE.RepeatWrapping;
+          this._textures[name].texture.anisotropy = aniso;
+        }
+      }
+    }
+  };
+})();

+ 21 - 0
src/utils.js

@@ -0,0 +1,21 @@
+export const utils = (function() {
+  return {
+    DictIntersection: function(dictA, dictB) {
+      const intersection = {};
+      for (let k in dictB) {
+        if (k in dictA) {
+          intersection[k] = dictA[k];
+        }
+      }
+      return intersection
+    },
+
+    DictDifference: function(dictA, dictB) {
+      const diff = {...dictA};
+      for (let k in dictB) {
+        delete diff[k];
+      }
+      return diff;
+    }
+  };
+})();