فهرست منبع

Initial commit.

simondevyoutube 5 سال پیش
والد
کامیت
8e50e74cec
13فایلهای تغییر یافته به همراه893 افزوده شده و 0 حذف شده
  1. 44 0
      base.css
  2. 13 0
      index.html
  3. BIN
      resources/heightmap-hi.png
  4. BIN
      resources/heightmap-simondev.jpg
  5. BIN
      resources/heightmap-test.jpg
  6. 60 0
      src/game.js
  7. 109 0
      src/graphics.js
  8. 454 0
      src/main.js
  9. 38 0
      src/math.js
  10. 54 0
      src/noise.js
  11. 42 0
      src/spline.js
  12. 58 0
      src/textures.js
  13. 21 0
      src/utils.js

+ 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/heightmap-hi.png


BIN
resources/heightmap-simondev.jpg


BIN
resources/heightmap-test.jpg


+ 60 - 0
src/game.js

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

+ 109 - 0
src/graphics.js

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

+ 454 - 0
src/main.js

@@ -0,0 +1,454 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {GUI} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/dat.gui.module.js';
+import {Sky} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/objects/Sky.js';
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {noise} from './noise.js';
+import {spline} from './spline.js';
+import {textures} from './textures.js';
+
+import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+
+
+let _APP = null;
+
+
+class HeightGenerator {
+  constructor(generator, position, minRadius, maxRadius) {
+    this._position = position.clone();
+    this._radius = [minRadius, maxRadius];
+    this._generator = generator;
+  }
+
+  Get(x, y) {
+    const distance = this._position.distanceTo(new THREE.Vector2(x, y));
+    let normalization = 1.0 - math.sat(
+        (distance - this._radius[0]) / (this._radius[1] - this._radius[0]));
+    normalization = normalization * normalization * (3 - 2 * normalization);
+
+    return [this._generator.Get(x, y), normalization];
+  }
+}
+
+class FlaredCornerHeightGenerator {
+  constructor() {
+  }
+
+  Get(x, y) {
+    if (x == -250 && y == 250) {
+      return [128, 1];
+    }
+    return [0, 1];
+  }
+}
+
+
+class BumpHeightGenerator {
+  constructor() {
+  }
+
+  Get(x, y) {
+    const dist = new THREE.Vector2(x, y).distanceTo(new THREE.Vector2(0, 0));
+
+    let h = 1.0 - math.sat(dist / 250.0);
+    h = h * h * h * (h * (h * 6 - 15) + 10);
+
+    return [h * 128, 1];
+  }
+}
+
+
+class Heightmap {
+  constructor(params, img) {
+    this._params = params;
+    this._data = graphics.GetImageData(img);
+  }
+
+  Get(x, y) {
+    const _GetPixelAsFloat = (x, y) => {
+      const position = (x + this._data.width * y) * 4;
+      const data = this._data.data;
+      return data[position] / 255.0;
+    }
+
+    // Bilinear filter
+    const offset = new THREE.Vector2(-250, -250);
+    const dimensions = new THREE.Vector2(500, 500);
+
+    const xf = 1.0 - math.sat((x - offset.x) / dimensions.x);
+    const yf = math.sat((y - offset.y) / dimensions.y);
+    const w = this._data.width - 1;
+    const h = this._data.height - 1;
+
+    const x1 = Math.floor(xf * w);
+    const y1 = Math.floor(yf * h);
+    const x2 = math.clamp(x1 + 1, 0, w);
+    const y2 = math.clamp(y1 + 1, 0, h);
+
+    const xp = xf * w - x1;
+    const yp = yf * h - y1;
+
+    const p11 = _GetPixelAsFloat(x1, y1);
+    const p21 = _GetPixelAsFloat(x2, y1);
+    const p12 = _GetPixelAsFloat(x1, y2);
+    const p22 = _GetPixelAsFloat(x2, y2);
+
+    const px1 = math.lerp(xp, p11, p21);
+    const px2 = math.lerp(xp, p12, p22);
+
+    return math.lerp(yp, px1, px2) * this._params.height;
+  }
+}
+
+
+
+class TerrainChunk {
+  constructor(params) {
+    this._params = params;
+    this._Init(params);
+  }
+
+  _Init(params) {
+    const size = new THREE.Vector3(
+        params.width * params.scale, 0, params.width * params.scale);
+
+    this._plane = new THREE.Mesh(
+        new THREE.PlaneGeometry(size.x, size.z, 128, 128),
+        new THREE.MeshStandardMaterial({
+            wireframe: false,
+            color: 0xFFFFFF,
+            side: THREE.FrontSide,
+            vertexColors: THREE.VertexColors,
+        }));
+    this._plane.position.add(params.offset);
+    this._plane.castShadow = false;
+    this._plane.receiveShadow = true;
+    params.group.add(this._plane);
+
+    this.Rebuild();
+  }
+
+  Rebuild() {
+    const offset = this._params.offset;
+    for (let v of this._plane.geometry.vertices) {
+      const heightPairs = [];
+      let normalization = 0;
+      v.z = 0;
+      for (let gen of this._params.heightGenerators) {
+        heightPairs.push(gen.Get(v.x + offset.x, v.y + offset.y));
+        normalization += heightPairs[heightPairs.length-1][1];
+      }
+
+      if (normalization > 0) {
+        for (let h of heightPairs) {
+          v.z += h[0] * h[1] / normalization;
+        }
+      }
+    }
+
+    // DEMO
+    if (this._params.heightGenerators.length > 1 && offset.x == 0 && offset.y == 0) {
+      const gen = this._params.heightGenerators[0];
+      const maxHeight = 16.0;
+      const GREEN = new THREE.Color(0x46b00c);
+
+      for (let f of this._plane.geometry.faces) {
+        const vs = [
+            this._plane.geometry.vertices[f.a],
+            this._plane.geometry.vertices[f.b],
+            this._plane.geometry.vertices[f.c]
+        ];
+
+        const vertexColours = [];
+        for (let v of vs) {
+          const [h, _] = gen.Get(v.x + offset.x, v.y + offset.y);
+          const a = math.sat(h / maxHeight);
+          const vc = new THREE.Color(0xFFFFFF);
+          vc.lerp(GREEN, a);
+
+          vertexColours.push(vc);
+        }
+        f.vertexColors = vertexColours;
+      }
+      this._plane.geometry.elementsNeedUpdate = true;
+    } else {
+      for (let f of this._plane.geometry.faces) {
+        f.vertexColors = [
+            new THREE.Color(0xFFFFFF),
+            new THREE.Color(0xFFFFFF),
+            new THREE.Color(0xFFFFFF),
+        ];
+      }
+
+    }
+    this._plane.geometry.verticesNeedUpdate = true;
+    this._plane.geometry.computeVertexNormals();
+  }
+}
+
+class TerrainChunkManager {
+  constructor(params) {
+    this._chunkSize = 500;
+    this._Init(params);
+  }
+
+  _Init(params) {
+    this._InitNoise(params);
+    this._InitTerrain(params);
+  }
+
+  _InitNoise(params) {
+    params.guiParams.noise = {
+      octaves: 10,
+      persistence: 0.5,
+      lacunarity: 2.0,
+      exponentiation: 3.9,
+      height: 64,
+      scale: 256.0,
+      noiseType: 'simplex',
+      seed: 1
+    };
+
+    const onNoiseChanged = () => {
+      for (let k in this._chunks) {
+        this._chunks[k].chunk.Rebuild();
+      }
+    };
+
+    const noiseRollup = params.gui.addFolder('Terrain.Noise');
+    noiseRollup.add(params.guiParams.noise, "noiseType", ['simplex', 'perlin']).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "scale", 64.0, 1024.0).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "persistence", 0.01, 1.0).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange(
+        onNoiseChanged);
+    noiseRollup.add(params.guiParams.noise, "height", 0, 256).onChange(
+        onNoiseChanged);
+
+    this._noise = new noise.Noise(params.guiParams.noise);
+
+    params.guiParams.heightmap = {
+      height: 16,
+    };
+
+    const heightmapRollup = params.gui.addFolder('Terrain.Heightmap');
+    heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange(
+        onNoiseChanged);
+  }
+
+  _InitTerrain(params) {
+    params.guiParams.terrain= {
+      wireframe: false,
+    };
+
+    this._group = new THREE.Group()
+    this._group.rotation.x = -Math.PI / 2;
+    params.scene.add(this._group);
+
+    const terrainRollup = params.gui.addFolder('Terrain');
+    terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => {
+      for (let k in this._chunks) {
+        this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe;
+      }
+    });
+
+    this._chunks = {};
+    this._params = params;
+
+    // DEMO
+    // this._AddChunk(0, 0);
+
+    for (let x = -1; x <= 1; x++) {
+      for (let z = -1; z <= 1; z++) {
+        this._AddChunk(x, z);
+      }
+    }
+  }
+
+  _Key(x, z) {
+    return x + '.' + z;
+  }
+
+  _AddChunk(x, z) {
+    const offset = new THREE.Vector2(x * this._chunkSize, z * this._chunkSize);
+    const chunk = new TerrainChunk({
+      group: this._group,
+      offset: new THREE.Vector3(offset.x, offset.y, 0),
+      scale: 1,
+      width: this._chunkSize,
+      heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)],
+    });
+
+    const k = this._Key(x, z);
+    const edges = [];
+    for (let xi = -1; xi <= 1; xi++) {
+      for (let zi = -1; zi <= 1; zi++) {
+        if (xi == 0 || zi == 0) {
+          continue;
+        }
+        edges.push(this._Key(x + xi, z + zi));
+      }
+    }
+
+    this._chunks[k] = {
+      chunk: chunk,
+      edges: edges
+    };
+  }
+
+  SetHeightmap(img) {
+    const heightmap = new HeightGenerator(
+        new Heightmap(this._params.guiParams.heightmap, img),
+        new THREE.Vector2(0, 0), 250, 300);
+
+    for (let k in this._chunks) {
+      this._chunks[k].chunk._params.heightGenerators.unshift(heightmap);
+      this._chunks[k].chunk.Rebuild();
+    }
+  }
+
+  Update(timeInSeconds) {
+  }
+}
+
+
+class TerrainSky {
+  constructor(params) {
+    this._Init(params);
+  }
+
+  _Init(params) {
+    this._sky = new Sky();
+    this._sky.scale.setScalar(10000);
+    params.scene.add(this._sky);
+
+    params.guiParams.sky = {
+      turbidity: 10.0,
+      rayleigh: 2,
+      mieCoefficient: 0.005,
+      mieDirectionalG: 0.8,
+      luminance: 1,
+    };
+
+    params.guiParams.sun = {
+      inclination: 0.31,
+      azimuth: 0.25,
+    };
+
+    const onShaderChange = () => {
+      for (let k in params.guiParams.sky) {
+        this._sky.material.uniforms[k].value = params.guiParams.sky[k];
+      }
+      for (let k in params.guiParams.general) {
+        this._sky.material.uniforms[k].value = params.guiParams.general[k];
+      }
+    };
+
+    const onSunChange = () => {
+      var theta = Math.PI * (params.guiParams.sun.inclination - 0.5);
+      var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5);
+
+      const sunPosition = new THREE.Vector3();
+      sunPosition.x = Math.cos(phi);
+      sunPosition.y = Math.sin(phi) * Math.sin(theta);
+      sunPosition.z = Math.sin(phi) * Math.cos(theta);
+
+      this._sky.material.uniforms['sunPosition'].value.copy(sunPosition);
+    };
+
+    const skyRollup = params.gui.addFolder('Sky');
+    skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange(
+        onShaderChange);
+    skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange(
+        onShaderChange);
+    skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange(
+        onShaderChange);
+    skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange(
+        onShaderChange);
+    skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange(
+        onShaderChange);
+
+    const sunRollup = params.gui.addFolder('Sun');
+    sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange(
+        onSunChange);
+    sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange(
+        onSunChange);
+
+    onShaderChange();
+    onSunChange();
+  }
+
+  Update(timeInSeconds) {
+  }
+}
+
+class ProceduralTerrain_Demo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._textures = new textures.TextureAtlas(this);
+    this._textures.onLoad = () => {};
+    this._controls = this._CreateControls();
+    this._CreateGUI();
+
+    this._entities['_terrain'] = new TerrainChunkManager({
+      scene: this._graphics.Scene,
+      gui: this._gui,
+      guiParams: this._guiParams,
+    });
+
+    this._entities['_sky'] = new TerrainSky({
+      scene: this._graphics.Scene,
+      gui: this._gui,
+      guiParams: this._guiParams,
+    });
+    this._LoadBackground();
+  }
+
+  _CreateGUI() {
+    this._guiParams = {
+      general: {
+      },
+    };
+    this._gui = new GUI();
+
+    const generalRollup = this._gui.addFolder('General');
+    this._gui.close();
+  }
+
+  _CreateControls() {
+    const controls = new OrbitControls(
+        this._graphics._camera, this._graphics._threejs.domElement);
+    controls.target.set(0, 50, 0);
+    controls.object.position.set(475, 345, 900);
+    controls.update();
+    return controls;
+  }
+
+  _LoadBackground() {
+    const loader = new THREE.TextureLoader(this._manager);
+
+    loader.load('./resources/heightmap-simondev.jpg', (result) => {
+      this._entities['_terrain'].SetHeightmap(result.image);
+    });
+  }
+
+  _OnStep(timeInSeconds) {
+  }
+}
+
+
+function _Main() {
+  _APP = new ProceduralTerrain_Demo();
+}
+
+_Main();

+ 38 - 0
src/math.js

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

+ 54 - 0
src/noise.js

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

+ 42 - 0
src/spline.js

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

+ 58 - 0
src/textures.js

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

+ 21 - 0
src/utils.js

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