Simon 4 سال پیش
والد
کامیت
09448f7cf6

+ 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/simplex-noise.png


BIN
resources/space-negx.jpg


BIN
resources/space-negy.jpg


BIN
resources/space-negz.jpg


BIN
resources/space-posx.jpg


BIN
resources/space-posy.jpg


BIN
resources/space-posz.jpg


+ 39 - 0
src/camera-track.js

@@ -0,0 +1,39 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {spline} from './spline.js';
+
+
+export const camera_track = (function() {
+
+  class _CameraTrack {
+    constructor(params) {
+      this._params = params;
+      this._currentTime = 0.0;
+      
+      const lerp = (t, p1, p2) => {
+        const p = new THREE.Vector3().lerpVectors(p1.pos, p2.pos, t);
+        const q = p1.rot.clone().slerp(p2.rot, t);
+
+        return {pos: p, rot: q};
+      };
+      this._spline = new spline.LinearSpline(lerp);
+
+      for (let p of params.points) {
+        this._spline.AddPoint(p.time, p.data);
+      }
+    }
+
+    Update(timeInSeconds) {
+      this._currentTime += timeInSeconds;
+
+      const r = this._spline.Get(this._currentTime);
+
+      this._params.camera.position.copy(r.pos);
+      this._params.camera.quaternion.copy(r.rot);
+    }
+  };
+
+  return {
+    CameraTrack: _CameraTrack,
+  };
+})();

+ 448 - 0
src/controls.js

@@ -0,0 +1,448 @@
+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(12, 12, 12);
+
+      this._SetupPointerLock();
+
+      this._controls = new PointerLockControls(
+          params.camera, document.body);
+      params.scene.add(this._controls.getObject());
+
+      const controlObject = this._controls.getObject();
+      this._position = new THREE.Vector3();
+      this._rotation = new THREE.Quaternion();
+      this._position.copy(controlObject.position);
+      this._rotation.copy(controlObject.quaternion);
+
+      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: 12,
+      };
+
+      const rollup = this._params.gui.addFolder('Camera.FPS');
+      rollup.add(this._params.guiParams.camera, "acceleration_x", 4.0, 24.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 -= 2 ** this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.backward) {
+        this._velocity.z += 2 ** this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.left) {
+        this._velocity.x -= 2 ** this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.right) {
+        this._velocity.x += 2 ** this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.up) {
+        this._velocity.y += 2 ** this._acceleration.y * timeInSeconds;
+      }
+      if (this._move.down) {
+        this._velocity.y -= 2 ** 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.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);
+
+      // this._position.lerp(controlObject.position, 0.15);
+      this._rotation.slerp(controlObject.quaternion, 0.15);
+
+      // controlObject.position.copy(this._position);
+      controlObject.quaternion.copy(this._rotation);
+    }
+  };
+
+  class _ShipControls {
+    constructor(params) {
+      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,
+        rocket: false,
+      };
+      this._velocity = new THREE.Vector3(0, 0, 0);
+      this._decceleration = new THREE.Vector3(-0.001, -0.0001, -1);
+      this._acceleration = new THREE.Vector3(100, 0.1, 25000);
+
+      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: 100,
+        acceleration_y: 0.1,
+      };
+
+      const rollup = this._params.gui.addFolder('Camera.Ship');
+      rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange(
+        () => {
+          this._acceleration.x = this._params.guiParams.camera.acceleration_x;
+        });
+      rollup.add(this._params.guiParams.camera, "acceleration_y", 0.001, 0.1).onChange(
+        () => {
+          this._acceleration.y = this._params.guiParams.camera.acceleration_y;
+        });
+    }
+
+    _onKeyDown(event) {
+      switch (event.keyCode) {
+        case 87: // w
+          this._move.forward = true;
+          break;
+        case 65: // a
+          this._move.left = true;
+          break;
+        case 83: // s
+          this._move.backward = true;
+          break;
+        case 68: // d
+          this._move.right = true;
+          break;
+        case 33: // PG_UP
+          this._acceleration.x *= 1.1;
+          break;
+        case 34: // PG_DOWN
+          this._acceleration.x *= 0.8;
+          break;
+        case 32: // SPACE
+          this._move.rocket = true;
+          break;
+        case 38: // up
+        case 37: // left
+        case 40: // down
+        case 39: // right
+          break;
+      }
+    }
+
+    _onKeyUp(event) {
+      switch(event.keyCode) {
+        case 87: // w
+          this._move.forward = false;
+          break;
+        case 65: // a
+          this._move.left = false;
+          break;
+        case 83: // s
+          this._move.backward = false;
+          break;
+        case 68: // d
+          this._move.right = false;
+          break;
+        case 33: // PG_UP
+          break;
+        case 34: // PG_DOWN
+          break;
+        case 32: // SPACE
+          this._move.rocket = false;
+          break;
+        case 38: // up
+        case 37: // left
+        case 40: // down
+        case 39: // right
+          break;
+      }
+    }
+
+    Update(timeInSeconds) {
+      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);
+
+      const controlObject = this._params.camera;
+      const _Q = new THREE.Quaternion();
+      const _A = new THREE.Vector3();
+      const _R = controlObject.quaternion.clone();
+
+      if (this._move.forward) {
+        _A.set(1, 0, 0);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.backward) {
+        _A.set(1, 0, 0);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.left) {
+        _A.set(0, 0, 1);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.right) {
+        _A.set(0, 0, 1);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.rocket) {
+        this._velocity.z -= this._acceleration.x * timeInSeconds;
+      }
+
+      controlObject.quaternion.copy(_R);
+
+      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 {
+    ShipControls: _ShipControls,
+    FPSControls: _FPSControls,
+    OrbitControls: _OrbitControls,
+  };
+})();

+ 82 - 0
src/demo.js

@@ -0,0 +1,82 @@
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {noise} from './noise.js';
+
+
+window.onload = function() {
+  function _Perlin() {
+    const canvas = document.getElementById("canvas"); 
+    const context = canvas.getContext("2d");
+  
+    const imgData = context.createImageData(canvas.width, canvas.height);
+  
+    const params = {
+      scale: 32,
+      noiseType: 'simplex',
+      persistence: 0.5,
+      octaves: 1,
+      lacunarity: 1,
+      exponentiation: 1,
+      height: 255
+    };
+    const noiseGen = new noise.Noise(params);
+
+    for (let x = 0; x < canvas.width; x++) {
+      for (let y = 0; y < canvas.height; y++) {
+        const pixelIndex = (y * canvas.width + x) * 4;
+
+        const n = noiseGen.Get(x, y);
+
+        imgData.data[pixelIndex] = n;
+        imgData.data[pixelIndex+1] = n;
+        imgData.data[pixelIndex+2] = n;
+        imgData.data[pixelIndex+3] = 255;
+      }
+    }
+  
+    context.putImageData(imgData, 0, 0);
+}
+
+
+function _Randomness() {
+  const canvas = document.getElementById("canvas"); 
+  const context = canvas.getContext("2d");
+
+  const imgData = context.createImageData(canvas.width, canvas.height);
+
+  const params = {
+    scale: 32,
+    noiseType: 'simplex',
+    persistence: 0.5,
+    octaves: 1,
+    lacunarity: 2,
+    exponentiation: 1,
+    height: 1
+  };
+  const noiseGen = new noise.Noise(params);
+  let foo = '';
+
+  for (let x = 0; x < canvas.width; x++) {
+    for (let y = 0; y < canvas.height; y++) {
+      const pixelIndex = (y * canvas.width + x) * 4;
+
+      const n = noiseGen.Get(x, y);
+      if (x == 0) {
+        foo += n + '\n';
+      }
+
+      imgData.data[pixelIndex] = n;
+      imgData.data[pixelIndex+1] = n;
+      imgData.data[pixelIndex+2] = n;
+      imgData.data[pixelIndex+3] = 255;
+    }
+  }
+  console.log(foo);
+
+  context.putImageData(imgData, 0, 0);
+}
+
+_Randomness();
+  
+};

+ 70 - 0
src/game.js

@@ -0,0 +1,70 @@
+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;
+        });
+      }
+
+      _AddEntity(name, entity, priority) {
+        this._entities[name] = {entity: entity, priority: priority};
+      }
+
+      _StepEntities(timeInSeconds) {
+        const sortedEntities = Object.values(this._entities);
+
+        sortedEntities.sort((a, b) => {
+          return a.priority - b.priority;
+        })
+
+        for (let s of sortedEntities) {
+          s.entity.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();
+      }
+    }
+  };
+})();

+ 220 - 0
src/graphics.js

@@ -0,0 +1,220 @@
+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';
+
+import {RenderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/RenderPass.js';
+import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/ShaderPass.js';
+import {CopyShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/CopyShader.js';
+import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/FXAAShader.js';
+import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/EffectComposer.js';
+
+import {scattering_shader} from './scattering-shader.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;
+      }
+
+      const canvas = document.createElement('canvas');
+      const context = canvas.getContext('webgl2', {alpha: false});
+
+      this._threejs = new THREE.WebGLRenderer({
+        canvas: canvas,
+        context: context,
+        antialias: false,
+      });
+      this._threejs.outputEncoding = THREE.LinearEncoding;
+      this._threejs.setPixelRatio(window.devicePixelRatio);
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+      this._threejs.autoClear = false;
+
+      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 = 0.1;
+      const far = 10000000.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);
+
+      const renderPass = new RenderPass(this.scene_, this.camera_);
+      const fxaaPass = new ShaderPass(FXAAShader);
+      // const depthPass = new ShaderPass(scattering_shader.Shader);
+
+      // this._depthPass = depthPass;
+
+      this.composer_ = new EffectComposer(this._threejs);
+      this.composer_.addPass(renderPass);
+      this.composer_.addPass(fxaaPass);
+      //this.composer_.addPass(depthPass);
+
+      const params = {
+        minFilter: THREE.NearestFilter,
+        magFilter: THREE.NearestFilter,
+        format: THREE.RGBAFormat,
+        type: THREE.FloatType,
+        generateMipmaps: false,
+      };
+
+      this._target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, params);
+      this._target.stencilBuffer = false;
+      this._target.depthBuffer = true;
+      this._target.depthTexture = new THREE.DepthTexture();
+      this._target.depthTexture.format = THREE.DepthFormat;
+      this._target.depthTexture.type = THREE.FloatType;
+      this._target.outputEncoding = THREE.LinearEncoding;
+
+      this._threejs.setRenderTarget(this._target);
+
+      const logDepthBufFC = 2.0 / ( Math.log(this.camera_.far + 1.0) / Math.LN2);
+
+      this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+      this._depthPass = new THREE.ShaderMaterial( {
+        vertexShader: scattering_shader.VS,
+        fragmentShader: scattering_shader.PS,
+        uniforms: {
+          cameraNear: { value: this.Camera.near },
+          cameraFar: { value: this.Camera.far },
+          cameraPosition: { value: this.Camera.position },
+          cameraForward: { value: null },
+          tDiffuse: { value: null },
+          tDepth: { value: null },
+          inverseProjection: { value: null },
+          inverseView: { value: null },
+          planetPosition: { value: null },
+          planetRadius: { value: null },
+          atmosphereRadius: { value: null },
+          logDepthBufFC: { value: logDepthBufFC },
+        }
+      } );
+      var postPlane = new THREE.PlaneBufferGeometry( 2, 2 );
+      var postQuad = new THREE.Mesh( postPlane, this._depthPass );
+      this._postScene = new THREE.Scene();
+      this._postScene.add( postQuad );
+
+      this._CreateLights();
+
+      return true;
+    }
+
+
+    _CreateLights() {
+      let light = new THREE.DirectionalLight(0xFFFFFF, 1);
+      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);
+      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);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this.scene_.add(light);
+
+      light = new THREE.DirectionalLight(0x202040, 1);
+      light.position.set(100, -100, 100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this.scene_.add(light);
+
+      light = new THREE.AmbientLight(0xFFFFFF, 1.0);
+      this.scene_.add(light);
+    }
+
+    _OnWindowResize() {
+      this.camera_.aspect = window.innerWidth / window.innerHeight;
+      this.camera_.updateProjectionMatrix();
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+      this.composer_.setSize(window.innerWidth, window.innerHeight);
+      this._target.setSize(window.innerWidth, window.innerHeight);
+    }
+
+    get Scene() {
+      return this.scene_;
+    }
+
+    get Camera() {
+      return this.camera_;
+    }
+
+    Render(timeInSeconds) {
+      this._threejs.setRenderTarget(this._target);
+
+      this._threejs.clear();
+      this._threejs.render(this.scene_, this.camera_);
+      //this.composer_.render();
+
+      this._threejs.setRenderTarget( null );
+
+      const forward = new THREE.Vector3();
+      this.camera_.getWorldDirection(forward);
+
+      this._depthPass.uniforms.inverseProjection.value = this.camera_.projectionMatrixInverse;
+      this._depthPass.uniforms.inverseView.value = this.camera_.matrixWorld;
+      this._depthPass.uniforms.tDiffuse.value = this._target.texture;
+      this._depthPass.uniforms.tDepth.value = this._target.depthTexture;
+      this._depthPass.uniforms.cameraNear.value = this.camera_.near;
+      this._depthPass.uniforms.cameraFar.value = this.camera_.far;
+      this._depthPass.uniforms.cameraPosition.value = this.camera_.position;
+      this._depthPass.uniforms.cameraForward.value = forward;
+      this._depthPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0);
+      this._depthPass.uniformsNeedUpdate = true;
+
+      this._threejs.render( this._postScene, this._postCamera );
+
+      this._stats.update();
+    }
+  }
+
+  return {
+    Graphics: _Graphics,
+    GetPixel: _GetPixel,
+    GetImageData: _GetImageData,
+  };
+})();

+ 81 - 0
src/main.js

@@ -0,0 +1,81 @@
+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';
+
+let _APP = null;
+
+
+class ProceduralTerrain_Demo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._CreateGUI();
+
+    this.graphics_.Camera.position.set(355898.9978932907, -16169.249553939484, -181920.2108868533);
+    this.graphics_.Camera.quaternion.set(0.3525209450519473, 0.6189868049149101, -0.58773147927222, 0.38360921119467495);
+    // this.graphics_.Camera.position.set(283679.0800079606, -314104.8959314113, -71872.13040166264);
+    // this.graphics_.Camera.quaternion.set(0.4461797373759622, 0.5541566632843257, -0.523697972824766, 0.46858773751767197);
+
+    this.graphics_.Camera.position.set(357183.28155512916, -19402.113225302386, -182320.80530987142);
+    this.graphics_.Camera.quaternion.set(0.2511776691104541, 0.6998229958650649, -0.48248862753627253, 0.46299274000447177);
+
+    this._AddEntity('_terrain', new terrain.TerrainChunkManager({
+        camera: this.graphics_.Camera,
+        scene: this.graphics_.Scene,
+        scattering: this.graphics_._depthPass,
+        gui: this._gui,
+        guiParams: this._guiParams,
+        game: this}), 1.0);
+
+    this._AddEntity('_controls', new controls.FPSControls({
+        camera: this.graphics_.Camera,
+        scene: this.graphics_.Scene,
+        domElement: this.graphics_._threejs.domElement,
+        gui: this._gui,
+        guiParams: this._guiParams}), 0.0);
+
+    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);
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/space-posx.jpg',
+        './resources/space-negx.jpg',
+        './resources/space-posy.jpg',
+        './resources/space-negy.jpg',
+        './resources/space-posz.jpg',
+        './resources/space-negz.jpg',
+    ]);
+    texture.encoding = THREE.sRGBEncoding;
+    this.graphics_.Scene.background = texture;
+  }
+
+  _OnStep(timeInSeconds) {
+  }
+}
+
+
+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);
+    },
+  };
+})();

