Browse Source

Uploaded.

Simon 4 years ago
parent
commit
f911091c59
51 changed files with 6309 additions and 0 deletions
  1. 67 0
      base.css
  2. 20 0
      index.html
  3. BIN
      resources/minecraft/textures/blocks/dirt.png
  4. BIN
      resources/minecraft/textures/blocks/grass-side.png
  5. BIN
      resources/minecraft/textures/blocks/grass.png
  6. BIN
      resources/minecraft/textures/blocks/moon.png
  7. BIN
      resources/minecraft/textures/blocks/ocean.png
  8. BIN
      resources/minecraft/textures/blocks/sand-side.png
  9. BIN
      resources/minecraft/textures/blocks/sand.png
  10. BIN
      resources/minecraft/textures/blocks/snow-side.png
  11. BIN
      resources/minecraft/textures/blocks/snow.png
  12. BIN
      resources/minecraft/textures/blocks/stone-side.png
  13. BIN
      resources/minecraft/textures/blocks/stone.png
  14. BIN
      resources/minecraft/textures/blocks/tree-bark.png
  15. BIN
      resources/minecraft/textures/blocks/tree-leaves.png
  16. BIN
      resources/pickaxe/scene.bin
  17. 231 0
      resources/pickaxe/scene.gltf
  18. BIN
      resources/pickaxe/textures/Material.009_baseColor.png
  19. BIN
      resources/simplex-noise.png
  20. BIN
      resources/terrain/space-negx.jpg
  21. BIN
      resources/terrain/space-negy.jpg
  22. BIN
      resources/terrain/space-negz.jpg
  23. BIN
      resources/terrain/space-posx.jpg
  24. BIN
      resources/terrain/space-posy.jpg
  25. BIN
      resources/terrain/space-posz.jpg
  26. BIN
      resources/ui/hotbar.png
  27. 103 0
      src/cloud-controller.js
  28. 16 0
      src/defs.js
  29. 88 0
      src/entity-manager.js
  30. 168 0
      src/entity.js
  31. 358 0
      src/foliage-sdfs.js
  32. 44 0
      src/hack-defs.js
  33. 103 0
      src/main.js
  34. 42 0
      src/math.js
  35. 59 0
      src/noise.js
  36. 346 0
      src/player-controller.js
  37. 479 0
      src/simplex-noise.js
  38. 338 0
      src/sparse-voxel-cell-manager.js
  39. 55 0
      src/texture-defs.js
  40. 88 0
      src/textures.js
  41. 52 0
      src/third-person-camera.js
  42. 278 0
      src/threejs-component.js
  43. 83 0
      src/ui-controller.js
  44. 21 0
      src/utils.js
  45. 1338 0
      src/voxel-block-builder.js
  46. 16 0
      src/voxel-builder-threaded-worker.js
  47. 368 0
      src/voxel-builder-threaded.js
  48. 651 0
      src/voxel-shader.js
  49. 459 0
      src/voxel-tools.js
  50. 366 0
      src/water.js
  51. 72 0
      src/worker-pool.js

+ 67 - 0
base.css

@@ -0,0 +1,67 @@
+body {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}
+
+.container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.ui {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  font-family: 'IM Fell French Canon', serif;
+}
+
+.icon-bar-layout {
+  position: absolute;
+  bottom: 10px;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.icon-bar {
+  width: 380px;
+  height: 44px;
+  display: flex;
+  justify-content: left;
+  background-image: url('./resources/ui/hotbar.png');
+  background-size: cover;
+  image-rendering: pixelated;
+}
+
+.icon-bar-item {
+  background-size: cover;
+  width: 36px;
+  height: 36px;
+  margin: 4px 3px;
+  background-size: cover;
+  image-rendering: pixelated;
+}
+
+.icon-bar-item:first-child {
+  margin-left: 4px;
+}
+
+.icon-bar-item.highlight {
+  outline: 6px solid rgb(237, 250, 116);
+  animation: blink-animation 0.5s ease-in-out infinite;
+  box-shadow: 0 0 20px #000000;
+}
+
+@keyframes blink-animation {
+  to {
+    opacity: 0.75;
+  }
+}

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SiMineCraft</title>
+  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+  <link rel="stylesheet" type="text/css" href="base.css">
+</head>
+<body>
+  <div class="ui" id="game-ui">
+    <div class="container" id="container">
+      <div class="icon-bar-layout">
+        <div class="icon-bar" id="icon-bar">
+        </div>
+      </div>
+    </div>
+  </div>
+  <script src="./src/main.js" type="module">
+  </script>
+</body>
+</html>

BIN
resources/minecraft/textures/blocks/dirt.png


BIN
resources/minecraft/textures/blocks/grass-side.png


BIN
resources/minecraft/textures/blocks/grass.png


BIN
resources/minecraft/textures/blocks/moon.png


BIN
resources/minecraft/textures/blocks/ocean.png


BIN
resources/minecraft/textures/blocks/sand-side.png


BIN
resources/minecraft/textures/blocks/sand.png


BIN
resources/minecraft/textures/blocks/snow-side.png


BIN
resources/minecraft/textures/blocks/snow.png


BIN
resources/minecraft/textures/blocks/stone-side.png


BIN
resources/minecraft/textures/blocks/stone.png


BIN
resources/minecraft/textures/blocks/tree-bark.png


BIN
resources/minecraft/textures/blocks/tree-leaves.png


BIN
resources/pickaxe/scene.bin


+ 231 - 0
resources/pickaxe/scene.gltf

@@ -0,0 +1,231 @@
+{
+  "accessors": [
+    {
+      "bufferView": 2,
+      "componentType": 5126,
+      "count": 458,
+      "max": [
+        13,
+        13,
+        1
+      ],
+      "min": [
+        -13,
+        -13,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 5496,
+      "componentType": 5126,
+      "count": 458,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "componentType": 5126,
+      "count": 458,
+      "max": [
+        1,
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1,
+        1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "componentType": 5126,
+      "count": 458,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "componentType": 5125,
+      "count": 1248,
+      "max": [
+        457
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "extras": {
+      "author": "Blender3D (https://sketchfab.com/Blender3D)",
+      "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
+      "source": "https://sketchfab.com/models/b1bc7d6a3db246d5b47449ae2b2706fd",
+      "title": "Minecraft Diamond-Pickaxe"
+    },
+    "generator": "Sketchfab-3.23.2",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 4992,
+      "byteOffset": 0,
+      "name": "floatBufferViews",
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteLength": 3664,
+      "byteOffset": 4992,
+      "byteStride": 8,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 10992,
+      "byteOffset": 8656,
+      "byteStride": 12,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 7328,
+      "byteOffset": 19648,
+      "byteStride": 16,
+      "name": "floatBufferViews",
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 26976,
+      "uri": "scene.bin"
+    }
+  ],
+  "images": [
+    {
+      "uri": "textures/Material.009_baseColor.png"
+    }
+  ],
+  "materials": [
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.009",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 0,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "roughnessFactor": 0.16121211199999996
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 0,
+            "TANGENT": 2,
+            "TEXCOORD_0": 3
+          },
+          "indices": 4,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    }
+  ],
+  "nodes": [
+    {
+      "children": [
+        1
+      ],
+      "name": "RootNode (gltf orientation matrix)",
+      "rotation": [
+        -0.70710678118654746,
+        -0,
+        -0,
+        0.70710678118654757
+      ]
+    },
+    {
+      "children": [
+        2
+      ],
+      "name": "RootNode (model correction matrix)"
+    },
+    {
+      "children": [
+        3
+      ],
+      "name": "Diamond-Pickaxe.obj.cleaner.materialmerger.gles"
+    },
+    {
+      "mesh": 0,
+      "name": ""
+    }
+  ],
+  "samplers": [
+    {
+      "magFilter": 9729,
+      "minFilter": 9987,
+      "wrapS": 10497,
+      "wrapT": 10497
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "OSG_Scene",
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "textures": [
+    {
+      "sampler": 0,
+      "source": 0
+    }
+  ]
+}
+

BIN
resources/pickaxe/textures/Material.009_baseColor.png


BIN
resources/simplex-noise.png


BIN
resources/terrain/space-negx.jpg


BIN
resources/terrain/space-negy.jpg


BIN
resources/terrain/space-negz.jpg


BIN
resources/terrain/space-posx.jpg


BIN
resources/terrain/space-posy.jpg


BIN
resources/terrain/space-posz.jpg


BIN
resources/ui/hotbar.png


+ 103 - 0
src/cloud-controller.js

@@ -0,0 +1,103 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {entity} from './entity.js';
+import {hack_defs} from './hack-defs.js';
+import {math} from './math.js';
+
+import {voxel_shader} from './voxel-shader.js';
+
+
+export const cloud_controller = (function() {
+
+  class CloudController extends entity.Component {
+    constructor() {
+      super();
+      this.clouds_ = []
+    }
+
+    InitEntity() {
+      const threejs = this.FindEntity('renderer').GetComponent('ThreeJSController');
+
+      const geo = new THREE.BoxGeometry(1, 1, 1);
+
+      this.group_ = new THREE.Group();
+
+      for (let i = 0; i < 20; ++i) {
+        const w = math.rand_int(5, 10) * 20;
+        const l = math.rand_int(5, 10) * 20;
+
+        const x = math.rand_int(-150, 150) * 10;
+        const y = math.rand_int(0, 10) * 25;
+        const z = math.rand_int(-150, 150) * 10;
+
+        const mat = new THREE.ShaderMaterial({
+            uniforms: {
+              cloudMin: {
+                value: null,
+              },
+              cloudMax: {
+                value: null,
+              },
+            },
+            vertexShader: voxel_shader.CLOUD.VS,
+            fragmentShader: voxel_shader.CLOUD.PS,
+            side: THREE.FrontSide,
+            // blending: THREE.AdditiveBlending,
+            transparent: true,
+        });
+        const box = new THREE.Mesh(geo, mat);
+        box.position.set(x, y, z);
+        box.scale.set(w, 50, l);
+
+        this.group_.add(box);
+        this.clouds_.push(box);
+      }
+
+      this.group_.visible = !hack_defs.skipClouds;
+
+      threejs.scene_.add(this.group_);
+
+      this.CreateSun_();
+    }
+
+    CreateSun_() {
+      const geo = new THREE.PlaneGeometry(300, 300);
+
+      const mat = new THREE.ShaderMaterial({
+          uniforms: {},
+          vertexShader: voxel_shader.SUN.VS,
+          fragmentShader: voxel_shader.SUN.PS,
+          side: THREE.FrontSide,
+          transparent: true,
+          blending: THREE.AdditiveBlending,
+      });
+      const sun = new THREE.Mesh(geo, mat);
+      sun.position.set(692, 39, -286);
+      sun.rotateX(0.5 * 2.0 * Math.PI);
+      sun.lookAt(0, 0, 0);
+
+      this.group_.add(sun);
+    }
+
+    Update(_) {
+      const player = this.FindEntity('player');
+      const cameraPosition = player.Position;
+
+      this.group_.position.set(cameraPosition.x, 250, cameraPosition.z);
+
+      for (let i = 0; i < this.clouds_.length; ++i) {
+        const cloud = this.clouds_[i];
+        cloud.updateMatrixWorld(true);
+        const mat = cloud.material;
+        mat.uniforms.cloudMin.value = new THREE.Vector3(-0.5, -0.5, -0.5);
+        mat.uniforms.cloudMax.value = new THREE.Vector3(0.5, 0.5, 0.5);
+        mat.uniforms.cloudMin.value.applyMatrix4(cloud.matrixWorld);
+        mat.uniforms.cloudMax.value.applyMatrix4(cloud.matrixWorld);
+      }
+    }
+  }
+
+  return {
+      CloudController: CloudController
+  };
+})();

+ 16 - 0
src/defs.js

@@ -0,0 +1,16 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const defs = (() => {
+
+  return {
+      FOG_RANGE: [100, 300],
+      UNDERWATER_RANGE: [0, 50],
+      FOG_COLOUR: new THREE.Color(0xcddef1).convertSRGBToLinear(),
+      MOON_COLOUR: new THREE.Color(0x808080).convertSRGBToLinear(),
+      UNDERWATER_COLOUR: new THREE.Color(0x3a6fb5).convertSRGBToLinear(),
+      SKY_COLOUR: new THREE.Color(0x7cbff6).convertSRGBToLinear(),
+      PLAYER_POS: [255.311252087425, 100, 290.98564212457086],
+      PLAYER_ROT: [0.02753162419089479, -0.7573631733845853, 0.031998988835540886, 0.6516280365237096],
+  };
+})();

+ 88 - 0
src/entity-manager.js

@@ -0,0 +1,88 @@
+
+
+export const entity_manager = (() => {
+
+  class EntityManager {
+    constructor() {
+      this._ids = 0;
+      this._entitiesMap = {};
+      this._entities = [];
+    }
+
+    _GenerateName() {
+      this._ids += 1;
+
+      return '__name__' + this._ids;
+    }
+
+    Get(n) {
+      return this._entitiesMap[n];
+    }
+
+    Filter(cb) {
+      return this._entities.filter(cb);
+    }
+
+    Add(e, n) {
+      if (!n) {
+        n = this._GenerateName();
+      }
+
+      this._entitiesMap[n] = e;
+      this._entities.push(e);
+
+      e.SetParent(this);
+      e.SetName(n);
+      e.InitEntity();
+    }
+
+    SetActive(e, b) {
+      const i = this._entities.indexOf(e);
+
+      if (!b) {
+        if (i < 0) {
+          return;
+        }
+  
+        this._entities.splice(i, 1);
+      } else {
+        if (i >= 0) {
+          return;
+        }
+
+        this._entities.push(e);
+      }
+    }
+
+    Update(timeElapsed) {
+      const dead = [];
+      const alive = [];
+      for (let i = 0; i < this._entities.length; ++i) {
+        const e = this._entities[i];
+
+        e.Update(timeElapsed);
+
+        if (e.dead_) {
+          dead.push(e);
+        } else {
+          alive.push(e);
+        }
+      }
+
+      for (let i = 0; i < dead.length; ++i) {
+        const e = dead[i];
+
+        delete this._entitiesMap[e.Name];
+  
+        e.Destroy();
+      }
+
+      this._entities = alive;
+    }
+  }
+
+  return {
+    EntityManager: EntityManager
+  };
+
+})();

+ 168 - 0
src/entity.js

@@ -0,0 +1,168 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const entity = (() => {
+
+  class Entity {
+    constructor() {
+      this._name = null;
+      this._components = {};
+
+      this._position = new THREE.Vector3();
+      this._rotation = new THREE.Quaternion();
+      this._handlers = {};
+      this.parent_ = null;
+      this.dead_ = false;
+    }
+
+    Destroy() {
+      for (let k in this._components) {
+        this._components[k].Destroy();
+      }
+      this._components = null;
+      this.parent_ = null;
+      this._handlers = null;
+    }
+
+    _RegisterHandler(n, h) {
+      if (!(n in this._handlers)) {
+        this._handlers[n] = [];
+      }
+      this._handlers[n].push(h);
+    }
+
+    SetParent(p) {
+      this.parent_ = p;
+    }
+
+    SetName(n) {
+      this._name = n;
+    }
+
+    get Name() {
+      return this._name;
+    }
+
+    get Manager() {
+      return this.parent_;
+    }
+
+    SetActive(b) {
+      this.parent_.SetActive(this, b);
+    }
+
+    SetDead() {
+      this.dead_ = true;
+    }
+
+    AddComponent(c) {
+      c.SetParent(this);
+      this._components[c.constructor.name] = c;
+
+      c.InitComponent();
+    }
+
+    InitEntity() {
+      for (let k in this._components) {
+        this._components[k].InitEntity();
+      }
+    }
+
+    GetComponent(n) {
+      return this._components[n];
+    }
+
+    FindEntity(n) {
+      return this.parent_.Get(n);
+    }
+
+    Broadcast(msg) {
+      if (!(msg.topic in this._handlers)) {
+        return;
+      }
+
+      for (let curHandler of this._handlers[msg.topic]) {
+        curHandler(msg);
+      }
+    }
+
+    SetPosition(p) {
+      this._position.copy(p);
+      this.Broadcast({
+          topic: 'update.position',
+          value: this._position,
+      });
+    }
+
+    SetQuaternion(r) {
+      this._rotation.copy(r);
+      this.Broadcast({
+          topic: 'update.rotation',
+          value: this._rotation,
+      });
+    }
+
+    get Position() {
+      return this._position;
+    }
+
+    get Quaternion() {
+      return this._rotation;
+    }
+
+    Update(timeElapsed) {
+      for (let k in this._components) {
+        this._components[k].Update(timeElapsed);
+      }
+    }
+  };
+
+  class Component {
+    constructor() {
+      this.parent_ = null;
+    }
+
+    Destroy() {
+    }
+
+    SetParent(p) {
+      this.parent_ = p;
+    }
+
+    InitComponent() {}
+    
+    InitEntity() {}
+
+    GetComponent(n) {
+      return this.parent_.GetComponent(n);
+    }
+
+    get Manager() {
+      return this.parent_.Manager;
+    }
+
+    get Parent() {
+      return this.parent_;
+    }
+
+    FindEntity(n) {
+      return this.parent_.FindEntity(n);
+    }
+
+    Broadcast(m) {
+      this.parent_.Broadcast(m);
+    }
+
+    Update(_) {}
+
+    _RegisterHandler(n, h) {
+      this.parent_._RegisterHandler(n, h);
+    }
+  };
+
+  return {
+    Entity: Entity,
+    Component: Component,
+  };
+
+})();

+ 358 - 0
src/foliage-sdfs.js

@@ -0,0 +1,358 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {noise} from './noise.js';
+import {math} from './math.js';
+
+
+export const foliage_sdfs = (() => {
+
+  function sdCappedCone(p, a, b, ra, rb) {
+    const ba = b.clone().sub(a);
+    const pa = p.clone().sub(a);
+    const rba  = rb - ra;
+    const baba = ba.dot(ba);
+    const papa = pa.dot(pa);
+    const paba = pa.dot(ba) / baba;
+    const x = Math.sqrt(papa - paba * paba * baba);
+    const cax = Math.max(0.0, x - ((paba < 0.5) ? ra : rb));
+    const cay = Math.abs(paba - 0.5) - 0.5;
+    const k = rba * rba + baba;
+    const f = math.sat((rba * (x - ra) + paba * baba) / k);
+    const cbx = x - ra - f * rba;
+    const cby = paba - f;
+    const s = (cbx < 0.0 && cay < 0.0) ? -1.0 : 1.0;
+    return s * Math.sqrt(
+        Math.min(cax * cax + cay * cay * baba,
+                 cbx * cbx + cby * cby * baba));
+  }
+  
+  function sdCappedCylinder(p, a, b, r) {
+    const ba = b.clone().sub(a);
+    const pa = p.clone().sub(a);
+    const baba = ba.dot(ba);
+    const paba = pa.dot(ba);
+  
+    // const x = length(pa*baba-ba*paba) - r*baba;
+    const x = (pa.clone().multiplyScalar(baba).sub(ba.clone().multiplyScalar(paba))).length() - r * baba;
+    const y = Math.abs(paba - baba * 0.5) - baba * 0.5;
+    const x2 = x * x;
+    const y2 = y * y * baba;
+    const d = (Math.max(x, y) < 0.0) ?
+        -Math.min(x2, y2) :
+        (((x > 0.0) ? x2 : 0.0) + ((y > 0.0) ? y2 : 0.0));
+    return Math.sign(d) * Math.sqrt(Math.abs(d)) / baba;
+  }
+  
+  function sdSphere(pos, radius) {
+    return pos.length() - radius;
+  }
+  
+  
+  const _TMP1 = new THREE.Vector3();
+  const _TMP_B1 = new THREE.Box3();
+  const _TMP_B2 = new THREE.Box3();
+  const _TMP_B3 = new THREE.Box3();
+  const _TMP_S1 = new THREE.Sphere();
+  const _TMP_Q = new THREE.Quaternion();
+  const _TMP_Q1 = new THREE.Quaternion();
+  const _TMP_Q2 = new THREE.Quaternion();
+  const _X_AXIS = new THREE.Vector3(1, 0, 0);
+  const _Y_AXIS = new THREE.Vector3(0, 1, 0);
+  const _Z_AXIS = new THREE.Vector3(0, 0, 1);
+  const _ORIGIN = new THREE.Vector3();
+  
+  class SDF {
+    constructor(pos) {
+      this.sdfs_ = [];
+      this.pos_ = pos.clone();
+      this.aabb_ = new THREE.Box3(
+          this.pos_.clone(), this.pos_.clone());
+    }
+  
+    get AABB() {
+      return this.aabb_;
+    }
+  
+    AddSphere(type, origin, radius) {
+      _TMP_S1.set(this.pos_.clone(), radius);
+      _TMP_S1.translate(origin);
+      _TMP_S1.getBoundingBox(_TMP_B1);
+  
+      this.aabb_.union(_TMP_B1);
+  
+      const o = origin.clone();
+  
+      this.sdfs_.push((pos) => {
+        _TMP1.copy(pos);
+        _TMP1.sub(o);
+        _TMP1.sub(this.pos_);
+  
+        if (sdSphere(_TMP1, radius) < 0) {
+          return type;
+        }
+        return null;
+      });
+    }
+  
+    AddCappedCone(type, offset, start, end, startRadius, endRadius) {
+      _TMP_S1.set(start.clone(), startRadius);
+      _TMP_S1.getBoundingBox(_TMP_B2);
+  
+      _TMP_S1.set(end.clone(), endRadius);
+      _TMP_S1.getBoundingBox(_TMP_B3);
+  
+      _TMP_B1.makeEmpty();
+      _TMP_B1.union(_TMP_B2);
+      _TMP_B1.union(_TMP_B3);
+      _TMP_B1.translate(offset);
+      _TMP_B1.translate(this.pos_);
+  
+      this.aabb_.union(_TMP_B1);
+  
+      const s = start.clone();
+      const e = end.clone();
+      const o = offset.clone();
+  
+      this.sdfs_.push((pos) => {
+        _TMP1.copy(pos);
+        _TMP1.sub(o);
+        _TMP1.sub(this.pos_);
+  
+        if (sdCappedCone(_TMP1, s, e, startRadius, endRadius) < 0) {
+          return type;
+        }
+        return null;
+      });
+    }
+  
+    Evaluate(pos) {
+      for (let i = 0; i < this.sdfs_.length; ++i) {
+        const res = this.sdfs_[i](pos);
+        if (res) {
+          return res;
+        }
+      }
+      return null;
+    }
+  };
+
+  const _N_Foliage = new noise.Noise({
+    seed: 7,
+    octaves: 1,
+    scale: 1,
+    persistence: 0.5,
+    lacunarity: 2.0,
+    exponentiation: 1,
+    height: 1,
+  });
+
+  function SPHERE(xPos, yPos, zPos, radius) {
+    const treeSDF = new SDF(new THREE.Vector3(xPos, yPos, zPos));
+    treeSDF.AddSphere('stone', new THREE.Vector3(), radius);
+    return treeSDF;
+  };
+
+  function CONE1(xPos, yPos, zPos) {
+    const treeSDF = new SDF(new THREE.Vector3(xPos, yPos, zPos));
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 20, 0), 5, 5);
+    return treeSDF;
+  };
+
+  function TREE1(xPos, yPos, zPos) {
+    // HACK
+    const height = 15;//_N_Foliage.Get(xPos, 1.0, zPos) * 20 + 10;
+    const lean = 5;//_N_Foliage.Get(xPos, 2.0, zPos) * 5 + 4;
+    const trunkEnd = new THREE.Vector3(lean, height, 0);
+    const rootEnd1 = new THREE.Vector3(-6, 0, 1);
+    const rootEnd2 = new THREE.Vector3(9, 0, -7);
+    const rootEnd3 = new THREE.Vector3(8, 0, 6);
+
+    const leavesRadius = 4;//_N_Foliage.Get(xPos, 1.0, zPos) * height * 0.125 + height * 0.25;
+    const angle = _N_Foliage.Get(xPos, 9.0, zPos) * 2 * Math.PI;
+    _TMP_Q.setFromAxisAngle(_Y_AXIS, angle);
+
+    trunkEnd.applyQuaternion(_TMP_Q);
+    rootEnd1.applyQuaternion(_TMP_Q);
+    rootEnd2.applyQuaternion(_TMP_Q);
+    rootEnd3.applyQuaternion(_TMP_Q);
+
+    // const leavesPos1 = trunkEnd.clone().add(new THREE.Vector3(-6, -2, -1));
+    // const leavesPos2 = trunkEnd.clone().add(new THREE.Vector3(9, -1, -3));
+    const treeSDF = new SDF(new THREE.Vector3(xPos, yPos, zPos));
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, -2, 0),
+        trunkEnd, 3, 0.5);
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 4, 0),
+        rootEnd1, 1, 1);
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 4, 0),
+        rootEnd2, 2, 1);
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 4, 0),
+        rootEnd3, 2, 1);
+    treeSDF.AddSphere('tree_leaves', trunkEnd, leavesRadius);
+    // treeSDF.AddSphere(leavesPos1, 4);
+    // treeSDF.AddSphere(leavesPos2, 4);
+    return treeSDF;
+  };
+
+  function TREE2(xPos, yPos, zPos) {
+    let noiseID = 100;
+
+    const height = _N_Foliage.Get(xPos, 1.0, zPos) * 20 + 20;
+    const lean = _N_Foliage.Get(xPos, 2.0, zPos) * 5 + 4;
+
+    const treeSDF = new SDF(new THREE.Vector3(xPos, yPos, zPos));
+    const angle1 = (0.01 + _N_Foliage.Get(xPos, noiseID++, zPos) * 0.02) * 2 * Math.PI;
+    const angle2 = _N_Foliage.Get(xPos, noiseID++, zPos) * 2 * Math.PI;
+
+    const _AddBranch = (base, height, width, rot, level) => {
+      width = Math.max(width, 1);
+      
+      if (level > 6) {
+        _TMP_Q.copy(rot);
+
+        _TMP1.set(0, 5, 0);
+        _TMP1.applyQuaternion(rot);
+        _TMP1.add(base);
+        treeSDF.AddSphere('tree_leaves', _TMP1, 5);
+        return;
+      }
+
+      const branchEnd = new THREE.Vector3(0, height, 0);
+      const angle1 = (0.03 + _N_Foliage.Get(xPos, noiseID++, zPos) * 0.08) * 2 * Math.PI;
+      const angle2 = (0.25 + _N_Foliage.Get(xPos, noiseID++, zPos) * 0.25) * 2 * Math.PI;
+
+      branchEnd.applyQuaternion(rot);
+      branchEnd.add(base);
+      treeSDF.AddCappedCone('tree_bark', _ORIGIN, base, branchEnd, width, width * 0.6);
+
+      _TMP_Q1.setFromAxisAngle(_X_AXIS, angle1);
+      _TMP_Q2.setFromAxisAngle(_Y_AXIS, angle2);
+      _TMP_Q.copy(rot);
+      _TMP_Q.multiply(_TMP_Q2);
+      _TMP_Q.multiply(_TMP_Q1);
+
+      _AddBranch(branchEnd.clone(), height * 0.6, width * 0.6, _TMP_Q.clone(), level + 1);
+
+      const angle3 = (_N_Foliage.Get(xPos, noiseID++, zPos) * 0.01) * 2 * Math.PI;
+      const angle4 = (_N_Foliage.Get(xPos, noiseID++, zPos) * 0.25) * 2 * Math.PI;
+
+      _TMP_Q1.setFromAxisAngle(_X_AXIS, -(angle1 + angle3));
+      _TMP_Q2.setFromAxisAngle(_Y_AXIS, -(angle2 + angle4));
+      _TMP_Q.copy(rot);
+      _TMP_Q.multiply(_TMP_Q2);
+      _TMP_Q.multiply(_TMP_Q1);
+
+      _AddBranch(branchEnd.clone(), height * 0.6, width * 0.6, _TMP_Q.clone(), level + 1);
+    };
+
+    _TMP_Q1.setFromAxisAngle(_X_AXIS, angle1);
+    _TMP_Q2.setFromAxisAngle(_Y_AXIS, angle2);
+    _TMP_Q.copy(_TMP_Q2);
+    _TMP_Q.multiply(_TMP_Q1);
+    _AddBranch(new THREE.Vector3(0, -5, 0), 20, 5, _TMP_Q.clone(), 1);
+
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 3, 0),
+        new THREE.Vector3(12, -1, 0), 2, 1);
+
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 4, 0),
+        new THREE.Vector3(-8, -1, -11), 2, 1);
+    treeSDF.AddCappedCone(
+        'tree_bark',
+        new THREE.Vector3(),
+        new THREE.Vector3(0, 2, 0),
+        new THREE.Vector3(-13, -1, -4), 2, 1);
+    return treeSDF;
+  };
+
+
+  function PALM_TREE1(xPos, yPos, zPos) {
+    let noiseID = 100;
+
+    const treeSDF = new SDF(new THREE.Vector3(xPos, yPos, zPos));
+    const angle1 = (0.01 + _N_Foliage.Get(xPos, noiseID++, zPos) * 0.02) * 2 * Math.PI;
+    const angle2 = _N_Foliage.Get(xPos, noiseID++, zPos) * 2 * Math.PI;
+
+    const _AddLeaf = (base, height, width, rot, level) => {
+      if (level > 7) {
+        return;
+      }
+      const branchEnd = new THREE.Vector3(4, 0, 0);
+      const angle1 = -0.075 * 2 * Math.PI;
+
+      branchEnd.applyQuaternion(rot);
+      branchEnd.add(base);
+      treeSDF.AddCappedCone('tree_leaves', _ORIGIN, base, branchEnd, width, width);
+
+      _TMP_Q1.setFromAxisAngle(_Z_AXIS, angle1);
+      _TMP_Q.copy(rot);
+      _TMP_Q.multiply(_TMP_Q1);
+
+      _AddLeaf(branchEnd.clone(), height, width, _TMP_Q.clone(), level + 1);
+    };
+
+    const _AddBranch = (base, height, width, rot, level) => {
+      width = Math.max(width, 1);
+
+      if (level > 3) {
+        _AddLeaf(base, height, 1, new THREE.Quaternion(), level);
+
+        _TMP_Q2.setFromAxisAngle(_Y_AXIS, 0.33 * 2.0 * Math.PI);
+        _AddLeaf(base, height, 1, _TMP_Q2.clone(), level);
+
+        _TMP_Q2.setFromAxisAngle(_Y_AXIS, 0.66 * 2.0 * Math.PI);
+        _AddLeaf(base, height, 1, _TMP_Q2.clone(), level);
+        return;
+      }
+
+      const branchEnd = new THREE.Vector3(0, height, 0);
+      const angle1 = (0.05 + _N_Foliage.Get(xPos, noiseID++, zPos) * 0.02) * 2 * Math.PI;
+
+      branchEnd.applyQuaternion(rot);
+      branchEnd.add(base);
+      treeSDF.AddCappedCone('tree_bark', _ORIGIN, base, branchEnd, width, width * 0.6);
+
+      _TMP_Q1.setFromAxisAngle(_X_AXIS, angle1);
+      _TMP_Q.copy(rot);
+      _TMP_Q.multiply(_TMP_Q1);
+
+      _AddBranch(branchEnd.clone(), height * 0.75, width * 0.75, _TMP_Q.clone(), level + 1);
+    };
+
+    _TMP_Q1.setFromAxisAngle(_X_AXIS, angle1);
+    _TMP_Q2.setFromAxisAngle(_Y_AXIS, angle2);
+    _TMP_Q.copy(_TMP_Q2);
+    _TMP_Q.multiply(_TMP_Q1);
+    _AddBranch(new THREE.Vector3(0, -5, 0), 15, 2, _TMP_Q.clone(), 1);
+
+    return treeSDF;
+  };
+
+  return {
+      TREE1: TREE1,
+      TREE2: TREE2,
+      PALM_TREE1: PALM_TREE1,
+      SPHERE: SPHERE,
+      CONE1: CONE1,
+  };
+})();

