瀏覽代碼

Initial commit.

Simon 5 年之前
父節點
當前提交
c4cc6d8b2f
共有 83 個文件被更改,包括 14137 次插入0 次删除
  1. 85 0
      LICENSE
  2. 13 0
      index.html
  3. 4 0
      resources/README.txt
  4. 二進制
      resources/blaster.jpg
  5. 二進制
      resources/dirt_01_diffuse-1024.png
  6. 二進制
      resources/dirt_01_normal-1024.jpg
  7. 二進制
      resources/explosion.png
  8. 二進制
      resources/grass1-albedo-512.jpg
  9. 二進制
      resources/grass1-albedo3-1024.png
  10. 二進制
      resources/grass1-normal-1024.jpg
  11. 二進制
      resources/models/star-destroyer/scene.bin
  12. 7498 0
      resources/models/star-destroyer/scene.gltf
  13. 二進制
      resources/models/star-destroyer/textures/Body_Top_baseColor.png
  14. 二進制
      resources/models/star-destroyer/textures/Body_Top_emissive.png
  15. 二進制
      resources/models/star-destroyer/textures/Body_Top_metallicRoughness.png
  16. 二進制
      resources/models/star-destroyer/textures/Body_baseColor.png
  17. 二進制
      resources/models/star-destroyer/textures/Body_emissive.png
  18. 二進制
      resources/models/star-destroyer/textures/Body_metallicRoughness.png
  19. 二進制
      resources/models/star-destroyer/textures/Bridge_Thing_emissive.png
  20. 二進制
      resources/models/star-destroyer/textures/Details_baseColor.png
  21. 二進制
      resources/models/star-destroyer/textures/Details_metallicRoughness.png
  22. 二進制
      resources/models/star-destroyer/textures/Engines_baseColor.png
  23. 二進制
      resources/models/star-destroyer/textures/Engines_emissive.png
  24. 二進制
      resources/models/star-destroyer/textures/Material.004_baseColor.jpeg
  25. 二進制
      resources/models/star-destroyer/textures/Material.004_metallicRoughness.png
  26. 二進制
      resources/models/star-destroyer/textures/Turret_baseColor.png
  27. 二進制
      resources/models/star-destroyer/textures/Turret_metallicRoughness.png
  28. 二進制
      resources/models/tie-fighter-gltf/scene.bin
  29. 298 0
      resources/models/tie-fighter-gltf/scene.gltf
  30. 二進制
      resources/models/tie-fighter-gltf/textures/hullblue_baseColor.png
  31. 二進制
      resources/models/tie-fighter-gltf/textures/hullblue_metallicRoughness.png
  32. 二進制
      resources/models/tie-fighter-gltf/textures/hullblue_normal.png
  33. 二進制
      resources/models/x-wing/scene.bin
  34. 921 0
      resources/models/x-wing/scene.gltf
  35. 二進制
      resources/models/x-wing/textures/lambert7_baseColor.png
  36. 二進制
      resources/models/x-wing/textures/lambert7_metallicRoughness.png
  37. 二進制
      resources/models/x-wing/textures/lambert7_normal.png
  38. 二進制
      resources/models/x-wing/textures/lambert8_baseColor.png
  39. 二進制
      resources/models/x-wing/textures/lambert8_metallicRoughness.png
  40. 二進制
      resources/models/x-wing/textures/lambert8_normal.png
  41. 二進制
      resources/rock-snow-ice-albedo-1024.png
  42. 二進制
      resources/rock-snow-ice-normal-1024.jpg
  43. 二進制
      resources/rough-wet-cobble-albedo-1024.png
  44. 二進制
      resources/rough-wet-cobble-normal-1024.jpg
  45. 二進制
      resources/sandy-rocks1-albedo-1024.png
  46. 二進制
      resources/sandy-rocks1-normal-1024.jpg
  47. 二進制
      resources/sandyground-albedo-1024.png
  48. 二進制
      resources/sandyground-normal-1024.jpg
  49. 二進制
      resources/simplex-noise.png
  50. 二進制
      resources/snow-packed-albedo-1024.png
  51. 二進制
      resources/snow-packed-normal-1024.jpg
  52. 二進制
      resources/space-negx.jpg
  53. 二進制
      resources/space-negy.jpg
  54. 二進制
      resources/space-negz.jpg
  55. 二進制
      resources/space-posx.jpg
  56. 二進制
      resources/space-posy.jpg
  57. 二進制
      resources/space-posz.jpg
  58. 二進制
      resources/worn-bumpy-rock-albedo-1024.png
  59. 二進制
      resources/worn-bumpy-rock-albedo-512.jpg
  60. 二進制
      resources/worn-bumpy-rock-normal-1024.jpg
  61. 356 0
      src/agent.js
  62. 211 0
      src/blaster.js
  63. 39 0
      src/camera-track.js
  64. 478 0
      src/controls.js
  65. 82 0
      src/demo.js
  66. 60 0
      src/game.js
  67. 248 0
      src/graphics.js
  68. 406 0
      src/main.js
  69. 38 0
      src/math.js
  70. 48 0
      src/noise.js
  71. 104 0
      src/particles.js
  72. 550 0
      src/perlin-noise.js
  73. 187 0
      src/quadtree.js
  74. 414 0
      src/scattering-shader.js
  75. 115 0
      src/sky.js
  76. 536 0
      src/space.js
  77. 76 0
      src/spline.js
  78. 309 0
      src/terrain-chunk.js
  79. 317 0
      src/terrain-shader.js
  80. 552 0
      src/terrain.js
  81. 84 0
      src/textures.js
  82. 21 0
      src/utils.js
  83. 87 0
      src/visibility.js

+ 85 - 0
LICENSE

