Browse Source

Initial commit.

simondevyoutube 5 years ago
parent
commit
92f5d411b8

+ 44 - 0
base.css

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

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SimonDevCraft</title>
+  <link rel="stylesheet" type="text/css" href="base.css">
+  <script src="https://github.com/mrdoob/three.js/blob/r112/build/three.module.js"></script>
+</head>
+<body>
+  <div id="target"></div>
+  <script src="./src/main.js" type="module">
+  </script>
+</body>
+</html>

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


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


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


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


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


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


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


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


BIN
resources/negx.jpg


BIN
resources/negy.jpg


BIN
resources/negz.jpg


BIN
resources/posx.jpg


BIN
resources/posy.jpg


BIN
resources/posz.jpg


+ 65 - 0
src/clouds.js

@@ -0,0 +1,65 @@
+import {math} from './math.js';
+import {voxels} from './voxels.js';
+
+
+export const clouds = (function() {
+
+  class CloudBlock {
+    constructor(game) {
+      this._game = game;
+      this._mgr = new voxels.InstancedBlocksManager(this._game);
+      this._CreateClouds();
+    }
+
+    _CreateClouds() {
+      this._cells = {};
+
+      for (let i = 0; i < 25; i++) {
+        const x = Math.floor(math.rand_range(-1000, 1000));
+        const z = Math.floor(math.rand_range(-1000, 1000));
+
+        const num = math.rand_int(2, 5);
+        for (let j = 0; j < num; j++) {
+          const w = 128;
+          const h = 128;
+          const xi = Math.floor(math.rand_range(-w * 0.75, w * 0.75));
+          const zi = Math.floor(math.rand_range(-h * 0.75, h * 0.75));
+
+          const xPos = x + xi;
+          const zPos = z + zi;
+
+          const k = xPos + '.' + zPos;
+          this._cells[k] = {
+            position: [xPos, 200, zPos],
+            type: 'cloud',
+            visible: true
+          }
+        }
+      }
+
+      this._mgr.RebuildFromCellBlock(this._cells);
+    }
+  }
+
+  class CloudManager {
+    constructor(game) {
+      this._game = game;
+      this._Init();
+    }
+
+    _Init() {
+      this._clouds = new CloudBlock(this._game);
+    }
+
+    Update(_) {
+      const cameraPosition = this._game._graphics._camera.position;
+
+      this._clouds._mgr._meshes['cloud'].position.x = cameraPosition.x;
+      this._clouds._mgr._meshes['cloud'].position.z = cameraPosition.z;
+    }
+  }
+
+  return {
+    CloudManager: CloudManager
+  };
+})();

+ 248 - 0
src/controls.js

@@ -0,0 +1,248 @@
+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';
+
+
+export const controls = (function() {
+  return {
+    // 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.
+    FPSControls: class {
+      constructor(params) {
+        this._cells = params.cells;
+        this._Init(params);
+      }
+
+      _Init(params) {
+        this._radius = 2;
+        this._enabled = false;
+        this._move = {
+          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, -10);
+        this._acceleration = new THREE.Vector3(30, 7, 80);
+
+        this._SetupPointerLock();
+
+        this._controls = new PointerLockControls(
+            params.camera, document.body);
+        this._controls.getObject().position.set(38, 50, 354);
+        params.scene.add(this._controls.getObject());
+
+        document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
+        document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
+      }
+
+      _onKeyDown(event) {
+        switch (event.keyCode) {
+          case 38: // up
+          case 87: // w
+            this._move.forward = true;
+            break;
+          case 37: // left
+          case 65: // a
+            this._move.left = true; break;
+          case 40: // down
+          case 83: // s
+            this._move.backward = true;
+            break;
+          case 39: // right
+          case 68: // d
+            this._move.right = true;
+            break;
+          case 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._move.forward = false;
+            break;
+          case 37: // left
+          case 65: // a
+            this._move.left = false;
+            break;
+          case 40: // down
+          case 83: // s
+            this._move.backward = false;
+            break;
+          case 39: // right
+          case 68: // d
+            this._move.right = false;
+            break;
+          case 33: // PG_UP
+            this._cells.ChangeActiveTool(1);
+            break;
+          case 34: // PG_DOWN
+          this._cells.ChangeActiveTool(-1);
+            break;
+          case 13: // enter
+            this._cells.PerformAction()
+            break;
+        }
+      }
+
+      _SetupPointerLock() {
+        const hasPointerLock = (
+            'pointerLockElement' in document ||
+            'mozPointerLockElement' in document ||
+            'webkitPointerLockElement' in document);
+        if (hasPointerLock) {
+          const lockChange = (event) => {
+            if (document.pointerLockElement === document.body ||
+                document.mozPointerLockElement === document.body ||
+                document.webkitPointerLockElement === document.body ) {
+              this._enabled = true;
+              this._controls.enabled = true;
+            } else {
+              this._controls.enabled = false;
+            }
+          };
+          const lockError = (event) => {
+            console.log(event);
+          };
+
+          document.addEventListener('pointerlockchange', lockChange, false);
+          document.addEventListener('webkitpointerlockchange', lockChange, false);
+          document.addEventListener('mozpointerlockchange', lockChange, false);
+          document.addEventListener('pointerlockerror', lockError, false);
+          document.addEventListener('mozpointerlockerror', lockError, false);
+          document.addEventListener('webkitpointerlockerror', lockError, false);
+
+          document.getElementById('target').addEventListener('click', (event) => {
+            document.body.requestPointerLock = (
+                document.body.requestPointerLock ||
+                document.body.mozRequestPointerLock ||
+                document.body.webkitRequestPointerLock);
+
+            if (/Firefox/i.test(navigator.userAgent)) {
+              const fullScreenChange = (event) => {
+                if (document.fullscreenElement === document.body ||
+                    document.mozFullscreenElement === document.body ||
+                    document.mozFullScreenElement === document.body) {
+                  document.removeEventListener('fullscreenchange', fullScreenChange);
+                  document.removeEventListener('mozfullscreenchange', fullScreenChange);
+                  document.body.requestPointerLock();
+                }
+              };
+              document.addEventListener(
+                  'fullscreenchange', fullScreenChange, false);
+              document.addEventListener(
+                  'mozfullscreenchange', fullScreenChange, false);
+              document.body.requestFullscreen = (
+                  document.body.requestFullscreen ||
+                  document.body.mozRequestFullscreen ||
+                  document.body.mozRequestFullScreen ||
+                  document.body.webkitRequestFullscreen);
+              document.body.requestFullscreen();
+            } else {
+              document.body.requestPointerLock();
+            }
+          }, false);
+        }
+      }
+
+      _FindIntersections(boxes, position) {
+        const sphere = new THREE.Sphere(position, this._radius);
+
+        const intersections = boxes.filter(b => {
+          return sphere.intersectsBox(b);
+        });
+
+        return intersections;
+      }
+
+      Update(timeInSeconds) {
+        if (!this._enabled) {
+          return;
+        }
+
+        const demo = false;
+        if (demo) {
+          this._controls.getObject().position.x += timeInSeconds * 10;
+          return;
+        }
+
+        const frameDecceleration = new THREE.Vector3(
+            this._velocity.x * this._decceleration.x,
+            this._decceleration.y,
+            this._velocity.z * this._decceleration.z
+        );
+        frameDecceleration.multiplyScalar(timeInSeconds);
+
+        this._velocity.add(frameDecceleration);
+
+        if (this._move.forward) {
+          this._velocity.z -= this._acceleration.z * timeInSeconds;
+        }
+        if (this._move.backward) {
+          this._velocity.z += this._acceleration.z * timeInSeconds;
+        }
+        if (this._move.left) {
+          this._velocity.x -= this._acceleration.x * timeInSeconds;
+        }
+        if (this._move.right) {
+          this._velocity.x += this._acceleration.x * timeInSeconds;
+        }
+
+        const controlObject = this._controls.getObject();
+        const cells = this._cells.LookupCells(
+            this._controls.getObject().position, 3);
+        const boxes = [];
+        for (let c of cells) {
+          boxes.push(...c.AsBox3Array(this._controls.getObject().position, 3));
+        }
+
+        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);
+
+        controlObject.position.add(forward);
+        controlObject.position.add(sideways);
+
+        let intersections = this._FindIntersections(
+            boxes, controlObject.position);
+        if (intersections.length > 0) {
+          controlObject.position.copy(oldPosition);
+        }
+
+        oldPosition.copy(controlObject.position);
+        controlObject.position.y += this._velocity.y * timeInSeconds;
+        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;
+        }
+
+        if (controlObject.position.y < -100) {
+          this._velocity.y = 0;
+          controlObject.position.y = 150;
+          this._standing = true;
+        }
+      }
+    }
+  };
+})();