+ 44 - 0
src/noise.js

@@ -0,0 +1,44 @@
+import {simplex} from './simplex-noise.js';
+
+export const noise = (function() {
+
+  class _NoiseGenerator {
+    constructor(params) {
+      this._params = params;
+      this._Init();
+    }
+
+    _Init() {
+      this._noise = new simplex.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
+  }
+})();

+ 442 - 0
src/quadtree.js

@@ -0,0 +1,442 @@
+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 0
+      m = new THREE.Matrix4();
+      m.makeRotationX(-Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, r, 0));
+      transforms.push(m);
+
+      // -Y 1
+      m = new THREE.Matrix4();
+      m.makeRotationX(Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, -r, 0));
+      transforms.push(m);
+
+      // +X 2
+      m = new THREE.Matrix4();
+      m.makeRotationY(Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(r, 0, 0));
+      transforms.push(m);
+
+      // -X 3
+      m = new THREE.Matrix4();
+      m.makeRotationY(-Math.PI / 2);
+      m.premultiply(new THREE.Matrix4().makeTranslation(-r, 0, 0));
+      transforms.push(m);
+
+      // +Z 4
+      m = new THREE.Matrix4();
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r));
+      transforms.push(m);
+      
+      // -Z 5
+      m = new THREE.Matrix4();
+      m.makeRotationY(Math.PI);
+      m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, -r));
+      transforms.push(m);
+
+      for (let i = 0; i < transforms.length; ++i) {
+        const t = transforms[i];
+        this.sides_.push({
+          transform: t.clone(),
+          quadtree: new QuadTree({
+            side: i,
+            size: r,
+            min_node_size: params.min_node_size,
+            max_node_size: params.max_node_size,
+            localToWorld: t,
+            worldToLocal: t.clone().invert()
+          }),
+        });
+      }
+
+      this.BuildRootNeighbourInfo_();
+    }
+
+    BuildRootNeighbourInfo_() {
+      const _FindClosestNeighbour = (edgeMidpoint, otherNodes) => {
+        const neighbours = [...otherNodes].sort((a, b) => {
+          return a.sphereCenter.distanceTo(edgeMidpoint) - b.sphereCenter.distanceTo(edgeMidpoint);
+        });
+        const test = [...otherNodes].map(c => {
+          return c.sphereCenter.distanceTo(edgeMidpoint);
+        }).sort((a, b) => a - b);
+        return neighbours[0];
+      };
+
+      const nodes = this.sides_.map(s => s.quadtree.root_);
+
+      for (let i = 0; i < 6; ++i) {
+        const node = nodes[i];
+        const edgeMidpoints = [
+          node.GetLeftEdgeMidpoint(),
+          node.GetTopEdgeMidpoint(),
+          node.GetRightEdgeMidpoint(),
+          node.GetBottomEdgeMidpoint(),
+        ];
+        const otherNodes = nodes.filter(n => n.side != node.side);
+
+        const neighbours = edgeMidpoints.map(p => _FindClosestNeighbour(p, otherNodes));
+        node.neighbours = neighbours.map(n => nodes[n.side]);
+      }
+    }
+
+    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);
+      }
+    }
+
+    BuildNeighbours() {
+      let queue = [];
+      for (let s of this.sides_) {
+        queue.push(s.quadtree.root_);
+      }
+
+      while (queue.length > 0) {
+        const node = queue.shift();
+
+        this.sides_[node.side].quadtree.BuildNeighbours_Child_(node);
+
+        for (let c of node.children) {
+          queue.push(c);
+        }
+      }
+    }
+  }
+
+  const LEFT = 0;
+  const TOP = 1;
+  const RIGHT = 2;
+  const BOTTOM = 3;
+
+  const TOP_LEFT = 2;
+  const TOP_RIGHT = 3;
+  const BOTTOM_LEFT = 0;
+  const BOTTOM_RIGHT = 1;
+
+  class Node {
+    constructor(params) {
+    }
+
+    GetNeighbour(side) {
+      return this.neighbours[side];
+    }
+
+    GetClosestChild(node) {
+      const children = [...this.children].sort((a, b) => {
+        return a.sphereCenter.distanceTo(node.sphereCenter) - b.sphereCenter.distanceTo(node.sphereCenter);
+      });
+      const test = [...this.children].map(c => {
+        return c.sphereCenter.distanceTo(node.sphereCenter);
+      }).sort((a, b) => a - b);
+      return children[0];
+    }
+
+    GetChild(pos) {
+      return this.children[pos];
+    }
+
+    GetClosestChildrenSharingEdge(edgePoint) {
+      if (this.children.length == 0) {
+        const edgePointLocal = edgePoint.clone().applyMatrix4(this.tree.worldToLocal);
+        if (edgePointLocal.x == this.bounds.min.x || edgePointLocal.x == this.bounds.max.x ||
+            edgePointLocal.y == this.bounds.min.y || edgePointLocal.y == this.bounds.max.y) {
+          return [this];
+        }
+        return [];
+      }
+
+      const matches = [];
+      for (let i = 0; i < this.children.length; ++i) {
+        const child = this.children[i];
+
+        matches.push(...child.GetClosestChildrenSharingEdge(edgePoint));
+      }
+      return matches;
+    }
+
+    GetLeftEdgeMidpoint() {
+      const v = new THREE.Vector3(this.bounds.min.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0);
+      v.applyMatrix4(this.localToWorld);
+      return v;
+    }
+
+    GetRightEdgeMidpoint() {
+      const v = new THREE.Vector3(this.bounds.max.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0);
+      v.applyMatrix4(this.localToWorld);
+      return v;
+    }
+
+    GetTopEdgeMidpoint() {
+      const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.max.y, 0);
+      v.applyMatrix4(this.localToWorld);
+      return v;
+    }
+
+    GetBottomEdgeMidpoint() {
+      const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.min.y, 0);
+      v.applyMatrix4(this.localToWorld);
+      return v;
+    }
+  };
+
+  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_ = new Node();
+      this.root_.side = params.side;
+      this.root_.bounds = b;
+      this.root_.children = [];
+      this.root_.parent = null;
+      this.root_.tree = this;
+      this.root_.center = b.getCenter(new THREE.Vector3());
+      this.root_.sphereCenter = b.getCenter(new THREE.Vector3());
+      this.root_.localToWorld = params.localToWorld;
+      this.root_.size = b.getSize(new THREE.Vector3());
+      this.root_.root = true;
+      this.root_.neighbours = [null, null, null, null];
+
+      this._params = params;
+      this.worldToLocal = params.worldToLocal;
+      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);
+      }
+    }
+
+    BuildNeighbours_Child_(node) {      
+      const children = node.children;
+      if (children.length == 0) {
+        const hx = (node.bounds.max.x + node.bounds.min.x) * 0.5;
+        const hy = (node.bounds.max.y + node.bounds.min.y) * 0.5;
+        const nx = node.bounds.min.x;
+        const ny = node.bounds.min.y;
+        const px = node.bounds.max.x;
+        const py = node.bounds.max.y;
+        const b1 = new THREE.Vector3(nx, hy, 0);
+        const b2 = new THREE.Vector3(hx, py, 0);
+        const b3 = new THREE.Vector3(px, hy, 0);
+        const b4 = new THREE.Vector3(hx, ny, 0);
+
+        return;
+      }
+
+      if (node.center.x == 375000 && node.center.y == -125000 && node.side == 1 && node.size.x == 50000) {
+        let a = 0;
+      }
+      if (node.root && node.side == 1) {
+        let a = 0;
+      }
+      if (node.center.x == 200000 && node.center.y == -200000 && node.side == 1) {
+        let a =0;
+      }
+       // Bottom left
+      let leftNeighbour = node.GetNeighbour(LEFT);
+      if (leftNeighbour.children.length > 0) {
+        if (leftNeighbour.side != node.side) {
+          leftNeighbour = leftNeighbour.GetClosestChild(children[0]);
+        } else {
+          leftNeighbour = leftNeighbour.GetChild(BOTTOM_RIGHT);
+        }
+      }
+
+      let bottomNeighbour = node.GetNeighbour(BOTTOM);
+      if (bottomNeighbour.children.length > 0) {
+        if (bottomNeighbour.side != node.side) {
+          bottomNeighbour = bottomNeighbour.GetClosestChild(children[0]);
+        } else {
+          bottomNeighbour = bottomNeighbour.GetChild(TOP_LEFT);
+        }
+      }
+      children[0].neighbours = [leftNeighbour, children[TOP_LEFT], children[BOTTOM_RIGHT], bottomNeighbour];
+
+      // Bottom right
+      let rightNeighbour = node.GetNeighbour(RIGHT);
+      if (rightNeighbour.children.length > 0) {
+        if (rightNeighbour.side != node.side) {
+          rightNeighbour = rightNeighbour.GetClosestChild(children[1]);
+        } else {
+          rightNeighbour = rightNeighbour.GetChild(BOTTOM_LEFT);
+        }
+      }
+
+      bottomNeighbour = node.GetNeighbour(BOTTOM);
+      if (bottomNeighbour.children.length > 0) {
+        if (bottomNeighbour.side != node.side) {
+          bottomNeighbour = bottomNeighbour.GetClosestChild(children[1]);
+        } else {
+          bottomNeighbour = bottomNeighbour.GetChild(TOP_RIGHT);
+        }
+      }
+      children[1].neighbours = [children[BOTTOM_LEFT], children[TOP_RIGHT], rightNeighbour, bottomNeighbour];
+
+      // Top left
+      leftNeighbour = node.GetNeighbour(LEFT);
+      if (leftNeighbour.children.length > 0) {
+        if (leftNeighbour.side != node.side) {
+          leftNeighbour = leftNeighbour.GetClosestChild(children[2]);
+        } else {
+          leftNeighbour = leftNeighbour.GetChild(TOP_RIGHT);
+        }
+      }
+
+      let topNeighbour = node.GetNeighbour(TOP);
+      if (topNeighbour.children.length > 0) {
+        if (topNeighbour.side != node.side) {
+          topNeighbour = topNeighbour.GetClosestChild(children[2]);
+        } else {
+          topNeighbour = topNeighbour.GetChild(BOTTOM_LEFT);
+        }
+      }
+      children[2].neighbours = [leftNeighbour, topNeighbour, children[TOP_RIGHT], children[BOTTOM_LEFT]];
+
+      // Top right
+      topNeighbour = node.GetNeighbour(TOP);
+      if (topNeighbour.children.length > 0) {
+        if (topNeighbour.side != node.side) {
+          topNeighbour = topNeighbour.GetClosestChild(children[3]);
+        } else {
+          topNeighbour = topNeighbour.GetChild(BOTTOM_RIGHT);
+        }
+      }
+
+      rightNeighbour = node.GetNeighbour(RIGHT);
+      if (rightNeighbour.children.length > 0) {
+        if (rightNeighbour.side != node.side) {
+          rightNeighbour = rightNeighbour.GetClosestChild(children[3]);
+        } else {
+          rightNeighbour = rightNeighbour.GetChild(TOP_LEFT);
+        }
+      }
+      children[3].neighbours = [children[TOP_LEFT], topNeighbour, rightNeighbour, children[BOTTOM_RIGHT]];
+    }
+
+    Insert(pos) {
+      this._Insert(this.root_, pos);
+    }
+
+    _Insert(child, pos) {
+      // hack
+      const distToChild = this._DistanceToChild(child, pos);
+
+      if ((distToChild < child.size.x * 1.0 && 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 {
+              side: child.side,
+              bounds: b,
+              children: [],
+              parent: child,
+              center: b.getCenter(new THREE.Vector3()),
+              size: b.getSize(new THREE.Vector3())
+            };
+          });
+
+      const nodes = [];
+      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);
+
+        const n = new Node();
+        n.side = child.side;
+        n.bounds = c.bounds;
+        n.children = [];
+        n.parent = child;
+        n.tree = this;
+        n.center = c.center;
+        n.sphereCenter = c.sphereCenter;
+        n.size = c.size;
+        n.localToWorld = child.localToWorld;
+        n.neighbours = [null, null, null, null];
+        nodes.push(n);
+      }
+
+      return nodes;
+    }
+  }
+
+  return {
+    QuadTree: QuadTree,
+    CubeQuadTree: CubeQuadTree,
+  }
+})();

+ 468 - 0
src/scattering-shader.js