+ 44 - 0
src/hack-defs.js

@@ -0,0 +1,44 @@
+
+
+export const hack_defs = (() => {
+
+  const _INTRO = {
+    enabled: false,
+    foliageEnabled: false,
+    introEnabled: false,
+    oceanEnabled: false,
+    hardcodedFoliageEnabled: true,
+    PLAYER_POS: [-1826.1306923527645, 27.940844444445403, -220.6986696117536],
+    PLAYER_ROT: [-0.0380279893805328, 0.3364980691628503, 0.013601301436886065, 0.9408176901358577],
+    CAMERA_POS: [-2150, -557],
+    CAMERA_DECCELERATION: [-10, 0, -10],
+    INTRO_RATE: 0.0005,
+    WORLD_SIZE: 24
+  };
+
+  return {
+    enabled: false,
+    foliageEnabled: true,
+    hardcodedFoliageEnabled: false,
+    introEnabled: false,
+    skipOceans: false,
+    skipClouds: false,
+    skipFoliageNoise: false,
+    skipPruning: false,
+    skipExteriorBlocks: false,
+    skipAO: false,
+    skipVariableLuminance: false,
+    skipGravity: false,
+    useFlatTerrain: false,
+    showTools: true,
+    fixedTerrainOrigin: false,
+    PLAYER_POS: [-1826.1306923527645, 27.940844444445403, -220.6986696117536],
+    PLAYER_ROT: [-0.0380279893805328, 0.3364980691628503, 0.013601301436886065, 0.9408176901358577],
+    CAMERA_POS: [0, 0],
+    CAMERA_DECCELERATION: [-10, 0, -10],
+    INTRO_RATE: 0.0005,
+    WORLD_BLOCK_SIZE: 16,
+    WORLD_SIZE: 24
+  };
+
+})();

+ 103 - 0
src/main.js

@@ -0,0 +1,103 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {threejs_component} from './threejs-component.js';
+import {sparse_voxel_cell_manager} from './sparse-voxel-cell-manager.js';
+
+import {entity_manager} from './entity-manager.js';
+import {entity} from './entity.js';
+import {cloud_controller} from './cloud-controller.js';
+import {player_controller} from './player-controller.js';
+import {voxel_tools} from './voxel-tools.js';
+import {hack_defs}  from './hack-defs.js';
+import {ui_controller} from './ui-controller.js';
+import {defs} from './defs.js';
+
+
+class LessCrappyMinecraftAttempt {
+  constructor() {
+    this._Initialize();
+  }
+
+  _Initialize() {
+    this.entityManager_ = new entity_manager.EntityManager();
+
+    this.LoadControllers_();
+
+    this.previousRAF_ = null;
+    this.RAF_();
+  }
+
+  CreateGUI_() {
+    this._guiParams = {
+      general: {
+      },
+    };
+    this._gui = new GUI();
+    this._gui.close();
+  }
+
+  LoadControllers_() {
+    const threejs = new entity.Entity();
+    threejs.AddComponent(new threejs_component.ThreeJSController());
+    this.entityManager_.Add(threejs, 'renderer');
+
+    // Hack
+    this.renderer_ = threejs.GetComponent('ThreeJSController');
+    this.scene_ = threejs.GetComponent('ThreeJSController').scene_;
+    this.camera_ = threejs.GetComponent('ThreeJSController').camera_;
+    this.threejs_ = threejs.GetComponent('ThreeJSController').threejs_;
+    
+    const voxelManager = new entity.Entity();
+    voxelManager.AddComponent(new sparse_voxel_cell_manager.SparseVoxelCellManager({
+        cellSize: hack_defs.WORLD_BLOCK_SIZE,
+        worldSize: hack_defs.WORLD_SIZE,
+    }));
+    this.entityManager_.Add(voxelManager, 'voxels');
+
+    const clouds = new entity.Entity();
+    clouds.AddComponent(new cloud_controller.CloudController());
+    this.entityManager_.Add(clouds);
+
+    const player = new entity.Entity();
+    player.AddComponent(new player_controller.PlayerController());
+    player.AddComponent(new voxel_tools.VoxelTools_Insert());
+    player.AddComponent(new voxel_tools.VoxelTools_Delete());
+    player.SetPosition(new THREE.Vector3(...defs.PLAYER_POS));
+    player.SetQuaternion(new THREE.Quaternion(...defs.PLAYER_ROT));
+
+    this.entityManager_.Add(player, 'player');
+
+    const ui = new entity.Entity();
+    ui.AddComponent(new ui_controller.UIController());
+    this.entityManager_.Add(ui, 'ui');
+  }
+
+  RAF_() {
+    requestAnimationFrame((t) => {
+      if (this.previousRAF_ === null) {
+        this.previousRAF_ = t;
+      }
+
+      this.Step_(t - this.previousRAF_);
+      this.renderer_.Render();
+      this.previousRAF_ = t;
+
+      setTimeout(() => {
+        this.RAF_();
+      }, 1);
+    });
+  }
+
+  Step_(timeElapsed) {
+    const timeElapsedS = Math.min(1.0 / 30.0, timeElapsed * 0.001);
+
+    this.entityManager_.Update(timeElapsedS);
+  }
+}
+
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  _APP = new LessCrappyMinecraftAttempt();
+});

+ 42 - 0
src/math.js

@@ -0,0 +1,42 @@
+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);
+    },
+
+    in_range: (x, a, b) => {
+      return x >= a && x <= b;
+    },
+  };
+})();

+ 59 - 0
src/noise.js

@@ -0,0 +1,59 @@
+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++) {
+        let noiseValue = noiseFunc.noise3D(
+          xs * frequency, ys * frequency, zs * frequency);
+
+        total += noiseValue * amplitude;
+        normalization += amplitude;
+        amplitude *= G;
+        frequency *= this.params_.lacunarity;
+      }
+      total /= normalization;
+
+      if (this.params_.ridged) {
+        total = 1.0 - Math.abs(total);
+      } else {
+        total = total * 0.5 + 0.5;
+      }
+
+      total = Math.pow(
+          total, this.params_.exponentiation);
+
+      if (this.params_.range) {
+        const range = this.params_.range;
+        total = range[0] + (range[1] - range[0]) * total;
+      }
+    
+      return total * this.params_.height;
+    }
+  }
+
+  return {
+    Noise: _NoiseGenerator
+  }
+})();

+ 346 - 0
src/player-controller.js

@@ -0,0 +1,346 @@
+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 {entity} from './entity.js';
+
+import {hack_defs} from './hack-defs.js';
+
+
+export const player_controller = (() => {
+
+  // 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 PlayerController extends entity.Component {
+    constructor() {
+      super();
+    }
+
+    InitEntity() {
+      this.radius_ = 1.5;
+      this.keys_ = {
+        forward: false,
+        backward: false,
+        left: false,
+        right: false,
+      };
+      this.standing = true;
+      this.velocity_ = new THREE.Vector3(0, 0, 0);
+      this.decceleration_ = new THREE.Vector3(-10, -9.8 * 5, -10);
+      this.acceleration_ = new THREE.Vector3(75, 20, 75);
+
+      // this.decceleration_ = new THREE.Vector3(-10, -9.8 * 2, -10);
+      this.acceleration_ = new THREE.Vector3(200, 25, 200);
+
+      const threejs = this.FindEntity('renderer').GetComponent('ThreeJSController');
+      this.element_ = threejs.threejs_.domElement;
+      this.camera_ = threejs.camera_;
+
+      this.SetupPointerLock_();
+
+      this.controls_ = new PointerLockControls(this.camera_, document.body);
+      threejs.scene_.add(this.controls_.getObject());
+
+      // HACK
+      if (hack_defs.enabled) {
+        this.controls_.getObject().position.set(...hack_defs.PLAYER_POS);
+        this.controls_.getObject().quaternion.set(...hack_defs.PLAYER_ROT);
+        this.decceleration_ = new THREE.Vector3(...hack_defs.CAMERA_DECCELERATION);
+      }
+
+      this.controls_.getObject().position.copy(this.Parent.Position);
+
+      document.addEventListener('keydown', (e) => this.OnKeyDown_(e), false);
+      document.addEventListener('keyup', (e) => this.OnKeyUp_(e), false);
+      // document.addEventListener('mouseup', (e) => this._onMouseUp(e), false);
+    }
+
+    OnKeyDown_(event) {
+      switch (event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this.keys_.forward = true;
+          break;
+        case 37: // left
+        case 65: // a
+          this.keys_.left = true;
+          break;
+        case 40: // down
+        case 83: // s
+          this.keys_.backward = true;
+          break;
+        case 39: // right
+        case 68: // d
+          this.keys_.right = true;
+          break;
+        case 32: // space
+          if (this.standing) {
+            this.velocity_.y = this.acceleration_.y;
+            this.standing = false;
+          }
+          break;
+      }
+    }
+
+    OnKeyUp_(event) {
+      switch(event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this.keys_.forward = false;
+          break;
+        case 37: // left
+        case 65: // a
+          this.keys_.left = false;
+          break;
+        case 40: // down
+        case 83: // s
+          this.keys_.backward = false;
+          break;
+        case 39: // right
+        case 68: // d
+          this.keys_.right = false;
+          break;
+        case 79: // o
+          // this.OnCycleTextures_(-1);
+          break;
+        case 80: // p
+          // this.OnCycleTextures_(1);
+          break;
+        case 84: // t
+          this.OnCycleTools_();
+          break;
+        case 219: // [
+          this.OnCycleTextures_(-1);
+          break;
+        case 221: // 
+          this.OnCycleTextures_(1);
+          break;
+        // case 33: // PG_UP
+        //   this.cells_.ChangeActiveTool(1);
+        //   break;
+        // case 34: // PG_DOWN
+        // this.cells_.ChangeActiveTool(-1);
+        //   break;
+        case 13: // enter
+          this.keys_.enter = true;
+          break;
+      }
+    }
+
+    _onMouseUp(event) {
+      this.keys_.enter = true;
+    }
+
+    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);
+
+        this.element_.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;
+    }
+
+    OnCycleTools_() {
+      const ui = this.FindEntity('ui').GetComponent('UIController');
+      ui.CycleTool_();
+    }
+
+    OnCycleTextures_(dir) {
+      const ui = this.FindEntity('ui').GetComponent('UIController');
+      ui.CycleBuildIcon_(dir);
+    }
+
+    Update(timeInSeconds) {
+      const controlObject = this.controls_.getObject();
+
+      const demo = false;
+      if (demo) {
+        controlObject.position.x += timeInSeconds * 5;
+        controlObject.position.z += timeInSeconds * 5;
+        this.Parent.SetPosition(controlObject.position);
+        this.Parent.Position.x += 220;
+        this.Parent.Position.z += 220;
+        return;
+      }
+
+      if (this.keys_.enter) {
+        this.Broadcast({topic: 'input.pressed', value: 'enter'});
+      }
+
+      this.keys_.enter = false;
+
+      const velocity = this.velocity_;
+      const frameDecceleration = new THREE.Vector3(
+          this.velocity_.x * this.decceleration_.x,
+          this.decceleration_.y,
+          this.velocity_.z * this.decceleration_.z
+      );
+
+      frameDecceleration.multiplyScalar(timeInSeconds);
+      frameDecceleration.z = Math.sign(frameDecceleration.z) * Math.min(
+          Math.abs(frameDecceleration.z), Math.abs(velocity.z));
+
+      if (hack_defs.skipGravity) {
+        frameDecceleration.y = Math.sign(frameDecceleration.y) * Math.min(
+            Math.abs(frameDecceleration.y), Math.abs(velocity.y));
+      }
+      this.velocity_.add(frameDecceleration);
+
+      // Gravity
+      if (!hack_defs.skipGravity) {
+        this.velocity_.y = Math.max(this.velocity_.y, -50);
+      }
+
+
+      if (this.keys_.forward) {
+        this.velocity_.z -= this.acceleration_.z * timeInSeconds;
+      }
+      if (this.keys_.backward) {
+        this.velocity_.z += this.acceleration_.z * timeInSeconds;
+      }
+      if (this.keys_.left) {
+        this.velocity_.x -= this.acceleration_.x * timeInSeconds;
+      }
+      if (this.keys_.right) {
+        this.velocity_.x += this.acceleration_.x * timeInSeconds;
+      }
+
+      const voxelManager = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+      const voxelList = voxelManager.FindVoxelsNear(
+          controlObject.position, 3).filter(v => v.type != 'ocean');
+
+      const AsAABB_ = (v) => {
+        const position = new THREE.Vector3(
+            v.position[0], v.position[1], v.position[2]);
+        const half = new THREE.Vector3(0.5, 0.5, 0.5);
+
+        const m1 = new THREE.Vector3();
+        m1.copy(position);
+        m1.sub(half);
+
+        const m2 = new THREE.Vector3();
+        m2.copy(position);
+        m2.add(half);
+
+        return new THREE.Box3(m1, m2);
+      }
+      const boxes = voxelList.map(v => AsAABB_(v));
+
+      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 sideways = new THREE.Vector3(1, 0, 0);
+      sideways.applyQuaternion(controlObject.quaternion);
+      sideways.normalize();
+
+      sideways.multiplyScalar(this.velocity_.x * timeInSeconds);
+      forward.multiplyScalar(this.velocity_.z * timeInSeconds);
+
+      const alreadyIntersecting = this._FindIntersections(
+          boxes, controlObject.position).length > 0;
+
+      controlObject.position.add(forward);
+      controlObject.position.add(sideways);
+
+      let intersections = this._FindIntersections(
+          boxes, controlObject.position);
+      if (intersections.length > 0 && !alreadyIntersecting) {
+        controlObject.position.copy(oldPosition);
+      }
+
+      oldPosition.copy(controlObject.position);
+      const _STEP_SIZE = 0.01;
+      let timeAcc = _STEP_SIZE;
+      while (timeAcc < timeInSeconds) {
+        controlObject.position.y += this.velocity_.y * timeAcc;
+        intersections = this._FindIntersections(boxes, controlObject.position);
+        if (intersections.length > 0) {
+          controlObject.position.copy(oldPosition);
+  
+          this.velocity_.y = Math.max(0, this.velocity_.y);
+          this.standing = true;
+          break;
+        }  
+        timeAcc = Math.min(timeAcc + _STEP_SIZE, timeInSeconds);
+      }
+
+      if (controlObject.position.y < -100) {
+        this.velocity_.y = 0;
+        controlObject.position.y = 250;
+        this.standing = true;
+      }
+
+      this.Parent.SetPosition(controlObject.position);
+      this.Parent.SetQuaternion(controlObject.quaternion);
+    }
+  };
+
+  return {
+      PlayerController: PlayerController,
+  };
+})();

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