@@ -0,0 +1,85 @@
+.header {
+  font-size: 3em;
+  color: white;
+  background: #404040;
+  text-align: center;
+  height: 2.5em;
+  text-shadow: 4px 4px 4px black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+#error {
+  font-size: 2em;
+  color: red;
+  height: 50px;
+  text-shadow: 2px 2px 2px black;
+  margin: 2em;
+  display: none;
+}
+
+.container {
+  width: 100% !important;
+  height: 100% !important;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  position: absolute;
+}
+
+.visible {
+  display: block;
+}
+
+#target {
+  width: 100% !important;
+  height: 100% !important;
+  position: absolute;
+}
+
+.guiRoot {
+  position: fixed;
+  top: 0px;
+  left: 0px;
+  cursor: pointer;
+  z-index: 10000;
+}
+
+.guiBox {
+  background: #0b1e2a;
+  mix-blend-mode: screen;
+  border-radius: 5px;
+  border-style: double;
+  border-color: #173869;
+  height: 80px;
+  width: 120px;
+}
+
+.guiBigText {
+  font-family: 'Teko', sans-serif;
+  font-size: 2em;
+  color: #e3f9ff;
+  mix-blend-mode: lighten;
+}
+
+.guiSmallText {
+  font-family: 'Teko', sans-serif;
+  font-size: 1em;
+  color: #e3f9ff;
+  mix-blend-mode: lighten;
+}
+
+.vertical {
+  display: flex;
+  flex-direction: column;
+  margin-left: 1em;
+}
+
+body {
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Spaceships Pew Pew</title>
+  <link href="https://fonts.googleapis.com/css2?family=Teko:wght@300&display=swap" rel="stylesheet">
+  <link rel="stylesheet" type="text/css" href="base.css">
+</head>
+<body>
+  <div id="target"></div>
+  <script src="./src/main.js" type="module">
+  </script>
+</body>
+</html>

+ 4 - 0
resources/README.txt

@@ -0,0 +1,4 @@
+Most of these textures were taken from freepbr.com or https://opengameart.org/content/36-free-ground-textures-diffuse-normals.
+
+They were all 2kx2k so they've been resaved as 1k.
+All models are free from https://sketchfab.com/

二進制
resources/blaster.jpg


二進制
resources/dirt_01_diffuse-1024.png


二進制
resources/dirt_01_normal-1024.jpg


二進制
resources/explosion.png


二進制
resources/grass1-albedo-512.jpg


二進制
resources/grass1-albedo3-1024.png


二進制
resources/grass1-normal-1024.jpg


二進制
resources/models/star-destroyer/scene.bin


+ 7498 - 0
resources/models/star-destroyer/scene.gltf

@@ -0,0 +1,7498 @@
+{
+  "accessors": [
+    {
+      "bufferView": 2,
+      "componentType": 5126,
+      "count": 3090,
+      "max": [
+        10.993896484375,
+        1.122859001159668,
+        6.3202352523803711
+      ],
+      "min": [
+        -8.6181268692016602,
+        -2.3041958808898926,
+        -6.3202352523803711
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 37080,
+      "componentType": 5126,
+      "count": 3090,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "componentType": 5126,
+      "count": 3090,
+      "max": [
+        1.000819206237793,
+        1.0000337362289429
+      ],
+      "min": [
+        0.00027027769829146564,
+        0.00027027769829146564
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "componentType": 5125,
+      "count": 4866,
+      "max": [
+        3089
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 74160,
+      "componentType": 5126,
+      "count": 1877,
+      "max": [
+        8.1145896911621094,
+        1.9492338895797729,
+        1.1069803237915039
+      ],
+      "min": [
+        -1.7829737663269043,
+        -1.4095573425292969,
+        -1.1069803237915039
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 96684,
+      "componentType": 5126,
+      "count": 1877,
+      "max": [
+        0.99976223707199097,
+        0.99768096208572388,
+        1
+      ],
+      "min": [
+        -0.9993550181388855,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 24720,
+      "componentType": 5126,
+      "count": 1877,
+      "max": [
+        0.99978876113891602,
+        1.0119031667709351
+      ],
+      "min": [
+        0.0031359978020191193,
+        0.00021124570048414171
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 19464,
+      "componentType": 5125,
+      "count": 3039,
+      "max": [
+        1876
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 119208,
+      "componentType": 5126,
+      "count": 786,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        1.0000003576278687
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 128640,
+      "componentType": 5126,
+      "count": 786,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 39736,
+      "componentType": 5126,
+      "count": 786,
+      "max": [
+        0.57578074932098389,
+        0.99989873170852661
+      ],
+      "min": [
+        0.00010130202281288803,
+        0.00010130202281288803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 31620,
+      "componentType": 5125,
+      "count": 3648,
+      "max": [
+        785
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 138072,
+      "componentType": 5126,
+      "count": 1568,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        8.7452621459960938
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 156888,
+      "componentType": 5126,
+      "count": 1568,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 46024,
+      "componentType": 5126,
+      "count": 1568,
+      "max": [
+        0.6706276535987854,
+        0.99991804361343384
+      ],
+      "min": [
+        8.1880163634195924e-05,
+        8.1880163634195924e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 46212,
+      "componentType": 5125,
+      "count": 7296,
+      "max": [
+        1567
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 175704,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.98982155323028564,
+        1.0000001192092896,
+        10.611758232116699
+      ],
+      "min": [
+        -0.9898216724395752,
+        -1.0000001192092896,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 179544,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.9898221492767334,
+        1,
+        1
+      ],
+      "min": [
+        -0.9898221492767334,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 58568,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.69948375225067139,
+        0.99990659952163696
+      ],
+      "min": [
+        9.3494010798167437e-05,
+        9.3494010798167437e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 75396,
+      "componentType": 5125,
+      "count": 1176,
+      "max": [
+        319
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 183384,
+      "componentType": 5126,
+      "count": 160,
+      "max": [
+        0.98982143402099609,
+        1,
+        1
+      ],
+      "min": [
+        -0.98982155323028564,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 185304,
+      "componentType": 5126,
+      "count": 160,
+      "max": [
+        0.9898221492767334,
+        1,
+        1
+      ],
+      "min": [
+        -0.9898221492767334,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 61128,
+      "componentType": 5126,
+      "count": 160,
+      "max": [
+        0.69948363304138184,
+        0.99990648031234741
+      ],
+      "min": [
+        9.3493996246252209e-05,
+        9.3493996246252209e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 80100,
+      "componentType": 5125,
+      "count": 588,
+      "max": [
+        159
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 187224,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        12.385623931884766
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 207720,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 62408,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        0.60674148797988892,
+        0.9998900294303894
+      ],
+      "min": [
+        0.0001101239540730603,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 82452,
+      "componentType": 5125,
+      "count": 7680,
+      "max": [
+        1707
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 228216,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.98982143402099609,
+        1,
+        13.267633438110352
+      ],
+      "min": [
+        -0.98982155323028564,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 232056,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.9898221492767334,
+        1,
+        1
+      ],
+      "min": [
+        -0.9898221492767334,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 76072,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.69948363304138184,
+        0.99990648031234741
+      ],
+      "min": [
+        9.3493996246252209e-05,
+        9.3493996246252209e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 113172,
+      "componentType": 5125,
+      "count": 1176,
+      "max": [
+        319
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 235896,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        12.385623931884766
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 256392,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 78632,
+      "componentType": 5126,
+      "count": 1708,
+      "max": [
+        0.60674148797988892,
+        0.9998900294303894
+      ],
+      "min": [
+        0.0001101239540730603,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 117876,
+      "componentType": 5125,
+      "count": 7680,
+      "max": [
+        1707
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 276888,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.98982143402099609,
+        1.0000002384185791,
+        13.267633438110352
+      ],
+      "min": [
+        -0.98982155323028564,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 280728,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.9898221492767334,
+        1,
+        1
+      ],
+      "min": [
+        -0.9898221492767334,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 92296,
+      "componentType": 5126,
+      "count": 320,
+      "max": [
+        0.69948363304138184,
+        0.99990648031234741
+      ],
+      "min": [
+        9.3493996246252209e-05,
+        9.3493996246252209e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 148596,
+      "componentType": 5125,
+      "count": 1176,
+      "max": [
+        319
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 284568,
+      "componentType": 5126,
+      "count": 48,
+      "max": [
+        3.0408148765563965,
+        0,
+        6.8863768577575684
+      ],
+      "min": [
+        -2.7250216007232666,
+        -2.0432300567626953,
+        -1.598746657371521
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 285144,
+      "componentType": 5126,
+      "count": 48,
+      "max": [
+        0.96980172395706177,
+        0.99998515844345093,
+        0.95780998468399048
+      ],
+      "min": [
+        -0.85419374704360962,
+        -1,
+        -0.99966657161712646
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 94856,
+      "componentType": 5126,
+      "count": 48,
+      "max": [
+        0.99489963054656982,
+        0.71928495168685913
+      ],
+      "min": [
+        -0.59492695331573486,
+        0.25827312469482422
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 153300,
+      "componentType": 5125,
+      "count": 72,
+      "max": [
+        47
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 285720,
+      "componentType": 5126,
+      "count": 120,
+      "max": [
+        1.0697997808456421,
+        1.3209954500198364,
+        1.0147562026977539
+      ],
+      "min": [
+        -1.2140761613845825,
+        -1.4447270631790161,
+        -1.0147562026977539
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 287160,
+      "componentType": 5126,
+      "count": 120,
+      "max": [
+        0.99991941452026367,
+        0.84551846981048584,
+        1
+      ],
+      "min": [
+        -0.86583554744720459,
+        -0.84929358959197998,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 95240,
+      "componentType": 5126,
+      "count": 120,
+      "max": [
+        0.99924743175506592,
+        0.90398275852203369
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 153588,
+      "componentType": 5125,
+      "count": 180,
+      "max": [
+        119
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 288600,
+      "componentType": 5126,
+      "count": 336,
+      "max": [
+        1.2132555246353149,
+        0.40549328923225403,
+        0.48390865325927734
+      ],
+      "min": [
+        -0.99367183446884155,
+        -19.057323455810547,
+        -16.516189575195312
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 292632,
+      "componentType": 5126,
+      "count": 336,
+      "max": [
+        0.99999189376831055,
+        0.99996656179428101,
+        0.99688130617141724
+      ],
+      "min": [
+        -0.9999726414680481,
+        -0.99996656179428101,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 96200,
+      "componentType": 5126,
+      "count": 336,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 154308,
+      "componentType": 5125,
+      "count": 504,
+      "max": [
+        335
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 296664,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        20.923713684082031
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 317976,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 98888,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        0.70138347148895264,
+        0.9998900294303894
+      ],
+      "min": [
+        0.0001101239540730603,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 156324,
+      "componentType": 5125,
+      "count": 7680,
+      "max": [
+        1775
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 339288,
+      "componentType": 5126,
+      "count": 854,
+      "max": [
+        1.1692826747894287,
+        1.0029010772705078,
+        1.0000003576278687
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 349536,
+      "componentType": 5126,
+      "count": 854,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 113096,
+      "componentType": 5126,
+      "count": 854,
+      "max": [
+        0.60674148797988892,
+        0.9998900294303894
+      ],
+      "min": [
+        0.017577648162841797,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 187044,
+      "componentType": 5125,
+      "count": 3840,
+      "max": [
+        853
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 359784,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1.1692831516265869,
+        1.002901554107666,
+        62.559841156005859
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 381096,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 119928,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        0.60674148797988892,
+        0.9998900294303894
+      ],
+      "min": [
+        0.017335770651698112,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 202404,
+      "componentType": 5125,
+      "count": 7680,
+      "max": [
+        1775
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 402408,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        3.3440821170806885,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -74.450851440429688,
+        -1.0000033378601074
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 403344,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 134136,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 233124,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 404280,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1.1692831516265869,
+        1.0029010772705078,
+        62.559841156005859
+      ],
+      "min": [
+        -0.26295268535614014,
+        -1.0029010772705078,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 425592,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 134760,
+      "componentType": 5126,
+      "count": 1776,
+      "max": [
+        0.60674148797988892,
+        0.9998900294303894
+      ],
+      "min": [
+        0.017335770651698112,
+        0.0001101239540730603
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 233604,
+      "componentType": 5125,
+      "count": 7680,
+      "max": [
+        1775
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 446904,
+      "componentType": 5126,
+      "count": 34,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 447312,
+      "componentType": 5126,
+      "count": 34,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -0.99197983741760254,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 148968,
+      "componentType": 5126,
+      "count": 34,
+      "max": [
+        0.875,
+        1
+      ],
+      "min": [
+        0.125,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 264324,
+      "componentType": 5125,
+      "count": 60,
+      "max": [
+        33
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 447720,
+      "componentType": 5126,
+      "count": 132,
+      "max": [
+        3.8688459396362305,
+        3.4965662956237793,
+        1.0000046491622925
+      ],
+      "min": [
+        -6.727564811706543,
+        -0.94613528251647949,
+        -1.0000061988830566
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 449304,
+      "componentType": 5126,
+      "count": 132,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 149240,
+      "componentType": 5126,
+      "count": 132,
+      "max": [
+        1.1240127086639404,
+        0.66790634393692017
+      ],
+      "min": [
+        -0.12401258945465088,
+        0.33209365606307983
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 264564,
+      "componentType": 5125,
+      "count": 198,
+      "max": [
+        131
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 450888,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.8817687034606934,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -16.334903717041016,
+        -1.0000064373016357
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 451824,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 150296,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.875,
+        1
+      ],
+      "min": [
+        0.125,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 265356,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 452760,
+      "componentType": 5126,
+      "count": 80,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.3292380571365356
+      ],
+      "min": [
+        -1.1028496026992798,
+        -0.9461357593536377,
+        -4.6994271278381348
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 453720,
+      "componentType": 5126,
+      "count": 80,
+      "max": [
+        0.99867904186248779,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867910146713257,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 150920,
+      "componentType": 5126,
+      "count": 80,
+      "max": [
+        0.875,
+        1
+      ],
+      "min": [
+        0.1249999925494194,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 265836,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        79
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 454680,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        8.1888408660888672,
+        1
+      ],
+      "min": [
+        -20.41261100769043,
+        -0.94613528251647949,
+        -1.0000039339065552
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 455616,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 151560,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.875,
+        1
+      ],
+      "min": [
+        0.125,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 266316,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 456552,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        2.8992624282836914,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -37.613254547119141,
+        -1.0000044107437134
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 457488,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 152184,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 266796,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 458424,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        2.2543642520904541,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -39.389755249023438,
+        -1.0000019073486328
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 459360,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 152808,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 267276,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 460296,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        4.1037168502807617,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -34.206123352050781,
+        -1.0000030994415283
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 461232,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99998509883880615,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 153432,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 267756,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 462168,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        4.1068506240844727,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -34.241718292236328,
+        -1.0000030994415283
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 463104,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99998509883880615,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 154056,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 268236,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 464040,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        3.4062883853912354,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -76.452255249023438,
+        -1.0000033378601074
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 464976,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 154680,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 268716,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 465912,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        2.5419878959655762,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -48.643943786621094,
+        -1.0000020265579224
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 466848,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 155304,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 269196,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 467784,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        2.5081110000610352,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -20.108104705810547,
+        -1.0000021457672119
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 468720,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.9999772310256958,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 155928,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 269676,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 469656,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        2.5022759437561035,
+        1,
+        1
+      ],
+      "min": [
+        -1.0437124967575073,
+        -47.366104125976562,
+        -1.0000022649765015
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 470592,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 156552,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 270156,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 471528,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.0139690637588501
+      ],
+      "min": [
+        -6.9581155776977539,
+        -3.3687677383422852,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 472464,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 157176,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.18320375680923462,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 270636,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 473400,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.034002423286438
+      ],
+      "min": [
+        -15.483651161193848,
+        -9.5168771743774414,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 474336,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 157800,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.045059442520141602,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 271116,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 475272,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.0244382619857788
+      ],
+      "min": [
+        -11.413389205932617,
+        -6.5816512107849121,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 476208,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 158424,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.18320375680923462,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 271596,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 477144,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.0104835033416748
+      ],
+      "min": [
+        -12.081388473510742,
+        -3.191619873046875,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 478080,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 159048,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.18320375680923462,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 272076,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 479016,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0437123775482178,
+        1,
+        1.0101351737976074
+      ],
+      "min": [
+        -11.718595504760742,
+        -3.0556142330169678,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 479952,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        0.99867868423461914,
+        1,
+        1
+      ],
+      "min": [
+        -0.99867868423461914,
+        -0.99995559453964233,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 159672,
+      "componentType": 5126,
+      "count": 78,
+      "max": [
+        1.0003663301467896,
+        0.958382248878479
+      ],
+      "min": [
+        -0.18320375680923462,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 272556,
+      "componentType": 5125,
+      "count": 120,
+      "max": [
+        77
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 480888,
+      "componentType": 5126,
+      "count": 915,
+      "max": [
+        0.60156762599945068,
+        0.41776180267333984,
+        0.99999994039535522
+      ],
+      "min": [
+        -1,
+        -0.41776180267333984,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 491868,
+      "componentType": 5126,
+      "count": 915,
+      "max": [
+        0.99372696876525879,
+        1,
+        0.99990302324295044
+      ],
+      "min": [
+        -1,
+        -1,
+        -0.99990302324295044
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 160296,
+      "componentType": 5126,
+      "count": 915,
+      "max": [
+        1.0406694412231445,
+        0.96392101049423218
+      ],
+      "min": [
+        0.012612477876245975,
+        0.016641978174448013
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 273036,
+      "componentType": 5125,
+      "count": 2730,
+      "max": [
+        914
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 502848,
+      "componentType": 5126,
+      "count": 1012,
+      "max": [
+        1.5721005201339722,
+        2.8940315246582031,
+        8.7775726318359375
+      ],
+      "min": [
+        -1.0000020265579224,
+        -4.7945513725280762,
+        -1.8207923173904419
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 514992,
+      "componentType": 5126,
+      "count": 1012,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 167616,
+      "componentType": 5126,
+      "count": 1012,
+      "max": [
+        0.89493244886398315,
+        0.99992793798446655
+      ],
+      "min": [
+        7.2194459789898247e-05,
+        0.00020612323714885861
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 283956,
+      "componentType": 5125,
+      "count": 3468,
+      "max": [
+        1011
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 527136,
+      "componentType": 5126,
+      "count": 72,
+      "max": [
+        2.2057473659515381,
+        0.078921690583229065,
+        1
+      ],
+      "min": [
+        -1.0016851425170898,
+        -0.21137754619121552,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 528000,
+      "componentType": 5126,
+      "count": 72,
+      "max": [
+        1,
+        0.98435753583908081,
+        1
+      ],
+      "min": [
+        -1,
+        -0.98435753583908081,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 175712,
+      "componentType": 5126,
+      "count": 72,
+      "max": [
+        0.53206419944763184,
+        0.9542202353477478
+      ],
+      "min": [
+        0.00012342211266513914,
+        0.00012342211266513914
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 297828,
+      "componentType": 5125,
+      "count": 108,
+      "max": [
+        71
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 528864,
+      "componentType": 5126,
+      "count": 108,
+      "max": [
+        2.489262580871582,
+        0,
+        10.535573959350586
+      ],
+      "min": [
+        -1,
+        -2.5568454265594482,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 530160,
+      "componentType": 5126,
+      "count": 108,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 176288,
+      "componentType": 5126,
+      "count": 108,
+      "max": [
+        0.61520057916641235,
+        0.99994516372680664
+      ],
+      "min": [
+        5.4783500672783703e-05,
+        0.031353689730167389
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 298260,
+      "componentType": 5125,
+      "count": 168,
+      "max": [
+        107
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 531456,
+      "componentType": 5126,
+      "count": 238,
+      "max": [
+        3.3449001312255859,
+        0.016933828592300415,
+        9.3602933883666992
+      ],
+      "min": [
+        -1,
+        -2.0485477447509766,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 534312,
+      "componentType": 5126,
+      "count": 238,
+      "max": [
+        1,
+        1,
+        0.99130237102508545
+      ],
+      "min": [
+        -1,
+        -1,
+        -0.92260938882827759
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 177152,
+      "componentType": 5126,
+      "count": 238,
+      "max": [
+        0.96648490428924561,
+        0.98904901742935181
+      ],
+      "min": [
+        6.3929604948498309e-05,
+        6.3929604948498309e-05
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 298932,
+      "componentType": 5125,
+      "count": 360,
+      "max": [
+        237
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 537168,
+      "componentType": 5126,
+      "count": 362,
+      "max": [
+        1.0612255334854126,
+        4.0255398750305176,
+        1.4142395257949829
+      ],
+      "min": [
+        -1.0612252950668335,
+        -4.4569025039672852,
+        -1.4142395257949829
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 541512,
+      "componentType": 5126,
+      "count": 362,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 179056,
+      "componentType": 5126,
+      "count": 362,
+      "max": [
+        0.875,
+        0.625
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 300372,
+      "componentType": 5125,
+      "count": 696,
+      "max": [
+        361
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 545856,
+      "componentType": 5126,
+      "count": 592,
+      "max": [
+        1.3551057577133179,
+        1.362985372543335,
+        17.205070495605469
+      ],
+      "min": [
+        -1.468503475189209,
+        -1.2682842016220093,
+        -1.3640221357345581
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 552960,
+      "componentType": 5126,
+      "count": 592,
+      "max": [
+        0.99658036231994629,
+        1,
+        0.99987292289733887
+      ],
+      "min": [
+        -0.99998033046722412,
+        -0.9975283145904541,
+        -0.9999845027923584
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 181952,
+      "componentType": 5126,
+      "count": 592,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.015244573354721069
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 303156,
+      "componentType": 5125,
+      "count": 1536,
+      "max": [
+        591
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 560064,
+      "componentType": 5126,
+      "count": 888,
+      "max": [
+        1.0948784351348877,
+        0.92706817388534546,
+        0.14945346117019653
+      ],
+      "min": [
+        -0.14051240682601929,
+        -0.92706817388534546,
+        -0.25009652972221375
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 570720,
+      "componentType": 5126,
+      "count": 888,
+      "max": [
+        0.99992096424102783,
+        1,
+        1
+      ],
+      "min": [
+        -0.99992096424102783,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 186688,
+      "componentType": 5126,
+      "count": 888,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 309300,
+      "componentType": 5125,
+      "count": 2328,
+      "max": [
+        887
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 581376,
+      "componentType": 5126,
+      "count": 480,
+      "max": [
+        0.95105832815170288,
+        1,
+        11.447335243225098
+      ],
+      "min": [
+        -0.95105785131454468,
+        -1.0000001192092896,
+        -0.99999994039535522
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 587136,
+      "componentType": 5126,
+      "count": 480,
+      "max": [
+        0.99207746982574463,
+        0.94352388381958008,
+        0.94352155923843384
+      ],
+      "min": [
+        -0.99207746982574463,
+        -0.94352394342422485,
+        -0.94352155923843384
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 193792,
+      "componentType": 5126,
+      "count": 480,
+      "max": [
+        1,
+        0.47238200902938843
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 318612,
+      "componentType": 5125,
+      "count": 480,
+      "max": [
+        479
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 592896,
+      "componentType": 5126,
+      "count": 328,
+      "max": [
+        1,
+        0.061526387929916382,
+        1.0160874128341675
+      ],
+      "min": [
+        -1,
+        -1.6672239303588867,
+        -1.0160874128341675
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 596832,
+      "componentType": 5126,
+      "count": 328,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 197632,
+      "componentType": 5126,
+      "count": 328,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        0,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 320532,
+      "componentType": 5125,
+      "count": 948,
+      "max": [
+        327
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 600768,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        3.1108009815216064,
+        1.4061223268508911,
+        38.185337066650391
+      ],
+      "min": [
+        -3.110797643661499,
+        -12.136653900146484,
+        -2.1927378177642822
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 614676,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        0.99750173091888428,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 200256,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        5.2154064178466797e-08,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 324324,
+      "componentType": 5125,
+      "count": 2148,
+      "max": [
+        1158
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 628584,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        3.1108009815216064,
+        1.4061223268508911,
+        38.185337066650391
+      ],
+      "min": [
+        -3.110797643661499,
+        -12.136653900146484,
+        -2.1927378177642822
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 642492,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        0.99750173091888428,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 209528,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        5.2154064178466797e-08,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 332916,
+      "componentType": 5125,
+      "count": 2148,
+      "max": [
+        1158
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 656400,
+      "componentType": 5126,
+      "count": 1153,
+      "max": [
+        3.1108009815216064,
+        1.4061223268508911,
+        38.185337066650391
+      ],
+      "min": [
+        -3.110797643661499,
+        -12.136654853820801,
+        -2.1927378177642822
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 670236,
+      "componentType": 5126,
+      "count": 1153,
+      "max": [
+        1,
+        0.9975016713142395,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 218800,
+      "componentType": 5126,
+      "count": 1153,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        5.2154064178466797e-08,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 341508,
+      "componentType": 5125,
+      "count": 2148,
+      "max": [
+        1152
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 684072,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        3.110795259475708,
+        1.4061223268508911,
+        38.185337066650391
+      ],
+      "min": [
+        -3.110797643661499,
+        -12.136653900146484,
+        -2.1927378177642822
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 697980,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        0.99750173091888428,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 228024,
+      "componentType": 5126,
+      "count": 1159,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        5.2154064178466797e-08,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 350100,
+      "componentType": 5125,
+      "count": 2148,
+      "max": [
+        1158
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 711888,
+      "componentType": 5126,
+      "count": 1163,
+      "max": [
+        3.1108086109161377,
+        1.4061223268508911,
+        20.548860549926758
+      ],
+      "min": [
+        -3.110797643661499,
+        -7.284764289855957,
+        -2.1927378177642822
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 725844,
+      "componentType": 5126,
+      "count": 1163,
+      "max": [
+        1,
+        0.9975016713142395,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 237296,
+      "componentType": 5126,
+      "count": 1163,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        5.2154064178466797e-08,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 358692,
+      "componentType": 5125,
+      "count": 2148,
+      "max": [
+        1162
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 739800,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98480778932571411,
+        59.404674530029297,
+        87.282310485839844
+      ],
+      "min": [
+        -32.181262969970703,
+        -1,
+        -1.0000066757202148
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 754776,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98481243848800659,
+        1,
+        1
+      ],
+      "min": [
+        -0.98481243848800659,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 246600,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.024473756551742554
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 367284,
+      "componentType": 5125,
+      "count": 3264,
+      "max": [
+        1247
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 769752,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98480796813964844,
+        59.404670715332031,
+        87.282302856445312
+      ],
+      "min": [
+        -32.181232452392578,
+        -1,
+        -1.0000066757202148
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 784728,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98481243848800659,
+        1,
+        1
+      ],
+      "min": [
+        -0.98481243848800659,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 256584,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.024473756551742554
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 380340,
+      "componentType": 5125,
+      "count": 3264,
+      "max": [
+        1247
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 799704,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98480796813964844,
+        59.404674530029297,
+        87.282295227050781
+      ],
+      "min": [
+        -32.181232452392578,
+        -1,
+        -1.0000066757202148
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 814680,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98481243848800659,
+        1,
+        1
+      ],
+      "min": [
+        -0.98481243848800659,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 266568,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.024473756551742554
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 393396,
+      "componentType": 5125,
+      "count": 3264,
+      "max": [
+        1247
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 829656,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98480796813964844,
+        59.404674530029297,
+        87.282302856445312
+      ],
+      "min": [
+        -32.181232452392578,
+        -1,
+        -1.0000066757202148
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 844632,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98481243848800659,
+        1,
+        1
+      ],
+      "min": [
+        -0.98481243848800659,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 276552,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.024473756551742554
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 406452,
+      "componentType": 5125,
+      "count": 3264,
+      "max": [
+        1247
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 859608,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98480796813964844,
+        32.641700744628906,
+        48.959812164306641
+      ],
+      "min": [
+        -32.181232452392578,
+        -1,
+        -1.0000066757202148
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 874584,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        0.98481243848800659,
+        1,
+        1
+      ],
+      "min": [
+        -0.98481243848800659,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 286536,
+      "componentType": 5126,
+      "count": 1248,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        4.4703483581542969e-08,
+        0.024473756551742554
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 419508,
+      "componentType": 5125,
+      "count": 3264,
+      "max": [
+        1247
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 889560,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        19.168619155883789
+      ],
+      "min": [
+        -1.0000019073486328,
+        -46.205730438232422,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 891528,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 296520,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        -1.5646219253540039e-07,
+        0.01000000536441803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 432564,
+      "componentType": 5125,
+      "count": 456,
+      "max": [
+        163
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 893496,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        19.168619155883789
+      ],
+      "min": [
+        -1,
+        -46.205738067626953,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 895464,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 297832,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        -1.5646219253540039e-07,
+        0.01000000536441803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 434388,
+      "componentType": 5125,
+      "count": 456,
+      "max": [
+        163
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 897432,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        19.168619155883789
+      ],
+      "min": [
+        -1,
+        -46.205730438232422,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 899400,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 299144,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        -1.5646219253540039e-07,
+        0.01000000536441803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 436212,
+      "componentType": 5125,
+      "count": 456,
+      "max": [
+        163
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 901368,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        19.168621063232422
+      ],
+      "min": [
+        -1.0000014305114746,
+        -46.205730438232422,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 903336,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 300456,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        -1.5646219253540039e-07,
+        0.01000000536441803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 438036,
+      "componentType": 5125,
+      "count": 456,
+      "max": [
+        163
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 905304,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        10.424897193908691
+      ],
+      "min": [
+        -1.0000038146972656,
+        -26.555355072021484,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 907272,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 301768,
+      "componentType": 5126,
+      "count": 164,
+      "max": [
+        1,
+        1
+      ],
+      "min": [
+        -1.5646219253540039e-07,
+        0.01000000536441803
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 439860,
+      "componentType": 5125,
+      "count": 456,
+      "max": [
+        163
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 909240,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.98993349075317383,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -0.98993313312530518,
+        -1,
+        -0.99270886182785034
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 915828,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.99573510885238647,
+        1,
+        0.98297816514968872
+      ],
+      "min": [
+        -0.99573510885238647,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 303080,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 441684,
+      "componentType": 5125,
+      "count": 2214,
+      "max": [
+        548
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 922416,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.98993349075317383,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -0.98993313312530518,
+        -1,
+        -0.99270886182785034
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 929004,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.99573510885238647,
+        1,
+        0.98297816514968872
+      ],
+      "min": [
+        -0.99573510885238647,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 307472,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 450540,
+      "componentType": 5125,
+      "count": 2214,
+      "max": [
+        548
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 935592,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.98993349075317383,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -0.98993313312530518,
+        -1,
+        -0.99270886182785034
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 942180,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        0.99573510885238647,
+        1,
+        0.98297816514968872
+      ],
+      "min": [
+        -0.99573510885238647,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 311864,
+      "componentType": 5126,
+      "count": 549,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 459396,
+      "componentType": 5125,
+      "count": 2214,
+      "max": [
+        548
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 948768,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        5.2558717727661133,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -0.98993313312530518,
+        -1.9023939371109009,
+        -5.6014018058776855
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 961944,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        0.99616628885269165,
+        1,
+        0.99591159820556641
+      ],
+      "min": [
+        -0.99573510885238647,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 316256,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 468252,
+      "componentType": 5125,
+      "count": 4428,
+      "max": [
+        1097
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 975120,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        13.09250545501709,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -0.98993313312530518,
+        -13.79865550994873,
+        -4.8468170166015625
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 988296,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        0.99819850921630859,
+        1,
+        0.99047261476516724
+      ],
+      "min": [
+        -0.99690014123916626,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 325040,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 485964,
+      "componentType": 5125,
+      "count": 4428,
+      "max": [
+        1097
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 1001472,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        0.98993349075317383,
+        2.5246820449829102,
+        0.98162180185317993
+      ],
+      "min": [
+        -29.493427276611328,
+        -65.940452575683594,
+        -13.386961936950684
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 1014648,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        0.99573510885238647,
+        1,
+        0.99929440021514893
+      ],
+      "min": [
+        -0.99814862012863159,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 333824,
+      "componentType": 5126,
+      "count": 1098,
+      "max": [
+        1.0000002384185791,
+        1
+      ],
+      "min": [
+        1.4901161193847656e-07,
+        0
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 503676,
+      "componentType": 5125,
+      "count": 4428,
+      "max": [
+        1097
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "extras": {
+      "author": "Todor (https://sketchfab.com/GoddHoward)",
+      "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
+      "source": "https://sketchfab.com/3d-models/star-destroyer-b5435fed6c3143f99b56f5de862a05bd",
+      "title": "Star Destroyer"
+    },
+    "generator": "Sketchfab-8.25.0",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 521388,
+      "byteOffset": 0,
+      "name": "floatBufferViews",
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteLength": 342608,
+      "byteOffset": 521388,
+      "byteStride": 8,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 1027824,
+      "byteOffset": 863996,
+      "byteStride": 12,
+      "name": "floatBufferViews",
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 1891820,
+      "uri": "scene.bin"
+    }
+  ],
+  "images": [
+    {
+      "uri": "textures/Body_baseColor.png"
+    },
+    {
+      "uri": "textures/Body_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/Body_emissive.png"
+    },
+    {
+      "uri": "textures/Body_Top_baseColor.png"
+    },
+    {
+      "uri": "textures/Body_Top_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/Body_Top_emissive.png"
+    },
+    {
+      "uri": "textures/Material.004_baseColor.jpeg"
+    },
+    {
+      "uri": "textures/Material.004_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/Engines_baseColor.png"
+    },
+    {
+      "uri": "textures/Engines_emissive.png"
+    },
+    {
+      "uri": "textures/Details_baseColor.png"
+    },
+    {
+      "uri": "textures/Details_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/Bridge_Thing_emissive.png"
+    },
+    {
+      "uri": "textures/Turret_baseColor.png"
+    },
+    {
+      "uri": "textures/Turret_metallicRoughness.png"
+    }
+  ],
+  "materials": [
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        1,
+        1,
+        1
+      ],
+      "emissiveTexture": {
+        "index": 2,
+        "texCoord": 0
+      },
+      "name": "Body",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 0,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 1,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        1,
+        1,
+        1
+      ],
+      "emissiveTexture": {
+        "index": 5,
+        "texCoord": 0
+      },
+      "name": "Body_Top",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 3,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 4,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.004",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 6,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 7,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Turret_Bed",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 6,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 7,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        1,
+        1,
+        1
+      ],
+      "emissiveTexture": {
+        "index": 2,
+        "texCoord": 0
+      },
+      "name": "Material.003",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 0,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 1,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        1,
+        1,
+        1
+      ],
+      "emissiveTexture": {
+        "index": 9,
+        "texCoord": 0
+      },
+      "name": "Engines",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 8,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "roughnessFactor": 0.5
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Details",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 10,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 11,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        1,
+        1,
+        1
+      ],
+      "emissiveTexture": {
+        "index": 12,
+        "texCoord": 0
+      },
+      "name": "Bridge_Thing",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 10,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 11,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Turret",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 13,
+          "texCoord": 0
+        },
+        "metallicFactor": 0,
+        "metallicRoughnessTexture": {
+          "index": 14,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 0,
+            "TEXCOORD_0": 2
+          },
+          "indices": 3,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 5,
+            "POSITION": 4,
+            "TEXCOORD_0": 6
+          },
+          "indices": 7,
+          "material": 1,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 9,
+            "POSITION": 8,
+            "TEXCOORD_0": 10
+          },
+          "indices": 11,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 13,
+            "POSITION": 12,
+            "TEXCOORD_0": 14
+          },
+          "indices": 15,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 17,
+            "POSITION": 16,
+            "TEXCOORD_0": 18
+          },
+          "indices": 19,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 21,
+            "POSITION": 20,
+            "TEXCOORD_0": 22
+          },
+          "indices": 23,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 25,
+            "POSITION": 24,
+            "TEXCOORD_0": 26
+          },
+          "indices": 27,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 29,
+            "POSITION": 28,
+            "TEXCOORD_0": 30
+          },
+          "indices": 31,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 33,
+            "POSITION": 32,
+            "TEXCOORD_0": 34
+          },
+          "indices": 35,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 37,
+            "POSITION": 36,
+            "TEXCOORD_0": 38
+          },
+          "indices": 39,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 41,
+            "POSITION": 40,
+            "TEXCOORD_0": 42
+          },
+          "indices": 43,
+          "material": 3,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 45,
+            "POSITION": 44,
+            "TEXCOORD_0": 46
+          },
+          "indices": 47,
+          "material": 4,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 49,
+            "POSITION": 48,
+            "TEXCOORD_0": 50
+          },
+          "indices": 51,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 53,
+            "POSITION": 52,
+            "TEXCOORD_0": 54
+          },
+          "indices": 55,
+          "material": 5,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 57,
+            "POSITION": 56,
+            "TEXCOORD_0": 58
+          },
+          "indices": 59,
+          "material": 5,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 61,
+            "POSITION": 60,
+            "TEXCOORD_0": 62
+          },
+          "indices": 63,
+          "material": 5,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 65,
+            "POSITION": 64,
+            "TEXCOORD_0": 66
+          },
+          "indices": 67,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 69,
+            "POSITION": 68,
+            "TEXCOORD_0": 70
+          },
+          "indices": 71,
+          "material": 5,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 73,
+            "POSITION": 72,
+            "TEXCOORD_0": 74
+          },
+          "indices": 75,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 77,
+            "POSITION": 76,
+            "TEXCOORD_0": 78
+          },
+          "indices": 79,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 81,
+            "POSITION": 80,
+            "TEXCOORD_0": 82
+          },
+          "indices": 83,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 85,
+            "POSITION": 84,
+            "TEXCOORD_0": 86
+          },
+          "indices": 87,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 89,
+            "POSITION": 88,
+            "TEXCOORD_0": 90
+          },
+          "indices": 91,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 93,
+            "POSITION": 92,
+            "TEXCOORD_0": 94
+          },
+          "indices": 95,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 97,
+            "POSITION": 96,
+            "TEXCOORD_0": 98
+          },
+          "indices": 99,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 101,
+            "POSITION": 100,
+            "TEXCOORD_0": 102
+          },
+          "indices": 103,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 105,
+            "POSITION": 104,
+            "TEXCOORD_0": 106
+          },
+          "indices": 107,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 109,
+            "POSITION": 108,
+            "TEXCOORD_0": 110
+          },
+          "indices": 111,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 113,
+            "POSITION": 112,
+            "TEXCOORD_0": 114
+          },
+          "indices": 115,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 117,
+            "POSITION": 116,
+            "TEXCOORD_0": 118
+          },
+          "indices": 119,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 121,
+            "POSITION": 120,
+            "TEXCOORD_0": 122
+          },
+          "indices": 123,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 125,
+            "POSITION": 124,
+            "TEXCOORD_0": 126
+          },
+          "indices": 127,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 129,
+            "POSITION": 128,
+            "TEXCOORD_0": 130
+          },
+          "indices": 131,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 133,
+            "POSITION": 132,
+            "TEXCOORD_0": 134
+          },
+          "indices": 135,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 137,
+            "POSITION": 136,
+            "TEXCOORD_0": 138
+          },
+          "indices": 139,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 141,
+            "POSITION": 140,
+            "TEXCOORD_0": 142
+          },
+          "indices": 143,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 145,
+            "POSITION": 144,
+            "TEXCOORD_0": 146
+          },
+          "indices": 147,
+          "material": 7,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 149,
+            "POSITION": 148,
+            "TEXCOORD_0": 150
+          },
+          "indices": 151,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 153,
+            "POSITION": 152,
+            "TEXCOORD_0": 154
+          },
+          "indices": 155,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 157,
+            "POSITION": 156,
+            "TEXCOORD_0": 158
+          },
+          "indices": 159,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 161,
+            "POSITION": 160,
+            "TEXCOORD_0": 162
+          },
+          "indices": 163,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 165,
+            "POSITION": 164,
+            "TEXCOORD_0": 166
+          },
+          "indices": 167,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 169,
+            "POSITION": 168,
+            "TEXCOORD_0": 170
+          },
+          "indices": 171,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 173,
+            "POSITION": 172,
+            "TEXCOORD_0": 174
+          },
+          "indices": 175,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 177,
+            "POSITION": 176,
+            "TEXCOORD_0": 178
+          },
+          "indices": 179,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 181,
+            "POSITION": 180,
+            "TEXCOORD_0": 182
+          },
+          "indices": 183,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 185,
+            "POSITION": 184,
+            "TEXCOORD_0": 186
+          },
+          "indices": 187,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 189,
+            "POSITION": 188,
+            "TEXCOORD_0": 190
+          },
+          "indices": 191,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 193,
+            "POSITION": 192,
+            "TEXCOORD_0": 194
+          },
+          "indices": 195,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 197,
+            "POSITION": 196,
+            "TEXCOORD_0": 198
+          },
+          "indices": 199,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 201,
+            "POSITION": 200,
+            "TEXCOORD_0": 202
+          },
+          "indices": 203,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 205,
+            "POSITION": 204,
+            "TEXCOORD_0": 206
+          },
+          "indices": 207,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 209,
+            "POSITION": 208,
+            "TEXCOORD_0": 210
+          },
+          "indices": 211,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 213,
+            "POSITION": 212,
+            "TEXCOORD_0": 214
+          },
+          "indices": 215,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 217,
+            "POSITION": 216,
+            "TEXCOORD_0": 218
+          },
+          "indices": 219,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 221,
+            "POSITION": 220,
+            "TEXCOORD_0": 222
+          },
+          "indices": 223,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 225,
+            "POSITION": 224,
+            "TEXCOORD_0": 226
+          },
+          "indices": 227,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 229,
+            "POSITION": 228,
+            "TEXCOORD_0": 230
+          },
+          "indices": 231,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 233,
+            "POSITION": 232,
+            "TEXCOORD_0": 234
+          },
+          "indices": 235,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 237,
+            "POSITION": 236,
+            "TEXCOORD_0": 238
+          },
+          "indices": 239,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 241,
+            "POSITION": 240,
+            "TEXCOORD_0": 242
+          },
+          "indices": 243,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 245,
+            "POSITION": 244,
+            "TEXCOORD_0": 246
+          },
+          "indices": 247,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 249,
+            "POSITION": 248,
+            "TEXCOORD_0": 250
+          },
+          "indices": 251,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 253,
+            "POSITION": 252,
+            "TEXCOORD_0": 254
+          },
+          "indices": 255,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 257,
+            "POSITION": 256,
+            "TEXCOORD_0": 258
+          },
+          "indices": 259,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 261,
+            "POSITION": 260,
+            "TEXCOORD_0": 262
+          },
+          "indices": 263,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 265,
+            "POSITION": 264,
+            "TEXCOORD_0": 266
+          },
+          "indices": 267,
+          "material": 8,
+          "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": "root"
+    },
+    {
+      "children": [
+        4,
+        6,
+        8,
+        10,
+        12,
+        14,
+        16,
+        18,
+        20,
+        22,
+        24,
+        26,
+        28,
+        30,
+        32,
+        34,
+        36,
+        38,
+        40,
+        42,
+        44,
+        46,
+        48,
+        50,
+        52,
+        54,
+        56,
+        58,
+        60,
+        62,
+        64,
+        66,
+        68,
+        70,
+        72,
+        74,
+        76,
+        78,
+        80,
+        82,
+        84,
+        86,
+        88,
+        90,
+        92,
+        94,
+        96,
+        98,
+        100,
+        102,
+        104,
+        106,
+        108,
+        110,
+        112,
+        114,
+        116,
+        118,
+        120,
+        122,
+        124,
+        126,
+        128,
+        130,
+        132,
+        134,
+        136
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        2.2204460492503131e-16,
+        1,
+        0,
+        0,
+        -1,
+        2.2204460492503131e-16,
+        0,
+        0,
+        0,
+        0,
+        1
+      ],
+      "name": "GLTF_SceneRootNode"
+    },
+    {
+      "children": [
+        5
+      ],
+      "name": "Body_0"
+    },
+    {
+      "mesh": 0,
+      "name": ""
+    },
+    {
+      "children": [
+        7
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        2.8749537467956543,
+        0,
+        -6.6335926055908203,
+        1.3362034559249878,
+        0,
+        1
+      ],
+      "name": "Body Top_1"
+    },
+    {
+      "mesh": 1,
+      "name": ""
+    },
+    {
+      "children": [
+        9
+      ],
+      "matrix": [
+        0.72296088933944702,
+        0,
+        0,
+        0,
+        0,
+        0.72296088933944702,
+        0,
+        0,
+        0,
+        0,
+        0.72296088933944702,
+        0,
+        -7.7796740531921387,
+        -0.6067313551902771,
+        0,
+        1
+      ],
+      "name": "Engine Center_2"
+    },
+    {
+      "mesh": 2,
+      "name": ""
+    },
+    {
+      "children": [
+        11
+      ],
+      "matrix": [
+        0.72296088933944702,
+        0,
+        0,
+        0,
+        0,
+        0.72296088933944702,
+        0,
+        0,
+        0,
+        0,
+        0.72296088933944702,
+        0,
+        -7.7796740531921387,
+        -0.6067313551902771,
+        -2.7997605800628662,
+        1
+      ],
+      "name": "Engines Side_3"
+    },
+    {
+      "mesh": 3,
+      "name": ""
+    },
+    {
+      "children": [
+        13
+      ],
+      "matrix": [
+        4.9148052779521034e-08,
+        -0.58305728435516158,
+        -0,
+        0,
+        0.58305728435516158,
+        4.9148052779521034e-08,
+        0,
+        0,
+        0,
+        -0,
+        0.58305728435516357,
+        0,
+        -6.5872435569763184,
+        -0.6094290018081665,
+        -2.80210280418396,
+        1
+      ],
+      "name": "Cylinder.001_4"
+    },
+    {
+      "mesh": 4,
+      "name": ""
+    },
+    {
+      "children": [
+        15
+      ],
+      "matrix": [
+        4.9148052779521034e-08,
+        -0.58305728435516158,
+        -0,
+        0,
+        0.58305728435516158,
+        4.9148052779521034e-08,
+        0,
+        0,
+        0,
+        -0,
+        0.58305728435516357,
+        0,
+        -6.5872435569763184,
+        -0.6094290018081665,
+        0,
+        1
+      ],
+      "name": "Cylinder.002_5"
+    },
+    {
+      "mesh": 5,
+      "name": ""
+    },
+    {
+      "children": [
+        17
+      ],
+      "matrix": [
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        -7.6839590072631836,
+        -0.26039281487464905,
+        -1.5853934288024902,
+        1
+      ],
+      "name": "Engines Top_6"
+    },
+    {
+      "mesh": 6,
+      "name": ""
+    },
+    {
+      "children": [
+        19
+      ],
+      "matrix": [
+        2.1799592699594047e-08,
+        -0.25861474871635348,
+        -0,
+        0,
+        0.25861474871635348,
+        2.1799592699594047e-08,
+        0,
+        0,
+        0,
+        -0,
+        0.25861474871635437,
+        0,
+        -7.2246246337890625,
+        -0.26143196225166321,
+        -1.5862956047058105,
+        1
+      ],
+      "name": "Cylinder.003_7"
+    },
+    {
+      "mesh": 7,
+      "name": ""
+    },
+    {
+      "children": [
+        21
+      ],
+      "matrix": [
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        -7.6839590072631836,
+        -0.91387021541595459,
+        -1.5853934288024902,
+        1
+      ],
+      "name": "Engines Bottom_8"
+    },
+    {
+      "mesh": 8,
+      "name": ""
+    },
+    {
+      "children": [
+        23
+      ],
+      "matrix": [
+        2.1799592699594047e-08,
+        -0.25861474871635348,
+        -0,
+        0,
+        0.25861474871635348,
+        2.1799592699594047e-08,
+        0,
+        0,
+        0,
+        -0,
+        0.25861474871635437,
+        0,
+        -7.2246246337890625,
+        -0.91490936279296875,
+        -1.5862956047058105,
+        1
+      ],
+      "name": "Cylinder.004_9"
+    },
+    {
+      "mesh": 9,
+      "name": ""
+    },
+    {
+      "children": [
+        25
+      ],
+      "matrix": [
+        0.99709055327899376,
+        -0.074395309567864554,
+        0.016606217994828719,
+        0,
+        0.075511472130267765,
+        0.97006216741336038,
+        -0.18810425597581312,
+        0,
+        -0.0021751661368687772,
+        0.19418307381057695,
+        1.0005377203307608,
+        0,
+        -4.2351341247558594,
+        0.31964609026908875,
+        -2.7979943752288818,
+        1
+      ],
+      "name": "Turret Bed_10"
+    },
+    {
+      "mesh": 10,
+      "name": ""
+    },
+    {
+      "children": [
+        27
+      ],
+      "matrix": [
+        0.71860713844353019,
+        -0.062869971286382956,
+        -0,
+        0,
+        0.030284333862698881,
+        0.34615155775418821,
+        0,
+        0,
+        0,
+        0,
+        1.6824305057525635,
+        0,
+        -5.5786843299865723,
+        2.978748083114624,
+        0,
+        1
+      ],
+      "name": "Bridge_11"
+    },
+    {
+      "mesh": 11,
+      "name": ""
+    },
+    {
+      "children": [
+        29
+      ],
+      "matrix": [
+        0.014400475259683168,
+        -0.17688142442289681,
+        -0.0034633509229756005,
+        0,
+        0.11108644575812324,
+        0.0090972451068926895,
+        -0.0027245460293437384,
+        0,
+        0.002892547928229342,
+        -0.0019464548003692269,
+        0.11143709405295155,
+        0,
+        2.4735407829284668,
+        -1.0964250564575195,
+        0.83889311552047729,
+        1
+      ],
+      "name": "Plane.001_12"
+    },
+    {
+      "mesh": 12,
+      "name": ""
+    },
+    {
+      "children": [
+        31
+      ],
+      "matrix": [
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        -7.2672309875488281,
+        -0.60674089193344116,
+        -2.7742810249328613,
+        1
+      ],
+      "name": "Engines Bottom.001_13"
+    },
+    {
+      "mesh": 13,
+      "name": ""
+    },
+    {
+      "children": [
+        33
+      ],
+      "matrix": [
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        0,
+        0,
+        0,
+        0.27849036455154419,
+        0,
+        -7.2672309875488281,
+        -0.60674089193344116,
+        0,
+        1
+      ],
+      "name": "Engines Bottom.002_14"
+    },
+    {
+      "mesh": 14,
+      "name": ""
+    },
+    {
+      "children": [
+        35
+      ],
+      "matrix": [
+        0.051507387310266495,
+        0,
+        0,
+        0,
+        0,
+        0.051507387310266495,
+        0,
+        0,
+        0,
+        0,
+        0.051507387310266495,
+        0,
+        -7.4701433181762695,
+        -0.26039281487464905,
+        -1.5853934288024902,
+        1
+      ],
+      "name": "Engines Top.001_15"
+    },
+    {
+      "mesh": 15,
+      "name": ""
+    },
+    {
+      "children": [
+        37
+      ],
+      "matrix": [
+        0.0019675809418364127,
+        0.022489507067065866,
+        0.00035078684760777543,
+        -0,
+        6.1132513019873866e-05,
+        0.00069898484506665439,
+        -0.045155934119155736,
+        0,
+        0.49424363136893396,
+        -0.043240799663868387,
+        -2.2828503405449336e-07,
+        0,
+        -6.060002326965332,
+        1.4624011516571045,
+        -1.6584311723709106,
+        1
+      ],
+      "name": "Cube.012_16"
+    },
+    {
+      "mesh": 16,
+      "name": ""
+    },
+    {
+      "children": [
+        39
+      ],
+      "matrix": [
+        0.051507387310266495,
+        0,
+        0,
+        0,
+        0,
+        0.051507387310266495,
+        0,
+        0,
+        0,
+        0,
+        0.051507387310266495,
+        0,
+        -7.4701433181762695,
+        -0.91598880290985107,
+        -1.5853934288024902,
+        1
+      ],
+      "name": "Engines Top.002_17"
+    },
+    {
+      "mesh": 17,
+      "name": ""
+    },
+    {
+      "children": [
+        41
+      ],
+      "matrix": [
+        -0.021231814586552412,
+        0.0018575428798349629,
+        0,
+        -0,
+        0.017230063255191105,
+        0.1969405456638986,
+        0,
+        0,
+        0,
+        0,
+        0.057840514928102493,
+        0,
+        -4.8231630325317383,
+        3.1352159976959229,
+        0,
+        1
+      ],
+      "name": "Cube_18"
+    },
+    {
+      "mesh": 18,
+      "name": ""
+    },
+    {
+      "children": [
+        43
+      ],
+      "matrix": [
+        -0.0034689251585906283,
+        -0.039650039346151508,
+        -0.0054590921864128544,
+        -0,
+        -0.0045376060525527161,
+        -0.051866210401844769,
+        0.37959386436484566,
+        0,
+        0.057620414952957444,
+        -0.005041125647794179,
+        -1.4067322984109592e-08,
+        0,
+        -4.8863797187805176,
+        2.7065653800964355,
+        -0.49422904849052429,
+        1
+      ],
+      "name": "Cube.003_19"
+    },
+    {
+      "mesh": 19,
+      "name": ""
+    },
+    {
+      "children": [
+        45
+      ],
+      "matrix": [
+        0.002360775152782808,
+        0.026983738271214312,
+        0.0004208911721150117,
+        -0,
+        0.00012018841574124108,
+        0.0013740810504058408,
+        -0.088767794927547342,
+        0,
+        0.057620409886854032,
+        -0.0050411408106934607,
+        -1.836568928039748e-08,
+        0,
+        -4.882288932800293,
+        2.9380886554718018,
+        -0.68038761615753174,
+        1
+      ],
+      "name": "Cube.004_20"
+    },
+    {
+      "mesh": 20,
+      "name": ""
+    },
+    {
+      "children": [
+        47
+      ],
+      "matrix": [
+        0.0085349972009017006,
+        -0.0008805335578512375,
+        -4.8643084180903693e-09,
+        -0,
+        0.0091106917070790886,
+        0.088309783908710679,
+        -2.5893338924696473e-09,
+        0,
+        -3.279071406688558e-08,
+        1.6869895323396104e-09,
+        -0.057840514928093154,
+        0,
+        -4.8061966896057129,
+        3.2369990348815918,
+        -0.097466707229614258,
+        1
+      ],
+      "name": "Cube.005_21"
+    },
+    {
+      "mesh": 21,
+      "name": ""
+    },
+    {
+      "children": [
+        49
+      ],
+      "matrix": [
+        1.2590300038246242e-09,
+        -0.022512478795792552,
+        -0.0030877675624746055,
+        -0,
+        2.2498862607306911e-08,
+        -0.048126835333980711,
+        0.3508859841433673,
+        0,
+        0.057840514928102285,
+        3.6743421710654416e-09,
+        -3.2047770195122415e-09,
+        0,
+        -4.9172654151916504,
+        2.6305530071258545,
+        -1.2976090908050537,
+        1
+      ],
+      "name": "Cube.006_22"
+    },
+    {
+      "mesh": 22,
+      "name": ""
+    },
+    {
+      "children": [
+        51
+      ],
+      "matrix": [
+        0.0024428226152747048,
+        0.027921533138318227,
+        0.00043551450731038838,
+        -0,
+        0.000120174645881888,
+        0.0013740684307445941,
+        -0.088767795141535336,
+        0,
+        0.43223824309359576,
+        -0.037816020461214607,
+        -1.996455103952971e-07,
+        0,
+        -3.5324151515960693,
+        1.4492390155792236,
+        -1.6250172853469849,
+        1
+      ],
+      "name": "Cube.007_23"
+    },
+    {
+      "mesh": 23,
+      "name": ""
+    },
+    {
+      "children": [
+        53
+      ],
+      "matrix": [
+        0.0038678817950257263,
+        0.044210000775174549,
+        0.00068957877815701305,
+        -0,
+        0.000120174645881888,
+        0.0013740684307445941,
+        -0.088767795141535336,
+        0,
+        0.97158696656679078,
+        -0.085003012099471434,
+        -4.4876402986776668e-07,
+        0,
+        -4.3190522193908691,
+        1.4077367782592773,
+        -1.7036296129226685,
+        1
+      ],
+      "name": "Cube.008_24"
+    },
+    {
+      "mesh": 24,
+      "name": ""
+    },
+    {
+      "children": [
+        55
+      ],
+      "matrix": [
+        0.0013632098633479105,
+        0.015581528161704293,
+        0.00024303756985230937,
+        -0,
+        0.000120174645881888,
+        0.0013740684307445941,
+        -0.088767795141535336,
+        0,
+        0.43223824309359576,
+        -0.037816020461214607,
+        -1.996455103952971e-07,
+        0,
+        -1.8060562610626221,
+        1.3519692420959473,
+        -1.4739513397216797,
+        1
+      ],
+      "name": "Cube.009_25"
+    },
+    {
+      "mesh": 25,
+      "name": ""
+    },
+    {
+      "children": [
+        57
+      ],
+      "matrix": [
+        0.0013632098633479105,
+        0.015581528161704293,
+        0.00024303756985230937,
+        -0,
+        0.000120174645881888,
+        0.0013740684307445941,
+        -0.088767795141535336,
+        0,
+        0.43223824309359576,
+        -0.037816020461214607,
+        -1.996455103952971e-07,
+        0,
+        -1.8149198293685913,
+        1.2506577968597412,
+        -1.4755315780639648,
+        1
+      ],
+      "name": "Cube.010_26"
+    },
+    {
+      "mesh": 26,
+      "name": ""
+    },
+    {
+      "children": [
+        59
+      ],
+      "matrix": [
+        0.0019675809418364127,
+        0.022489507067065866,
+        0.00035078684760777543,
+        -0,
+        6.1132513019873866e-05,
+        0.00069898484506665439,
+        -0.045155934119155736,
+        0,
+        0.49424363136893396,
+        -0.043240799663868387,
+        -2.2828503405449336e-07,
+        0,
+        -5.6416358947753906,
+        1.7153143882751465,
+        -1.7036296129226685,
+        1
+      ],
+      "name": "Cube.011_27"
+    },
+    {
+      "mesh": 27,
+      "name": ""
+    },
+    {
+      "children": [
+        61
+      ],
+      "matrix": [
+        0.0020869073916560347,
+        0.044329781089022607,
+        0.00068958186642024369,
+        -0,
+        6.4824373696064939e-05,
+        0.0013777956448339102,
+        -0.088767795045551948,
+        0,
+        0.97421933532549976,
+        -0.045863191629133403,
+        -4.1622561105609256e-07,
+        0,
+        -3.6093587875366211,
+        0.89719510078430176,
+        -2.1144657135009766,
+        1
+      ],
+      "name": "Cube.013_28"
+    },
+    {
+      "mesh": 28,
+      "name": ""
+    },
+    {
+      "children": [
+        63
+      ],
+      "matrix": [
+        0.0020869073916560347,
+        0.044329781089022607,
+        0.00068958186642024369,
+        -0,
+        0.0001491116785210516,
+        0.0031692619542061273,
+        -0.204187316639797,
+        0,
+        0.97421927578679424,
+        -0.045863188826237834,
+        -4.162255856187673e-07,
+        0,
+        -1.976670503616333,
+        0.58968186378479004,
+        -1.9506465196609497,
+        1
+      ],
+      "name": "Cube.014_29"
+    },
+    {
+      "mesh": 29,
+      "name": ""
+    },
+    {
+      "children": [
+        65
+      ],
+      "matrix": [
+        0.0020869073916560347,
+        0.044329781089022607,
+        0.00068958186642024369,
+        -0,
+        6.4824373696064939e-05,
+        0.0013777956448339102,
+        -0.088767795045551948,
+        0,
+        0.97421933532549976,
+        -0.045863191629133403,
+        -4.1622561105609256e-07,
+        0,
+        -3.0278692245483398,
+        0.52021026611328125,
+        -2.0577366352081299,
+        1
+      ],
+      "name": "Cube.015_30"
+    },
+    {
+      "mesh": 30,
+      "name": ""
+    },
+    {
+      "children": [
+        67
+      ],
+      "matrix": [
+        0.0020869078096887512,
+        0.043776056101458828,
+        -0.0070186839940475651,
+        -0,
+        0.00028062342939219224,
+        -0.060851127623134472,
+        -0.37944958680282975,
+        0,
+        0.97421927532399832,
+        -0.045166497984124844,
+        0.0079636958168927997,
+        0,
+        -1.9275661706924438,
+        1.0597057342529297,
+        -0.45574837923049927,
+        1
+      ],
+      "name": "Cube.016_31"
+    },
+    {
+      "mesh": 31,
+      "name": ""
+    },
+    {
+      "children": [
+        69
+      ],
+      "matrix": [
+        0.0015230584948637935,
+        0.031948461646183014,
+        -0.0051223471541337554,
+        -0,
+        0.00020480343961855232,
+        -0.044410120241486099,
+        -0.27692834025789353,
+        0,
+        0.71100080173043889,
+        -0.032963232294278909,
+        0.0058120325207741694,
+        0,
+        -1.6876404285430908,
+        0.97333317995071411,
+        -1.205798864364624,
+        1
+      ],
+      "name": "Cube.017_32"
+    },
+    {
+      "mesh": 32,
+      "name": ""
+    },
+    {
+      "children": [
+        71
+      ],
+      "matrix": [
+        0.0020869078096887512,
+        0.043776056101458828,
+        -0.0070186839940475651,
+        -0,
+        0.00028062342939219224,
+        -0.060851127623134472,
+        -0.37944958680282975,
+        0,
+        0.97421927532399832,
+        -0.045166497984124844,
+        0.0079636958168927997,
+        0,
+        -2.1690521240234375,
+        1.2942943572998047,
+        -1.080988883972168,
+        1
+      ],
+      "name": "Cube.018_33"
+    },
+    {
+      "mesh": 33,
+      "name": ""
+    },
+    {
+      "children": [
+        73
+      ],
+      "matrix": [
+        0.00085387818237788577,
+        0.017911389780634786,
+        -0.0028717613225166506,
+        -0,
+        0.0002208774436279292,
+        -0.047895649840735528,
+        -0.29866306922487829,
+        0,
+        0.9742195134788203,
+        -0.045166509025396102,
+        0.0079636977636748096,
+        0,
+        -2.1670100688934326,
+        1.6040545701980591,
+        -0.33254051208496094,
+        1
+      ],
+      "name": "Cube.019_34"
+    },
+    {
+      "mesh": 34,
+      "name": ""
+    },
+    {
+      "children": [
+        75
+      ],
+      "matrix": [
+        0.00085387818237788577,
+        0.017911389780634786,
+        -0.0028717613225166506,
+        -0,
+        0.0002208774436279292,
+        -0.047895649840735528,
+        -0.29866306922487829,
+        0,
+        0.9742195134788203,
+        -0.045166509025396102,
+        0.0079636977636748096,
+        0,
+        -2.1364748477935791,
+        1.461487889289856,
+        -0.31170830130577087,
+        1
+      ],
+      "name": "Cube.020_35"
+    },
+    {
+      "mesh": 35,
+      "name": ""
+    },
+    {
+      "children": [
+        77
+      ],
+      "matrix": [
+        -0.12801064734310014,
+        0.01119947923178761,
+        7.2206729634641648e-08,
+        0,
+        0.01119947923178761,
+        0.12801064734312048,
+        -3.1526135576912037e-09,
+        0,
+        -7.2206729634641648e-08,
+        3.1526135576912037e-09,
+        -0.12849962711332194,
+        0,
+        -4.8527631759643555,
+        2.9136266708374023,
+        0,
+        1
+      ],
+      "name": "Cylinder.006_36"
+    },
+    {
+      "mesh": 36,
+      "name": ""
+    },
+    {
+      "children": [
+        79
+      ],
+      "matrix": [
+        0.058224888724718045,
+        -0.12486369996998495,
+        -0,
+        0,
+        0.21166853927564097,
+        0.098702642551921585,
+        0,
+        0,
+        0,
+        -0,
+        0.1377718448638916,
+        0,
+        -7.3072075843811035,
+        -1.4836986064910889,
+        -0.47922417521476746,
+        1
+      ],
+      "name": "Cylinder.008_37"
+    },
+    {
+      "mesh": 37,
+      "name": ""
+    },
+    {
+      "children": [
+        81
+      ],
+      "matrix": [
+        0.28523325783551667,
+        -0.024954673799764356,
+        -0,
+        0,
+        0.024954673799764356,
+        0.28523325783551667,
+        0,
+        0,
+        0,
+        0,
+        0.28632274270057678,
+        0,
+        -3.3921537399291992,
+        2.0293593406677246,
+        0,
+        1
+      ],
+      "name": "Plane.003_38"
+    },
+    {
+      "mesh": 38,
+      "name": ""
+    },
+    {
+      "children": [
+        83
+      ],
+      "matrix": [
+        0.16172886835857606,
+        -0.014149440571790952,
+        -1.8632590490316401e-09,
+        0,
+        0.01393447844783869,
+        0.15927184340816511,
+        -0.028191199633785094,
+        0,
+        0.0024570264307423274,
+        0.028083923477864738,
+        0.1598802357774354,
+        0,
+        -1.8051602840423584,
+        1.664212703704834,
+        -0.79094594717025757,
+        1
+      ],
+      "name": "Plane.004_39"
+    },
+    {
+      "mesh": 39,
+      "name": ""
+    },
+    {
+      "children": [
+        85
+      ],
+      "matrix": [
+        0.61913024563227836,
+        -0.01356198477381493,
+        0.16941008653120124,
+        0,
+        0.050072610561853213,
+        0.62613396712934766,
+        -0.13287194882512074,
+        0,
+        -0.13827077781737257,
+        0.1203376497089016,
+        0.51496129771964572,
+        0,
+        1.0105128288269043,
+        -0.0064834579825401306,
+        -2.3211956024169922,
+        1
+      ],
+      "name": "Plane.002_40"
+    },
+    {
+      "mesh": 40,
+      "name": ""
+    },
+    {
+      "children": [
+        87
+      ],
+      "matrix": [
+        0.27952108078938681,
+        -0.024454923118674868,
+        -0,
+        0,
+        0.0029570427231445534,
+        0.033799156672979364,
+        0,
+        0,
+        0,
+        0,
+        0.37728410959243774,
+        0,
+        -5.1847968101501465,
+        3.4441909790039062,
+        0,
+        1
+      ],
+      "name": "Cube.002_41"
+    },
+    {
+      "mesh": 41,
+      "name": ""
+    },
+    {
+      "children": [
+        89
+      ],
+      "matrix": [
+        0.15468640518005736,
+        5.111039724178181e-05,
+        -0.001175050825342121,
+        0,
+        -5.0645883424353589e-05,
+        0.15212511833197193,
+        -5.0256903282603854e-05,
+        0,
+        0.0011750338109846963,
+        5.1494259565217205e-05,
+        0.15468640518199392,
+        0,
+        -5.183934211730957,
+        3.3443665504455566,
+        -1.2253230810165405,
+        1
+      ],
+      "name": "Cylinder_42"
+    },
+    {
+      "mesh": 42,
+      "name": ""
+    },
+    {
+      "children": [
+        91
+      ],
+      "matrix": [
+        0.46953588944142693,
+        -0.09135723790556112,
+        -1.3902048976784683e-08,
+        0,
+        8.2027577630462533e-09,
+        -5.4683341805498773e-08,
+        0.63639599084853882,
+        0,
+        -0.091357237905561967,
+        -0.46953588944142521,
+        -3.9168081807090923e-08,
+        0,
+        -5.3930978775024414,
+        3.6967334747314453,
+        0,
+        1
+      ],
+      "name": "Cylinder.005_43"
+    },
+    {
+      "mesh": 43,
+      "name": ""
+    },
+    {
+      "children": [
+        93
+      ],
+      "matrix": [
+        0.23607814311981201,
+        0,
+        0,
+        0,
+        0,
+        0.23607814311981201,
+        0,
+        0,
+        0,
+        0,
+        0.23607814311981201,
+        0,
+        -5.1811957359313965,
+        3.6219244003295898,
+        -1.2331937551498413,
+        1
+      ],
+      "name": "Icosphere_44"
+    },
+    {
+      "mesh": 44,
+      "name": ""
+    },
+    {
+      "children": [
+        95
+      ],
+      "matrix": [
+        0.20138910308519609,
+        -0.017619261556155067,
+        -0,
+        0,
+        0.01237360800320644,
+        0.14143100207414944,
+        0,
+        0,
+        0,
+        0,
+        0.36067813634872437,
+        0,
+        -5.155787467956543,
+        3.7406558990478516,
+        0,
+        1
+      ],
+      "name": "Plane_45"
+    },
+    {
+      "mesh": 45,
+      "name": ""
+    },
+    {
+      "children": [
+        97
+      ],
+      "matrix": [
+        0.072498364967646931,
+        -0.0063427841743497705,
+        -8.4218279411271471e-10,
+        0,
+        0.0075573899606495316,
+        0.086381376598355639,
+        -0.015289551520522651,
+        0,
+        0.0020790794030800122,
+        0.023763971669477659,
+        0.13528696360292758,
+        0,
+        -2.4471092224121094,
+        0.33722633123397827,
+        -2.5323588848114014,
+        1
+      ],
+      "name": "Turret Barrel Holder_46"
+    },
+    {
+      "mesh": 46,
+      "name": ""
+    },
+    {
+      "children": [
+        99
+      ],
+      "matrix": [
+        0.072498364967646931,
+        -0.0063427841743497705,
+        -8.4218279411271471e-10,
+        0,
+        0.0075573899606495316,
+        0.086381376598355639,
+        -0.015289551520522651,
+        0,
+        0.0020790794030800122,
+        0.023763971669477659,
+        0.13528696360292758,
+        0,
+        -3.1562430858612061,
+        0.39926749467849731,
+        -2.5323588848114014,
+        1
+      ],
+      "name": "Turret Barrel Holder.001_47"
+    },
+    {
+      "mesh": 47,
+      "name": ""
+    },
+    {
+      "children": [
+        101
+      ],
+      "matrix": [
+        0.072498364967646931,
+        -0.0063427841743497705,
+        -8.4218279411271471e-10,
+        0,
+        0.0075573899606495316,
+        0.086381376598355639,
+        -0.015289551520522651,
+        0,
+        0.0020790794030800122,
+        0.023763971669477659,
+        0.13528696360292758,
+        0,
+        -3.8647615909576416,
+        0.46125480532646179,
+        -2.5323588848114014,
+        1
+      ],
+      "name": "Turret Barrel Holder.002_48"
+    },
+    {
+      "mesh": 48,
+      "name": ""
+    },
+    {
+      "children": [
+        103
+      ],
+      "matrix": [
+        0.072498364967646931,
+        -0.0063427841743497705,
+        -8.4218279411271471e-10,
+        0,
+        0.0075573899606495316,
+        0.086381376598355639,
+        -0.015289551520522651,
+        0,
+        0.0020790794030800122,
+        0.023763971669477659,
+        0.13528696360292758,
+        0,
+        -4.5587210655212402,
+        0.52196842432022095,
+        -2.5323588848114014,
+        1
+      ],
+      "name": "Turret Barrel Holder.003_49"
+    },
+    {
+      "mesh": 49,
+      "name": ""
+    },
+    {
+      "children": [
+        105
+      ],
+      "matrix": [
+        0.072498364967646931,
+        -0.0063427841743497705,
+        -8.4218279411271471e-10,
+        0,
+        0.0075573899606495316,
+        0.086381376598355639,
+        -0.015289551520522651,
+        0,
+        0.0020790794030800122,
+        0.023763971669477659,
+        0.13528696360292758,
+        0,
+        4.5585131645202637,
+        0.0089014768600463867,
+        -1.3022745847702026,
+        1
+      ],
+      "name": "Turret Barrel Holder.004_50"
+    },
+    {
+      "mesh": 50,
+      "name": ""
+    },
+    {
+      "children": [
+        107
+      ],
+      "matrix": [
+        0.012665935261023343,
+        -0.0011081256611223508,
+        -1.5786278666633354e-10,
+        0,
+        -0.0015553172406266487,
+        -0.017777371388004864,
+        0.088318597189980433,
+        -0,
+        0.0010861751206613884,
+        0.012415039057015593,
+        0.0025181118595697834,
+        0,
+        -2.249732494354248,
+        0.31777825951576233,
+        -2.685492992401123,
+        1
+      ],
+      "name": "Turret Barrels_51"
+    },
+    {
+      "mesh": 51,
+      "name": ""
+    },
+    {
+      "children": [
+        109
+      ],
+      "matrix": [
+        0.012665935261023343,
+        -0.0011081256611223508,
+        -1.5786278666633354e-10,
+        0,
+        -0.0015553172406266487,
+        -0.017777371388004864,
+        0.088318597189980433,
+        -0,
+        0.0010861751206613884,
+        0.012415039057015593,
+        0.0025181118595697834,
+        0,
+        -2.9588663578033447,
+        0.37981942296028137,
+        -2.685492992401123,
+        1
+      ],
+      "name": "Turret Barrels.001_52"
+    },
+    {
+      "mesh": 52,
+      "name": ""
+    },
+    {
+      "children": [
+        111
+      ],
+      "matrix": [
+        0.012665935261023343,
+        -0.0011081256611223508,
+        -1.5786278666633354e-10,
+        0,
+        -0.0015553172406266487,
+        -0.017777371388004864,
+        0.088318597189980433,
+        -0,
+        0.0010861751206613884,
+        0.012415039057015593,
+        0.0025181118595697834,
+        0,
+        -3.6673848628997803,
+        0.44180673360824585,
+        -2.685492992401123,
+        1
+      ],
+      "name": "Turret Barrels.002_53"
+    },
+    {
+      "mesh": 53,
+      "name": ""
+    },
+    {
+      "children": [
+        113
+      ],
+      "matrix": [
+        0.012665935261023343,
+        -0.0011081256611223508,
+        -1.5786278666633354e-10,
+        0,
+        -0.0015553172406266487,
+        -0.017777371388004864,
+        0.088318597189980433,
+        -0,
+        0.0010861751206613884,
+        0.012415039057015593,
+        0.0025181118595697834,
+        0,
+        -4.3613443374633789,
+        0.50252032279968262,
+        -2.685492992401123,
+        1
+      ],
+      "name": "Turret Barrels.003_54"
+    },
+    {
+      "mesh": 54,
+      "name": ""
+    },
+    {
+      "children": [
+        115
+      ],
+      "matrix": [
+        0.012665935261023343,
+        -0.0011081256611223508,
+        -1.5786278666633354e-10,
+        0,
+        -0.0015553172406266487,
+        -0.017777371388004864,
+        0.088318597189980433,
+        -0,
+        0.0010861751206613884,
+        0.012415039057015593,
+        0.0025181118595697834,
+        0,
+        4.755889892578125,
+        -0.010546594858169556,
+        -1.4554086923599243,
+        1
+      ],
+      "name": "Turret Barrels.004_55"
+    },
+    {
+      "mesh": 55,
+      "name": ""
+    },
+    {
+      "children": [
+        117
+      ],
+      "matrix": [
+        0.27603503807485757,
+        -0.024149933199853094,
+        -3.2065821035086228e-09,
+        0,
+        0.0018659998281888312,
+        0.021328479108598603,
+        -0.0037751526200094972,
+        0,
+        0.0041935957990746203,
+        0.047932989771730508,
+        0.27287983392761478,
+        0,
+        -2.456472635269165,
+        0.23020294308662415,
+        -2.560706615447998,
+        1
+      ],
+      "name": "Turret Base_56"
+    },
+    {
+      "mesh": 56,
+      "name": ""
+    },
+    {
+      "children": [
+        119
+      ],
+      "matrix": [
+        0.27603503807485757,
+        -0.024149933199853094,
+        -3.2065821035086228e-09,
+        0,
+        0.0018659998281888312,
+        0.021328479108598603,
+        -0.0037751526200094972,
+        0,
+        0.0041935957990746203,
+        0.047932989771730508,
+        0.27287983392761478,
+        0,
+        -3.1656064987182617,
+        0.29224410653114319,
+        -2.560706615447998,
+        1
+      ],
+      "name": "Turret Base.001_57"
+    },
+    {
+      "mesh": 57,
+      "name": ""
+    },
+    {
+      "children": [
+        121
+      ],
+      "matrix": [
+        0.27603503807485757,
+        -0.024149933199853094,
+        -3.2065821035086228e-09,
+        0,
+        0.0018659998281888312,
+        0.021328479108598603,
+        -0.0037751526200094972,
+        0,
+        0.0041935957990746203,
+        0.047932989771730508,
+        0.27287983392761478,
+        0,
+        -3.8741252422332764,
+        0.35423141717910767,
+        -2.560706615447998,
+        1
+      ],
+      "name": "Turret Base.002_58"
+    },
+    {
+      "mesh": 58,
+      "name": ""
+    },
+    {
+      "children": [
+        123
+      ],
+      "matrix": [
+        0.27603503807485757,
+        -0.024149933199853094,
+        -3.2065821035086228e-09,
+        0,
+        0.0018659998281888312,
+        0.021328479108598603,
+        -0.0037751526200094972,
+        0,
+        0.0041935957990746203,
+        0.047932989771730508,
+        0.27287983392761478,
+        0,
+        -4.568084716796875,
+        0.41494500637054443,
+        -2.560706615447998,
+        1
+      ],
+      "name": "Turret Base.003_59"
+    },
+    {
+      "mesh": 59,
+      "name": ""
+    },
+    {
+      "children": [
+        125
+      ],
+      "matrix": [
+        0.27603503807485757,
+        -0.024149933199853094,
+        -3.2065821035086228e-09,
+        0,
+        0.0018659998281888312,
+        0.021328479108598603,
+        -0.0037751526200094972,
+        0,
+        0.0041935957990746203,
+        0.047932989771730508,
+        0.27287983392761478,
+        0,
+        4.5491495132446289,
+        -0.098121911287307739,
+        -1.3306223154067993,
+        1
+      ],
+      "name": "Turret Base.004_60"
+    },
+    {
+      "mesh": 60,
+      "name": ""
+    },
+    {
+      "children": [
+        127
+      ],
+      "matrix": [
+        0.12366494536399841,
+        0,
+        0,
+        0,
+        0,
+        0.073536344386032904,
+        0.099425473427183039,
+        0,
+        0,
+        -0.099425473427183039,
+        0.073536344386032904,
+        0,
+        0.93107414245605469,
+        0.65293723344802856,
+        0,
+        1
+      ],
+      "name": "Turret Front 2_61"
+    },
+    {
+      "mesh": 61,
+      "name": ""
+    },
+    {
+      "children": [
+        129
+      ],
+      "matrix": [
+        0.091497006335003198,
+        -1.0611391537398267e-09,
+        0.083194450197192238,
+        0,
+        0.060717885880542749,
+        0.084540776702524092,
+        -0.066777347361302558,
+        0,
+        -0.056874026390000637,
+        0.090254505629440304,
+        0.062549884473558803,
+        0,
+        1.2723963260650635,
+        0.62001651525497437,
+        0,
+        1
+      ],
+      "name": "Turret Front 2.001_62"
+    },
+    {
+      "mesh": 62,
+      "name": ""
+    },
+    {
+      "children": [
+        131
+      ],
+      "matrix": [
+        0.09132807590187339,
+        -0.083379861261232424,
+        3.3708505134205727e-12,
+        0,
+        0.083377788139403219,
+        0.091325805158940412,
+        -0.00087204869861630765,
+        0,
+        0.00058797017199821802,
+        0.00064401864072306304,
+        0.12366187061074223,
+        0,
+        0.59605741500854492,
+        0.68004840612411499,
+        0,
+        1
+      ],
+      "name": "Turret Front 2.002_63"
+    },
+    {
+      "mesh": 63,
+      "name": ""
+    },
+    {
+      "children": [
+        133
+      ],
+      "matrix": [
+        0.02565578932345321,
+        0.087981156419715212,
+        0.083030809348824841,
+        0,
+        0.12097437408834585,
+        -0.018658666314264695,
+        -0.017608909596752223,
+        0,
+        -6.5209824857233298e-08,
+        0.084877494062343989,
+        -0.089937921443477994,
+        0,
+        3.6019797325134277,
+        -1.3912484645843506,
+        -0.39227783679962158,
+        1
+      ],
+      "name": "Turret Front 2.003_64"
+    },
+    {
+      "mesh": 64,
+      "name": ""
+    },
+    {
+      "children": [
+        135
+      ],
+      "matrix": [
+        0.096600838456748328,
+        1.2838423612554585e-09,
+        0.07720943414722016,
+        0,
+        0.073944876729306594,
+        -0.03557940521809845,
+        -0.092516376073075376,
+        0,
+        0.022213777860183494,
+        0.11843616270466997,
+        -0.027792843482667869,
+        0,
+        -0.44477462768554688,
+        -1.5284135341644287,
+        -1.0584101676940918,
+        1
+      ],
+      "name": "Turret Front 2.004_65"
+    },
+    {
+      "mesh": 65,
+      "name": ""
+    },
+    {
+      "children": [
+        137
+      ],
+      "matrix": [
+        0.11392551944147788,
+        6.9365311805772969e-10,
+        -0.04810399912554212,
+        0,
+        -0.047232363878173575,
+        -0.023434844660446038,
+        -0.1118612111953973,
+        0,
+        -0.0091158397501251414,
+        0.12142416055967394,
+        -0.021589196601756063,
+        0,
+        -5.0203056335449219,
+        -1.1297869682312012,
+        -4.390777587890625,
+        1
+      ],
+      "name": "Turret Front 2.005_66"
+    },
+    {
+      "mesh": 66,
+      "name": ""
+    }
+  ],
+  "samplers": [
+    {
+      "magFilter": 9729,
+      "minFilter": 9987,
+      "wrapS": 10497,
+      "wrapT": 10497
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "OSG_Scene",
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "textures": [
+    {
+      "sampler": 0,
+      "source": 0
+    },
+    {
+      "sampler": 0,
+      "source": 1
+    },
+    {
+      "sampler": 0,
+      "source": 2
+    },
+    {
+      "sampler": 0,
+      "source": 3
+    },
+    {
+      "sampler": 0,
+      "source": 4
+    },
+    {
+      "sampler": 0,
+      "source": 5
+    },
+    {
+      "sampler": 0,
+      "source": 6
+    },
+    {
+      "sampler": 0,
+      "source": 7
+    },
+    {
+      "sampler": 0,
+      "source": 8
+    },
+    {
+      "sampler": 0,
+      "source": 9
+    },
+    {
+      "sampler": 0,
+      "source": 10
+    },
+    {
+      "sampler": 0,
+      "source": 11
+    },
+    {
+      "sampler": 0,
+      "source": 12
+    },
+    {
+      "sampler": 0,
+      "source": 13
+    },
+    {
+      "sampler": 0,
+      "source": 14
+    }
+  ]
+}
+

二進制
resources/models/star-destroyer/textures/Body_Top_baseColor.png


二進制
resources/models/star-destroyer/textures/Body_Top_emissive.png


二進制
resources/models/star-destroyer/textures/Body_Top_metallicRoughness.png


二進制
resources/models/star-destroyer/textures/Body_baseColor.png


二進制
resources/models/star-destroyer/textures/Body_emissive.png


二進制
resources/models/star-destroyer/textures/Body_metallicRoughness.png


二進制
resources/models/star-destroyer/textures/Bridge_Thing_emissive.png


二進制
resources/models/star-destroyer/textures/Details_baseColor.png


二進制
resources/models/star-destroyer/textures/Details_metallicRoughness.png


二進制
resources/models/star-destroyer/textures/Engines_baseColor.png


二進制
resources/models/star-destroyer/textures/Engines_emissive.png


二進制
resources/models/star-destroyer/textures/Material.004_baseColor.jpeg


二進制
resources/models/star-destroyer/textures/Material.004_metallicRoughness.png


二進制
resources/models/star-destroyer/textures/Turret_baseColor.png


二進制
resources/models/star-destroyer/textures/Turret_metallicRoughness.png


二進制
resources/models/tie-fighter-gltf/scene.bin


+ 298 - 0
resources/models/tie-fighter-gltf/scene.gltf

@@ -0,0 +1,298 @@
+{
+  "accessors": [
+    {
+      "bufferView": 2,
+      "componentType": 5126,
+      "count": 3393,
+      "max": [
+        42.081470489501953,
+        56.691089630126953,
+        44.649555206298828
+      ],
+      "min": [
+        -42.128803253173828,
+        -54.971416473388672,
+        -46.216358184814453
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 40716,
+      "componentType": 5126,
+      "count": 3393,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "componentType": 5126,
+      "count": 3393,
+      "max": [
+        1,
+        1,
+        0.99999970197677612,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1,
+        1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "componentType": 5126,
+      "count": 3393,
+      "max": [
+        0.99807441234588623,
+        0.99950003623962402
+      ],
+      "min": [
+        0.00050000002374872565,
+        0.00050000002374872565
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "componentType": 5125,
+      "count": 8013,
+      "max": [
+        3392
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "extras": {
+      "author": "robin.vdv_art (https://sketchfab.com/robin.vdv_art)",
+      "license": "CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)",
+      "source": "https://sketchfab.com/3d-models/star-wars-tie-fighter-low-poly-pbr-7396dadd810d43528ff6d7c4e198d653",
+      "title": "Star Wars || Tie fighter || Low poly || PBR"
+    },
+    "generator": "Sketchfab-8.30.0",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 32052,
+      "byteOffset": 0,
+      "name": "floatBufferViews",
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteLength": 27144,
+      "byteOffset": 32052,
+      "byteStride": 8,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 81432,
+      "byteOffset": 59196,
+      "byteStride": 12,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 54288,
+      "byteOffset": 140628,
+      "byteStride": 16,
+      "name": "floatBufferViews",
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 194916,
+      "uri": "scene.bin"
+    }
+  ],
+  "images": [
+    {
+      "uri": "textures/hullblue_baseColor.png"
+    },
+    {
+      "uri": "textures/hullblue_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/hullblue_normal.png"
+    }
+  ],
+  "materials": [
+    {
+      "doubleSided": true,
+      "name": "hullblue",
+      "normalTexture": {
+        "index": 2,
+        "scale": 1,
+        "texCoord": 0
+      },
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 0,
+          "texCoord": 0
+        },
+        "metallicFactor": 1,
+        "metallicRoughnessTexture": {
+          "index": 1,
+          "texCoord": 0
+        },
+        "roughnessFactor": 0.32395922635876673
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "name": "Sphere001_hullblue_0",
+      "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
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        -1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1
+      ],
+      "name": "9bbde8bb95fd4a9ea60f10fd7addb0cd.fbx"
+    },
+    {
+      "children": [
+        4
+      ],
+      "name": "RootNode"
+    },
+    {
+      "children": [
+        5
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        -0.99999999999999911,
+        4.3711387965113384e-08,
+        0,
+        0,
+        -4.3711387965113384e-08,
+        -0.99999999999999911,
+        0,
+        0,
+        0,
+        0,
+        1
+      ],
+      "name": "Sphere001"
+    },
+    {
+      "mesh": 0,
+      "name": "Sphere001_hullblue_0"
+    }
+  ],
+  "samplers": [
+    {
+      "magFilter": 9729,
+      "minFilter": 9987,
+      "wrapS": 10497,
+      "wrapT": 10497
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "OSG_Scene",
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "textures": [
+    {
+      "sampler": 0,
+      "source": 0
+    },
+    {
+      "sampler": 0,
+      "source": 1
+    },
+    {
+      "sampler": 0,
+      "source": 2
+    }
+  ]
+}
+

二進制
resources/models/tie-fighter-gltf/textures/hullblue_baseColor.png


二進制
resources/models/tie-fighter-gltf/textures/hullblue_metallicRoughness.png


二進制
resources/models/tie-fighter-gltf/textures/hullblue_normal.png


二進制
resources/models/x-wing/scene.bin


+ 921 - 0
resources/models/x-wing/scene.gltf

@@ -0,0 +1,921 @@
+{
+  "accessors": [
+    {
+      "bufferView": 2,
+      "componentType": 5126,
+      "count": 183,
+      "max": [
+        0.037121415138244629,
+        2.617652416229248,
+        -0.4952666163444519
+      ],
+      "min": [
+        -0.037121415138244629,
+        2.4308793544769287,
+        -0.72735202312469482
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 2196,
+      "componentType": 5126,
+      "count": 183,
+      "max": [
+        0.9999995231628418,
+        0.99158108234405518,
+        0.99579089879989624
+      ],
+      "min": [
+        -0.9999995231628418,
+        0.00059056759346276522,
+        -0.00083738320972770452
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "componentType": 5126,
+      "count": 183,
+      "max": [
+        1,
+        0.99999719858169556,
+        0.88710075616836548,
+        1
+      ],
+      "min": [
+        -1,
+        -0.053852587938308716,
+        -0.82815927267074585,
+        -1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "componentType": 5126,
+      "count": 183,
+      "max": [
+        0.077496454119682312,
+        0.058563843369483948
+      ],
+      "min": [
+        0.001953125,
+        0.001953125
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "componentType": 5125,
+      "count": 630,
+      "max": [
+        182
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 4392,
+      "componentType": 5126,
+      "count": 161,
+      "max": [
+        0.18755339086055756,
+        2.8332698345184326,
+        0.088692963123321533
+      ],
+      "min": [
+        -0.18755339086055756,
+        2.4252176284790039,
+        -0.3415229320526123
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 6324,
+      "componentType": 5126,
+      "count": 161,
+      "max": [
+        0.99607813358306885,
+        0.99999821186065674,
+        0.99993783235549927
+      ],
+      "min": [
+        -0.99607813358306885,
+        -0.5912243127822876,
+        -0.9987824559211731
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "byteOffset": 2928,
+      "componentType": 5126,
+      "count": 161,
+      "max": [
+        0.99998646974563599,
+        0.9986422061920166,
+        0.99657326936721802,
+        1
+      ],
+      "min": [
+        -0.99998641014099121,
+        -0.44349688291549683,
+        -0.9999847412109375,
+        -1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 1464,
+      "componentType": 5126,
+      "count": 161,
+      "max": [
+        0.6777646541595459,
+        0.080187238752841949
+      ],
+      "min": [
+        0.20373937487602234,
+        0.001953125
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 2520,
+      "componentType": 5125,
+      "count": 654,
+      "max": [
+        160
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 8256,
+      "componentType": 5126,
+      "count": 86,
+      "max": [
+        0.021723806858062744,
+        2.7172439098358154,
+        -0.44914624094963074
+      ],
+      "min": [
+        -0.021723806858062744,
+        2.4825718402862549,
+        -0.55800890922546387
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 9288,
+      "componentType": 5126,
+      "count": 86,
+      "max": [
+        0.99325919151306152,
+        0.97777533531188965,
+        0.99014973640441895
+      ],
+      "min": [
+        -0.99325913190841675,
+        -0.86921441555023193,
+        -0.9996572732925415
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "byteOffset": 5504,
+      "componentType": 5126,
+      "count": 86,
+      "max": [
+        0.95377719402313232,
+        0.81636089086532593,
+        0.9849969744682312,
+        1
+      ],
+      "min": [
+        -0.99996942281723022,
+        -0.75313061475753784,
+        -0.99696564674377441,
+        1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 2752,
+      "componentType": 5126,
+      "count": 86,
+      "max": [
+        0.64555466175079346,
+        0.058929130434989929
+      ],
+      "min": [
+        0.60282778739929199,
+        0.001953125
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 5136,
+      "componentType": 5125,
+      "count": 366,
+      "max": [
+        85
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 10320,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        4.9269957542419434,
+        4.0200834274291992,
+        5.3744158744812012
+      ],
+      "min": [
+        -4.9269957542419434,
+        2.4605989456176758,
+        -0.91309356689453125
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 79080,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99997568130493164,
+        0.99999403953552246,
+        1
+      ],
+      "min": [
+        -0.99997580051422119,
+        -0.99972498416900635,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "byteOffset": 6880,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99986892938613892,
+        0.99999994039535522,
+        1,
+        1
+      ],
+      "min": [
+        -0.99986892938613892,
+        -0.99930751323699951,
+        -1,
+        -1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 3440,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99092870950698853,
+        0.99510931968688965
+      ],
+      "min": [
+        0.0074482806958258152,
+        0.010042082518339157
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 6600,
+      "componentType": 5125,
+      "count": 22008,
+      "max": [
+        5729
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 147840,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        4.9236788749694824,
+        2.393578052520752,
+        5.3744158744812012
+      ],
+      "min": [
+        -4.9236788749694824,
+        0.80872094631195068,
+        -0.91309356689453125
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 216600,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99997568130493164,
+        0.99972498416900635,
+        1
+      ],
+      "min": [
+        -0.99997580051422119,
+        -0.99999403953552246,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "byteOffset": 98560,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99986892938613892,
+        0.99930751323699951,
+        1,
+        1
+      ],
+      "min": [
+        -0.99986892938613892,
+        -0.99999994039535522,
+        -1,
+        -1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 49280,
+      "componentType": 5126,
+      "count": 5730,
+      "max": [
+        0.99092870950698853,
+        0.99510931968688965
+      ],
+      "min": [
+        0.0074482806958258152,
+        0.010042082518339157
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 94632,
+      "componentType": 5125,
+      "count": 22008,
+      "max": [
+        5729
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 285360,
+      "componentType": 5126,
+      "count": 907,
+      "max": [
+        0.77740305662155151,
+        3.3604762554168701,
+        4.4743952751159668
+      ],
+      "min": [
+        -0.77740305662155151,
+        1.7015378475189209,
+        -4.9360575675964355
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 2,
+      "byteOffset": 296244,
+      "componentType": 5126,
+      "count": 907,
+      "max": [
+        0.99992930889129639,
+        1,
+        1
+      ],
+      "min": [
+        -0.99992930889129639,
+        -1,
+        -0.99997901916503906
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 3,
+      "byteOffset": 190240,
+      "componentType": 5126,
+      "count": 907,
+      "max": [
+        0.99999874830245972,
+        0.99995929002761841,
+        0.99999874830245972,
+        1
+      ],
+      "min": [
+        -0.99999868869781494,
+        -0.99983668327331543,
+        -0.99999874830245972,
+        -1
+      ],
+      "type": "VEC4"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 95120,
+      "componentType": 5126,
+      "count": 907,
+      "max": [
+        1.0036741495132446,
+        0.998046875
+      ],
+      "min": [
+        0.001953125,
+        0.001953125
+      ],
+      "type": "VEC2"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 182664,
+      "componentType": 5125,
+      "count": 3498,
+      "max": [
+        906
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "extras": {
+      "author": "ran1102 (https://sketchfab.com/ran1102)",
+      "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
+      "source": "https://sketchfab.com/3d-models/x-wing-t-70-f8340a7cf72f4cda86286a68d4581d9e",
+      "title": "X-Wing T-70"
+    },
+    "generator": "Sketchfab-8.51.0",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 196656,
+      "byteOffset": 0,
+      "name": "floatBufferViews",
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteLength": 102376,
+      "byteOffset": 196656,
+      "byteStride": 8,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 307128,
+      "byteOffset": 299032,
+      "byteStride": 12,
+      "name": "floatBufferViews",
+      "target": 34962
+    },
+    {
+      "buffer": 0,
+      "byteLength": 204752,
+      "byteOffset": 606160,
+      "byteStride": 16,
+      "name": "floatBufferViews",
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 810912,
+      "uri": "scene.bin"
+    }
+  ],
+  "images": [
+    {
+      "uri": "textures/lambert7_baseColor.png"
+    },
+    {
+      "uri": "textures/lambert7_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/lambert7_normal.png"
+    },
+    {
+      "uri": "textures/lambert8_baseColor.png"
+    },
+    {
+      "uri": "textures/lambert8_metallicRoughness.png"
+    },
+    {
+      "uri": "textures/lambert8_normal.png"
+    }
+  ],
+  "materials": [
+    {
+      "alphaMode": "BLEND",
+      "doubleSided": true,
+      "name": "lambert7",
+      "normalTexture": {
+        "index": 2,
+        "scale": 1,
+        "texCoord": 0
+      },
+      "occlusionTexture": {
+        "index": 1,
+        "strength": 1,
+        "texCoord": 0
+      },
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 0,
+          "texCoord": 0
+        },
+        "metallicFactor": 1,
+        "metallicRoughnessTexture": {
+          "index": 1,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "name": "lambert8",
+      "normalTexture": {
+        "index": 5,
+        "scale": 1,
+        "texCoord": 0
+      },
+      "occlusionTexture": {
+        "index": 4,
+        "strength": 1,
+        "texCoord": 0
+      },
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          1,
+          1,
+          1,
+          1
+        ],
+        "baseColorTexture": {
+          "index": 3,
+          "texCoord": 0
+        },
+        "metallicFactor": 1,
+        "metallicRoughnessTexture": {
+          "index": 4,
+          "texCoord": 0
+        },
+        "roughnessFactor": 1
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "name": "d_lambert7_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 0,
+            "TANGENT": 2,
+            "TEXCOORD_0": 3
+          },
+          "indices": 4,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "b_lambert7_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 6,
+            "POSITION": 5,
+            "TANGENT": 7,
+            "TEXCOORD_0": 8
+          },
+          "indices": 9,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "c_lambert7_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 11,
+            "POSITION": 10,
+            "TANGENT": 12,
+            "TEXCOORD_0": 13
+          },
+          "indices": 14,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "polySurface19_lambert8_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 16,
+            "POSITION": 15,
+            "TANGENT": 17,
+            "TEXCOORD_0": 18
+          },
+          "indices": 19,
+          "material": 1,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "H3_lambert8_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 21,
+            "POSITION": 20,
+            "TANGENT": 22,
+            "TEXCOORD_0": 23
+          },
+          "indices": 24,
+          "material": 1,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "polySurface6_lambert7_0",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 26,
+            "POSITION": 25,
+            "TANGENT": 27,
+            "TEXCOORD_0": 28
+          },
+          "indices": 29,
+          "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
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        -1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1
+      ],
+      "name": "ac9b519f195942e5b2a1e7b9568769c4.fbx"
+    },
+    {
+      "children": [
+        4
+      ],
+      "name": "RootNode"
+    },
+    {
+      "children": [
+        5,
+        16,
+        21
+      ],
+      "name": "group15"
+    },
+    {
+      "children": [
+        6
+      ],
+      "name": "group"
+    },
+    {
+      "children": [
+        7
+      ],
+      "name": "group8"
+    },
+    {
+      "children": [
+        8
+      ],
+      "name": "group7"
+    },
+    {
+      "children": [
+        9
+      ],
+      "name": "group"
+    },
+    {
+      "children": [
+        10,
+        12,
+        14
+      ],
+      "name": "group1"
+    },
+    {
+      "children": [
+        11
+      ],
+      "name": "d"
+    },
+    {
+      "mesh": 0,
+      "name": "d_lambert7_0"
+    },
+    {
+      "children": [
+        13
+      ],
+      "name": "b"
+    },
+    {
+      "mesh": 1,
+      "name": "b_lambert7_0"
+    },
+    {
+      "children": [
+        15
+      ],
+      "name": "c"
+    },
+    {
+      "mesh": 2,
+      "name": "c_lambert7_0"
+    },
+    {
+      "children": [
+        17,
+        19
+      ],
+      "name": "group14"
+    },
+    {
+      "children": [
+        18
+      ],
+      "name": "polySurface19"
+    },
+    {
+      "mesh": 3,
+      "name": "polySurface19_lambert8_0"
+    },
+    {
+      "children": [
+        20
+      ],
+      "name": "H3"
+    },
+    {
+      "mesh": 4,
+      "name": "H3_lambert8_0"
+    },
+    {
+      "children": [
+        22
+      ],
+      "name": "polySurface6"
+    },
+    {
+      "mesh": 5,
+      "name": "polySurface6_lambert7_0"
+    }
+  ],
+  "samplers": [
+    {
+      "magFilter": 9729,
+      "minFilter": 9987,
+      "wrapS": 10497,
+      "wrapT": 10497
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "OSG_Scene",
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "textures": [
+    {
+      "sampler": 0,
+      "source": 0
+    },
+    {
+      "sampler": 0,
+      "source": 1
+    },
+    {
+      "sampler": 0,
+      "source": 2
+    },
+    {
+      "sampler": 0,
+      "source": 3
+    },
+    {
+      "sampler": 0,
+      "source": 4
+    },
+    {
+      "sampler": 0,
+      "source": 5
+    }
+  ]
+}
+

二進制
resources/models/x-wing/textures/lambert7_baseColor.png


二進制
resources/models/x-wing/textures/lambert7_metallicRoughness.png


二進制
resources/models/x-wing/textures/lambert7_normal.png


二進制
resources/models/x-wing/textures/lambert8_baseColor.png


二進制
resources/models/x-wing/textures/lambert8_metallicRoughness.png


二進制
resources/models/x-wing/textures/lambert8_normal.png


二進制
resources/rock-snow-ice-albedo-1024.png


二進制
resources/rock-snow-ice-normal-1024.jpg


二進制
resources/rough-wet-cobble-albedo-1024.png


二進制
resources/rough-wet-cobble-normal-1024.jpg


二進制
resources/sandy-rocks1-albedo-1024.png


二進制
resources/sandy-rocks1-normal-1024.jpg


二進制
resources/sandyground-albedo-1024.png


二進制
resources/sandyground-normal-1024.jpg


二進制
resources/simplex-noise.png


二進制
resources/snow-packed-albedo-1024.png


二進制
resources/snow-packed-normal-1024.jpg


二進制
resources/space-negx.jpg


二進制
resources/space-negy.jpg


二進制
resources/space-negz.jpg


二進制
resources/space-posx.jpg


二進制
resources/space-posy.jpg


二進制
resources/space-posz.jpg


二進制
resources/worn-bumpy-rock-albedo-1024.png


二進制
resources/worn-bumpy-rock-albedo-512.jpg


二進制
resources/worn-bumpy-rock-normal-1024.jpg


+ 356 - 0
src/agent.js

@@ -0,0 +1,356 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {math} from './math.js';
+
+
+export const agent = (function() {
+
+  const _BOID_FORCE_ORIGIN = 50;
+  const _BOID_FORCE_ALIGNMENT = 10;
+  const _BOID_FORCE_SEPARATION = 20;
+  const _BOID_FORCE_COLLISION = 50;
+  const _BOID_FORCE_COHESION = 5;
+  const _BOID_FORCE_WANDER = 3;
+  
+  const _M = new THREE.Matrix4();
+  const _V = new THREE.Vector3();
+  const _A = new THREE.Vector2();
+  const _B = new THREE.Vector2();
+  const _AP = new THREE.Vector2();
+  const _AB = new THREE.Vector2();
+  const _BA = new THREE.Vector2();
+  const _PT2 = new THREE.Vector2();
+  const _PT3 = new THREE.Vector3();
+
+  const _Q = new THREE.Quaternion();
+  const _V_0 = new THREE.Vector3(0, 0, 0);
+  const _V_Y = new THREE.Vector3(0, 1, 0);
+  const _V_SC_0_1 = new THREE.Vector3(0.1, 0.1, 0.1);
+
+  function _Key(x, y) {
+    return x + '.' + y;
+  }
+  
+  class LineRenderer {
+    constructor(game) {
+      this._game = game;
+  
+      this._materials = {};
+      this._group = new THREE.Group();
+  
+      this._game._graphics.Scene.add(this._group);
+    }
+  
+    Reset() {
+      this._lines = [];
+      this._group.remove(...this._group.children);
+    }
+  
+    Add(pt1, pt2, hexColour) {
+      const geometry = new THREE.Geometry();
+      geometry.vertices.push(pt1.clone());
+      geometry.vertices.push(pt2.clone());
+  
+      let material = this._materials[hexColour];
+      if (!material) {
+        this._materials[hexColour] = new THREE.LineBasicMaterial(
+            {
+              color: hexColour,
+              linewidth: 3,
+            });
+        material = this._materials[hexColour];
+      }
+  
+      const line = new THREE.Line(geometry, material);
+      this._lines.push(line);
+      this._group.add(line);
+    }
+  }
+  
+
+  class _Agent {
+    constructor(game, params) {
+      this._mesh = params.mesh;
+  
+      this._group = new THREE.Group();
+      this._group.add(this._mesh);
+      this._group.position.set(
+          math.rand_range(-250, 250),
+          math.rand_range(-250, 250),
+          math.rand_range(-250, 250));
+      this._group.position.add(params.seekGoal);
+
+      this._direction = new THREE.Vector3(
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1));
+      this._velocity = this._direction.clone();
+  
+      const speedMultiplier = math.rand_range(params.speedMin, params.speedMax);
+      this._maxSteeringForce = params.maxSteeringForce * speedMultiplier;
+      this._maxSpeed  = params.speed * speedMultiplier;
+      this._acceleration = params.acceleration * speedMultiplier;
+  
+      const scale = 1.0 / speedMultiplier;
+      this._radius = scale;
+  
+      this._game = game;
+      game._graphics.Scene.add(this._group);
+      this._visibilityIndex = game._visibilityGrid.UpdateItem(
+          this._mesh.uuid, this);
+  
+      this._wanderAngle = 0;
+      this._seekGoal = params.seekGoal;
+      this._fireCooldown = 0.0;
+      this._params = params;
+      this._health = 100.0;
+    }
+  
+    get Enemy() {
+      return true;
+    }
+
+    get Position() {
+      return this._group.position;
+    }
+  
+    get Velocity() {
+      return this._velocity;
+    }
+  
+    get Direction() {
+      return this._direction;
+    }
+  
+    get Radius() {
+      return this._radius;
+    }
+  
+    get Health() {
+      return this._health;
+    }
+
+    get Dead() {
+      return (this._health <= 0.0);
+    }
+  
+    TakeDamage(dmg) {
+      this._health -= dmg;
+      if (this._health <= 0.0) {
+        this._game._entities['_explosionSystem'].Splode(this.Position);
+        this._game._visibilityGrid.RemoveItem(this._mesh.uuid, this._visibilityIndex);
+        this._game._graphics.Scene.remove(this._group);
+        this._game.EnemyDied();
+      }    
+    }
+  
+    Update(timeInSeconds) {
+      if (this.Dead) {
+        return;
+      }
+  
+      const local = this._game._visibilityGrid.GetLocalEntities(
+          this.Position, 15);
+  
+      this._ApplySteering(timeInSeconds, local);
+  
+      const frameVelocity = this._velocity.clone();
+      frameVelocity.multiplyScalar(timeInSeconds);
+      this._group.position.add(frameVelocity);
+  
+      this._group.quaternion.setFromUnitVectors(
+          new THREE.Vector3(0, 1, 0), this.Direction);
+  
+      this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+          this._mesh.uuid, this, this._visibilityIndex);
+  
+      if (this._displayDebug) {
+        this._UpdateDebug(local);
+      }
+    }
+  
+    _ApplySteering(timeInSeconds, local) {
+      const separationVelocity = this._ApplySeparation(local);
+  
+      // Only apply alignment and cohesion to allies
+      const allies = local.filter((e) => {
+        return (e.Enemy && this._seekGoal.equals(e._seekGoal));
+      });
+  
+      this._fireCooldown -= timeInSeconds;
+      if (this._fireCooldown <= 0.0) {
+        const neighbourhood = this._game._visibilityGrid.GetLocalEntities(
+          this.Position, 100);
+  
+        const enemies = neighbourhood.filter((e) => {
+          return !e.Enemy;
+        });
+
+        if (enemies.length > 0) {
+          const p = this._game._entities['_blasterSystem'].CreateParticle();
+          p.Start = this.Direction.clone();
+          p.Start.multiplyScalar(100.0);
+          p.Start.add(this.Position);
+          p.End = p.Start.clone();
+          p.Velocity = this.Direction.clone().multiplyScalar(500);
+          p.Length = 25;
+          p.Colours = [
+              this._params.colour.clone(), new THREE.Color(0.0, 0.0, 0.0)];
+          p.Life = 1.0;
+          p.TotalLife = 1.0;
+          p.Width = 0.25;
+    
+          this._fireCooldown = 0.5;
+        }
+      }
+  
+      const alignmentVelocity = this._ApplyAlignment(allies);
+      const cohesionVelocity = this._ApplyCohesion(allies);
+      const originVelocity = this._ApplySeek(this._seekGoal);
+      const wanderVelocity = this._ApplyWander();
+      const collisionVelocity = this._ApplyCollisionAvoidance();
+  
+      const steeringForce = new THREE.Vector3(0, 0, 0);
+      steeringForce.add(separationVelocity);
+      steeringForce.add(alignmentVelocity);
+      steeringForce.add(cohesionVelocity);
+      steeringForce.add(originVelocity);
+      steeringForce.add(wanderVelocity);
+      steeringForce.add(collisionVelocity);
+  
+      steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+  
+      // Clamp the force applied
+      if (steeringForce.length() > this._maxSteeringForce) {
+        steeringForce.normalize();
+        steeringForce.multiplyScalar(this._maxSteeringForce);
+      }
+  
+      this._velocity.add(steeringForce);
+  
+      // Clamp velocity
+      if (this._velocity.length() > this._maxSpeed) {
+        this._velocity.normalize();
+        this._velocity.multiplyScalar(this._maxSpeed);
+      }
+  
+      this._direction = this._velocity.clone();
+      this._direction.normalize();
+    }
+  
+    _ApplyCollisionAvoidance() {
+      const colliders = this._game._visibilityGrid.GetGlobalItems();
+  
+      const ray = new THREE.Ray(this.Position, this.Direction);
+      const force = new THREE.Vector3(0, 0, 0);
+  
+      for (const c of colliders) {
+        if (c.Position.distanceTo(this.Position) > c.QuickRadius) {
+          continue;
+        }
+  
+        const result = ray.intersectBox(c.AABB, new THREE.Vector3());
+        if (result) {
+          const distanceToCollision = result.distanceTo(this.Position);
+          if (distanceToCollision < 2) {
+            let a = 0;
+          }
+          const dirToCenter = c.Position.clone().sub(this.Position).normalize();
+          const dirToCollision = result.clone().sub(this.Position).normalize();
+          const steeringDirection = dirToCollision.sub(dirToCenter).normalize();
+          steeringDirection.multiplyScalar(_BOID_FORCE_COLLISION);
+          force.add(steeringDirection);
+        }
+      }
+  
+      return force;
+    }
+  
+    _ApplyWander() {
+      this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
+      const randomPointOnCircle = new THREE.Vector3(
+          Math.cos(this._wanderAngle),
+          0,
+          Math.sin(this._wanderAngle));
+      const pointAhead = this._direction.clone();
+      pointAhead.multiplyScalar(5);
+      pointAhead.add(randomPointOnCircle);
+      pointAhead.normalize();
+      return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
+    }
+  
+    _ApplySeparation(local) {
+      if (local.length == 0) {
+        return new THREE.Vector3(0, 0, 0);
+      }
+  
+      const forceVector = new THREE.Vector3(0, 0, 0);
+      for (let e of local) {
+        const distanceToEntity = Math.max(
+            e.Position.distanceTo(this.Position) - 1.5 * (this.Radius + e.Radius),
+            0.001);
+        const directionFromEntity = new THREE.Vector3().subVectors(
+            this.Position, e.Position);
+        const multiplier = (_BOID_FORCE_SEPARATION / distanceToEntity);
+        directionFromEntity.normalize();
+        forceVector.add(
+            directionFromEntity.multiplyScalar(multiplier));
+      }
+      return forceVector;
+    }
+  
+    _ApplyAlignment(local) {
+      const forceVector = new THREE.Vector3(0, 0, 0);
+  
+      for (let e of local) {
+        const entityDirection = e.Direction;
+        forceVector.add(entityDirection);
+      }
+  
+      forceVector.normalize();
+      forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
+  
+      return forceVector;
+    }
+  
+    _ApplyCohesion(local) {
+      const forceVector = new THREE.Vector3(0, 0, 0);
+  
+      if (local.length == 0) {
+        return forceVector;
+      }
+  
+      const averagePosition = new THREE.Vector3(0, 0, 0);
+      for (let e of local) {
+        averagePosition.add(e.Position);
+      }
+  
+      averagePosition.multiplyScalar(1.0 / local.length);
+  
+      const directionToAveragePosition = averagePosition.clone().sub(
+          this.Position);
+      directionToAveragePosition.normalize();
+      directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
+  
+      // HACK: Floating point error from accumulation of positions.
+      directionToAveragePosition.y = 0;
+  
+      return directionToAveragePosition;
+    }
+  
+    _ApplySeek(destination) {
+      const distance = Math.max(0,((
+          this.Position.distanceTo(destination) - 50) / 2000)) ** 2;
+      const direction = destination.clone().sub(this.Position);
+      direction.normalize();
+  
+      const forceVector = direction.multiplyScalar(
+          _BOID_FORCE_ORIGIN * distance);
+      return forceVector;
+    }
+  }
+
+  return {
+    Agent: _Agent,
+  };
+})();

+ 211 - 0
src/blaster.js

@@ -0,0 +1,211 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+export const blaster = (function() {
+
+  const _VS = `#version 300 es
+out vec2 v_UV;
+out vec3 vColor;
+
+void main() {
+  vColor = color;
+  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
+  gl_Position = projectionMatrix * mvPosition;
+  v_UV = uv;
+}
+`;
+
+  const _PS = `#version 300 es
+uniform sampler2D diffuse;
+
+in vec2 v_UV;
+in vec3 vColor;
+out vec4 out_FragColor;
+
+void main() {
+  out_FragColor = vec4(vColor, 1.0) * texture(diffuse, v_UV);
+}
+`;
+
+  return {
+      BlasterSystem: class {
+        constructor(params) {
+          this._Initialize(params);
+        }
+
+        _Initialize(params) {
+          const uniforms = {
+            diffuse: {
+              value: new THREE.TextureLoader().load(params.texture)
+            }
+          };
+          this._material = new THREE.ShaderMaterial( {
+            uniforms: uniforms,
+            vertexShader: _VS,
+            fragmentShader: _PS,
+
+            blending: THREE.AdditiveBlending,
+            depthTest: true,
+            depthWrite: false,
+            transparent: true,
+            vertexColors: true,
+            side: THREE.DoubleSide,
+          });
+
+          this._geometry = new THREE.BufferGeometry();
+
+          this._particleSystem = new THREE.Mesh(this._geometry, this._material);
+          this._particleSystem.frustumCulled = false;
+
+          this._game = params.game;
+          this._params = params;
+
+          this._liveParticles = [];
+
+          this._game._graphics._scene.add(this._particleSystem);
+        }
+
+        CreateParticle() {
+          const p = {
+            Start: new THREE.Vector3(0, 0, 0),
+            End: new THREE.Vector3(0, 0, 0),
+            Colour: new THREE.Color(),
+            Size: 1,
+            Alive: true,
+          };
+          this._liveParticles.push(p);
+          return p;
+        }
+
+        Update(timeInSeconds) {
+          const _R = new THREE.Ray();
+          const _M = new THREE.Vector3();
+          const _S = new THREE.Sphere();
+          const _C = new THREE.Vector3();
+
+          for (const p of this._liveParticles) {
+            p.Life -= timeInSeconds;
+            if (p.Life <= 0) {
+              p.Alive = false;
+              continue;
+            }
+
+            p.End.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
+
+            const segment = p.End.clone().sub(p.Start);
+            if (segment.length() > p.Length) {
+              const dir = p.Velocity.clone().normalize();
+              p.Start = p.End.clone().sub(dir.multiplyScalar(p.Length));
+            }
+
+            // Find intersections
+            _R.direction.copy(p.Velocity);
+            _R.direction.normalize();
+            _R.origin.copy(p.Start);
+
+            const blasterLength = p.End.distanceTo(p.Start);
+            _M.addVectors(p.Start, p.End);
+            _M.multiplyScalar(0.5);
+
+            const potentialList = this._params.visibility.GetLocalEntities(_M, blasterLength * 0.5);
+
+            // Technically we should sort by distance, but I'll just use the first hit. Good enough.
+            if (potentialList.length == 0) {
+              continue;
+            }
+
+            for (let candidate of potentialList) {
+              _S.center.copy(candidate.Position);
+              _S.radius = 2.0;
+
+              if (!_R.intersectSphere(_S, _C)) {
+                continue;
+              }
+
+              if (_C.distanceTo(p.Start) > blasterLength) {
+                continue;
+              }
+
+              p.Alive = false;
+              candidate.TakeDamage(100.0);
+              break;
+            }
+          }
+
+          this._liveParticles = this._liveParticles.filter(p => {
+            return p.Alive;
+          });
+
+          this._GenerateBuffers();
+        }
+
+        _GenerateBuffers() {
+          const indices = [];
+          const positions = [];
+          const colors = [];
+          const uvs = [];
+
+          const square = [0, 1, 2, 2, 3, 0];
+          let indexBase = 0;
+
+          for (const p of this._liveParticles) {
+            indices.push(...square.map(i => i + indexBase));
+            indexBase += 4;
+
+            const v1 = p.End.clone().applyMatrix4(
+                this._particleSystem.modelViewMatrix);
+            const v2 = p.Start.clone().applyMatrix4(
+                this._particleSystem.modelViewMatrix);
+            const dir = new THREE.Vector3().subVectors(v1, v2);
+            dir.z = 0;
+            dir.normalize();
+
+            const up = new THREE.Vector3(-dir.y, dir.x, 0);
+
+            const dirWS = up.clone().transformDirection(
+                this._game._graphics._camera.matrixWorld);
+            dirWS.multiplyScalar(p.Width);
+
+            const p1 = new THREE.Vector3().copy(p.Start);
+            p1.add(dirWS);
+
+            const p2 = new THREE.Vector3().copy(p.Start);
+            p2.sub(dirWS);
+
+            const p3 = new THREE.Vector3().copy(p.End);
+            p3.sub(dirWS);
+
+            const p4 = new THREE.Vector3().copy(p.End);
+            p4.add(dirWS);
+
+            positions.push(p1.x, p1.y, p1.z);
+            positions.push(p2.x, p2.y, p2.z);
+            positions.push(p3.x, p3.y, p3.z);
+            positions.push(p4.x, p4.y, p4.z);
+
+            uvs.push(0.0, 0.0);
+            uvs.push(1.0, 0.0);
+            uvs.push(1.0, 1.0);
+            uvs.push(0.0, 1.0);
+
+            const c = p.Colours[0].lerp(
+                p.Colours[1], 1.0 - p.Life / p.TotalLife);
+            for (let i = 0; i < 4; i++) {
+              colors.push(c.r, c.g, c.b);
+            }
+          }
+
+          this._geometry.setAttribute(
+              'position', new THREE.Float32BufferAttribute(positions, 3));
+          this._geometry.setAttribute(
+              'uv', new THREE.Float32BufferAttribute(uvs, 2));
+          this._geometry.setAttribute(
+              'color', new THREE.Float32BufferAttribute(colors, 3));
+          this._geometry.setIndex(
+              new THREE.BufferAttribute(new Uint32Array(indices), 1));
+
+          this._geometry.attributes.position.needsUpdate = true;
+          this._geometry.attributes.uv.needsUpdate = true;
+        }
+      }
+  };
+})();

+ 39 - 0
src/camera-track.js

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

+ 478 - 0
src/controls.js

@@ -0,0 +1,478 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/PointerLockControls.js';
+import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
+
+import {math} from './math.js';
+
+
+export const controls = (function() {
+
+  class _OrbitControls {
+    constructor(params) {
+      this._params = params;
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._controls = new OrbitControls(params.camera, params.domElement);
+      this._controls.target.set(0, 0, 0);
+      this._controls.update();
+    }
+
+    Update() {
+    }
+  }
+
+  // FPSControls was adapted heavily from a threejs example. Movement control
+  // and collision detection was completely rewritten, but credit to original
+  // class for the setup code.
+  class _FPSControls {
+    constructor(params) {
+      this._cells = params.cells;
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._params = params;
+      this._radius = 2;
+      this._enabled = false;
+      this._move = {
+        forward: false,
+        backward: false,
+        left: false,
+        right: false,
+        up: false,
+        down: false,
+      };
+      this._standing = true;
+      this._velocity = new THREE.Vector3(0, 0, 0);
+      this._decceleration = new THREE.Vector3(-10, -10, -10);
+      this._acceleration = new THREE.Vector3(50000, 50000, 50000);
+
+      this._SetupPointerLock();
+
+      this._controls = new PointerLockControls(
+          params.camera, document.body);
+      params.scene.add(this._controls.getObject());
+
+      document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
+      document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
+
+      this._InitGUI();
+    }
+
+    _InitGUI() {
+      this._params.guiParams.camera = {
+        acceleration_x: 50000,
+      };
+
+      const rollup = this._params.gui.addFolder('Camera.FPS');
+      rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 50000.0).onChange(
+        () => {
+          this._acceleration.set(
+            this._params.guiParams.camera.acceleration_x,
+            this._params.guiParams.camera.acceleration_x,
+            this._params.guiParams.camera.acceleration_x);
+        });
+    }
+
+    _onKeyDown(event) {
+      switch (event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this._move.forward = true;
+          break;
+        case 37: // left
+        case 65: // a
+          this._move.left = true;
+          break;
+        case 40: // down
+        case 83: // s
+          this._move.backward = true;
+          break;
+        case 39: // right
+        case 68: // d
+          this._move.right = true;
+          break;
+        case 33: // PG_UP
+          this._move.up = true;
+          break;
+        case 34: // PG_DOWN
+          this._move.down = true;
+          break;
+      }
+    }
+
+    _onKeyUp(event) {
+      switch(event.keyCode) {
+        case 38: // up
+        case 87: // w
+          this._move.forward = false;
+          break;
+        case 37: // left
+        case 65: // a
+          this._move.left = false;
+          break;
+        case 40: // down
+        case 83: // s
+          this._move.backward = false;
+          break;
+        case 39: // right
+        case 68: // d
+          this._move.right = false;
+          break;
+        case 33: // PG_UP
+          this._move.up = false;
+          break;
+        case 34: // PG_DOWN
+          this._move.down = false;
+          break;
+      }
+    }
+
+    _SetupPointerLock() {
+      const hasPointerLock = (
+          'pointerLockElement' in document ||
+          'mozPointerLockElement' in document ||
+          'webkitPointerLockElement' in document);
+      if (hasPointerLock) {
+        const lockChange = (event) => {
+          if (document.pointerLockElement === document.body ||
+              document.mozPointerLockElement === document.body ||
+              document.webkitPointerLockElement === document.body ) {
+            this._enabled = true;
+            this._controls.enabled = true;
+          } else {
+            this._controls.enabled = false;
+          }
+        };
+        const lockError = (event) => {
+          console.log(event);
+        };
+
+        document.addEventListener('pointerlockchange', lockChange, false);
+        document.addEventListener('webkitpointerlockchange', lockChange, false);
+        document.addEventListener('mozpointerlockchange', lockChange, false);
+        document.addEventListener('pointerlockerror', lockError, false);
+        document.addEventListener('mozpointerlockerror', lockError, false);
+        document.addEventListener('webkitpointerlockerror', lockError, false);
+
+        document.getElementById('target').addEventListener('click', (event) => {
+          document.body.requestPointerLock = (
+              document.body.requestPointerLock ||
+              document.body.mozRequestPointerLock ||
+              document.body.webkitRequestPointerLock);
+
+          if (/Firefox/i.test(navigator.userAgent)) {
+            const fullScreenChange = (event) => {
+              if (document.fullscreenElement === document.body ||
+                  document.mozFullscreenElement === document.body ||
+                  document.mozFullScreenElement === document.body) {
+                document.removeEventListener('fullscreenchange', fullScreenChange);
+                document.removeEventListener('mozfullscreenchange', fullScreenChange);
+                document.body.requestPointerLock();
+              }
+            };
+            document.addEventListener(
+                'fullscreenchange', fullScreenChange, false);
+            document.addEventListener(
+                'mozfullscreenchange', fullScreenChange, false);
+            document.body.requestFullscreen = (
+                document.body.requestFullscreen ||
+                document.body.mozRequestFullscreen ||
+                document.body.mozRequestFullScreen ||
+                document.body.webkitRequestFullscreen);
+            document.body.requestFullscreen();
+          } else {
+            document.body.requestPointerLock();
+          }
+        }, false);
+      }
+    }
+
+    _FindIntersections(boxes, position) {
+      const sphere = new THREE.Sphere(position, this._radius);
+
+      const intersections = boxes.filter(b => {
+        return sphere.intersectsBox(b);
+      });
+
+      return intersections;
+    }
+
+    Update(timeInSeconds) {
+      if (!this._enabled) {
+        return;
+      }
+
+      const frameDecceleration = new THREE.Vector3(
+          this._velocity.x * this._decceleration.x,
+          this._velocity.y * this._decceleration.y,
+          this._velocity.z * this._decceleration.z
+      );
+      frameDecceleration.multiplyScalar(timeInSeconds);
+
+      this._velocity.add(frameDecceleration);
+
+      if (this._move.forward) {
+        this._velocity.z -= this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.backward) {
+        this._velocity.z += this._acceleration.z * timeInSeconds;
+      }
+      if (this._move.left) {
+        this._velocity.x -= this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.right) {
+        this._velocity.x += this._acceleration.x * timeInSeconds;
+      }
+      if (this._move.up) {
+        this._velocity.y += this._acceleration.y * timeInSeconds;
+      }
+      if (this._move.down) {
+        this._velocity.y -= this._acceleration.y * timeInSeconds;
+      }
+
+      const controlObject = this._controls.getObject();
+
+      const oldPosition = new THREE.Vector3();
+      oldPosition.copy(controlObject.position);
+
+      const forward = new THREE.Vector3(0, 0, 1);
+      forward.applyQuaternion(controlObject.quaternion);
+      forward.normalize();
+
+      const updown = new THREE.Vector3(0, 1, 0);
+
+      const sideways = new THREE.Vector3(1, 0, 0);
+      sideways.applyQuaternion(controlObject.quaternion);
+      sideways.normalize();
+
+      sideways.multiplyScalar(this._velocity.x * timeInSeconds);
+      updown.multiplyScalar(this._velocity.y * timeInSeconds);
+      forward.multiplyScalar(this._velocity.z * timeInSeconds);
+
+      controlObject.position.add(forward);
+      controlObject.position.add(sideways);
+      controlObject.position.add(updown);
+
+      oldPosition.copy(controlObject.position);
+    }
+  };
+
+  class _ShipControls {
+    constructor(params) {
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._params = params;
+      this._radius = 2;
+      this._enabled = false;
+      this._move = {
+        forward: false,
+        backward: false,
+        left: false,
+        right: false,
+        up: false,
+        down: false,
+        rocket: false,
+      };
+      this._decceleration = new THREE.Vector3(-0.0005, -0.0001, -1);
+      this._acceleration = new THREE.Vector3(100, 0.5, 25000);
+
+      this._params.target._model.position.copy(this._params.camera.position);
+      this._params.target._model.quaternion.copy(this._params.camera.quaternion);
+
+      document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
+      document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
+
+      this._InitGUI();
+    }
+
+    _InitGUI() {
+      this._params.guiParams.camera = {
+        acceleration_x: 100,
+        acceleration_y: 0.5,
+      };
+
+      const rollup = this._params.gui.addFolder('Camera.Ship');
+      rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange(
+        () => {
+          this._acceleration.x = this._params.guiParams.camera.acceleration_x;
+        });
+      rollup.add(this._params.guiParams.camera, "acceleration_y", 0.1, 0.5).onChange(
+        () => {
+          this._acceleration.y = this._params.guiParams.camera.acceleration_y;
+        });
+    }
+
+    _onKeyDown(event) {
+      switch (event.keyCode) {
+        case 87: // w
+          this._move.forward = true;
+          break;
+        case 65: // a
+          this._move.left = true;
+          break;
+        case 83: // s
+          this._move.backward = true;
+          break;
+        case 68: // d
+          this._move.right = true;
+          break;
+        case 33: // PG_UP
+          this._move.rollLeft = true;
+          break;
+        case 34: // PG_DOWN
+          this._move.rollRight = true;
+          break;
+        case 32: // SPACE
+          this._move.rocket = true;
+          break;
+        case 13: // ENTER
+          this._move.fire = true;
+        case 38: // up
+        case 37: // left
+        case 40: // down
+        case 39: // right
+          break;
+      }
+    }
+
+    _onKeyUp(event) {
+      switch(event.keyCode) {
+        case 87: // w
+          this._move.forward = false;
+          break;
+        case 65: // a
+          this._move.left = false;
+          break;
+        case 83: // s
+          this._move.backward = false;
+          break;
+        case 68: // d
+          this._move.right = false;
+          break;
+        case 33: // PG_UP
+          this._move.rollLeft = false;
+          break;
+        case 34: // PG_DOWN
+        this._move.rollRight = false;
+          break;
+        case 32: // SPACE
+          this._move.rocket = false;
+          break;
+        case 13: // ENTER
+          this._move.fire = false;
+        case 38: // up
+        case 37: // left
+        case 40: // down
+        case 39: // right
+          break;
+      }
+    }
+
+    Update(timeInSeconds) {
+      const velocity = this._params.target.Velocity;
+      const frameDecceleration = new THREE.Vector3(
+          velocity.x * this._decceleration.x,
+          velocity.y * this._decceleration.y,
+          velocity.z * this._decceleration.z
+      );
+      frameDecceleration.multiplyScalar(timeInSeconds);
+
+      velocity.add(frameDecceleration);
+      velocity.z = -math.clamp(Math.abs(velocity.z), 25.0, 125.0);
+
+      const controlObject = this._params.target;
+      const _Q = new THREE.Quaternion();
+      const _A = new THREE.Vector3();
+      const _R = controlObject._model.quaternion.clone();
+
+      if (this._move.forward) {
+        _A.set(1, 0, 0);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.backward) {
+        _A.set(1, 0, 0);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.left) {
+        _A.set(0, 1, 0);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.right) {
+        _A.set(0, 1, 0);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.rollLeft) {
+        _A.set(0, 0, -1);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.rollRight) {
+        _A.set(0, 0, -1);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
+        _R.multiply(_Q);
+      }
+      if (this._move.rocket) {
+        velocity.z -= this._acceleration.x * timeInSeconds;
+      }
+
+      controlObject._model.quaternion.copy(_R);
+
+      const oldPosition = new THREE.Vector3();
+      oldPosition.copy(controlObject._model.position);
+
+      const forward = new THREE.Vector3(0, 0, 1);
+      forward.applyQuaternion(controlObject._model.quaternion);
+      forward.normalize();
+
+      const updown = new THREE.Vector3(0, 1, 0);
+
+      const sideways = new THREE.Vector3(1, 0, 0);
+      sideways.applyQuaternion(controlObject._model.quaternion);
+      sideways.normalize();
+
+      sideways.multiplyScalar(velocity.x * timeInSeconds);
+      updown.multiplyScalar(velocity.y * timeInSeconds);
+      forward.multiplyScalar(velocity.z * timeInSeconds);
+
+      controlObject._model.position.add(forward);
+      controlObject._model.position.add(sideways);
+      controlObject._model.position.add(updown);
+      controlObject._velocity.copy(velocity);
+
+      oldPosition.copy(controlObject._model.position);
+
+      // Now place the camera in relation
+      const offsetFactor = (-velocity.z - 25.0) / 100.0;
+      const offset = new THREE.Vector3(0, 4, math.smootherstep(offsetFactor, 10.0, 15.0));
+      offset.applyQuaternion(this._params.camera.quaternion);
+
+      this._params.camera.quaternion.slerp(this._params.target._model.quaternion, timeInSeconds * 2.0);
+  
+      const position = new THREE.Vector3();
+      position.copy(this._params.target._model.position);
+      position.add(offset);
+  
+      this._params.camera.position.copy(position);
+      this._params.camera.updateProjectionMatrix();
+  
+      if (this._move.fire) {
+        this._params.target.Fire();
+      }
+    }
+  };
+
+  return {
+    ShipControls: _ShipControls,
+    FPSControls: _FPSControls,
+    OrbitControls: _OrbitControls,
+  };
+})();

+ 82 - 0
src/demo.js

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

+ 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._StepEntities(timeInSeconds);
+        this._OnStep(timeInSeconds);
+        this._graphics.Render(timeInSeconds);
+
+        this._RAF();
+      }
+    }
+  };
+})();

+ 248 - 0
src/graphics.js

@@ -0,0 +1,248 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import Stats from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/stats.module.js';
+import {WEBGL} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/WebGL.js';
+
+import {RenderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/RenderPass.js';
+import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/ShaderPass.js';
+import {CopyShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/CopyShader.js';
+import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/shaders/FXAAShader.js';
+import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/EffectComposer.js';
+
+import {UnrealBloomPass} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/postprocessing/UnrealBloomPass.js';
+
+import {scattering_shader} from './scattering-shader.js';
+
+
+export const graphics = (function() {
+
+  function _GetImageData(image) {
+    const canvas = document.createElement('canvas');
+    canvas.width = image.width;
+    canvas.height = image.height;
+
+    const context = canvas.getContext( '2d' );
+    context.drawImage(image, 0, 0);
+
+    return context.getImageData(0, 0, image.width, image.height);
+  }
+
+  function _GetPixel(imagedata, x, y) {
+    const position = (x + imagedata.width * y) * 4;
+    const data = imagedata.data;
+    return {
+        r: data[position],
+        g: data[position + 1],
+        b: data[position + 2],
+        a: data[position + 3]
+    };
+  }
+
+  class _Graphics {
+    constructor(game) {
+    }
+
+    Initialize() {
+      if (!WEBGL.isWebGL2Available()) {
+        return false;
+      }
+
+      const canvas = document.createElement('canvas');
+      const context = canvas.getContext('webgl2', {alpha: false});
+
+      this._threejs = new THREE.WebGLRenderer({
+        canvas: canvas,
+        context: context,
+      });
+      this._threejs.setPixelRatio(window.devicePixelRatio);
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+      this._threejs.autoClear = false;
+
+      const target = document.getElementById('target');
+      target.appendChild(this._threejs.domElement);
+
+      this._stats = new Stats();
+      //target.appendChild(this._stats.dom);
+
+      window.addEventListener('resize', () => {
+        this._OnWindowResize();
+      }, false);
+
+      const fov = 60;
+      const aspect = 1920 / 1080;
+      const near = 0.1;
+      const far = 100000.0;
+      this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this._camera.position.set(75, 20, 0);
+
+      this._scene = new THREE.Scene();
+      this._scene.background = new THREE.Color(0xaaaaaa);
+
+      const renderPass = new RenderPass(this._scene, this._camera);
+      const fxaaPass = new ShaderPass(FXAAShader);
+      const scatterPass = new ShaderPass(scattering_shader.Shader);
+      const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight));
+      bloomPass.threshold = 1.5;
+      bloomPass.strength = 2.5;
+      bloomPass.radius = 0.3;
+      bloomPass.exposure = 1.5;
+
+      this._bloomPass = bloomPass;
+      this._fxassPass = fxaaPass;
+      // this.scatterPass = depthPass;
+      this._fxassPass.setSize(window.innerWidth, window.innerHeight);
+      this._bloomPass.setSize(window.innerWidth, window.innerHeight);
+
+      this._composer = new EffectComposer(this._threejs);
+      this._composer.addPass(renderPass);
+      // this._composer.addPass(fxaaPass);
+      // this._composer.addPass(scatterPass);
+      //this._composer.addPass();
+      this._composer.renderToScreen = false;
+      //this._composer.addPass(depthPass);
+
+      function _CreateBuffer() {
+        const buf = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
+        buf.texture.format = THREE.RGBAFormat;
+        buf.texture.type = THREE.FloatType;
+        buf.texture.minFilter = THREE.NearestFilter;
+        buf.texture.magFilter = THREE.NearestFilter;
+        buf.texture.generateMipmaps = false;
+        buf.stencilBuffer = false;
+        buf.depthBuffer = true;
+        buf.depthTexture = new THREE.DepthTexture();
+        buf.depthTexture.format = THREE.DepthFormat;
+        buf.depthTexture.type = THREE.FloatType;
+        return buf;
+      }
+
+      this._targets = [_CreateBuffer(), _CreateBuffer()];
+
+      this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+      this.scatterPass = new THREE.ShaderMaterial( {
+        vertexShader: scattering_shader.VS,
+        fragmentShader: scattering_shader.PS,
+        uniforms: {
+          cameraNear: { value: this.Camera.near },
+          cameraFar: { value: this.Camera.far },
+          cameraPosition: { value: this.Camera.position },
+          cameraForward: { value: null },
+          tDiffuse: { value: null },
+          tDepth: { value: null },
+          inverseProjection: { value: null },
+          inverseView: { value: null },
+          planetPosition: { value: null },
+          planetRadius: { value: null },
+          atmosphereRadius: { value: null },
+        }
+      } );
+      var postPlane = new THREE.PlaneBufferGeometry( 2, 2 );
+      var postQuad = new THREE.Mesh( postPlane, this.scatterPass );
+      this._postScene = new THREE.Scene();
+      this._postScene.add(postQuad);
+
+      this._CreateLights();
+
+      return true;
+    }
+
+
+    _CreateLights() {
+      let light = new THREE.DirectionalLight(0xFFFFFF, 1);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x404040, 1);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x404040, 1);
+      light.position.set(100, 100, -100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.DirectionalLight(0x202040, 1);
+      light.position.set(100, -100, 100);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = false;
+      this._scene.add(light);
+
+      light = new THREE.AmbientLight(0xFFFFFF, 1.0);
+      this._scene.add(light);
+    }
+
+    _OnWindowResize() {
+      this._camera.aspect = window.innerWidth / window.innerHeight;
+      this._camera.updateProjectionMatrix();
+      this._threejs.setSize(window.innerWidth, window.innerHeight);
+      this._composer.setSize(window.innerWidth, window.innerHeight);
+      this._targets[0].setSize(window.innerWidth, window.innerHeight);
+      this._targets[1].setSize(window.innerWidth, window.innerHeight);
+      this._bloomPass.setSize(window.innerWidth, window.innerHeight);
+      this._fxassPass.setSize(window.innerWidth, window.innerHeight);
+    }
+
+    get Scene() {
+      return this._scene;
+    }
+
+    get Camera() {
+      return this._camera;
+    }
+
+    Render(timeInSeconds) {
+      const forward = new THREE.Vector3();
+      this._camera.getWorldDirection(forward);
+
+      let src = this._targets[0];
+      let dst = this._targets[1];
+      let tmp = null;
+
+      let firstDepth = dst;
+
+      this._threejs.autoClearDepth = false;
+      this._threejs.setRenderTarget(dst);
+      this._threejs.clear();
+      this._threejs.render(this._scene, this._camera);
+      this._threejs.setRenderTarget(null);
+
+      // tmp = src;
+      // src = dst;
+      // dst = tmp;
+      // this._fxassPass.render(this._threejs, src, dst, timeInSeconds, false);
+
+      // tmp = src;
+      // src = dst;
+      // dst = tmp;
+      this._bloomPass.render(this._threejs, src, dst, timeInSeconds, false);
+
+      this.scatterPass.uniforms.inverseProjection.value = this._camera.projectionMatrixInverse;
+      this.scatterPass.uniforms.inverseView.value = this._camera.matrixWorld;
+      this.scatterPass.uniforms.tDiffuse.value = dst.texture;
+      this.scatterPass.uniforms.tDepth.value = firstDepth.depthTexture;
+      this.scatterPass.uniforms.cameraNear.value = this._camera.near;
+      this.scatterPass.uniforms.cameraFar.value = this._camera.far;
+      this.scatterPass.uniforms.cameraPosition.value = this._camera.position;
+      this.scatterPass.uniforms.cameraForward.value = forward;
+      this.scatterPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0);
+      this.scatterPass.uniforms.planetRadius.value = 4000.0;
+      this.scatterPass.uniforms.atmosphereRadius.value = 4100.0;
+      this.scatterPass.uniformsNeedUpdate = true;
+
+      this._threejs.setRenderTarget(null);
+      this._threejs.render(this._postScene, this._postCamera);
+
+      this._stats.update();
+    }
+  }
+
+  return {
+    Graphics: _Graphics,
+    GetPixel: _GetPixel,
+    GetImageData: _GetImageData,
+  };
+})();

+ 406 - 0
src/main.js

@@ -0,0 +1,406 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {ColladaLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/ColladaLoader.js';
+import {FBXLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/FBXLoader.js';
+import {GLTFLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
+import {GUI} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/dat.gui.module.js';
+import {BufferGeometryUtils} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/utils/BufferGeometryUtils.js';
+
+import {agent} from './agent.js';
+import {controls} from './controls.js';
+import {game} from './game.js';
+import {math} from './math.js';
+import {terrain} from './terrain.js';
+import {visibility} from './visibility.js';
+
+import {particles} from './particles.js';
+import {blaster} from './blaster.js';
+
+
+let _APP = null;
+
+const _NUM_BOIDS = 100;
+const _BOID_SPEED = 100;
+const _BOID_ACCELERATION = _BOID_SPEED / 2.5;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 20.0;
+const _BOID_FORCE_ORIGIN = 50;
+const _BOID_FORCE_ALIGNMENT = 10;
+const _BOID_FORCE_SEPARATION = 20;
+const _BOID_FORCE_COLLISION = 50;
+const _BOID_FORCE_COHESION = 5;
+const _BOID_FORCE_WANDER = 3;
+
+
+class PlayerEntity {
+  constructor(params) {
+    this._model = params.model;
+    this._params = params;
+    this._game = params.game;
+    this._fireCooldown = 0.0;
+    this._velocity = new THREE.Vector3(0, 0, 0);
+    this._direction = new THREE.Vector3(0, 0, -1);
+    this._health = 1000.0;
+
+    const x = 2.75;
+    const y1 = 1.5;
+    const y2 = 0.4;
+    const z = 4.0;
+    this._offsets = [
+        new THREE.Vector3(-x, y1, -z),
+        new THREE.Vector3(x, y1, -z),
+        new THREE.Vector3(-x, -y2, -z),
+        new THREE.Vector3(x, -y2, -z),
+    ];
+
+    this._offsetIndex = 0;
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._model.uuid, this);
+  }
+
+  get Enemy() {
+    return false;
+  }
+
+  get Velocity() {
+    return this._velocity;
+  }
+
+  get Direction() {
+    return this._direction;
+  }
+
+  get Position() {
+    return this._model.position;
+  }
+
+  get Radius() {
+    return 1.0;
+  }
+
+  get Health() {
+    return this._health;
+  }
+
+  get Dead() {
+    return (this._health <= 0.0);
+  }
+
+  TakeDamage(dmg) {
+    this._params.game._entities['_explosionSystem'].Splode(this.Position);
+
+    this._health -= dmg;
+    if (this._health <= 0.0) {
+      this._game._visibilityGrid.RemoveItem(this._model.uuid, this._game._visibilityIndex);
+    }    
+  }
+
+  Fire() {
+    if (this._fireCooldown > 0.0) {
+      return;
+    }
+
+    this._fireCooldown = 0.05;
+
+    const p = this._params.game._entities['_blasterSystem'].CreateParticle();
+    p.Start = this._offsets[this._offsetIndex].clone();
+    p.Start.applyQuaternion(this._model.quaternion);
+    p.Start.add(this.Position);
+    p.End = p.Start.clone();
+    p.Velocity = this.Direction.clone().multiplyScalar(500.0);
+    p.Length = 50.0;
+    p.Colours = [
+        new THREE.Color(4.0, 0.5, 0.5), new THREE.Color(0.0, 0.0, 0.0)];
+    p.Life = 2.0;
+    p.TotalLife = 2.0;
+    p.Width = 0.25;
+
+    this._offsetIndex = (this._offsetIndex + 1) % this._offsets.length;
+  }
+
+  Update(timeInSeconds) {
+    if (this.Dead) {
+      return;
+    }
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._model.uuid, this, this._visibilityIndex);
+    this._fireCooldown -= timeInSeconds;
+    this._burstCooldown = Math.max(this._burstCooldown, 0.0);
+    this._direction.copy(this._velocity);
+    this._direction.normalize();
+    this._direction.applyQuaternion(this._model.quaternion);
+  }
+}
+
+
+class ExplodeParticles {
+  constructor(game) {
+    this._particleSystem = new particles.ParticleSystem(
+        game, {texture: "./resources/explosion.png"});
+    this._particles = [];
+  }
+
+  Splode(origin) {
+    for (let i = 0; i < 96; i++) {
+      const p = this._particleSystem.CreateParticle();
+      p.Position.copy(origin);
+      p.Velocity = new THREE.Vector3(
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1)
+      );
+      p.Velocity.normalize();
+      p.Velocity.multiplyScalar(50);
+      p.TotalLife = 2.0;
+      p.Life = p.TotalLife;
+      p.Colours = [new THREE.Color(0xFF8010), new THREE.Color(0xFF8010)];
+      p.Sizes = [4, 16];
+      p.Size = p.Sizes[0];
+      this._particles.push(p);
+    }
+  }
+
+  Update(timeInSeconds) {
+    const _V = new THREE.Vector3();
+
+    this._particles = this._particles.filter(p => {
+      return p.Alive;
+    });
+    for (const p of this._particles) {
+      p.Life -= timeInSeconds;
+      if (p.Life <= 0) {
+        p.Alive = false;
+      }
+      p.Position.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
+
+      _V.copy(p.Velocity);
+      _V.multiplyScalar(10.0 * timeInSeconds);
+      const velocityLength = p.Velocity.length();
+
+      if (_V.length() > velocityLength) {
+        _V.normalize();
+        _V.multiplyScalar(velocityLength)
+      }
+
+      p.Velocity.sub(_V);
+      p.Size = math.lerp(p.Life / p.TotalLife, p.Sizes[0], p.Sizes[1]);
+      p.Colour.copy(p.Colours[0]);
+      p.Colour.lerp(p.Colours[1], 1.0 - p.Life / p.TotalLife);
+      p.Opacity = math.smootherstep(p.Life / p.TotalLife, 0.0, 1.0);
+    }
+    this._particleSystem.Update();
+  }
+};
+
+
+class ProceduralTerrain_Demo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._CreateGUI();
+
+    this._userCamera = new THREE.Object3D();
+    this._userCamera.position.set(4100, 0, 0);
+
+    this._graphics.Camera.position.set(10340, 880, -2130);
+    this._graphics.Camera.quaternion.set(-0.032, 0.885, 0.062, 0.46);
+
+    this._score = 0;
+
+    // This is 2D but eh, whatever.
+    this._visibilityGrid = new visibility.VisibilityGrid(
+      [new THREE.Vector3(-10000, 0, -10000), new THREE.Vector3(10000, 0, 10000)],
+      [100, 100]);
+
+    this._entities['_explosionSystem'] = new ExplodeParticles(this);
+    this._entities['_blasterSystem'] = new blaster.BlasterSystem(
+        {
+            game: this,
+            texture: "./resources/blaster.jpg",
+            visibility: this._visibilityGrid,
+        });
+
+    this._entities['_terrain'] = new terrain.TerrainChunkManager({
+      camera: this._graphics.Camera,
+      scene: this._graphics.Scene,
+      gui: this._gui,
+      guiParams: this._guiParams,
+      game: this
+    });
+
+    this._library = {};
+
+    let loader = new GLTFLoader();
+    loader.setPath('./resources/models/x-wing/');
+    loader.load('scene.gltf', (gltf) => {
+      const model = gltf.scene.children[0];
+      model.scale.setScalar(0.5);
+
+      const group = new THREE.Group();
+      group.add(model);
+
+      this._graphics.Scene.add(group);
+
+      this._entities['player'] = new PlayerEntity(
+          {model: group, camera: this._graphics.Camera, game: this});
+
+      this._entities['_controls'] = new controls.ShipControls({
+        target: this._entities['player'],
+        camera: this._graphics.Camera,
+        scene: this._graphics.Scene,
+        domElement: this._graphics._threejs.domElement,
+        gui: this._gui,
+        guiParams: this._guiParams,
+      });
+    });
+
+    loader = new GLTFLoader();
+    loader.setPath('./resources/models/tie-fighter-gltf/');
+    loader.load('scene.gltf', (obj) => {
+      // This is bad, but I only want the mesh and I know this only has 1.
+      // This is what you get when you don't have an art pipeline and don't feel like making one.
+      obj.scene.traverse((c) => {
+        if (c.isMesh) {
+          const model = obj.scene.children[0];
+          model.scale.setScalar(0.05);
+          model.rotateX(Math.PI);
+
+          const mat = new THREE.MeshStandardMaterial({
+            map: new THREE.TextureLoader().load(
+                './resources/models/tie-fighter-gltf/textures/hullblue_baseColor.png'),
+            normalMap: new THREE.TextureLoader().load(
+                './resources/models/tie-fighter-gltf/textures/hullblue_normal.png'),
+          });
+
+          model.material = mat;
+
+          this._library['tie-fighter'] = model;
+        }
+
+        if (this._library['tie-fighter']) {
+          this._CreateEnemyShips();
+        }
+      });
+    });
+
+    this._LoadBackground();
+  }
+
+  _CreateEnemyShips() {
+    const positions = [
+      new THREE.Vector3(8000, 0, 0),
+      new THREE.Vector3(-7000, 50, -100),
+    ];
+    const colours = [
+      new THREE.Color(4.0, 0.5, 0.5),
+      new THREE.Color(0.5, 0.5, 4.0),
+    ];
+
+    for (let j = 0; j < 2; j++) {
+      const p = positions[j];
+
+      let loader = new GLTFLoader();
+      loader.setPath('./resources/models/star-destroyer/');
+      loader.load('scene.gltf', (gltf) => {
+        const model = gltf.scene.children[0];
+        model.scale.setScalar(20.0);
+        model.rotateZ(Math.PI / 2.0);
+
+        const cruiser = model;
+        cruiser.position.set(p.x, p.y, p.z);
+        cruiser.castShadow = true;
+        cruiser.receiveShadow = true;
+        cruiser.updateWorldMatrix();
+        this._graphics.Scene.add(cruiser);  
+      });
+
+      for (let i = 0; i < _NUM_BOIDS; i++) {
+        let params = {
+          mesh: this._library['tie-fighter'].clone(),
+          speedMin: 1.0,
+          speedMax: 1.0,
+          speed: _BOID_SPEED,
+          maxSteeringForce: _BOID_FORCE_MAX,
+          acceleration: _BOID_ACCELERATION,
+          seekGoal: p,
+          colour: colours[j],
+        };
+    
+        const e = new agent.Agent(this, params);
+        this._entities['_boid_' + i] = e;
+      }
+      break;
+    }
+  }
+
+  EnemyDied() {
+    this._score++;
+    document.getElementById('scoreText').innerText = this._score;
+  }
+
+  _CreateGUI() {
+    this._CreateGameGUI();
+    this._CreateControlGUI();
+  }
+
+  _CreateGameGUI() {
+    const guiDiv = document.createElement('div');
+    guiDiv.className = 'guiRoot guiBox';
+
+    const scoreDiv = document.createElement('div');
+    scoreDiv.className = 'vertical';
+
+    const scoreTitle = document.createElement('div');
+    scoreTitle.className = 'guiBigText';
+    scoreTitle.innerText = 'KILLS';
+
+    const scoreText = document.createElement('div');
+    scoreText.className = 'guiSmallText';
+    scoreText.innerText = '0';
+    scoreText.id = 'scoreText';
+
+    scoreDiv.appendChild(scoreTitle);
+    scoreDiv.appendChild(scoreText);
+
+    guiDiv.appendChild(scoreDiv);
+    document.body.appendChild(guiDiv);
+  }
+
+  _CreateControlGUI() {
+    this._guiParams = {
+      general: {
+      },
+    };
+    this._gui = new GUI();
+    this._gui.hide();
+
+    const generalRollup = this._gui.addFolder('General');
+    this._gui.close();
+  }
+
+  _LoadBackground() {
+    this._graphics.Scene.background = new THREE.Color(0xFFFFFF);
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/space-posx.jpg',
+        './resources/space-negx.jpg',
+        './resources/space-posy.jpg',
+        './resources/space-negy.jpg',
+        './resources/space-posz.jpg',
+        './resources/space-negz.jpg',
+    ]);
+    this._graphics._scene.background = texture;
+  }
+
+  _OnStep(timeInSeconds) {
+  }
+}
+
+
+function _Main() {
+  _APP = new ProceduralTerrain_Demo();
+}
+
+_Main();

+ 38 - 0
src/math.js

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

+ 48 - 0
src/noise.js

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

+ 104 - 0
src/particles.js

@@ -0,0 +1,104 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+export const particles = (function() {
+
+  const _VS = `
+attribute float size;
+attribute vec4 colour;
+
+varying vec4 vColour;
+
+void main() {
+  vColour = colour;
+  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
+  gl_PointSize = size * ( 300.0 / -mvPosition.z );
+  gl_Position = projectionMatrix * mvPosition;
+}
+`;
+
+  const _PS = `
+uniform sampler2D pointTexture;
+varying vec4 vColour;
+
+void main() {
+  gl_FragColor = vColour * vColour.w;
+  gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord );
+}
+`;
+
+  return {
+      ParticleSystem: class {
+        constructor(game, params) {
+          this._Initialize(game, params);
+        }
+
+        _Initialize(game, params) {
+          const uniforms = {
+          pointTexture: {
+                  value: new THREE.TextureLoader().load(params.texture)
+              }
+          };
+          this._material = new THREE.ShaderMaterial( {
+              uniforms: uniforms,
+              vertexShader: _VS,
+              fragmentShader: _PS,
+
+              blending: THREE.AdditiveBlending,
+              depthTest: true,
+              depthWrite: false,
+              transparent: true,
+              vertexColors: true
+          } );
+
+          this._geometry = new THREE.BufferGeometry();
+
+          this._particleSystem = new THREE.Points(this._geometry, this._material);
+          this._particleSystem.frustumCulled = false;
+
+          game._graphics._scene.add(this._particleSystem);
+
+          this._liveParticles = [];
+        }
+
+        CreateParticle() {
+          const p = {
+            Position: new THREE.Vector3(0, 0, 0),
+            Colour: new THREE.Color(),
+            Opacity: 1.0,
+            Size: 1,
+            Alive: true,
+          };
+          this._liveParticles.push(p);
+          return p;
+        }
+
+        Update() {
+          this._liveParticles = this._liveParticles.filter(p => {
+            return p.Alive;
+          });
+
+          const positions = [];
+          const colours = [];
+          const sizes = [];
+
+          for (const p of this._liveParticles) {
+            positions.push(p.Position.x, p.Position.y, p.Position.z);
+            colours.push(p.Colour.r, p.Colour.g, p.Colour.b, p.Opacity);
+            sizes.push(p.Size);
+          }
+
+          this._geometry.setAttribute(
+              'position', new THREE.Float32BufferAttribute(positions, 3));
+          this._geometry.setAttribute(
+              'colour', new THREE.Float32BufferAttribute(colours, 4));
+          this._geometry.setAttribute(
+              'size', new THREE.Float32BufferAttribute(sizes, 1).setUsage(
+                  THREE.DynamicDrawUsage));
+
+          this._geometry.attributes.position.needsUpdate = true;
+          this._geometry.attributes.colour.needsUpdate = true;
+          this._geometry.attributes.size.needsUpdate = true;
+        }
+      }
+  };
+})();

+ 550 - 0
src/perlin-noise.js

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

+ 187 - 0
src/quadtree.js

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

+ 414 - 0
src/scattering-shader.js

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

+ 115 - 0
src/sky.js

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

+ 536 - 0
src/space.js

@@ -0,0 +1,536 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {game} from './game.js';
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {visibility} from './visibility.js';
+import {particles} from './particles.js';
+import {blaster} from './blaster.js';
+import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/OBJLoader.js';
+
+let _APP = null;
+
+const _NUM_BOIDS = 300;
+const _BOID_SPEED = 25;
+const _BOID_ACCELERATION = _BOID_SPEED / 2.5;
+const _BOID_FORCE_MAX = _BOID_ACCELERATION / 20.0;
+const _BOID_FORCE_ORIGIN = 50;
+const _BOID_FORCE_ALIGNMENT = 10;
+const _BOID_FORCE_SEPARATION = 20;
+const _BOID_FORCE_COLLISION = 50;
+const _BOID_FORCE_COHESION = 5;
+const _BOID_FORCE_WANDER = 3;
+
+
+class LineRenderer {
+  constructor(game) {
+    this._game = game;
+
+    this._materials = {};
+    this._group = new THREE.Group();
+
+    this._game._graphics.Scene.add(this._group);
+  }
+
+  Reset() {
+    this._lines = [];
+    this._group.remove(...this._group.children);
+  }
+
+  Add(pt1, pt2, hexColour) {
+    const geometry = new THREE.Geometry();
+    geometry.vertices.push(pt1);
+    geometry.vertices.push(pt2);
+
+    let material = this._materials[hexColour];
+    if (!material) {
+      this._materials[hexColour] = new THREE.LineBasicMaterial(
+          {color: hexColour});
+      material = this._materials[hexColour];
+    }
+
+    const line = new THREE.Line(geometry, material);
+    this._lines.push(line);
+    this._group.add(line);
+  }
+}
+
+class ExplodeParticles {
+  constructor(game) {
+    this._particleSystem = new particles.ParticleSystem(
+        game, {texture: "./resources/blaster.jpg"});
+    this._particles = [];
+  }
+
+  Splode(origin) {
+    for (let i = 0; i < 128; i++) {
+      const p = this._particleSystem.CreateParticle();
+      p.Position.copy(origin);
+      p.Velocity = new THREE.Vector3(
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1)
+      );
+      p.Velocity.normalize();
+      p.Velocity.multiplyScalar(125);
+      p.TotalLife = 2.0;
+      p.Life = p.TotalLife;
+      p.Colours = [new THREE.Color(0xFF8000), new THREE.Color(0x800000)];
+      p.Sizes = [3, 12];
+      p.Size = p.Sizes[0];
+      this._particles.push(p);
+    }
+  }
+
+  Update(timeInSeconds) {
+    this._particles = this._particles.filter(p => {
+      return p.Alive;
+    });
+    for (const p of this._particles) {
+      p.Life -= timeInSeconds;
+      if (p.Life <= 0) {
+        p.Alive = false;
+      }
+      p.Position.add(p.Velocity.clone().multiplyScalar(timeInSeconds));
+      p.Velocity.multiplyScalar(0.75);
+      p.Size = math.lerp(p.Life / p.TotalLife, p.Sizes[0], p.Sizes[1]);
+      p.Colour.copy(p.Colours[0]);
+      p.Colour.lerp(p.Colours[1], 1.0 - p.Life / p.TotalLife);
+    }
+    this._particleSystem.Update();
+  }
+};
+
+
+class Boid {
+  constructor(game, params) {
+    this._mesh = new THREE.Mesh(
+        params.geometry,
+        new THREE.MeshStandardMaterial({color: 0x808080}));
+    this._mesh.castShadow = true;
+    this._mesh.receiveShadow = false;
+
+    this._group = new THREE.Group();
+    this._group.add(this._mesh);
+    this._group.position.set(
+        math.rand_range(-250, 250),
+        math.rand_range(-250, 250),
+        math.rand_range(-250, 250));
+    this._direction = new THREE.Vector3(
+        math.rand_range(-1, 1),
+        math.rand_range(-1, 1),
+        math.rand_range(-1, 1));
+    this._velocity = this._direction.clone();
+
+    const speedMultiplier = math.rand_range(params.speedMin, params.speedMax);
+    this._maxSteeringForce = params.maxSteeringForce * speedMultiplier;
+    this._maxSpeed  = params.speed * speedMultiplier;
+    this._acceleration = params.acceleration * speedMultiplier;
+
+    const scale = 1.0 / speedMultiplier;
+    this._radius = scale;
+    this._mesh.scale.setScalar(scale * params.scale);
+    //this._mesh.rotateX(Math.PI / 2);
+
+    this._game = game;
+    game._graphics.Scene.add(this._group);
+    this._visibilityIndex = game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this);
+
+    this._wanderAngle = 0;
+    this._seekGoal = params.seekGoal;
+    this._fireCooldown = 0.0;
+    this._params = params;
+  }
+
+  DisplayDebug() {
+    const geometry = new THREE.SphereGeometry(10, 64, 64);
+    const material = new THREE.MeshBasicMaterial({
+      color: 0xFF0000,
+      transparent: true,
+      opacity: 0.25,
+    });
+    const mesh = new THREE.Mesh(geometry, material);
+    this._group.add(mesh);
+
+    this._mesh.material.color.setHex(0xFF0000);
+    this._displayDebug = true;
+    this._lineRenderer = new LineRenderer(this._game);
+  }
+
+  _UpdateDebug(local) {
+    this._lineRenderer.Reset();
+    this._lineRenderer.Add(
+        this.Position, this.Position.clone().add(this._velocity),
+        0xFFFFFF);
+    for (const e of local) {
+      this._lineRenderer.Add(this.Position, e.Position, 0x00FF00);
+    }
+  }
+
+  get Position() {
+    return this._group.position;
+  }
+
+  get Velocity() {
+    return this._velocity;
+  }
+
+  get Direction() {
+    return this._direction;
+  }
+
+  get Radius() {
+    return this._radius;
+  }
+
+  Step(timeInSeconds) {
+    const local = this._game._visibilityGrid.GetLocalEntities(
+        this.Position, 15);
+
+    this._ApplySteering(timeInSeconds, local);
+
+    const frameVelocity = this._velocity.clone();
+    frameVelocity.multiplyScalar(timeInSeconds);
+    this._group.position.add(frameVelocity);
+
+    this._group.quaternion.setFromUnitVectors(
+        new THREE.Vector3(0, 1, 0), this.Direction);
+
+    this._visibilityIndex = this._game._visibilityGrid.UpdateItem(
+        this._mesh.uuid, this, this._visibilityIndex);
+
+    if (this._displayDebug) {
+      this._UpdateDebug(local);
+    }
+  }
+
+  _ApplySteering(timeInSeconds, local) {
+    const separationVelocity = this._ApplySeparation(local);
+
+    // Only apply alignment and cohesion to allies
+    const allies = local.filter((e) => {
+      return this._seekGoal.equals(e._seekGoal);
+    });
+
+    const enemies = local.filter((e) => {
+      return !this._seekGoal.equals(e._seekGoal);
+    });
+
+    this._fireCooldown -= timeInSeconds;
+    if (enemies.length > 0 && this._fireCooldown <= 0) {
+      const p = this._game._blasters.CreateParticle();
+      p.Start = this.Position.clone();
+      p.End = this.Position.clone();
+      p.Velocity = this.Direction.clone().multiplyScalar(300);
+      p.Length = 50;
+      p.Colours = [
+          this._params.colour.clone(), new THREE.Color(0.0, 0.0, 0.0)];
+      p.Life = 2.0;
+      p.TotalLife = 2.0;
+      p.Width = 0.25;
+
+      if (Math.random() < 0.025) {
+        this._game._explosionSystem.Splode(enemies[0].Position);
+      }
+      this._fireCooldown = 0.25;
+    }
+
+    const alignmentVelocity = this._ApplyAlignment(allies);
+    const cohesionVelocity = this._ApplyCohesion(allies);
+    const originVelocity = this._ApplySeek(this._seekGoal);
+    const wanderVelocity = this._ApplyWander();
+    const collisionVelocity = this._ApplyCollisionAvoidance();
+
+    const steeringForce = new THREE.Vector3(0, 0, 0);
+    steeringForce.add(separationVelocity);
+    steeringForce.add(alignmentVelocity);
+    steeringForce.add(cohesionVelocity);
+    steeringForce.add(originVelocity);
+    steeringForce.add(wanderVelocity);
+    steeringForce.add(collisionVelocity);
+
+    steeringForce.multiplyScalar(this._acceleration * timeInSeconds);
+
+    // Clamp the force applied
+    if (steeringForce.length() > this._maxSteeringForce) {
+      steeringForce.normalize();
+      steeringForce.multiplyScalar(this._maxSteeringForce);
+    }
+
+    this._velocity.add(steeringForce);
+
+    // Clamp velocity
+    if (this._velocity.length() > this._maxSpeed) {
+      this._velocity.normalize();
+      this._velocity.multiplyScalar(this._maxSpeed);
+    }
+
+    this._direction = this._velocity.clone();
+    this._direction.normalize();
+  }
+
+  _ApplyCollisionAvoidance() {
+    const colliders = this._game._visibilityGrid.GetGlobalItems();
+
+    const ray = new THREE.Ray(this.Position, this.Direction);
+    const force = new THREE.Vector3(0, 0, 0);
+
+    for (const c of colliders) {
+      if (c.Position.distanceTo(this.Position) > c.QuickRadius) {
+        continue;
+      }
+
+      const result = ray.intersectBox(c.AABB, new THREE.Vector3());
+      if (result) {
+        const distanceToCollision = result.distanceTo(this.Position);
+        if (distanceToCollision < 2) {
+          let a = 0;
+        }
+        const dirToCenter = c.Position.clone().sub(this.Position).normalize();
+        const dirToCollision = result.clone().sub(this.Position).normalize();
+        const steeringDirection = dirToCollision.sub(dirToCenter).normalize();
+        steeringDirection.multiplyScalar(_BOID_FORCE_COLLISION);
+        force.add(steeringDirection);
+      }
+    }
+
+    return force;
+  }
+
+  _ApplyWander() {
+    this._wanderAngle += 0.1 * math.rand_range(-2 * Math.PI, 2 * Math.PI);
+    const randomPointOnCircle = new THREE.Vector3(
+        Math.cos(this._wanderAngle),
+        0,
+        Math.sin(this._wanderAngle));
+    const pointAhead = this._direction.clone();
+    pointAhead.multiplyScalar(5);
+    pointAhead.add(randomPointOnCircle);
+    pointAhead.normalize();
+    return pointAhead.multiplyScalar(_BOID_FORCE_WANDER);
+  }
+
+  _ApplySeparation(local) {
+    if (local.length == 0) {
+      return new THREE.Vector3(0, 0, 0);
+    }
+
+    const forceVector = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      const distanceToEntity = Math.max(
+          e.Position.distanceTo(this.Position) - 1.5 * (this.Radius + e.Radius),
+          0.001);
+      const directionFromEntity = new THREE.Vector3().subVectors(
+          this.Position, e.Position);
+      const multiplier = (_BOID_FORCE_SEPARATION / distanceToEntity);
+      directionFromEntity.normalize();
+      forceVector.add(
+          directionFromEntity.multiplyScalar(multiplier));
+    }
+    return forceVector;
+  }
+
+  _ApplyAlignment(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    for (let e of local) {
+      const entityDirection = e.Direction;
+      forceVector.add(entityDirection);
+    }
+
+    forceVector.normalize();
+    forceVector.multiplyScalar(_BOID_FORCE_ALIGNMENT);
+
+    return forceVector;
+  }
+
+  _ApplyCohesion(local) {
+    const forceVector = new THREE.Vector3(0, 0, 0);
+
+    if (local.length == 0) {
+      return forceVector;
+    }
+
+    const averagePosition = new THREE.Vector3(0, 0, 0);
+    for (let e of local) {
+      averagePosition.add(e.Position);
+    }
+
+    averagePosition.multiplyScalar(1.0 / local.length);
+
+    const directionToAveragePosition = averagePosition.clone().sub(
+        this.Position);
+    directionToAveragePosition.normalize();
+    directionToAveragePosition.multiplyScalar(_BOID_FORCE_COHESION);
+
+    // HACK: Floating point error from accumulation of positions.
+    directionToAveragePosition.y = 0;
+
+    return directionToAveragePosition;
+  }
+
+  _ApplySeek(destination) {
+    const distance = Math.max(0,((
+        this.Position.distanceTo(destination) - 50) / 500)) ** 2;
+    const direction = destination.clone().sub(this.Position);
+    direction.normalize();
+
+    const forceVector = direction.multiplyScalar(
+        _BOID_FORCE_ORIGIN * distance);
+    return forceVector;
+  }
+}
+
+
+class OpenWorldDemo extends game.Game {
+  constructor() {
+    super();
+  }
+
+  _OnInitialize() {
+    this._entities = [];
+
+    this._bloomPass = this._graphics.AddPostFX(
+        graphics.PostFX.UnrealBloomPass,
+        {
+            threshold: 0.75,
+            strength: 2.5,
+            radius: 0,
+            resolution: {
+              x: 1024,
+              y: 1024,
+            }
+        });
+
+    this._glitchPass = this._graphics.AddPostFX(
+        graphics.PostFX.GlitchPass, {});
+    this._glitchCooldown = 15;
+
+    this._glitchPass.enabled = false;
+
+    this._LoadBackground();
+
+    const geometries = {};
+    const loader = new OBJLoader();
+    loader.load("./resources/fighter.obj", (result) => {
+      geometries.fighter = result.children[0].geometry;
+      loader.load("./resources/cruiser.obj", (result) => {
+        geometries.cruiser = result.children[0].geometry;
+        this._CreateBoids(geometries);
+      });
+    });
+    this._CreateEntities();
+  }
+
+  _LoadBackground() {
+    const loader = new THREE.CubeTextureLoader();
+    const texture = loader.load([
+        './resources/space-posx.jpg',
+        './resources/space-negx.jpg',
+        './resources/space-posy.jpg',
+        './resources/space-negy.jpg',
+        './resources/space-posz.jpg',
+        './resources/space-negz.jpg',
+    ]);
+    this._graphics._scene.background = texture;
+  }
+
+  _CreateEntities() {
+    // This is 2D but eh, whatever.
+    this._visibilityGrid = new visibility.VisibilityGrid(
+        [new THREE.Vector3(-500, 0, -500), new THREE.Vector3(500, 0, 500)],
+        [100, 100]);
+
+    this._explosionSystem = new ExplodeParticles(this);
+
+    this._blasters = new blaster.BlasterSystem(
+        this, {texture: "./resources/blaster.jpg"});
+  }
+
+  _CreateBoids(geometries) {
+    const positions = [
+        new THREE.Vector3(-200, 50, -100),
+        new THREE.Vector3(0, 0, 0)];
+    const colours = [
+        new THREE.Color(0.5, 0.5, 4.0),
+        new THREE.Color(4.0, 0.5, 0.5)
+    ];
+    for (let i = 0; i < 2; i++) {
+      const p = positions[i];
+      const cruiser = new THREE.Mesh(
+          geometries.cruiser,
+          new THREE.MeshStandardMaterial({
+              color: 0x404040
+          }));
+      cruiser.position.set(p.x, p.y, p.z);
+      cruiser.castShadow = true;
+      cruiser.receiveShadow = true;
+      cruiser.rotation.x = Math.PI / 2;
+      cruiser.scale.setScalar(10, 10, 10);
+      cruiser.updateWorldMatrix();
+      this._graphics.Scene.add(cruiser);
+
+      cruiser.geometry.computeBoundingBox();
+      const b = cruiser.geometry.boundingBox.clone().applyMatrix4(
+          cruiser.matrixWorld);
+
+      this._visibilityGrid.AddGlobalItem({
+        Position: p,
+        AABB: b,
+        QuickRadius: 200,
+        Velocity: new THREE.Vector3(0, 0, 0),
+        Direction: new THREE.Vector3(0, 1, 0),
+      });
+
+      let params = {
+        geometry: geometries.fighter,
+        speedMin: 1.0,
+        speedMax: 1.0,
+        speed: _BOID_SPEED,
+        maxSteeringForce: _BOID_FORCE_MAX,
+        acceleration: _BOID_ACCELERATION,
+        scale: 0.4,
+        seekGoal: p,
+        colour: colours[i]
+      };
+      for (let i = 0; i < _NUM_BOIDS; i++) {
+        const e = new Boid(this, params);
+        this._entities.push(e);
+      }
+    }
+
+    //this._entities[0].DisplayDebug();
+  }
+
+  _OnStep(timeInSeconds) {
+    timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
+
+    this._blasters.Update(timeInSeconds);
+    this._explosionSystem.Update(timeInSeconds);
+
+    this._glitchCooldown -= timeInSeconds;
+    if (this._glitchCooldown < 0) {
+      this._glitchCooldown = math.rand_range(5, 10);
+      this._glitchPass.enabled = !this._glitchPass.enabled;
+    }
+
+    this._StepEntities(timeInSeconds);
+  }
+
+  _StepEntities(timeInSeconds) {
+    if (this._entities.length == 0) {
+      return;
+    }
+
+    for (let e of this._entities) {
+      e.Step(timeInSeconds);
+    }
+  }
+}
+
+
+function _Main() {
+  _APP = new OpenWorldDemo();
+}
+
+_Main();

+ 76 - 0
src/spline.js

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

+ 309 - 0
src/terrain-chunk.js

@@ -0,0 +1,309 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+
+export const terrain_chunk = (function() {
+
+  class TerrainChunk {
+    constructor(params) {
+      this._params = params;
+      this._Init(params);
+    }
+    
+    Destroy() {
+      this._params.group.remove(this._plane);
+    }
+
+    Hide() {
+      this._plane.visible = false;
+    }
+
+    Show() {
+      this._plane.visible = true;
+    }
+
+    _Init(params) {
+      this._geometry = new THREE.BufferGeometry();
+      this._plane = new THREE.Mesh(this._geometry, params.material);
+      this._plane.castShadow = false;
+      this._plane.receiveShadow = true;
+      this._params.group.add(this._plane);
+    }
+
+    _GenerateHeight(v) {
+      return this._params.heightGenerators[0].Get(v.x, v.y, v.z)[0];
+    }
+
+    *_Rebuild() {
+      const _D = new THREE.Vector3();
+      const _D1 = new THREE.Vector3();
+      const _D2 = new THREE.Vector3();
+      const _P = new THREE.Vector3();
+      const _H = new THREE.Vector3();
+      const _W = new THREE.Vector3();
+      const _C = new THREE.Vector3();
+      const _S = new THREE.Vector3();
+
+      const _N = new THREE.Vector3();
+      const _N1 = new THREE.Vector3();
+      const _N2 = new THREE.Vector3();
+      const _N3 = new THREE.Vector3();
+
+      const positions = [];
+      const colors = [];
+      const normals = [];
+      const tangents = [];
+      const uvs = [];
+      const weights1 = [];
+      const weights2 = [];
+      const indices = [];
+      const wsPositions = [];
+
+      const localToWorld = this._params.group.matrix;
+      const resolution = this._params.resolution;
+      const radius = this._params.radius;
+      const offset = this._params.offset;
+      const width = this._params.width;
+      const half = width / 2;
+
+      for (let x = 0; x < resolution + 1; x++) {
+        const xp = width * x / resolution;
+        for (let y = 0; y < resolution + 1; y++) {
+          const yp = width * y / resolution;
+
+          // Compute position
+          _P.set(xp - half, yp - half, radius);
+          _P.add(offset);
+          _P.normalize();
+          _D.copy(_P);
+          _P.multiplyScalar(radius);
+          _P.z -= radius;
+
+          // Compute a world space position to sample noise
+          _W.copy(_P);
+          _W.applyMatrix4(localToWorld);
+
+          const height = this._GenerateHeight(_W) * 0.25;
+
+          // Purturb height along z-vector
+          _H.copy(_D);
+          _H.multiplyScalar(height);
+          _P.add(_H);
+
+          positions.push(_P.x, _P.y, _P.z);
+
+          _S.set(_W.x, _W.y, height);
+
+          const color = this._params.colourGenerator.GetColour(_S);
+          colors.push(color.r, color.g, color.b);
+          normals.push(_D.x, _D.y, _D.z);
+          tangents.push(1, 0, 0, 1);
+          wsPositions.push(_W.x, _W.y, height);
+          // TODO GUI
+          uvs.push(_P.x / 200.0, _P.y / 200.0);
+        }
+      }
+      yield;
+
+      for (let i = 0; i < resolution; i++) {
+        for (let j = 0; j < resolution; j++) {
+          indices.push(
+              i * (resolution + 1) + j,
+              (i + 1) * (resolution + 1) + j + 1,
+              i * (resolution + 1) + j + 1);
+          indices.push(
+              (i + 1) * (resolution + 1) + j,
+              (i + 1) * (resolution + 1) + j + 1,
+              i * (resolution + 1) + j);
+        }
+      }
+      yield;
+
+      const up = [...normals];
+
+      for (let i = 0, n = indices.length; i < n; i+= 3) {
+        const i1 = indices[i] * 3;
+        const i2 = indices[i+1] * 3;
+        const i3 = indices[i+2] * 3;
+
+        _N1.fromArray(positions, i1);
+        _N2.fromArray(positions, i2);
+        _N3.fromArray(positions, i3);
+
+        _D1.subVectors(_N3, _N2);
+        _D2.subVectors(_N1, _N2);
+        _D1.cross(_D2);
+
+        normals[i1] += _D1.x;
+        normals[i2] += _D1.x;
+        normals[i3] += _D1.x;
+
+        normals[i1+1] += _D1.y;
+        normals[i2+1] += _D1.y;
+        normals[i3+1] += _D1.y;
+
+        normals[i1+2] += _D1.z;
+        normals[i2+2] += _D1.z;
+        normals[i3+2] += _D1.z;
+      }
+      yield;
+
+      for (let i = 0, n = normals.length; i < n; i+=3) {
+        _N.fromArray(normals, i);
+        _N.normalize();
+        normals[i] = _N.x;
+        normals[i+1] = _N.y;
+        normals[i+2] = _N.z;
+      }
+      yield;
+
+      let count = 0;
+      for (let i = 0, n = indices.length; i < n; i+=3) {
+        const splats = [];
+        const i1 = indices[i] * 3;
+        const i2 = indices[i+1] * 3;
+        const i3 = indices[i+2] * 3;
+        const indexes = [i1, i2, i3];
+        for (let j = 0; j < 3; j++) {
+          const j1 = indexes[j];
+          _P.fromArray(wsPositions, j1);
+          _N.fromArray(normals, j1);
+          _D.fromArray(up, j1);
+          const s = this._params.colourGenerator.GetSplat(_P, _N, _D);
+          splats.push(s);
+        }
+
+        const splatStrengths = {};
+        for (let k in splats[0]) {
+          splatStrengths[k] = {key: k, strength: 0.0};
+        }
+        for (let curSplat of splats) {
+          for (let k in curSplat) {
+            splatStrengths[k].strength += curSplat[k].strength;
+          }
+        }
+
+        let typeValues = Object.values(splatStrengths);
+        typeValues.sort((a, b) => {
+          if (a.strength < b.strength) {
+            return 1;
+          }
+          if (a.strength > b.strength) {
+            return -1;
+          }
+          return 0;
+        });
+
+        const w1 = indices[i] * 4;
+        const w2 = indices[i+1] * 4;
+        const w3 = indices[i+2] * 4;
+
+        for (let s = 0; s < 3; s++) {
+          let total = (
+              splats[s][typeValues[0].key].strength +
+              splats[s][typeValues[1].key].strength +
+              splats[s][typeValues[2].key].strength +
+              splats[s][typeValues[3].key].strength);
+          const normalization = 1.0 / total;
+
+          splats[s][typeValues[0].key].strength *= normalization;
+          splats[s][typeValues[1].key].strength *= normalization;
+          splats[s][typeValues[2].key].strength *= normalization;
+          splats[s][typeValues[3].key].strength *= normalization;
+        }
+ 
+        weights1.push(splats[0][typeValues[3].key].index);
+        weights1.push(splats[0][typeValues[2].key].index);
+        weights1.push(splats[0][typeValues[1].key].index);
+        weights1.push(splats[0][typeValues[0].key].index);
+
+        weights1.push(splats[1][typeValues[3].key].index);
+        weights1.push(splats[1][typeValues[2].key].index);
+        weights1.push(splats[1][typeValues[1].key].index);
+        weights1.push(splats[1][typeValues[0].key].index);
+
+        weights1.push(splats[2][typeValues[3].key].index);
+        weights1.push(splats[2][typeValues[2].key].index);
+        weights1.push(splats[2][typeValues[1].key].index);
+        weights1.push(splats[2][typeValues[0].key].index);
+
+        weights2.push(splats[0][typeValues[3].key].strength);
+        weights2.push(splats[0][typeValues[2].key].strength);
+        weights2.push(splats[0][typeValues[1].key].strength);
+        weights2.push(splats[0][typeValues[0].key].strength);
+
+        weights2.push(splats[1][typeValues[3].key].strength);
+        weights2.push(splats[1][typeValues[2].key].strength);
+        weights2.push(splats[1][typeValues[1].key].strength);
+        weights2.push(splats[1][typeValues[0].key].strength);
+
+        weights2.push(splats[2][typeValues[3].key].strength);
+        weights2.push(splats[2][typeValues[2].key].strength);
+        weights2.push(splats[2][typeValues[1].key].strength);
+        weights2.push(splats[2][typeValues[0].key].strength);
+
+        count++;
+        if ((count % 1000) == 0) {
+          yield;
+        }
+      }
+      yield;
+
+      function _Unindex(src, stride) {
+        const dst = [];
+        for (let i = 0, n = indices.length; i < n; i+= 3) {
+          const i1 = indices[i] * stride;
+          const i2 = indices[i+1] * stride;
+          const i3 = indices[i+2] * stride;
+
+          for (let j = 0; j < stride; j++) {
+            dst.push(src[i1 + j]);
+          }
+          for (let j = 0; j < stride; j++) {
+            dst.push(src[i2 + j]);
+          }
+          for (let j = 0; j < stride; j++) {
+            dst.push(src[i3 + j]);
+          }
+        }
+        return dst;
+      }
+
+      const uiPositions = _Unindex(positions, 3);
+      yield;
+
+      const uiColours = _Unindex(colors, 3);
+      yield;
+
+      const uiNormals = _Unindex(normals, 3);
+      yield;
+
+      const uiTangents = _Unindex(tangents, 4);
+      yield;
+
+      const uiUVs = _Unindex(uvs, 2);
+      yield;
+
+      const uiWeights1 = weights1;
+      const uiWeights2 = weights2;
+
+      this._geometry.setAttribute(
+          'position', new THREE.Float32BufferAttribute(uiPositions, 3));
+      this._geometry.setAttribute(
+          'color', new THREE.Float32BufferAttribute(uiColours, 3));
+      this._geometry.setAttribute(
+          'normal', new THREE.Float32BufferAttribute(uiNormals, 3));
+      this._geometry.setAttribute(
+          'tangent', new THREE.Float32BufferAttribute(uiTangents, 4));
+      this._geometry.setAttribute(
+          'weights1', new THREE.Float32BufferAttribute(uiWeights1, 4));
+      this._geometry.setAttribute(
+          'weights2', new THREE.Float32BufferAttribute(uiWeights2, 4));
+      this._geometry.setAttribute(
+          'uv', new THREE.Float32BufferAttribute(uiUVs, 2));
+    }
+  }
+
+  return {
+    TerrainChunk: TerrainChunk
+  }
+})();

+ 317 - 0
src/terrain-shader.js

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

+ 552 - 0
src/terrain.js

@@ -0,0 +1,552 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import {graphics} from './graphics.js';
+import {math} from './math.js';
+import {noise} from './noise.js';
+import {quadtree} from './quadtree.js';
+import {spline} from './spline.js';
+import {terrain_chunk} from './terrain-chunk.js';
+import {terrain_shader} from './terrain-shader.js';
+import {textures} from './textures.js';
+import {utils} from './utils.js';
+
+export const terrain = (function() {
+
+  const _WHITE = new THREE.Color(0x808080);
+
+  const _DEEP_OCEAN = new THREE.Color(0x20020FF);
+  const _SHALLOW_OCEAN = new THREE.Color(0x8080FF);
+  const _BEACH = new THREE.Color(0xd9d592);
+  const _SNOW = new THREE.Color(0xFFFFFF);
+  const _ApplyWeightsOREST_TROPICAL = new THREE.Color(0x4f9f0f);
+  const _ApplyWeightsOREST_TEMPERATE = new THREE.Color(0x2b960e);
+  const _ApplyWeightsOREST_BOREAL = new THREE.Color(0x29c100);
+  
+  const _GREEN = new THREE.Color(0x80FF80);
+  const _RED = new THREE.Color(0xFF8080);
+  const _BLACK = new THREE.Color(0x000000);
+  
+  const _MIN_CELL_SIZE = 500;
+  const _MIN_CELL_RESOLUTION = 96;
+  const _PLANET_RADIUS = 4000;
+
+
+  class HeightGenerator {
+    constructor(generator, position, minRadius, maxRadius) {
+      this._position = position.clone();
+      this._radius = [minRadius, maxRadius];
+      this._generator = generator;
+    }
+  
+    Get(x, y, z) {
+      return [this._generator.Get(x, y, z), 1];
+    }
+  }
+  
+  
+  class FixedHeightGenerator {
+    constructor() {}
+  
+    Get() {
+      return [50, 1];
+    }
+  }
+   
+
+  class TextureSplatter {
+    constructor(params) {
+      const _colourLerp = (t, p0, p1) => {
+        const c = p0.clone();
+  
+        return c.lerp(p1, t);
+      };
+      this._colourSpline = [
+        new spline.LinearSpline(_colourLerp),
+        new spline.LinearSpline(_colourLerp)
+      ];
+
+      // Arid
+      this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d));
+      this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc));
+      this._colourSpline[0].AddPoint(1.0, _SNOW);
+  
+      // Humid
+      this._colourSpline[1].AddPoint(0.0, _ApplyWeightsOREST_BOREAL);
+      this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c));
+      this._colourSpline[1].AddPoint(1.0, _SNOW);
+
+      this._oceanSpline = new spline.LinearSpline(_colourLerp);
+      this._oceanSpline.AddPoint(0, _DEEP_OCEAN);
+      this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN);
+      this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN);
+
+      this._params = params;
+    }
+  
+    _BaseColour(x, y, z) {
+      const m = this._params.biomeGenerator.Get(x, y, z);
+      const h = math.sat(z / 100.0);
+  
+      const c1 = this._colourSpline[0].Get(h);
+      const c2 = this._colourSpline[1].Get(h);
+  
+      let c = c1.lerp(c2, m);
+
+      if (h < 0.1) {
+        c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05));
+      }
+      return c;      
+    }
+
+    _Colour(x, y, z) {
+      const c = this._BaseColour(x, y, z);
+      const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0;
+
+      c.offsetHSL(0.0, 0.0, r * 0.01);
+      return c;
+    }
+
+    _GetTextureWeights(p, n, up) {
+      const m = this._params.biomeGenerator.Get(p.x, p.y, p.z);
+      const h = p.z / (100.0 * 0.25);
+
+      const types = {
+        dirt: {index: 0, strength: 0.0},
+        grass: {index: 1, strength: 0.0},
+        gravel: {index: 2, strength: 0.0},
+        rock: {index: 3, strength: 0.0},
+        snow: {index: 4, strength: 0.0},
+        snowrock: {index: 5, strength: 0.0},
+        cobble: {index: 6, strength: 0.0},
+        sandyrock: {index: 7, strength: 0.0},
+      };
+
+      function _ApplyWeights(dst, v, m) {
+        for (let k in types) {
+          types[k].strength *= m;
+        }
+        types[dst].strength = v;
+      };
+
+      types.grass.strength = 1.0;
+      _ApplyWeights('gravel', 1.0 - m, m);
+
+      if (h < 0.2) {
+        const s = 1.0 - math.sat((h - 0.1) / 0.05);
+        _ApplyWeights('cobble', s, 1.0 - s);
+
+        if (h < 0.1) {
+          const s = 1.0 - math.sat((h - 0.05) / 0.05);
+          _ApplyWeights('sandyrock', s, 1.0 - s);
+        }
+      } else {
+        if (h > 0.125) {
+          const s = (math.sat((h - 0.125) / 1.25));
+          _ApplyWeights('rock', s, 1.0 - s);
+        }
+
+        if (h > 1.5) {
+          const s = math.sat((h - 0.75) / 2.0);
+          _ApplyWeights('snow', s, 1.0 - s);
+        }
+      }
+
+      // In case nothing gets set.
+      types.dirt.strength = 0.01;
+
+      let total = 0.0;
+      for (let k in types) {
+        total += types[k].strength;
+      }
+      if (total < 0.01) {
+        const a = 0;
+      }
+      const normalization = 1.0 / total;
+
+      for (let k in types) {
+        types[k].strength / normalization;
+      }
+
+      return types;
+    }
+
+    GetColour(position) {
+      return this._Colour(position.x, position.y, position.z);
+    }
+
+    GetSplat(position, normal, up) {
+      return this._GetTextureWeights(position, normal, up);
+    }
+  }
+
+  
+  class FixedColourGenerator {
+    constructor(params) {
+      this._params = params;
+    }
+  
+    Get() {
+      return this._params.colour;
+    }
+  }
+  
+  
+
+  class TerrainChunkRebuilder {
+    constructor(params) {
+      this._pool = {};
+      this._params = params;
+      this._Reset();
+    }
+
+    AllocateChunk(params) {
+      const w = params.width;
+
+      if (!(w in this._pool)) {
+        this._pool[w] = [];
+      }
+
+      let c = null;
+      if (this._pool[w].length > 0) {
+        c = this._pool[w].pop();
+        c._params = params;
+      } else {
+        c = new terrain_chunk.TerrainChunk(params);
+      }
+
+      c.Hide();
+
+      this._queued.push(c);
+
+      return c;    
+    }
+
+    _RecycleChunks(chunks) {
+      for (let c of chunks) {
+        if (!(c.chunk._params.width in this._pool)) {
+          this._pool[c.chunk._params.width] = [];
+        }
+
+        c.chunk.Destroy();
+      }
+    }
+
+    _Reset() {
+      this._active = null;
+      this._queued = [];
+      this._old = [];
+      this._new = [];
+    }
+
+    get Busy() {
+      return this._active || this._queued.length > 0;
+    }
+
+    Rebuild(chunks) {
+      if (this.Busy) {
+        return;
+      }
+      for (let k in chunks) {
+        this._queued.push(chunks[k].chunk);
+      }
+    }
+
+    Update() {
+      if (this._active) {
+        const r = this._active.next();
+        if (r.done) {
+          this._active = null;
+        }
+      } else {
+        const b = this._queued.pop();
+        if (b) {
+          this._active = b._Rebuild();
+          this._new.push(b);
+        }
+      }
+
+      if (this._active) {
+        return;
+      }
+
+      if (!this._queued.length) {
+        this._RecycleChunks(this._old);
+        for (let b of this._new) {
+          b.Show();
+        }
+        this._Reset();
+      }
+    }
+  }
+
+  class TerrainChunkManager {
+    constructor(params) {
+      this._Init(params);
+    }
+
+    _Init(params) {
+      this._params = params;
+
+      const loader = new THREE.TextureLoader();
+
+      const noiseTexture = loader.load('./resources/simplex-noise.png');
+      noiseTexture.wrapS = THREE.RepeatWrapping;
+      noiseTexture.wrapT = THREE.RepeatWrapping;
+
+      const diffuse = new textures.TextureAtlas(params);
+      diffuse.Load('diffuse', [
+        './resources/dirt_01_diffuse-1024.png',
+        './resources/grass1-albedo3-1024.png',
+        './resources/sandyground-albedo-1024.png',
+        './resources/worn-bumpy-rock-albedo-1024.png',
+        './resources/rock-snow-ice-albedo-1024.png',
+        './resources/snow-packed-albedo-1024.png',
+        './resources/rough-wet-cobble-albedo-1024.png',
+        './resources/sandy-rocks1-albedo-1024.png',
+      ]);
+      diffuse.onLoad = () => {     
+        this._material.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas;
+      };
+
+      const normal = new textures.TextureAtlas(params);
+      normal.Load('normal', [
+        './resources/dirt_01_normal-1024.jpg',
+        './resources/grass1-normal-1024.jpg',
+        './resources/sandyground-normal-1024.jpg',
+        './resources/worn-bumpy-rock-normal-1024.jpg',
+        './resources/rock-snow-ice-normal-1024.jpg',
+        './resources/snow-packed-normal-1024.jpg',
+        './resources/rough-wet-cobble-normal-1024.jpg',
+        './resources/sandy-rocks1-normal-1024.jpg',
+      ]);
+      normal.onLoad = () => {     
+        this._material.uniforms.normalMap.value = normal.Info['normal'].atlas;
+      };
+
+      this._material = new THREE.MeshStandardMaterial({
+        wireframe: false,
+        wireframeLinewidth: 1,
+        color: 0xFFFFFF,
+        side: THREE.FrontSide,
+        vertexColors: THREE.VertexColors,
+        // normalMap: texture,
+      });
+
+      this._material = new THREE.RawShaderMaterial({
+        uniforms: {
+          diffuseMap: {
+          },
+          normalMap: {
+          },
+          noiseMap: {
+            value: noiseTexture
+          },
+        },
+        vertexShader: terrain_shader.VS,
+        fragmentShader: terrain_shader.PS,
+        side: THREE.FrontSide
+      });
+
+      this._builder = new TerrainChunkRebuilder();
+
+      this._InitNoise(params);
+      this._InitBiomes(params);
+      this._InitTerrain(params);
+    }
+
+    _InitNoise(params) {
+      params.guiParams.noise = {
+        octaves: 10,
+        persistence: 0.5,
+        lacunarity: 1.6,
+        exponentiation: 7.5,
+        height: 900.0,
+        scale: 1800.0,
+        seed: 1
+      };
+
+      const onNoiseChanged = () => {
+        this._builder.Rebuild(this._chunks);
+      };
+
+      const noiseRollup = params.gui.addFolder('Terrain.Noise');
+      noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange(
+          onNoiseChanged);
+
+      this._noise = new noise.Noise(params.guiParams.noise);
+
+      params.guiParams.heightmap = {
+        height: 16,
+      };
+
+      const heightmapRollup = params.gui.addFolder('Terrain.Heightmap');
+      heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange(
+          onNoiseChanged);
+    }
+
+    _InitBiomes(params) {
+      params.guiParams.biomes = {
+        octaves: 2,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        scale: 2048.0,
+        noiseType: 'simplex',
+        seed: 2,
+        exponentiation: 1,
+        height: 1.0
+      };
+
+      const onNoiseChanged = () => {
+        this._builder.Rebuild(this._chunks);
+      };
+
+      const noiseRollup = params.gui.addFolder('Terrain.Biomes');
+      noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange(
+          onNoiseChanged);
+      noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange(
+          onNoiseChanged);
+
+      this._biomes = new noise.Noise(params.guiParams.biomes);
+
+      const colourParams = {
+        octaves: 1,
+        persistence: 0.5,
+        lacunarity: 2.0,
+        exponentiation: 1.0,
+        scale: 256.0,
+        noiseType: 'simplex',
+        seed: 2,
+        height: 1.0,
+      };
+      this._colourNoise = new noise.Noise(colourParams);
+    }
+
+    _InitTerrain(params) {
+      params.guiParams.terrain= {
+        wireframe: false,
+      };
+
+      this._groups = [...new Array(6)].map(_ => new THREE.Group());
+      params.scene.add(...this._groups);
+
+      const terrainRollup = params.gui.addFolder('Terrain');
+      terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => {
+        for (let k in this._chunks) {
+          this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe;
+        }
+      });
+
+      this._chunks = {};
+      this._params = params;
+    }
+
+    _CellIndex(p) {
+      const xp = p.x + _MIN_CELL_SIZE * 0.5;
+      const yp = p.z + _MIN_CELL_SIZE * 0.5;
+      const x = Math.floor(xp / _MIN_CELL_SIZE);
+      const z = Math.floor(yp / _MIN_CELL_SIZE);
+      return [x, z];
+    }
+
+    _CreateTerrainChunk(group, offset, width, resolution) {
+      const params = {
+        group: group,
+        material: this._material,
+        width: width,
+        offset: offset,
+        radius: _PLANET_RADIUS,
+        resolution: resolution,
+        biomeGenerator: this._biomes,
+        colourGenerator: new TextureSplatter({biomeGenerator: this._biomes, colourNoise: this._colourNoise}),
+        heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)],
+      };
+
+      return this._builder.AllocateChunk(params);
+    }
+
+    Update(_) {
+      this._builder.Update();
+      if (!this._builder.Busy) {
+        this._UpdateVisibleChunks_Quadtree();
+      }
+    }
+
+    _UpdateVisibleChunks_Quadtree() {
+      function _Key(c) {
+        return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']';
+      }
+
+      const q = new quadtree.CubeQuadTree({
+        radius: _PLANET_RADIUS,
+        min_node_size: _MIN_CELL_SIZE,
+      });
+      q.Insert(this._params.camera.position);
+
+      const sides = q.GetChildren();
+
+      let newTerrainChunks = {};
+      const center = new THREE.Vector3();
+      const dimensions = new THREE.Vector3();
+      for (let i = 0; i < sides.length; i++) {
+        this._groups[i].matrix = sides[i].transform;
+        this._groups[i].matrixAutoUpdate = false;
+        for (let c of sides[i].children) {
+          c.bounds.getCenter(center);
+          c.bounds.getSize(dimensions);
+  
+          const child = {
+            index: i,
+            group: this._groups[i],
+            position: [center.x, center.y, center.z],
+            bounds: c.bounds,
+            size: dimensions.x,
+          };
+  
+          const k = _Key(child);
+          newTerrainChunks[k] = child;
+        }
+      }
+
+      const intersection = utils.DictIntersection(this._chunks, newTerrainChunks);
+      const difference = utils.DictDifference(newTerrainChunks, this._chunks);
+      const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks));
+
+      this._builder._old.push(...recycle);
+
+      newTerrainChunks = intersection;
+
+      for (let k in difference) {
+        const [xp, yp, zp] = difference[k].position;
+
+        const offset = new THREE.Vector3(xp, yp, zp);
+        newTerrainChunks[k] = {
+          position: [xp, zp],
+          chunk: this._CreateTerrainChunk(
+              difference[k].group, offset, difference[k].size, _MIN_CELL_RESOLUTION),
+        };
+      }
+
+      this._chunks = newTerrainChunks;
+    }
+  }
+
+  return {
+    TerrainChunkManager: TerrainChunkManager
+  }
+})();

+ 84 - 0
src/textures.js

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

+ 21 - 0
src/utils.js

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

+ 87 - 0
src/visibility.js

@@ -0,0 +1,87 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import {math} from './math.js';
+
+export const visibility = (function() {
+  return {
+      VisibilityGrid: class {
+        constructor(bounds, dimensions) {
+          const [x, y] = dimensions;
+          this._cells = [...Array(x)].map(_ => [...Array(y)].map(_ => ({})));
+          this._dimensions = dimensions;
+          this._bounds = bounds;
+          this._cellSize = bounds[1].clone().sub(bounds[0]);
+          this._cellSize.multiply(
+              new THREE.Vector3(1.0 / dimensions[0], 0, 1.0 / dimensions[1]));
+          this._globalItems = [];
+        }
+
+        AddGlobalItem(entity) {
+          this._globalItems.push(entity);
+        }
+
+        GetGlobalItems() {
+          return [...this._globalItems];
+        }
+
+        RemoveItem(uuid, previous) {
+          const [prevX, prevY] = previous;
+
+          delete this._cells[prevX][prevY][uuid];
+        }
+
+        UpdateItem(uuid, entity, previous=null) {
+          const [x, y] = this._GetCellIndex(entity.Position);
+
+          if (previous) {
+            const [prevX, prevY] = previous;
+            if (prevX == x && prevY == y) {
+              return [x, y];
+            }
+
+            delete this._cells[prevX][prevY][uuid];
+          }
+          this._cells[x][y][uuid] = entity;
+
+          return [x, y];
+        }
+
+        GetLocalEntities(position, radius) {
+          const [x, y] = this._GetCellIndex(position);
+
+          const cellSize = Math.min(this._cellSize.x, this._cellSize.z);
+          const cells = Math.ceil(radius / cellSize);
+
+          let local = [];
+          const xMin = Math.max(x - cells, 0);
+          const yMin = Math.max(y - cells, 0);
+          const xMax = Math.min(this._dimensions[0] - 1, x + cells);
+          const yMax = Math.min(this._dimensions[1] - 1, y + cells);
+          for (let xi = xMin; xi <= xMax; xi++) {
+            for (let yi = yMin; yi <= yMax; yi++) {
+              local.push(...Object.values(this._cells[xi][yi]));
+            }
+          }
+
+          local = local.filter((e) => {
+            const distance = e.Position.distanceTo(position);
+
+            return distance != 0.0 && distance < radius;
+          });
+
+          return local;
+        }
+
+        _GetCellIndex(position) {
+          const x = math.sat((this._bounds[0].x - position.x) / (
+              this._bounds[0].x - this._bounds[1].x));
+          const y = math.sat((this._bounds[0].z - position.z) / (
+              this._bounds[0].z - this._bounds[1].z));
+
+          const xIndex = Math.floor(x * (this._dimensions[0] - 1));
+          const yIndex = Math.floor(y * (this._dimensions[1] - 1));
+
+          return [xIndex, yIndex];
+        }
+      }
+  };
+})();