@@ -0,0 +1,468 @@
+
+export const scattering_shader = (function() {
+
+  const _VS = `
+
+  #define saturate(a) clamp( a, 0.0, 1.0 )
+
+  out vec2 vUv;
+
+  void main() {
+    vUv = uv;
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+  }
+  `;
+  
+
+  const _PS = `
+  #include <packing>
+
+  #define saturate(a) clamp( a, 0.0, 1.0 )
+
+  #define PI 3.141592
+  #define PRIMARY_STEP_COUNT 16
+  #define LIGHT_STEP_COUNT 8
+
+
+  in vec2 vUv;
+  
+  uniform sampler2D tDiffuse;
+  uniform sampler2D tDepth;
+  uniform float cameraNear;
+  uniform float cameraFar;
+  uniform vec3 cameraForward;
+  uniform mat4 inverseProjection;
+  uniform mat4 inverseView;
+
+  uniform vec3 planetPosition;
+  uniform float planetRadius;
+  uniform float atmosphereRadius;
+  
+  uniform float logDepthBufFC;
+
+  vec3 _ScreenToWorld(vec3 posS) {
+
+    float depthValue = posS.z;
+    float v_depth = pow(2.0, depthValue / (logDepthBufFC * 0.5));
+    float z_view = v_depth - 1.0;
+
+    vec4 posCLIP = vec4(posS.xy * 2.0 - 1.0, 0.0, 1.0);
+    vec4 posVS = inverseProjection * posCLIP;
+
+    posVS = vec4(posVS.xyz / posVS.w, 1.0);
+    posVS.xyz = normalize(posVS.xyz) * z_view;
+
+    vec4 posWS = inverseView * posVS;
+
+    return posWS.xyz;
+  }
+
+  vec3 _ScreenToWorld_Normal(vec3 pos) {
+    vec3 posS = pos;
+
+    vec4 posP = vec4(posS.xyz * 2.0 - 1.0, 1.0);
+    vec4 posVS = inverseProjection * posP;
+    
+    posVS = vec4((posVS.xyz / posVS.w), 1.0);
+    vec4 posWS = inverseView * posVS;
+    
+    return posWS.xyz;
+  }
+
+  // source: https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs
+  vec3 RRTAndODTFit( vec3 v ) {
+    vec3 a = v * ( v + 0.0245786 ) - 0.000090537;
+    vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;
+    return a / b;
+  }
+  // this implementation of ACES is modified to accommodate a brighter viewing environment.
+  // the scale factor of 1/0.6 is subjective. see discussion in #19621.
+  vec3 ACESFilmicToneMapping( vec3 color ) {
+    // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
+    const mat3 ACESInputMat = mat3(
+      vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source
+      vec3( 0.35458, 0.90834, 0.13383 ),
+      vec3( 0.04823, 0.01566, 0.83777 )
+    );
+    // ODT_SAT => XYZ => D60_2_D65 => sRGB
+    const mat3 ACESOutputMat = mat3(
+      vec3(  1.60475, -0.10208, -0.00327 ), // transposed from source
+      vec3( -0.53108,  1.10813, -0.07276 ),
+      vec3( -0.07367, -0.00605,  1.07602 )
+    );
+    color *= 1.0 / 0.6;
+    color = ACESInputMat * color;
+    // Apply RRT and ODT
+    color = RRTAndODTFit( color );
+    color = ACESOutputMat * color;
+    // Clamp to [0, 1]
+    return saturate( color );
+  }
+
+  float _SoftLight(float a, float b) {
+    return (b < 0.5 ?
+        (2.0 * a * b + a * a * (1.0 - 2.0 * b)) :
+        (2.0 * a * (1.0 - b) + sqrt(a) * (2.0 * b - 1.0))
+    );
+  }
+
+  vec3 _SoftLight(vec3 a, vec3 b) {
+    return vec3(
+        _SoftLight(a.x, b.x),
+        _SoftLight(a.y, b.y),
+        _SoftLight(a.z, b.z)
+    );
+  }
+
+  bool _RayIntersectsSphere(
+      vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius, out float t0, out float t1) {
+    vec3 oc = rayStart - sphereCenter;
+    float a = dot(rayDir, rayDir);
+    float b = 2.0 * dot(oc, rayDir);
+    float c = dot(oc, oc) - sphereRadius * sphereRadius;
+    float d =  b * b - 4.0 * a * c;
+
+    // Also skip single point of contact
+    if (d <= 0.0) {
+      return false;
+    }
+
+    float r0 = (-b - sqrt(d)) / (2.0 * a);
+    float r1 = (-b + sqrt(d)) / (2.0 * a);
+
+    t0 = min(r0, r1);
+    t1 = max(r0, r1);
+
+    return (t1 >= 0.0);
+  }
+
+
+  vec3 _SampleLightRay(
+      vec3 origin, vec3 sunDir, float planetScale, float planetRadius, float totalRadius,
+      float rayleighScale, float mieScale, float absorptionHeightMax, float absorptionFalloff) {
+
+    float t0, t1;
+    _RayIntersectsSphere(origin, sunDir, planetPosition, totalRadius, t0, t1);
+
+    float actualLightStepSize = (t1 - t0) / float(LIGHT_STEP_COUNT);
+    float virtualLightStepSize = actualLightStepSize * planetScale;
+    float lightStepPosition = 0.0;
+
+    vec3 opticalDepthLight = vec3(0.0);
+
+    for (int j = 0; j < LIGHT_STEP_COUNT; j++) {
+      vec3 currentLightSamplePosition = origin + sunDir * (lightStepPosition + actualLightStepSize * 0.5);
+
+      // Calculate the optical depths and accumulate
+      float currentHeight = length(currentLightSamplePosition) - planetRadius;
+      float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualLightStepSize;
+      float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualLightStepSize;
+      float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
+      currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualLightStepSize;
+
+      opticalDepthLight += vec3(
+          currentOpticalDepthRayleigh,
+          currentOpticalDepthMie,
+          currentOpticalDepthOzone);
+
+      lightStepPosition += actualLightStepSize;
+    }
+
+    return opticalDepthLight;
+  }
+
+  void _ComputeScattering(
+      vec3 worldSpacePos, vec3 rayDirection, vec3 rayOrigin, vec3 sunDir,
+      out vec3 scatteringColour, out vec3 scatteringOpacity) {
+
+    vec3 betaRayleigh = vec3(5.5e-6, 13.0e-6, 22.4e-6);
+    float betaMie = 21e-6;
+    vec3 betaAbsorption = vec3(2.04e-5, 4.97e-5, 1.95e-6);
+    float g = 0.76;
+    float sunIntensity = 40.0;
+
+    float planetRadius = planetRadius;
+    float atmosphereRadius = atmosphereRadius - planetRadius;
+    float totalRadius = planetRadius + atmosphereRadius;
+
+    float referencePlanetRadius = 6371000.0;
+    float referenceAtmosphereRadius = 100000.0;
+    float referenceTotalRadius = referencePlanetRadius + referenceAtmosphereRadius;
+    float referenceRatio = referencePlanetRadius / referenceAtmosphereRadius;
+
+    float scaleRatio = planetRadius / atmosphereRadius;
+    float planetScale = referencePlanetRadius / planetRadius;
+    float atmosphereScale = scaleRatio / referenceRatio;
+    float maxDist = distance(worldSpacePos, rayOrigin);
+
+    float rayleighScale = 8500.0 / (planetScale * atmosphereScale);
+    float mieScale = 1200.0 / (planetScale * atmosphereScale);
+    float absorptionHeightMax = 32000.0 * (planetScale * atmosphereScale);
+    float absorptionFalloff = 3000.0 / (planetScale * atmosphereScale);;
+
+    float mu = dot(rayDirection, sunDir);
+    float mumu = mu * mu;
+    float gg = g * g;
+    float phaseRayleigh = 3.0 / (16.0 * PI) * (1.0 + mumu);
+    float phaseMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg));
+
+    // Early out if ray doesn't intersect atmosphere.
+    float t0, t1;
+    if (!_RayIntersectsSphere(rayOrigin, rayDirection, planetPosition, totalRadius, t0, t1)) {
+      scatteringOpacity = vec3(1.0);
+      return;
+    }
+
+    // Clip the ray between the camera and potentially the planet surface.
+    t0 = max(0.0, t0);
+    t1 = min(maxDist, t1);
+
+    float actualPrimaryStepSize = (t1 - t0) / float(PRIMARY_STEP_COUNT);
+    float virtualPrimaryStepSize = actualPrimaryStepSize * planetScale;
+    float primaryStepPosition = 0.0;
+
+    vec3 accumulatedRayleigh = vec3(0.0);
+    vec3 accumulatedMie = vec3(0.0);
+    vec3 opticalDepth = vec3(0.0);
+
+    // Take N steps along primary ray
+    for (int i = 0; i < PRIMARY_STEP_COUNT; i++) {
+      vec3 currentPrimarySamplePosition = rayOrigin + rayDirection * (
+          primaryStepPosition + actualPrimaryStepSize * 0.5);
+
+      float currentHeight = max(0.0, length(currentPrimarySamplePosition) - planetRadius);
+
+      float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualPrimaryStepSize;
+      float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualPrimaryStepSize;
+
+      // Taken from https://www.shadertoy.com/view/wlBXWK
+      float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
+      currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualPrimaryStepSize;
+
+      opticalDepth += vec3(currentOpticalDepthRayleigh, currentOpticalDepthMie, currentOpticalDepthOzone);
+
+      // Sample light ray and accumulate optical depth.
+      vec3 opticalDepthLight = _SampleLightRay(
+          currentPrimarySamplePosition, sunDir,
+          planetScale, planetRadius, totalRadius,
+          rayleighScale, mieScale, absorptionHeightMax, absorptionFalloff);
+
+      vec3 r = (
+          betaRayleigh * (opticalDepth.x + opticalDepthLight.x) +
+          betaMie * (opticalDepth.y + opticalDepthLight.y) + 
+          betaAbsorption * (opticalDepth.z + opticalDepthLight.z));
+      vec3 attn = exp(-r);
+
+      accumulatedRayleigh += currentOpticalDepthRayleigh * attn;
+      accumulatedMie += currentOpticalDepthMie * attn;
+
+      primaryStepPosition += actualPrimaryStepSize;
+    }
+
+    scatteringColour = sunIntensity * (phaseRayleigh * betaRayleigh * accumulatedRayleigh + phaseMie * betaMie * accumulatedMie);
+    scatteringOpacity = exp(
+        -(betaMie * opticalDepth.y + betaRayleigh * opticalDepth.x + betaAbsorption * opticalDepth.z));
+  }
+
+  vec3 _ApplyGroundFog(
+      in vec3 rgb,
+      float distToPoint,
+      float height,
+      in vec3 worldSpacePos,
+      in vec3 rayOrigin,
+      in vec3 rayDir,
+      in vec3 sunDir)
+  {
+    vec3 up = normalize(rayOrigin);
+
+    float skyAmt = dot(up, rayDir) * 0.25 + 0.75;
+    skyAmt = saturate(skyAmt);
+    skyAmt *= skyAmt;
+
+    vec3 DARK_BLUE = vec3(0.1, 0.2, 0.3);
+    vec3 LIGHT_BLUE = vec3(0.5, 0.6, 0.7);
+    vec3 DARK_ORANGE = vec3(0.7, 0.4, 0.05);
+    vec3 BLUE = vec3(0.5, 0.6, 0.7);
+    vec3 YELLOW = vec3(1.0, 0.9, 0.7);
+
+    vec3 fogCol = mix(DARK_BLUE, LIGHT_BLUE, skyAmt);
+    float sunAmt = max(dot(rayDir, sunDir), 0.0);
+    fogCol = mix(fogCol, YELLOW, pow(sunAmt, 16.0));
+
+    float be = 0.0025;
+    float fogAmt = (1.0 - exp(-distToPoint * be));
+
+    // Sun
+    sunAmt = 0.5 * saturate(pow(sunAmt, 256.0));
+
+    return mix(rgb, fogCol, fogAmt) + sunAmt * YELLOW;
+  }
+
+  vec3 _ApplySpaceFog(
+      in vec3 rgb,
+      in float distToPoint,
+      in float height,
+      in vec3 worldSpacePos,
+      in vec3 rayOrigin,
+      in vec3 rayDir,
+      in vec3 sunDir)
+  {
+    float atmosphereThickness = (atmosphereRadius - planetRadius);
+
+    float t0 = -1.0;
+    float t1 = -1.0;
+
+    // This is a hack since the world mesh has seams that we haven't fixed yet.
+    if (_RayIntersectsSphere(
+        rayOrigin, rayDir, planetPosition, planetRadius, t0, t1)) {
+      if (distToPoint > t0) {
+        distToPoint = t0;
+        worldSpacePos = rayOrigin + t0 * rayDir;
+      }
+    }
+
+    if (!_RayIntersectsSphere(
+        rayOrigin, rayDir, planetPosition, planetRadius + atmosphereThickness * 5.0, t0, t1)) {
+      return rgb * 0.5;
+    }
+
+    // Figure out a better way to do this
+    float silhouette = saturate((distToPoint - 10000.0) / 10000.0);
+
+    // Glow around planet
+    float scaledDistanceToSurface = 0.0;
+
+    // Calculate the closest point between ray direction and planet. Use a point in front of the
+    // camera to force differences as you get closer to planet.
+    vec3 fakeOrigin = rayOrigin + rayDir * atmosphereThickness;
+    float t = max(0.0, dot(rayDir, planetPosition - fakeOrigin) / dot(rayDir, rayDir));
+    vec3 pb = fakeOrigin + t * rayDir;
+
+    scaledDistanceToSurface = saturate((distance(pb, planetPosition) - planetRadius) / atmosphereThickness);
+    scaledDistanceToSurface = smoothstep(0.0, 1.0, 1.0 - scaledDistanceToSurface);
+    //scaledDistanceToSurface = smoothstep(0.0, 1.0, scaledDistanceToSurface);
+
+    float scatteringFactor = scaledDistanceToSurface * silhouette;
+
+    // Fog on surface
+    t0 = max(0.0, t0);
+    t1 = min(distToPoint, t1);
+
+    vec3 intersectionPoint = rayOrigin + t1 * rayDir;
+    vec3 normalAtIntersection = normalize(intersectionPoint);
+
+    float distFactor = exp(-distToPoint * 0.0005 / (atmosphereThickness));
+    float fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
+    fresnel = smoothstep(0.0, 1.0, fresnel);
+
+    float extinctionFactor = saturate(fresnel * distFactor) * (1.0 - silhouette);
+
+    // Front/Back Lighting
+    vec3 BLUE = vec3(0.5, 0.6, 0.75);
+    vec3 YELLOW = vec3(1.0, 0.9, 0.7);
+    vec3 RED = vec3(0.035, 0.0, 0.0);
+
+    float NdotL = dot(normalAtIntersection, sunDir);
+    float wrap = 0.5;
+    float NdotL_wrap = max(0.0, (NdotL + wrap) / (1.0 + wrap));
+    float RdotS = max(0.0, dot(rayDir, sunDir));
+    float sunAmount = RdotS;
+
+    vec3 backLightingColour = YELLOW * 0.1;
+    vec3 frontLightingColour = mix(BLUE, YELLOW, pow(sunAmount, 32.0));
+
+    vec3 fogColour = mix(backLightingColour, frontLightingColour, NdotL_wrap);
+
+    extinctionFactor *= NdotL_wrap;
+
+    // Sun
+    float specular = pow((RdotS + 0.5) / (1.0 + 0.5), 64.0);
+
+    fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
+    fresnel *= fresnel;
+
+    float sunFactor = (length(pb) - planetRadius) / (atmosphereThickness * 5.0);
+    sunFactor = (1.0 - saturate(sunFactor));
+    sunFactor *= sunFactor;
+    sunFactor *= sunFactor;
+    sunFactor *= specular * fresnel;
+
+    vec3 baseColour = mix(rgb, fogColour, extinctionFactor);
+    vec3 litColour = baseColour + _SoftLight(fogColour * scatteringFactor + YELLOW * sunFactor, baseColour);
+    vec3 blendedColour = mix(baseColour, fogColour, scatteringFactor);
+    blendedColour += blendedColour + _SoftLight(YELLOW * sunFactor, blendedColour);
+    return mix(litColour, blendedColour, scaledDistanceToSurface * 0.25);
+  }
+
+  vec3 _ApplyFog(
+    in vec3 rgb,
+    in float distToPoint,
+    in float height,
+    in vec3 worldSpacePos,
+    in vec3 rayOrigin,
+    in vec3 rayDir,
+    in vec3 sunDir)
+  {
+    float distToPlanet = max(0.0, length(rayOrigin) - planetRadius);
+    float atmosphereThickness = (atmosphereRadius - planetRadius);
+
+    vec3 groundCol = _ApplyGroundFog(
+      rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
+    vec3 spaceCol = _ApplySpaceFog(
+      rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
+
+    float blendFactor = saturate(distToPlanet / (atmosphereThickness * 0.5));
+
+    blendFactor = smoothstep(0.0, 1.0, blendFactor);
+    blendFactor = smoothstep(0.0, 1.0, blendFactor);
+
+    return mix(groundCol, spaceCol, blendFactor);
+  }
+
+  void main() {
+    float z = texture2D(tDepth, vUv).x;
+    vec3 posWS = _ScreenToWorld(vec3(vUv, z));
+    float dist = length(posWS - cameraPosition);
+    float height = max(0.0, length(cameraPosition) - planetRadius);
+    vec3 cameraDirection = normalize(posWS - cameraPosition);
+
+
+    vec3 diffuse = texture2D(tDiffuse, vUv).xyz;
+    vec3 lightDir = normalize(vec3(1, 1, -1));
+
+    // diffuse = _ApplyFog(diffuse, dist, height, posWS, cameraPosition, cameraDirection, lightDir);
+
+    vec3 scatteringColour = vec3(0.0);
+    vec3 scatteringOpacity = vec3(1.0, 1.0, 1.0);
+    _ComputeScattering(
+        posWS, cameraDirection, cameraPosition,
+        lightDir, scatteringColour, scatteringOpacity
+    );
+
+    // diffuse = diffuse * scatteringOpacity + scatteringColour;
+    // diffuse = ACESFilmicToneMapping(diffuse);
+    diffuse = pow(diffuse, vec3(1.0 / 2.0));
+
+    gl_FragColor.rgb = diffuse;
+    gl_FragColor.a = 1.0;
+  }
+  `;
+  
+
+  const _Shader = {
+    uniforms: {
+      "tDiffuse": { value: null },
+      "tDepth": { value: null },
+      "cameraNear": { value: 0.0 },
+      "cameraFar": { value: 0.0 },
+    },
+    vertexShader: _VS,
+    fragmentShader: _PS,
+  };
+
+  return {
+    Shader: _Shader,
+    VS: _VS,
+    PS: _PS,
+  };
+})();