+ 338 - 0
src/sparse-voxel-cell-manager.js

@@ -0,0 +1,338 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {entity} from './entity.js';
+import {utils} from './utils.js';
+import {voxel_builder_threaded} from './voxel-builder-threaded.js';
+import {voxel_shader} from './voxel-shader.js';
+import {textures} from './textures.js';
+import {texture_defs} from './texture-defs.js';
+import {defs} from './defs.js';
+import {hack_defs} from './hack-defs.js';
+
+export const sparse_voxel_cell_manager = (() => {
+
+  class SparseVoxelCellManager extends entity.Component {
+    constructor(params) {
+      super();
+      this.blocks_ = {};
+      this.cellDimensions_ = new THREE.Vector3(params.cellSize, params.cellSize, params.cellSize);
+      this.visibleDimensions_ = [params.worldSize, params.worldSize];
+      this.dirtyBlocks_ = {};
+      this.ids_ = 0;
+      this.totalTime_ = 0.0;
+    }
+
+    InitEntity() {
+      // HACK
+      this.scene_ = this.FindEntity('renderer').GetComponent('ThreeJSController').scene_;
+
+      this.materialOpaque_ = new THREE.ShaderMaterial({
+          uniforms: {
+            diffuseMap: {
+                value: null,
+            },
+            noiseMap: {
+                value: null,
+            },
+            fogColour: {
+                value: defs.FOG_COLOUR.clone(),
+            },
+            fogDensity: {
+                value: 0.000065,
+            },
+            fogRange: {
+                value: new THREE.Vector2(250, 250),
+            },
+            fogTime: {
+                value: 0.0,
+            },
+            fade: {
+              value: 1.0,
+            },
+            flow: {
+              value: 0.0,
+            },
+          },
+          vertexShader: voxel_shader.VOXEL.VS,
+          fragmentShader: voxel_shader.VOXEL.PS,
+          side: THREE.FrontSide
+      });
+      this.materialTransparent_ = this.materialOpaque_.clone();
+      this.materialTransparent_.side = THREE.FrontSide;
+      this.materialTransparent_.transparent = true;
+
+      this.LoadTextures_();
+
+      this.builder_ = new voxel_builder_threaded.VoxelBuilder_Threaded({
+          scene: this.scene_,
+          dimensions: this.cellDimensions_,
+          materialOpaque: this.materialOpaque_,
+          materialTransparent: this.materialTransparent_,
+          blockTypes: this.blockTypes_,
+      });
+    }
+
+    LoadTextures_() {
+      this.blockTypes_ = {};
+      const textureSet = new Set();
+      for (let k in texture_defs.DEFS) {
+        const t = texture_defs.DEFS[k];
+
+        this.blockTypes_[k] = {
+            textures: [],
+        };
+        if (t.texture instanceof Array) {
+          for (let i = 0; i < t.texture.length; ++i) {
+            textureSet.add(t.texture[i]);
+            this.blockTypes_[k].textures.push(t.texture[i]);
+          }
+        } else {
+          for (let i = 0; i < 6; ++i) {
+            textureSet.add(t.texture);
+            this.blockTypes_[k].textures.push(t.texture);
+          }
+        }
+      }
+
+      const textureBlocks = [...textureSet];
+      for (let k in this.blockTypes_) {
+        for (let i = 0; i < 6; ++i) {
+          this.blockTypes_[k].textures[i] = textureBlocks.indexOf(this.blockTypes_[k].textures[i]);
+        }
+      }
+
+      const path = './resources/minecraft/textures/blocks/';
+      const diffuse = new textures.TextureAtlas();
+      diffuse.Load('diffuse', textureBlocks.map(t => path + t));
+      diffuse.onLoad = () => {
+        this.materialOpaque_.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas;
+        this.materialTransparent_.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas;
+      };
+
+      const loader = new THREE.TextureLoader();
+      const noiseTexture = loader.load('./resources/simplex-noise.png');
+      noiseTexture.wrapS = THREE.RepeatWrapping;
+      noiseTexture.wrapT = THREE.RepeatWrapping;
+      noiseTexture.minFilter = THREE.LinearMipMapLinearFilter;
+      noiseTexture.magFilter = THREE.NearestFilter;
+      this.materialOpaque_.uniforms.noiseMap.value = noiseTexture;
+      this.materialTransparent_.uniforms.noiseMap.value = noiseTexture;
+    }
+
+    Key_(x, y, z) {
+      return x + '.' + y + '.' + z;
+    }
+
+    BlockIndex_(xp, zp) {
+      const x = Math.floor(xp / this.cellDimensions_.x);
+      const z = Math.floor(zp / this.cellDimensions_.z);
+      return [x, z];
+    }
+
+    FindBlock_(xp, zp) {
+      const [cx, cz] = this.BlockIndex_(xp, zp);
+      const k = this.Key_(cx, 0, cz);
+      if (k in this.blocks_) {
+        return this.blocks_[k];
+      }
+      return null;
+    }
+
+    GetAdjacentBlocks(xp, zp) {
+      const blocks = [];
+      for (let xi = -1; xi <= 1; ++xi) {
+        for (let zi = -1; zi <= 1; ++zi) {
+          if (xi == 0 && zi == 0) {
+            continue;
+          }
+          const [cx, cz] = this.BlockIndex_(xp, zp);
+          const k = this.Key_(cx + xi, 0, cz + zi);
+          if (k in this.blocks_) {
+            blocks.push(this.blocks_[k]);
+          }
+        }
+      }
+      return blocks;
+    }
+
+    InsertVoxelAt(pos, type, skippable) {
+      const block = this.FindBlock_(pos[0], pos[2]);
+      if (!block) {
+        return;
+      }
+
+      block.InsertVoxelAt(pos, type, skippable);
+    }
+
+    RemoveVoxelAt(pos) {
+      const block = this.FindBlock_(pos[0], pos[2]);
+      if (!block) {
+        return;
+      }
+
+      block.RemoveVoxelAt(pos);
+    }
+
+    HasVoxelAt(x, y, z) {
+      const block = this.FindBlock_(x, z);
+      if (!block) {
+        return false;
+      }
+
+      return block.HasVoxelAt(x, y, z);
+    }
+
+    FindVoxelsNear(pos, radius) {
+      // TODO only lookup really close by
+      const [xn, zn] = this.BlockIndex_(pos.x - radius, pos.z - radius);
+      const [xp, zp] = this.BlockIndex_(pos.x + radius, pos.z + radius);
+
+      const voxels = [];
+      for (let xi = xn; xi <= xp; xi++) {
+        for (let zi = zn; zi <= zp; zi++) {
+          const k = this.Key_(xi, 0, zi);
+          if (k in this.blocks_) {
+            const c = this.blocks_[k];
+
+            voxels.push(...c.FindVoxelsNear(pos, radius));
+          }
+        }
+      }
+
+      return voxels;
+    }
+
+    FindIntersectionsWithRay(ray, maxDistance) {
+      const voxels = this.FindVoxelsNear(ray.origin, maxDistance);
+      const intersections = [];
+
+      const AsAABB_ = (v) => {
+        const position = new THREE.Vector3(
+            v.position[0], v.position[1], v.position[2]);
+        const half = new THREE.Vector3(0.5, 0.5, 0.5);
+
+        const m1 = new THREE.Vector3();
+        m1.copy(position);
+        m1.sub(half);
+
+        const m2 = new THREE.Vector3();
+        m2.copy(position);
+        m2.add(half);
+
+        return new THREE.Box3(m1, m2);
+      }
+
+      const boxes = voxels.map(v => AsAABB_(v));
+      const _TMP_V = new THREE.Vector3();
+
+      for (let i = 0; i < boxes.length; ++i) {
+        if (ray.intersectBox(boxes[i], _TMP_V)) {
+          intersections.push({
+              voxel: voxels[i],
+              aabb: boxes[i],
+              intersectionPoint: _TMP_V.clone(),
+              distance: _TMP_V.distanceTo(ray.origin)
+          });
+        }
+      }
+
+      intersections.sort((a, b) => {
+        return a.distance - b.distance;
+      });
+
+      return intersections;
+    }
+
+    Update(timeElapsed) {
+      this.builder_.Update(timeElapsed);
+      if (!this.builder_.Busy) {
+        this.UpdateTerrain_();
+      }
+
+      this.totalTime_ += timeElapsed;
+      this.materialOpaque_.uniforms.fogTime.value = this.totalTime_ * 0.5;
+      this.materialTransparent_.uniforms.fogTime.value = this.totalTime_ * 0.5;
+      this.materialTransparent_.uniforms.flow.value = this.totalTime_ * 0.5;
+
+      // HACK, awful
+      const threejs = this.FindEntity('renderer').GetComponent('ThreeJSController');
+      threejs.sky_.material.uniforms.whiteBlend.value = this.builder_.currentTime_;
+      const player = this.FindEntity('player');
+      if (player.Position.y < 6 && !hack_defs.skipOceans) {
+        this.materialOpaque_.uniforms.fogRange.value.set(...defs.UNDERWATER_RANGE);
+        this.materialTransparent_.uniforms.fogRange.value.set(...defs.UNDERWATER_RANGE);
+        this.materialOpaque_.uniforms.fogColour.value.copy(defs.UNDERWATER_COLOUR);
+        this.materialTransparent_.uniforms.fogColour.value.copy(defs.UNDERWATER_COLOUR);
+        threejs.sky_.material.uniforms.bottomColor.value.copy(defs.UNDERWATER_COLOUR);
+      } else {
+        this.materialOpaque_.uniforms.fogRange.value.set(...defs.FOG_RANGE);
+        this.materialTransparent_.uniforms.fogRange.value.set(...defs.FOG_RANGE);
+        this.materialOpaque_.uniforms.fogColour.value.copy(defs.FOG_COLOUR);
+        this.materialTransparent_.uniforms.fogColour.value.copy(defs.FOG_COLOUR);
+        threejs.sky_.material.uniforms.bottomColor.value.copy(defs.FOG_COLOUR);
+      }
+      threejs.sky_.material.needsUpdate = true;
+      this.materialOpaque_.needsUpdate = true;
+      this.materialTransparent_.needsUpdate = true;
+    }
+
+    UpdateTerrain_() {
+      const player = this.FindEntity('player');
+      const cellIndex = hack_defs.fixedTerrainOrigin ?
+          this.BlockIndex_(...hack_defs.CAMERA_POS) :
+          this.BlockIndex_(player.Position.x, player.Position.z);
+
+      const xs = this.visibleDimensions_[0];
+      const zs = this.visibleDimensions_[1];
+      let cells = {};
+
+      for (let x = -xs; x <= xs; x++) {
+        for (let z = -zs; z <= zs; z++) {
+          const xi = x + cellIndex[0];
+          const zi = z + cellIndex[1];
+
+          const key = this.Key_(xi, 0, zi);
+          cells[key] = [xi, zi];
+        }
+      }
+
+      const intersection = utils.DictIntersection(this.blocks_, cells);
+      const difference = utils.DictDifference(cells, this.blocks_);
+      const recycle = Object.values(utils.DictDifference(this.blocks_, cells));
+
+      this.builder_.ScheduleDestroy(recycle);
+
+      cells = intersection;
+
+      const sortedDifference = [];
+
+      for (let k in difference) {
+        const [xi, zi] = difference[k];
+        const d = ((cellIndex[0] - xi) ** 2 + (cellIndex[1] - zi) ** 2) ** 0.5;
+        sortedDifference.push([d, k, difference[k]])
+      }
+
+      sortedDifference.sort((a, b) => { return a[0] - b[0]; });
+
+      for (let i = 0; i < sortedDifference.length; ++i) {
+        const k = sortedDifference[i][1];
+        const [xi, zi] = sortedDifference[i][2];
+        const offset = new THREE.Vector3(
+            xi * this.cellDimensions_.x, 0, zi * this.cellDimensions_.z);
+  
+        cells[k] = this.builder_.AllocateBlock({
+            parent: this,
+            offset: offset
+        });
+      }
+
+
+      this.blocks_ = cells;
+    }
+  }
+
+
+  return {
+      SparseVoxelCellManager: SparseVoxelCellManager,
+  };
+})();

+ 55 - 0
src/texture-defs.js

@@ -0,0 +1,55 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const texture_defs = (() => {
+
+  return {
+      DEFS: {
+        ocean: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'ocean.png',
+        },
+        dirt: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'dirt.png',
+        },
+        sand: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'sand.png',
+        },
+        stone: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'stone.png',
+        },
+        tree_bark: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'tree-bark.png',
+        },
+        tree_leaves: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'tree-leaves.png',
+        },
+        moon: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: 'moon.png',
+        },
+        snow: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: [
+                'snow-side.png', 'snow-side.png',
+                'snow.png', 'snow.png',
+                'snow-side.png', 'snow-side.png'
+            ],
+        },
+        grass: {
+            colour: new THREE.Color(0xFFFFFF),
+            texture: [
+                'grass-side.png', 'grass-side.png',
+                'grass.png', 'dirt.png',
+                'grass-side.png', 'grass-side.png'
+            ],
+        },
+      },
+  };
+
+})();

+ 88 - 0
src/textures.js

@@ -0,0 +1,88 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const textures = (() => {
+
+  // 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 );
+  }
+
+class TextureAtlas {
+    constructor() {
+      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_;
+    }
+
+    LoadTexture_(n) {
+      const t = this.loader_.load(n);
+      t.encoding = THREE.sRGBEncoding;
+      return t;
+    }
+    
+    OnLoad_() {
+      for (let k in this.textures_) {
+        const atlas = this.textures_[k];
+        const data = new Uint8Array(atlas.textures.length * 4 * 16 * 16);
+
+        for (let t = 0; t < atlas.textures.length; t++) {
+          const curTexture = atlas.textures[t];
+          const curData = _GetImageData(curTexture.image);
+          const offset = t * (4 * 16 * 16);
+
+          data.set(curData.data, offset);
+        }
+  
+        const diffuse = new THREE.DataTexture2DArray(data, 16, 16, atlas.textures.length);
+        diffuse.format = THREE.RGBAFormat;
+        diffuse.type = THREE.UnsignedByteType;
+        diffuse.minFilter = THREE.LinearMipMapLinearFilter;
+        diffuse.magFilter = THREE.NearestFilter;
+        diffuse.wrapS = THREE.ClampToEdgeWrapping;
+        diffuse.wrapT = THREE.ClampToEdgeWrapping;
+        diffuse.generateMipmaps = true;
+        diffuse.encoding = THREE.sRGBEncoding;
+
+        atlas.atlas = diffuse;
+      }
+
+      this.onLoad();
+    }
+
+    LoadAtlas_(atlas, names) {
+      this.textures_[atlas] = {
+        textures: names.map(n => this.LoadTexture_(n) ),
+        atlas: null,
+      };
+    }
+  };
+
+  return {
+      TextureAtlas: TextureAtlas,
+  };
+})();

+ 52 - 0
src/third-person-camera.js

@@ -0,0 +1,52 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {entity} from './entity.js';
+
+
+export const third_person_camera = (() => {
+  
+  class ThirdPersonCamera extends entity.Component {
+    constructor(params) {
+      super();
+
+      this._params = params;
+      this._camera = params.camera;
+
+      this._currentPosition = new THREE.Vector3();
+      this._currentLookat = new THREE.Vector3();
+    }
+
+    _CalculateIdealOffset() {
+      const idealOffset = new THREE.Vector3(-0, 10, -15);
+      idealOffset.applyQuaternion(this._params.target._rotation);
+      idealOffset.add(this._params.target._position);
+      return idealOffset;
+    }
+
+    _CalculateIdealLookat() {
+      const idealLookat = new THREE.Vector3(0, 5, 20);
+      idealLookat.applyQuaternion(this._params.target._rotation);
+      idealLookat.add(this._params.target._position);
+      return idealLookat;
+    }
+
+    Update(timeElapsed) {
+      const idealOffset = this._CalculateIdealOffset();
+      const idealLookat = this._CalculateIdealLookat();
+
+      // const t = 0.05;
+      // const t = 4.0 * timeElapsed;
+      const t = 1.0 - Math.pow(0.01, timeElapsed);
+
+      this._currentPosition.lerp(idealOffset, t);
+      this._currentLookat.lerp(idealLookat, t);
+
+      this._camera.position.copy(this._currentPosition);
+      this._camera.lookAt(this._currentLookat);
+    }
+  }
+
+  return {
+    ThirdPersonCamera: ThirdPersonCamera
+  };
+
+})();

+ 278 - 0
src/threejs-component.js