+ 49 - 0
src/game.js

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

+ 65 - 0
src/graphics.js

@@ -0,0 +1,65 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import Stats from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/stats.module.js';
+import {WEBGL} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+
+
+export const graphics = (function() {
+  return {
+    Graphics: class {
+      constructor(game) {
+      }
+
+      Initialize() {
+        if (!WEBGL.isWebGL2Available()) {
+          return false;
+        }
+
+        this._threejs = new THREE.WebGLRenderer({
+            antialias: true,
+        });
+        this._threejs.setPixelRatio(window.devicePixelRatio);
+        this._threejs.setSize(window.innerWidth, window.innerHeight);
+
+        const target = document.getElementById('target');
+        target.appendChild(this._threejs.domElement);
+
+        this._stats = new Stats();
+				target.appendChild(this._stats.dom);
+
+        window.addEventListener('resize', () => {
+          this._OnWindowResize();
+        }, false);
+
+        const fov = 60;
+        const aspect = 1920 / 1080;
+        const near = 0.1;
+        const far = 10000.0;
+        this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+
+        this._scene = new THREE.Scene();
+        this._scene.background = new THREE.Color(0xaaaaaa);
+
+        return true;
+      }
+
+      _OnWindowResize() {
+        this._camera.aspect = window.innerWidth / window.innerHeight;
+        this._camera.updateProjectionMatrix();
+        this._threejs.setSize(window.innerWidth, window.innerHeight);
+      }
+
+      get Scene() {
+        return this._scene;
+      }
+
+      get Camera() {
+        return this._camera;
+      }
+
+      Render(timeInSeconds) {
+        this._threejs.render(this._scene, this._camera);
+        this._stats.update();
+      }
+    }
+  };
+})();

+ 69 - 0
src/main.js

@@ -0,0 +1,69 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import 'https://cdn.jsdelivr.net/npm/[email protected]/simplex-noise.js';
+import {clouds} from './clouds.js';
+import {controls} from './controls.js';
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {textures} from './textures.js';
+import {voxels} from './voxels.js';
+
+
+let _APP = null;
+
+
+class SimonDevCraft extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = {};
+
+    this._LoadBackground();
+
+    this._atlas = new textures.TextureAtlas(this);
+    this._atlas.onLoad = () => {
+      this._entities['_voxels'] = new voxels.SparseVoxelCellManager(this);
+      this._entities['_clouds'] = new clouds.CloudManager(this);
+      this._entities['_controls'] = new controls.FPSControls(
+          {
+            cells: this._entities['_voxels'],
+            scene: this._graphics.Scene,
+            camera: this._graphics.Camera
+          });
+    };
+  }
+
+  _LoadBackground() {
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/posx.jpg',
+        './resources/posx.jpg',
+        './resources/posy.jpg',
+        './resources/negy.jpg',
+        './resources/posx.jpg',
+        './resources/posx.jpg',
+    ]);
+    this._graphics.Scene.background = texture;
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    this._StepEntities(timeInSeconds);
+  }
+
+  _StepEntities(timeInSeconds) {
+    for (let k in this._entities) {
+      this._entities[k].Update(timeInSeconds);
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new SimonDevCraft();
+}
+
+_Main();

+ 28 - 0
src/math.js

@@ -0,0 +1,28 @@
+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;
+    },
+
+    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);
+    },
+  };
+})();