+ 479 - 0
src/simplex-noise.js

@@ -0,0 +1,479 @@
+/*
+ * A fast javascript implementation of simplex noise by Jonas Wagner
+
+Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java.
+Which is based on example code by Stefan Gustavson ([email protected]).
+With Optimisations by Peter Eastman ([email protected]).
+Better rank ordering method by Stefan Gustavson in 2012.
+
+
+ Copyright (c) 2018 Jonas Wagner
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+// (function() {
+
+export const simplex = (function() {
+
+  'use strict';
+
+  var F2 = 0.5 * (Math.sqrt(3.0) - 1.0);
+  var G2 = (3.0 - Math.sqrt(3.0)) / 6.0;
+  var F3 = 1.0 / 3.0;
+  var G3 = 1.0 / 6.0;
+  var F4 = (Math.sqrt(5.0) - 1.0) / 4.0;
+  var G4 = (5.0 - Math.sqrt(5.0)) / 20.0;
+
+  function SimplexNoise(randomOrSeed) {
+    var random;
+    if (typeof randomOrSeed == 'function') {
+      random = randomOrSeed;
+    }
+    else if (randomOrSeed) {
+      random = alea(randomOrSeed);
+    } else {
+      random = Math.random;
+    }
+    this.p = buildPermutationTable(random);
+    this.perm = new Uint8Array(512);
+    this.permMod12 = new Uint8Array(512);
+    for (var i = 0; i < 512; i++) {
+      this.perm[i] = this.p[i & 255];
+      this.permMod12[i] = this.perm[i] % 12;
+    }
+
+  }
+  SimplexNoise.prototype = {
+    grad3: new Float32Array([1, 1, 0,
+      -1, 1, 0,
+      1, -1, 0,
+
+      -1, -1, 0,
+      1, 0, 1,
+      -1, 0, 1,
+
+      1, 0, -1,
+      -1, 0, -1,
+      0, 1, 1,
+
+      0, -1, 1,
+      0, 1, -1,
+      0, -1, -1]),
+    grad4: new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
+      0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1,
+      1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1,
+      -1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1,
+      1, 1, 0, 1, 1, 1, 0, -1, 1, -1, 0, 1, 1, -1, 0, -1,
+      -1, 1, 0, 1, -1, 1, 0, -1, -1, -1, 0, 1, -1, -1, 0, -1,
+      1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 0,
+      -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 0]),
+    noise2D: function(xin, yin) {
+      var permMod12 = this.permMod12;
+      var perm = this.perm;
+      var grad3 = this.grad3;
+      var n0 = 0; // Noise contributions from the three corners
+      var n1 = 0;
+      var n2 = 0;
+      // Skew the input space to determine which simplex cell we're in
+      var s = (xin + yin) * F2; // Hairy factor for 2D
+      var i = Math.floor(xin + s);
+      var j = Math.floor(yin + s);
+      var t = (i + j) * G2;
+      var X0 = i - t; // Unskew the cell origin back to (x,y) space
+      var Y0 = j - t;
+      var x0 = xin - X0; // The x,y distances from the cell origin
+      var y0 = yin - Y0;
+      // For the 2D case, the simplex shape is an equilateral triangle.
+      // Determine which simplex we are in.
+      var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
+      if (x0 > y0) {
+        i1 = 1;
+        j1 = 0;
+      } // lower triangle, XY order: (0,0)->(1,0)->(1,1)
+      else {
+        i1 = 0;
+        j1 = 1;
+      } // upper triangle, YX order: (0,0)->(0,1)->(1,1)
+      // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
+      // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
+      // c = (3-sqrt(3))/6
+      var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
+      var y1 = y0 - j1 + G2;
+      var x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords
+      var y2 = y0 - 1.0 + 2.0 * G2;
+      // Work out the hashed gradient indices of the three simplex corners
+      var ii = i & 255;
+      var jj = j & 255;
+      // Calculate the contribution from the three corners
+      var t0 = 0.5 - x0 * x0 - y0 * y0;
+      if (t0 >= 0) {
+        var gi0 = permMod12[ii + perm[jj]] * 3;
+        t0 *= t0;
+        n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
+      }
+      var t1 = 0.5 - x1 * x1 - y1 * y1;
+      if (t1 >= 0) {
+        var gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3;
+        t1 *= t1;
+        n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
+      }
+      var t2 = 0.5 - x2 * x2 - y2 * y2;
+      if (t2 >= 0) {
+        var gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3;
+        t2 *= t2;
+        n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
+      }
+      // Add contributions from each corner to get the final noise value.
+      // The result is scaled to return values in the interval [-1,1].
+      return 70.0 * (n0 + n1 + n2);
+    },
+    // 3D simplex noise
+    noise3D: function(xin, yin, zin) {
+      var permMod12 = this.permMod12;
+      var perm = this.perm;
+      var grad3 = this.grad3;
+      var n0, n1, n2, n3; // Noise contributions from the four corners
+      // Skew the input space to determine which simplex cell we're in
+      var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D
+      var i = Math.floor(xin + s);
+      var j = Math.floor(yin + s);
+      var k = Math.floor(zin + s);
+      var t = (i + j + k) * G3;
+      var X0 = i - t; // Unskew the cell origin back to (x,y,z) space
+      var Y0 = j - t;
+      var Z0 = k - t;
+      var x0 = xin - X0; // The x,y,z distances from the cell origin
+      var y0 = yin - Y0;
+      var z0 = zin - Z0;
+      // For the 3D case, the simplex shape is a slightly irregular tetrahedron.
+      // Determine which simplex we are in.
+      var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
+      var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
+      if (x0 >= y0) {
+        if (y0 >= z0) {
+          i1 = 1;
+          j1 = 0;
+          k1 = 0;
+          i2 = 1;
+          j2 = 1;
+          k2 = 0;
+        } // X Y Z order
+        else if (x0 >= z0) {
+          i1 = 1;
+          j1 = 0;
+          k1 = 0;
+          i2 = 1;
+          j2 = 0;
+          k2 = 1;
+        } // X Z Y order
+        else {
+          i1 = 0;
+          j1 = 0;
+          k1 = 1;
+          i2 = 1;
+          j2 = 0;
+          k2 = 1;
+        } // Z X Y order
+      }
+      else { // x0<y0
+        if (y0 < z0) {
+          i1 = 0;
+          j1 = 0;
+          k1 = 1;
+          i2 = 0;
+          j2 = 1;
+          k2 = 1;
+        } // Z Y X order
+        else if (x0 < z0) {
+          i1 = 0;
+          j1 = 1;
+          k1 = 0;
+          i2 = 0;
+          j2 = 1;
+          k2 = 1;
+        } // Y Z X order
+        else {
+          i1 = 0;
+          j1 = 1;
+          k1 = 0;
+          i2 = 1;
+          j2 = 1;
+          k2 = 0;
+        } // Y X Z order
+      }
+      // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),
+      // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and
+      // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where
+      // c = 1/6.
+      var x1 = x0 - i1 + G3; // Offsets for second corner in (x,y,z) coords
+      var y1 = y0 - j1 + G3;
+      var z1 = z0 - k1 + G3;
+      var x2 = x0 - i2 + 2.0 * G3; // Offsets for third corner in (x,y,z) coords
+      var y2 = y0 - j2 + 2.0 * G3;
+      var z2 = z0 - k2 + 2.0 * G3;
+      var x3 = x0 - 1.0 + 3.0 * G3; // Offsets for last corner in (x,y,z) coords
+      var y3 = y0 - 1.0 + 3.0 * G3;
+      var z3 = z0 - 1.0 + 3.0 * G3;
+      // Work out the hashed gradient indices of the four simplex corners
+      var ii = i & 255;
+      var jj = j & 255;
+      var kk = k & 255;
+      // Calculate the contribution from the four corners
+      var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
+      if (t0 < 0) n0 = 0.0;
+      else {
+        var gi0 = permMod12[ii + perm[jj + perm[kk]]] * 3;
+        t0 *= t0;
+        n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0 + grad3[gi0 + 2] * z0);
+      }
+      var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
+      if (t1 < 0) n1 = 0.0;
+      else {
+        var gi1 = permMod12[ii + i1 + perm[jj + j1 + perm[kk + k1]]] * 3;
+        t1 *= t1;
+        n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1 + grad3[gi1 + 2] * z1);
+      }
+      var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
+      if (t2 < 0) n2 = 0.0;
+      else {
+        var gi2 = permMod12[ii + i2 + perm[jj + j2 + perm[kk + k2]]] * 3;
+        t2 *= t2;
+        n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2 + grad3[gi2 + 2] * z2);
+      }
+      var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
+      if (t3 < 0) n3 = 0.0;
+      else {
+        var gi3 = permMod12[ii + 1 + perm[jj + 1 + perm[kk + 1]]] * 3;
+        t3 *= t3;
+        n3 = t3 * t3 * (grad3[gi3] * x3 + grad3[gi3 + 1] * y3 + grad3[gi3 + 2] * z3);
+      }
+      // Add contributions from each corner to get the final noise value.
+      // The result is scaled to stay just inside [-1,1]
+      return 32.0 * (n0 + n1 + n2 + n3);
+    },
+    // 4D simplex noise, better simplex rank ordering method 2012-03-09
+    noise4D: function(x, y, z, w) {
+      var perm = this.perm;
+      var grad4 = this.grad4;
+
+      var n0, n1, n2, n3, n4; // Noise contributions from the five corners
+      // Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in
+      var s = (x + y + z + w) * F4; // Factor for 4D skewing
+      var i = Math.floor(x + s);
+      var j = Math.floor(y + s);
+      var k = Math.floor(z + s);
+      var l = Math.floor(w + s);
+      var t = (i + j + k + l) * G4; // Factor for 4D unskewing
+      var X0 = i - t; // Unskew the cell origin back to (x,y,z,w) space
+      var Y0 = j - t;
+      var Z0 = k - t;
+      var W0 = l - t;
+      var x0 = x - X0; // The x,y,z,w distances from the cell origin
+      var y0 = y - Y0;
+      var z0 = z - Z0;
+      var w0 = w - W0;
+      // For the 4D case, the simplex is a 4D shape I won't even try to describe.
+      // To find out which of the 24 possible simplices we're in, we need to
+      // determine the magnitude ordering of x0, y0, z0 and w0.
+      // Six pair-wise comparisons are performed between each possible pair
+      // of the four coordinates, and the results are used to rank the numbers.
+      var rankx = 0;
+      var ranky = 0;
+      var rankz = 0;
+      var rankw = 0;
+      if (x0 > y0) rankx++;
+      else ranky++;
+      if (x0 > z0) rankx++;
+      else rankz++;
+      if (x0 > w0) rankx++;
+      else rankw++;
+      if (y0 > z0) ranky++;
+      else rankz++;
+      if (y0 > w0) ranky++;
+      else rankw++;
+      if (z0 > w0) rankz++;
+      else rankw++;
+      var i1, j1, k1, l1; // The integer offsets for the second simplex corner
+      var i2, j2, k2, l2; // The integer offsets for the third simplex corner
+      var i3, j3, k3, l3; // The integer offsets for the fourth simplex corner
+      // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.
+      // Many values of c will never occur, since e.g. x>y>z>w makes x<z, y<w and x<w
+      // impossible. Only the 24 indices which have non-zero entries make any sense.
+      // We use a thresholding to set the coordinates in turn from the largest magnitude.
+      // Rank 3 denotes the largest coordinate.
+      i1 = rankx >= 3 ? 1 : 0;
+      j1 = ranky >= 3 ? 1 : 0;
+      k1 = rankz >= 3 ? 1 : 0;
+      l1 = rankw >= 3 ? 1 : 0;
+      // Rank 2 denotes the second largest coordinate.
+      i2 = rankx >= 2 ? 1 : 0;
+      j2 = ranky >= 2 ? 1 : 0;
+      k2 = rankz >= 2 ? 1 : 0;
+      l2 = rankw >= 2 ? 1 : 0;
+      // Rank 1 denotes the second smallest coordinate.
+      i3 = rankx >= 1 ? 1 : 0;
+      j3 = ranky >= 1 ? 1 : 0;
+      k3 = rankz >= 1 ? 1 : 0;
+      l3 = rankw >= 1 ? 1 : 0;
+      // The fifth corner has all coordinate offsets = 1, so no need to compute that.
+      var x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords
+      var y1 = y0 - j1 + G4;
+      var z1 = z0 - k1 + G4;
+      var w1 = w0 - l1 + G4;
+      var x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords
+      var y2 = y0 - j2 + 2.0 * G4;
+      var z2 = z0 - k2 + 2.0 * G4;
+      var w2 = w0 - l2 + 2.0 * G4;
+      var x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords
+      var y3 = y0 - j3 + 3.0 * G4;
+      var z3 = z0 - k3 + 3.0 * G4;
+      var w3 = w0 - l3 + 3.0 * G4;
+      var x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords
+      var y4 = y0 - 1.0 + 4.0 * G4;
+      var z4 = z0 - 1.0 + 4.0 * G4;
+      var w4 = w0 - 1.0 + 4.0 * G4;
+      // Work out the hashed gradient indices of the five simplex corners
+      var ii = i & 255;
+      var jj = j & 255;
+      var kk = k & 255;
+      var ll = l & 255;
+      // Calculate the contribution from the five corners
+      var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0;
+      if (t0 < 0) n0 = 0.0;
+      else {
+        var gi0 = (perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32) * 4;
+        t0 *= t0;
+        n0 = t0 * t0 * (grad4[gi0] * x0 + grad4[gi0 + 1] * y0 + grad4[gi0 + 2] * z0 + grad4[gi0 + 3] * w0);
+      }
+      var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1;
+      if (t1 < 0) n1 = 0.0;
+      else {
+        var gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32) * 4;
+        t1 *= t1;
+        n1 = t1 * t1 * (grad4[gi1] * x1 + grad4[gi1 + 1] * y1 + grad4[gi1 + 2] * z1 + grad4[gi1 + 3] * w1);
+      }
+      var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2;
+      if (t2 < 0) n2 = 0.0;
+      else {
+        var gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32) * 4;
+        t2 *= t2;
+        n2 = t2 * t2 * (grad4[gi2] * x2 + grad4[gi2 + 1] * y2 + grad4[gi2 + 2] * z2 + grad4[gi2 + 3] * w2);
+      }
+      var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3;
+      if (t3 < 0) n3 = 0.0;
+      else {
+        var gi3 = (perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32) * 4;
+        t3 *= t3;
+        n3 = t3 * t3 * (grad4[gi3] * x3 + grad4[gi3 + 1] * y3 + grad4[gi3 + 2] * z3 + grad4[gi3 + 3] * w3);
+      }
+      var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4;
+      if (t4 < 0) n4 = 0.0;
+      else {
+        var gi4 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32) * 4;
+        t4 *= t4;
+        n4 = t4 * t4 * (grad4[gi4] * x4 + grad4[gi4 + 1] * y4 + grad4[gi4 + 2] * z4 + grad4[gi4 + 3] * w4);
+      }
+      // Sum up and scale the result to cover the range [-1,1]
+      return 27.0 * (n0 + n1 + n2 + n3 + n4);
+    }
+  };
+
+  function buildPermutationTable(random) {
+    var i;
+    var p = new Uint8Array(256);
+    for (i = 0; i < 256; i++) {
+      p[i] = i;
+    }
+    for (i = 0; i < 255; i++) {
+      var r = i + ~~(random() * (256 - i));
+      var aux = p[i];
+      p[i] = p[r];
+      p[r] = aux;
+    }
+    return p;
+  }
+  SimplexNoise._buildPermutationTable = buildPermutationTable;
+
+  function alea() {
+    // Johannes Baagøe <[email protected]>, 2010
+    var s0 = 0;
+    var s1 = 0;
+    var s2 = 0;
+    var c = 1;
+
+    var mash = masher();
+    s0 = mash(' ');
+    s1 = mash(' ');
+    s2 = mash(' ');
+
+    for (var i = 0; i < arguments.length; i++) {
+      s0 -= mash(arguments[i]);
+      if (s0 < 0) {
+        s0 += 1;
+      }
+      s1 -= mash(arguments[i]);
+      if (s1 < 0) {
+        s1 += 1;
+      }
+      s2 -= mash(arguments[i]);
+      if (s2 < 0) {
+        s2 += 1;
+      }
+    }
+    mash = null;
+    return function() {
+      var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
+      s0 = s1;
+      s1 = s2;
+      return s2 = t - (c = t | 0);
+    };
+  }
+  function masher() {
+    var n = 0xefc8249d;
+    return function(data) {
+      data = data.toString();
+      for (var i = 0; i < data.length; i++) {
+        n += data.charCodeAt(i);
+        var h = 0.02519603282416938 * n;
+        n = h >>> 0;
+        h -= n;
+        h *= n;
+        n = h >>> 0;
+        h -= n;
+        n += h * 0x100000000; // 2^32
+      }
+      return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
+    };
+  }
+
+  // // amd
+  // if (typeof define !== 'undefined' && define.amd) define(function() {return SimplexNoise;});
+  // // common js
+  // if (typeof exports !== 'undefined') exports.SimplexNoise = SimplexNoise;
+  // // browser
+  // else if (typeof window !== 'undefined') window.SimplexNoise = SimplexNoise;
+  // // nodejs
+  // if (typeof module !== 'undefined') {
+  //   module.exports = SimplexNoise;
+  // }
+  return {
+    SimplexNoise: SimplexNoise
+  };
+
+})();

+ 115 - 0
src/sky.js

@@ -0,0 +1,115 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {Sky} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/objects/Sky.js';
+import {Water} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/objects/Water.js';
+
+
+export const sky = (function() {
+
+  class TerrainSky {
+    constructor(params) {
+      this._params = params;
+      this._Init(params);
+    }
+
+    _Init(params) {
+      const waterGeometry = new THREE.PlaneBufferGeometry(10000, 10000, 100, 100);
+
+      this._water = new Water(
+        waterGeometry,
+        {
+          textureWidth: 2048,
+          textureHeight: 2048,
+          waterNormals: new THREE.TextureLoader().load( 'resources/waternormals.jpg', function ( texture ) {
+            texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+          } ),
+          alpha: 0.5,
+          sunDirection: new THREE.Vector3(1, 0, 0),
+          sunColor: 0xffffff,
+          waterColor: 0x001e0f,
+          distortionScale: 0.0,
+          fog: undefined
+        }
+      );
+      // this._water.rotation.x = - Math.PI / 2;
+      // this._water.position.y = 4;
+
+      this._sky = new Sky();
+      this._sky.scale.setScalar(10000);
+
+      this._group = new THREE.Group();
+      //this._group.add(this._water);
+      this._group.add(this._sky);
+
+      params.scene.add(this._group);
+
+      params.guiParams.sky = {
+        turbidity: 10.0,
+        rayleigh: 2,
+        mieCoefficient: 0.005,
+        mieDirectionalG: 0.8,
+        luminance: 1,
+      };
+
+      params.guiParams.sun = {
+        inclination: 0.31,
+        azimuth: 0.25,
+      };
+
+      const onShaderChange = () => {
+        for (let k in params.guiParams.sky) {
+          this._sky.material.uniforms[k].value = params.guiParams.sky[k];
+        }
+        for (let k in params.guiParams.general) {
+          this._sky.material.uniforms[k].value = params.guiParams.general[k];
+        }
+      };
+
+      const onSunChange = () => {
+        var theta = Math.PI * (params.guiParams.sun.inclination - 0.5);
+        var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5);
+
+        const sunPosition = new THREE.Vector3();
+        sunPosition.x = Math.cos(phi);
+        sunPosition.y = Math.sin(phi) * Math.sin(theta);
+        sunPosition.z = Math.sin(phi) * Math.cos(theta);
+
+        this._sky.material.uniforms['sunPosition'].value.copy(sunPosition);
+        this._water.material.uniforms['sunDirection'].value.copy(sunPosition.normalize());
+      };
+
+      const skyRollup = params.gui.addFolder('Sky');
+      skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange(
+          onShaderChange);
+      skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange(
+          onShaderChange);
+      skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange(
+          onShaderChange);
+      skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange(
+          onShaderChange);
+      skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange(
+          onShaderChange);
+
+      const sunRollup = params.gui.addFolder('Sun');
+      sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange(
+          onSunChange);
+      sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange(
+          onSunChange);
+
+      onShaderChange();
+      onSunChange();
+    }
+
+    Update(timeInSeconds) {
+      this._water.material.uniforms['time'].value += timeInSeconds;
+
+      this._group.position.x = this._params.camera.position.x;
+      this._group.position.z = this._params.camera.position.z;
+    }
+  }
+
+
+  return {
+    TerrainSky: TerrainSky
+  }
+})();

+ 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,
+  };
+})();

+ 644 - 0
src/terrain-builder-threaded-worker.js

@@ -0,0 +1,644 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {noise} from './noise.js';
+import {texture_splatter} from './texture-splatter.js' ;
+import {math} from './math.js';
+
+
+const _D = new THREE.Vector3();
+const _D1 = new THREE.Vector3();
+const _D2 = new THREE.Vector3();
+const _P = new THREE.Vector3();
+const _P1 = new THREE.Vector3();
+const _P2 = new THREE.Vector3();
+const _P3 = new THREE.Vector3();
+const _H = new THREE.Vector3();
+const _W = new THREE.Vector3();
+const _S = new THREE.Vector3();
+const _C = new THREE.Vector3();
+
+const _N = new THREE.Vector3();
+const _N1 = new THREE.Vector3();
+const _N2 = new THREE.Vector3();
+const _N3 = new THREE.Vector3();
+
+
+class _TerrainBuilderThreadedWorker {
+  constructor() {
+  }
+
+  Init(params) {
+    this.cachedParams_ = {...params};
+    this.params_ = params;
+    this.params_.offset = new THREE.Vector3(...params.offset);
+    this.params_.origin = new THREE.Vector3(...params.origin);
+    this.params_.noise = new noise.Noise(params.noiseParams);
+    this.params_.heightGenerators = [
+        new texture_splatter.HeightGenerator(
+            this.params_.noise, params.offset,
+            params.heightGeneratorsParams.min, params.heightGeneratorsParams.max)
+    ];
+
+    this.params_.biomeGenerator = new noise.Noise(params.biomesParams);
+    this.params_.colourNoise = new noise.Noise(params.colourNoiseParams);
+    this.params_.colourGenerator = new texture_splatter.TextureSplatter(
+        {
+          biomeGenerator: this.params_.biomeGenerator,
+          colourNoise: this.params_.colourNoise
+        });
+  }
+
+  _GenerateHeight(v) {
+    return this.params_.heightGenerators[0].Get(v.x, v.y, v.z)[0];
+  }
+
+  GenerateNormals_(positions, indices) {
+    const normals = new Array(positions.length).fill(0.0);
+    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;
+    }
+    return normals;
+  }
+
+  GenerateIndices_() {
+    const resolution = this.params_.resolution + 2;
+    const indices = [];
+    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);
+      }
+    }
+    return indices;
+  }
+
+  _ComputeNormal_CentralDifference(xp, yp, stepSize) {
+    const localToWorld = this.params_.worldMatrix;
+    const radius = this.params_.radius;
+    const offset = this.params_.offset;
+    const width = this.params_.width;
+    const half = width / 2;
+    const resolution = this.params_.resolution + 2;
+    const effectiveResolution = resolution - 2;
+
+    // Compute position
+    const _ComputeWSPosition = (xpos, ypos) => {
+      const xp = width * xpos;
+      const yp = width * ypos;
+      _P.set(xp - half, yp - half, radius);
+      _P.add(offset);
+      _P.normalize();
+      _D.copy(_P);
+      _D.transformDirection(localToWorld);
+
+      _P.multiplyScalar(radius);
+      _P.z -= radius;
+      _P.applyMatrix4(localToWorld);
+
+      // Purturb height along z-vector
+      const height = this._GenerateHeight(_P);
+      _H.copy(_D);
+      _H.multiplyScalar(height);
+      _P.add(_H);
+
+      return _P;
+    };
+
+    const _ComputeWSPositionFromWS = (pos) => {
+      _P.copy(pos);
+      _P.normalize();
+      _D.copy(_P);
+      _P.multiplyScalar(radius);
+
+      // Purturb height along z-vector
+      const height = this._GenerateHeight(_P);
+      _H.copy(_D);
+      _H.multiplyScalar(height);
+      _P.add(_H);
+
+      return _P;
+    };
+
+    const _SphericalToCartesian = (theta, phi) => {
+      const x = (Math.sin(theta) * Math.cos(phi));
+      const y = (Math.sin(theta) * Math.sin(phi));
+      const z = (Math.cos(theta));
+      _P.set(x, y, z);
+      _P.multiplyScalar(radius);
+      const height = this._GenerateHeight(_P);
+      _P.set(x, y, z);
+      _P.multiplyScalar(height + radius);
+      return _P;
+    };
+
+    //
+    _P3.copy(_ComputeWSPosition(xp, yp));
+    _D.copy(_P3);
+    _D.normalize();
+
+    const phi = Math.atan2(_D.y, _D.x);
+    const theta = Math.atan2((_D.x * _D.x + _D.y * _D.y) ** 0.5, _D.z);
+
+    _P1.copy(_ComputeWSPosition(xp, yp));
+    _P2.copy(_SphericalToCartesian(theta, phi));
+
+    // Fixme - Fixed size right now, calculate an appropriate delta
+    const delta = 0.001;
+
+    _P1.copy(_SphericalToCartesian(theta - delta, phi));
+    _P2.copy(_SphericalToCartesian(theta + delta, phi));
+    _D1.subVectors(_P1, _P2);
+    _D1.normalize();
+
+    _P1.copy(_SphericalToCartesian(theta, phi - delta));
+    _P2.copy(_SphericalToCartesian(theta, phi + delta));
+    _D2.subVectors(_P1, _P2);
+    _D2.normalize();
+
+  
+    _P1.copy(_D1);
+    _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution);
+    _P2.copy(_P1);
+    _P2.multiplyScalar(-1)
+    _P1.add(_P3);
+    _P2.add(_P3);
+    _P1.copy(_ComputeWSPositionFromWS(_P1));
+    _P2.copy(_ComputeWSPositionFromWS(_P2));
+    _D1.subVectors(_P1, _P2);
+    _D1.normalize();
+
+    _P1.copy(_D2);
+    _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution);
+    _P2.copy(_P1);
+    _P2.multiplyScalar(-1)
+    _P1.add(_P3);
+    _P2.add(_P3);
+    _P1.copy(_ComputeWSPositionFromWS(_P1));
+    _P2.copy(_ComputeWSPositionFromWS(_P2));
+    _D2.subVectors(_P1, _P2);
+    _D2.normalize();
+
+    _D1.cross(_D2);
+
+    return _D1;
+  }
+
+  RebuildEdgeNormals_(normals) {
+    const resolution = this.params_.resolution + 2;
+    const effectiveResolution = resolution - 2;
+
+    let x = 1;
+    for (let z = 1; z <= resolution-1; z+=1) {
+      const i = x * (resolution + 1) + z;
+      _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
+      normals[i * 3 + 0] = _N.x;
+      normals[i * 3 + 1] = _N.y;
+      normals[i * 3 + 2] = _N.z;
+    }
+
+    let z = resolution - 1;
+    for (let x = 1; x <= resolution-1; x+=1) {
+      const i = (x) * (resolution + 1) + z;
+      _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
+      normals[i * 3 + 0] = _N.x;
+      normals[i * 3 + 1] = _N.y;
+      normals[i * 3 + 2] = _N.z;
+    }
+
+    x = resolution - 1;
+    for (let z = 1; z <= resolution-1; z+=1) {
+      const i = x * (resolution + 1) + z;
+      _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
+      normals[i * 3 + 0] = _N.x;
+      normals[i * 3 + 1] = _N.y;
+      normals[i * 3 + 2] = _N.z;
+    }
+
+    z = 1;
+    for (let x = 1; x <= resolution-1; x+=1) {
+      const i = (x) * (resolution + 1) + z;
+      _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
+      normals[i * 3 + 0] = _N.x;
+      normals[i * 3 + 1] = _N.y;
+      normals[i * 3 + 2] = _N.z;
+    }
+  }
+
+  FixEdgesToMatchNeighbours_(positions, normals, colours) {
+    const resolution = this.params_.resolution + 2;
+    const effectiveResolution = resolution - 2;
+
+    if (this.params_.neighbours[0] > 1) {
+      const x = 1;
+      const stride = this.params_.neighbours[0];
+      for (let z = 1; z <= resolution-1; z+=1) {
+        const i = x * (resolution + 1) + z;
+        // colours[i * 3 + 0] = 0;
+        // colours[i * 3 + 1] = 0;
+        // colours[i * 3 + 2] = 1;
+
+        _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
+        normals[i * 3 + 0] = _N.x;
+        normals[i * 3 + 1] = _N.y;
+        normals[i * 3 + 2] = _N.z;
+      }
+      for (let z = 1; z <= resolution-1-stride; z+=stride) {
+        const i1 = x * (resolution + 1) + z;
+        const i2 = x * (resolution + 1) + (z + stride);
+
+        for (let s = 1; s < stride; ++s) {
+          const i = x * (resolution + 1) + z + s;
+          const p = s / stride;
+          for (let j = 0; j < 3; ++j) {
+            positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
+            normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
+          }
+          // colours[i * 3 + 0] = 0;
+          // colours[i * 3 + 1] = 1;
+          // colours[i * 3 + 2] = 0;
+        }
+      }
+    }
+
+    if (this.params_.neighbours[1] > 1) {
+      const z = resolution - 1;
+      const stride = this.params_.neighbours[1];
+      for (let x = 1; x <= resolution-1; x+=1) {
+        const i = (x) * (resolution + 1) + z;
+        // colours[i * 3 + 0] = 0;
+        // colours[i * 3 + 1] = 0;
+        // colours[i * 3 + 2] = 1;
+
+        _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
+        normals[i * 3 + 0] = _N.x;
+        normals[i * 3 + 1] = _N.y;
+        normals[i * 3 + 2] = _N.z;
+      }
+      for (let x = 1; x <= resolution-1-stride; x+=stride) {
+        const i1 = (x) * (resolution + 1) + z;
+        const i2 = (x + stride) * (resolution + 1) + z;
+
+        for (let s = 1; s < stride; ++s) {
+          const i = (x + s) * (resolution + 1) + z;
+          const p = s / stride;
+          for (let j = 0; j < 3; ++j) {
+            positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
+            normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
+          }
+          // colours[i * 3 + 0] = 1;
+          // colours[i * 3 + 1] = 1;
+          // colours[i * 3 + 2] = 0;
+        }
+      }
+    }
+
+    if (this.params_.neighbours[2] > 1) {
+      const x = resolution - 1;
+      const stride = this.params_.neighbours[2];
+      for (let z = 1; z <= resolution-1; z+=1) {
+        const i = x * (resolution + 1) + z;
+        // colours[i * 3 + 0] = 0;
+        // colours[i * 3 + 1] = 0;
+        // colours[i * 3 + 2] = 1;
+
+        _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
+        normals[i * 3 + 0] = _N.x;
+        normals[i * 3 + 1] = _N.y;
+        normals[i * 3 + 2] = _N.z;
+      }
+      for (let z = 1; z <= resolution-1-stride; z+=stride) {
+        const i1 = x * (resolution + 1) + z;
+        const i2 = x * (resolution + 1) + (z + stride);
+
+        for (let s = 1; s < stride; ++s) {
+          const i = x * (resolution + 1) + z + s;
+          const p = s / stride;
+          for (let j = 0; j < 3; ++j) {
+            positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
+            normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
+          }
+          // colours[i * 3 + 0] = 0;
+          // colours[i * 3 + 1] = 1;
+          // colours[i * 3 + 2] = 1;
+        }
+      }
+    }
+
+    if (this.params_.neighbours[3] > 1) {
+      const z = 1;
+      const stride = this.params_.neighbours[3];
+      for (let x = 1; x <= resolution-1; x+=1) {
+        const i = (x) * (resolution + 1) + z;
+        // colours[i * 3 + 0] = 0;
+        // colours[i * 3 + 1] = 0;
+        // colours[i * 3 + 2] = 1;
+
+        _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
+        normals[i * 3 + 0] = _N.x;
+        normals[i * 3 + 1] = _N.y;
+        normals[i * 3 + 2] = _N.z;
+      }
+      for (let x = 1; x <= resolution-1-stride; x+=stride) {
+        const i1 = (x) * (resolution + 1) + z;
+        const i2 = (x + stride) * (resolution + 1) + z;
+
+        for (let s = 1; s < stride; ++s) {
+          const i = (x + s) * (resolution + 1) + z;
+          const p = s / stride;
+          for (let j = 0; j < 3; ++j) {
+            positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
+            normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
+          }
+          // colours[i * 3 + 0] = 1;
+          // colours[i * 3 + 1] = 0;
+          // colours[i * 3 + 2] = 0;
+        }
+      }
+    }
+  }
+
+  FixEdgeSkirt_(positions, up, normals) {
+    const resolution = this.params_.resolution + 2;
+
+    const _ApplyFix = (x, y, xp, yp) => {
+      const skirtIndex = x * (resolution + 1) + y;
+      const proxyIndex = xp * (resolution + 1) + yp;
+
+      _P.fromArray(positions, proxyIndex * 3);
+      _D.fromArray(up, proxyIndex * 3);
+      _D.multiplyScalar(0);
+      _P.add(_D);
+      positions[skirtIndex * 3 + 0] = _P.x;
+      positions[skirtIndex * 3 + 1] = _P.y;
+      positions[skirtIndex * 3 + 2] = _P.z;
+
+      // Normal will be fucked, copy it from proxy point
+      normals[skirtIndex * 3 + 0] = normals[proxyIndex * 3 + 0];
+      normals[skirtIndex * 3 + 1] = normals[proxyIndex * 3 + 1];
+      normals[skirtIndex * 3 + 2] = normals[proxyIndex * 3 + 2];
+    };
+
+    for (let y = 0; y <= resolution; ++y) {
+      _ApplyFix(0, y, 1, y);
+    }
+    for (let y = 0; y <= resolution; ++y) {
+      _ApplyFix(resolution, y, resolution - 1, y);
+    }
+    for (let x = 0; x <= resolution; ++x) {
+      _ApplyFix(x, 0, x, 1);
+    }
+    for (let x = 0; x <= resolution; ++x) {
+      _ApplyFix(x, resolution, x, resolution - 1);
+    }
+  }
+
+  NormalizeNormals_(normals) {
+    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;
+    }
+  }
+
+  RebuildEdgePositions_(positions) {
+    const localToWorld = this.params_.worldMatrix;
+    const resolution = this.params_.resolution + 2;
+    const radius = this.params_.radius;
+    const offset = this.params_.offset;
+    const origin = this.params_.origin;
+    const width = this.params_.width;
+    const half = width / 2;
+    const effectiveResolution = resolution - 2;
+
+    const _ComputeOriginOffsetPosition = (xpos, ypos) => {
+      const xp = width * xpos;
+      const yp = width * ypos;
+      _P.set(xp - half, yp - half, radius);
+      _P.add(offset);
+      _P.normalize();
+      _D.copy(_P);
+      _D.transformDirection(localToWorld);
+
+      _P.multiplyScalar(radius);
+      _P.z -= radius;
+      _P.applyMatrix4(localToWorld);
+
+      // Keep the absolute world space position to sample noise
+      _W.copy(_P);
+
+      // Move the position relative to the origin
+      _P.sub(origin);
+
+      // Purturb height along z-vector
+      const height = this._GenerateHeight(_W);
+      _H.copy(_D);
+      _H.multiplyScalar(height);
+      _P.add(_H);
+
+      return _P;
+    }
+
+    let x = 1;
+    for (let z = 1; z <= resolution-1; z++) {
+      const i = x * (resolution + 1) + z;
+      const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
+      positions[i * 3 + 0] = p.x;
+      positions[i * 3 + 1] = p.y;
+      positions[i * 3 + 2] = p.z;
+    }
+
+    let z = resolution - 1;
+    for (let x = 1; x <= resolution-1; x++) {
+      const i = (x) * (resolution + 1) + z;
+      const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
+      positions[i * 3 + 0] = p.x;
+      positions[i * 3 + 1] = p.y;
+      positions[i * 3 + 2] = p.z;
+    }
+
+    x = resolution - 1;
+    for (let z = 1; z <= resolution-1; z++) {
+      const i = x * (resolution + 1) + z;
+      const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
+      positions[i * 3 + 0] = p.x;
+      positions[i * 3 + 1] = p.y;
+      positions[i * 3 + 2] = p.z;
+    }
+
+    z = 1;
+    for (let x = 1; x <= resolution-1; x++) {
+      const i = (x) * (resolution + 1) + z;
+      const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
+      positions[i * 3 + 0] = p.x;
+      positions[i * 3 + 1] = p.y;
+      positions[i * 3 + 2] = p.z;
+    }
+  }
+
+  Rebuild() {
+    const positions = [];
+    const up = [];
+    const coords = [];
+
+    const localToWorld = this.params_.worldMatrix;
+    const resolution = this.params_.resolution + 2;
+    const radius = this.params_.radius;
+    const offset = this.params_.offset;
+    const origin = this.params_.origin;
+    const width = this.params_.width;
+    const half = width / 2;
+    const effectiveResolution = resolution - 2;
+
+    for (let x = -1; x <= effectiveResolution + 1; x++) {
+      const xp = width * x / effectiveResolution;
+      for (let y = -1; y <= effectiveResolution + 1; y++) {
+        const yp = width * y / effectiveResolution;
+
+        // Compute position
+        _P.set(xp - half, yp - half, radius);
+        _P.add(offset);
+        _P.normalize();
+        _D.copy(_P);
+        _D.transformDirection(localToWorld);
+
+        _P.multiplyScalar(radius);
+        _P.z -= radius;
+        _P.applyMatrix4(localToWorld);
+
+        // Keep the absolute world space position to sample noise
+        _W.copy(_P);
+
+        // Move the position relative to the origin
+        _P.sub(origin);
+
+        // Purturb height along z-vector
+        const height = this._GenerateHeight(_W);
+        _H.copy(_D);
+        _H.multiplyScalar(height);
+        _P.add(_H);
+
+        positions.push(_P.x, _P.y, _P.z);
+
+        _C.copy(_W);
+        _C.add(_H);
+        coords.push(_C.x, _C.y, _C.z);
+
+        _S.set(_W.x, _W.y, height);
+
+        up.push(_D.x, _D.y, _D.z);
+      }
+    }
+
+    const colours = new Array(positions.length).fill(1.0);
+
+    // Generate indices
+    const indices = this.GenerateIndices_();
+    const normals = this.GenerateNormals_(positions, indices);
+
+    this.RebuildEdgePositions_(positions);
+    this.RebuildEdgeNormals_(normals);
+    this.FixEdgesToMatchNeighbours_(positions, normals, colours);
+    this.FixEdgeSkirt_(positions, up, normals);
+    this.NormalizeNormals_(normals);
+
+    const bytesInFloat32 = 4;
+    const bytesInInt32 = 4;
+    const positionsArray = new Float32Array(
+        new SharedArrayBuffer(bytesInFloat32 * positions.length));
+    const coloursArray = new Float32Array(
+        new SharedArrayBuffer(bytesInFloat32 * colours.length));
+    const normalsArray = new Float32Array(
+        new SharedArrayBuffer(bytesInFloat32 * normals.length));
+    const coordsArray = new Float32Array(
+        new SharedArrayBuffer(bytesInFloat32 * coords.length));
+    const indicesArray = new Uint32Array(
+        new SharedArrayBuffer(bytesInInt32 * indices.length));
+
+    positionsArray.set(positions, 0);
+    coloursArray.set(colours, 0);
+    normalsArray.set(normals, 0);
+    coordsArray.set(coords, 0);
+    indicesArray.set(indices, 0);
+
+    return {
+      positions: positionsArray,
+      colours: coloursArray,
+      normals: normalsArray,
+      coords: coordsArray,
+      indices: indicesArray,
+    };
+  }
+
+  QuickRebuild(mesh) {
+    const positions = mesh.positions;
+    const normals = mesh.normals;
+    const colours = mesh.colours;
+    const up = [];
+    const indices = mesh.indices;
+
+    const localToWorld = this.params_.worldMatrix;
+    const resolution = this.params_.resolution + 2;
+    const radius = this.params_.radius;
+    const offset = this.params_.offset;
+    const origin = this.params_.origin;
+    const width = this.params_.width;
+    const half = width / 2;
+    const effectiveResolution = resolution - 2;
+
+    colours.fill(1.0);
+
+    this.RebuildEdgePositions_(positions);
+    this.RebuildEdgeNormals_(normals);
+    this.FixEdgesToMatchNeighbours_(positions, normals, colours);
+    this.FixEdgeSkirt_(positions, up, normals);
+    this.NormalizeNormals_(normals);
+
+    return mesh;
+  }
+}
+
+const _CHUNK = new _TerrainBuilderThreadedWorker();
+
+self.onmessage = (msg) => {
+  if (msg.data.subject == 'build_chunk') {
+    _CHUNK.Init(msg.data.params);
+
+    const rebuiltData = _CHUNK.Rebuild();
+    self.postMessage({subject: 'build_chunk_result', data: rebuiltData});
+  } else if (msg.data.subject == 'rebuild_chunk') {
+    _CHUNK.Init(msg.data.params);
+
+    const rebuiltData = _CHUNK.QuickRebuild(msg.data.mesh);
+    self.postMessage({subject: 'quick_rebuild_chunk_result', data: rebuiltData});
+  }
+};

+ 205 - 0
src/terrain-builder-threaded.js

@@ -0,0 +1,205 @@
+
+import {terrain_chunk} from './terrain-chunk.js';
+
+
+export const terrain_builder_threaded = (function() {
+
+  const _NUM_WORKERS = 7;
+
+  let _IDs = 0;
+
+  class WorkerThread {
+    constructor(s) {
+      this.worker_ = new Worker(s, {type: 'module'});
+      this.worker_.onmessage = (e) => {
+        this._OnMessage(e);
+      };
+      this._resolve = null;
+      this._id = _IDs++;
+    }
+
+    _OnMessage(e) {
+      const resolve = this._resolve;
+      this._resolve = null;
+      resolve(e.data);
+    }
+
+    get id() {
+      return this._id;
+    }
+
+    postMessage(s, resolve) {
+      this._resolve = resolve;
+      this.worker_.postMessage(s);
+    }
+  }
+
+  class WorkerThreadPool {
+    constructor(sz, entry) {
+      this.workers_ = [...Array(sz)].map(_ => new WorkerThread(entry));
+      this.free_ = [...this.workers_];
+      this.busy_ = {};
+      this.queue_ = [];
+    }
+
+    get length() {
+      return this.workers_.length;
+    }
+
+    get Busy() {
+      return this.queue_.length > 0 || Object.keys(this.busy_).length > 0;
+    }
+
+    Enqueue(workItem, resolve) {
+      this.queue_.push([workItem, resolve]);
+      this._PumpQueue();
+    }
+
+    _PumpQueue() {
+      while (this.free_.length > 0 && this.queue_.length > 0) {
+        const w = this.free_.pop();
+        this.busy_[w.id] = w;
+
+        const [workItem, workResolve] = this.queue_.shift();
+
+        w.postMessage(workItem, (v) => {
+          delete this.busy_[w.id];
+          this.free_.push(w);
+          workResolve(v);
+          this._PumpQueue();
+        });
+      }
+    }
+  }
+
+  class _TerrainChunkRebuilder_Threaded {
+    constructor(params) {
+      this.pool_ = {};
+      this.old_ = [];
+
+      this.workerPool_ = new WorkerThreadPool(
+        _NUM_WORKERS, 'src/terrain-builder-threaded-worker.js');
+  
+      this.params_ = params;
+    }
+
+    _OnResult(chunk, msg) {
+      if (msg.subject == 'build_chunk_result') {
+        chunk.RebuildMeshFromData(msg.data);
+      } else if (msg.subject == 'quick_rebuild_chunk_result') {
+        chunk.QuickRebuildMeshFromData(msg.data);
+      }
+    }
+
+    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();
+
+      const threadedParams = {
+        noiseParams: params.noiseParams,
+        colourNoiseParams: params.colourNoiseParams,
+        biomesParams: params.biomesParams,
+        colourGeneratorParams: params.colourGeneratorParams,
+        heightGeneratorsParams: params.heightGeneratorsParams,
+        width: params.width,
+        neighbours: params.neighbours,
+        offset: params.offset.toArray(),
+        origin: params.origin.toArray(),
+        radius: params.radius,
+        resolution: params.resolution,
+        worldMatrix: params.transform,
+      };
+
+      const msg = {
+        subject: 'build_chunk',
+        params: threadedParams,
+      };
+
+      this.workerPool_.Enqueue(msg, (m) => {
+        this._OnResult(c, m);
+      });
+
+      return c;    
+    }
+
+    RetireChunks(chunks) {
+      this.old_.push(...chunks);
+    }
+
+    _RecycleChunks(chunks) {
+      for (let c of chunks) {
+        if (!(c.chunk.params_.width in this.pool_)) {
+          this.pool_[c.chunk.params_.width] = [];
+        }
+
+        c.chunk.Destroy();
+      }
+    }
+
+    get Busy() {
+      return this.workerPool_.Busy;
+    }
+
+    Rebuild(chunks) {
+      for (let k in chunks) {
+        this.workerPool_.Enqueue(chunks[k].chunk.params_);
+      }
+    }
+
+    QuickRebuild(chunks) {
+      for (let k in chunks) {
+        const chunk = chunks[k];
+        const params = chunk.chunk.params_;
+
+        const threadedParams = {
+          noiseParams: params.noiseParams,
+          colourNoiseParams: params.colourNoiseParams,
+          biomesParams: params.biomesParams,
+          colourGeneratorParams: params.colourGeneratorParams,
+          heightGeneratorsParams: params.heightGeneratorsParams,
+          width: params.width,
+          neighbours: params.neighbours,
+          offset: params.offset.toArray(),
+          origin: params.origin.toArray(),
+          radius: params.radius,
+          resolution: params.resolution,
+          worldMatrix: params.transform,
+        };
+
+        const msg = {
+          subject: 'rebuild_chunk',
+          params: threadedParams,
+          mesh: chunk.chunk.rebuildData_,
+        };
+  
+        this.workerPool_.Enqueue(msg, (m) => {
+          this._OnResult(chunk.chunk, m);
+        });
+      }
+    }
+
+    Update() {
+      if (!this.Busy) {
+        this._RecycleChunks(this.old_);
+        this.old_ = [];
+      }
+    }
+  }
+
+  return {
+    TerrainChunkRebuilder_Threaded: _TerrainChunkRebuilder_Threaded
+  }
+})();

+ 100 - 0
src/terrain-builder.js

@@ -0,0 +1,100 @@
+import {terrain_chunk} from './terrain-chunk.js';
+
+
+export const terrain_builder = (function() {
+
+  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;    
+    }
+
+    RetireChunks(chunks) {
+      this._old.push(...chunks);
+    }
+
+    _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();
+      }
+    }
+  }
+
+  return {
+    TerrainChunkRebuilder: _TerrainChunkRebuilder
+  }
+})();

+ 79 - 0
src/terrain-chunk.js

@@ -0,0 +1,79 @@
+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.mesh_);
+    }
+
+    Hide() {
+      this.mesh_.visible = false;
+    }
+
+    Show() {
+      this.mesh_.visible = true;
+    }
+
+    _Init(params) {
+      this.geometry_ = new THREE.BufferGeometry();
+      this.mesh_ = new THREE.Mesh(this.geometry_, params.material);
+      this.mesh_.castShadow = false;
+      this.mesh_.receiveShadow = true;
+      this.mesh_.frustumCulled = false;
+      this.params_.group.add(this.mesh_);
+      this.Reinit(params);
+    }
+
+    Update(cameraPosition) {
+      this.mesh_.position.copy(this.params_.origin);
+      this.mesh_.position.sub(cameraPosition);
+    }
+
+    Reinit(params) {
+      this.params_ = params;
+      this.mesh_.position.set(0, 0, 0);
+    }
+
+    SetWireframe(b) {
+      this.mesh_.material.wireframe = b;
+    }
+
+    RebuildMeshFromData(data) {
+      this.geometry_.setAttribute(
+          'position', new THREE.Float32BufferAttribute(data.positions, 3));
+      this.geometry_.setAttribute(
+          'color', new THREE.Float32BufferAttribute(data.colours, 3));
+      this.geometry_.setAttribute(
+          'normal', new THREE.Float32BufferAttribute(data.normals, 3));
+      this.geometry_.setAttribute(
+          'coords', new THREE.Float32BufferAttribute(data.coords, 3));
+      this.geometry_.setIndex(
+          new THREE.BufferAttribute(data.indices, 1));
+      this.rebuildData_ = data;
+      this.geometry_.attributes.position.needsUpdate = true;
+      this.geometry_.attributes.normal.needsUpdate = true;
+      this.geometry_.attributes.color.needsUpdate = true;
+      this.geometry_.attributes.coords.needsUpdate = true;
+    }
+
+    QuickRebuildMeshFromData(data) {
+      this.geometry_.attributes.position.array.set(data.positions, 0)
+      this.geometry_.attributes.normal.array.set(data.normals, 0)
+      this.geometry_.attributes.color.array.set(data.colours, 0)
+      this.geometry_.attributes.position.needsUpdate = true;
+      this.geometry_.attributes.normal.needsUpdate = true;
+      this.geometry_.attributes.color.needsUpdate = true;
+    }
+  }
+
+  return {
+    TerrainChunk: TerrainChunk
+  }
+})();

+ 12 - 0
src/terrain-constants.js

@@ -0,0 +1,12 @@
+
+
+export const terrain_constants = (function() {
+  return {
+    QT_MIN_CELL_SIZE: 25,
+    QT_MIN_CELL_RESOLUTION: 48,
+    PLANET_RADIUS: 400000.0,
+
+    NOISE_HEIGHT: 20000.0,
+    NOISE_SCALE: 18000.0,
+  }
+})();

+ 288 - 0
src/terrain-shader.js

@@ -0,0 +1,288 @@
+export const terrain_shader = (function() {
+
+  const _VS = `#version 300 es
+
+precision highp float;
+
+uniform mat4 modelMatrix;
+uniform mat4 modelViewMatrix;
+uniform mat4 viewMatrix;
+uniform mat4 projectionMatrix;
+uniform vec3 cameraPosition;
+uniform float fogDensity;
+uniform vec3 cloudScale;
+
+// Attributes
+in vec3 position;
+in vec3 normal;
+in vec3 coords;
+in vec3 color;
+
+// Outputs
+out vec4 vColor;
+out vec3 vNormal;
+out vec3 vCoords;
+out vec3 vVSPos;
+out vec3 vRepeatingCoords;
+out float vFragDepth;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+void main(){
+  mat4 terrainMatrix = mat4(
+      viewMatrix[0],
+      viewMatrix[1],
+      viewMatrix[2],
+      vec4(0.0, 0.0, 0.0, 1.0));
+
+  gl_Position = projectionMatrix * terrainMatrix * modelMatrix * vec4(position, 1.0);
+
+  vNormal = normal;
+
+  vColor = vec4(color, 1);
+  vCoords = (modelMatrix * vec4(position, 1.0)).xyz + cameraPosition;
+  vVSPos = (terrainMatrix * modelMatrix * vec4(position, 1.0)).xyz;
+
+  vec3 pos = coords;
+  float p = 32768.0;
+  float a = 1024.0;
+  vRepeatingCoords = (4.0 * a / p) * abs(mod(pos, p) - p * 0.5);
+
+  vFragDepth = 1.0 + gl_Position.w;
+}
+  `;
+  
+
+  const _PS = `#version 300 es
+
+precision highp float;
+precision highp int;
+precision highp sampler2DArray;
+
+uniform sampler2DArray normalMap;
+uniform sampler2DArray diffuseMap;
+uniform sampler2D noiseMap;
+
+uniform mat4 modelMatrix;
+uniform mat4 modelViewMatrix;
+uniform vec3 cameraPosition;
+uniform float logDepthBufFC;
+
+in vec4 vColor;
+in vec3 vNormal;
+in vec3 vCoords;
+in vec3 vRepeatingCoords;
+in vec3 vVSPos;
+in float vFragDepth;
+
+out vec4 out_FragColor;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+const float _TRI_SCALE = 10.0;
+
+float sum( vec3 v ) { return v.x+v.y+v.z; }
+
+vec4 hash4( vec2 p ) {
+  return fract(
+    sin(vec4(1.0+dot(p,vec2(37.0,17.0)), 
+              2.0+dot(p,vec2(11.0,47.0)),
+              3.0+dot(p,vec2(41.0,29.0)),
+              4.0+dot(p,vec2(23.0,31.0))))*103.0);
+}
+
+vec4 _CalculateLighting(
+    vec3 lightDirection, vec3 lightColour, vec3 worldSpaceNormal, vec3 viewDirection) {
+  float NdotL = saturate(dot(worldSpaceNormal, lightDirection));
+  // return vec4(lightColour * diffuse, 0.0);
+
+  vec3 H = normalize(lightDirection + viewDirection);
+  float NdotH = dot(worldSpaceNormal, H);
+  float specular = saturate(pow(NdotH, 8.0));
+
+  return vec4(lightColour * NdotL, specular * NdotL);
+}
+
+vec4 _ComputeLighting(vec3 worldSpaceNormal, vec3 sunDir, vec3 viewDirection) {
+  // Hardcoded, whee!
+  vec4 lighting;
+  
+  lighting += _CalculateLighting(
+      sunDir, vec3(1.0, 1.0, 1.0), worldSpaceNormal, viewDirection);
+  // lighting += _CalculateLighting(
+  //     vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection);
+
+  // lighting += vec4(0.15, 0.15, 0.15, 0.0);
+  
+  return lighting;
+}
+
+vec4 _TerrainBlend_4(vec4 samples[4]) {
+  float depth = 0.2;
+  float ma = max(
+      samples[0].w,
+      max(
+          samples[1].w,
+          max(samples[2].w, samples[3].w))) - depth;
+
+  float b1 = max(samples[0].w - ma, 0.0);
+  float b2 = max(samples[1].w - ma, 0.0);
+  float b3 = max(samples[2].w - ma, 0.0);
+  float b4 = max(samples[3].w - ma, 0.0);
+
+  vec4 numer = (
+      samples[0] * b1 + samples[1] * b2 +
+      samples[2] * b3 + samples[3] * b4);
+  float denom = (b1 + b2 + b3 + b4);
+  return numer / denom;
+}
+
+vec4 _TerrainBlend_4_lerp(vec4 samples[4]) {
+  return (
+      samples[0] * samples[0].w + samples[1] * samples[1].w +
+      samples[2] * samples[2].w + samples[3] * samples[3].w);
+}
+
+// Lifted from https://www.shadertoy.com/view/Xtl3zf
+vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) {
+  float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup
+  float l = k*8.0;
+  float f = fract(l);
+  
+  float ia = floor(l+0.5); // suslik's method (see comments)
+  float ib = floor(l);
+  f = min(f, 1.0-f)*2.0;
+
+  vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
+  vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
+
+  vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z));
+  vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z));
+
+  return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz)));
+}
+
+vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
+  vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
+  vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
+  vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
+
+  vec3 weights = abs(normal.xyz);
+  weights = weights / (weights.x + weights.y + weights.z);
+
+  return dx * weights.x + dy * weights.y + dz * weights.z;
+}
+
+vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
+  // Tangent Reconstruction
+  // Triplanar uvs
+  vec2 uvX = pos.zy; // x facing plane
+  vec2 uvY = pos.xz; // y facing plane
+  vec2 uvZ = pos.xy; // z facing plane
+  // Tangent space normal maps
+  vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+  vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+  vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+
+  vec3 weights = abs(normal.xyz);
+  weights = weights / (weights.x + weights.y + weights.z);
+
+  // Get the sign (-1 or 1) of the surface normal
+  vec3 axis = sign(normal);
+  // Construct tangent to world matrices for each axis
+  vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
+  vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
+  mat3 tbnX = mat3(tangentX, bitangentX, normal);
+
+  vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
+  vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
+  mat3 tbnY = mat3(tangentY, bitangentY, normal);
+
+  vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
+  vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
+  mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
+
+  // Apply tangent to world matrix and triblend
+  // Using clamp() because the cross products may be NANs
+  vec3 worldNormal = normalize(
+      clamp(tbnX * tx, -1.0, 1.0) * weights.x +
+      clamp(tbnY * ty, -1.0, 1.0) * weights.y +
+      clamp(tbnZ * tz, -1.0, 1.0) * weights.z
+      );
+  return vec4(worldNormal, 0.0);
+}
+
+vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
+  vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
+  vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
+  vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
+
+  vec3 weights = abs(normal.xyz);
+  weights = weights / (weights.x + weights.y + weights.z);
+
+  return dx * weights.x + dy * weights.y + dz * weights.z;
+}
+
+vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
+  vec2 uvx = pos.zy;
+  vec2 uvy = pos.xz;
+  vec2 uvz = pos.xy;
+  vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+  vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+  vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
+
+  vec3 weights = abs(normal.xyz);
+  weights *= weights;
+  weights = weights / (weights.x + weights.y + weights.z);
+
+  vec3 axis = sign(normal);
+  vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
+  vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
+  mat3 tbnX = mat3(tangentX, bitangentX, normal);
+
+  vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
+  vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
+  mat3 tbnY = mat3(tangentY, bitangentY, normal);
+
+  vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
+  vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
+  mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
+
+  vec3 worldNormal = normalize(
+      clamp(tbnX * tx, -1.0, 1.0) * weights.x +
+      clamp(tbnY * ty, -1.0, 1.0) * weights.y +
+      clamp(tbnZ * tz, -1.0, 1.0) * weights.z);
+  return vec4(worldNormal, 0.0);
+}
+
+void main() {
+  vec3 worldPosition = vCoords;
+  vec3 eyeDirection = normalize(worldPosition - cameraPosition);
+  vec3 sunDir = normalize(vec3(1, 1, -1));
+  vec3 worldSpaceNormal = normalize(vNormal);
+
+  // Bit of a hack to remove lighting on dark side of planet
+  vec3 diffuse = vec3(0.75);
+  vec3 planetNormal = normalize(worldPosition);
+  float planetLighting = saturate(dot(planetNormal, sunDir));
+
+  vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection);
+  vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse + lighting.w * 0.1;
+  // vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25);
+
+  finalColour *= lighting.xyz;
+  finalColour = lighting.xyz;
+  // finalColour = vColor.xyz;
+
+  out_FragColor = vec4(finalColour, 1);
+  gl_FragDepth = log2(vFragDepth) * logDepthBufFC * 0.5;
+}
+
+  `;
+  
+  return {
+    VS: _VS,
+    PS: _PS,
+  };
+})();
+  

+ 338 - 0
src/terrain.js

@@ -0,0 +1,338 @@
+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 {terrain_shader} from './terrain-shader.js';
+import {terrain_builder_threaded} from './terrain-builder-threaded.js';
+import {terrain_constants} from './terrain-constants.js';
+import {texture_splatter} from './texture-splatter.js';
+import {textures} from './textures.js';
+import {utils} from './utils.js';
+
+export const terrain = (function() {
+
+  class TerrainChunkManager {
+    constructor(params) {
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this.params_ = params;
+
+      this.builder_ = new terrain_builder_threaded.TerrainChunkRebuilder_Threaded();
+      // this.builder_ = new terrainbuilder_.TerrainChunkRebuilder();
+
+      this.LoadTextures_();
+
+      this.InitNoise_(params);
+      this.InitBiomes_(params);
+      this.InitTerrain_(params);
+    }
+
+    LoadTextures_() {
+      const loader = new THREE.TextureLoader();
+
+      const noiseTexture = loader.load('./resources/simplex-noise.png');
+      noiseTexture.wrapS = THREE.RepeatWrapping;
+      noiseTexture.wrapT = THREE.RepeatWrapping;
+
+      this.material_ = new THREE.RawShaderMaterial({
+        uniforms: {
+          diffuseMap: {
+          },
+          normalMap: {
+          },
+          noiseMap: {
+            value: noiseTexture
+          },
+          logDepthBufFC: {
+            value: 2.0 / (Math.log(this.params_.camera.far + 1.0) / Math.LN2),
+          }
+        },
+        vertexShader: terrain_shader.VS,
+        fragmentShader: terrain_shader.PS,
+        side: THREE.FrontSide
+      });
+    }
+
+    InitNoise_(params) {
+      params.guiParams.noise = {
+        octaves: 13,
+        persistence: 0.5,
+        lacunarity: 1.6,
+        exponentiation: 7.5,
+        height: terrain_constants.NOISE_HEIGHT,
+        scale: terrain_constants.NOISE_SCALE,
+        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, 20000).onChange(
+          onNoiseChanged);
+
+      this.noise_ = new noise.Noise(params.guiParams.noise);
+      this.noiseParams_ = 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,
+        scale: 2048.0,
+        noiseType: 'simplex',
+        seed: 2,
+        exponentiation: 1,
+        height: 1.0
+      };
+
+      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);
+      this.biomesParams_ = params.guiParams.biomes;
+
+      const colourParams = {
+        octaves: 1,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 1.0,
+        scale: 256.0,
+        noiseType: 'simplex',
+        seed: 2,
+        height: 1.0,
+      };
+      this.colourNoise_ = new noise.Noise(colourParams);
+      this.colourNoiseParams_ = colourParams;
+    }
+
+    InitTerrain_(params) {
+      params.guiParams.terrain = {
+        wireframe: false,
+        fixedCamera: 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.SetWireframe(params.guiParams.terrain.wireframe);
+        }
+      });
+
+      terrainRollup.add(params.guiParams.terrain, "fixedCamera");
+
+      this.chunks_ = {};
+      this.params_ = params;
+    }
+
+    _CreateTerrainChunk(group, groupTransform, offset, cameraPosition, width, neighbours, resolution) {
+      const params = {
+        group: group,
+        transform: groupTransform,
+        material: this.material_,
+        width: width,
+        offset: offset,
+        origin: cameraPosition.clone(),
+        radius: terrain_constants.PLANET_RADIUS,
+        resolution: resolution,
+        neighbours: neighbours,
+        biomeGenerator: this.biomes_,
+        colourGenerator: new texture_splatter.TextureSplatter(
+            {biomeGenerator: this.biomes_, colourNoise: this.colourNoise_}),
+        heightGenerators: [new texture_splatter.HeightGenerator(
+            this.noise_, offset, 100000, 100000 + 1)],
+        noiseParams: this.noiseParams_,
+        colourNoiseParams: this.colourNoiseParams_,
+        biomesParams: this.biomesParams_,
+        colourGeneratorParams: {
+          biomeGeneratorParams: this.biomesParams_,
+          colourNoiseParams: this.colourNoiseParams_,
+        },
+        heightGeneratorsParams: {
+          min: 100000,
+          max: 100000 + 1,
+        }
+      };
+
+      return this.builder_.AllocateChunk(params);
+    }
+
+    Update(_) {
+      const cameraPosition = this.params_.camera.position.clone();
+      if (this.params_.guiParams.terrain.fixedCamera) {
+        cameraPosition.copy(this.cachedCamera_);
+      } else {
+        this.cachedCamera_ = cameraPosition.clone();
+      }
+
+      this.builder_.Update();
+      if (!this.builder_.Busy) {
+        for (let k in this.chunks_) {
+          this.chunks_[k].chunk.Show();
+        }
+        this.UpdateVisibleChunks_Quadtree_(cameraPosition);
+      }
+
+      for (let k in this.chunks_) {
+        this.chunks_[k].chunk.Update(this.params_.camera.position);
+      }
+      for (let c of this.builder_.old_) {
+        c.chunk.Update(this.params_.camera.position);
+      }
+
+      this.params_.scattering.uniforms.planetRadius.value = terrain_constants.PLANET_RADIUS;
+      this.params_.scattering.uniforms.atmosphereRadius.value = terrain_constants.PLANET_RADIUS * 1.01;
+    }
+
+    UpdateVisibleChunks_Quadtree_(cameraPosition) {
+      function _Key(c) {
+        return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']';
+      }
+
+      const q = new quadtree.CubeQuadTree({
+        radius: terrain_constants.PLANET_RADIUS,
+        min_node_size: terrain_constants.QT_MIN_CELL_SIZE,
+        max_node_size: terrain_constants.QT_MAX_CELL_SIZE,
+      });
+      q.Insert(cameraPosition);
+      q.BuildNeighbours();
+
+      const sides = q.GetChildren();
+
+      let newTerrainChunks = {};
+      const center = new THREE.Vector3();
+      const dimensions = new THREE.Vector3();
+
+      const _Child = (c) => {
+        c.bounds.getCenter(center);
+        c.bounds.getSize(dimensions);
+
+        const child = {
+          index: c.side,
+          group: this.groups_[c.side],
+          transform: sides[c.side].transform,
+          position: [center.x, center.y, center.z],
+          bounds: c.bounds,
+          size: dimensions.x,
+          neighbours: c.neighbours.map(n => n.size.x / c.size.x),
+          neighboursOriginal: c.neighbours,
+        };
+        return child;
+      };
+
+      for (let i = 0; i < sides.length; i++) {
+        for (let c of sides[i].children) {
+          const child = _Child(c);
+          const k = _Key(child);
+
+          const left = c.neighbours[0].GetClosestChildrenSharingEdge(c.GetLeftEdgeMidpoint());
+          const top = c.neighbours[1].GetClosestChildrenSharingEdge(c.GetTopEdgeMidpoint());
+          const right = c.neighbours[2].GetClosestChildrenSharingEdge(c.GetRightEdgeMidpoint());
+          const bottom = c.neighbours[3].GetClosestChildrenSharingEdge(c.GetBottomEdgeMidpoint());
+
+          child.neighbourKeys = [...left, ...top, ...right, ...bottom].map(n => _Key(_Child(n)));
+          child.debug = [left, top, right, bottom];
+  
+          newTerrainChunks[k] = child;
+        }
+      }
+
+
+      const allChunks = newTerrainChunks;
+      const intersection = utils.DictIntersection(this.chunks_, newTerrainChunks);
+      const difference = utils.DictDifference(newTerrainChunks, this.chunks_);
+      const recycle = Object.values(utils.DictDifference(this.chunks_, newTerrainChunks));
+
+      if (0) {
+        const partialRebuilds = {};
+
+        for (let k in difference) {
+          for (let n of difference[k].neighbourKeys) {
+            if (n in this.chunks_) {
+              partialRebuilds[n] = newTerrainChunks[n];
+            }
+          }
+        }
+        for (let k in partialRebuilds) {
+          if (k in intersection) {
+            recycle.push(this.chunks_[k]);
+            delete intersection[k];
+            difference[k] = allChunks[k];
+          }
+        }
+      }
+
+      this.builder_.RetireChunks(recycle);
+
+      newTerrainChunks = intersection;
+
+      const partialRebuilds = {};
+
+      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, difference[k].transform,
+              offset, cameraPosition, difference[k].size, difference[k].neighbours,
+              terrain_constants.QT_MIN_CELL_RESOLUTION),
+        };
+
+        for (let n of difference[k].neighbourKeys) {
+          if (n in this.chunks_) {
+            partialRebuilds[n] = intersection[n];
+            partialRebuilds[n].chunk.params_.neighbours = allChunks[n].neighbours;
+          }
+        }
+      }
+
+      this.builder_.QuickRebuild(partialRebuilds);
+
+      this.chunks_ = newTerrainChunks;
+    }
+  }
+
+  return {
+    TerrainChunkManager: TerrainChunkManager
+  }
+})();

+ 188 - 0
src/texture-splatter.js

@@ -0,0 +1,188 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {math} from './math.js';
+import {spline} from './spline.js';
+import {terrain_constants} from './terrain-constants.js';
+
+
+export const texture_splatter = (function() {
+
+  const _HEIGHT_NORMALIZATION = terrain_constants.NOISE_HEIGHT / 10.0;
+
+  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_BOREAL = new THREE.Color(0x29c100);
+  
+  const _GREEN = new THREE.Color(0x80FF80);
+  const _RED = new THREE.Color(0xFF8080);
+  const _BLACK = new THREE.Color(0x000000);
+
+
+  class FixedHeightGenerator {
+    constructor() {}
+  
+    Get() {
+      return [50, 1];
+    }
+  }
+  
+
+  class FixedColourGenerator {
+    constructor(params) {
+      this._params = params;
+    }
+  
+    Get() {
+      return this._params.colour;
+    }
+  }
+
+
+  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 TextureSplatter {
+    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;
+    }
+
+    _BaseColour(x, y, z) {
+      const m = this._params.biomeGenerator.Get(x, y, z);
+      const h = math.sat(z / 100.0);
+
+      const c1 = this._colourSpline[0].Get(h);
+      const c2 = this._colourSpline[1].Get(h);
+
+      let c = c1.lerp(c2, m);
+
+      if (h < 0.1) {
+        c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05));
+      }
+      return c;      
+    }
+
+    _Colour(x, y, z) {
+      const c = this._BaseColour(x, y, z);
+      const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0;
+
+      c.offsetHSL(0.0, 0.0, r * 0.01);
+      return c;
+    }
+
+    _GetTextureWeights(p, n, up) {
+      const m = this._params.biomeGenerator.Get(p.x, p.y, p.z);
+      const h = p.z / _HEIGHT_NORMALIZATION;
+
+      const types = {
+        dirt: {index: 0, strength: 0.0},
+        grass: {index: 1, strength: 0.0},
+        gravel: {index: 2, strength: 0.0},
+        rock: {index: 3, strength: 0.0},
+        snow: {index: 4, strength: 0.0},
+        snowrock: {index: 5, strength: 0.0},
+        cobble: {index: 6, strength: 0.0},
+        sandyrock: {index: 7, strength: 0.0},
+      };
+
+      function _ApplyWeights(dst, v, m) {
+        for (let k in types) {
+          types[k].strength *= m;
+        }
+        types[dst].strength = v;
+      };
+
+      types.grass.strength = 1.0;
+      _ApplyWeights('gravel', 1.0 - m, m);
+
+      if (h < 0.2) {
+        const s = 1.0 - math.sat((h - 0.1) / 0.05);
+        _ApplyWeights('cobble', s, 1.0 - s);
+
+        if (h < 0.1) {
+          const s = 1.0 - math.sat((h - 0.05) / 0.05);
+          _ApplyWeights('sandyrock', s, 1.0 - s);
+        }
+      } else {
+        if (h > 0.125) {
+          const s = (math.sat((h - 0.125) / 1.25));
+          _ApplyWeights('rock', s, 1.0 - s);
+        }
+
+        if (h > 1.5) {
+          const s = math.sat((h - 0.75) / 2.0);
+          _ApplyWeights('snow', s, 1.0 - s);
+        }
+      }
+
+      // In case nothing gets set.
+      types.dirt.strength = 0.01;
+
+      let total = 0.0;
+      for (let k in types) {
+        total += types[k].strength;
+      }
+      if (total < 0.01) {
+        const a = 0;
+      }
+      const normalization = 1.0 / total;
+
+      for (let k in types) {
+        types[k].strength / normalization;
+      }
+
+      return types;
+    }
+
+    GetColour(position) {
+      return this._Colour(position.x, position.y, position.z);
+    }
+
+    GetSplat(position, normal, up) {
+      return this._GetTextureWeights(position, normal, up);
+    }
+  }
+
+  return {
+    HeightGenerator: HeightGenerator,
+    TextureSplatter: TextureSplatter,
+  }
+})();

+ 80 - 0
src/textures.js

@@ -0,0 +1,80 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const textures = (function() {
+
+  // Taken from https://github.com/mrdoob/three.js/issues/758
+  function _GetImageData( image ) {
+    var canvas = document.createElement('canvas');
+    canvas.width = image.width;
+    canvas.height = image.height;
+
+    var context = canvas.getContext('2d');
+    context.drawImage( image, 0, 0 );
+
+    return context.getImageData( 0, 0, image.width, image.height );
+  }
+
+  return {
+    TextureAtlas: class {
+      constructor(params) {
+        this.game_ = params.game;
+        this.Create_();
+        this.onLoad = () => {};
+      }
+
+      Load(atlas, names) {
+        this.LoadAtlas_(atlas, names);
+      }
+
+      Create_() {
+        this.manager_ = new THREE.LoadingManager();
+        this.loader_ = new THREE.TextureLoader(this.manager_);
+        this.textures_ = {};
+
+        this.manager_.onLoad = () => {
+          this.OnLoad_();
+        };
+      }
+
+      get Info() {
+        return this.textures_;
+      }
+
+      OnLoad_() {
+        for (let k in this.textures_) {
+          const atlas = this.textures_[k];
+          const data = new Uint8Array(atlas.textures.length * 4 * 1024 * 1024);
+
+          for (let t = 0; t < atlas.textures.length; t++) {
+            const curTexture = atlas.textures[t];
+            const curData = _GetImageData(curTexture.image);
+            const offset = t * (4 * 1024 * 1024);
+
+            data.set(curData.data, offset);
+          }
+    
+          const diffuse = new THREE.DataTexture2DArray(data, 1024, 1024, atlas.textures.length);
+          diffuse.format = THREE.RGBAFormat;
+          diffuse.type = THREE.UnsignedByteType;
+          diffuse.minFilter = THREE.LinearMipMapLinearFilter;
+          diffuse.magFilter = THREE.LinearFilter;
+          diffuse.wrapS = THREE.RepeatWrapping;
+          diffuse.wrapT = THREE.RepeatWrapping;
+          diffuse.generateMipmaps = true;
+          diffuse.encoding = THREE.sRGBEncoding;
+
+          atlas.atlas = diffuse;
+        }
+
+        this.onLoad();
+      }
+
+      LoadAtlas_(atlas, names) {
+        this.textures_[atlas] = {
+          textures: names.map(n => this.loader_.load(n))
+        };
+      }
+    }
+  };
+})();

+ 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;
+    }
+  };
+})();