@@ -0,0 +1,278 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/EffectComposer.js';
+import {RenderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/RenderPass.js';
+import {UnrealBloomPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/UnrealBloomPass.js';
+import {SSAOPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/SSAOPass.js';
+import {SAOPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/SAOPass.js';
+import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/ShaderPass.js';
+
+import {GammaCorrectionShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/GammaCorrectionShader.js';
+import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/FXAAShader.js';
+
+import {entity} from "./entity.js";
+import {hack_defs} from "./hack-defs.js";
+import {defs} from "./defs.js";
+
+import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+
+
+import { GUI } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/dat.gui.module.js';
+
+export const threejs_component = (() => {
+
+  const _VS = `
+  varying vec3 vWorldPosition;
+  
+  void main() {
+    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
+    vWorldPosition = worldPosition.xyz;
+  
+    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+  }`;
+  
+  
+  const _FS = `
+  uniform vec3 topColor;
+  uniform vec3 bottomColor;
+  uniform vec3 playerPos;
+  uniform float offset;
+  uniform float exponent;
+  uniform float whiteBlend;
+  uniform float time;
+  uniform samplerCube background;
+  
+  varying vec3 vWorldPosition;
+  
+  float sdPlane(vec3 p, vec3 n, float h) {
+    // n must be normalized
+    return dot(p, n) + h;
+  }
+
+  void main() {
+    vec3 viewDirection = normalize(vWorldPosition - cameraPosition);
+    vec3 stars = sRGBToLinear(textureCube(background, viewDirection)).xyz;
+ 
+    float h = normalize(vWorldPosition + offset).y;
+    float t = max(pow(max(h, 0.0), exponent), 0.0);
+  
+    float f = exp(min(0.0, -vWorldPosition.y * 0.0125));
+  
+    float heightMix = clamp((playerPos.y - 500.0) / 1000.0, 0.0, 1.0);
+    heightMix = smoothstep(0.0, 1.0, heightMix);
+    heightMix = smoothstep(0.0, 1.0, heightMix);
+
+    float wrapFactor = playerPos.y / 500.0;
+    float normalMix = clamp((viewDirection.y + wrapFactor) / (1.0 + wrapFactor), 0.0, 1.0);
+    normalMix = pow(normalMix, 0.250);
+
+    vec3 topMix = mix(topColor, stars, heightMix * normalMix);
+
+    // Normal
+    vec3 sky = mix(topMix, bottomColor, f);
+    // Moon
+    // vec3 sky = mix(stars, bottomColor, f);
+    float skyMix = clamp(whiteBlend, 0.0, 1.0);
+    sky = mix(bottomColor, sky, skyMix * skyMix);
+    gl_FragColor = vec4(sky, 1.0);
+    // gl_FragColor = vec4(vec3(normalMix * normalMix), 1.0);
+  }`;
+
+  class ThreeJSController extends entity.Component {
+    constructor() {
+      super();
+    }
+
+    InitEntity() {
+      this.threejs_ = new THREE.WebGLRenderer({
+        antialias: false,
+      });
+
+      this.threejs_.shadowMap.enabled = true;
+      this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
+      this.threejs_.setPixelRatio(window.devicePixelRatio);
+      this.threejs_.setSize(window.innerWidth, window.innerHeight);
+      this.threejs_.domElement.id = 'threejs';
+  
+      document.getElementById('container').appendChild(this.threejs_.domElement);
+  
+      window.addEventListener('resize', () => {
+        this.OnResize_();
+      }, false);
+  
+      const fov = 60;
+      const aspect = 1920 / 1080;
+      const near = 0.5;
+      const far = 10000.0;
+      this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this.camera_.position.set(15, 50, 15);
+      this.camera_.lookAt(0, 0, 0);
+  
+      this.uiCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+
+      this.scene_ = new THREE.Scene();
+      this.scene_.add(this.camera_);
+
+      this.uiScene_ = new THREE.Scene();
+      this.uiScene_.add(this.uiCamera_);
+  
+      let light = new THREE.DirectionalLight(0x8088b3, 0.7);
+      light.position.set(-10, 500, 10);
+      light.target.position.set(0, 0, 0);
+      this.scene_.add(light);
+      this.uiScene_.add(light.clone());
+      
+      this.sun_ = light;
+
+      const params = {
+          minFilter: THREE.LinearFilter,
+          magFilter: THREE.LinearFilter,
+          format: THREE.RGBAFormat,
+          type: THREE.FloatType,
+      };
+
+      const hdr = new THREE.WebGLRenderTarget(
+          window.innerWidth, window.innerHeight, params);
+      hdr.stencilBuffer = false;
+      hdr.depthBuffer = true;
+      hdr.depthTexture = new THREE.DepthTexture();
+      hdr.depthTexture.format = THREE.DepthFormat;
+      hdr.depthTexture.type = THREE.UnsignedIntType;
+
+      this.fxaa_ = new ShaderPass(FXAAShader);
+
+      const uiPass = new RenderPass(this.uiScene_, this.uiCamera_);
+      uiPass.clear = false;
+
+      this.composer_ = new EffectComposer(this.threejs_, hdr);
+      this.composer_.addPass(new RenderPass(this.scene_, this.camera_));
+      this.composer_.addPass(uiPass);
+      this.composer_.addPass(this.fxaa_);
+      this.composer_.addPass(new ShaderPass(GammaCorrectionShader));
+
+      // So dumb. Don't judge me.
+      const m1 = new THREE.Mesh(
+          new THREE.BoxBufferGeometry(0.1, 0.01, 0.01),
+          new THREE.MeshBasicMaterial({
+              color: new THREE.Color(0xFFFFFF),
+              depthWrite: false,
+              depthTest: false,
+          }));
+      m1.position.set(0, 0, -2);
+      const m2 = new THREE.Mesh(
+          new THREE.BoxBufferGeometry(0.01, 0.1, 0.01),
+          new THREE.MeshBasicMaterial({
+              color: new THREE.Color(0xFFFFFF),
+              depthWrite: false,
+              depthTest: false,
+          }));
+      m2.position.set(0, 0, -2);
+      this.uiCamera_.add(m1);
+      this.uiCamera_.add(m2);
+
+      if (!hack_defs.showTools) {
+        m1.visible = false;
+        m2.visible = false;
+      }
+
+      // const controls = new OrbitControls(
+      //   this.camera_, this.threejs_.domElement);
+      // controls.target.set(2, 0, 2);
+      // controls.update();
+
+      this.LoadSky_();
+      this.OnResize_();
+    }
+
+    OnResize_() {
+      this.camera_.aspect = window.innerWidth / window.innerHeight;
+      this.camera_.updateProjectionMatrix();
+      this.threejs_.setSize(window.innerWidth, window.innerHeight);
+      this.composer_.setSize(window.innerWidth, window.innerHeight);
+
+      const pixelRatio = this.threejs_.getPixelRatio();
+
+      this.fxaa_.material.uniforms[ 'resolution' ].value.x = 1 / ( window.innerWidth * pixelRatio );
+      this.fxaa_.material.uniforms[ 'resolution' ].value.y = 1 / ( window.innerHeight * pixelRatio );
+    }
+
+    LoadSky_() {
+      const hemiLight = new THREE.HemisphereLight(0x424a75, 0xFFFFFF, 0.9);
+      this.scene_.add(hemiLight);
+      this.uiScene_.add(hemiLight.clone());
+  
+  
+      const loader = new THREE.CubeTextureLoader();
+      const texture = loader.load([
+          './resources/terrain/space-posx.jpg',
+          './resources/terrain/space-negx.jpg',
+          './resources/terrain/space-posy.jpg',
+          './resources/terrain/space-negy.jpg',
+          './resources/terrain/space-posz.jpg',
+          './resources/terrain/space-negz.jpg',
+      ]);
+      texture.encoding = THREE.sRGBEncoding;
+  
+      const uniforms = {
+        "topColor": { value: defs.SKY_COLOUR.clone() },
+        "bottomColor": { value: defs.FOG_COLOUR.clone() },
+        "offset": { value: 0 },
+        "exponent": { value: 0.6 },
+        "background": { value: texture },
+        "whiteBlend": { value: 0.0 },
+        "playerPos": { value: new THREE.Vector3() },
+        time: {
+          value: 0.0,
+        },
+      };
+      // uniforms["topColor"].value.copy(hemiLight.color);
+  
+      const skyGeo = new THREE.SphereBufferGeometry(5000, 32, 15);
+      const skyMat = new THREE.ShaderMaterial({
+          uniforms: uniforms,
+          vertexShader: _VS,
+          fragmentShader: _FS,
+          side: THREE.BackSide,
+      });
+  
+      const sky = new THREE.Mesh(skyGeo, skyMat);
+      this.sky_ = sky;
+      this.scene_.add(sky);
+    }
+
+    Update(timeElapsed) {
+      const player = this.FindEntity('player');
+      if (!player) {
+        return;
+      }
+      const pos = player._position;
+  
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(player.Quaternion);
+      forward.multiplyScalar(750);
+
+      this.sun_.position.copy(pos);
+      this.sun_.position.add(new THREE.Vector3(-50, 200, -10));
+      this.sun_.target.position.copy(pos);
+      this.sun_.updateMatrixWorld();
+      this.sun_.target.updateMatrixWorld();
+
+      this.sky_.position.copy(new THREE.Vector3(pos.x, 0, pos.z));
+      this.sky_.material.uniforms.playerPos.value.copy(pos);
+      this.sky_.material.uniforms.time.value += timeElapsed;
+      this.sky_.material.needsUpdate = true;
+    }
+
+    Render() {
+      this.uiCamera_.position.copy(this.camera_.position);
+      this.uiCamera_.quaternion.copy(this.camera_.quaternion);
+
+      this.composer_.render();
+      // this.threejs_.render(this.scene_, this.camera_);
+    }
+  }
+
+  return {
+      ThreeJSController: ThreeJSController,
+  };
+})();

+ 83 - 0
src/ui-controller.js

@@ -0,0 +1,83 @@
+import {entity} from './entity.js';
+
+import {texture_defs} from './texture-defs.js';
+import {hack_defs} from './hack-defs.js';
+
+
+export const ui_controller = (() => {
+
+  class UIController extends entity.Component {
+    constructor() {
+      super();
+    }
+
+    InitEntity() {
+      this.iconBar_ = document.getElementById('icon-bar');
+      this.icons_ = [];
+
+      const blockTypes = [
+          'dirt', 'stone', 'sand', 'grass', 'snow', 'moon', 'tree_bark', 'tree_leaves'
+      ];
+
+      for (let b of blockTypes) {
+        const e = document.createElement('DIV');
+
+        let textureName = texture_defs.DEFS[b].texture;
+        if (textureName instanceof Array) {
+          textureName = textureName[2];
+        }
+
+        e.className = 'icon-bar-item';
+        e.style = "background-image: url('./resources/minecraft/textures/blocks/" + textureName + "');";
+        e.blockType = b;
+
+        this.iconBar_.appendChild(e);
+        this.icons_.push(e);
+      }
+
+      this.toolTypes_ = ['build', 'break'];
+      this.toolIndex_ = 0;
+      this.iconIndex_ = 0;
+      this.icons_[0].classList.toggle('highlight');
+      this.UpdateToolBlockType_();
+      this.UpdateToolType_();
+
+      if (!hack_defs.showTools) {
+        this.iconBar_.style.display = 'none';
+      }
+    }
+
+    CycleBuildIcon_(dir) {
+      this.icons_[this.iconIndex_].classList.remove('highlight');
+      this.iconIndex_ = (this.iconIndex_ + this.icons_.length + dir) % this.icons_.length;
+      this.icons_[this.iconIndex_].classList.toggle('highlight');
+
+      this.UpdateToolBlockType_();
+    }
+
+    CycleTool_() {
+      this.toolIndex_ = (this.toolIndex_ + 1) % this.toolTypes_.length;
+      this.UpdateToolType_();
+    }
+
+    UpdateToolBlockType_() {
+      const player = this.FindEntity('player');
+      player.Broadcast({
+          topic: 'ui.blockChanged',
+          value: this.icons_[this.iconIndex_].blockType,
+      });
+    }
+
+    UpdateToolType_() {
+      const player = this.FindEntity('player');
+      player.Broadcast({
+          topic: 'ui.toolChanged',
+          value: this.toolTypes_[this.toolIndex_],
+      });
+    }
+  };
+
+  return {
+      UIController: UIController,
+  };
+})();

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

+ 1338 - 0
src/voxel-block-builder.js

@@ -0,0 +1,1338 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {noise} from './noise.js';
+import {math} from './math.js';
+import {foliage_sdfs} from './foliage-sdfs.js';
+import {hack_defs} from './hack-defs.js';
+
+
+export const voxel_block_builder = (() => {
+
+  const _VOXEL_HEIGHT = 128;
+  const _OCEAN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.05);
+  const _BEACH_LEVEL = _OCEAN_LEVEL + 4;
+  const _SNOW_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.7);
+  const _MOUNTAIN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.3);
+
+  const _OCEAN_C = new THREE.Color(0x8080FF);
+  const _BEACH_C = new THREE.Color(0xFFFF80);
+  const _SNOW_C = new THREE.Color(0xFFFFFF);
+  const _STONE_C = new THREE.Color(0x404040);
+  const _GRASS_C = new THREE.Color(0x40FF40);
+
+  function Biome(e, m) {
+    if (e < _OCEAN_LEVEL) return 'sand';
+    if (e < _BEACH_LEVEL) return 'sand';
+
+    if (e > _SNOW_LEVEL) {
+      return 'snow';
+    }
+
+    if (e > _MOUNTAIN_LEVEL) {
+      // if (m < 0.2) {
+      //   return 'stone';
+      // } else if (m < 0.25) {
+      //   return 'grass';
+      // }
+    }
+    return 'grass';
+  }
+
+
+  class TerrainGeneratorFlat {
+    constructor() {
+    }
+
+    Get(x, z) {
+      if (x == 0 && z == 0) {
+        // return ['grass', 4097];
+      }
+      return ['grass', 0];
+    }
+  };
+
+  const _N_Perlin = new noise.Noise({
+    seed: 6,
+    octaves: 1,
+    scale: 128,
+    persistence: 0.5,
+    lacunarity: 2.0,
+    exponentiation: 4,
+    height: 32,
+  });
+
+  class TerrainGeneratorBasicPerlin {
+    constructor(params) {
+      this.params_ = params;
+    }
+
+    Get(x, y) {
+      const height = _N_Perlin.Get(x, y, 0.0);
+      
+      const elevation =  Math.floor(height);
+    
+      if (elevation < _OCEAN_LEVEL + 2) {
+        return ['sand', elevation];
+      }
+      return ['grass', elevation];
+    }
+  };
+
+
+  function BiomeDemo(e, m, roll) {
+    if (e < _OCEAN_LEVEL) return 'sand';
+    if (e < _BEACH_LEVEL) return 'sand';
+
+    if (e > _SNOW_LEVEL * roll) {
+      return 'snow';
+    }
+
+    if (e > _MOUNTAIN_LEVEL * roll) {
+      return 'stone';
+    }
+
+    // if (m < 0.1) {
+    //   return 'sand';
+    // }
+
+    return 'grass';
+  }
+
+  const _TMP_V1 = new THREE.Vector3();
+
+
+  class TerrainGeneratorDemo {
+    constructor(params) {
+      this.params_ = params;
+      this.pos_ = new THREE.Vector3(-1943, 0, -419);
+
+      this.N_Demo1_ = new noise.Noise({
+        seed: 6,
+        octaves: 5,
+        scale: 1024,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 4,
+        height: 1,
+      });
+    
+      this.N_Demo2_ = new noise.Noise({
+        seed: 4,
+        octaves: 10,
+        scale: 4096,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 7,
+        height: 1,
+        ridged: true,
+      });
+    
+    
+      this.N_Demo3_ = new noise.Noise({
+        seed: 10,
+        octaves: 3,
+        scale: 32,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 7,
+        height: 1
+      });
+    }
+
+    Get(x, y) {
+      const normalizedHeight = this.N_Demo1_.Get(x, y, 0.0);
+      const normalizedHeight2 = this.N_Demo2_.Get(x, y, 0.0);
+      // const areaHeight = _N_Height.Get(x, y, 0);
+      const areaHeight = 128;
+      let variableHeight = areaHeight * normalizedHeight;
+      let mountainHeight = 128 * normalizedHeight2;
+
+      const p = _TMP_V1.set(x, 0, y);
+      const d = p.distanceTo(this.pos_);
+
+      const f3 = this.N_Demo3_.Get(x, 20, y) * 0.2 + 0.8;
+    
+      // Mix from mountains to rolling hills
+      const f1 = math.sat((d - 50) / 25);
+      const mixedHeight = math.lerp(f1 ** 0.25, mountainHeight, variableHeight);
+
+      // Mix from that to flat ocean
+      const f2 = math.sat((d - 200) / 200);
+      const finalHeight = math.lerp(f2, mixedHeight, 0);
+      
+      const elevation =  Math.floor(finalHeight);
+      const moisture = _N_Moisture.Get(x, y, 0.0);
+    
+      const roll = this.N_Demo3_.Get(x, 10, y) * 0.4 + 0.6;
+    
+      return [BiomeDemo(elevation, moisture, roll), elevation];
+    }
+  };
+
+
+  class TerrainGeneratorMoon {
+    constructor(params) {
+      this.params_ = params;
+
+      this.N_Moon_ = new noise.Noise({
+        seed: 4,
+        octaves: 5,
+        scale: 1024,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 4,
+        height: 1,
+      });
+
+      this.N_Craters_ = new noise.Noise({
+        seed: 7,
+        octaves: 1,
+        scale: 0.99,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 1,
+        height: 1,
+      });
+    
+
+      this.InitCraters_();
+    }
+
+    InitCraters_() {
+      // Hack, dimensions needs to be divisible by stride, good luck to anybody who
+      // changes this.
+      this.craters_ = [];
+      for (let x = -this.params_.dimensions.x * 10; x <= this.params_.dimensions.x * 10; x+= 8) {
+        for (let z = -this.params_.dimensions.z * 10; z <= this.params_.dimensions.z * 10; z+= 8) {
+          const xPos = x + this.params_.offset.x;
+          const zPos = z + this.params_.offset.z;
+
+          const roll = this.N_Craters_.Get(xPos, 0.0, zPos);
+          if (roll > 0.95) {
+            const craterSize = Math.min(
+                (this.N_Craters_.Get(xPos, 1.0, zPos) ** 4.0) * 100, 50.0) + 4.0;
+            this.craters_.push([new THREE.Vector3(xPos, 0, zPos), craterSize]);
+          }
+        }
+      }
+      let a = 0;
+    }
+
+    Get(x, z) {
+      const n1 = this.N_Moon_.Get(x, z, 10.0);
+      const n2 = this.N_Moon_.Get(x, z, 20.0);
+      const normalizedHeight = Math.round(this.N_Moon_.Get(x + n1, z + n2, 0.0) * 64);
+    
+      let totalHeight = normalizedHeight;
+    
+      for (let i = 0; i < this.craters_.length; ++i) {
+        const pos = new THREE.Vector3(x, 0, z);
+        const [crater, radius] = this.craters_[i];
+        const d = crater.distanceTo(pos);
+        const craterWidth = radius;
+        if (d < craterWidth * 2) {
+          // Just some random crap to make a crater-like thing
+          const rimWidth = radius / 4;
+          const rimStart = Math.abs(d - (craterWidth - rimWidth));
+          const rimFactor = math.sat(rimStart / rimWidth);
+          const rimHeightFactor = 1.0 - rimFactor ** 0.5;
+          const rimHeight = radius / 10;
+    
+          const craterFactor = 1.0 - math.sat((d - (craterWidth - rimWidth * 2)) / rimWidth) ** 2;
+    
+          totalHeight += rimHeightFactor * rimHeight + craterFactor * -(rimHeight * 2.0);
+        }
+      }
+      return ['moon', Math.round(totalHeight)];
+    }
+  };
+
+
+  class TerrainGeneratorWorld {
+    constructor(params) {
+      this.params_ = params;
+
+      this.moon_ = new TerrainGeneratorMoon(params);
+      this.grass_ = new TerrainGeneratorGrass(params);
+      this.sand_ = new TerrainGeneratorSand(params);
+      this.rocky_ = new TerrainGeneratorRocky(params);
+      
+      this.N_Height_ = new noise.Noise({
+          seed: 100,
+          octaves: 1,
+          scale: 4096,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 32,
+      });
+      this.N_Roll_ = new noise.Noise({
+          seed: 200,
+          octaves: 1,
+          scale: 8,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 1,
+      });
+      this.N_ = new noise.Noise({
+          seed: 4,
+          octaves: 0.99,
+          scale: 1,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 4,
+      });
+      this.N_Types_ = new noise.Noise({
+          seed: 8,
+          octaves: 0.99,
+          scale: 1,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 4,
+      });
+    }
+
+    Biome_(x, z, elevation, moisture) {
+      const mp = math.smootherstep(moisture, 0, 1);
+      const ep = math.smootherstep(elevation / 128.0, 0, 1);
+
+      const m1e1 = ['sand', 0];
+      const m1e2 = this.moon_.Get(x, z);
+      const m2e1 = ['grass', 0];
+      const m2e2 = ['stone', 0];
+
+      const r1 = math.lerp(mp, m1e1[1], m2e1[1]);
+      const r2 = math.lerp(mp, m1e2[1], m2e2[1]);
+      const r3 = math.lerp(ep, r1, r2);
+
+      const f1 = mp < 0.5 ? m1e1[0] : m2e1[0];
+      const f2 = mp < 0.5 ? m1e2[0] : m2e2[0];
+      const f3 = ep < 0.5 ? f1 : f2;
+
+      return [f3, Math.floor(r3)];
+    }
+
+    Get2(x, z) {
+      const height = this.N_Height_.Get(x, 0.0, z);
+      const elevation =  Math.floor(height);
+      const moisture = this.N_Moisture_.Get(x, 0.0, z);
+
+      return this.Biome_(x, z, elevation, moisture);
+    }
+
+    ChooseTerrainType_(x, z) {
+      const cellSize = 1024.0;
+      const cellIndex = [Math.floor(x / cellSize), Math.floor(z / cellSize)];
+      const cellPosition = [cellIndex[0] * cellSize, cellIndex[1] * cellSize];
+      const cellCenter = [
+          Math.round(this.N_.Get(cellIndex[0], 0.0, cellIndex[1]) * cellSize),
+          Math.round(this.N_.Get(cellIndex[0], 1.0, cellIndex[1]) * cellSize)];
+
+      cellCenter[0] = cellPosition[0] + cellSize * 0.5;
+      cellCenter[1] = cellPosition[1] + cellSize * 0.5;
+
+      const dist = ((x - cellCenter[0]) ** 2 + (z - cellCenter[1]) ** 2) ** 0.5;
+      const falloff = math.sat((dist - cellSize * 0.25) / (cellSize * 0.25));
+
+      const biomeType = Math.round(this.N_Types_.Get(cellIndex[0], 0.0, cellIndex[1]));
+
+      let res = null;
+      if (biomeType == 0) {
+        res = this.rocky_.Get(x, z);
+      } else if (biomeType == 1) {
+        res = this.sand_.Get(x, z);
+      } else if (biomeType == 2) {
+        res = this.grass_.Get(x, z);
+      } else if (biomeType == 3) {
+        res = ['snow', 15];
+      } else if (biomeType == 4) {
+        res = this.moon_.Get(x, z);
+      }
+
+      res[1] = math.lerp(math.smootherstep(falloff, 0.0, 1.0), res[1], 0.0);
+      const roll = this.N_Roll_.Get(x, 2.0, z);
+
+      const typeFalloff = math.sat((dist - cellSize * 0.375) / (cellSize * 0.125));
+
+      if (typeFalloff > roll) {
+        res[0] = 'grass';
+      }
+
+      if (res[1] < _OCEAN_LEVEL) {
+        res[0] = 'sand';
+      }
+
+      return res;
+    }
+
+    Get(x, z) {
+      const result = this.ChooseTerrainType_(x, z);
+      result[1] = Math.round(result[1]);
+      return result;
+    }
+  };
+
+
+  class TerrainGeneratorRocky {
+    constructor(params) {
+      this.params_ = params;
+
+      this.N_Terrain_ = new noise.Noise({
+          seed: 9,
+          octaves: 6,
+          scale: 500.005,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 6,
+          height: 64,
+          ridged: true,
+      });
+
+      this.N_Roll_ = new noise.Noise({
+          seed: 200,
+          octaves: 2,
+          scale: 8,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 1,
+      });
+
+      this.N_Height_ = new noise.Noise({
+          seed: 100,
+          octaves: 1,
+          scale: 64,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 1,
+          range: [0.25, 1],
+      });
+    }
+
+    Get(x, z) {
+      const height = this.N_Terrain_.Get(x, 0.0, z) * this.N_Height_.Get(x, 0, z);
+
+      const elevation =  Math.floor(height);
+      const roll = this.N_Roll_.Get(x, 0.0, z);
+
+      const heightFactor = (elevation / 32.0);
+      let type = 'stone';
+      if (roll > heightFactor) {
+        type = 'dirt';
+      }
+
+      return [type, elevation];
+    }
+  };
+
+
+  class TerrainGeneratorSand {
+    constructor(params) {
+      this.params_ = params;
+
+      this.N_Terrain_ = new noise.Noise({
+          seed: 4,
+          octaves: 4,
+          scale: 500.005,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 6,
+          height: 1,
+          range: [-1, 1],
+      });
+
+      this.N_Height_ = new noise.Noise({
+          seed: 4,
+          octaves: 3,
+          scale: 500.005,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 64,
+      });
+    }
+
+    Get(x, z) {
+      const n1 = [this.N_Terrain_.Get(x, 0.0, z), this.N_Terrain_.Get(x, 1.0, z)];
+      const height = this.N_Height_.Get(x + n1[0], 0.0, z + n1[1]);
+
+      const elevation =  Math.floor(height);
+    
+      return ['sand', elevation];
+    }
+  };
+
+
+  class TerrainGeneratorGrass {
+    constructor(params) {
+      this.params_ = params;
+
+      this.N_Terrain_ = new noise.Noise({
+          seed: 4,
+          octaves: 6,
+          scale: 4096,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 6,
+          height: 1,
+      });
+
+      this.N_Height_ = new noise.Noise({
+          seed: 4,
+          octaves: 3,
+          scale: 4096,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 512,
+      });
+    
+      this.N_Plateaus_ = new noise.Noise({
+          seed: 5,
+          octaves: 4,
+          scale: 512,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 2,
+          height: 1,
+      });
+    
+      this.N_PlateausNum_ = new noise.Noise({
+          seed: 6,
+          octaves: 4,
+          scale: 1024,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 1,
+          height: 20,
+      });
+
+      this.N_Moisture_ = new noise.Noise({
+          seed: 3,
+          octaves: 3,
+          scale: 512,
+          persistence: 0.5,
+          lacunarity: 2.0,
+          exponentiation: 4,
+          height: 1,
+      });
+    }
+
+    Get(x, y) {
+      const normalizedHeight = this.N_Terrain_.Get(x, y, 0.0);
+      const areaHeight = this.N_Height_.Get(x, y, 0);
+      let variableHeight = areaHeight * normalizedHeight;
+      if (this.N_Plateaus_.Get(x, y, 0.0) > 0.25) {
+        const numPlateaus = Math.round(10 + this.N_PlateausNum_.Get(x, y, 0));
+        const plateauHeight = Math.round(areaHeight / numPlateaus);
+        variableHeight = Math.round(variableHeight / plateauHeight) * plateauHeight;
+      }
+    
+      const elevation =  Math.floor(variableHeight);
+      const moisture = this.N_Moisture_.Get(x, y, 0.0);
+    
+      return [Biome(elevation, moisture), elevation];
+    }
+  };
+
+
+  // HACKY TODO: Pass a terrain generation object through instead of these
+  // loose functions.
+
+  const _N_Luminance = new noise.Noise({
+    seed: 10,
+    octaves: 1,
+    scale: 0.99,
+    persistence: 0.5,
+    lacunarity: 2.0,
+    exponentiation: 4,
+    height: 1
+  });
+
+
+  const _N_FadeIn = new noise.Noise({
+    seed: 11,
+    octaves: 4,
+    scale: 2.01,
+    persistence: 0.5,
+    lacunarity: 2.0,
+    exponentiation: 1,
+    height: 1,
+  });
+
+
+  // Using a straight scale of 1.0 seems to produce bad values when using
+  // integer inputs.
+  const _N_Foliage = new noise.Noise({
+    seed: 7,
+    octaves: 1,
+    scale: 0.99,
+    persistence: 0.5,
+    lacunarity: 2.0,
+    exponentiation: 1,
+    height: 1,
+  });
+
+
+
+  class SDFList {
+    constructor() {
+      this.sdfs_ = [];
+    }
+
+    Add(sdf) {
+      this.sdfs_.push(sdf);
+    }
+
+    Intersects(aabb) {
+      for (let i = 0; i < this.sdfs_.length; ++i) {
+        const s = this.sdfs_[i];
+        if (s.AABB.intersectsBox(aabb)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    Evaluate(x, y, z) {
+      const pos = new THREE.Vector3(x, y, z);
+
+      for (let i = 0; i < this.sdfs_.length; ++i) {
+        const s = this.sdfs_[i];
+        if (s.AABB.containsPoint(pos)) {
+          const res = s.Evaluate(pos);
+          if (res) {
+            return res;
+          }
+        }
+      }
+    }
+  };
+
+
+  class _VoxelBuilderThreadedWorker {
+    constructor() {
+      this.Create_();
+    }
+
+    Create_() {
+      const pxGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      pxGeometry.rotateY(Math.PI / 2);
+      pxGeometry.translate(0.5, 0, 0);
+
+      const nxGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      nxGeometry.rotateY(-Math.PI / 2);
+      nxGeometry.translate(-0.5, 0, 0);
+
+      const pyGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      pyGeometry.rotateX(-Math.PI / 2);
+      pyGeometry.translate(0, 0.5, 0);
+
+      const nyGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      nyGeometry.rotateX(Math.PI / 2);
+      nyGeometry.translate(0, -0.5, 0);
+
+      const pzGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      pzGeometry.translate(0, 0, 0.5);
+
+      const nzGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      nzGeometry.rotateY( Math.PI );
+      nzGeometry.translate(0, 0, -0.5);
+
+      const invertUvs = [pxGeometry, nxGeometry, pzGeometry, nzGeometry];
+      for (let geo of invertUvs) {
+        for (let i = 0; i < geo.attributes.uv.array.length; i+=2) {
+          geo.attributes.uv.array[i + 1] = 1.0 - geo.attributes.uv.array[i + 1];
+        }
+      }
+
+      this.geometries_ = [
+          pxGeometry, nxGeometry,
+          pyGeometry, nyGeometry,
+          pzGeometry, nzGeometry
+      ];
+    }
+
+    Init(params) {
+      this.params_ = params;
+      this.params_.offset = new THREE.Vector3(...params.offset);
+      this.params_.dimensions = new THREE.Vector3(...params.dimensions);
+      if (hack_defs.useFlatTerrain) {
+        this.terrainGenerator_ = new TerrainGeneratorFlat(params);
+      } else {
+        this.terrainGenerator_ = new TerrainGeneratorWorld(params);
+        // this.terrainGenerator_ = new TerrainGeneratorBasicPerlin(params);
+        // this.terrainGenerator_ = new TerrainGeneratorMoon(params);
+      }
+    }
+
+    GenerateNoise_(x, y) {
+      return this.terrainGenerator_.Get(x, y);
+    }
+
+    Key_(x, y, z) {
+      return x + '.' + y + '.' + z;
+    }
+
+    PruneHiddenVoxels_(cells) {
+      if (hack_defs.skipPruning) {
+        return Object.assign({}, cells);
+      }
+
+      const prunedVoxels = {};
+      for (let k in cells) {
+        const curCell = cells[k];
+
+        const k1 = this.Key_(
+            curCell.position[0] + 1,
+            curCell.position[1],
+            curCell.position[2]);
+        const k2 = this.Key_(
+            curCell.position[0] - 1,
+            curCell.position[1],
+            curCell.position[2]);
+        const k3 = this.Key_(
+            curCell.position[0],
+            curCell.position[1] + 1,
+            curCell.position[2]);
+        const k4 = this.Key_(
+            curCell.position[0],
+            curCell.position[1] - 1,
+            curCell.position[2]);
+        const k5 = this.Key_(
+            curCell.position[0],
+            curCell.position[1],
+            curCell.position[2] + 1);
+        const k6 = this.Key_(
+            curCell.position[0],
+            curCell.position[1],
+            curCell.position[2] - 1);
+
+        const keys = [k1, k2, k3, k4, k5, k6];
+        let visible = false;
+        for (let i = 0; i < 6; ++i) {
+          const faceHidden = (keys[i] in cells);
+          curCell.facesHidden[i] = faceHidden;
+
+          if (!faceHidden) {
+            visible = true;
+          }
+        }
+
+        if (visible) {
+          prunedVoxels[k] = curCell;
+        }
+      }
+      return prunedVoxels;
+    }
+
+    CreateFoliageSDFs_() {
+      const sdfs = new SDFList();
+
+      if (hack_defs.hardcodedFoliageEnabled) {
+        const xPos = 10;
+        const zPos = 10;
+        const yPos = 0;
+
+        // sdfs.Add(foliage_sdfs.SPHERE(0, 10, 0, 10));
+        // sdfs.Add(foliage_sdfs.TREE2(0, 0, 0));
+        // sdfs.Add(foliage_sdfs.PALM_TREE1(0, 0, 0));
+
+        // sdfs.Add(foliage_sdfs.TREE2(50, 0, -50));
+        // sdfs.Add(foliage_sdfs.TREE2(70, 0, 10));
+        // sdfs.Add(foliage_sdfs.CONE1(0, 0, 0));
+        // sdfs.Add(foliage_sdfs.CRATER1(10, 0, 10));
+
+        sdfs.Add(foliage_sdfs.TREE1(19568, 0, 1608));
+
+        // sdfs.Add(foliage_sdfs.TREE2(-1955, 13, -251));
+        // sdfs.Add(foliage_sdfs.TREE2(-1855, 14, -381));
+        // sdfs.Add(foliage_sdfs.TREE1(-1815, 11, -285));
+      }
+
+      if (hack_defs.foliageEnabled) {
+        for (let x = -this.params_.dimensions.x * 4; x < this.params_.dimensions.x * 4; x+= 16) {
+          for (let z = -this.params_.dimensions.z * 4; z < this.params_.dimensions.z * 4; z+= 16) {
+            const xPos = x + this.params_.offset.x;
+            const zPos = z + this.params_.offset.z;
+    
+            const roll = _N_Foliage.Get(xPos, 0.0, zPos);
+            if (roll > 0.8) {
+              const [atlasType, yOffset] = this.GenerateNoise_(xPos, zPos);
+              const yPos = yOffset;
+    
+              if (yPos <= _OCEAN_LEVEL) {
+                continue;
+              }
+
+              if (atlasType == 'grass') {
+                let treeType = foliage_sdfs.TREE1;
+                if (_N_Foliage.Get(xPos, 1.0, zPos) < 0.15) {
+                  treeType = foliage_sdfs.TREE2;
+                }
+                sdfs.Add(treeType(xPos, yPos, zPos));
+              } else if (atlasType == 'sand') {
+                let treeType = foliage_sdfs.PALM_TREE1;
+                sdfs.Add(treeType(xPos, yPos, zPos));
+              }
+            }
+          }
+        }  
+      }
+      return sdfs;
+    }
+
+    CreateTerrain_() {
+      const cells = {};
+      const toRemove = [];
+
+      const xn = hack_defs.skipExteriorBlocks ? 0 : -1;
+      const zn = hack_defs.skipExteriorBlocks ? 0 : -1;
+      const xp = (hack_defs.skipExteriorBlocks ?
+          this.params_.dimensions.x : this.params_.dimensions.x + 1);
+      const zp = (hack_defs.skipExteriorBlocks ?
+          this.params_.dimensions.x : this.params_.dimensions.x + 1);
+
+      for (let x = xn; x < xp; x++) {
+        for (let z = zn; z < zp; z++) {
+          const xPos = x + this.params_.offset.x;
+          const zPos = z + this.params_.offset.z;
+
+          const [atlasType, yOffset] = this.GenerateNoise_(xPos, zPos);
+          const yPos = yOffset;
+
+          const k = this.Key_(xPos, yPos, zPos);
+
+          cells[k] = {
+            position: [xPos, yPos, zPos],
+            type: atlasType,
+            visible: true,
+            facesHidden: [false, false, false, false, false],
+            ao: [null, null, null, null, null, null],
+          };
+
+          // HACK
+          if (hack_defs.introEnabled) {
+            for (let yi = yPos - 1; yi > -20; yi--) {
+              const ky = this.Key_(xPos, yi, zPos);
+    
+              cells[ky] = {
+                position: [xPos, yi, zPos],
+                type: 'dirt',
+                visible: true,
+                facesHidden: [false, false, false, false, false],
+                ao: [null, null, null, null, null, null],
+              };
+            }
+          }
+
+          // Possibly have to generate cliffs
+          let lowestAdjacent = yOffset;
+          for (let xi = -1; xi <= 1; xi++) {
+            for (let zi = -1; zi <= 1; zi++) {
+              const [_, otherOffset] = this.GenerateNoise_(xPos + xi, zPos + zi);
+              lowestAdjacent = Math.min(otherOffset, lowestAdjacent);
+            }
+          }
+
+          if (lowestAdjacent < yOffset) {
+            for (let yi = lowestAdjacent + 1; yi < yOffset; yi++) {
+              const ki = this.Key_(xPos, yi, zPos);
+              cells[ki] = {
+                position: [xPos, yi, zPos],
+                type: atlasType,
+                visible: true,
+                facesHidden: [false, false, false, false, false],
+                ao: [null, null, null, null, null, null],
+              };
+
+              if (atlasType == 'grass' || atlasType == 'snow') {
+                cells[ki].type = 'dirt';
+              }
+            }
+          }
+        }
+      }
+
+      return cells;
+    }
+
+    ApplySDFsToVoxels_(sdfs, cells) {
+      const p1 = this.params_.offset.clone();
+      const p2 = this.params_.offset.clone().add(this.params_.dimensions);
+      const aabb = new THREE.Box3(p1, p2);
+
+      if (sdfs.Intersects(aabb) || true) {
+        for (let x = -1; x < this.params_.dimensions.x + 1; x++) {
+          for (let z = -1; z < this.params_.dimensions.z + 1; z++) {
+            const xPos = x + this.params_.offset.x;
+            const zPos = z + this.params_.offset.z;
+            const [_, yOffset] = this.GenerateNoise_(xPos, zPos);
+
+            for (let y = 0; y < 100; y++) {
+              const yPos = yOffset + y;
+              const k = this.Key_(xPos, yPos, zPos);
+              if (k in cells) {
+                continue;
+              }
+
+              const res = sdfs.Evaluate(xPos, yPos, zPos);
+              if (res) {
+                let roll = 0;
+                if (res == 'tree_leaves' && !hack_defs.skipFoliageNoise) {
+                  roll = _N_Foliage.Get(xPos, yPos, zPos);
+                }
+                if (roll < 0.7) {
+                  cells[k] = {
+                    position: [xPos, yPos, zPos],
+                    type: res,
+                    visible: true,
+                    facesHidden: [false, false, false, false, false],
+                    ao: [null, null, null, null, null, null],
+                  };
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    CreateOcean_(groundVoxels) {
+      const cells = {};
+
+      for (let x = -1; x < this.params_.dimensions.x + 1; x++) {
+        for (let z = -1; z < this.params_.dimensions.z + 1; z++) {
+          const xPos = x + this.params_.offset.x;
+          const zPos = z + this.params_.offset.z;
+
+          const [_, yPos] = this.GenerateNoise_(xPos, zPos);
+
+          if (yPos < _OCEAN_LEVEL) {
+            const ko = this.Key_(xPos, _OCEAN_LEVEL, zPos);
+            cells[ko] = {
+              position: [xPos, _OCEAN_LEVEL, zPos],
+              type: 'ocean',
+              visible: true,
+              facesHidden: [false, false, false, false, false],
+              ao: [null, null, null, null, null, null],
+            };
+
+            // HACK
+            if (hack_defs.introEnabled) {
+              for (let yi = 1; yi < 20; ++yi) {
+                const ky = this.Key_(xPos, _OCEAN_LEVEL - yi, zPos);
+    
+                if (!(ky in groundVoxels)) {
+                  cells[ky] = {
+                    position: [xPos, yi, zPos],
+                    type: 'ocean',
+                    visible: true,
+                    facesHidden: [false, false, false, false, false],
+                    ao: [null, null, null, null, null, null],
+                  };
+                }
+              }
+            }
+          }
+        }
+      }
+
+      return cells;
+    }
+
+    BuildAO_(cells) {
+      if (hack_defs.skipAO) {
+        return;
+      }
+      for (let k in cells) {
+        const curCell = cells[k];
+
+        const _Occlusion = (x, y, z) => {
+          const k = this.Key_(
+              curCell.position[0] + x, curCell.position[1] + y, curCell.position[2] + z);
+          if (k in cells) {
+            return 0.75;
+          }
+          return 1.0;
+        }
+
+
+        // +x
+        if (!curCell.facesHidden[0]) {
+          curCell.ao[0] = [
+            _Occlusion(1, 0, 1) * _Occlusion(1, 1, 0) * _Occlusion(1, 1, 1),
+            _Occlusion(1, 0, -1) * _Occlusion(1, 1, 0) * _Occlusion(1, 1, -1),
+            _Occlusion(1, 0, 1) * _Occlusion(1, -1, 0) * _Occlusion(1, -1, 1),
+            _Occlusion(1, 0, -1) * _Occlusion(1, -1, 0) * _Occlusion(1, -1, -1)
+          ];
+        }
+
+        // -x
+        if (!curCell.facesHidden[1]) {
+          curCell.ao[1] = [
+            _Occlusion(-1, 0, -1) * _Occlusion(-1, 1, 0) * _Occlusion(-1, 1, -1),
+            _Occlusion(-1, 0, 1) * _Occlusion(-1, 1, 0) * _Occlusion(-1, 1, 1),
+            _Occlusion(-1, 0, -1) * _Occlusion(-1, -1, 0) * _Occlusion(-1, -1, -1),
+            _Occlusion(-1, 0, 1) * _Occlusion(-1, -1, 0) * _Occlusion(-1, -1, 1),
+          ];
+        }
+
+        // +y
+        if (!curCell.facesHidden[2]) {
+          // curCell.ao[2] = [1.0, 1.0, 0.5, 0.5];
+          curCell.ao[2] = [
+            _Occlusion(0, 1, -1) * _Occlusion(-1, 1, 0) * _Occlusion(-1, 1, -1),
+            _Occlusion(0, 1, -1) * _Occlusion(1, 1, 0) * _Occlusion(1, 1, -1),
+            _Occlusion(0, 1, 1) * _Occlusion(-1, 1, 0) * _Occlusion(-1, 1, 1),
+            _Occlusion(0, 1, 1) * _Occlusion(1, 1, 0) * _Occlusion(1, 1, 1),
+          ];
+        }
+
+        // -y
+        if (!curCell.facesHidden[3]) {
+          curCell.ao[3] = [
+            _Occlusion(0, -1, 1) * _Occlusion(-1, -1, 0) * _Occlusion(-1, -1, 1),
+            _Occlusion(0, -1, 1) * _Occlusion(1, -1, 0) * _Occlusion(1, -1, 1),
+            _Occlusion(0, -1, -1) * _Occlusion(-1, -1, 0) * _Occlusion(-1, -1, -1),
+            _Occlusion(0, -1, -1) * _Occlusion(1, -1, 0) * _Occlusion(1, -1, -1),
+          ];
+        }
+
+        // +z
+        if (!curCell.facesHidden[4]) {
+          curCell.ao[4] = [
+            _Occlusion(-1, 0, 1) * _Occlusion(0, 1, 1) * _Occlusion(-1, 1, 1),
+            _Occlusion(1, 0, 1) * _Occlusion(0, 1, 1) * _Occlusion(1, 1, 1),
+            _Occlusion(-1, 0, 1) * _Occlusion(0, -1, 1) * _Occlusion(-1, -1, 1),
+            _Occlusion(1, 0, 1) * _Occlusion(0, -1, 1) * _Occlusion(1, -1, 1),
+          ];
+        }
+
+        // -z
+        if (!curCell.facesHidden[5]) {
+          curCell.ao[5] = [
+            _Occlusion(1, 0, -1) * _Occlusion(0, 1, -1) * _Occlusion(1, 1, -1),
+            _Occlusion(-1, 0, -1) * _Occlusion(0, 1, -1) * _Occlusion(-1, 1, -1),
+            _Occlusion(1, 0, -1) * _Occlusion(0, -1, -1) * _Occlusion(1, -1, -1),
+            _Occlusion(-1, 0, -1) * _Occlusion(0, -1, -1) * _Occlusion(-1, -1, -1),
+          ];
+        }
+      }
+      return cells;
+    }
+
+    ApplyFadeIn_(cells) {
+      if (this.params_.currentTime < 0.0 || this.params_.currentTime > 1.0) {
+        return;
+      }
+
+      const timeBiased = this.params_.currentTime ** 2;
+      const yLowerBound = timeBiased;
+      const yUpperBound = timeBiased + 0.1;
+      const yRange = yUpperBound - yLowerBound;
+
+      const toRemove = [];
+      for (let k in cells) {
+        const curCell = cells[k];
+
+        const roll = _N_FadeIn.Get(...curCell.position);
+
+        const yNormalized = (curCell.position[1] + 50.0) / 250.0;
+        const yFactor = (yNormalized - yLowerBound) / yRange;
+        if (roll < yFactor) {
+          toRemove.push(k);
+        }
+      }
+
+      for (let i = 0; i < toRemove.length; ++i) {
+        delete cells[toRemove[i]];
+      }
+    }
+
+    RemoveExteriorVoxels_(cells) {
+      const toRemove = [];
+      const xMin = this.params_.offset.x;
+      const zMin = this.params_.offset.z;
+      const xMax = this.params_.offset.x + this.params_.dimensions.x;
+      const zMax = this.params_.offset.z + this.params_.dimensions.z;
+
+      for (let k in cells) {
+        const cell = cells[k];
+        if (cell.position[0] < xMin || cell.position[0] >= xMax ||
+            cell.position[2] < zMin || cell.position[2] >= zMax) {
+          toRemove.push(k);        
+        }
+      }
+      for (let i = 0; i < toRemove.length; ++i) {
+        delete cells[toRemove[i]];
+      }
+    }
+
+    MergeCustomVoxels_(cells) {
+      const customVoxels = this.params_.customVoxels;
+
+      const toRemove = [];
+      for (let k in customVoxels) {
+        const c = customVoxels[k];
+        if (c.visible) {
+          c.facesHidden = [false, false, false, false, false];
+          c.ao = [null, null, null, null, null, null];
+        } else {
+          toRemove.push(k);
+        }
+      }
+      Object.assign(cells, customVoxels);
+
+      for (let i = 0; i < toRemove.length; ++i) {
+        delete cells[toRemove[i]];
+      }
+    }
+
+    RemoveVoxelAndFill_(pos, voxels) {
+      const kv = this.Key_(...pos);
+
+      const custom = {};
+      custom[kv] = {
+          position: [...pos],
+          visible: false,
+      };
+
+      const [_, groundLevel] = this.GenerateNoise_(pos[0], pos[2]);
+
+      if (pos[1] <= groundLevel) {
+        for (let xi = -1; xi <= 1; xi++) {
+          for (let yi = -1; yi <= 1; yi++) {
+            for (let zi = -1; zi <= 1; zi++) {
+              const xPos = pos[0] + xi;
+              const yPos = pos[1] + yi;
+              const zPos = pos[2] + zi;
+
+              const [voxelType, groundLevelAdjacent] = this.GenerateNoise_(xPos, zPos);
+              const k = this.Key_(xPos, yPos, zPos);
+
+              if (!(k in voxels) && yPos < groundLevelAdjacent) {
+                let type = 'dirt';
+
+                if (voxelType == 'sand') {
+                  type = 'sand';
+                }
+
+                if (yPos < groundLevelAdjacent - 2) {
+                  type = 'stone';
+                }
+
+                if (voxelType == 'moon') {
+                  type = 'moon';
+                }
+
+                custom[k] = {
+                    position: [xPos, yPos, zPos],
+                    type: type,
+                    visible: true,
+                };
+              }
+            }
+          }
+        }
+      }
+      return custom;
+    }
+
+    Rebuild() {
+      // Create terrain
+      const terrainVoxels = this.CreateTerrain_();
+
+      // Create trees and shit
+      // You need to create the SDF's for adjacent voxels, in case they bleed over
+      const sdfs = this.CreateFoliageSDFs_();
+
+      this.ApplySDFsToVoxels_(sdfs, terrainVoxels);
+
+      const oceanVoxels = !hack_defs.skipOceans ? this.CreateOcean_(terrainVoxels) : {};
+
+      this.ApplyFadeIn_(oceanVoxels);
+      this.ApplyFadeIn_(terrainVoxels);
+
+      // Now prune it a bit, these need to be done separately
+      const prunedMeshVoxels = this.PruneHiddenVoxels_(terrainVoxels);
+      const prunedOceanVoxels = this.PruneHiddenVoxels_(oceanVoxels);
+
+      this.BuildAO_(prunedMeshVoxels);
+
+      // By specifying mesh second, it overwrites any potential ocean voxels, which
+      // is what we want.
+      const prunedVoxels = Object.assign({}, prunedOceanVoxels, prunedMeshVoxels);
+
+      // Remove extra
+      this.RemoveExteriorVoxels_(prunedVoxels);
+
+      const data = this.BuildMeshDataFromVoxels_(prunedVoxels);
+
+      // Full set to be retained by block instance.
+      const voxels = Object.assign({}, terrainVoxels, oceanVoxels);
+
+      this.RemoveExteriorVoxels_(voxels);
+
+      for (let k in voxels) {
+        const c = voxels[k];
+        voxels[k] = {
+            type: c.type,
+            position: c.position,
+            visible: c.visible,
+        };
+      }
+
+      data.voxels = voxels;
+
+      return data;
+    }
+
+    PartialRebuild(existingVoxels, neighbouringVoxels) {
+      const voxels = Object.assign({}, existingVoxels, neighbouringVoxels);
+      const toRemove = [];
+      for (let k in voxels) {
+        const c = voxels[k];
+        if (c.visible) {
+          c.facesHidden = [false, false, false, false, false];
+          c.ao = [null, null, null, null, null, null];
+        } else {
+          toRemove.push(k);
+        }
+      }
+
+      for (let i = 0; i < toRemove.length; ++i) {
+        delete voxels[toRemove[i]];
+      }
+
+      const prunedVoxels = this.PruneHiddenVoxels_(voxels);
+
+      this.BuildAO_(prunedVoxels);
+
+      // Remove extra
+      this.RemoveExteriorVoxels_(prunedVoxels);
+
+      const data = this.BuildMeshDataFromVoxels_(prunedVoxels);
+
+      data.voxels = existingVoxels;
+
+      return data;
+    }
+
+    BuildMeshDataFromVoxels_(cells) {
+      const meshes = {};
+
+      meshes.opaque = {
+          positions: [],
+          uvs: [],
+          uvSlices: [], 
+          normals: [],
+          indices: [],
+          colours: [],
+      };
+      meshes.transparent = {
+          positions: [],
+          uvs: [],
+          uvSlices: [], 
+          normals: [],
+          indices: [],
+          colours: [],
+      };
+
+      for (let c in cells) {
+        const curCell = cells[c];
+
+        for (let i = 0; i < 6; ++i) {
+          if (curCell.facesHidden[i]) {
+            continue;
+          }
+
+          const targetData = curCell.type == 'ocean' ? meshes.transparent : meshes.opaque;
+
+          const bi = targetData.positions.length / 3;
+          const localPositions = [...this.geometries_[i].attributes.position.array];
+          for (let j = 0; j < 3; ++j) {
+            for (let v = 0; v < 4; ++v) {
+              localPositions[v * 3 + j] += curCell.position[j];
+            }
+          }
+          targetData.positions.push(...localPositions);
+          targetData.uvs.push(...this.geometries_[i].attributes.uv.array);
+          targetData.normals.push(...this.geometries_[i].attributes.normal.array);
+
+          const luminance = _N_Luminance.Get(...curCell.position) * 0.1 + 0.9;
+          for (let v = 0; v < 4; ++v) {
+            targetData.uvSlices.push(this.params_.blockTypes[curCell.type].textures[i]);
+
+            const colour = new THREE.Color(0xFFFFFF);
+            if (!hack_defs.skipVariableLuminance) {
+              colour.multiplyScalar(luminance);
+            }
+
+            if (curCell.ao[i]) {
+              colour.multiplyScalar(curCell.ao[i][v]);
+            }
+
+            colour.convertSRGBToLinear();
+      
+            targetData.colours.push(colour.r, colour.g, colour.b);
+          }
+
+          const localIndices = [...this.geometries_[i].index.array];
+          for (let j = 0; j < localIndices.length; ++j) {
+            localIndices[j] += bi;
+          }
+          targetData.indices.push(...localIndices);
+        }
+      }
+
+      const bytesInFloat32 = 4;
+      const bytesInInt32 = 4;
+
+      const data = {};
+
+      for (let k in meshes) {
+        const positionsArray = new Float32Array(
+            new SharedArrayBuffer(bytesInFloat32 * meshes[k].positions.length));
+        const normalsArray = new Float32Array(
+            new SharedArrayBuffer(bytesInFloat32 * meshes[k].normals.length));
+        const uvsArray = new Float32Array(
+            new SharedArrayBuffer(bytesInFloat32 * meshes[k].uvs.length));
+        const uvSlicesArray = new Float32Array(
+            new SharedArrayBuffer(bytesInFloat32 * meshes[k].uvSlices.length));
+        const coloursArray = new Float32Array(
+            new SharedArrayBuffer(bytesInFloat32 * meshes[k].colours.length));
+        const indicesArray = new Uint32Array(
+            new SharedArrayBuffer(bytesInInt32 * meshes[k].indices.length));
+
+        positionsArray.set(meshes[k].positions, 0);
+        normalsArray.set(meshes[k].normals, 0);
+        uvsArray.set(meshes[k].uvs, 0);
+        uvSlicesArray.set(meshes[k].uvSlices, 0);
+        coloursArray.set(meshes[k].colours, 0);
+        indicesArray.set(meshes[k].indices, 0);
+
+        data[k] = {
+            positions: positionsArray,
+            uvs: uvsArray,
+            uvSlices: uvSlicesArray,
+            normals: normalsArray,
+            colours: coloursArray,
+            indices: indicesArray,
+        };
+      }
+
+      data.buildId = this.params_.buildId;
+
+      return data;
+    }
+  };
+
+  return {
+      VoxelBlockBuilder: _VoxelBuilderThreadedWorker,
+  };
+})();

+ 16 - 0
src/voxel-builder-threaded-worker.js

@@ -0,0 +1,16 @@
+
+import {voxel_block_builder} from './voxel-block-builder.js';
+
+
+const _BLOCK = new voxel_block_builder.VoxelBlockBuilder();
+
+self.onmessage = (msg) => {
+  if (msg.data.subject == 'build_chunk') {
+    _BLOCK.Init(msg.data.params);
+
+    const rebuiltData = _BLOCK.Rebuild();
+    self.postMessage({subject: 'build_chunk_result', data: rebuiltData});
+  }
+}
+
+export const voxel_builder_threaded_worker = (() => {})();

+ 368 - 0
src/voxel-builder-threaded.js

@@ -0,0 +1,368 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {worker_pool} from './worker-pool.js';
+
+import {voxel_block_builder} from './voxel-block-builder.js';
+import {hack_defs} from './hack-defs.js';
+
+
+export const voxel_builder_threaded = (() => {
+
+  
+  class SparseVoxelCellBlock {
+    constructor(params) {
+      this.params_ = params;
+      this.voxels_ = {};
+      this.group_ = new THREE.Group();
+      this.buildId_ = 0;
+      this.lastBuiltId_ = -1;
+      this.building_ = false;
+      this.dirty_ = false;
+
+      // Use this for emergency rebuilds when voxels inserted/deleted.
+      this.builder_ = new voxel_block_builder.VoxelBlockBuilder();
+      params.scene.add(this.group_);
+    }
+
+    Destroy() {
+      this.ReleaseAssets_();
+      this.group_.parent.remove(this.group_);
+    }
+
+    ReleaseAssets_() {
+      this.group_.traverse(c => {
+        if (c.material) {
+          c.material.dispose();
+        }
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      if (this.opaqueMesh_) {
+        this.group_.remove(this.opaqueMesh_);
+      }
+      if (this.transparentMesh_) {
+        this.group_.remove(this.transparentMesh_);
+      }
+    }
+
+    Show() {
+      this.group_.visible = true;;
+    }
+
+    Hide() {
+      this.group_.visible = false;;
+    }
+
+    get Destroyed() {
+      return !this.group_.parent;
+    }
+
+    get Dirty() {
+      return this.dirty_;
+    }
+
+    Key_(x, y, z) {
+      return x + '.' + y + '.' + z;
+    }
+
+    InsertVoxelAt(pos, type, skippable) {
+      const k = this.Key_(...pos);
+      if (k in this.voxels_ && skippable) {
+        return;
+      }
+
+      const v = {
+          position: [...pos],
+          type: type,
+          visible: true,
+      };
+
+      this.voxels_[k] = v;
+      this.buildId_++;
+      this.dirty_ = true;
+
+      // Get nearby voxels
+      const neighbours = this.params_.parent.GetAdjacentBlocks(
+          this.params_.offset.x, this.params_.offset.z);
+  
+      for (let xi = -1; xi <= 1; ++xi) {
+        for (let yi = -1; yi <= 1; ++yi) {
+          for (let zi = -1; zi <= 1; ++zi) {
+            for (let ni = 0; ni < neighbours.length; ++ni) {
+              const k = this.Key_(pos[0] + xi, pos[1] + yi, pos[2] + zi);
+
+              if (k in neighbours[ni].voxels_) {
+                // Force the neighbour to rebuild since we shared some voxels
+                neighbours[ni].buildId_++;
+                neighbours[ni].dirty_ = true;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    RemoveVoxelAt(pos) {
+      this.buildId_++;
+      this.dirty_ = true;
+
+      const params = {
+        buildId: this.buildId_,
+        offset: this.params_.offset.toArray(),
+        dimensions: this.params_.dimensions.toArray(),
+        blockTypes: this.params_.blockTypes,
+        currentTime: 0.0,
+      };
+
+      this.builder_.Init(params);
+
+      // Only write fill voxels if needed
+      const kv = this.Key_(...pos);
+      this.voxels_[kv].visible = false;
+      const fillVoxels = this.builder_.RemoveVoxelAndFill_(pos, this.voxels_);
+      for (let k in fillVoxels) {
+        this.params_.parent.InsertVoxelAt(
+            fillVoxels[k].position, fillVoxels[k].type, true);
+      }
+
+      const neighbours = this.params_.parent.GetAdjacentBlocks(
+        this.params_.offset.x, this.params_.offset.z);
+
+      for (let xi = -1; xi <= 1; ++xi) {
+        for (let yi = -1; yi <= 1; ++yi) {
+          for (let zi = -1; zi <= 1; ++zi) {
+            for (let ni = 0; ni < neighbours.length; ++ni) {
+              const k = this.Key_(pos[0] + xi, pos[1] + yi, pos[2] + zi);
+
+              if (k in neighbours[ni].voxels_) {
+                // Force the neighbour to rebuild since we shared some voxels
+                neighbours[ni].buildId_++;
+                neighbours[ni].dirty_ = true;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    PartialRebuild() {
+      const neighbours = this.params_.parent.GetAdjacentBlocks(
+          this.params_.offset.x, this.params_.offset.z);
+
+      const neighbourVoxels = {};
+      const xn = this.params_.offset.x - 1;
+      const zn = this.params_.offset.z - 1;
+      const xp = this.params_.offset.x + this.params_.dimensions.x;
+      const zp = this.params_.offset.z + this.params_.dimensions.z;
+      for (let ni = 0; ni < neighbours.length; ++ni) {
+        const neighbour = neighbours[ni];
+        for (let k in neighbour.voxels_) {
+          const v = neighbour.voxels_[k];
+          if (v.position[0] == xn || v.position[0] == xp ||
+              v.position[2] == zn || v.position[2] == zp) {
+            neighbourVoxels[k] = v;
+          }
+        }
+      }
+
+      const params = {
+        buildId: this.buildId_,
+        offset: this.params_.offset.toArray(),
+        dimensions: this.params_.dimensions.toArray(),
+        blockTypes: this.params_.blockTypes,
+        currentTime: 0.0,
+      };
+
+      this.builder_.Init(params);
+      const data = this.builder_.PartialRebuild(this.voxels_, neighbourVoxels);
+
+      this.RebuildMeshFromData(data);
+
+      this.dirty_ = false;
+    }
+
+    HasVoxelAt(x, y, z) {
+      const k = this.Key_(x, y, z);
+      if (!(k in this.voxels_)) {
+        return false;
+      }
+
+      return this.voxels_[k].visible;
+    }
+
+    FindVoxelsNear(pos, radius) {
+      const xp = Math.ceil(pos.x + (radius + 1));
+      const yp = Math.ceil(pos.y + (radius + 1));
+      const zp = Math.ceil(pos.z + (radius + 1));
+      const xn = Math.floor(pos.x - (radius + 1));
+      const yn = Math.floor(pos.y - (radius + 1));
+      const zn = Math.floor(pos.z - (radius + 1));
+
+      const voxels = [];
+      for (let xi = xn; xi <= xp; ++xi) {
+        for (let yi = yn; yi <= yp; ++yi) {
+          for (let zi = zn; zi <= zp; ++zi) {
+            const k = this.Key_(xi, yi, zi);
+            if (k in this.voxels_) {
+              if (this.voxels_[k].visible) {
+                voxels.push(this.voxels_[k]);
+              }
+            }
+          }
+        }
+      }
+      return voxels;
+    }
+
+    BuildGeometry_(data, mat) {
+      const geo = new THREE.BufferGeometry();
+      const mesh = new THREE.Mesh(geo, mat);
+      mesh.castShadow = false;
+      mesh.receiveShadow = true;
+
+      geo.setAttribute(
+          'position', new THREE.Float32BufferAttribute(data.positions, 3));
+      geo.setAttribute(
+          'normal', new THREE.Float32BufferAttribute(data.normals, 3));
+      geo.setAttribute(
+          'uv', new THREE.Float32BufferAttribute(data.uvs, 2));
+      geo.setAttribute(
+          'uvSlice', new THREE.Float32BufferAttribute(data.uvSlices, 1));
+      geo.setAttribute(
+          'colour', new THREE.Float32BufferAttribute(data.colours, 3));
+      geo.setIndex(
+          new THREE.BufferAttribute(data.indices, 1));
+      geo.attributes.position.needsUpdate = true;
+      geo.attributes.normal.needsUpdate = true;
+      geo.attributes.uv.needsUpdate = true;
+      geo.attributes.colour.needsUpdate = true;
+
+      geo.computeBoundingBox();
+      geo.computeBoundingSphere();
+
+      return mesh;
+    }
+
+    RebuildMeshFromData(data) {
+      this.ReleaseAssets_();
+
+      if (data.opaque.positions.length > 0) {
+        this.opaqueMesh_ = this.BuildGeometry_(
+            data.opaque, this.params_.materialOpaque);
+        this.group_.add(this.opaqueMesh_);
+      }
+      if (data.transparent.positions.length > 0) {
+        this.transparentMesh_ = this.BuildGeometry_(
+            data.transparent, this.params_.materialTransparent);
+        this.group_.add(this.transparentMesh_);
+      }
+
+      this.voxels_ = data.voxels;
+      this.lastBuiltId_ = data.buildId;
+    }
+  };
+
+  const _NUM_WORKERS = 7;
+
+  class VoxelBuilder_Threaded {
+    constructor(params) {
+      this.old_ = [];
+      this.blocks_ = [];
+
+      this.workerPool_ = new worker_pool.WorkerPool(
+          _NUM_WORKERS, 'src/voxel-builder-threaded-worker.js');
+  
+      this.params_ = params;
+      this.currentTime_ = 0.01;
+    }
+
+    OnResult_(block, msg) {
+      if (msg.subject == 'build_chunk_result') {
+        block.RebuildMeshFromData(msg.data);
+        block.Show();
+      }
+    }
+
+    AllocateBlock(params) {
+      const blockParams = {...this.params_, ...params};
+      const block = new SparseVoxelCellBlock(blockParams);
+
+      block.Hide();
+
+      this.blocks_.push(block);
+
+      this.RebuildBlock_(block);
+
+      return block;    
+    }
+
+    RebuildBlock_(block) {
+      if (block.building_) {
+        return;
+      }
+
+      const msg = {
+        subject: 'build_chunk',
+        params: {
+          buildId: block.buildId_,
+          offset: block.params_.offset.toArray(),
+          dimensions: this.params_.dimensions.toArray(),
+          blockTypes: this.params_.blockTypes,
+          currentTime: this.currentTime_,
+        },
+      };
+
+      // HACK
+      block.building_ = true;
+
+      this.workerPool_.Enqueue(msg, (m) => {
+        block.building_ = false;
+        this.OnResult_(block, m);
+      });
+    }
+
+    ScheduleDestroy(blocks) {
+      this.old_.push(...blocks);
+    }
+
+    DestroyBlocks_(blocks) {
+      for (let c of blocks) {
+        c.Destroy();
+      }
+    }
+
+    get Busy() {
+      return this.workerPool_.Busy;
+    }
+
+    Update(timeElapsed) {
+      if (!this.Busy) {
+        this.DestroyBlocks_(this.old_);
+        this.old_ = [];
+      }
+
+      this.blocks_ = this.blocks_.filter(b => !b.Destroyed);
+
+      for (let i = 0; i < this.blocks_.length; ++i) {
+        if (hack_defs.introEnabled) {
+          this.RebuildBlock_(this.blocks_[i]);
+        }
+
+        if (this.blocks_[i].Dirty) {
+          this.blocks_[i].PartialRebuild();
+        }
+      }
+
+      if (hack_defs.introEnabled) {
+        this.currentTime_ += timeElapsed * hack_defs.INTRO_RATE;
+      } else {
+        this.currentTime_ = 2;
+      }
+    }
+  }
+
+  return {
+      VoxelBuilder_Threaded: VoxelBuilder_Threaded,
+  };
+})();

+ 651 - 0
src/voxel-shader.js

@@ -0,0 +1,651 @@
+
+export const voxel_shader = (() => {
+
+  const _PLACEMENT_VS = `
+  precision mediump float;
+  
+  // Attributes, declared by three.js
+  // attribute vec3 position;
+  // attribute vec3 normal;
+  // attribute vec2 uv;
+  
+  // Outputs
+  varying vec2 vUV;
+    
+  void main(){
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+    vUV = uv;
+  }
+    `;
+    
+    const _PLACEMENT_PS = `
+  precision mediump float;
+  precision mediump sampler2DArray;
+  
+  uniform float time;
+  uniform vec3 edgeColour;
+  
+  #define saturate(a) clamp( a, 0.0, 1.0 )
+  
+  varying vec2 vUV;
+
+  float sdf_Box(vec2 coords, vec2 bounds) {
+    vec2 dist = abs(coords) - bounds;
+    return length(max(dist, 0.0)) + min(max(dist.x, dist.y), 0.0);
+  }
+
+  float smootherstep(float a, float b, float x) {
+    x = clamp((x - a) / (b - a), 0.0, 1.0);
+    return x * x * x * (x * ( x * 6.0 - 15.0) + 10.0);
+  }
+
+  void main() {
+    float d = sdf_Box(vUV - 0.5, vec2(0.5));
+
+    float s = smoothstep(0.0, 0.1, abs(d));
+    float edgeColouring = mix(0.0, 1.0, 1.0 - s);
+  
+    float blink = clamp(sin(time * 10.0), 0.0, 1.0) * 0.1 + 0.9;
+    gl_FragColor = vec4(edgeColour, edgeColouring * blink);
+  }
+    `;
+
+    const _SUN_VS = `
+precision mediump float;
+
+// Attributes, declared by three.js
+// attribute vec3 position;
+// attribute vec3 normal;
+// attribute vec2 uv;
+
+// Outputs
+varying vec3 vWorldPosition;
+varying vec2 vUV;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+void main(){
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+
+  vUV = uv;
+}
+  `;
+  
+  const _SUN_PS = `
+precision mediump float;
+precision mediump sampler2DArray;
+
+varying vec3 vWorldPosition;
+varying vec2 vUV;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+float sdCircle( vec2 p, float r )
+{
+    return length(p) - r;
+}
+
+float sdf_Box(vec2 coords, vec2 bounds) {
+  vec2 dist = abs(coords) - bounds;
+  return length(max(dist, 0.0)) + min(max(dist.x, dist.y), 0.0);
+}
+
+float smootherstep(float a, float b, float x) {
+  x = clamp((x - a) / (b - a), 0.0, 1.0);
+  return x * x * x * (x * ( x * 6.0 - 15.0) + 10.0);
+}
+
+float hash1( vec2 p )
+{
+    p  = 50.0*fract( p*0.3183099 );
+    return fract( p.x*p.y*(p.x+p.y) );
+}
+
+float noise( in vec2 x )
+{
+    vec2 p = floor(x);
+    vec2 w = fract(x);
+    vec2 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+
+    float a = hash1(p+vec2(0,0));
+    float b = hash1(p+vec2(1,0));
+    float c = hash1(p+vec2(0,1));
+    float d = hash1(p+vec2(1,1));
+    
+    return -1.0+2.0*( a + (b-a)*u.x + (c-a)*u.y + (a - b - c + d)*u.x*u.y );
+}
+
+void main() {
+  vec2 uvStepped = floor(vUV * 32.0) / 32.0;
+  float sunRadius = 0.125;
+  //float d = 1.0 - clamp(sdCircle(uvStepped - 0.5, sunRadius) / (0.5 - sunRadius), 0.0, 1.0);
+  float d = 1.0 - clamp(sdf_Box(uvStepped - 0.5, vec2(sunRadius)) / (0.5 - sunRadius), 0.0, 1.0);
+
+  float noiseValue = noise(uvStepped * 32.0) * 0.1 + 0.9;
+
+  vec4 core = sRGBToLinear(vec4(0.96, 0.9, 0.7, 1.0));
+  vec4 corona = sRGBToLinear(vec4(0.5, 0.2, 0.05, 1.0)) * noiseValue;
+  vec4 sunColour = mix(corona, core, d * d);
+  gl_FragColor = vec4(sunColour.xyz, d);
+}
+  `;
+
+  const _CLOUD_VS = `
+precision mediump float;
+
+// Attributes, declared by three.js
+// attribute vec3 position;
+// attribute vec3 normal;
+// attribute vec2 uv;
+
+// Outputs
+varying vec3 vWorldPosition;
+varying vec3 vNormal;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+void main(){
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+
+  vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
+  vNormal = normal;
+}
+  `;
+  
+  const _CLOUD_PS = `
+precision mediump float;
+precision mediump sampler2DArray;
+
+varying vec3 vWorldPosition;
+varying vec3 vNormal;
+
+uniform vec3 cloudMin;
+uniform vec3 cloudMax;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+// https://gist.github.com/DomNomNom/46bb1ce47f68d255fd5d
+float GetAABBDepth(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) {
+  vec3 tMin = (boxMin - rayOrigin) / rayDir;
+  vec3 tMax = (boxMax - rayOrigin) / rayDir;
+  vec3 t1 = min(tMin, tMax);
+  vec3 t2 = max(tMin, tMax);
+  float tNear = max(max(t1.x, t1.y), t1.z);
+  float tFar = min(min(t2.x, t2.y), t2.z);
+
+  return (tFar - tNear);
+}
+
+
+vec4 GetCloudShading(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) {
+  float depth = GetAABBDepth(rayOrigin, rayDir, boxMin, boxMax);
+
+  float cloudDensity = 0.01;
+  float opacity = 1.0 - exp(-cloudDensity * cloudDensity * depth * depth);
+  vec3 sunDir = normalize(vec3(-1.0, -4.0, 0.0));
+
+  return vec4(vec3(saturate(dot(sunDir, vNormal)) + 0.75), opacity);
+}
+
+float sdBox( vec3 p, vec3 b, float r ) {
+    vec3 d = abs(p) - b;
+    return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0)) - r;
+}
+
+float map( in vec3 pos )
+{
+  return sdBox(pos, (cloudMax - cloudMin) * 0.25, 0.0);
+}
+
+// http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm
+vec3 calcNormal( in vec3 pos )
+{
+    // inspired by tdhooper and klems - a way to prevent the compiler from inlining map() 4 times
+    vec3 n = vec3(0.0);
+    for( int i=0; i<4; i++ )
+    {
+        vec3 e = 0.5773*(2.0*vec3((((i+3)>>1)&1),((i>>1)&1),(i&1))-1.0);
+        n += e*map(pos+0.0005*e);
+    }
+    return normalize(n);
+}
+
+void main() {
+  vec3 fixedPosition = (cloudMax + cloudMin) * 0.5;
+  
+  vec3 viewDirection = normalize(vWorldPosition - cameraPosition);
+  vec4 cloudColour = GetCloudShading(cameraPosition, viewDirection, cloudMin, cloudMax);
+
+  gl_FragColor = cloudColour;
+}
+  `;
+
+  const _VOXEL_VS = `
+precision mediump float;
+
+// Attributes, declared by three.js
+// attribute vec3 position;
+// attribute vec3 normal;
+// attribute vec2 uv;
+attribute vec3 colour;
+attribute float uvSlice;
+
+// Outputs
+varying vec3 vNormal;
+varying vec3 vColour;
+varying vec3 vWorldPosition;
+varying vec3 vUV;
+
+uniform float fogTime;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+void main(){
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+
+  vNormal = normal;
+  vColour = colour;
+  vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
+  vUV = vec3(uv, uvSlice);
+}
+  `;
+  
+  const _VOXEL_PS = `
+precision mediump float;
+precision mediump sampler2DArray;
+
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+//==========================================================================================
+// hashes
+//==========================================================================================
+
+float hash1( vec2 p )
+{
+    p  = 50.0*fract( p*0.3183099 );
+    return fract( p.x*p.y*(p.x+p.y) );
+}
+
+float hash1( float n )
+{
+    return fract( n*17.0*fract( n*0.3183099 ) );
+}
+
+vec2 hash2( float n ) { return fract(sin(vec2(n,n+1.0))*vec2(43758.5453123,22578.1459123)); }
+
+
+vec2 hash2( vec2 p ) 
+{
+    const vec2 k = vec2( 0.3183099, 0.3678794 );
+    p = p*k + k.yx;
+    return fract( 16.0 * k*fract( p.x*p.y*(p.x+p.y)) );
+}
+
+//==========================================================================================
+// noises
+//==========================================================================================
+
+// value noise, and its analytical derivatives
+vec4 noised( in vec3 x )
+{
+    vec3 p = floor(x);
+    vec3 w = fract(x);
+    
+    vec3 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+    vec3 du = 30.0*w*w*(w*(w-2.0)+1.0);
+
+    float n = p.x + 317.0*p.y + 157.0*p.z;
+    
+    float a = hash1(n+0.0);
+    float b = hash1(n+1.0);
+    float c = hash1(n+317.0);
+    float d = hash1(n+318.0);
+    float e = hash1(n+157.0);
+	float f = hash1(n+158.0);
+    float g = hash1(n+474.0);
+    float h = hash1(n+475.0);
+
+    float k0 =   a;
+    float k1 =   b - a;
+    float k2 =   c - a;
+    float k3 =   e - a;
+    float k4 =   a - b - c + d;
+    float k5 =   a - c - e + g;
+    float k6 =   a - b - e + f;
+    float k7 = - a + b + c - d + e - f - g + h;
+
+    return vec4( -1.0+2.0*(k0 + k1*u.x + k2*u.y + k3*u.z + k4*u.x*u.y + k5*u.y*u.z + k6*u.z*u.x + k7*u.x*u.y*u.z), 
+                      2.0* du * vec3( k1 + k4*u.y + k6*u.z + k7*u.y*u.z,
+                                      k2 + k5*u.z + k4*u.x + k7*u.z*u.x,
+                                      k3 + k6*u.x + k5*u.y + k7*u.x*u.y ) );
+}
+
+float noise( in vec3 x )
+{
+    vec3 p = floor(x);
+    vec3 w = fract(x);
+    
+    vec3 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+    
+    float n = p.x + 317.0*p.y + 157.0*p.z;
+    
+    float a = hash1(n+0.0);
+    float b = hash1(n+1.0);
+    float c = hash1(n+317.0);
+    float d = hash1(n+318.0);
+    float e = hash1(n+157.0);
+	  float f = hash1(n+158.0);
+    float g = hash1(n+474.0);
+    float h = hash1(n+475.0);
+
+    float k0 =   a;
+    float k1 =   b - a;
+    float k2 =   c - a;
+    float k3 =   e - a;
+    float k4 =   a - b - c + d;
+    float k5 =   a - c - e + g;
+    float k6 =   a - b - e + f;
+    float k7 = - a + b + c - d + e - f - g + h;
+
+    return -1.0+2.0*(k0 + k1*u.x + k2*u.y + k3*u.z + k4*u.x*u.y + k5*u.y*u.z + k6*u.z*u.x + k7*u.x*u.y*u.z);
+}
+
+vec3 noised( in vec2 x )
+{
+    vec2 p = floor(x);
+    vec2 w = fract(x);
+    
+    vec2 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+    vec2 du = 30.0*w*w*(w*(w-2.0)+1.0);
+    
+    float a = hash1(p+vec2(0,0));
+    float b = hash1(p+vec2(1,0));
+    float c = hash1(p+vec2(0,1));
+    float d = hash1(p+vec2(1,1));
+
+    float k0 = a;
+    float k1 = b - a;
+    float k2 = c - a;
+    float k4 = a - b - c + d;
+
+    return vec3( -1.0+2.0*(k0 + k1*u.x + k2*u.y + k4*u.x*u.y), 
+                      2.0* du * vec2( k1 + k4*u.y,
+                                      k2 + k4*u.x ) );
+}
+
+float noise( in vec2 x )
+{
+    vec2 p = floor(x);
+    vec2 w = fract(x);
+    vec2 u = w*w*w*(w*(w*6.0-15.0)+10.0);
+    
+#if 0
+    p *= 0.3183099;
+    float kx0 = 50.0*fract( p.x );
+    float kx1 = 50.0*fract( p.x+0.3183099 );
+    float ky0 = 50.0*fract( p.y );
+    float ky1 = 50.0*fract( p.y+0.3183099 );
+
+    float a = fract( kx0*ky0*(kx0+ky0) );
+    float b = fract( kx1*ky0*(kx1+ky0) );
+    float c = fract( kx0*ky1*(kx0+ky1) );
+    float d = fract( kx1*ky1*(kx1+ky1) );
+#else
+    float a = hash1(p+vec2(0,0));
+    float b = hash1(p+vec2(1,0));
+    float c = hash1(p+vec2(0,1));
+    float d = hash1(p+vec2(1,1));
+#endif
+    
+    return -1.0+2.0*( a + (b-a)*u.x + (c-a)*u.y + (a - b - c + d)*u.x*u.y );
+}
+
+//==========================================================================================
+// fbm constructions
+//==========================================================================================
+
+const mat3 m3  = mat3( 0.00,  0.80,  0.60,
+                      -0.80,  0.36, -0.48,
+                      -0.60, -0.48,  0.64 );
+const mat3 m3i = mat3( 0.00, -0.80, -0.60,
+                       0.80,  0.36, -0.48,
+                       0.60, -0.48,  0.64 );
+const mat2 m2 = mat2(  0.80,  0.60,
+                      -0.60,  0.80 );
+const mat2 m2i = mat2( 0.80, -0.60,
+                       0.60,  0.80 );
+
+//------------------------------------------------------------------------------------------
+
+float fbm_4( in vec3 x )
+{
+    float f = 2.0;
+    float s = 0.5;
+    float a = 0.0;
+    float b = 0.5;
+    for( int i=0; i<4; i++ )
+    {
+        float n = noise(x);
+        a += b*n;
+        b *= s;
+        x = f*m3*x;
+    }
+	return a;
+}
+
+vec4 fbmd_8( in vec3 x )
+{
+    float f = 1.92;
+    float s = 0.5;
+    float a = 0.0;
+    float b = 0.5;
+    vec3  d = vec3(0.0);
+    mat3  m = mat3(1.0,0.0,0.0,
+                   0.0,1.0,0.0,
+                   0.0,0.0,1.0);
+    for( int i=0; i<7; i++ )
+    {
+        vec4 n = noised(x);
+        a += b*n.x;          // accumulate values		
+        d += b*m*n.yzw;      // accumulate derivatives
+        b *= s;
+        x = f*m3*x;
+        m = f*m3i*m;
+    }
+	return vec4( a, d );
+}
+
+vec4 fbmd_4( in vec3 x )
+{
+    float f = 1.92;
+    float s = 0.5;
+    float a = 0.0;
+    float b = 0.5;
+    vec3  d = vec3(0.0);
+    mat3  m = mat3(1.0,0.0,0.0,
+                   0.0,1.0,0.0,
+                   0.0,0.0,1.0);
+    for( int i=0; i<4; i++ )
+    {
+        vec4 n = noised(x);
+        a += b*n.x;          // accumulate values		
+        d += b*m*n.yzw;      // accumulate derivatives
+        b *= s;
+        x = f*m3*x;
+        m = f*m3i*m;
+    }
+	return vec4( a, d );
+}
+
+float fbm_9( in vec2 x )
+{
+    float f = 1.9;
+    float s = 0.55;
+    float a = 0.0;
+    float b = 0.5;
+    for( int i=0; i<9; i++ )
+    {
+        float n = noise(x);
+        a += b*n;
+        b *= s;
+        x = f*m2*x;
+    }
+	return a;
+}
+
+vec3 fbmd_9( in vec2 x )
+{
+    float f = 1.9;
+    float s = 0.55;
+    float a = 0.0;
+    float b = 0.5;
+    vec2  d = vec2(0.0);
+    mat2  m = mat2(1.0,0.0,0.0,1.0);
+    for( int i=0; i<9; i++ )
+    {
+        vec3 n = noised(x);
+        a += b*n.x;          // accumulate values		
+        d += b*m*n.yz;       // accumulate derivatives
+        b *= s;
+        x = f*m2*x;
+        m = f*m2i*m;
+    }
+	return vec3( a, d );
+}
+
+float fbm_4( in vec2 x )
+{
+    float f = 1.9;
+    float s = 0.55;
+    float a = 0.0;
+    float b = 0.5;
+    for( int i=0; i<4; i++ )
+    {
+        float n = noise(x);
+        a += b*n;
+        b *= s;
+        x = f*m2*x;
+    }
+	return a;
+}
+
+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);
+}
+
+float smootherstep(float a, float b, float x) {
+  x = x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+  return x * (b - a) + a;
+}
+
+uniform vec3 fogColour;
+uniform float fogDensity;
+uniform vec2 fogRange;
+uniform float fogTime;
+uniform float fade;
+uniform float flow;
+
+uniform sampler2DArray diffuseMap;
+uniform sampler2D noiseMap;
+
+varying vec3 vUV;
+varying vec3 vNormal;
+varying vec3 vColour;
+varying vec3 vWorldPosition;
+
+vec4 _FogWithHeight() {
+  vec3 fogOrigin = cameraPosition;
+  vec3 fogDirection = normalize(vWorldPosition - fogOrigin);
+  float fogDepth = distance(vWorldPosition, fogOrigin);
+
+  // vec3 noiseSampleCoord = vWorldPosition * 0.05 + vec3(0, fogTime * 0.5, 0);
+  // float noiseSample = fbm_4(noiseSampleCoord + fbm_4(noiseSampleCoord)) * 0.5 + 0.5;
+  // fogDepth *= mix(noiseSample, 1.0, saturate((fogDepth - 250.0) / 500.0));
+  fogDepth *= fogDepth;
+
+  float heightFactor = 0.025;
+  float fogFactor = heightFactor * exp(-fogOrigin.y * fogDensity) * (
+      1.0 - exp(-fogDepth * fogDirection.y * fogDensity)) / fogDirection.y;
+  fogFactor = saturate(fogFactor);
+
+  return vec4(fogColour, fogFactor);
+}
+
+
+vec4 _Fog() {
+  vec3 fogOrigin = cameraPosition;
+  vec3 fogDirection = normalize(vWorldPosition - fogOrigin);
+  float fogDepth = distance(vWorldPosition, fogOrigin);
+
+  float fogFactor = saturate((fogDepth - fogRange.x) / fogRange.y);
+
+  vec3 noiseSampleCoord = vWorldPosition * 0.05 + vec3(0, fogTime * 0.5, 0);
+  float noiseSample = fbm_4(noiseSampleCoord + fbm_4(noiseSampleCoord)) * 0.5 + 0.5;
+  float noiseDropoff = saturate((fogDepth - fogRange.x) / fogRange.y);
+  noiseDropoff *= noiseDropoff;
+  noiseDropoff = mix(noiseSample, 1.0, noiseDropoff);
+
+  fogFactor *= noiseDropoff;
+
+  vec3 fogColourWithNoise = fogColour * noiseDropoff;
+
+  return vec4(fogColourWithNoise, smootherstep(0.0, 1.0, fogFactor));
+}
+
+void main() {
+  vec4 diffuse = sRGBToLinear(texture2D(diffuseMap, vUV));
+
+  vec3 hemiLight1 = vec3(1.0, 1.0, 1.0);
+  vec3 hemiLight2 = vec3(0.5, 0.1, 0.5);
+  vec3 sunLightDir = normalize(vec3(0.1, 1.0, 0.0));
+  vec3 lighting = saturate(dot(vNormal, sunLightDir)) * 0.25 + vColour * 1.0;
+  vec4 outColour = vec4(diffuse.xyz * lighting, 0.75 * fade);
+
+  vec3 noiseDir = abs(vNormal);
+  vec2 noiseCoords = (
+      noiseDir.x * vWorldPosition.yz +
+      noiseDir.y * vWorldPosition.xz +
+      noiseDir.z * vWorldPosition.xy);
+
+  vec4 noisePixel = texture2D(noiseMap, noiseCoords / 64.0) * 0.2 + 0.8;
+  outColour.xyz *= noisePixel.xyz;
+
+  vec4 fog = _FogWithHeight();
+  outColour.xyz = mix(outColour.xyz, fog.xyz, fog.w);
+
+  gl_FragColor = outColour;
+}
+  `;
+  
+    return {
+        VOXEL: {
+            VS: _VOXEL_VS,
+            PS: _VOXEL_PS,
+        },
+        CLOUD: {
+            VS: _CLOUD_VS,
+            PS: _CLOUD_PS,
+        },
+        SUN: {
+            VS: _SUN_VS,
+            PS: _SUN_PS,
+        },
+        PLACEMENT: {
+            VS: _PLACEMENT_VS,
+            PS: _PLACEMENT_PS,
+        },
+    };
+  })();
+  

+ 459 - 0
src/voxel-tools.js

@@ -0,0 +1,459 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {GLTFLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
+
+import {entity} from './entity.js';
+import {voxel_shader} from './voxel-shader.js';
+import {hack_defs} from './hack-defs.js';
+
+
+export const voxel_tools = (() => {
+
+  class VoxelTools_Insert extends entity.Component {
+    constructor() {
+      super();
+
+      this.voxelType_ = 'stone';
+      this.timer_ = 0;
+      this.active_ = false;
+    }
+
+    InitComponent() {
+      this._RegisterHandler('input.pressed', (m) => this.OnInput_(m));
+      this._RegisterHandler('ui.blockChanged', (m) => this.OnBlockIcon_(m));
+      this._RegisterHandler('ui.toolChanged', (m) => this.OnToolChanged_(m));
+    }
+
+    OnToolChanged_(msg) {
+      if (!hack_defs.showTools) {
+        return;
+      }
+
+      if (msg.value != 'build') {
+        this.LoseFocus();
+      } else {
+        this.GainFocus();
+      }
+    }
+
+    LoseFocus() {
+      this.voxelMeshGroup_.visible = false;
+      this.placementMesh_.visible = false;
+      this.active_ = false;
+    }
+
+    GainFocus() {
+      this.voxelMeshGroup_.visible = true;
+      this.placementMesh_.visible = true;
+      this.active_ = true;
+    }
+
+    OnBlockIcon_(msg) {
+      this.voxelType_ = msg.value;
+      this.UpdateVoxelMesh_();
+    }
+
+    UpdateVoxelMesh_() {
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+
+      const colours = [];
+      const uvSlices = [];
+      for (let f = 0; f < 6; ++f) {
+        for (let i = 0; i < 4 * 3; ++i) {
+          colours.push(1.0, 1.0, 1.0);
+          uvSlices.push(voxels.blockTypes_[this.voxelType_].textures[2]);
+        }
+      }
+      this.voxelMesh_.geometry.setAttribute(
+        'colour', new THREE.Float32BufferAttribute(colours, 3));
+      this.voxelMesh_.geometry.setAttribute(
+        'uvSlice', new THREE.Float32BufferAttribute(uvSlices, 1));
+    }
+
+    InitEntity() {
+      // HACK
+      const scene = this.FindEntity('renderer').GetComponent('ThreeJSController').scene_;
+      const camera = this.FindEntity('renderer').GetComponent('ThreeJSController').uiCamera_;
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+
+      const geo = new THREE.BoxBufferGeometry(1, 1, 1);
+
+      const p1 = new THREE.ShaderMaterial({
+          uniforms: {
+              time: {value: 0.0},
+              edgeColour: { value: new THREE.Color(0x000000) },
+          },
+          vertexShader: voxel_shader.PLACEMENT.VS,
+          fragmentShader: voxel_shader.PLACEMENT.PS,
+          side: THREE.FrontSide,
+          blending: THREE.NormalBlending,
+          transparent: true,
+          depthWrite: false,
+      });
+      const p2 = p1.clone();
+      p2.side = THREE.BackSide;
+
+      const m1 = new THREE.Mesh(geo, p1);
+      const m2 = new THREE.Mesh(geo, p2);
+      m1.renderOrder = 1;
+      this.placementMesh_ = new THREE.Group();
+      this.placementMesh_.add(m1);
+      this.placementMesh_.add(m2);
+      this.placementMesh_.scale.setScalar(0.999);
+      this.material1_ = p1;
+      this.material2_ = p2;
+
+      const voxelGeo = new THREE.BoxBufferGeometry(1, 1, 1);
+
+      this.voxelMesh_ = new THREE.Mesh(voxelGeo, voxels.materialOpaque_.clone());
+      this.voxelMesh_.position.set(1.25, -1.25, -4);
+      this.voxelMesh_.rotateY(0.125 * 2 * Math.PI);
+      this.voxelMesh_.material.depthWrite = false;
+      this.voxelMesh_.material.depthTest = false;
+
+      this.voxelMeshGroup_ = new THREE.Group();
+      this.voxelMeshGroup_.add(this.voxelMesh_);
+      this.voxelMeshGroup_.position.set(0, 0, 2);
+      this.voxelMeshRotEnd_ = this.voxelMeshGroup_.quaternion.clone();
+      this.voxelMeshGroup_.rotateX(-0.125 * 2 * Math.PI);
+      this.voxelMeshRotStart_ = this.voxelMeshGroup_.quaternion.clone();
+      this.voxelMeshGroup_.quaternion.identity();
+
+      camera.add(this.voxelMeshGroup_);
+
+      const rotFrames = new THREE.QuaternionKeyframeTrack(
+          '.quaternion',
+          [0, 1],
+          [...this.voxelMeshRotStart_.toArray(), ...this.voxelMeshRotEnd_.toArray()]);
+      
+      const rotClip = new THREE.AnimationClip('rot', -1, [rotFrames]);
+
+      this.mixer_ = new THREE.AnimationMixer(this.voxelMeshGroup_);
+      this.action_ = this.mixer_.clipAction(rotClip);
+
+      scene.add(this.placementMesh_);
+
+      this.UpdateVoxelMesh_();
+      this.LoseFocus();
+    }
+
+    OnInput_(msg) {
+      if (!this.active_) {
+        return;
+      }
+
+      if (msg.value == 'enter') {
+        this.PerformAction();
+      }
+    }
+
+    PerformAction() {
+      if (!this.active_) {
+        return;
+      }
+
+      if (!this.placementMesh_.visible) {
+        return;
+      }
+
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+      const possibleCoords = [
+          this.placementMesh_.position.x, this.placementMesh_.position.y, this.placementMesh_.position.z];
+
+      if (!voxels.HasVoxelAt(...possibleCoords)) {
+        voxels.InsertVoxelAt(possibleCoords, this.voxelType_);
+
+        this.action_.setLoop(THREE.LoopOnce, 1);
+        this.action_.clampWhenFinished = true;
+        this.action_.timeScale = 3.0;
+        this.action_.reset();
+        this.action_.play();  
+      }
+    }
+
+    Update(timeInSeconds) {
+      if (!this.active_) {
+        return;
+      }
+
+      this.mixer_.update(timeInSeconds);
+      this.timer_ += timeInSeconds;
+      this.material1_.uniforms.time.value = this.timer_;
+      this.material2_.uniforms.time.value = this.timer_;
+      this.material1_.needsUpdate = true;
+      this.material2_.needsUpdate = true;
+
+      // HACK
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+      this.voxelMesh_.material.uniforms.diffuseMap.value = voxels.materialOpaque_.uniforms.diffuseMap.value;
+      this.placementMesh_.visible = false;
+      
+      const player = this.FindEntity('player');
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(player.Quaternion);
+
+      const ray = new THREE.Ray(player.Position, forward);
+      const intersections = voxels.FindIntersectionsWithRay(ray, 5).filter(i => i.voxel.visible);
+      if (!intersections.length) {
+        return;
+      }
+
+      const possibleCoords = [...intersections[0].voxel.position];
+
+      // Now pick which side to put block on
+      const coords = this.FindClosestSide_(possibleCoords, ray);
+      if (!coords) {
+        return;
+      }
+
+      if (!voxels.HasVoxelAt(...coords)) {
+        this.placementMesh_.position.set(...coords);
+        this.placementMesh_.visible = true;
+      }
+    }
+
+    FindClosestSide_(possibleCoords, ray) {
+      const sides = [
+          [...possibleCoords], [...possibleCoords], [...possibleCoords], 
+          [...possibleCoords], [...possibleCoords], [...possibleCoords], 
+      ];
+      sides[0][0] -= 1;
+      sides[1][0] += 1;
+      sides[2][1] -= 1;
+      sides[3][1] += 1;
+      sides[4][2] -= 1;
+      sides[5][2] += 1;
+
+      const AsAABB_ = (v) => {
+        const position = new THREE.Vector3(...v);
+        const half = new THREE.Vector3(0.5, 0.5, 0.5);
+
+        const m1 = new THREE.Vector3();
+        m1.copy(position);
+        m1.sub(half);
+
+        const m2 = new THREE.Vector3();
+        m2.copy(position);
+        m2.add(half);
+
+        return new THREE.Box3(m1, m2);
+      }
+
+      const boxes = sides.map(v => AsAABB_(v));
+      const _TMP_V = new THREE.Vector3();
+
+      const intersections = [];
+      for (let i = 0; i < boxes.length; ++i) {
+        if (ray.intersectBox(boxes[i], _TMP_V)) {
+          intersections.push({
+              position: sides[i],
+              distance: _TMP_V.distanceTo(ray.origin)
+          });
+        }
+      }
+
+      intersections.sort((a, b) => {
+        return a.distance - b.distance;
+      });
+
+      if (intersections.length > 0) {
+        return intersections[0].position;
+      }
+      return null;
+    }
+  };
+
+
+  class VoxelTools_Delete extends entity.Component {
+    constructor() {
+      super();
+      this.timer_ = 0;
+      this.active_ = true;
+    }
+
+    InitEntity() {
+      this.LoadModel_();
+    }
+
+    InitComponent() {
+      this._RegisterHandler('input.pressed', (m) => this.OnInput_(m));
+      this._RegisterHandler('ui.toolChanged', (m) => this.OnToolChanged_(m));
+    }
+
+    OnToolChanged_(msg) {
+      if (!hack_defs.showTools) {
+        return;
+      }
+
+      if (msg.value != 'break') {
+        this.LoseFocus();
+      } else {
+        this.GainFocus();
+      }
+    }
+
+    LoseFocus() {
+      this.balls_.visible = false;
+      this.placementMesh_.visible = false;
+      this.active_ = false;
+    }
+
+    GainFocus() {
+      this.balls_.visible = true;
+      this.placementMesh_.visible = true;
+      this.active_ = true;
+    }
+
+    LoadModel_() {
+      const scene = this.FindEntity('renderer').GetComponent('ThreeJSController').scene_;
+      const camera = this.FindEntity('renderer').GetComponent('ThreeJSController').uiCamera_;
+
+      this.balls_ = new THREE.Group();
+      camera.add(this.balls_);
+
+      const loader = new GLTFLoader();
+      loader.load('./resources/pickaxe/scene.gltf', (gltf) => {
+        gltf.scene.traverse(c => {
+          if (c.material) {
+            c.material.depthWrite = false;
+            c.material.depthTest = false;
+          }
+        });
+
+        this.mesh_ = gltf.scene;
+        this.mesh_.position.set(2, 2, 1);
+        this.mesh_.scale.setScalar(0.1);
+        this.mesh_.rotateZ(0.25 * 2 * Math.PI);
+        this.mesh_.rotateY(-0.1 * 2 * Math.PI);
+
+        this.group_ = new THREE.Group();
+        this.group_.add(this.mesh_);
+        this.group_.position.set(0, -3, -4);
+        const endRot = this.group_.quaternion.clone();
+        this.group_.rotateX(-0.25 * 2 * Math.PI);
+        const startRot = this.group_.quaternion.clone();
+        this.group_.quaternion.identity();
+
+        this.balls_.add(this.group_);
+  
+        const rotFrames = new THREE.QuaternionKeyframeTrack(
+            '.quaternion',
+            [0, 1, 2],
+            [...endRot.toArray(), ...startRot.toArray(), ...endRot.toArray()]);
+        
+        const rotClip = new THREE.AnimationClip('rot', -1, [rotFrames]);
+  
+        this.mixer_ = new THREE.AnimationMixer(this.group_);
+        this.action_ = this.mixer_.clipAction(rotClip);
+      });
+
+      const geo = new THREE.BoxBufferGeometry(1, 1, 1);
+
+      const p1 = new THREE.ShaderMaterial({
+          uniforms: {
+              time: {value: 0.0},
+              edgeColour: { value: new THREE.Color(0xFF0000) },
+          },
+          vertexShader: voxel_shader.PLACEMENT.VS,
+          fragmentShader: voxel_shader.PLACEMENT.PS,
+          side: THREE.FrontSide,
+          blending: THREE.NormalBlending,
+          transparent: true,
+          depthWrite: false,
+      });
+      const p2 = p1.clone();
+      p2.side = THREE.BackSide;
+
+      const m1 = new THREE.Mesh(geo, p1);
+      const m2 = new THREE.Mesh(geo, p2);
+      m1.renderOrder = 1;
+      this.placementMesh_ = new THREE.Group();
+      this.placementMesh_.add(m1);
+      this.placementMesh_.add(m2);
+      this.placementMesh_.scale.setScalar(1.0001);
+      this.material1_ = p1;
+      this.material2_ = p2;
+
+      scene.add(this.placementMesh_);
+
+      this.LoseFocus();
+    }
+
+    OnInput_(msg) {
+      if (!this.active_) {
+        return;
+      }
+
+      if (msg.value == 'enter') {
+        this.PerformAction();
+      }
+    }
+
+    PerformAction() {
+      if (!this.active_) {
+        return;
+      }
+
+      if (!this.placementMesh_.visible) {
+        return;
+      }
+
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+      const possibleCoords = [
+          this.placementMesh_.position.x, this.placementMesh_.position.y, this.placementMesh_.position.z];
+
+      if (voxels.HasVoxelAt(...possibleCoords)) {
+        voxels.RemoveVoxelAt(possibleCoords);
+
+        if (this.action_) {
+          this.action_.setLoop(THREE.LoopOnce, 1);
+          this.action_.clampWhenFinished = true;
+          this.action_.timeScale = 10.0;
+          this.action_.reset();
+          this.action_.play();  
+        }
+      }
+    }
+
+    Update(timeInSeconds) {
+      if (!this.active_) {
+        return;
+      }
+
+      if (this.mixer_) {
+        this.mixer_.update(timeInSeconds);
+      }
+
+      this.timer_ += timeInSeconds;
+      this.material1_.uniforms.time.value = this.timer_;
+      this.material2_.uniforms.time.value = this.timer_;
+      this.material1_.needsUpdate = true;
+      this.material2_.needsUpdate = true;
+
+      // HACK
+      const voxels = this.FindEntity('voxels').GetComponent('SparseVoxelCellManager');
+
+      const player = this.FindEntity('player');
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(player.Quaternion);
+
+      const ray = new THREE.Ray(player.Position, forward);
+      const intersections = voxels.FindIntersectionsWithRay(ray, 4);
+      if (!intersections.length) {
+        return;
+      }
+
+      const possibleCoords = [...intersections[0].voxel.position];
+
+      if (voxels.HasVoxelAt(...possibleCoords)) {
+        this.placementMesh_.position.set(...possibleCoords);
+        this.placementMesh_.visible = true;
+      }
+    }
+  };
+
+  return {
+      VoxelTools_Insert: VoxelTools_Insert,
+      VoxelTools_Delete: VoxelTools_Delete,
+  };
+})();

+ 366 - 0
src/water.js

@@ -0,0 +1,366 @@
+import {
+	Color,
+	FrontSide,
+	LinearEncoding,
+	LinearFilter,
+	MathUtils,
+	Matrix4,
+	Mesh,
+	NoToneMapping,
+	PerspectiveCamera,
+	Plane,
+	RGBFormat,
+	ShaderMaterial,
+	UniformsLib,
+	UniformsUtils,
+	Vector3,
+	Vector4,
+	WebGLRenderTarget
+} from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+/**
+ * Work based on :
+ * http://slayvin.net : Flat mirror for three.js
+ * http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror
+ * http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL
+ */
+
+var Water = function ( geometry, options ) {
+
+	Mesh.call( this, geometry );
+
+	var scope = this;
+
+	options = options || {};
+
+	var textureWidth = options.textureWidth !== undefined ? options.textureWidth : 512;
+	var textureHeight = options.textureHeight !== undefined ? options.textureHeight : 512;
+
+	var clipBias = options.clipBias !== undefined ? options.clipBias : 0.0;
+	var alpha = options.alpha !== undefined ? options.alpha : 1.0;
+	var time = options.time !== undefined ? options.time : 0.0;
+	var normalSampler = options.waterNormals !== undefined ? options.waterNormals : null;
+	var sunDirection = options.sunDirection !== undefined ? options.sunDirection : new Vector3( 0.70707, 0.70707, 0.0 );
+	var sunColor = new Color( options.sunColor !== undefined ? options.sunColor : 0xffffff );
+	var waterColor = new Color( options.waterColor !== undefined ? options.waterColor : 0x7F7F7F );
+	var eye = options.eye !== undefined ? options.eye : new Vector3( 0, 0, 0 );
+	var distortionScale = options.distortionScale !== undefined ? options.distortionScale : 20.0;
+	var side = options.side !== undefined ? options.side : FrontSide;
+	var fog = options.fog !== undefined ? options.fog : false;
+
+	//
+
+	var mirrorPlane = new Plane();
+	var normal = new Vector3();
+	var mirrorWorldPosition = new Vector3();
+	var cameraWorldPosition = new Vector3();
+	var rotationMatrix = new Matrix4();
+	var lookAtPosition = new Vector3( 0, 0, - 1 );
+	var clipPlane = new Vector4();
+
+	var view = new Vector3();
+	var target = new Vector3();
+	var q = new Vector4();
+
+	var textureMatrix = new Matrix4();
+
+	var mirrorCamera = new PerspectiveCamera();
+
+	var parameters = {
+		minFilter: LinearFilter,
+		magFilter: LinearFilter,
+		format: RGBFormat
+	};
+
+	var renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, parameters );
+
+	if ( ! MathUtils.isPowerOfTwo( textureWidth ) || ! MathUtils.isPowerOfTwo( textureHeight ) ) {
+
+		renderTarget.texture.generateMipmaps = false;
+
+	}
+
+	var mirrorShader = {
+
+		uniforms: UniformsUtils.merge( [
+			UniformsLib[ 'fog' ],
+			UniformsLib[ 'lights' ],
+			{
+				'normalSampler': { value: null },
+				'mirrorSampler': { value: null },
+				'alpha': { value: 1.0 },
+				'time': { value: 0.0 },
+				'size': { value: 1.0 },
+				'distortionScale': { value: 20.0 },
+				'textureMatrix': { value: new Matrix4() },
+				'sunColor': { value: new Color( 0x7F7F7F ) },
+				'sunDirection': { value: new Vector3( 0.70707, 0.70707, 0 ) },
+				'eye': { value: new Vector3() },
+				'waterColor': { value: new Color( 0x555555 ) }
+			}
+		] ),
+
+		vertexShader: [
+			'uniform mat4 textureMatrix;',
+			'uniform float time;',
+
+			'varying vec4 mirrorCoord;',
+			'varying vec4 worldPosition;',
+
+		 	'#include <common>',
+		 	'#include <fog_pars_vertex>',
+			'#include <shadowmap_pars_vertex>',
+			'#include <logdepthbuf_pars_vertex>',
+
+			'void main() {',
+			'	mirrorCoord = modelMatrix * vec4( position, 1.0 );',
+			'	worldPosition = mirrorCoord.xyzw;',
+			'	mirrorCoord = textureMatrix * mirrorCoord;',
+			'	vec4 mvPosition =  modelViewMatrix * vec4( position, 1.0 );',
+			'	gl_Position = projectionMatrix * mvPosition;',
+
+			'#include <beginnormal_vertex>',
+			'#include <defaultnormal_vertex>',
+			'#include <logdepthbuf_vertex>',
+			'#include <fog_vertex>',
+			'#include <shadowmap_vertex>',
+			'}'
+		].join( '\n' ),
+
+		fragmentShader: [
+			'uniform sampler2D mirrorSampler;',
+			'uniform float alpha;',
+			'uniform float time;',
+			'uniform float size;',
+			'uniform float distortionScale;',
+			'uniform sampler2D normalSampler;',
+			'uniform vec3 sunColor;',
+			'uniform vec3 sunDirection;',
+			'uniform vec3 eye;',
+			'uniform vec3 waterColor;',
+
+			'varying vec4 mirrorCoord;',
+			'varying vec4 worldPosition;',
+
+			'vec4 getNoise( vec2 uv ) {',
+			'	vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);',
+			'	vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );',
+			'	vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );',
+			'	vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );',
+			'	vec4 noise = texture2D( normalSampler, uv0 ) +',
+			'		texture2D( normalSampler, uv1 ) +',
+			'		texture2D( normalSampler, uv2 ) +',
+			'		texture2D( normalSampler, uv3 );',
+			'	return noise * 0.5 - 1.0;',
+			'}',
+
+			'void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) {',
+			'	vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) );',
+			'	float direction = max( 0.0, dot( eyeDirection, reflection ) );',
+			'	specularColor += pow( direction, shiny ) * sunColor * spec;',
+			'	diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse;',
+			'}',
+
+			'#include <common>',
+			'#include <packing>',
+			'#include <bsdfs>',
+			'#include <fog_pars_fragment>',
+			'#include <logdepthbuf_pars_fragment>',
+			'#include <lights_pars_begin>',
+			'#include <shadowmap_pars_fragment>',
+			'#include <shadowmask_pars_fragment>',
+
+			'void main() {',
+
+			'#include <logdepthbuf_fragment>',
+			'	vec4 noise = getNoise( worldPosition.xz * size );',
+			'	vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );',
+
+			'	vec3 diffuseLight = vec3(0.0);',
+			'	vec3 specularLight = vec3(0.0);',
+
+			'	vec3 worldToEye = eye-worldPosition.xyz;',
+			'	vec3 eyeDirection = normalize( worldToEye );',
+			'	sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );',
+
+			'	float distance = length(worldToEye);',
+
+			'	vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale;',
+			'	vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.w + distortion ) );',
+
+			'	float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );',
+			'	float rf0 = 0.3;',
+			'	float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );',
+			'	vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;',
+			'	vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance);',
+			'	vec3 outgoingLight = albedo;',
+			'	gl_FragColor = vec4( outgoingLight, alpha );',
+
+			'#include <tonemapping_fragment>',
+			'#include <fog_fragment>',
+			'}'
+		].join( '\n' )
+
+	};
+
+	var material = new ShaderMaterial( {
+		fragmentShader: mirrorShader.fragmentShader,
+		vertexShader: mirrorShader.vertexShader,
+		uniforms: UniformsUtils.clone( mirrorShader.uniforms ),
+		lights: true,
+		side: side,
+		fog: fog
+	} );
+
+	material.uniforms[ 'mirrorSampler' ].value = renderTarget.texture;
+	material.uniforms[ 'textureMatrix' ].value = textureMatrix;
+	material.uniforms[ 'alpha' ].value = alpha;
+	material.uniforms[ 'time' ].value = time;
+	material.uniforms[ 'normalSampler' ].value = normalSampler;
+	material.uniforms[ 'sunColor' ].value = sunColor;
+	material.uniforms[ 'waterColor' ].value = waterColor;
+	material.uniforms[ 'sunDirection' ].value = sunDirection;
+	material.uniforms[ 'distortionScale' ].value = distortionScale;
+
+	material.uniforms[ 'eye' ].value = eye;
+
+	scope.material = material;
+
+	scope.onBeforeRender = function ( renderer, scene, camera ) {
+
+		mirrorWorldPosition.setFromMatrixPosition( scope.matrixWorld );
+		cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld );
+
+		rotationMatrix.extractRotation( scope.matrixWorld );
+
+		normal.set( 0, 0, 1 );
+		normal.applyMatrix4( rotationMatrix );
+
+		view.subVectors( mirrorWorldPosition, cameraWorldPosition );
+
+		// Avoid rendering when mirror is facing away
+
+		if ( view.dot( normal ) > 0 ) return;
+
+		view.reflect( normal ).negate();
+		view.add( mirrorWorldPosition );
+
+		rotationMatrix.extractRotation( camera.matrixWorld );
+
+		lookAtPosition.set( 0, 0, - 1 );
+		lookAtPosition.applyMatrix4( rotationMatrix );
+		lookAtPosition.add( cameraWorldPosition );
+
+		target.subVectors( mirrorWorldPosition, lookAtPosition );
+		target.reflect( normal ).negate();
+		target.add( mirrorWorldPosition );
+
+		mirrorCamera.position.copy( view );
+		mirrorCamera.up.set( 0, 1, 0 );
+		mirrorCamera.up.applyMatrix4( rotationMatrix );
+		mirrorCamera.up.reflect( normal );
+		mirrorCamera.lookAt( target );
+
+		mirrorCamera.far = camera.far; // Used in WebGLBackground
+
+		mirrorCamera.updateMatrixWorld();
+		mirrorCamera.projectionMatrix.copy( camera.projectionMatrix );
+
+		// Update the texture matrix
+		textureMatrix.set(
+			0.5, 0.0, 0.0, 0.5,
+			0.0, 0.5, 0.0, 0.5,
+			0.0, 0.0, 0.5, 0.5,
+			0.0, 0.0, 0.0, 1.0
+		);
+		textureMatrix.multiply( mirrorCamera.projectionMatrix );
+		textureMatrix.multiply( mirrorCamera.matrixWorldInverse );
+
+		// Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
+		// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
+		mirrorPlane.setFromNormalAndCoplanarPoint( normal, mirrorWorldPosition );
+		mirrorPlane.applyMatrix4( mirrorCamera.matrixWorldInverse );
+
+		clipPlane.set( mirrorPlane.normal.x, mirrorPlane.normal.y, mirrorPlane.normal.z, mirrorPlane.constant );
+
+		var projectionMatrix = mirrorCamera.projectionMatrix;
+
+		q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ];
+		q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ];
+		q.z = - 1.0;
+		q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ];
+
+		// Calculate the scaled plane vector
+		clipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) );
+
+		// Replacing the third row of the projection matrix
+		projectionMatrix.elements[ 2 ] = clipPlane.x;
+		projectionMatrix.elements[ 6 ] = clipPlane.y;
+		projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias;
+		projectionMatrix.elements[ 14 ] = clipPlane.w;
+
+		eye.setFromMatrixPosition( camera.matrixWorld );
+
+		// Render
+
+		if ( renderer.outputEncoding !== LinearEncoding ) {
+
+			console.warn( 'THREE.Water: WebGLRenderer must use LinearEncoding as outputEncoding.' );
+			scope.onBeforeRender = function () {};
+
+			return;
+
+		}
+
+		if ( renderer.toneMapping !== NoToneMapping ) {
+
+			console.warn( 'THREE.Water: WebGLRenderer must use NoToneMapping as toneMapping.' );
+			scope.onBeforeRender = function () {};
+
+			return;
+
+		}
+
+		var currentRenderTarget = renderer.getRenderTarget();
+
+		var currentXrEnabled = renderer.xr.enabled;
+		var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
+
+		scope.visible = false;
+
+		renderer.xr.enabled = false; // Avoid camera modification and recursion
+		renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
+
+		renderer.setRenderTarget( renderTarget );
+
+		renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
+
+		if ( renderer.autoClear === false ) renderer.clear();
+		renderer.render( scene, mirrorCamera );
+
+		scope.visible = true;
+
+		renderer.xr.enabled = currentXrEnabled;
+		renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
+
+		renderer.setRenderTarget( currentRenderTarget );
+
+		// Restore viewport
+
+		var viewport = camera.viewport;
+
+		if ( viewport !== undefined ) {
+
+			renderer.state.viewport( viewport );
+
+		}
+
+	};
+
+};
+
+Water.prototype = Object.create( Mesh.prototype );
+Water.prototype.constructor = Water;
+
+export { Water };

+ 72 - 0
src/worker-pool.js

@@ -0,0 +1,72 @@
+export const worker_pool = (() => {
+
+  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 WorkerPool {
+    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_();
+        });
+      }
+    }
+  }
+
+  return {
+      WorkerPool: WorkerPool,
+  };
+})();