+ 136 - 0
src/textures.js

@@ -0,0 +1,136 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const textures = (function() {
+  return {
+    // Originally I planned to do texture atlasing, then got lazy.
+    TextureAtlas: class {
+      constructor(game) {
+        this._game = game;
+        this._Create(game);
+        this.onLoad = () => {};
+      }
+
+      _Create(game) {
+        this._manager = new THREE.LoadingManager();
+        this._loader = new THREE.TextureLoader(this._manager);
+        this._textures = {};
+
+        this._LoadType(
+            'grass',
+            ['resources/minecraft/textures/blocks/grass_combined.png'],
+            new THREE.Vector2(1.0, 1.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'desert',
+            ['resources/minecraft/textures/blocks/grass_combined.png'],
+            new THREE.Vector2(1.0, 1.0),
+            [new THREE.Color(0xbfb755), new THREE.Color(0xbfb755)]
+        );
+
+        this._LoadType(
+            'dirt',
+            ['resources/minecraft/textures/blocks/dirt.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'sand',
+            ['resources/minecraft/textures/blocks/sand.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'ocean',
+            ['resources/minecraft/textures/blocks/sand.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'water',
+            ['resources/minecraft/textures/blocks/water_single.png'],
+            new THREE.Vector2(1.0, 2.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'stone',
+            ['resources/minecraft/textures/blocks/stone.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'snow',
+            ['resources/minecraft/textures/blocks/snow.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'log_spruce',
+            ['resources/minecraft/textures/blocks/log_spruce_combined.png'],
+            new THREE.Vector2(1.0, 2.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._LoadType(
+            'leaves_spruce',
+            ['resources/minecraft/textures/blocks/leaves_spruce_opaque.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        // Whatever, don't judge me.
+        this._LoadType(
+            'cloud',
+            ['resources/minecraft/textures/blocks/snow.png'],
+            new THREE.Vector2(1.0, 4.0),
+            [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
+        );
+
+        this._manager.onLoad = () => {
+          this._OnLoad();
+        };
+
+        this._game = game;
+      }
+
+      get Info() {
+        return this._textures;
+      }
+
+      _OnLoad() {
+        this.onLoad();
+      }
+
+      _LoadType(name, textureNames, offset, colourRange) {
+        this._textures[name] = {
+          colourRange: colourRange,
+          uvOffset: [
+              offset.x,
+              offset.y,
+          ],
+          textures: textureNames.map(n => this._loader.load(n))
+        };
+        if (this._textures[name].textures.length > 1) {
+        } else {
+          const caps = this._game._graphics._threejs.capabilities;
+          const aniso = caps.getMaxAnisotropy();
+
+          this._textures[name].texture = this._textures[name].textures[0];
+          this._textures[name].texture.minFilter = THREE.LinearMipMapLinearFilter;
+          this._textures[name].texture.magFilter = THREE.NearestFilter;
+          this._textures[name].texture.wrapS = THREE.RepeatWrapping;
+          this._textures[name].texture.wrapT = THREE.RepeatWrapping;
+          this._textures[name].texture.anisotropy = aniso;
+        }
+      }
+    }
+  };
+})();

+ 21 - 0
src/utils.js

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

+ 782 - 0
src/voxels.js

@@ -0,0 +1,782 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import 'https://cdn.jsdelivr.net/npm/[email protected]/simplex-noise.js';
+import {BufferGeometryUtils} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/utils/BufferGeometryUtils.js';
+import {math} from './math.js';
+import {utils} from './utils.js';
+import {voxels_shader} from './voxels_shader.js';
+import {voxels_tool} from './voxels_tool.js';
+
+
+export const voxels = (function() {
+
+  const _VOXEL_HEIGHT = 128;
+  const _OCEAN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.12);
+  const _BEACH_LEVEL = _OCEAN_LEVEL + 2;
+  const _SNOW_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.8);
+  const _MOUNTAIN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.5);
+
+  // HACKY TODO: Pass a terrain generation object through instead of these
+  // loose functions.
+  const _N1 = new SimplexNoise(2);
+  const _N2 = new SimplexNoise(3);
+  const _N3 = new SimplexNoise(4);
+  function _SimplexNoise(gen, nx, ny){
+    return gen.noise2D(nx, ny) * 0.5 + 0.5;
+  }
+
+  function Noise(gen, x, y, sc, octaves, persistence, exponentiation) {
+    const xs = x / sc;
+    const ys = y / sc;
+    let amplitude = 1.0;
+    let frequency = 1.0;
+    let normalization = 0;
+    let total = 0;
+    for (let o = 0; o < octaves; o++) {
+      total += _SimplexNoise(gen, xs * frequency, ys * frequency) * amplitude;
+      normalization += amplitude;
+      amplitude *= persistence;
+      frequency *= 2.0;
+    }
+    total /= normalization;
+    return Math.pow(total, exponentiation);
+  }
+
+  function Biome(e, m) {
+    if (e < _OCEAN_LEVEL) return 'ocean';
+    if (e < _BEACH_LEVEL) return 'sand';
+
+    if (e > _SNOW_LEVEL) {
+      return 'snow';
+    }
+
+    if (e > _MOUNTAIN_LEVEL) {
+      if (m < 0.1) {
+        return 'stone';
+      } else if (m < 0.25) {
+        return 'hills';
+      }
+    }
+
+    // if (m < 0.1) {
+    //   return 'desert';
+    // }
+
+    return 'grass';
+  }
+
+  class InstancedBlocksManager {
+    constructor(game, cell) {
+      this._game = game;
+      this._geometryBuffers = {};
+      this._meshes = {};
+      this._materials = {};
+      this._Create(game);
+    }
+
+    _Create(game) {
+      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.attributes.uv.array[5] = 3.0 / 4.0;
+      pyGeometry.attributes.uv.array[7] = 3.0 / 4.0;
+      pyGeometry.attributes.uv.array[1] = 4.0 / 4.0;
+      pyGeometry.attributes.uv.array[3] = 4.0 / 4.0;
+      pyGeometry.rotateX(-Math.PI / 2);
+      pyGeometry.translate(0, 0.5, 0);
+
+      const nyGeometry = new THREE.PlaneBufferGeometry(1, 1);
+      nyGeometry.attributes.uv.array[5] = 1.0 / 4.0;
+      nyGeometry.attributes.uv.array[7] = 1.0 / 4.0;
+      nyGeometry.attributes.uv.array[1] = 2.0 / 4.0;
+      nyGeometry.attributes.uv.array[3] = 2.0 / 4.0;
+      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 flipGeometries = [
+        pxGeometry, nxGeometry, pzGeometry, nzGeometry
+      ];
+
+      for (let g of flipGeometries) {
+        g.attributes.uv.array[5] = 2.0 / 4.0;
+        g.attributes.uv.array[7] = 2.0 / 4.0;
+        g.attributes.uv.array[1] = 3.0 / 4.0;
+        g.attributes.uv.array[3] = 3.0 / 4.0;
+      }
+
+      this._geometries = [
+        pxGeometry, nxGeometry,
+        pyGeometry, nyGeometry,
+        pzGeometry, nzGeometry
+      ];
+
+      this._geometries = {
+        cube: BufferGeometryUtils.mergeBufferGeometries(this._geometries),
+        plane: pyGeometry,
+      };
+    }
+
+    RebuildFromCellBlock(cells) {
+      const cellsOfType = {};
+
+      for (let k in cells) {
+        const c = cells[k];
+        if (!(c.type in cellsOfType)) {
+          cellsOfType[c.type] = [];
+        }
+        if (c.visible) {
+          cellsOfType[c.type].push(c);
+        }
+      }
+
+      for (let k in cellsOfType) {
+        this._RebuildFromCellType(cellsOfType[k], k);
+      }
+
+      for (let k in this._geometryBuffers) {
+        if (!(k in cellsOfType)) {
+          this._RebuildFromCellType([], k);
+        }
+      }
+    }
+
+    _GetBaseGeometryForCellType(cellType) {
+      if (cellType == 'water') {
+        return this._geometries.plane;
+      }
+      return this._geometries.cube;
+    }
+
+    _RebuildFromCellType(cells, cellType) {
+      const textureInfo = this._game._atlas.Info[cellType];
+
+      if (!(cellType in this._geometryBuffers)) {
+        this._geometryBuffers[cellType] = new THREE.InstancedBufferGeometry();
+
+        this._materials[cellType] = new THREE.RawShaderMaterial({
+          uniforms: {
+            diffuseTexture: {
+              value: textureInfo.texture
+            },
+            skybox: {
+              value: this._game._graphics._scene.background
+            },
+            fogDensity: {
+              value:  0.005
+            },
+            cloudScale: {
+              value: [1, 1, 1]
+            }
+          },
+          vertexShader: voxels_shader.VS,
+          fragmentShader: voxels_shader.PS,
+          side: THREE.FrontSide
+        });
+
+        // HACKY: Need to have some sort of material manager and pass
+        // these params.
+        if (cellType == 'water') {
+          this._materials[cellType].blending = THREE.NormalBlending;
+          this._materials[cellType].depthWrite = false;
+          this._materials[cellType].depthTest = true;
+          this._materials[cellType].transparent = true;
+        }
+
+        if (cellType == 'cloud') {
+          this._materials[cellType].uniforms.fogDensity.value = 0.001;
+          this._materials[cellType].uniforms.cloudScale.value = [64, 10, 64];
+        }
+
+        this._meshes[cellType] = new THREE.Mesh(
+            this._geometryBuffers[cellType], this._materials[cellType]);
+        this._game._graphics._scene.add(this._meshes[cellType]);
+      }
+
+      this._geometryBuffers[cellType].maxInstancedCount = cells.length;
+
+      const baseGeometry = this._GetBaseGeometryForCellType(cellType);
+
+      this._geometryBuffers[cellType].setAttribute(
+          'position', new THREE.Float32BufferAttribute(
+              [...baseGeometry.attributes.position.array], 3));
+      this._geometryBuffers[cellType].setAttribute(
+          'uv', new THREE.Float32BufferAttribute(
+              [...baseGeometry.attributes.uv.array], 2));
+      this._geometryBuffers[cellType].setAttribute(
+          'normal', new THREE.Float32BufferAttribute(
+              [...baseGeometry.attributes.normal.array], 3));
+      this._geometryBuffers[cellType].setIndex(
+          new THREE.BufferAttribute(
+              new Uint32Array([...baseGeometry.index.array]), 1));
+
+      const offsets = [];
+      const uvOffsets = [];
+      const colors = [];
+
+      const box = new THREE.Box3();
+
+      for (let c in cells) {
+        const curCell = cells[c];
+
+        let randomLuminance = Noise(
+            _N2, curCell.position[0], curCell.position[2], 16, 8, 0.6, 2) * 0.2 + 0.8;
+        if (curCell.luminance !== undefined) {
+          randomLuminance = curCell.luminance;
+        } else if (cellType == 'cloud') {
+          randomLuminance = 1;
+        }
+
+        const colour = textureInfo.colourRange[0].clone();
+        colour.r *= randomLuminance;
+        colour.g *= randomLuminance;
+        colour.b *= randomLuminance;
+
+        colors.push(colour.r, colour.g, colour.b);
+        offsets.push(...curCell.position);
+        uvOffsets.push(...textureInfo.uvOffset);
+        box.expandByPoint(new THREE.Vector3(
+            curCell.position[0],
+            curCell.position[1],
+            curCell.position[2]));
+      }
+
+      this._geometryBuffers[cellType].setAttribute(
+          'color', new THREE.InstancedBufferAttribute(
+              new Float32Array(colors), 3));
+      this._geometryBuffers[cellType].setAttribute(
+          'offset', new THREE.InstancedBufferAttribute(
+              new Float32Array(offsets), 3));
+      this._geometryBuffers[cellType].setAttribute(
+          'uvOffset', new THREE.InstancedBufferAttribute(
+              new Float32Array(uvOffsets), 2));
+      this._geometryBuffers[cellType].attributes.offset.needsUpdate = true;
+      this._geometryBuffers[cellType].attributes.uvOffset.uvOffset = true;
+      this._geometryBuffers[cellType].attributes.color.uvOffset = true;
+
+      this._geometryBuffers[cellType].boundingBox = box;
+      this._geometryBuffers[cellType].boundingSphere = new THREE.Sphere();
+      box.getBoundingSphere(this._geometryBuffers[cellType].boundingSphere);
+    }
+
+    Update() {
+    }
+  };
+
+  const _RAND_VALS = {};
+
+  class SparseVoxelCellBlock {
+    constructor(game, parent, offset, dimensions, id) {
+      this._game = game;
+      this._parent = parent;
+      this._atlas = game._atlas;
+      this._blockOffset = offset;
+      this._blockDimensions = dimensions;
+      this._mgr = new InstancedBlocksManager(this._game, this);
+      this._id = id;
+
+      this._Init();
+    }
+
+    get ID() {
+      return this._id;
+    }
+
+    _GenerateNoise(x, y) {
+      const elevation = Math.floor(Noise(_N1, x, y, 1024, 6, 0.4, 5.65) * 128);
+      const moisture = Noise(_N2, x, y, 512, 6, 0.5, 4);
+
+      return [Biome(elevation, moisture), elevation];
+    }
+
+    _Init() {
+      this._cells = {};
+
+      for (let x = 0; x < this._blockDimensions.x; x++) {
+        for (let z = 0; z < this._blockDimensions.z; z++) {
+          const xPos = x + this._blockOffset.x;
+          const zPos = z + this._blockOffset.z;
+
+          const [atlasType, yOffset] = this._GenerateNoise(xPos, zPos);
+
+          this._cells[xPos + '.' + yOffset + '.' + zPos] = {
+            position: [xPos, yOffset, zPos],
+            type: atlasType,
+            visible: true
+          };
+
+          if (atlasType == 'ocean') {
+            this._cells[xPos + '.' + _OCEAN_LEVEL + '.' + zPos] = {
+              position: [xPos, _OCEAN_LEVEL, zPos],
+              type: 'water',
+              visible: true
+            };
+          } else {
+            // 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) {
+              const heightDifference = yOffset - lowestAdjacent;
+              for (let yi = lowestAdjacent + 1; yi < yOffset; yi++) {
+                this._cells[xPos + '.' + yi + '.' + zPos] = {
+                  position: [xPos, yi, zPos],
+                  type: 'dirt',
+                  visible: true
+                };
+              }
+            }
+          }
+        }
+      }
+
+      this._GenerateTrees();
+    }
+
+    _GenerateTrees() {
+      // This is terrible, but works fine for demo purposes. Just a straight up
+      // grid of trees, with random removal/jittering.
+      for (let x = 0; x < this._blockDimensions.x; x++) {
+        for (let z = 0; z < this._blockDimensions.z; z++) {
+          const xPos = this._blockOffset.x + x;
+          const zPos = this._blockOffset.z + z;
+          if (xPos % 11 != 0 || zPos % 11 != 0) {
+            continue;
+          }
+
+          const roll = Math.random();
+          if (roll < 0.35) {
+            const xTreePos = xPos + math.rand_int(-3, 3);
+            const zTreePos = zPos + math.rand_int(-3, 3);
+
+            const [terrainType, _] = this._GenerateNoise(xTreePos, zTreePos);
+            if (terrainType != 'grass') {
+              continue;
+            }
+
+            this._MakeSpruceTree(xTreePos, zTreePos);
+          }
+        }
+      }
+    }
+
+    HasVoxelAt(x, y, z) {
+      const k = this._Key(x, y, z);
+      if (!(k in this._cells)) {
+        return false;
+      }
+
+      return this._cells[k].visible;
+    }
+
+    InsertVoxel(cellData, overwrite=true) {
+      const k = this._Key(
+          cellData.position[0],
+          cellData.position[1],
+          cellData.position[2]);
+      if (!overwrite && k in this._cells) {
+        return;
+      }
+      this._cells[k] = cellData;
+      this._parent.MarkDirty(this);
+    }
+
+    RemoveVoxel(key) {
+      const v = this._cells[key];
+      this._cells[key].visible = false;
+
+      this._parent.MarkDirty(this);
+
+      // Probably better to just pregenerate these voxels, version 2 maybe.
+      const [atlasType, groundLevel] = this._GenerateNoise(
+          v.position[0], v.position[2]);
+
+      if (v.position[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 = v.position[0] + xi;
+              const zPos = v.position[2] + zi;
+              const yPos = v.position[1] + yi;
+
+              const [adjacentType, groundLevelAdjacent] = this._GenerateNoise(xPos, zPos);
+              const k = this._Key(xPos, yPos, zPos);
+
+              if (!(k in this._cells) && yPos < groundLevelAdjacent) {
+                let type = 'dirt';
+
+                if (adjacentType == 'sand') {
+                  type = 'sand';
+                }
+
+                if (yPos < groundLevelAdjacent - 2) {
+                  type = 'stone';
+                }
+
+                // This is potentially out of bounds of the cell, so route the
+                // voxel insertion via parent.
+                this._parent.InsertVoxel({
+                  position: [xPos, yPos, zPos],
+                  type: type,
+                  visible: true
+                }, false);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    Build() {
+      this._mgr.RebuildFromCellBlock(this._cells);
+    }
+
+    _Key(x, y, z) {
+      return x + '.' + y + '.' + z;
+    }
+
+    _MakeSpruceTree(x, z) {
+      const [_, yOffset] = this._GenerateNoise(x, z);
+
+      // TODO: Technically, inserting into cells can go outside the bounds
+      // of an individual SparseVoxelCellBlock. These calls should be routed
+      // to the parent.
+      const treeHeight = math.rand_int(3, 5);
+      for (let y = 1; y < treeHeight; y++) {
+        const yPos = y + yOffset;
+        const k = this._Key(x, yPos, z);
+        this._cells[k] = {
+          position: [x, yPos, z],
+          type: 'log_spruce',
+          visible: true
+        };
+      }
+
+      for (let h = 0; h < 2; h++) {
+        for (let xi = -2; xi <= 2; xi++) {
+          for (let zi = -2; zi <= 2; zi++) {
+            if (Math.abs(xi) == 2 && Math.abs(zi) == 2) {
+              continue;
+            }
+
+            const yPos = yOffset + h + treeHeight;
+            const xPos = x + xi;
+            const zPos = z + zi;
+            const k = xPos + '.' + yPos + '.' + zPos;
+            this._cells[k] = {
+              position: [xPos, yPos, zPos],
+              type: 'leaves_spruce',
+              visible: true
+            };
+          }
+        }
+      }
+
+      for (let h = 0; h < 2; h++) {
+        for (let xi = -1; xi <= 1; xi++) {
+          for (let zi = -1; zi <= 1; zi++) {
+            if (Math.abs(xi) == 1 && Math.abs(zi) == 1) {
+              continue;
+            }
+
+            const yPos = yOffset + h + treeHeight + 2;
+            const xPos = x + xi;
+            const zPos = z + zi;
+            const k = xPos + '.' + yPos + '.' + zPos;
+            this._cells[k] = {
+              position: [xPos, yPos, zPos],
+              type: 'leaves_spruce',
+              visible: true
+            };
+          }
+        }
+      }
+    }
+
+    AsVoxelArray(pos, radius) {
+      const x = Math.floor(pos.x);
+      const y = Math.floor(pos.y);
+      const z = Math.floor(pos.z);
+
+      const voxels = [];
+      for (let xi = -radius; xi <= radius; xi++) {
+        for (let yi = -radius; yi <= radius; yi++) {
+          for (let zi = -radius; zi <= radius; zi++) {
+            const xPos = xi + x;
+            const yPos = yi + y;
+            const zPos = zi + z;
+            const k = xPos + '.' + yPos + '.' + zPos;
+            if (k in this._cells) {
+              const cell = this._cells[k];
+              if (!cell.visible) {
+                continue;
+              }
+
+              if (cell.blinker !== undefined) {
+                continue;
+              }
+
+              const position = new THREE.Vector3(
+                  cell.position[0], cell.position[1], cell.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);
+
+              const box = new THREE.Box3(m1, m2);
+              const voxelData = {...cell};
+              voxelData.aabb = box;
+              voxelData.key = k;
+              voxels.push(voxelData);
+            }
+          }
+        }
+      }
+
+      return voxels;
+    }
+
+    AsBox3Array(pos, radius) {
+      const x = Math.floor(pos.x);
+      const y = Math.floor(pos.y);
+      const z = Math.floor(pos.z);
+
+      const boxes = [];
+      for (let xi = -radius; xi <= radius; xi++) {
+        for (let yi = -radius; yi <= radius; yi++) {
+          for (let zi = -radius; zi <= radius; zi++) {
+            const xPos = xi + x;
+            const yPos = yi + y;
+            const zPos = zi + z;
+            const k = xPos + '.' + yPos + '.' + zPos;
+            if (k in this._cells) {
+              const cell = this._cells[k];
+              if (!cell.visible) {
+                continue;
+              }
+
+              const position = new THREE.Vector3(
+                  cell.position[0], cell.position[1], cell.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);
+
+              const box = new THREE.Box3(m1, m2);
+              boxes.push(box);
+            }
+          }
+        }
+      }
+
+      return boxes;
+    }
+  };
+
+  class SparseVoxelCellManager {
+    constructor(game) {
+      this._game = game;
+      this._cells = {};
+      this._cellDimensions = new THREE.Vector3(32, 32, 32);
+      this._visibleDimensions = [32, 32];
+      this._dirtyBlocks = {};
+      this._ids = 0;
+
+      this._tools = [
+        null,
+        new voxels_tool.InsertTool(this),
+        new voxels_tool.DeleteTool(this),
+      ];
+      this._activeTool = 0;
+    }
+
+    _Key(x, y, z) {
+      return x + '.' + y + '.' + z;
+    }
+
+    _CellIndex(xp, yp) {
+      const x = Math.floor(xp / this._cellDimensions.x);
+      const z = Math.floor(yp / this._cellDimensions.z);
+      return [x, z];
+    }
+
+    MarkDirty(block) {
+      this._dirtyBlocks[block.ID] = block;
+    }
+
+    InsertVoxel(cellData, overwrite=true) {
+      const [x, z] = this._CellIndex(cellData.position[0], cellData.position[2]);
+      const key = this._Key(x, 0, z);
+
+      if (key in this._cells) {
+        this._cells[key].InsertVoxel(cellData, overwrite);
+      }
+    }
+
+    _FindIntersections(ray, maxDistance) {
+      const camera = this._game._graphics._camera;
+      const cells = this.LookupCells(camera.position, maxDistance);
+      const intersections = [];
+
+      for (let c of cells) {
+        const voxels = c.AsVoxelArray(camera.position, maxDistance);
+
+        for (let v of voxels) {
+          const intersectionPoint = new THREE.Vector3();
+
+          if (ray.intersectBox(v.aabb, intersectionPoint)) {
+            intersections.push({
+                cell: c,
+                voxel: v,
+                intersectionPoint: intersectionPoint,
+                distance: intersectionPoint.distanceTo(camera.position)
+            });
+          }
+        }
+      }
+
+      intersections.sort((a, b) => {
+        const d1 = a.intersectionPoint.distanceTo(camera.position);
+        const d2 = b.intersectionPoint.distanceTo(camera.position);
+        if (d1 < d2) {
+          return -1;
+        } else if (d2 < d1) {
+          return 1;
+        } else {
+          return 0;
+        }
+      });
+
+      return intersections;
+    }
+
+    ChangeActiveTool(dir) {
+      if (this._tools[this._activeTool]) {
+        this._tools[this._activeTool].LoseFocus();
+      }
+
+      this._activeTool += dir + this._tools.length;
+      this._activeTool %= this._tools.length;
+    }
+
+    PerformAction() {
+      if (this._tools[this._activeTool]) {
+        this._tools[this._activeTool].PerformAction();
+      }
+    }
+
+    LookupCells(pos, radius) {
+      // TODO only lookup really close by
+      const [x, z] = this._CellIndex(pos.x, pos.z);
+
+      const cells = [];
+      for (let xi = -1; xi <= 1; xi++) {
+        for (let zi = -1; zi <= 1; zi++) {
+          const key = this._Key(x + xi, 0, z + zi);
+          if (key in this._cells) {
+            cells.push(this._cells[key]);
+          }
+        }
+      }
+
+      return cells;
+    }
+
+    Update(timeInSeconds) {
+      if (this._tools[this._activeTool]) {
+        this._tools[this._activeTool].Update(timeInSeconds);
+      }
+
+      this._UpdateDirtyBlocks();
+      this._UpdateTerrain();
+    }
+
+    _UpdateDirtyBlocks() {
+      for (let k in this._dirtyBlocks) {
+        const b = this._dirtyBlocks[k];
+        b.Build();
+        delete this._dirtyBlocks[k];
+        break;
+      }
+    }
+
+    _UpdateTerrain() {
+      const cameraPosition = this._game._graphics._camera.position;
+      const cellIndex = this._CellIndex(cameraPosition.x, cameraPosition.z);
+
+      const xs = Math.floor((this._visibleDimensions[0] - 1 ) / 2);
+      const zs = Math.floor((this._visibleDimensions[1] - 1) / 2);
+      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._cells, cells);
+      const difference = utils.DictDifference(cells, this._cells);
+      const recycle = Object.values(utils.DictDifference(this._cells, cells));
+
+      cells = intersection;
+
+      for (let k in difference) {
+        const [xi, zi] = difference[k];
+        const offset = new THREE.Vector3(
+            xi * this._cellDimensions.x, 0, zi * this._cellDimensions.z);
+
+        let block = recycle.pop();
+        if (block) {
+          // TODO MAKE PUBLIC API
+          block._blockOffset = offset;
+          block._Init();
+        } else {
+          block = new voxels.SparseVoxelCellBlock(
+              this._game, this, offset, this._cellDimensions, this._ids++);
+        }
+
+        this.MarkDirty(block);
+
+        cells[k] = block;
+      }
+
+      this._cells = cells;
+    }
+  }
+
+  return {
+    InstancedBlocksManager: InstancedBlocksManager,
+    SparseVoxelCellBlock: SparseVoxelCellBlock,
+    SparseVoxelCellManager: SparseVoxelCellManager,
+  };
+})();

+ 114 - 0
src/voxels_shader.js

@@ -0,0 +1,114 @@
+
+export const voxels_shader = (function() {
+
+const _VS = `
+precision highp float;
+
+uniform mat4 modelViewMatrix;
+uniform mat4 projectionMatrix;
+uniform vec3 cameraPosition;
+uniform float fogDensity;
+uniform vec3 cloudScale;
+
+// Attributes
+attribute vec3 position;
+attribute vec3 normal;
+attribute vec3 color;
+attribute vec2 uv;
+
+// Instance attributes
+attribute vec3 offset;
+attribute vec2 uvOffset;
+
+// Outputs
+varying vec2 vUV;
+varying vec4 vColor;
+varying vec4 vLight;
+varying vec3 vNormal;
+varying float vFog;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+
+float _Fog2(const vec3 worldPosition, const float density) {
+  vec4 viewPosition = modelViewMatrix * vec4(worldPosition, 1.0);
+
+  float att = density * viewPosition.z;
+  att = att * att * -1.442695;
+  return 1.0 - clamp(exp2(att), 0.0, 1.0);
+}
+
+vec4 _ComputeLighting() {
+  // Hardcoded vertex lighting is the best lighting.
+  float lighting = clamp(dot(normal, normalize(vec3(1, 1, 0.5))), 0.0, 1.0);
+  vec3 diffuseColour = vec3(1, 1, 1);
+  vec4 diffuseLighting = vec4(diffuseColour * lighting, 1);
+
+  lighting = clamp(dot(normal, normalize(vec3(-1, 1, -1))), 0.0, 1.0);
+  diffuseColour = vec3(0.25, 0.25, 0.25);
+  diffuseLighting += vec4(diffuseColour * lighting, 1);
+
+  lighting = clamp(dot(normal, normalize(vec3(1, 1, 1))), 0.0, 1.0);
+  diffuseColour = vec3(0.5, 0.5, 0.5);
+  diffuseLighting += vec4(diffuseColour * lighting, 1);
+
+  vec4 ambientLighting = vec4(1, 1, 1, 1);
+
+  return diffuseLighting + ambientLighting;
+}
+
+void main(){
+  vec3 worldPosition = offset + position * cloudScale;
+
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(worldPosition, 1.0);
+
+  vUV = uv * uvOffset;
+  vNormal = normalize(worldPosition - cameraPosition);
+  vFog = _Fog2(worldPosition, fogDensity);
+
+  vLight = _ComputeLighting();
+  vColor = vec4(color, 1);
+}
+`;
+
+const _PS = `
+precision highp float;
+
+uniform sampler2D diffuseTexture;
+uniform samplerCube skybox;
+
+varying vec2 vUV;
+varying vec4 vColor;
+varying vec4 vLight;
+varying vec3 vNormal;
+varying float vFog;
+
+#define saturate(a) clamp( a, 0.0, 1.0 )
+
+vec3 _ACESFilmicToneMapping(vec3 x) {
+    float a = 2.51;
+    float b = 0.03;
+    float c = 2.43;
+    float d = 0.59;
+    float e = 0.14;
+    return saturate((x*(a*x+b))/(x*(c*x+d)+e));
+}
+
+void main() {
+  vec4 fragmentColor = texture2D(diffuseTexture, vUV);
+  fragmentColor *= vColor;
+  fragmentColor *= vLight;
+
+  vec4 outColor = vec4(
+      _ACESFilmicToneMapping(fragmentColor.xyz), fragmentColor.a);
+  vec4 fogColor = textureCube(skybox, vNormal);
+
+  gl_FragColor = mix(outColor, fogColor, vFog);
+}
+`;
+
+  return {
+    VS: _VS,
+    PS: _PS,
+  };
+})();

+ 166 - 0
src/voxels_tool.js

@@ -0,0 +1,166 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const voxels_tool = (function() {
+
+  // HACKY TODO: Separate luminance and highlight, right now one overwrites the
+  // other.
+  class InsertTool {
+    constructor(parent) {
+      this._parent = parent;
+      this._cell = null;
+      this._prev = null;
+      this._blinkTimer = 0;
+      this._luminance = 1;
+    }
+
+    LoseFocus() {
+      if (this._prev) {
+        this._parent.MarkDirty(this._prev.cell);
+        this._prev.cell.RemoveVoxel(
+            this._prev.cell._Key(
+                this._prevVoxel.position[0],
+                this._prevVoxel.position[1],
+                this._prevVoxel.position[2]));
+        this._prev = null;
+        this._prevVoxel = null;
+      }
+    }
+
+    PerformAction() {
+      this.LoseFocus();
+
+      const camera = this._parent._game._graphics._camera;
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(camera.quaternion);
+
+      const ray = new THREE.Ray(camera.position, forward);
+      const intersections = this._parent._FindIntersections(ray, 5);
+      if (!intersections.length) {
+        return;
+      }
+
+      const possibleCoords = [...intersections[0].voxel.position];
+      possibleCoords[1] += 1;
+
+      if (!intersections[0].cell.HasVoxelAt(
+          possibleCoords[0], possibleCoords[1], possibleCoords[2])) {
+        intersections[0].cell.InsertVoxel({
+            position: [...possibleCoords],
+            type: 'stone',
+            visible: true
+        }, true);
+      }
+    }
+
+    Update(timeInSeconds) {
+      const camera = this._parent._game._graphics._camera;
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(camera.quaternion);
+
+      const ray = new THREE.Ray(camera.position, forward);
+      const intersections = this._parent._FindIntersections(ray, 5);
+      if (intersections.length) {
+        if (this._prev) {
+          this._parent.MarkDirty(this._prev.cell);
+          this._prev.cell.RemoveVoxel(
+              this._prev.cell._Key(
+                  this._prevVoxel.position[0],
+                  this._prevVoxel.position[1],
+                  this._prevVoxel.position[2]));
+        }
+        const cur = intersections[0];
+        const newVoxel = {
+          position: [...cur.voxel.position],
+          visible: true,
+          type: 'stone',
+          blinker: true
+        };
+        newVoxel.position[1] += 1;
+
+        if (cur.cell.HasVoxelAt(newVoxel.position[0],
+                                newVoxel.position[1],
+                                newVoxel.position[2])) {
+          return;
+        }
+
+        this._prev = cur;
+        this._prevVoxel = newVoxel;
+        this._blinkTimer -= timeInSeconds;
+        if (this._blinkTimer < 0) {
+          this._blinkTimer = 0.25;
+          if (this._luminance == 1) {
+            this._luminance = 2;
+          } else {
+            this._luminance = 1;
+          }
+        }
+        const k = cur.cell._Key(newVoxel.position[0],
+                                newVoxel.position[1],
+                                newVoxel.position[2]);
+        intersections[0].cell.InsertVoxel(newVoxel);
+        intersections[0].cell._cells[k].luminance = this._luminance;
+      }
+    }
+  };
+
+  class DeleteTool {
+    constructor(parent) {
+      this._parent = parent;
+      this._cell = null;
+      this._blinkTimer = 0;
+      this._luminance = 1;
+    }
+
+    LoseFocus() {
+      if (this._prev) {
+        this._prev.cell._cells[this._prev.voxel.key].luminance = 1;
+        this._parent.MarkDirty(this._prev.cell);
+      }
+    }
+
+    PerformAction() {
+      const camera = this._parent._game._graphics._camera;
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(camera.quaternion);
+
+      const ray = new THREE.Ray(camera.position, forward);
+      const intersections = this._parent._FindIntersections(ray, 5);
+      if (!intersections.length) {
+        return;
+      }
+
+      intersections[0].cell.RemoveVoxel(intersections[0].voxel.key);
+    }
+
+    Update(timeInSeconds) {
+      this.LoseFocus();
+
+      const camera = this._parent._game._graphics._camera;
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(camera.quaternion);
+
+      const ray = new THREE.Ray(camera.position, forward);
+      const intersections = this._parent._FindIntersections(ray, 5);
+      if (intersections.length) {
+        this._prev = intersections[0];
+        this._blinkTimer -= timeInSeconds;
+        if (this._blinkTimer < 0) {
+          this._blinkTimer = 0.25;
+          if (this._luminance == 1) {
+            this._luminance = 2;
+          } else {
+            this._luminance = 1;
+          }
+        }
+        intersections[0].cell._cells[intersections[0].voxel.key].luminance = this._luminance;
+        this._parent.MarkDirty(intersections[0].cell);
+      }
+    }
+  };
+
+  return {
+    InsertTool: InsertTool,
+    DeleteTool: DeleteTool,
+  };
+})();