Browse Source

Initial commit

Simon 3 years ago
parent
commit
cb3a12be72
93 changed files with 15177 additions and 0 deletions
  1. 80 0
      base.css
  2. 25 0
      index.html
  3. BIN
      resources/crawl.png
  4. BIN
      resources/crawl.xcf
  5. BIN
      resources/models/star-destroyer/collision.blend
  6. BIN
      resources/models/star-destroyer/collision.blend1
  7. BIN
      resources/models/star-destroyer/scene-collision.glb
  8. BIN
      resources/models/star-destroyer/scene-final.glb
  9. BIN
      resources/models/star-destroyer/scene.bin
  10. 7498 0
      resources/models/star-destroyer/scene.gltf
  11. BIN
      resources/models/star-destroyer/star-destroyer.blend
  12. BIN
      resources/models/star-destroyer/star-destroyer.blend1
  13. BIN
      resources/models/star-destroyer/textures/Body_Top_baseColor.png
  14. BIN
      resources/models/star-destroyer/textures/Body_Top_emissive.png
  15. BIN
      resources/models/star-destroyer/textures/Body_Top_metallicRoughness.png
  16. BIN
      resources/models/star-destroyer/textures/Body_baseColor.png
  17. BIN
      resources/models/star-destroyer/textures/Body_emissive.png
  18. BIN
      resources/models/star-destroyer/textures/Body_metallicRoughness.png
  19. BIN
      resources/models/star-destroyer/textures/Bridge_Thing_emissive.png
  20. BIN
      resources/models/star-destroyer/textures/Details_baseColor.png
  21. BIN
      resources/models/star-destroyer/textures/Details_metallicRoughness.png
  22. BIN
      resources/models/star-destroyer/textures/Engines_baseColor.png
  23. BIN
      resources/models/star-destroyer/textures/Engines_emissive.png
  24. BIN
      resources/models/star-destroyer/textures/Material.004_baseColor.jpeg
  25. BIN
      resources/models/star-destroyer/textures/Material.004_metallicRoughness.png
  26. BIN
      resources/models/star-destroyer/textures/Turret_baseColor.png
  27. BIN
      resources/models/star-destroyer/textures/Turret_metallicRoughness.png
  28. BIN
      resources/models/star-destroyer/turret.blend
  29. BIN
      resources/models/star-destroyer/turret.glb
  30. BIN
      resources/models/tie-fighter-gltf/scene.bin
  31. 298 0
      resources/models/tie-fighter-gltf/scene.gltf
  32. BIN
      resources/models/tie-fighter-gltf/textures/hullblue_baseColor.png
  33. BIN
      resources/models/tie-fighter-gltf/textures/hullblue_metallicRoughness.png
  34. BIN
      resources/models/tie-fighter-gltf/textures/hullblue_normal.png
  35. BIN
      resources/models/x-wing/scene.bin
  36. 921 0
      resources/models/x-wing/scene.gltf
  37. BIN
      resources/models/x-wing/textures/lambert7_baseColor.png
  38. BIN
      resources/models/x-wing/textures/lambert7_metallicRoughness.png
  39. BIN
      resources/models/x-wing/textures/lambert7_normal.png
  40. BIN
      resources/models/x-wing/textures/lambert8_baseColor.png
  41. BIN
      resources/models/x-wing/textures/lambert8_metallicRoughness.png
  42. BIN
      resources/models/x-wing/textures/lambert8_normal.png
  43. BIN
      resources/sounds/explosion.ogg
  44. BIN
      resources/sounds/laser.ogg
  45. BIN
      resources/sounds/shields.ogg
  46. BIN
      resources/terrain/space-negx.jpg
  47. BIN
      resources/terrain/space-negy.jpg
  48. BIN
      resources/terrain/space-negz.jpg
  49. BIN
      resources/terrain/space-posx.jpg
  50. BIN
      resources/terrain/space-posy.jpg
  51. BIN
      resources/terrain/space-posz.jpg
  52. BIN
      resources/textures/fx/blaster.jpg
  53. BIN
      resources/textures/fx/fire.png
  54. BIN
      resources/textures/fx/smoke.png
  55. 238 0
      src/ammojs-component.js
  56. 98 0
      src/atmosphere-effect.js
  57. 77 0
      src/basic-rigid-body.js
  58. 139 0
      src/crawl-controller.js
  59. 147 0
      src/crosshair.js
  60. 233 0
      src/enemy-ai-controller.js
  61. 89 0
      src/entity-manager.js
  62. 213 0
      src/entity.js
  63. 269 0
      src/explode-component.js
  64. 143 0
      src/floating-descriptor.js
  65. 210 0
      src/fx/blaster.js
  66. 89 0
      src/health-controller.js
  67. 135 0
      src/load-controller.js
  68. 167 0
      src/main.js
  69. 42 0
      src/math.js
  70. 128 0
      src/mesh-rigid-body.js
  71. 45 0
      src/noise.js
  72. 347 0
      src/particle-system.js
  73. 123 0
      src/player-controller.js
  74. 111 0
      src/player-input.js
  75. 75 0
      src/player-ps4-input.js
  76. 167 0
      src/render-component.js
  77. 248 0
      src/shields-controller.js
  78. 204 0
      src/shields-ui-controller.js
  79. 109 0
      src/ship-effects.js
  80. 479 0
      src/simplex-noise.js
  81. 52 0
      src/spatial-grid-controller.js
  82. 164 0
      src/spatial-hash-grid.js
  83. 342 0
      src/spawners.js
  84. 85 0
      src/star-destroyer-fighter-controller.js
  85. 30 0
      src/star-destroyer-turret.js
  86. 61 0
      src/third-person-camera.js
  87. 8 0
      src/three-defs.js
  88. 409 0
      src/threejs-component.js
  89. 203 0
      src/tie-fighter-controller.js
  90. 266 0
      src/turret-controller.js
  91. 77 0
      src/ui-controller.js
  92. 212 0
      src/xwing-controller.js
  93. 121 0
      src/xwing-effects.js

+ 80 - 0
base.css

@@ -0,0 +1,80 @@
+body {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}
+
+.container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.ui {
+  width: 100%;
+  height: 100%;            
+  position: absolute;
+  top: 0;
+  left: 0;
+  font-family: 'Odibee Sans', cursive;
+  font-size: 40px;
+}
+
+.row {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: flex-end;
+}
+
+.chat-ui {
+  display: flex;
+  flex-direction: column;
+  background: rgba(1.0, 1.0, 1.0, 0.0);
+  width: 800px;
+  height: 200px;
+  padding: 10px 10px;
+  padding-bottom: 200px;
+  border-radius: 10px;
+}
+
+.chat-ui-text-area {
+  display: flex;
+  height: 100%;
+  width: 100%;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.chat-text {
+  font-size: .75em;
+  text-shadow: 2px 2px 5px black;
+  color: white;
+  text-align: center;
+}
+
+.chat-text.fadeOut {
+  opacity: 1.0;
+  animation: fadeOut 5s ease-in-out forwards;
+}
+
+@keyframes fadeOut {
+  0% {
+    opacity: 1.0;
+    visibility: visible;
+  }
+  90% {
+    opacity: 1.0;
+    visibility: visible;
+  }
+  100% {
+    opacity: 0.0;
+    visibility: hidden;
+  }
+}

+ 25 - 0
index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Spaceships Pew Pew</title>
+  <link rel="preconnect" href="https://fonts.gstatic.com">
+  <link href="https://fonts.googleapis.com/css2?family=Odibee+Sans&display=swap" rel="stylesheet">
+  <link rel="stylesheet" type="text/css" href="base.css">
+</head>
+<body>
+  <div class="container" id="container">
+    <div class="ui" id="game-ui">
+      <div class="row">
+        <div class="chat-ui">
+          <div class="chat-ui-text-area" id="chat-ui-text-area">
+            <div class="chat-text fadeOut"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <script src="https://cdn.jsdelivr.net/gh/kripken/ammo.js@HEAD/builds/ammo.js"></script>
+  <script src="./src/main.js" type="module">
+  </script>
+</body>
+</html>

BIN
resources/crawl.png


BIN
resources/crawl.xcf


BIN
resources/models/star-destroyer/collision.blend


BIN
resources/models/star-destroyer/collision.blend1


BIN
resources/models/star-destroyer/scene-collision.glb


BIN
resources/models/star-destroyer/scene-final.glb


BIN
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
+    }
+  ]
+}
+

BIN
resources/models/star-destroyer/star-destroyer.blend


BIN
resources/models/star-destroyer/star-destroyer.blend1


BIN
resources/models/star-destroyer/textures/Body_Top_baseColor.png


BIN
resources/models/star-destroyer/textures/Body_Top_emissive.png


BIN
resources/models/star-destroyer/textures/Body_Top_metallicRoughness.png


BIN
resources/models/star-destroyer/textures/Body_baseColor.png


BIN
resources/models/star-destroyer/textures/Body_emissive.png


BIN
resources/models/star-destroyer/textures/Body_metallicRoughness.png


BIN
resources/models/star-destroyer/textures/Bridge_Thing_emissive.png


BIN
resources/models/star-destroyer/textures/Details_baseColor.png


BIN
resources/models/star-destroyer/textures/Details_metallicRoughness.png


BIN
resources/models/star-destroyer/textures/Engines_baseColor.png


BIN
resources/models/star-destroyer/textures/Engines_emissive.png


BIN
resources/models/star-destroyer/textures/Material.004_baseColor.jpeg


BIN
resources/models/star-destroyer/textures/Material.004_metallicRoughness.png


BIN
resources/models/star-destroyer/textures/Turret_baseColor.png


BIN
resources/models/star-destroyer/textures/Turret_metallicRoughness.png


BIN
resources/models/star-destroyer/turret.blend


BIN
resources/models/star-destroyer/turret.glb


BIN
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
+    }
+  ]
+}
+

BIN
resources/models/tie-fighter-gltf/textures/hullblue_baseColor.png


BIN
resources/models/tie-fighter-gltf/textures/hullblue_metallicRoughness.png


BIN
resources/models/tie-fighter-gltf/textures/hullblue_normal.png


BIN
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
+    }
+  ]
+}
+

BIN
resources/models/x-wing/textures/lambert7_baseColor.png


BIN
resources/models/x-wing/textures/lambert7_metallicRoughness.png


BIN
resources/models/x-wing/textures/lambert7_normal.png


BIN
resources/models/x-wing/textures/lambert8_baseColor.png


BIN
resources/models/x-wing/textures/lambert8_metallicRoughness.png


BIN
resources/models/x-wing/textures/lambert8_normal.png


BIN
resources/sounds/explosion.ogg


BIN
resources/sounds/laser.ogg


BIN
resources/sounds/shields.ogg


BIN
resources/terrain/space-negx.jpg


BIN
resources/terrain/space-negy.jpg


BIN
resources/terrain/space-negz.jpg


BIN
resources/terrain/space-posx.jpg


BIN
resources/terrain/space-posy.jpg


BIN
resources/terrain/space-posz.jpg


BIN
resources/textures/fx/blaster.jpg


BIN
resources/textures/fx/fire.png


BIN
resources/textures/fx/smoke.png


+ 238 - 0
src/ammojs-component.js

@@ -0,0 +1,238 @@
+import {THREE} from './three-defs.js';
+import {entity} from './entity.js';
+
+
+export const ammojs_component = (() => {
+
+  class AmmoJSRigidBody {
+    constructor() {
+    }
+
+    Destroy() {
+      Ammo.destroy(this.body_);
+      Ammo.destroy(this.info_);
+      Ammo.destroy(this.shape_);
+      Ammo.destroy(this.inertia_);
+      Ammo.destroy(this.motionState_);
+      Ammo.destroy(this.transform_);
+      Ammo.destroy(this.userData_);
+
+      if (this.mesh_) {
+        Ammo.destroy(this.mesh_);
+      }
+    }
+
+    InitBox(pos, quat, size, userData) {
+      this.transform_ = new Ammo.btTransform();
+      this.transform_.setIdentity();
+      this.transform_.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
+      this.transform_.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
+      this.motionState_ = new Ammo.btDefaultMotionState(this.transform_);
+
+      let btSize = new Ammo.btVector3(size.x * 0.5, size.y * 0.5, size.z * 0.5);
+      this.shape_ = new Ammo.btBoxShape(btSize);
+      this.shape_.setMargin(0.05);
+
+      this.inertia_ = new Ammo.btVector3(0, 0, 0);
+      this.shape_.calculateLocalInertia(10, this.inertia_);
+
+      this.info_ = new Ammo.btRigidBodyConstructionInfo(10, this.motionState_, this.shape_, this.inertia_);
+      this.body_ = new Ammo.btRigidBody(this.info_);
+
+      this.userData_ = new Ammo.btVector3(0, 0, 0);
+      this.userData_.userData = userData;
+      this.body_.setUserPointer(this.userData_);
+
+      Ammo.destroy(btSize);
+    }
+
+    InitMesh(src, pos, quat, userData) {
+      const A0 = new Ammo.btVector3(0, 0, 0);
+      const A1 = new Ammo.btVector3(0, 0, 0);
+      const A2 = new Ammo.btVector3(0, 0, 0);
+
+      const V0 = new THREE.Vector3();
+      const V1 = new THREE.Vector3();
+      const V2 = new THREE.Vector3();
+
+      this.mesh_ = new Ammo.btTriangleMesh(true, true);
+
+      src.traverse(c => {
+        c.updateMatrixWorld(true);
+        if (c.geometry) {
+          const p = c.geometry.attributes.position.array;
+          for (let i = 0; i < c.geometry.index.count; i+=3) {
+            const i0 = c.geometry.index.array[i] * 3;
+            const i1 = c.geometry.index.array[i+1] * 3;
+            const i2 = c.geometry.index.array[i+2] * 3;
+
+            V0.fromArray(p, i0).applyMatrix4(c.matrixWorld);
+            V1.fromArray(p, i1).applyMatrix4(c.matrixWorld);
+            V2.fromArray(p, i2).applyMatrix4(c.matrixWorld);
+
+            A0.setX(V0.x);
+            A0.setY(V0.y);
+            A0.setZ(V0.z);
+            A1.setX(V1.x);
+            A1.setY(V1.y);
+            A1.setZ(V1.z);
+            A2.setX(V2.x);
+            A2.setY(V2.y);
+            A2.setZ(V2.z);
+            this.mesh_.addTriangle(A0, A1, A2, false);
+          }
+        }
+      });
+
+      this.inertia_ = new Ammo.btVector3(0, 0, 0);
+      this.shape_ = new Ammo.btBvhTriangleMeshShape(this.mesh_, true, true);
+      this.shape_.setMargin(0.05);
+      this.shape_.calculateLocalInertia(10, this.inertia_);
+
+      this.transform_ = new Ammo.btTransform();
+      this.transform_.setIdentity();
+      this.transform_.getOrigin().setValue(pos.x, pos.y, pos.z);
+      this.transform_.getRotation().setValue(quat.x, quat.y, quat.z, quat.w);
+      this.motionState_ = new Ammo.btDefaultMotionState(this.transform_);
+
+      this.info_ = new Ammo.btRigidBodyConstructionInfo(10, this.motionState_, this.shape_, this.inertia_);
+      this.body_ = new Ammo.btRigidBody(this.info_);
+
+      this.userData_ = new Ammo.btVector3(0, 0, 0);
+      this.userData_.userData = userData;
+      this.body_.setUserPointer(this.userData_);
+
+      Ammo.destroy(A0);
+      Ammo.destroy(A1);
+      Ammo.destroy(A2);
+    }
+  }
+
+  class AmmoJSController extends entity.Component {
+    constructor() {
+      super();
+    }
+
+    Destroy() {
+      Ammo.Destroy(this.physicsWorld_);
+      Ammo.Destroy(this.solver_);
+      Ammo.Destroy(this.broadphase_);
+      Ammo.Destroy(this.dispatcher_);
+      Ammo.Destroy(this.collisionConfiguration_);
+    }
+
+    InitEntity() {
+      this.collisionConfiguration_ = new Ammo.btDefaultCollisionConfiguration();
+      this.dispatcher_ = new Ammo.btCollisionDispatcher(this.collisionConfiguration_);
+      this.broadphase_ = new Ammo.btDbvtBroadphase();
+      this.solver_ = new Ammo.btSequentialImpulseConstraintSolver();
+      this.physicsWorld_ = new Ammo.btDiscreteDynamicsWorld(
+          this.dispatcher_, this.broadphase_, this.solver_, this.collisionConfiguration_);
+
+      this.tmpRayOrigin_ = new Ammo.btVector3();
+      this.tmpRayDst_ = new Ammo.btVector3();
+      this.rayCallback_ = new Ammo.ClosestRayResultCallback(this.tmpRayOrigin_, this.tmpRayDst_);
+    }
+
+    RayTest(start, end) {
+      const rayCallback = Ammo.castObject(this.rayCallback_, Ammo.RayResultCallback);
+      rayCallback.set_m_closestHitFraction(1);
+      rayCallback.set_m_collisionObject(null);
+
+      this.tmpRayOrigin_.setValue(start.x, start.y, start.z);
+      this.tmpRayDst_.setValue(end.x, end.y, end.z);
+      this.rayCallback_.get_m_rayFromWorld().setValue(start.x, start.y, start.z);
+      this.rayCallback_.get_m_rayToWorld().setValue(end.x, end.y, end.z);
+
+      this.physicsWorld_.rayTest(this.tmpRayOrigin_, this.tmpRayDst_, this.rayCallback_);
+
+      const hits = [];
+      if (this.rayCallback_.hasHit()) {
+        const obj = this.rayCallback_.m_collisionObject;
+        const ud0 = Ammo.castObject(obj.getUserPointer(), Ammo.btVector3).userData;
+
+        const point = this.rayCallback_.get_m_hitPointWorld();
+
+        hits.push({
+          name: ud0.name,
+          position: new THREE.Vector3(point.x(), point.y(), point.z())
+        });
+      }
+      return hits;
+    }
+
+    RemoveRigidBody(body) {
+      this.physicsWorld_.removeRigidBody(body.body_);
+      body.Destroy();
+    }
+
+    CreateBox(pos, quat, size, userData) {
+      const box = new AmmoJSRigidBody();
+
+      box.InitBox(pos, quat, size, userData);
+
+      this.physicsWorld_.addRigidBody(box.body_);
+
+      box.body_.setActivationState(4);
+      box.body_.setCollisionFlags(2);
+
+      return box;
+    }
+
+    CreateMesh(src, pos, quat, userData) {
+      const mesh = new AmmoJSRigidBody();
+
+      mesh.InitMesh(src, pos, quat, userData);
+
+      this.physicsWorld_.addRigidBody(mesh.body_);
+
+      mesh.body_.setActivationState(4);
+      mesh.body_.setCollisionFlags(2);
+
+      return mesh;
+    }
+
+    StepSimulation(timeElapsedS) {
+      this.physicsWorld_.stepSimulation(timeElapsedS, 10);
+
+      const dispatcher = this.physicsWorld_.getDispatcher();
+      const numManifolds = this.dispatcher_.getNumManifolds();
+    
+      const collisions = {};
+
+      for (let i=0; i < numManifolds; i++) {
+        const contactManifold = dispatcher.getManifoldByIndexInternal(i);
+        const numContacts = contactManifold.getNumContacts();
+
+        if (numContacts > 0) {
+          const rb0 = contactManifold.getBody0();
+          const rb1 = contactManifold.getBody1();
+          const ud0 = Ammo.castObject(rb0.getUserPointer(), Ammo.btVector3).userData;
+          const ud1 = Ammo.castObject(rb1.getUserPointer(), Ammo.btVector3).userData;
+
+          if (!(ud0.name in collisions)) {
+            collisions[ud0.name] = [];
+          }
+          collisions[ud0.name].push(ud1.name);
+
+          if (!(ud1.name in collisions)) {
+            collisions[ud1.name] = [];
+          }
+          collisions[ud1.name].push(ud0.name);
+        }
+      }
+
+      for (let k in collisions) {
+        const e = this.FindEntity(k);
+        e.Broadcast({topic: 'physics.collision', value: collisions[k]});
+      }
+    }
+
+    Update(_) {
+    }
+  }
+
+  return {
+      AmmoJSController: AmmoJSController,
+  };
+})();

+ 98 - 0
src/atmosphere-effect.js

@@ -0,0 +1,98 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from "./entity.js";
+
+
+export const atmosphere_effect = (() => {
+
+  class AtmosphereFXEmitter extends particle_system.ParticleEmitter {
+    constructor(parent) {
+      super();
+      this.parent_ = parent;
+      this.blend_ = 0.0;
+    }
+
+    OnUpdate_() {
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const radius = 50.0;
+      const p = new THREE.Vector3(
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius).add(this.parent_.Position);
+
+      const life = 1.0;
+
+      const d = this.parent_.Forward.clone().multiplyScalar(-100);
+
+      return {
+          position: p,
+          size: (Math.random() * 0.75 + 0.25) * 1.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+
+  class AtmosphereEffect extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    InitEntity() {
+      this.particles_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: this.params_.scene,
+          texture: './resources/textures/fx/smoke.png',
+      });
+
+      this.SetupFX_();
+    }
+
+    Destroy() {
+      this.particles_.Destroy();
+      this.particles_ = null;
+    }
+
+    SetupFX_() {
+      const emitter = new AtmosphereFXEmitter(this.Parent);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.1, 0.5);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0xFFFFFF));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0xFFFFFF));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 2.0);
+      emitter.sizeSpline_.AddPoint(1.0, 2.0);
+      emitter.blend_ = 1.0;
+      this.particles_.AddEmitter(emitter);
+      emitter.SetEmissionRate(200);
+      emitter.AddParticles(1);
+    }
+
+    Update(timeElapsed) {
+      this.particles_.Update(timeElapsed);
+    }
+  }
+  
+  return {
+    AtmosphereEffect: AtmosphereEffect,
+  };
+})();

+ 77 - 0
src/basic-rigid-body.js

@@ -0,0 +1,77 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+
+export const basic_rigid_body = (() => {
+
+  class BasicRigidBody extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Destroy() {
+      this.FindEntity('physics').GetComponent('AmmoJSController').RemoveRigidBody(this.body_);
+    }
+
+    InitEntity() {
+      const pos = this.Parent.Position;
+      const quat = this.Parent.Quaternion;
+
+      this.body_ = this.FindEntity('physics').GetComponent('AmmoJSController').CreateBox(
+          pos, quat, this.params_.box, {name: this.Parent.Name});
+      
+      const geometry = new THREE.BoxGeometry(1, 1, 1);
+      const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
+      this.debug_ = new THREE.Mesh(geometry, material);
+      this.debug_.scale.copy(this.params_.box);
+
+      this.Parent.Attributes.roughRadius = Math.max(
+          this.params_.box.x,
+          Math.max(this.params_.box.y, this.params_.box.z));
+      this.Broadcast({topic: 'physics.loaded'});
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('update.position', (m) => { this.OnPosition_(m); });
+      this.RegisterHandler_('update.rotation', (m) => { this.OnRotation_(m); });
+      this.RegisterHandler_('physics.collision', (m) => { this.OnCollision_(m); });
+    }
+
+    OnCollision_() {
+    }
+
+    OnPosition_(m) {
+      this.OnTransformChanged_();
+    }
+
+    OnRotation_(m) {
+      this.OnTransformChanged_();
+    }
+
+    OnTransformChanged_() {
+      const pos = this.Parent.Position;
+      const quat = this.Parent.Quaternion;
+      const ms = this.body_.motionState_;
+      const t = this.body_.transform_;
+      
+      ms.getWorldTransform(t);
+      t.setIdentity();
+      t.getOrigin().setValue(pos.x, pos.y, pos.z);
+      t.getRotation().setValue(quat.x, quat.y, quat.z, quat.w);
+      ms.setWorldTransform(t);
+
+      const origin = pos;
+      this.debug_.position.copy(origin);
+      this.debug_.quaternion.copy(quat);
+    }
+
+    Update(_) {
+    }
+  };
+
+  return {
+    BasicRigidBody: BasicRigidBody,
+  };
+})();

+ 139 - 0
src/crawl-controller.js

@@ -0,0 +1,139 @@
+import {THREE} from "./three-defs.js";
+
+import {entity} from "./entity.js";
+
+
+export const crawl_controller = (() => {
+
+  const _VS = `
+  out vec2 v_UV;
+  
+  void main() {
+    vec4 mvPosition = modelMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPosition;
+    v_UV = uv;
+  }
+  `;
+  
+    const _PS = `
+  uniform sampler2D diffuse;
+  
+  in vec2 v_UV;
+  
+  void main() {
+    vec4 t = texture(diffuse, v_UV);
+    if (t.w < 0.5) {
+      discard;
+    }
+    gl_FragColor = t;
+    // gl_FragColor = vec4(1.0);
+  }
+  `;
+
+  class CrawlController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    InitEntity() {
+      const _TMP_M0 = new THREE.Matrix4();
+      const _TMP_Q0 = new THREE.Quaternion();
+
+      _TMP_M0.lookAt(
+          new THREE.Vector3(), new THREE.Vector3(-5, 50, -10), THREE.Object3D.DefaultUp);
+      _TMP_Q0.setFromRotationMatrix(_TMP_M0);
+
+      this.start_ = _TMP_Q0.clone();
+
+      _TMP_M0.lookAt(
+          new THREE.Vector3(), new THREE.Vector3(0, 3, -10), THREE.Object3D.DefaultUp);
+      _TMP_Q0.setFromRotationMatrix(_TMP_M0);
+
+      this.end_ = _TMP_Q0.clone();
+
+      this.params_.camera.quaternion.copy(this.start_);
+
+      // const hemiLight = new THREE.HemisphereLight(0xFFFFFF, 0xFFFFFFF, 0.6);
+      // hemiLight.color.setHSL(0.6, 1, 0.6);
+      // hemiLight.groundColor.setHSL(0.095, 1, 0.75);
+      // this.params_.scene.add(hemiLight);
+
+      let light = new THREE.DirectionalLight(0xFFFFFF, 0.25);
+      light.position.set(0, 0, 0);
+      light.target.position.set(0, 0, -1);
+      light.updateMatrixWorld();
+      light.target.updateMatrixWorld();
+      this.params_.scene.add(light);
+
+
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      const t = loader.LoadTexture('./resources/', 'crawl.png');
+      t.anisotropy = 4;
+
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+      this.material_ = new THREE.ShaderMaterial( {
+        uniforms: {
+          diffuse: {value: t}
+        },
+        vertexShader: _VS,
+        fragmentShader: _PS,
+
+        blending: THREE.NormalBlending,
+        depthTest: true,
+        depthWrite: true,
+        transparent: true,
+        vertexColors: false,
+        alphaTest: 0.5,
+        alphaToCoverage: true,
+        side: THREE.DoubleSide,
+      });
+
+      const geometry = new THREE.PlaneBufferGeometry( 1, 1 );
+      this.plane = new THREE.Mesh( geometry, this.material_ );
+
+      // this.plane.position.set(-0.5, -0.0, 0);
+      this.plane.scale.set(120, 120);
+      this.plane.position.add(threejs.camera_.position);
+      this.plane.rotateX(-Math.PI * 0.45);
+
+      // const p = this.plane;
+      // this.plane = new THREE.Group();
+      // this.plane.add(p);
+      // this.plane.scale.set(100, 100);
+      // this.plane.position.add(threejs.camera_.position);
+      // plane.scale.set(100, 100, 0);
+
+      // msg.value.parent.add(this.sprite_);
+      threejs.scene_.add(this.plane);
+
+      this.timeElapsed_ = 0;
+    }
+  
+    Update(timeElapsed) {
+      this.timeElapsed_ += timeElapsed;
+
+      const t = 1.0 - Math.pow(0.95, timeElapsed);
+
+      this.params_.camera.quaternion.slerp(this.end_, t);
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+      // const forward = new THREE.Vector3(0, 0, -1);
+      // forward.applyQuaternion(this.params_.camera.quaternion);
+
+      this.plane.position.set(0, -30, -this.timeElapsed_ * 4 + 30);
+      // this.plane.position.add(threejs.camera_.position);
+
+      // this.plane.lookAt(forward);
+
+      // this.plane.rotation.set(-Math.PI * 0.5, 0, 0);
+      // this.plane.quaternion.premultiply(threejs.camera_.quaternion);
+    }
+  };
+
+  return {
+    CrawlController: CrawlController,
+  };
+
+})();

+ 147 - 0
src/crosshair.js

@@ -0,0 +1,147 @@
+import {THREE} from './three-defs.js';
+import {entity} from './entity.js';
+
+
+export const crosshair = (() => {
+
+  class Crosshair extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.visible_ = true;
+    }
+
+    Destroy() {
+      if (!this.sprite_) {
+        this.visible_ = false;
+        return;
+      }
+
+      this.sprite_.traverse(c => {
+        if (c.material) {
+          let materials = c.material;
+          if (!(c.material instanceof Array)) {
+            materials = [c.material];
+          }
+          for (let m of materials) {
+            m.dispose();
+          }
+        }
+
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      if (this.sprite_.parent) {
+        this.sprite_.parent.remove(this.sprite_);
+      }
+    }
+
+    InitComponent() {
+      this.RegisterHandler_(
+          'render.loaded', (m) => this.OnCreateSprite_(m));
+      this.RegisterHandler_(
+          'health.death', (m) => { this.OnDeath_(m); });
+    }
+
+    OnDeath_() {
+      this.Destroy();
+    }
+
+    OnCreateSprite_(msg) {
+      if (!this.visible_) {
+        return;
+      }
+
+      const size = 128;
+      this.element_ = document.createElement('canvas');
+      this.context2d_ = this.element_.getContext('2d');
+      this.context2d_.canvas.width = size;
+      this.context2d_.canvas.height = size;
+
+      this.context2d_.fillStyle = "#FFFFFF";
+      this.context2d_.lineWidth = 5;
+
+      this.context2d_.translate(size * 0.5, size * 0.5);
+      this.context2d_.rotate(Math.PI / 4);
+      this.context2d_.translate(-size * 0.5, -size * 0.5);
+
+      const _DrawLine = () => {
+        this.context2d_.translate(size * 0.5, size * 0.5);
+        this.context2d_.rotate(Math.PI / 2);
+        this.context2d_.translate(-size * 0.5, -size * 0.5);
+        this.context2d_.beginPath();
+        this.context2d_.moveTo(size * 0.48, size * 0.25);
+        this.context2d_.moveTo(size * 0.495, size * 0.45);
+        this.context2d_.lineTo(size * 0.505, size * 0.45);
+        this.context2d_.lineTo(size * 0.52, size * 0.25);
+        this.context2d_.lineTo(size * 0.48, size * 0.25);
+        this.context2d_.fill();
+      }
+
+      for (let i = 0; i < 4; ++i) {
+        _DrawLine();
+      }
+
+      this.context2d_.strokeStyle = '#FFFFFF';
+      this.context2d_.lineWidth = 3;
+      this.context2d_.setTransform(1, 0, 0, 1, 0, 0);
+      this.context2d_.beginPath();
+      this.context2d_.arc(
+          size * 0.5, size * 0.5, size * 0.4,
+          Math.PI * -0.25, Math.PI * 0.25);
+      this.context2d_.stroke();
+
+      this.context2d_.beginPath();
+      this.context2d_.arc(
+          size * 0.5, size * 0.5, size * 0.4,
+          Math.PI * 0.75, Math.PI * 1.25);
+      this.context2d_.stroke();
+
+      const map = new THREE.CanvasTexture(this.context2d_.canvas);
+      map.anisotropy = 2;
+
+      this.sprite_ = new THREE.Sprite(
+          new THREE.SpriteMaterial({map: map, color: 0xffffff, fog: false}));
+      this.sprite_.scale.set(4, 4, 1)
+      this.sprite_.position.set(0, 5, 0);
+      // msg.value.parent.add(this.sprite_);
+
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      threejs.uiScene_.add(this.sprite_);
+    }
+
+    Update() {
+      if (!this.sprite_) {
+        return;
+      }
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      const camera = threejs.camera_;
+
+      const ndc = new THREE.Vector3(0, 0, -10);
+
+      this.sprite_.scale.set(0.15, 0.15 * camera.aspect, 1);
+      this.sprite_.position.copy(ndc);
+
+      const physics = this.FindEntity('physics').GetComponent('AmmoJSController');
+      const forward = this.Parent.Forward.clone();
+      forward.multiplyScalar(1000);
+      forward.add(this.Parent.Position);
+
+      const hits = physics.RayTest(this.Parent.Position, forward).filter(
+          (h) => {
+            return h.name != this.Parent.Name;
+          }
+      );
+      if (hits.length > 0) {
+        this.sprite_.material.color.setRGB(1, 0, 0);
+      } else {
+        this.sprite_.material.color.setRGB(1, 1, 1);
+      }
+    }
+  };
+
+  return {
+    Crosshair: Crosshair,
+  };
+})();

+ 233 - 0
src/enemy-ai-controller.js

@@ -0,0 +1,233 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+import {math} from './math.js';
+
+
+export const enemy_ai_controller = (() => {
+  
+  const _TMP_V3_0 = new THREE.Vector3();
+
+  const _COLLISION_FORCE = 25;
+  const _WANDER_FORCE = 1;
+  const _ATTACK_FORCE = 25;
+  const _MAX_TARGET_DISTANCE = 1500;
+  const _MAX_ANGLE = 0.9;
+
+  const _TMP_M0 = new THREE.Matrix4();
+  const _TMP_Q0 = new THREE.Quaternion();
+
+  class EnemyAIController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.grid_ = params.grid;
+      this.Init_();
+    }
+  
+    Init_() {
+      this.maxSteeringForce_ = 30;
+      this.maxSpeed_  = 100;
+      this.acceleration_ = 10;
+      this.velocity_ = new THREE.Vector3(0, 0, -1);
+      this.wanderAngle_ = 0.0;
+      this.quaternion_ = new THREE.Quaternion();
+      this.target_ = null;
+    }
+
+    ApplySteering_(timeElapsed) {
+      // 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));
+      // });
+  
+      // const alignmentVelocity = this._ApplyAlignment(allies);
+      const originVelocity = this.ApplySeek_(
+          this.FindEntity('star-destroyer'));
+      const wanderVelocity = this.ApplyWander_();
+      const collisionVelocity = this.ApplyCollisionAvoidance_();
+      const attackVelocity = this.ApplyAttack_();
+  
+      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.add(attackVelocity);
+
+      steeringForce.multiplyScalar(this.acceleration_ * timeElapsed);
+  
+      // // Clamp the force applied
+      if (steeringForce.length() > this.maxSteeringForce_ * timeElapsed) {
+        steeringForce.normalize();
+        steeringForce.multiplyScalar(this.maxSteeringForce_ * timeElapsed);
+      }
+  
+      this.velocity_.add(steeringForce);
+  
+      // // Clamp velocity
+      this.velocity_.normalize();
+      const forward = this.velocity_.clone();
+      this.velocity_.multiplyScalar(this.maxSpeed_);
+
+      const frameVelocity = this.velocity_.clone();
+      frameVelocity.multiplyScalar(timeElapsed);
+      frameVelocity.add(this.Parent.Position);
+
+      this.Parent.SetPosition(frameVelocity);
+
+      const t = 1.0 - Math.pow(0.05, timeElapsed);
+
+      _TMP_M0.lookAt(
+          new THREE.Vector3(), forward, THREE.Object3D.DefaultUp);
+      _TMP_Q0.setFromRotationMatrix(_TMP_M0);
+      this.Parent.SetQuaternion(_TMP_Q0);
+
+      // this.quaternion_.setFromUnitVectors(
+      //   new THREE.Vector3(0, 1, 0), forward);
+      // this.Parent.Quaternion.slerp(this.quaternion_, t);
+      // this.Parent.SetQuaternion(this.Parent.Quaternion);
+    }
+
+    Update(timeElapsed) {
+      if (!this.Parent.Attributes.roughRadius) {
+        return;
+      }
+      this.ApplySteering_(timeElapsed);
+      this.MaybeFire_();
+    }
+
+    MaybeFire_() {
+      // DEMO
+      // if (Math.random() < 0.01) {
+      //   this.Broadcast({topic: 'player.fire'});
+      //   return;
+      // }
+      if (!this.target_) {
+        return;
+      }
+
+      const forward = this.Parent.Forward;
+      const dirToTarget = this.target_.Position.clone().sub(
+          this.Parent.Position);
+      dirToTarget.normalize();
+
+      const angle = dirToTarget.dot(forward);
+      if (angle > _MAX_ANGLE) {
+        this.Broadcast({topic: 'player.fire'});
+        return;
+      }
+    }
+  
+    ApplyCollisionAvoidance_() {
+      const pos = this.Parent.Position;
+      const colliders = this.grid_.FindNear([pos.x, pos.z], [500, 500]).filter(
+          c => c.entity.ID != this.Parent.ID
+      );
+  
+      // Hardcoded is best
+      const starDestroyer = this.FindEntity('star-destroyer');
+      if (starDestroyer.Attributes.roughRadius) {
+        colliders.push({entity: starDestroyer});
+      }
+
+      const force = new THREE.Vector3(0, 0, 0);
+  
+      for (const c of colliders) {
+        const entityPos = c.entity.Position;
+        const entityRadius = c.entity.Attributes.roughRadius;
+        const dist = entityPos.distanceTo(pos);
+
+        if (dist > (entityRadius + 500)) {
+          continue;
+        }
+
+        const directionFromEntity = _TMP_V3_0.subVectors(
+            pos, entityPos);
+        const multiplier = (entityRadius + this.Parent.Attributes.roughRadius) / Math.max(1, (dist - 200));
+        directionFromEntity.normalize();
+        directionFromEntity.multiplyScalar(multiplier * _COLLISION_FORCE);
+        force.add(directionFromEntity);
+      }
+  
+      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.Parent.Forward.clone();
+      pointAhead.multiplyScalar(20);
+      pointAhead.add(randomPointOnCircle);
+      pointAhead.normalize();
+      return pointAhead.multiplyScalar(_WANDER_FORCE);
+    }
+
+    ApplySeek_(destroyer) {
+      if (!destroyer.Attributes.roughRadius) {
+        return new THREE.Vector3(0, 0, 0);
+      }
+      const dist = this.Parent.Position.distanceTo(destroyer.Position);
+      const radius = destroyer.Attributes.roughRadius;
+      const distFactor = Math.max(
+          0, ((dist - radius) / (radius * 0.25))) ** 2;
+      const direction = destroyer.Position.clone().sub(this.Parent.Position);
+      direction.normalize();
+  
+      const forceVector = direction.multiplyScalar(distFactor);
+      return forceVector;
+    }
+
+    AcquireTarget_() {
+      const pos = this.Parent.Position;
+      const enemies = this.params_.grid.FindNear(
+          [pos.x, pos.z], [1000, 1000]).filter(
+          c => c.entity.Attributes.team == 'allies'
+      );
+
+      if (enemies.length == 0) {
+        return;
+      }
+
+      this.target_ = enemies[0].entity;
+    }
+
+    ApplyAttack_() {
+      if (!this.target_) {
+        this.AcquireTarget_();
+        return new THREE.Vector3(0, 0, 0);
+      }
+
+      if (this.target_.Position.distanceTo(this.Parent.Position) > _MAX_TARGET_DISTANCE) {
+        this.target_ = null;
+        return new THREE.Vector3(0, 0, 0);
+      }
+
+      if (this.target_.IsDead) {
+        this.target_ = null;
+        return new THREE.Vector3(0, 0, 0);
+      }
+
+      const direction = this.target_.Position.clone().sub(this.Parent.Position);
+      direction.normalize();
+  
+      const dist = this.Parent.Position.distanceTo(this.target_.Position);
+      const falloff = math.sat(dist / 200);
+
+      const forceVector = direction.multiplyScalar(_ATTACK_FORCE * falloff);
+      return forceVector;
+    }
+  };
+
+  return {
+    EnemyAIController: EnemyAIController,
+  };
+
+})();

+ 89 - 0
src/entity-manager.js

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

+ 213 - 0
src/entity.js

@@ -0,0 +1,213 @@
+import {THREE} from './three-defs.js';
+
+
+export const entity = (() => {
+
+  class Entity {
+    constructor() {
+      this.name_ = null;
+      this.id_ = null;
+      this.components_ = {};
+      this.attributes_ = {};
+
+      this._position = new THREE.Vector3();
+      this._rotation = new THREE.Quaternion();
+      this.handlers_ = {};
+      this.parent_ = null;
+      this.dead_ = false;
+    }
+
+    Destroy() {
+      for (let k in this.components_) {
+        this.components_[k].Destroy();
+      }
+      this.components_ = null;
+      this.parent_ = null;
+      this.handlers_ = null;
+    }
+
+    RegisterHandler_(n, h) {
+      if (!(n in this.handlers_)) {
+        this.handlers_[n] = [];
+      }
+      this.handlers_[n].push(h);
+    }
+
+    SetParent(p) {
+      this.parent_ = p;
+    }
+
+    SetName(n) {
+      this.name_ = n;
+    }
+
+    SetId(id) {
+      this.id_ = id;
+    }
+
+    get Name() {
+      return this.name_;
+    }
+
+    get ID() {
+      return this.id_;
+    }
+
+    get Manager() {
+      return this.parent_;
+    }
+
+    get Attributes() {
+      return this.attributes_;
+    }
+
+    get IsDead() {
+      return this.dead_;
+    }
+
+    SetActive(b) {
+      this.parent_.SetActive(this, b);
+    }
+
+    SetDead() {
+      this.dead_ = true;
+    }
+
+    AddComponent(c) {
+      c.SetParent(this);
+      this.components_[c.constructor.name] = c;
+
+      c.InitComponent();
+    }
+
+    InitEntity() {
+      for (let k in this.components_) {
+        this.components_[k].InitEntity();
+      }
+    }
+
+    GetComponent(n) {
+      return this.components_[n];
+    }
+
+    FindEntity(n) {
+      return this.parent_.Get(n);
+    }
+
+    Broadcast(msg) {
+      if (this.IsDead) {
+        return;
+      }
+      if (!(msg.topic in this.handlers_)) {
+        return;
+      }
+
+      for (let curHandler of this.handlers_[msg.topic]) {
+        curHandler(msg);
+      }
+    }
+
+    SetPosition(p) {
+      this._position.copy(p);
+      this.Broadcast({
+          topic: 'update.position',
+          value: this._position,
+      });
+    }
+
+    SetQuaternion(r) {
+      this._rotation.copy(r);
+      this.Broadcast({
+          topic: 'update.rotation',
+          value: this._rotation,
+      });
+    }
+
+    get Position() {
+      return this._position;
+    }
+
+    get Quaternion() {
+      return this._rotation;
+    }
+
+    get Forward() {
+      const forward = new THREE.Vector3(0, 0, -1);
+      forward.applyQuaternion(this._rotation);
+      return forward;
+    }
+
+    get Up() {
+      const forward = new THREE.Vector3(0, 1, 0);
+      forward.applyQuaternion(this._rotation);
+      return forward;
+    }
+
+    Update(timeElapsed, pass) {
+      for (let k in this.components_) {
+        const c = this.components_[k];
+        if (c.Pass == pass) {
+          c.Update(timeElapsed);
+        }
+      }
+    }
+  };
+
+  class Component {
+    constructor() {
+      this.parent_ = null;
+      this.pass_ = 0;
+    }
+
+    Destroy() {
+    }
+
+    SetParent(p) {
+      this.parent_ = p;
+    }
+
+    SetPass(p) {
+      this.pass_ = p;
+    }
+
+    get Pass() {
+      return this.pass_;
+    }
+
+    InitComponent() {}
+    
+    InitEntity() {}
+
+    GetComponent(n) {
+      return this.parent_.GetComponent(n);
+    }
+
+    get Manager() {
+      return this.parent_.Manager;
+    }
+
+    get Parent() {
+      return this.parent_;
+    }
+
+    FindEntity(n) {
+      return this.parent_.FindEntity(n);
+    }
+
+    Broadcast(m) {
+      this.parent_.Broadcast(m);
+    }
+
+    Update(_) {}
+
+    RegisterHandler_(n, h) {
+      this.parent_.RegisterHandler_(n, h);
+    }
+  };
+
+  return {
+    Entity: Entity,
+    Component: Component,
+  };
+
+})();

+ 269 - 0
src/explode-component.js

@@ -0,0 +1,269 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+import {particle_system} from "./particle-system.js";
+import {math} from './math.js';
+
+
+export const explode_component = (() => {
+
+  class ExplosionEffectEmitter extends particle_system.ParticleEmitter {
+    constructor(origin) {
+      super();
+      this.origin_ = origin.clone();
+      this.blend_ = 0.0;
+    }
+
+    OnUpdate_() {
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const radius = 1.0;
+      const life = (Math.random() * 0.75 + 0.25) * 2.0;
+      const p = new THREE.Vector3(
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius);
+
+      const d = p.clone().normalize();
+      p.copy(d);
+      p.multiplyScalar(radius);
+      p.add(this.origin_);
+      d.multiplyScalar(50.0);
+
+      return {
+          position: p,
+          size: (Math.random() * 0.5 + 0.5) * 5.0,
+          colour: new THREE.Color(),
+          alpha: 0.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 0.9,
+      };
+    }
+  };
+
+  class TinyExplosionEffectEmitter extends particle_system.ParticleEmitter {
+    constructor(origin) {
+      super();
+      this.origin_ = origin.clone();
+      this.blend_ = 0.0;
+    }
+
+    OnUpdate_() {
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const radius = 1.0;
+      const life = (Math.random() * 0.75 + 0.25) * 2.0;
+      const p = new THREE.Vector3(
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius,
+          (Math.random() * 2 - 1) * radius);
+
+      const d = p.clone().normalize();
+      p.copy(d);
+      p.multiplyScalar(radius);
+      p.add(this.origin_);
+      d.multiplyScalar(25.0);
+
+      return {
+          position: p,
+          size: (Math.random() * 0.5 + 0.5) * 5.0,
+          colour: new THREE.Color(),
+          alpha: 0.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 0.75,
+      };
+    }
+  };
+
+  class ExplodeEffect extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+
+      this.group_ = new THREE.Group();
+      params.scene.add(this.group_);
+
+      this.particles_ = new particle_system.ParticleSystem({
+          camera: params.camera,
+          parent: params.scene,
+          texture: './resources/textures/fx/fire.png',
+      });
+      this.timer_ = 10.0;
+    }
+
+    Destroy() {
+      this.particles_.Destroy();
+      this.group_.parent.remove(this.group_);
+    }
+
+    InitEntity() {
+      this.group_.position.copy(this.Parent.Position);
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.LoadSound('./resources/sounds/', 'explosion.ogg', (s) => {
+        this.group_.add(s);
+        s.setRefDistance(100);
+        s.setMaxDistance(1000);
+        s.play();  
+      });
+
+      for (let i = 0; i < 3; ++i) {
+        const r = 4.0;
+        const p = new THREE.Vector3(
+            (Math.random() * 2 - 1) * r,
+            (Math.random() * 2 - 1) * r,
+            (Math.random() * 2 - 1) * r);
+        p.add(this.Parent.Position);
+
+        let emitter = new ExplosionEffectEmitter(p);
+        emitter.alphaSpline_.AddPoint(0.0, 0.0);
+        emitter.alphaSpline_.AddPoint(0.5, 1.0);
+        emitter.alphaSpline_.AddPoint(1.0, 0.0);
+        
+        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x800000));
+        emitter.colourSpline_.AddPoint(0.3, new THREE.Color(0xFF0000));
+        emitter.colourSpline_.AddPoint(0.4, new THREE.Color(0xdeec42));
+        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0xf4a776));
+        
+        emitter.sizeSpline_.AddPoint(0.0, 0.5);
+        emitter.sizeSpline_.AddPoint(0.5, 3.0);
+        emitter.sizeSpline_.AddPoint(1.0, 0.5);
+        emitter.blend_ = 0.0;
+        emitter.delay_ = i * 0.5;
+        emitter.AddParticles(200);
+  
+        this.particles_.AddEmitter(emitter);
+  
+        emitter = new ExplosionEffectEmitter(p);
+        emitter.alphaSpline_.AddPoint(0.0, 0.0);
+        emitter.alphaSpline_.AddPoint(0.7, 1.0);
+        emitter.alphaSpline_.AddPoint(1.0, 0.0);
+        
+        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x000000));
+        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x000000));
+        
+        emitter.sizeSpline_.AddPoint(0.0, 0.5);
+        emitter.sizeSpline_.AddPoint(0.5, 4.0);
+        emitter.sizeSpline_.AddPoint(1.0, 4.0);
+        emitter.blend_ = 1.0;
+        emitter.delay_ = i * 0.5 + 0.25;
+        emitter.AddParticles(50);
+  
+        this.particles_.AddEmitter(emitter);
+      }
+    }
+
+    Update(timeElapsed) {
+      this.particles_.Update(timeElapsed);
+      this.timer_ -= timeElapsed;
+      if (this.timer_ <= 0) {
+        this.Parent.SetDead(true);
+      }
+    }
+  };
+
+  class TinyExplodeEffect extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+
+      this.group_ = new THREE.Group();
+      params.scene.add(this.group_);
+
+      this.particles_ = new particle_system.ParticleSystem({
+          camera: params.camera,
+          parent: params.scene,
+          texture: './resources/textures/fx/fire.png',
+      });
+      this.timer_ = 10.0;
+    }
+
+    Destroy() {
+      this.particles_.Destroy();
+      this.group_.parent.remove(this.group_);
+    }
+
+    InitEntity() {
+      this.group_.position.copy(this.Parent.Position);
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.LoadSound('./resources/sounds/', 'explosion.ogg', (s) => {
+        this.group_.add(s);
+        s.setRefDistance(10);
+        s.setMaxDistance(5000);
+        s.play();  
+      });
+
+      const p = this.Parent.Position.clone();
+
+      let emitter = new TinyExplosionEffectEmitter(p);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.5, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x800000));
+      emitter.colourSpline_.AddPoint(0.3, new THREE.Color(0xFF0000));
+      emitter.colourSpline_.AddPoint(0.4, new THREE.Color(0xdeec42));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0xf4a776));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.5, 3.0);
+      emitter.sizeSpline_.AddPoint(1.0, 0.5);
+      emitter.blend_ = 0.0;
+      emitter.AddParticles(100);
+
+      this.particles_.AddEmitter(emitter);
+
+      emitter = new TinyExplosionEffectEmitter(p);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.7, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x000000));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x000000));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.5, 4.0);
+      emitter.sizeSpline_.AddPoint(1.0, 4.0);
+      emitter.blend_ = 1.0;
+      emitter.delay_ = 0.25;
+      emitter.AddParticles(50);
+
+      this.particles_.AddEmitter(emitter);
+    }
+
+    Update(timeElapsed) {
+      this.particles_.Update(timeElapsed);
+      this.timer_ -= timeElapsed;
+      if (this.timer_ <= 0) {
+        this.Parent.SetDead(true);
+      }
+    }
+  };
+
+  return {
+    ExplodeEffect: ExplodeEffect,
+    TinyExplodeEffect: TinyExplodeEffect,
+  };
+})();

+ 143 - 0
src/floating-descriptor.js

@@ -0,0 +1,143 @@
+import {THREE} from './three-defs.js';
+import {entity} from './entity.js';
+
+
+export const floating_descriptor = (() => {
+
+  class FloatingDescriptor extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.visible_ = true;
+    }
+
+    Destroy() {
+      if (!this.sprite_) {
+        this.visible_ = false;
+        return;
+      }
+
+      this.sprite_.traverse(c => {
+        if (c.material) {
+          let materials = c.material;
+          if (!(c.material instanceof Array)) {
+            materials = [c.material];
+          }
+          for (let m of materials) {
+            m.dispose();
+          }
+        }
+
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      if (this.sprite_.parent) {
+        this.sprite_.parent.remove(this.sprite_);
+      }
+    }
+
+    InitComponent() {
+      this.RegisterHandler_(
+          'render.loaded', (m) => this.OnCreateSprite_(m));
+      this.RegisterHandler_(
+          'health.death', (m) => { this.OnDeath_(m); });
+    }
+
+    OnDeath_() {
+      this.Destroy();
+    }
+
+    OnCreateSprite_(msg) {
+      if (!this.visible_) {
+        return;
+      }
+
+      const size = 128;
+      const hexSize = 0.4;
+      const crosshairSize = 0.4;
+      const offset = -8;
+      this.element_ = document.createElement('canvas');
+      this.context2d_ = this.element_.getContext('2d');
+      this.context2d_.canvas.width = size;
+      this.context2d_.canvas.height = size;
+      this.context2d_.beginPath();
+      this.context2d_.moveTo(
+        size * 0.5 + size * hexSize * Math.sin(0),
+        size * 0.5 + offset + size * hexSize * Math.cos(0));
+      for (let i = 0; i < 7; ++i) {
+        this.context2d_.lineTo(
+          size * 0.5 + size * hexSize * Math.sin(i * 2 * Math.PI / 6),
+          size * 0.5 + offset + size * hexSize * Math.cos(i * 2 * Math.PI / 6));
+      }
+      this.context2d_.fillStyle = "#FF0000";
+      this.context2d_.fill();
+      this.context2d_.beginPath();
+      this.context2d_.arc(
+          size * 0.5, size * 0.5 + offset, size * 0.5 * crosshairSize,
+          0, 2 * Math.PI);
+      this.context2d_.strokeStyle = '#FFFFFF';
+      this.context2d_.lineWidth = 5;
+      this.context2d_.stroke();
+      this.context2d_.beginPath();
+      this.context2d_.moveTo(size * 0.5, size * 0.2 + offset);
+      this.context2d_.lineTo(size * 0.5, size * 0.8 + offset);
+      this.context2d_.stroke();
+      this.context2d_.beginPath();
+      this.context2d_.moveTo(size * 0.2, size * 0.5 + offset);
+      this.context2d_.lineTo(size * 0.8, size * 0.5 + offset);
+      this.context2d_.stroke();
+      this.context2d_.fillStyle = '#FFF';
+      this.context2d_.font = "bold 24px Arial";
+      this.context2d_.textAlign = 'center';
+      this.context2d_.fillText('DESTROY', size * 0.5, size - 0);
+
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+      const map = new THREE.CanvasTexture(this.context2d_.canvas);
+      map.anisotropy = 2;
+
+      this.sprite_ = new THREE.Sprite(
+          new THREE.SpriteMaterial({
+              map: map,
+              color: 0xffffff,
+              fog: false,
+              depthTest: false,
+              depthWrite: false,
+          }));
+      // this.sprite_.scale.set(0.2, 0.2, 1)
+      this.sprite_.scale.set(8, 8, 1)
+      this.sprite_.position.set(0, 20, 0);
+
+      // msg.value.parent.add(this.sprite_);
+      threejs.uiScene_.add(this.sprite_);
+    }
+
+    Update() {
+      if (!this.sprite_) {
+        return;
+      }
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      const camera = threejs.camera_;
+      const ndc = new THREE.Vector3(0, 30, 0);
+      ndc.applyQuaternion(camera.quaternion);
+      ndc.add(this.Parent.Position);
+      ndc.project(camera);
+
+      this.sprite_.visible = true;
+
+      if (ndc.z < -1 || ndc.z > 1) {
+        this.sprite_.visible = false;
+      }
+      ndc.unproject(threejs.uiCamera_);
+      ndc.z = -10;
+
+      this.sprite_.scale.set(0.15 / camera.aspect, 0.15, 1);
+      this.sprite_.position.copy(ndc);
+    }
+  };
+
+  return {
+    FloatingDescriptor: FloatingDescriptor,
+  };
+})();

+ 210 - 0
src/fx/blaster.js

@@ -0,0 +1,210 @@
+import {THREE} from '../three-defs.js';
+
+import {entity} from '../entity.js';
+
+
+export const blaster = (() => {
+
+  const _VS = `
+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 = `
+uniform sampler2D diffuse;
+
+in vec2 v_UV;
+in vec3 vColor;
+
+void main() {
+  gl_FragColor = vec4(vColor, 1.0) * texture(diffuse, v_UV);
+}
+`;
+
+  class BlasterSystem extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.Init_();
+    }
+
+    Init_(params) {
+      const uniforms = {
+        diffuse: {
+          value: new THREE.TextureLoader().load(this.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.liveParticles_ = [];
+
+      this.params_.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;
+
+      const worldToView = this.params_.camera.matrixWorldInverse;
+      const viewToWorld = this.params_.camera.matrixWorld;
+
+      for (const p of this.liveParticles_) {
+        indices.push(...square.map(i => i + indexBase));
+        indexBase += 4;
+
+        const cameraToStart = this.params_.camera.position.clone().sub(p.Start);
+        cameraToStart.normalize();
+        const startToEnd = p.Start.clone().sub(p.End);
+        startToEnd.normalize();
+        const upWS = cameraToStart.clone().cross(startToEnd);
+        upWS.multiplyScalar(p.Width);
+
+        const p1 = new THREE.Vector3().copy(p.Start);
+        p1.add(upWS);
+
+        const p2 = new THREE.Vector3().copy(p.Start);
+        p2.sub(upWS);
+
+        const p3 = new THREE.Vector3().copy(p.End);
+        p3.sub(upWS);
+
+        const p4 = new THREE.Vector3().copy(p.End);
+        p4.add(upWS);
+
+        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;
+    }
+  };
+
+  return {
+    BlasterSystem: BlasterSystem,
+  };
+})();

+ 89 - 0
src/health-controller.js

@@ -0,0 +1,89 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+
+export const health_controller = (() => {
+
+  class HealthController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    InitEntity() {
+      this.Parent.Attributes.health = this.params_.maxHealth;
+      this.Parent.Attributes.maxHealth = this.params_.maxHealth;
+      this.Parent.Attributes.shields = 0;
+      this.Parent.Attributes.maxShields = 0;
+
+      if (this.params_.shields) {
+        this.Parent.Attributes.shields = this.params_.shields;
+        this.Parent.Attributes.maxShields = this.params_.shields;
+      }
+      this.Parent.Attributes.dead = false;
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('player.hit', (m) => { this.OnHit_(m); });
+      this.RegisterHandler_('physics.collision', (m) => { this.OnCollision_(m); });
+    }
+
+    OnCollision_() {
+      if (this.Parent.Attributes.dead) {
+        return;
+      }
+
+      if (this.params_.ignoreCollisions) {
+        return;
+      }
+
+      this.TakeDamage_(1000000);
+    }
+
+    OnHit_(msg) {
+      if (this.Parent.Attributes.dead) {
+        return;
+      }
+
+      const spawner = this.FindEntity('spawners').GetComponent('ShipSmokeSpawner');
+      spawner.Spawn(this.Parent);
+
+      this.TakeDamage_(msg.value);
+    }
+
+    TakeDamage_(dmg) {
+      if (this.Parent.Attributes.maxShields) {
+        this.Parent.Attributes.shields -= dmg;
+        if (this.Parent.Attributes.shields < 0) {
+          dmg = Math.abs(this.Parent.Attributes.shields);
+          this.Parent.Attributes.shields = 0;
+        } else {
+          dmg = 0;
+        }
+      }
+      this.Parent.Attributes.health -= dmg;
+
+      this.Broadcast({topic: 'health.damage'});
+
+      if (this.Parent.Attributes.health <= 0) {
+        this.Parent.Attributes.dead = true;
+        this.Broadcast({topic: 'health.dead'});
+        this.Parent.SetDead(true);
+        const e = this.FindEntity('spawners').GetComponent('ExplosionSpawner').Spawn(this.Parent.Position);
+        e.Broadcast({topic: 'health.dead'});
+      }
+    }
+
+    Update(_) {
+      // DEMO
+      // if (Math.random() < 0.0005) {
+      //   this.OnHit_({value: 0});
+      // }
+    }
+  };
+
+  return {
+    HealthController: HealthController
+  };
+})();

+ 135 - 0
src/load-controller.js

@@ -0,0 +1,135 @@
+import {THREE, FBXLoader, GLTFLoader, SkeletonUtils} from './three-defs.js';
+
+import {entity} from "./entity.js";
+
+
+export const load_controller = (() => {
+
+  class LoadController extends entity.Component {
+    constructor() {
+      super();
+
+      this.textures_ = {};
+      this.models_ = {};
+      this.sounds_ = {};
+      this.playing_ = [];
+    }
+
+    LoadTexture(path, name) {
+      if (!(name in this.textures_)) {
+        const loader = new THREE.TextureLoader();
+        loader.setPath(path);
+
+        this.textures_[name] = {loader: loader, texture: loader.load(name)};
+        this.textures_[name].encoding = THREE.sRGBEncoding;
+      }
+
+      return this.textures_[name].texture;
+    }
+
+    LoadSound(path, name, onLoad) {
+      if (!(name in this.sounds_)) {
+        const loader = new THREE.AudioLoader();
+        loader.setPath(path);
+
+        loader.load(name, (buf) => {
+          this.sounds_[name] = {
+            buffer: buf
+          };
+          const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+          const s = new THREE.PositionalAudio(threejs.listener_);
+          s.setBuffer(buf);
+          s.setRefDistance(10);
+          s.setMaxDistance(500);
+          onLoad(s);
+          this.playing_.push(s);
+        });
+      } else {
+        const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+        const s = new THREE.PositionalAudio(threejs.listener_);
+        s.setBuffer(this.sounds_[name].buffer);
+        s.setRefDistance(25);
+        s.setMaxDistance(1000);
+        onLoad(s);
+        this.playing_.push(s);
+      }
+    }
+
+    Load(path, name, onLoad) {
+      if (name.endsWith('glb') || name.endsWith('gltf')) {
+        this.LoadGLB(path, name, onLoad);
+      } else if (name.endsWith('fbx')) {
+        this.LoadFBX(path, name, onLoad);
+      } else {
+        // Silently fail, because screw you future me.
+      }
+    }
+
+
+    LoadFBX(path, name, onLoad) {
+      if (!(name in this.models_)) {
+        const loader = new FBXLoader();
+        loader.setPath(path);
+
+        this.models_[name] = {loader: loader, asset: null, queue: [onLoad]};
+        this.models_[name].loader.load(name, (fbx) => {
+          this.models_[name].asset = fbx;
+
+          const queue = this.models_[name].queue;
+          this.models_[name].queue = null;
+          for (let q of queue) {
+            const clone = this.models_[name].asset.clone();
+            q(clone);
+          }
+        });
+      } else if (this.models_[name].asset == null) {
+        this.models_[name].queue.push(onLoad);
+      } else {
+        const clone = this.models_[name].asset.clone();
+        onLoad(clone);
+      }
+    }
+
+    LoadGLB(path, name, onLoad) {
+      const fullName = path + name;
+      if (!(fullName in this.models_)) {
+        const loader = new GLTFLoader();
+        loader.setPath(path);
+
+        this.models_[fullName] = {loader: loader, asset: null, queue: [onLoad]};
+        this.models_[fullName].loader.load(name, (glb) => {
+          this.models_[fullName].asset = glb;
+
+          const queue = this.models_[fullName].queue;
+          this.models_[fullName].queue = null;
+          for (let q of queue) {
+            const clone = {...glb};
+            clone.scene = SkeletonUtils.clone(clone.scene);
+
+            q(clone.scene);
+          }
+        });
+      } else if (this.models_[fullName].asset == null) {
+        this.models_[fullName].queue.push(onLoad);
+      } else {
+        const clone = {...this.models_[fullName].asset};
+        clone.scene = SkeletonUtils.clone(clone.scene);
+
+        onLoad(clone.scene);
+      }
+    }
+
+    Update(timeElapsed) {
+      for (let i = 0; i < this.playing_.length; ++i) {
+        if (!this.playing_[i].isPlaying) {
+          this.playing_[i].parent.remove(this.playing_[i]);
+        }
+      }
+      this.playing_ = this.playing_.filter(s => s.isPlaying);
+    }
+  }
+
+  return {
+      LoadController: LoadController,
+  };
+})();

+ 167 - 0
src/main.js

@@ -0,0 +1,167 @@
+import {entity_manager} from './entity-manager.js';
+import {entity} from './entity.js';
+
+import {load_controller} from './load-controller.js';
+import {spawners} from './spawners.js';
+
+import {spatial_hash_grid} from './spatial-hash-grid.js';
+import {threejs_component} from './threejs-component.js';
+import {ammojs_component} from './ammojs-component.js';
+import {blaster} from './fx/blaster.js';
+import {ui_controller} from './ui-controller.js';
+import {crawl_controller} from './crawl-controller.js';
+
+import {math} from './math.js';
+
+import {THREE} from './three-defs.js';
+
+
+class QuickGame2_Sequel {
+  constructor() {
+    this._Initialize();
+  }
+
+  _Initialize() {
+    this.entityManager_ = new entity_manager.EntityManager();
+
+    this.OnGameStarted_();
+  }
+
+  OnGameStarted_() {
+    this.grid_ = new spatial_hash_grid.SpatialHashGrid(
+        [[-5000, -5000], [5000, 5000]], [100, 100]);
+
+    this.LoadControllers_();
+
+    this.previousRAF_ = null;
+    this.RAF_();
+  }
+
+  LoadControllers_() {
+    const threejs = new entity.Entity();
+    threejs.AddComponent(new threejs_component.ThreeJSController());
+    this.entityManager_.Add(threejs, 'threejs');
+
+    const ammojs = new entity.Entity();
+    ammojs.AddComponent(new ammojs_component.AmmoJSController());
+    this.entityManager_.Add(ammojs, 'physics');
+
+    // Hack
+    this.ammojs_ = ammojs.GetComponent('AmmoJSController');
+    this.scene_ = threejs.GetComponent('ThreeJSController').scene_;
+    this.camera_ = threejs.GetComponent('ThreeJSController').camera_;
+    this.threejs_ = threejs.GetComponent('ThreeJSController');
+
+    const l = new entity.Entity();
+    l.AddComponent(new load_controller.LoadController());
+    this.entityManager_.Add(l, 'loader');
+
+    const fx = new entity.Entity();
+    fx.AddComponent(new blaster.BlasterSystem({
+        scene: this.scene_,
+        camera: this.camera_,
+        texture: './resources/textures/fx/blaster.jpg',
+    }));
+    this.entityManager_.Add(fx, 'fx');
+
+    // DEMO
+    const ui = new entity.Entity();
+    ui.AddComponent(new ui_controller.UIController());
+    this.entityManager_.Add(ui, 'ui');
+
+    const basicParams = {
+      grid: this.grid_,
+      scene: this.scene_,
+      camera: this.camera_,
+    };
+
+    // DEMO
+    // const crawl = new entity.Entity();
+    // crawl.AddComponent(new crawl_controller.CrawlController(basicParams))
+    // this.entityManager_.Add(crawl);
+
+    const spawner = new entity.Entity();
+    spawner.AddComponent(new spawners.PlayerSpawner(basicParams));
+    spawner.AddComponent(new spawners.TieFighterSpawner(basicParams));
+    spawner.AddComponent(new spawners.XWingSpawner(basicParams));
+    spawner.AddComponent(new spawners.StarDestroyerSpawner(basicParams));
+    spawner.AddComponent(new spawners.StarDestroyerTurretSpawner(basicParams));
+    spawner.AddComponent(new spawners.ExplosionSpawner(basicParams));
+    spawner.AddComponent(new spawners.TinyExplosionSpawner(basicParams));
+    spawner.AddComponent(new spawners.ShipSmokeSpawner(basicParams));
+    this.entityManager_.Add(spawner, 'spawners');
+
+    // DEMO
+    spawner.GetComponent('PlayerSpawner').Spawn();
+
+    // DEMO
+    // for (let i = 0; i < 35; ++i) {
+    //   const e = spawner.GetComponent('TieFighterSpawner').Spawn();
+    //   const n = new THREE.Vector3(
+    //     math.rand_range(-1, 1),
+    //     math.rand_range(-1, 1),
+    //     math.rand_range(-1, 1),
+    //   );
+    //   n.normalize();
+    //   n.multiplyScalar(300);
+    //   // n.add(new THREE.Vector3(0, 0, 1000));
+    //   e.SetPosition(n);
+    // }
+    
+    // for (let i = 0; i < 6; ++i) {
+    //   const e = spawner.GetComponent('XWingSpawner').Spawn();
+    //   const n = new THREE.Vector3(
+    //     math.rand_range(-1, 1),
+    //     math.rand_range(-1, 1),
+    //     math.rand_range(-1, 1),
+    //   );
+    //   n.normalize();
+    //   n.multiplyScalar(300);
+    //   // n.add(new THREE.Vector3(0, 0, 800));
+    //   e.SetPosition(n);
+    // }
+
+    spawner.GetComponent('StarDestroyerSpawner').Spawn();
+  }
+
+  RAF_() {
+    requestAnimationFrame((t) => {
+      if (this.previousRAF_ === null) {
+        this.previousRAF_ = t;
+      } else {
+        this.Step_(t - this.previousRAF_);
+        this.threejs_.Render();
+        this.previousRAF_ = t;
+      }
+
+      setTimeout(() => {
+        this.RAF_();
+      }, 1);
+    });
+  }
+
+  Step_(timeElapsed) {
+    // DEMO
+    // const timeElapsedS = Math.min(1.0 / 30.0, timeElapsed * 0.001) * 0.5;
+    const timeElapsedS = Math.min(1.0 / 30.0, timeElapsed * 0.001);
+
+    this.entityManager_.Update(timeElapsedS, 0);
+    this.entityManager_.Update(timeElapsedS, 1);
+
+    this.ammojs_.StepSimulation(timeElapsedS);
+  }
+}
+
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', () => {
+  const _Setup = () => {
+    Ammo().then(function(AmmoLib) {
+      Ammo = AmmoLib;
+      _APP = new QuickGame2_Sequel();
+    }); 
+    document.body.removeEventListener('click', _Setup);
+  };
+  document.body.addEventListener('click', _Setup);
+});

+ 42 - 0
src/math.js

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

+ 128 - 0
src/mesh-rigid-body.js

@@ -0,0 +1,128 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+
+export const mesh_rigid_body = (() => {
+
+  class MeshRigidBody extends entity.Component {
+    constructor(params) {
+      super();
+      this.group_ = new THREE.Group();
+      this.params_ = params;
+      // this.params_.scene.add(this.group_);
+    }
+
+    Destroy() {
+      this.group_.traverse(c => {
+        if (c.material) {
+          c.material.dispose();
+        }
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      this.params_.scene.remove(this.group_);
+      this.FindEntity('physics').GetComponent('AmmoJSController').RemoveRigidBody(this.body_);
+    }
+
+    InitEntity() {
+      this._LoadModels();
+    }
+  
+    _LoadModels() {
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.Load(
+          this.params_.resourcePath, this.params_.resourceName, (mdl) => {
+        this._OnLoaded(mdl);
+      });
+    }
+
+    _OnLoaded(obj) {
+      this.target_ = obj;
+      this.group_.add(this.target_);
+      this.group_.position.copy(this.Parent.Position);
+      this.group_.quaternion.copy(this.Parent.Quaternion);
+
+      this.target_.scale.setScalar(this.params_.scale);
+      this.target_.traverse(c => {
+        if (c.geometry) {
+          c.geometry.computeBoundingBox();
+        }
+      });
+
+      this.Broadcast({
+          topic: 'loaded.collision',
+          value: this.target_,
+      });
+    }
+
+    OnMeshLoaded_(msg) {
+      const target = msg.value;
+      const pos = this.Parent.Position;
+      const quat = this.Parent.Quaternion;
+
+      this.body_ = this.FindEntity('physics').GetComponent('AmmoJSController').CreateMesh(
+          target, pos, quat, {name: this.Parent.Name});
+
+      const s = new THREE.Sphere();
+      const b = new THREE.Box3().setFromObject(target);
+      b.getBoundingSphere(s);
+      this.Parent.Attributes.roughRadius = s.radius;
+
+      this.OnTransformChanged_();
+      this.Broadcast({topic: 'physics.loaded'});
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('loaded.collision', (m) => this.OnMeshLoaded_(m) );
+      this.RegisterHandler_('update.position', (m) => { this.OnPosition_(m); });
+      this.RegisterHandler_('update.rotation', (m) => { this.OnRotation_(m); });
+    }
+
+    OnPosition_(m) {
+      this.OnTransformChanged_();
+    }
+
+    OnRotation_(m) {
+      this.OnTransformChanged_();
+    }
+
+    // OnTransformChanged_() {
+    //   const pos = this.Parent.Position;
+    //   const quat = this.Parent.Quaternion;
+    //   const ms = this.body_.motionState_;
+    //   const t = this.body_.transform_;
+      
+    //   ms.getWorldTransform(t);
+    //   t.setIdentity();
+    //   t.getOrigin().setValue(pos.x, pos.y, pos.z);
+    //   t.getRotation().setValue(quat.x, quat.y, quat.z, quat.w);
+    //   ms.setWorldTransform(t);
+
+    //   const origin = pos;
+    //   this.group_.position.copy(origin);
+    //   this.group_.quaternion.copy(quat);
+    // }
+
+
+    OnTransformChanged_() {
+      const pos = this.Parent.Position;
+      const quat = this.Parent.Quaternion;
+      const ms = this.body_.body_.getMotionState();
+
+      const t = new Ammo.btTransform();
+      t.setIdentity();
+      t.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
+      t.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
+      ms.setWorldTransform(t);
+    }
+
+    Update(_) {
+    }
+  };
+
+  return {
+    MeshRigidBody: MeshRigidBody,
+  };
+})();

+ 45 - 0
src/noise.js

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

+ 347 - 0
src/particle-system.js

@@ -0,0 +1,347 @@
+import {THREE} from './three-defs.js';
+
+
+export const particle_system = (() => {
+
+  const _VS = `
+  uniform float pointMultiplier;
+  
+  attribute float size;
+  attribute float angle;
+  attribute float blend;
+  attribute vec4 colour;
+  
+  varying vec4 vColour;
+  varying vec2 vAngle;
+  varying float vBlend;
+  
+  void main() {
+    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
+  
+    gl_Position = projectionMatrix * mvPosition;
+    gl_PointSize = size * pointMultiplier / gl_Position.w;
+  
+    vAngle = vec2(cos(angle), sin(angle));
+    vColour = colour;
+    vBlend = blend;
+  }`;
+  
+  const _FS = `
+  
+  uniform sampler2D diffuseTexture;
+  
+  varying vec4 vColour;
+  varying vec2 vAngle;
+  varying float vBlend;
+  
+  void main() {
+    vec2 coords = (gl_PointCoord - 0.5) * mat2(vAngle.x, vAngle.y, -vAngle.y, vAngle.x) + 0.5;
+    gl_FragColor = texture2D(diffuseTexture, coords) * vColour;
+    gl_FragColor.xyz *= gl_FragColor.w;
+    gl_FragColor.w *= vBlend;
+  }`;
+  
+   
+  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]);
+    }
+  }
+  
+  class ParticleEmitter {
+    constructor() {
+      this.alphaSpline_ = new LinearSpline((t, a, b) => {
+        return a + t * (b - a);
+      });
+  
+      this.colourSpline_ = new LinearSpline((t, a, b) => {
+        const c = a.clone();
+        return c.lerp(b, t);
+      });
+  
+      this.sizeSpline_ = new LinearSpline((t, a, b) => {
+        return a + t * (b - a);
+      });
+
+      this.emissionRate_ = 0.0;
+      this.emissionAccumulator_ = 0.0;
+      this.particles_ = [];
+      this.emitterLife_ = null;
+      this.delay_ = 0.0;
+    }
+
+    OnDestroy() {
+    }
+
+    UpdateParticles_(timeElapsed) {
+      for (let p of this.particles_) {
+        p.life -= timeElapsed;
+      }
+  
+      this.particles_ = this.particles_.filter(p => {
+        return p.life > 0.0;
+      });
+  
+      for (let i = 0; i < this.particles_.length; ++i) {
+        const p = this.particles_[i];
+        const t = 1.0 - p.life / p.maxLife;
+
+        if (t < 0 || t > 1) {
+          let a =  0;
+        }
+  
+        p.rotation += timeElapsed * 0.5;
+        p.alpha = this.alphaSpline_.Get(t);
+        p.currentSize = p.size * this.sizeSpline_.Get(t);
+        p.colour.copy(this.colourSpline_.Get(t));
+  
+        p.position.add(p.velocity.clone().multiplyScalar(timeElapsed));
+        p.velocity.multiplyScalar(p.drag);
+  
+        // const drag = p.velocity.clone();
+        // drag.multiplyScalar(timeElapsed * 0.1);
+        // drag.x = Math.sign(p.velocity.x) * Math.min(Math.abs(drag.x), Math.abs(p.velocity.x));
+        // drag.y = Math.sign(p.velocity.y) * Math.min(Math.abs(drag.y), Math.abs(p.velocity.y));
+        // drag.z = Math.sign(p.velocity.z) * Math.min(Math.abs(drag.z), Math.abs(p.velocity.z));
+        // p.velocity.sub(drag);
+      }
+    }
+    
+    CreateParticle_() {
+      const life = (Math.random() * 0.75 + 0.25) * 5.0;
+      return {
+          position: new THREE.Vector3(
+              (Math.random() * 2 - 1) * 4.0 + -44,
+              (Math.random() * 2 - 1) * 4.0 + 0,
+              (Math.random() * 2 - 1) * 4.0 + 12),
+          size: (Math.random() * 0.5 + 0.5) * 2.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: new THREE.Vector3(0, 1, 0),
+          blend: 0.0,
+          drag: 1.0,
+      };
+    }
+
+    get IsAlive() {
+      if (this.emitterLife_ === null) {
+        return this.particles_.length > 0;
+      } else {
+        return this.emitterLife_ > 0.0 || this.particles_.length > 0;
+      }
+    }
+
+    get IsEmitterAlive() {
+      return (this.emitterLife_ === null || this.emitterLife_ > 0.0);
+    }
+
+    SetLife(life) {
+      this.emitterLife_ = life;
+    }
+
+    SetEmissionRate(rate) {
+      this.emissionRate_ = rate;
+    }
+
+    OnUpdate_(_) {
+    }
+
+    Update(timeElapsed) {
+      if(this.delay_ > 0.0) {
+        this.delay_ -= timeElapsed;
+        return;
+      }
+
+      this.OnUpdate_(timeElapsed);
+
+      if (this.emitterLife_ !== null) {
+        this.emitterLife_ -= timeElapsed;
+      }
+
+      if (this.emissionRate_ > 0.0 && this.IsEmitterAlive) {
+        this.emissionAccumulator_ += timeElapsed;
+        const n = Math.floor(this.emissionAccumulator_ * this.emissionRate_);
+        this.emissionAccumulator_ -= n / this.emissionRate_;
+    
+        for (let i = 0; i < n; i++) {
+          const p = this.CreateParticle_();
+          this.particles_.push(p);
+        }
+      }
+
+      this.UpdateParticles_(timeElapsed);
+    }
+  };
+
+  
+  class ParticleSystem {
+    constructor(params) {
+      const uniforms = {
+          diffuseTexture: {
+              value: new THREE.TextureLoader().load(params.texture)
+          },
+          pointMultiplier: {
+              value: window.innerHeight / (2.0 * Math.tan(0.5 * 60.0 * Math.PI / 180.0))
+          }
+      };
+  
+      this.material_ = new THREE.ShaderMaterial({
+          uniforms: uniforms,
+          vertexShader: _VS,
+          fragmentShader: _FS,
+          blending: THREE.CustomBlending,
+          blendEquation: THREE.AddEquation,
+          blendSrc: THREE.OneFactor,
+          blendDst: THREE.OneMinusSrcAlphaFactor,
+          depthTest: true,
+          depthWrite: false,
+          transparent: true,
+          vertexColors: true
+      });
+  
+      this.camera_ = params.camera;
+      this.particles_ = [];
+  
+      this.geometry_ = new THREE.BufferGeometry();
+      this.geometry_.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
+      this.geometry_.setAttribute('size', new THREE.Float32BufferAttribute([], 1));
+      this.geometry_.setAttribute('colour', new THREE.Float32BufferAttribute([], 4));
+      this.geometry_.setAttribute('angle', new THREE.Float32BufferAttribute([], 1));
+      this.geometry_.setAttribute('blend', new THREE.Float32BufferAttribute([], 1));
+  
+      this.points_ = new THREE.Points(this.geometry_, this.material_);
+  
+      params.parent.add(this.points_);
+  
+      this.emitters_ = [];
+      this.particles_ = [];
+  
+      this.UpdateGeometry_();
+    }
+  
+    Destroy() {
+      this.material_.dispose();
+      this.geometry_.dispose();
+      if (this.points_.parent) {
+        this.points_.parent.remove(this.points_);
+      }
+    }
+  
+
+    AddEmitter(e) {
+      this.emitters_.push(e);
+    }
+
+    UpdateGeometry_() {
+      const positions = [];
+      const sizes = [];
+      const colours = [];
+      const angles = [];
+      const blends = [];
+  
+      const box = new THREE.Box3();
+      for (let p of this.particles_) {
+        positions.push(p.position.x, p.position.y, p.position.z);
+        colours.push(p.colour.r, p.colour.g, p.colour.b, p.alpha);
+        sizes.push(p.currentSize);
+        angles.push(p.rotation);
+        blends.push(p.blend);
+  
+        box.expandByPoint(p.position);
+      }
+  
+      this.geometry_.setAttribute(
+          'position', new THREE.Float32BufferAttribute(positions, 3));
+      this.geometry_.setAttribute(
+          'size', new THREE.Float32BufferAttribute(sizes, 1));
+      this.geometry_.setAttribute(
+          'colour', new THREE.Float32BufferAttribute(colours, 4));
+      this.geometry_.setAttribute(
+          'angle', new THREE.Float32BufferAttribute(angles, 1));
+      this.geometry_.setAttribute(
+          'blend', new THREE.Float32BufferAttribute(blends, 1));
+    
+      this.geometry_.attributes.position.needsUpdate = true;
+      this.geometry_.attributes.size.needsUpdate = true;
+      this.geometry_.attributes.colour.needsUpdate = true;
+      this.geometry_.attributes.angle.needsUpdate = true;
+      this.geometry_.attributes.blend.needsUpdate = true;
+      this.geometry_.boundingBox = box;
+      this.geometry_.boundingSphere = new THREE.Sphere();
+  
+      box.getBoundingSphere(this.geometry_.boundingSphere);
+    }
+  
+    UpdateParticles_(timeElapsed) {
+      this.particles_ = this.emitters_.map(e => e.particles_);
+      this.particles_ = this.particles_.flat();
+      this.particles_.sort((a, b) => {
+        const d1 = this.camera_.position.distanceTo(a.position);
+        const d2 = this.camera_.position.distanceTo(b.position);
+  
+        if (d1 > d2) {
+          return -1;
+        }
+  
+        if (d1 < d2) {
+          return 1;
+        }
+  
+        return 0;
+      });
+    }
+
+    UpdateEmitters_(timeElapsed) {
+      for (let i = 0; i < this.emitters_.length; ++i) {
+        this.emitters_[i].Update(timeElapsed);
+      }
+
+      const dead = this.emitters_.filter(e => !e.IsAlive);
+      for (let d of dead) {
+        d.OnDestroy();
+      }
+      this.emitters_= this.emitters_.filter(e => e.IsAlive);
+    }
+  
+    Update(timeElapsed) {
+      this.UpdateEmitters_(timeElapsed);
+      this.UpdateParticles_(timeElapsed);
+      this.UpdateGeometry_();
+    }
+  };
+
+  return {
+      ParticleEmitter: ParticleEmitter,
+      ParticleSystem: ParticleSystem,
+  };
+})();

+ 123 - 0
src/player-controller.js

@@ -0,0 +1,123 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+import {math} from './math.js';
+
+
+export const player_controller = (() => {
+
+  class PlayerController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.dead_ = false;
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('physics.collision', (m) => { this.OnCollision_(m); });
+    }
+
+    InitEntity() {
+      this.decceleration_ = new THREE.Vector3(-0.0005, -0.0001, -1);
+      this.acceleration_ = new THREE.Vector3(100, 0.5, 25000);
+      this.velocity_ = new THREE.Vector3(0, 0, 0);
+    }
+
+    OnCollision_() {
+      if (!this.dead_) {
+        this.dead_ = true;
+        console.log('EXPLODE ' + this.Parent.Name);
+        this.Broadcast({topic: 'health.dead'});
+      }
+    }
+
+    Fire_() {
+      this.Broadcast({
+          topic: 'player.fire'
+      });
+    }
+
+    Update(timeInSeconds) {
+      if (this.dead_) {
+        return;
+      }
+
+      const input = this.Parent.Attributes.InputCurrent;
+      if (!input) {
+        return;
+      }
+
+      const velocity = this.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), 50.0, 125.0);
+  
+      const _PARENT_Q = this.Parent.Quaternion.clone();
+      const _PARENT_P = this.Parent.Position.clone();
+
+      const _Q = new THREE.Quaternion();
+      const _A = new THREE.Vector3();
+      const _R = _PARENT_Q.clone();
+  
+      const acc = this.acceleration_.clone();
+      if (input.shift) {
+        acc.multiplyScalar(2.0);
+      }
+  
+      if (input.axis1Forward) {
+        _A.set(1, 0, 0);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * acc.y * input.axis1Forward);
+        _R.multiply(_Q);
+      }
+      if (input.axis1Side) {
+        _A.set(0, 1, 0);
+        _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * acc.y * input.axis1Side);
+        _R.multiply(_Q);
+      }
+      if (input.axis2Side) {
+        _A.set(0, 0, -1);
+        _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * acc.y * input.axis2Side);
+        _R.multiply(_Q);
+      }
+  
+      const forward = new THREE.Vector3(0, 0, 1);
+      forward.applyQuaternion(_PARENT_Q);
+      forward.normalize();
+  
+      const updown = new THREE.Vector3(0, 1, 0);
+      updown.applyQuaternion(_PARENT_Q);
+      updown.normalize();
+
+      const sideways = new THREE.Vector3(1, 0, 0);
+      sideways.applyQuaternion(_PARENT_Q);
+      sideways.normalize();
+  
+      sideways.multiplyScalar(velocity.x * timeInSeconds);
+      updown.multiplyScalar(velocity.y * timeInSeconds);
+      forward.multiplyScalar(velocity.z * timeInSeconds);
+  
+      const pos = _PARENT_P;
+      pos.add(forward);
+      pos.add(sideways);
+      pos.add(updown);
+
+      this.Parent.SetPosition(pos);
+      this.Parent.SetQuaternion(_R);
+
+      if (input.space) {
+        this.Fire_();
+      }
+    }
+  };
+  
+  return {
+    PlayerController: PlayerController,
+  };
+
+})();

+ 111 - 0
src/player-input.js

@@ -0,0 +1,111 @@
+import {entity} from "./entity.js";
+
+
+export const player_input = (() => {
+
+  class PlayerInput extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+  
+    InitEntity() {
+      this.Parent.Attributes.InputCurrent = {
+        axis1Forward: 0.0,
+        axis1Side: 0.0,
+        axis2Forward: 0.0,
+        axis2Side: 0.0,
+        pageUp: false,
+        pageDown: false,
+        space: false,
+        shift: false,
+        backspace: false,
+      };
+      this.Parent.Attributes.InputPrevious = {
+        ...this.Parent.Attributes.InputCurrent};
+
+      document.addEventListener('keydown', (e) => this.OnKeyDown_(e), false);
+      document.addEventListener('keyup', (e) => this.OnKeyUp_(e), false);
+    }
+  
+    OnKeyDown_(event) {
+      if (event.currentTarget.activeElement != document.body) {
+        return;
+      }
+      switch (event.keyCode) {
+        case 87: // w
+          this.Parent.Attributes.InputCurrent.axis1Forward = -1.0;
+          break;
+        case 65: // a
+          this.Parent.Attributes.InputCurrent.axis1Side = -1.0;
+          break;
+        case 83: // s
+          this.Parent.Attributes.InputCurrent.axis1Forward = 1.0;
+          break;
+        case 68: // d
+          this.Parent.Attributes.InputCurrent.axis1Side = 1.0;
+          break;
+        case 33: // PG_UP
+          this.Parent.Attributes.InputCurrent.pageUp = true;
+          break;
+        case 34: // PG_DOWN
+          this.Parent.Attributes.InputCurrent.pageDown = true;
+          break;
+        case 32: // SPACE
+          this.Parent.Attributes.InputCurrent.space = true;
+          break;
+        case 16: // SHIFT
+          this.Parent.Attributes.InputCurrent.shift = true;
+          break;
+        case 8: // BACKSPACE
+          this.Parent.Attributes.InputCurrent.backspace = true;
+          break;
+      }
+    }
+  
+    OnKeyUp_(event) {
+      if (event.currentTarget.activeElement != document.body) {
+        return;
+      }
+      switch(event.keyCode) {
+        case 87: // w
+          this.Parent.Attributes.InputCurrent.axis1Forward = 0.0;
+          break;
+        case 65: // a
+          this.Parent.Attributes.InputCurrent.axis1Side = 0.0;
+          break;
+        case 83: // s
+          this.Parent.Attributes.InputCurrent.axis1Forward = 0.0;
+          break;
+        case 68: // d
+          this.Parent.Attributes.InputCurrent.axis1Side = 0.0;
+          break;
+        case 33: // PG_UP
+          this.Parent.Attributes.InputCurrent.pageUp = false;
+          break;
+        case 34: // PG_DOWN
+          this.Parent.Attributes.InputCurrent.pageDown = false;
+          break;
+        case 32: // SPACE
+          this.Parent.Attributes.InputCurrent.space = false;
+          break;
+        case 16: // SHIFT
+          this.Parent.Attributes.InputCurrent.shift = false;
+          break;
+        case 8: // BACKSPACE
+          this.Parent.Attributes.InputCurrent.backspace = false;
+          break;
+      }
+    }
+
+    Update(_) {
+      this.Parent.Attributes.InputPrevious = {
+          ...this.Parent.Attributes.InputCurrent};
+    }
+  };
+
+  return {
+    PlayerInput: PlayerInput,
+  };
+
+})();

+ 75 - 0
src/player-ps4-input.js

@@ -0,0 +1,75 @@
+import {entity} from "./entity.js";
+
+
+export const player_ps4_input = (() => {
+
+  window.addEventListener("gamepadconnected", () => {
+    const a = 0;
+  });
+
+
+  class PlayerPS4Input extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+  
+    InitEntity() {
+      this.Parent.Attributes.InputCurrent = {
+        axis1Forward: 0.0,
+        axis1Side: 0.0,
+        axis2Forward: 0.0,
+        axis2Side: 0.0,
+        pageUp: false,
+        pageDown: false,
+        space: false,
+        shift: false,
+        backspace: false,
+      };
+      this.Parent.Attributes.InputPrevious = {
+          ...this.Parent.Attributes.InputCurrent};
+    }
+  
+    ButtonPressed_(gp, index) {
+      const curButton = gp.buttons[index];
+      if (typeof(curButton) == 'object') {
+        return curButton.pressed;
+      }
+      return curButton == 1.0;
+    }
+
+    Update(_) {
+      const gamepads = navigator.getGamepads();
+      if (!gamepads) {
+        return;
+      }
+
+      const cur = gamepads[0];
+      if (!cur) {
+        return;
+      }
+
+      // X
+      this.Parent.Attributes.InputCurrent.space = this.ButtonPressed_(cur, 0);
+
+      // O
+      this.Parent.Attributes.InputCurrent.shift = this.ButtonPressed_(cur, 1);
+
+      // R/L
+      this.Parent.Attributes.InputCurrent.pageUp = this.ButtonPressed_(cur, 4);
+      this.Parent.Attributes.InputCurrent.pageDown = this.ButtonPressed_(cur, 5);
+      this.Parent.Attributes.InputCurrent.axis1Forward = cur.axes[1];
+      this.Parent.Attributes.InputCurrent.axis1Side = cur.axes[0];
+      this.Parent.Attributes.InputCurrent.axis2Forward = cur.axes[3];
+      this.Parent.Attributes.InputCurrent.axis2Side = cur.axes[2];
+
+      this.Parent.Attributes.InputPrevious = {
+          ...this.Parent.Attributes.InputCurrent};
+    }
+  };
+
+  return {
+    PlayerPS4Input: PlayerPS4Input,
+  };
+
+})();

+ 167 - 0
src/render-component.js

@@ -0,0 +1,167 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+
+export const render_component = (() => {
+
+  class RenderComponent extends entity.Component {
+    constructor(params) {
+      super();
+      this.group_ = new THREE.Group();
+      this.target_ = null;
+      this.offset_ = null;
+      this.params_ = params;
+      this.params_.scene.add(this.group_);
+    }
+
+    Destroy() {
+      this.group_.traverse(c => {
+        if (c.material) {
+          c.material.dispose();
+        }
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      this.params_.scene.remove(this.group_);
+    }
+
+    InitEntity() {
+      this._LoadModels();
+    }
+  
+    InitComponent() {
+      this.RegisterHandler_('update.position', (m) => { this.OnPosition_(m); });
+      this.RegisterHandler_('update.rotation', (m) => { this.OnRotation_(m); });
+      this.RegisterHandler_('render.visible', (m) => { this.OnVisible_(m); });
+      this.RegisterHandler_('render.offset', (m) => { this.OnOffset_(m.offset); });
+    }
+
+    OnVisible_(m) {
+      this.group_.visible = m.value;
+    }
+
+    OnPosition_(m) {
+      this.group_.position.copy(m.value);
+    }
+
+    OnRotation_(m) {
+      this.group_.quaternion.copy(m.value);
+    }
+
+    OnOffset_(offset) {
+      this.offset_ = offset;
+      if (!this.offset_) {
+        return;
+      }
+
+      if (this.target_) {
+        this.target_.position.copy(this.offset_.position);
+        this.target_.quaternion.copy(this.offset_.quaternion);
+      }
+    }
+
+    _LoadModels() {
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.Load(
+          this.params_.resourcePath, this.params_.resourceName, (mdl) => {
+        this._OnLoaded(mdl);
+      });
+    }
+
+    _OnLoaded(obj) {
+      this.target_ = obj;
+      this.group_.add(this.target_);
+      this.group_.position.copy(this.Parent.Position);
+      this.group_.quaternion.copy(this.Parent.Quaternion);
+
+      this.target_.scale.setScalar(this.params_.scale);
+      if (this.params_.offset) {
+        this.offset_ = this.params_.offset;
+      }
+      this.OnOffset_(this.offset_);
+
+      const textures = {};
+      if (this.params_.textures) {
+        const loader = this.FindEntity('loader').GetComponent('LoadController');
+
+        for (let k in this.params_.textures.names) {
+          const t = loader.LoadTexture(
+              this.params_.textures.resourcePath, this.params_.textures.names[k]);
+          t.encoding = THREE.sRGBEncoding;
+
+          if (this.params_.textures.wrap) {
+            t.wrapS = THREE.RepeatWrapping;
+            t.wrapT = THREE.RepeatWrapping;
+          }
+
+          textures[k] = t;
+        }
+      }
+
+      this.target_.traverse(c => {
+        let materials = c.material;
+        if (!(c.material instanceof Array)) {
+          materials = [c.material];
+        }
+
+        if (c.geometry) {
+          c.geometry.computeBoundingBox();
+        }
+
+        for (let m of materials) {
+          if (m) {
+            // HACK
+            m.depthWrite = true;
+            m.transparent = false;
+
+            if (this.params_.onMaterial) {
+              this.params_.onMaterial(m);
+            }
+            for (let k in textures) {
+              if (m.name.search(k) >= 0) {
+                m.map = textures[k];
+              }
+            }
+            if (this.params_.specular) {
+              m.specular = this.params_.specular;
+            }
+            if (this.params_.emissive) {
+              m.emissive = this.params_.emissive;
+            }
+            if (this.params_.colour) {
+              m.color = this.params_.colour;
+            }
+          }
+        }
+        if (this.params_.receiveShadow !== undefined) {
+          c.receiveShadow = this.params_.receiveShadow;
+        }
+        if (this.params_.castShadow !== undefined) {
+          c.castShadow = this.params_.castShadow;
+        }
+        if (this.params_.visible !== undefined) {
+          c.visible = this.params_.visible;
+        }
+
+        c.castShadow = true;
+        c.receiveShadow = true;
+      });
+
+      this.Broadcast({
+          topic: 'render.loaded',
+          value: this.target_,
+      });
+    }
+
+    Update(timeInSeconds) {
+    }
+  };
+
+
+  return {
+      RenderComponent: RenderComponent,
+  };
+
+})();

+ 248 - 0
src/shields-controller.js

@@ -0,0 +1,248 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+import {math} from './math.js';
+import {noise} from './noise.js';
+
+
+export const shields_controller = (() => {
+
+  const _SHIELD_VS = `
+  varying vec3 vNormal;
+  varying vec3 vNoiseCoords;
+  varying vec3 vWorldPosition;
+
+  void main() {
+    vec4 worldPosition = modelMatrix * vec4(position, 1.0);
+    vWorldPosition = worldPosition.xyz;
+    vec3 worldNormal = normalize(
+        mat3(modelMatrix[0].xyz,
+             modelMatrix[1].xyz,
+             modelMatrix[2].xyz) * normal);
+    vNormal = worldNormal;
+
+    vec3 noiseCoords = normalize(
+        mat3(modelMatrix[0].xyz,
+             modelMatrix[1].xyz,
+             modelMatrix[2].xyz) * (normal * vec3(0.5, 1.0, 1.0)));
+    vNoiseCoords = noiseCoords;
+
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+  }`;
+  
+  
+  const _SHIELD_FS = `
+  varying vec3 vNormal;
+  varying vec3 vNoiseCoords;
+  varying vec3 vWorldPosition;
+  uniform float time;
+  uniform float visibility;
+  
+  // Taken from https://www.shadertoy.com/view/4sfGzS
+  float hash(vec3 p)  // replace this by something better
+  {
+    p  = fract( p*0.3183099+.1 );
+    p *= 17.0;
+    return fract( p.x*p.y*p.z*(p.x+p.y+p.z) );
+  }
+
+  float noise( in vec3 x )
+  {
+    vec3 i = floor(x);
+    vec3 f = fract(x);
+    f = f*f*(3.0-2.0*f);
+  
+    return mix(mix(mix( hash(i+vec3(0,0,0)), 
+                        hash(i+vec3(1,0,0)),f.x),
+                   mix( hash(i+vec3(0,1,0)), 
+                        hash(i+vec3(1,1,0)),f.x),f.y),
+               mix(mix( hash(i+vec3(0,0,1)), 
+                        hash(i+vec3(1,0,1)),f.x),
+                   mix( hash(i+vec3(0,1,1)), 
+                        hash(i+vec3(1,1,1)),f.x),f.y),f.z);
+  }
+
+  float FBM(vec3 p, int octaves) {
+    float w = length(fwidth(p));
+    float G = pow(2.0, -1.0);
+    float amplitude = 1.0;
+    float frequency = 1.0;
+    float lacunarity = 2.0;
+    float normalization = 0.0;
+    float total = 0.0;
+    
+    for (int i = 0; i < octaves; ++i) {
+      float noiseValue = noise(p * frequency);
+
+      noiseValue = abs(noiseValue);
+      // noiseValue *= noiseValue;
+
+      total += noiseValue * amplitude;
+      normalization += amplitude;
+      amplitude *= G;
+      frequency *= lacunarity;
+    }
+
+    // total = total * 0.5 + 0.5;
+    // total = pow(total, 1.0);
+
+    return total;
+  }
+
+  vec2  hash2( vec2  p ) { p = vec2( dot(p,vec2(117.13,313.74)), dot(p,vec2(269.5,183.3)) ); return fract(sin(p)*43758.5453); }
+
+  // The parameter w controls the smoothness
+  float voronoi( in vec3 coords)
+  {
+    vec2 x = coords.xy;
+    float w = 0.0;
+      vec2 n = floor( x );
+      vec2 f = fract( x );
+
+    vec4 m = vec4( 8.0, 0.0, 0.0, 0.0 );
+      for( int j=-2; j<=2; j++ )
+      for( int i=-2; i<=2; i++ )
+      {
+          vec2 g = vec2( float(i),float(j) );
+          // vec2 o = hash2( n + g );
+          vec3 ng = vec3(n + g, coords.z);
+          vec2 o = vec2(
+              noise(ng + vec3(217.3, 325.3, 0.0)),
+              noise(ng));
+      
+          // distance to cell		
+      float d = length(g - f + o);
+      d = pow(d, 1.25);
+      
+      float h = smoothstep( 0.0, 1.0, 0.5 + 0.5*(m.x-d)/w );
+      
+        m.x   = mix( m.x,     d, h ) - h*(1.0-h)*w/(1.0+3.0*w); // distance
+      }
+    
+    return clamp(m.x, 0.0, 1.0);
+  }
+
+  void main() {
+    vec3 norm = normalize(vNormal);
+    vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
+    float fresnel = clamp(dot(viewDirection, norm), 0.0, 1.0);
+    fresnel = 1.0 - fresnel;
+    fresnel *= fresnel;
+    fresnel = smoothstep(0.0, 1.0, fresnel);
+
+    float fbm1 = FBM(vNoiseCoords * 5.0, 8);
+    float fbm2 = FBM(vNoiseCoords * 5.0 + vec3(fbm1 + time * 2.0), 8);
+
+    fbm2 = clamp(fbm2, 0.0, 1.0) * 2.0 - 1.0;
+    fbm2 = (1.0 - abs(fbm2)) * 0.5 + 0.75 * fbm1;
+    // float n2 = voronoi(vNoiseCoords * 5.0);
+    vec3 col = vec3(0.25, 0.25, 0.75) * (fbm2) * fresnel;
+
+    // float edgeGlow = fresnel * fresnel;
+    // edgeGlow *= edgeGlow;
+    // edgeGlow *= 0.5;
+    // col += vec3(edgeGlow);
+  
+    gl_FragColor = vec4(col * visibility, 1.0);
+  }`;
+
+
+  function easeOutBounce(x) {
+    const n1 = 7.5625;
+    const d1 = 2.75;
+    
+    if (x < 1 / d1) {
+      return n1 * x * x;
+    } else if (x < 2 / d1) {
+      return n1 * (x -= 1.5 / d1) * x + 0.75;
+    } else if (x < 2.5 / d1) {
+      return n1 * (x -= 2.25 / d1) * x + 0.9375;
+    } else {
+      return n1 * (x -= 2.625 / d1) * x + 0.984375;
+    }
+  }
+
+  class ShieldsController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.timeElapsed_ = 0.0;
+      this.shieldsVisible_ = false;
+      this.shieldsTimer_ = 0.0;
+      this.noise_ = new noise.Noise({
+        octaves: 6,
+        persistence: 0.5,
+        lacunarity: 1.6,
+        exponentiation: 1.0,
+        height: 1.0,
+        scale: 0.1,
+        seed: 1
+      });
+    }
+
+    InitEntity() {
+      const sphereGeo = new THREE.SphereBufferGeometry(1, 32, 32);
+      const shieldMat = new THREE.ShaderMaterial({
+          uniforms: {
+            time: { value: 0.0 },
+            visibility: { value: 0.0 },
+          },
+          vertexShader: _SHIELD_VS,
+          fragmentShader: _SHIELD_FS,
+          side: THREE.FrontSide,
+          depthTest: true,
+          depthWrite: false,
+          transparent: true,
+          blending: THREE.AdditiveBlending,
+      });
+
+      this.shields_ = new THREE.Mesh(sphereGeo, shieldMat);
+      this.shields_.scale.set(20, 10, 10);
+
+      const group = this.GetComponent('RenderComponent').group_;
+      group.add(this.shields_);
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('health.damage', () => { this.OnHit_(); });
+    }
+
+    OnHit_() {
+      if (this.Parent.Attributes.dead) {
+        return;
+      }
+
+      if (this.Parent.Attributes.shields > 0) {
+        const loader = this.FindEntity('loader').GetComponent('LoadController');
+        loader.LoadSound('./resources/sounds/', 'shields.ogg', (s) => {
+          const group = this.GetComponent('RenderComponent').group_;
+          group.add(s);
+          s.play();  
+        });
+
+        this.shieldsVisible_ = true;
+        this.shieldsTimer_ = 0.0;
+      }
+    }
+
+    Update(timeElapsed) {
+      this.timeElapsed_ += timeElapsed;
+      this.shieldsTimer_ += timeElapsed;
+      this.shields_.material.uniforms.time.value = this.timeElapsed_;
+
+      this.Parent.Attributes.shields += timeElapsed * 0.5;
+      this.Parent.Attributes.shields = Math.min(
+        this.Parent.Attributes.shields, this.Parent.Attributes.maxShields);
+
+      if (this.shieldsVisible_) {
+        const e = 1.0 - math.sat(easeOutBounce(this.shieldsTimer_ * 0.2)) ** 2;
+        const t = this.noise_.Get(this.shieldsTimer_, 21.4, 53.2);
+        this.shields_.material.uniforms.visibility.value = t * e;
+      }
+    }
+  };
+
+  return {
+    ShieldsController: ShieldsController
+  };
+})();

+ 204 - 0
src/shields-ui-controller.js

@@ -0,0 +1,204 @@
+import {THREE} from './three-defs.js';
+import {entity} from './entity.js';
+
+
+export const shields_ui_controller = (() => {
+
+  const _VS = `
+  varying vec3 vWorldPosition;
+  varying vec2 vUV;
+  
+  void main() {
+    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
+    vWorldPosition = worldPosition.xyz;
+    vUV = uv;
+  
+    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+  }`;
+  
+  
+  const _FS = `
+  uniform float time;
+  uniform float shields;
+  uniform vec3 colour;
+  
+  varying vec3 vWorldPosition;
+  varying vec2 vUV;
+
+  float sdf_Box(vec2 coords, vec2 bounds) {
+    vec2 dist = abs(coords) - bounds;
+    return length(max(dist, 0.0)) + min(max(dist.x, dist.y), 0.0);
+  }
+
+  float smootherstep(float a, float b, float x) {
+    x = clamp((x - a) / (b - a), 0.0, 1.0);
+    return x * x * x * (x * ( x * 6.0 - 15.0) + 10.0);
+  }
+
+  // The MIT License
+  // Copyright © 2013 Inigo Quilez
+  // https://www.youtube.com/c/InigoQuilez
+  // https://iquilezles.org/
+  // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+  // https://www.shadertoy.com/view/4sfGzS
+  float hash(vec3 p)  // replace this by something better
+  {
+      p  = fract( p*0.3183099+.1 );
+    p *= 17.0;
+      return fract( p.x*p.y*p.z*(p.x+p.y+p.z) );
+  }
+  
+  float noise( in vec3 x )
+  {
+      vec3 i = floor(x);
+      vec3 f = fract(x);
+      f = f*f*(3.0-2.0*f);
+    
+      return mix(mix(mix( hash(i+vec3(0,0,0)), 
+                          hash(i+vec3(1,0,0)),f.x),
+                     mix( hash(i+vec3(0,1,0)), 
+                          hash(i+vec3(1,1,0)),f.x),f.y),
+                 mix(mix( hash(i+vec3(0,0,1)), 
+                          hash(i+vec3(1,0,1)),f.x),
+                     mix( hash(i+vec3(0,1,1)), 
+                          hash(i+vec3(1,1,1)),f.x),f.y),f.z);
+  }
+
+  const mat3 _M = mat3( 0.00,  0.80,  0.60,
+                      -0.80,  0.36, -0.48,
+                      -0.60, -0.48,  0.64 );
+
+  float FBM(vec3 p, int octaves) {
+    float total = 0.0;
+    float amplitude = 0.5;
+    vec3 q = p;
+
+    for (int i = 0; i < octaves; ++i) {
+      total += noise(q) * amplitude;
+      amplitude *= 0.5;
+
+      q = _M * q * 2.0;
+    }
+
+    total = 1.0 - abs(total * 2.0 - 1.0);
+    total = abs(total * 2.0 - 1.0);
+    total = abs(total * 2.0 - 1.0);
+    total = abs(total * 2.0 - 1.0);
+    // total = total * 0.5 + 0.5;
+    // total = pow(total, 1.0);
+
+    return total;
+  }
+
+  void main() {
+    vec2 boxCoords = fract(vUV * vec2(8.0, 1.0)) - 0.5;
+    float d = sdf_Box(boxCoords, vec2(0.3, 0.4)) - 0.1;
+
+    float a = smoothstep(0.0, -0.05, d) * step(round(vUV.x * 8.0 + 0.5)/8.0, shields);
+    float c = mix(0.9, 1.0, clamp(smoothstep(-0.05, -0.2, d), 0.0, 1.0));
+    vec3 col = colour;
+
+    col *= 1.0 - exp(-16.0*abs(d));
+    col *= vec3(pow(16.0*vUV.x*(1.0-vUV.x)*vUV.y*(1.0-vUV.y), 0.1));
+    col *= vec3(FBM(vec3(vUV * vec2(8.0, 1.0), 0.5*time), 6) * 0.2 + 0.8);
+
+    col *= c;
+
+    gl_FragColor = vec4(col, a);
+  }`;
+
+
+  const _C1 = new THREE.Color(0xfa5959);
+  const _C2 = new THREE.Color(0x2ce93c);
+
+
+  class ShieldsUIController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.timeElapsed_ = 0.0;
+      this.colourTarget_ = _C2.clone();
+    }
+
+    Destroy() {
+      this.sprite_.traverse(c => {
+        if (c.material) {
+          let materials = c.material;
+          if (!(c.material instanceof Array)) {
+            materials = [c.material];
+          }
+          for (let m of materials) {
+            m.dispose();
+          }
+        }
+
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      if (this.sprite_.parent) {
+        this.sprite_.parent.remove(this.sprite_);
+      }
+    }
+
+    InitEntity() {
+      this.CreateSprite_();
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('player.hit', (m) => { this.UpdateShieldColour_(); } );
+    }
+
+    UpdateShieldColour_() {
+      const t = this.Parent.Attributes.shields / this.Parent.Attributes.maxShields;
+      this.colourTarget_ = _C1.clone().lerpHSL(_C2, t);
+    }
+
+    OnDeath_() {
+      this.Destroy();
+    }
+
+    CreateSprite_() {
+      const mat = new THREE.ShaderMaterial({
+        uniforms: {
+          time: {value: 0.0},
+          shields: {value: 0.0},
+          colour: {value: _C2.clone()}
+        },
+        vertexShader: _VS,
+        fragmentShader: _FS,
+        side: THREE.FrontSide,
+        transparent: true,
+      });
+
+      this.sprite_ = new THREE.Sprite(mat);
+      this.sprite_.scale.set(32, 4, 1)
+      this.sprite_.position.set(0, 5, 0);
+
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      threejs.uiScene_.add(this.sprite_);
+    }
+
+    Update(timeElapsed) {
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      const camera = threejs.camera_;
+      this.timeElapsed_ += timeElapsed;
+
+      const ndc = new THREE.Vector3(0, 1.5, -10);
+
+      this.UpdateShieldColour_();
+
+      const t = 1.0 - Math.pow(0.05, timeElapsed);
+
+      this.sprite_.material.uniforms.colour.value.lerpHSL(this.colourTarget_, t);
+      this.sprite_.material.uniforms.time.value = this.timeElapsed_;
+      this.sprite_.material.uniforms.shields.value = this.Parent.Attributes.shields / this.Parent.Attributes.maxShields;
+      this.sprite_.scale.set(0.8, 0.1 * camera.aspect, 1);
+      this.sprite_.position.copy(ndc);
+    }
+  };
+
+  return {
+    ShieldsUIController: ShieldsUIController,
+  };
+})();

+ 109 - 0
src/ship-effects.js

@@ -0,0 +1,109 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from "./entity.js";
+
+export const ship_effect = (() => {
+
+  class SmokeFXEmitter extends particle_system.ParticleEmitter {
+    constructor(offset, parent) {
+      super();
+      this.parent_ = parent;
+      this.offset_ = offset;
+      this.blend_ = 1.0;
+    }
+
+    OnUpdate_() {
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const life = (Math.random() * 0.85 + 0.15) * 4.0;
+      const p = this.offset_.clone().applyQuaternion(this.parent_.Quaternion).add(this.parent_.Position);
+      const d = new THREE.Vector3(0, 0, 0);
+
+      return {
+          position: p,
+          size: (Math.random() * 0.5 + 0.5) * 5.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+
+  class ShipEffects extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.particles_ = null;
+      this.emitter_ = null;
+    }
+
+
+    Destroy() {
+      this.particles_.Destroy();
+      this.particles_ = null;
+    }
+
+    InitEntity() {
+      this.particles_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: this.params_.scene,
+          texture: './resources/textures/fx/smoke.png',
+      });
+      this.OnDamaged_();
+    }
+
+    OnDamaged_() {
+      const emitter = new SmokeFXEmitter(new THREE.Vector3(0, 0, 5), this.Parent);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.7, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x808080));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x404040));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.25, 2.0);
+      emitter.sizeSpline_.AddPoint(0.75, 4.0);
+      emitter.sizeSpline_.AddPoint(1.0, 10.0);
+      emitter.SetEmissionRate(50);
+      emitter.SetLife(3.0);
+      emitter.blend_ = 1.0;
+      this.particles_.AddEmitter(emitter);
+      emitter.AddParticles(10);
+
+      this.emitter_ = emitter;
+    }
+
+    Update(timeElapsed) {
+      this.particles_.Update(timeElapsed);
+
+      if (!this.emitter_.IsAlive) {
+        this.Parent.SetDead(true);
+        return;
+      }
+      if (this.params_.target.IsDead) {
+        this.emitter_.SetLife(0.0);
+        return;
+      }
+      this.Parent.SetPosition(this.params_.target.Position);
+    }
+  }
+  
+  return {
+    ShipEffects: ShipEffects,
+  };
+})();

+ 479 - 0
src/simplex-noise.js

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

+ 52 - 0
src/spatial-grid-controller.js

@@ -0,0 +1,52 @@
+import {entity} from './entity.js';
+
+
+export const spatial_grid_controller = (() => {
+
+  class SpatialGridController extends entity.Component {
+    constructor(params) {
+      super();
+
+      this.grid_ = params.grid;
+    }
+
+    Destroy() {
+      this.grid_.Remove(this.client_);
+      this.client_ = null;
+    }
+
+    InitEntity() {
+      this.RegisterHandler_('physics.loaded', () => this.OnPhysicsLoaded_());
+
+      const pos = [
+        this.Parent.Position.x,
+        this.Parent.Position.z,
+      ];
+
+      this.client_ = this.grid_.NewClient(pos, [1, 1]);
+      this.client_.entity = this.parent_;
+    }
+
+    OnPhysicsLoaded_() {
+      this.RegisterHandler_('update.position', (m) => this.OnPosition_());
+      this.OnPosition_();
+    }
+
+    OnPosition_() {
+      const pos = this.Parent.Position;
+      this.client_.position = [pos.x, pos.z];
+      this.grid_.UpdateClient(this.client_);
+    }
+
+    FindNearbyEntities(range) {
+      const results = this.grid_.FindNear(
+          [this.parent_._position.x, this.parent_._position.z], [range, range]);
+          
+      return results.filter(c => c.entity != this.parent_);
+    }
+  };
+
+  return {
+      SpatialGridController: SpatialGridController,
+  };
+})();

+ 164 - 0
src/spatial-hash-grid.js

@@ -0,0 +1,164 @@
+
+import {math} from './math.js';
+
+
+export const spatial_hash_grid = (() => {
+
+  class SpatialHashGrid {
+    constructor(bounds, dimensions) {
+      const [x, y] = dimensions;
+      this._cells = [...Array(x)].map(_ => [...Array(y)].map(_ => (null)));
+      this._dimensions = dimensions;
+      this._bounds = bounds;
+      this._queryIds = 0;
+      this.ids_ = 0;
+    }
+  
+    _GetCellIndex(position) {
+      const x = math.sat((position[0] - this._bounds[0][0]) / (
+          this._bounds[1][0] - this._bounds[0][0]));
+      const y = math.sat((position[1] - this._bounds[0][1]) / (
+          this._bounds[1][1] - this._bounds[0][1]));
+  
+      const xIndex = Math.floor(x * (this._dimensions[0] - 1));
+      const yIndex = Math.floor(y * (this._dimensions[1] - 1));
+  
+      return [xIndex, yIndex];
+    }
+  
+    NewClient(position, dimensions) {
+      const client = {
+        position: position,
+        dimensions: dimensions,
+        _cells: {
+          min: null,
+          max: null,
+          nodes: null,
+        },
+        _queryId: -1,
+        id_: this.ids_++,
+      };
+  
+      this._Insert(client);
+  
+      return client;
+    }
+  
+    UpdateClient(client) {
+      const [x, y] = client.position;
+      const [w, h] = client.dimensions;
+  
+      const i1 = this._GetCellIndex([x - w / 2, y - h / 2]);
+      const i2 = this._GetCellIndex([x + w / 2, y + h / 2]);
+  
+      if (client._cells.min[0] == i1[0] &&
+          client._cells.min[1] == i1[1] &&
+          client._cells.max[0] == i2[0] &&
+          client._cells.max[1] == i2[1]) {
+        return;
+      }
+  
+      this.Remove(client);
+      this._Insert(client);
+    }
+  
+    FindNear(position, bounds) {
+      const [x, y] = position;
+      const [w, h] = bounds;
+  
+      const i1 = this._GetCellIndex([x - w / 2, y - h / 2]);
+      const i2 = this._GetCellIndex([x + w / 2, y + h / 2]);
+  
+      const clients = [];
+      const queryId = this._queryIds++;
+  
+      for (let x = i1[0], xn = i2[0]; x <= xn; ++x) {
+        for (let y = i1[1], yn = i2[1]; y <= yn; ++y) {
+          let head = this._cells[x][y];
+  
+          while (head) {
+            const v = head.client;
+            head = head.next;
+  
+            if (v._queryId != queryId) {
+              v._queryId = queryId;
+              clients.push(v);
+            }
+          }
+        }
+      }
+
+      return clients;
+    }
+  
+    _Insert(client) {
+      const [x, y] = client.position;
+      const [w, h] = client.dimensions;
+  
+      const i1 = this._GetCellIndex([x - w / 2, y - h / 2]);
+      const i2 = this._GetCellIndex([x + w / 2, y + h / 2]);
+  
+      const nodes = [];
+  
+      for (let x = i1[0], xn = i2[0]; x <= xn; ++x) {
+        nodes.push([]);
+  
+        for (let y = i1[1], yn = i2[1]; y <= yn; ++y) {
+          const xi = x - i1[0];
+  
+          const head = {
+            next: null,
+            prev: null,
+            client: client,
+          };
+  
+          nodes[xi].push(head);
+  
+          head.next = this._cells[x][y];
+          if (this._cells[x][y]) {
+            this._cells[x][y].prev = head;
+          }
+  
+          this._cells[x][y] = head;
+        }
+      }
+  
+      client._cells.min = i1;
+      client._cells.max = i2;
+      client._cells.nodes = nodes;
+    }
+  
+    Remove(client) {
+      const i1 = client._cells.min;
+      const i2 = client._cells.max;
+  
+      for (let x = i1[0], xn = i2[0]; x <= xn; ++x) {
+        for (let y = i1[1], yn = i2[1]; y <= yn; ++y) {
+          const xi = x - i1[0];
+          const yi = y - i1[1];
+          const node = client._cells.nodes[xi][yi];
+  
+          if (node.next) {
+            node.next.prev = node.prev;
+          }
+          if (node.prev) {
+            node.prev.next = node.next;
+          }
+  
+          if (!node.prev) {
+            this._cells[x][y] = node.next;
+          }
+        }
+      }
+  
+      client._cells.min = null;
+      client._cells.max = null;
+      client._cells.nodes = null;
+    }
+  }
+
+  return {
+    SpatialHashGrid: SpatialHashGrid,
+  };
+
+})();

+ 342 - 0
src/spawners.js

@@ -0,0 +1,342 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+import {player_controller} from './player-controller.js';
+import {player_input} from './player-input.js';
+import {player_ps4_input} from './player-ps4-input.js';
+import {render_component} from './render-component.js';
+import {xwing_controller} from './xwing-controller.js';
+import {xwing_effect} from './xwing-effects.js';
+import {third_person_camera} from './third-person-camera.js';
+import {tie_fighter_controller} from './tie-fighter-controller.js';
+import {basic_rigid_body} from './basic-rigid-body.js';
+import {mesh_rigid_body} from './mesh-rigid-body.js';
+import {explode_component} from './explode-component.js';
+import {health_controller} from './health-controller.js';
+import {ship_effect} from './ship-effects.js';
+import {floating_descriptor} from './floating-descriptor.js';
+import {crosshair} from './crosshair.js';
+import {spatial_grid_controller} from './spatial-grid-controller.js';
+import {enemy_ai_controller} from './enemy-ai-controller.js';
+import {star_destroyer_fighter_controller} from './star-destroyer-fighter-controller.js';
+import {turret_controller} from './turret-controller.js';
+import {shields_controller} from './shields-controller.js';
+import {shields_ui_controller} from './shields-ui-controller.js';
+import {atmosphere_effect} from './atmosphere-effect.js';
+
+
+export const spawners = (() => {
+
+  class PlayerSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+        offset: new THREE.Vector3(0, -5, -4),
+        blasterStrength: 10,
+      };
+
+      const player = new entity.Entity();
+      player.Attributes.team = 'allies';
+      player.SetPosition(new THREE.Vector3(0, 600, -300));
+      player.AddComponent(
+        new spatial_grid_controller.SpatialGridController(
+            {grid: this.params_.grid}));
+      player.AddComponent(new render_component.RenderComponent({
+        scene: params.scene,
+        resourcePath: './resources/models/x-wing/',
+        resourceName: 'scene.gltf',
+        scale: 2,
+        offset: {
+          position: new THREE.Vector3(0, -5, -4),
+          quaternion: new THREE.Quaternion(),
+        },
+      }));
+      player.AddComponent(new xwing_controller.XWingController(params));
+      player.AddComponent(new xwing_effect.XWingEffects(params));
+      player.AddComponent(new player_input.PlayerInput());
+      player.AddComponent(new player_ps4_input.PlayerPS4Input());
+      player.AddComponent(new player_controller.PlayerController());
+      player.AddComponent(new basic_rigid_body.BasicRigidBody({
+        box: new THREE.Vector3(18, 6, 8),
+      }));
+      player.AddComponent(new health_controller.HealthController({
+        maxHealth: 50,
+        shields: 50,
+      }));
+      player.AddComponent(new crosshair.Crosshair());
+      player.AddComponent(
+        new third_person_camera.ThirdPersonCamera({
+            camera: this.params_.camera,
+            target: player}));
+      player.AddComponent(
+          new shields_controller.ShieldsController(params));
+      player.AddComponent(
+          new shields_ui_controller.ShieldsUIController(params));
+      player.AddComponent(
+          new atmosphere_effect.AtmosphereEffect(params));
+
+      this.Manager.Add(player, 'player');
+
+      return player;
+    }
+  };
+
+  class TieFighterSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+        blasterStrength: 20,
+      };
+
+      const e = new entity.Entity();
+      e.AddComponent(
+        new spatial_grid_controller.SpatialGridController(
+            {grid: this.params_.grid}));
+      e.AddComponent(new render_component.RenderComponent({
+        scene: params.scene,
+        resourcePath: './resources/models/tie-fighter-gltf/',
+        resourceName: 'scene.gltf',
+        scale: 0.15,
+        colour: new THREE.Color(0xFFFFFF),
+      }));
+      e.AddComponent(new tie_fighter_controller.TieFighterController(params));
+      e.AddComponent(new basic_rigid_body.BasicRigidBody({
+        box: new THREE.Vector3(15, 15, 15)
+      }));
+      e.AddComponent(new health_controller.HealthController({
+        maxHealth: 50,
+      }));
+      // DEMO
+      e.AddComponent(new floating_descriptor.FloatingDescriptor());
+      e.AddComponent(new enemy_ai_controller.EnemyAIController({
+        grid: this.params_.grid,
+      }));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  class XWingSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+        blasterStrength: 10,
+        offset: new THREE.Vector3(0, -5, -4),
+      };
+
+      const e = new entity.Entity();
+      e.AddComponent(
+        new spatial_grid_controller.SpatialGridController(
+            {grid: this.params_.grid}));
+      e.AddComponent(new render_component.RenderComponent({
+        scene: params.scene,
+        resourcePath: './resources/models/x-wing/',
+        resourceName: 'scene.gltf',
+        scale: 2,
+        offset: {
+          position: new THREE.Vector3(0, -5, -4),
+          quaternion: new THREE.Quaternion(),
+        },
+      }));
+      e.AddComponent(new xwing_effect.XWingEffects(params));
+      e.AddComponent(new xwing_controller.XWingController(params));
+      e.AddComponent(new basic_rigid_body.BasicRigidBody({
+        box: new THREE.Vector3(15, 15, 15)
+      }));
+      e.AddComponent(new health_controller.HealthController({
+        maxHealth: 50,
+        shields: 50,
+      }));
+      // e.AddComponent(new floating_descriptor.FloatingDescriptor());
+      e.AddComponent(new enemy_ai_controller.EnemyAIController({
+        grid: this.params_.grid,
+      }));
+      e.AddComponent(
+          new shields_controller.ShieldsController(params));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  class StarDestroyerSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+      };
+
+      const e = new entity.Entity();
+      e.SetPosition(new THREE.Vector3(0, 0, -1000));
+      e.AddComponent(new render_component.RenderComponent({
+        scene: params.scene,
+        resourcePath: './resources/models/star-destroyer/',
+        resourceName: 'scene-final.glb',
+        scale: 50.0,
+        colour: new THREE.Color(0.5, 0.5, 0.5),
+      }));
+      e.AddComponent(new mesh_rigid_body.MeshRigidBody({
+        scene: params.scene,
+        resourcePath: './resources/models/star-destroyer/',
+        resourceName: 'scene-collision.glb',
+        scale: 50.0,
+      }));
+      e.AddComponent(
+          new star_destroyer_fighter_controller.StarDestroyerFighterController());
+
+      this.Manager.Add(e, 'star-destroyer');
+
+      return e;
+    }
+  };
+
+  class StarDestroyerTurretSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn(position, quaternion, correction) {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+        grid: this.params_.grid,
+        blasterStrength: 10,
+      };
+
+      position.add(new THREE.Vector3(0, 0, -1000));
+
+      const e = new entity.Entity();
+      e.SetPosition(position);
+      e.SetQuaternion(quaternion);
+      e.AddComponent(new render_component.RenderComponent({
+        scene: params.scene,
+        resourcePath: './resources/models/star-destroyer/',
+        resourceName: 'turret.glb',
+        scale: 50.0,
+        offset: {
+          position: new THREE.Vector3(),
+          quaternion: correction,
+        },
+      }));
+      e.AddComponent(new basic_rigid_body.BasicRigidBody({
+        box: new THREE.Vector3(25, 25, 25)
+      }));
+      // e.AddComponent(new floating_descriptor.FloatingDescriptor());
+      e.AddComponent(new health_controller.HealthController({
+        maxHealth: 40,
+        ignoreCollisions: true,
+      }));
+      e.AddComponent(new turret_controller.TurretController(params));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  class ShipSmokeSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn(target) {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+        target: target,
+      };
+
+      const e = new entity.Entity();
+      e.SetPosition(target.Position);
+      e.AddComponent(new ship_effect.ShipEffects(params));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  class ExplosionSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn(pos) {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+      };
+
+      const e = new entity.Entity();
+      e.SetPosition(pos);
+      e.AddComponent(new explode_component.ExplodeEffect(params));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  class TinyExplosionSpawner extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn(pos) {
+      const params = {
+        camera: this.params_.camera,
+        scene: this.params_.scene,
+      };
+
+      const e = new entity.Entity();
+      e.SetPosition(pos);
+      e.AddComponent(new explode_component.TinyExplodeEffect(params));
+
+      this.Manager.Add(e);
+
+      return e;
+    }
+  };
+
+  return {
+    PlayerSpawner: PlayerSpawner,
+    TieFighterSpawner: TieFighterSpawner,
+    XWingSpawner: XWingSpawner,
+    StarDestroyerSpawner: StarDestroyerSpawner,
+    StarDestroyerTurretSpawner: StarDestroyerTurretSpawner,
+    ExplosionSpawner: ExplosionSpawner,
+    TinyExplosionSpawner: TinyExplosionSpawner,
+    ShipSmokeSpawner: ShipSmokeSpawner,
+  };
+})();

+ 85 - 0
src/star-destroyer-fighter-controller.js

@@ -0,0 +1,85 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+import {math} from './math.js';
+
+
+export const star_destroyer_fighter_controller = (() => {
+
+
+  class StarDestroyerFighterController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.fighters_ = [];
+
+
+      const down = new THREE.Quaternion().setFromUnitVectors(
+          new THREE.Vector3(0, 0, -1), new THREE.Vector3(0, -1, 0));
+      const up = new THREE.Quaternion();
+
+      this.turretOffsets_ = [
+        [new THREE.Vector3(0.93 * 50, 0.65 * 50, 0), up],
+        [new THREE.Vector3(1.27 * 50, 0.62 * 50, 0), up],
+        [new THREE.Vector3(0.59 * 50, 0.68 * 50, 0), up],
+
+        [new THREE.Vector3(-5.02 * 50, -1.13 * 50, 4.39 * 50), down],
+        [new THREE.Vector3(-5.02 * 50, -1.13 * 50, -4.39 * 50), down],
+
+        [new THREE.Vector3(-0.44 * 50, -1.52 * 50, 1.05 * 50), down],
+        [new THREE.Vector3(-0.44 * 50, -1.52 * 50, -1.05 * 50), down],
+
+        [new THREE.Vector3(3.6 * 50, -1.39 * 50, 0.39 * 50), down],
+        [new THREE.Vector3(3.6 * 50, -1.39 * 50, -0.39 * 50), down],
+      ];
+    }
+
+    SpawnFighters_() {
+      const spawner = this.FindEntity('spawners').GetComponent('TieFighterSpawner');
+      for (let i = 0; i < 20; ++i) {
+        const n = new THREE.Vector3(
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+          math.rand_range(-1, 1),
+        );
+        n.normalize();
+        n.multiplyScalar(800);
+        n.add(this.Parent.Position);
+
+        const e = spawner.Spawn();
+        e.SetPosition(n);
+
+        this.fighters_.push(e);
+      }
+    }
+    
+    SpawnTurrets_() {
+      const turretSpawner = this.FindEntity('spawners').GetComponent(
+          'StarDestroyerTurretSpawner');
+
+      const correction = new THREE.Quaternion().setFromUnitVectors(
+          new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, -1));
+
+      for (let i = 0; i < this.turretOffsets_.length; ++i) {
+        const [pos, quat] = this.turretOffsets_[i];
+        const e = turretSpawner.Spawn(pos, quat, correction);
+        this.fighters_.push(e);
+      }
+  }
+
+    Update(_) {
+      if (this.fighters_.length > 0) {
+        return;
+      }
+
+      // DEMO
+      this.SpawnFighters_();
+      this.SpawnTurrets_();
+    }
+  };
+
+  return {
+    StarDestroyerFighterController: StarDestroyerFighterController,
+  };
+
+})();

+ 30 - 0
src/star-destroyer-turret.js

@@ -0,0 +1,30 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from "./entity.js";
+
+
+export const star_destroyer_turret = (() => {
+
+  class StarDestroyerTurret extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+
+    Destroy() {
+      this.particles_.Destroy();
+    }
+
+    InitEntity() {
+    }
+
+    Update(timeElapsed) {
+    }
+  }
+  
+  return {
+    ShipEffects: ShipEffects,
+  };
+})();

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

@@ -0,0 +1,61 @@
+import {THREE} from './three-defs.js';
+
+import {entity} from './entity.js';
+
+
+export const third_person_camera = (() => {
+  
+  class ThirdPersonCamera extends entity.Component {
+    constructor(params) {
+      super();
+
+      this.params_ = params;
+      this.camera_ = params.camera;
+
+      this.currentPosition_ = new THREE.Vector3();
+      this.SetPass(1);
+    }
+
+    _CalculateIdealOffset() {
+      const idealOffset = new THREE.Vector3(0, 10, 20);
+      const input = this.Parent.Attributes.InputCurrent;
+
+      if (input.axis1Side) {
+        idealOffset.lerp(
+            new THREE.Vector3(10 * input.axis1Side, 5, 20), Math.abs(input.axis1Side));
+      }
+      
+      if (input.axis1Forward < 0) {
+        idealOffset.lerp(
+          new THREE.Vector3(0, 0, 18 * -input.axis1Forward), Math.abs(input.axis1Forward));
+      }
+
+      if (input.axis1Forward > 0) {
+        idealOffset.lerp(
+          new THREE.Vector3(0, 5, 15 * input.axis1Forward), Math.abs(input.axis1Forward));
+      }
+
+      idealOffset.applyQuaternion(this.params_.target.Quaternion);
+      idealOffset.add(this.params_.target.Position);
+
+      return idealOffset;
+    }
+
+    Update(timeElapsed) {
+      const idealOffset = this._CalculateIdealOffset();
+
+      const t1 = 1.0 - Math.pow(0.05, timeElapsed);
+      const t2 = 1.0 - Math.pow(0.01, timeElapsed);
+
+      this.currentPosition_.lerp(idealOffset, t1);
+
+      this.camera_.position.copy(this.currentPosition_);
+      this.camera_.quaternion.slerp(this.params_.target.Quaternion, t2);
+    }
+  }
+
+  return {
+    ThirdPersonCamera: ThirdPersonCamera
+  };
+
+})();

+ 8 - 0
src/three-defs.js

@@ -0,0 +1,8 @@
+import * as THREE from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
+
+import {FBXLoader} from 'https://cdn.skypack.dev/[email protected]/examples/jsm/loaders/FBXLoader.js';
+import {GLTFLoader} from 'https://cdn.skypack.dev/[email protected]/examples/jsm/loaders/GLTFLoader.js';
+import {SkeletonUtils} from 'https://cdn.skypack.dev/[email protected]/examples/jsm/utils/SkeletonUtils.js';
+import {OrbitControls} from 'https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls.js';
+
+export {THREE, FBXLoader, GLTFLoader, SkeletonUtils, OrbitControls};

+ 409 - 0
src/threejs-component.js

@@ -0,0 +1,409 @@
+import {THREE, OrbitControls} from './three-defs.js';
+
+import {entity} from "./entity.js";
+
+
+export const threejs_component = (() => {
+
+    const _NOISE_GLSL = `
+  vec3 mod289(vec3 x)
+  {
+      return x - floor(x / 289.0) * 289.0;
+  }
+
+  vec4 mod289(vec4 x)
+  {
+      return x - floor(x / 289.0) * 289.0;
+  }
+
+  vec4 permute(vec4 x)
+  {
+      return mod289((x * 34.0 + 1.0) * x);
+  }
+
+  vec4 taylorInvSqrt(vec4 r)
+  {
+      return 1.79284291400159 - r * 0.85373472095314;
+  }
+
+  vec4 snoise(vec3 v)
+  {
+      const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
+
+      // First corner
+      vec3 i  = floor(v + dot(v, vec3(C.y)));
+      vec3 x0 = v   - i + dot(i, vec3(C.x));
+
+      // Other corners
+      vec3 g = step(x0.yzx, x0.xyz);
+      vec3 l = 1.0 - g;
+      vec3 i1 = min(g.xyz, l.zxy);
+      vec3 i2 = max(g.xyz, l.zxy);
+
+      vec3 x1 = x0 - i1 + C.x;
+      vec3 x2 = x0 - i2 + C.y;
+      vec3 x3 = x0 - 0.5;
+
+      // Permutations
+      i = mod289(i); // Avoid truncation effects in permutation
+      vec4 p =
+        permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0))
+                              + i.y + vec4(0.0, i1.y, i2.y, 1.0))
+                              + i.x + vec4(0.0, i1.x, i2.x, 1.0));
+
+      // Gradients: 7x7 points over a square, mapped onto an octahedron.
+      // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
+      vec4 j = p - 49.0 * floor(p / 49.0);  // mod(p,7*7)
+
+      vec4 x_ = floor(j / 7.0);
+      vec4 y_ = floor(j - 7.0 * x_); 
+
+      vec4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0;
+      vec4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0;
+
+      vec4 h = 1.0 - abs(x) - abs(y);
+
+      vec4 b0 = vec4(x.xy, y.xy);
+      vec4 b1 = vec4(x.zw, y.zw);
+
+      vec4 s0 = floor(b0) * 2.0 + 1.0;
+      vec4 s1 = floor(b1) * 2.0 + 1.0;
+      vec4 sh = -step(h, vec4(0.0));
+
+      vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
+      vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
+
+      vec3 g0 = vec3(a0.xy, h.x);
+      vec3 g1 = vec3(a0.zw, h.y);
+      vec3 g2 = vec3(a1.xy, h.z);
+      vec3 g3 = vec3(a1.zw, h.w);
+
+      // Normalize gradients
+      vec4 norm = taylorInvSqrt(vec4(dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3)));
+      g0 *= norm.x;
+      g1 *= norm.y;
+      g2 *= norm.z;
+      g3 *= norm.w;
+
+      // Compute noise and gradient at P
+      vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
+      vec4 m2 = m * m;
+      vec4 m3 = m2 * m;
+      vec4 m4 = m2 * m2;
+      vec3 grad =
+        -6.0 * m3.x * x0 * dot(x0, g0) + m4.x * g0 +
+        -6.0 * m3.y * x1 * dot(x1, g1) + m4.y * g1 +
+        -6.0 * m3.z * x2 * dot(x2, g2) + m4.z * g2 +
+        -6.0 * m3.w * x3 * dot(x3, g3) + m4.w * g3;
+      vec4 px = vec4(dot(x0, g0), dot(x1, g1), dot(x2, g2), dot(x3, g3));
+      return 42.0 * vec4(grad, dot(m4, px));
+  }
+
+  float FBM(vec3 p, int octaves) {
+    float w = length(fwidth(p));
+    float G = pow(2.0, -1.0);
+    float amplitude = 1.0;
+    float frequency = 1.0;
+    float lacunarity = 2.0;
+    float normalization = 0.0;
+    float total = 0.0;
+    
+    for (int i = 0; i < octaves; ++i) {
+      float noiseValue = snoise(p * frequency).w * smoothstep(1.0, 0.5, w);
+
+      // noiseValue = abs(noiseValue);
+      // noiseValue *= noiseValue;
+
+      total += noiseValue * amplitude;
+      normalization += amplitude;
+      amplitude *= G;
+      frequency *= lacunarity;
+    }
+
+    total = total * 0.5 + 0.5;
+
+    // total = pow(total, exponeniation);
+
+    return total;
+  }
+  `;
+
+
+  const _SKY_VS = `
+  varying vec3 vWorldPosition;
+  
+  void main() {
+    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
+    vWorldPosition = worldPosition.xyz;
+  
+    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+  }`;
+  
+  
+  const _SKY_FS = `
+  uniform samplerCube background;
+  
+  varying vec3 vWorldPosition;
+  
+  void main() {
+    vec3 viewDirection = normalize(vWorldPosition - cameraPosition);
+    vec3 stars = textureCube(background, viewDirection).xyz;
+  
+    gl_FragColor = vec4(stars, 1.0);
+  }`;
+
+  const _PLANET_VS = `
+  varying vec3 vWorldPosition;
+  varying vec3 vNormal;
+  
+  void main() {
+    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
+    vWorldPosition = worldPosition.xyz;
+  
+    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+    vNormal = normal;
+  }`;
+
+  const _PLANET_FS = _NOISE_GLSL + `
+
+  uniform float time;
+
+  varying vec3 vWorldPosition;
+  varying vec3 vNormal;
+  
+  float map(vec3 pos) {
+    return FBM(pos, 10);
+  }
+
+  vec3 calcNormal(vec3 pos) {
+    vec2 eps = vec2( 0.0005, 0.0 );
+    vec3 nor = vec3(map(pos+eps.xyy) - map(pos-eps.xyy),
+                    map(pos+eps.yxy) - map(pos-eps.yxy),
+                    map(pos+eps.yyx) - map(pos-eps.yyx));
+    return normalize(nor);
+  }
+
+  void main() {
+    vec3 viewDir = normalize(cameraPosition - vWorldPosition);
+    vec3 normal = normalize(vNormal);
+    vec3 noiseCoords = vec3(atan(normal.x, normal.z) * 0.5, asin(normal.y) * 2.0, time * 0.0025);
+
+    vec3 n1 = vec3(
+        FBM(noiseCoords + vec3(0.0), 8),
+        FBM(noiseCoords + vec3(1.2, 1.3, 0.0), 8), 0.0);
+    vec3 n2 = vec3(
+        FBM(noiseCoords + 2.0 * n1 + vec3(1.7, 9.2, 0.0), 8),
+        FBM(noiseCoords + 2.0 * n1 + vec3(8.3, 2.8, 0.0), 8), 0.0);
+
+    float n3 = FBM(noiseCoords + 2.0 * n2, 8);
+
+    vec3 col1 = vec3(0.24, 0.58, 0.67);
+    vec3 col2 = vec3(0.08, 0.25, 0.72);
+
+    vec3 col3 = vec3(1.0);
+    vec3 col4 = vec3(0.26, 0.68, 0.11);
+
+    vec3 albedo = vec3(0.0);
+    vec3 c1 = mix(col1, col2, smoothstep(0.0, 1.0, n3));
+    vec3 c2 = mix(col3, col4, smoothstep(0.0, 1.0, n3));
+    albedo = mix(c1, c2, smoothstep(0.75, 1.0, length(n2)));
+    albedo *= (0.1 * n3 + 0.9);
+
+    vec3 lightDir = vec3(0.0, 1.0, 0.0);
+    vec3 reflectionDir = reflect(viewDir, normal);
+
+    vec3 scatterColour = vec3(1.0, 0.25, 0.25);
+    vec3 extinctColour = vec3(0.35, .75, .90);
+
+    float wrap = 0.1;
+    float diffuse = max(0.0, ((dot(normal, lightDir)) + wrap) / (1.0 + wrap));
+    float spec = max(0.0, dot(-reflectionDir, lightDir));
+    float extinct = diffuse * pow(1.0 - max(0.0, dot(viewDir, normal)), 2.0);
+
+    vec3 colour = vec3(0.0);
+
+    vec3 scatterLight = mix(scatterColour, vec3(1.0), smoothstep(0.0, 0.4, diffuse)) * diffuse;
+    
+    colour = (scatterLight + 0.02) * albedo;
+    colour = mix(colour, extinctColour, extinct);
+
+    // vec3 norm = calcNormal(noiseCoords * 0.5);
+    // vec3 clouds = diffuse * max(0.0, dot(norm, lightDir)) * vec3(1.0);
+
+    // colour += clouds;
+
+    // Color += Sun*RGB(255,250,230);
+
+    gl_FragColor = vec4(colour, 1.0);
+
+    // vec3 normal = normalize(vNormal);
+    // float fbm = map(normal * 10.0);
+
+    // vec3 sandy = vec3(.73, .63, .43);
+    // vec3 vegetation = vec3(.06, .73, .19);
+    // vec3 diffuse = mix(sandy, vegetation, fbm);
+
+    // normal = normalize(normal + 0.5 * calcNormal(normal * 10.0));
+    // float lighting = dot(normal, vec3(0.0, 1.0, 0.0));
+    // gl_FragColor = vec4(diffuse * lighting, 1.0);
+  }`;
+
+  class ThreeJSController extends entity.Component {
+    constructor() {
+      super();
+    }
+
+    InitEntity() {
+      this.threejs_ = new THREE.WebGLRenderer({
+        antialias: true,
+      });
+      this.threejs_.outputEncoding = THREE.sRGBEncoding;
+      // this.threejs_.gammaFactor = 2.2;
+      this.threejs_.shadowMap.enabled = true;
+      this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
+      this.threejs_.setPixelRatio(window.devicePixelRatio);
+      this.threejs_.setSize(window.innerWidth, window.innerHeight);
+      this.threejs_.domElement.id = 'threejs';
+      this.threejs_.physicallyCorrectLights = true;
+  
+      document.getElementById('container').appendChild(this.threejs_.domElement);
+  
+      window.addEventListener('resize', () => {
+        this.OnResize_();
+      }, false);
+  
+      const fov = 60;
+      const aspect = 1920 / 1080;
+      const near = 1.0;
+      const far = 10000.0;
+      this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this.camera_.position.set(20, 5, 15);
+      this.scene_ = new THREE.Scene();
+
+      this.listener_ = new THREE.AudioListener();
+      this.camera_.add(this.listener_);
+
+      this.crawlCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this.crawlScene_ = new THREE.Scene();
+
+      this.uiCamera_ = new THREE.OrthographicCamera(
+          -1, 1, 1 * aspect, -1 * aspect, 1, 1000);
+      this.uiScene_ = new THREE.Scene();
+  
+      let light = new THREE.DirectionalLight(0x8088b3, 1.0);
+      light.position.set(-10, 500, 10);
+      light.target.position.set(0, 0, 0);
+      light.castShadow = true;
+      light.shadow.bias = -0.001;
+      light.shadow.mapSize.width = 4096;
+      light.shadow.mapSize.height = 4096;
+      light.shadow.camera.near = 1.0;
+      light.shadow.camera.far = 1000.0;
+      light.shadow.camera.left = 500;
+      light.shadow.camera.right = -500;
+      light.shadow.camera.top = 500;
+      light.shadow.camera.bottom = -500;
+      this.scene_.add(light);
+
+      this.sun_ = light;
+
+      light = new THREE.AmbientLight(0xFFFFFF, 0.035);
+      this.scene_.add(light);
+
+      this.LoadBackground_();
+      // this.LoadPlanet_();
+      this.OnResize_();
+    }
+
+    LoadBackground_() {
+      const loader = new THREE.CubeTextureLoader();
+      const texture = loader.load([
+          './resources/terrain/space-posx.jpg',
+          './resources/terrain/space-negx.jpg',
+          './resources/terrain/space-posy.jpg',
+          './resources/terrain/space-negy.jpg',
+          './resources/terrain/space-posz.jpg',
+          './resources/terrain/space-negz.jpg',
+      ]);
+      texture.encoding = THREE.sRGBEncoding;
+  
+      const uniforms = {
+        "background": { value: texture },
+      };
+  
+      const skyGeo = new THREE.SphereBufferGeometry(5000, 32, 15);
+      const skyMat = new THREE.ShaderMaterial({
+          uniforms: uniforms,
+          vertexShader: _SKY_VS,
+          fragmentShader: _SKY_FS,
+          side: THREE.BackSide
+      });
+
+      this.sky_ = new THREE.Mesh(skyGeo, skyMat);
+      this.scene_.add(this.sky_);
+    }
+
+    LoadPlanet_() {
+      const planetGeo = new THREE.SphereBufferGeometry(5000, 48, 48);
+      const planetMat = new THREE.ShaderMaterial({
+          uniforms: {
+            'time': { value: 0.0 },
+          },
+          vertexShader: _PLANET_VS,
+          fragmentShader: _PLANET_FS,
+          side: THREE.FrontSide
+      });
+
+      const planet = new THREE.Mesh(planetGeo, planetMat);
+      planet.position.set(6000, -1000, 0);
+      this.planet_ = planet;
+      this.sky_.add(planet);
+    }
+
+    OnResize_() {
+      this.camera_.aspect = window.innerWidth / window.innerHeight;
+      this.camera_.updateProjectionMatrix();
+      this.crawlCamera_.aspect = window.innerWidth / window.innerHeight;
+      this.crawlCamera_.updateProjectionMatrix();
+
+      this.uiCamera_.left = -this.camera_.aspect;
+      this.uiCamera_.right = this.camera_.aspect;
+      this.uiCamera_.updateProjectionMatrix();
+
+      this.threejs_.setSize(window.innerWidth, window.innerHeight);
+    }
+
+    Render() {
+      this.threejs_.autoClearColor = true;
+      this.threejs_.render(this.scene_, this.camera_);
+      this.threejs_.autoClearColor = false;
+      this.threejs_.render(this.crawlScene_, this.crawlCamera_);
+      this.threejs_.autoClearColor = false;
+      this.threejs_.render(this.uiScene_, this.uiCamera_);
+    }
+
+    Update(timeElapsed) {
+      const player = this.FindEntity('player');
+      if (!player) {
+        return;
+      }
+      const pos = player._position;
+  
+      this.sun_.position.copy(pos);
+      this.sun_.position.add(new THREE.Vector3(-10, 500, 10));
+      this.sun_.target.position.copy(pos);
+      this.sun_.updateMatrixWorld();
+      this.sun_.target.updateMatrixWorld();
+
+      if (this.planet_) {
+        this.planet_.material.uniforms.time.value += timeElapsed;
+      }
+
+      this.sky_.position.copy(pos);
+    }
+  }
+
+  return {
+      ThreeJSController: ThreeJSController,
+  };
+})();

+ 203 - 0
src/tie-fighter-controller.js

@@ -0,0 +1,203 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from './entity.js';
+
+
+export const tie_fighter_controller = (() => {
+
+  class FlashFXEmitter extends particle_system.ParticleEmitter {
+    constructor(offset, parent) {
+      super();
+      this.offset_ = offset;
+      this.parent_ = parent;
+      this.blend_ = 0.0;
+      this.light_ = null;
+      this.life_ = 1.0;
+      this.maxLife_ = 1.0;
+    }
+
+    OnDestroy() {
+      this.light_.parent.remove(this.light_);
+    }
+
+    OnUpdate_(timeElapsed) {
+      if (!this.light_) {
+        return;
+      }
+      this.life_ = Math.max(0.0, this.life_ - timeElapsed);
+      this.light_.intensity = 20.0 * (this.life_ / this.maxLife_);
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const origin = this.offset_.clone();
+
+      const life = 0.2;
+      const p = origin;
+
+      const d = new THREE.Vector3(0, 0, 0);
+
+      // DEMO
+      this.light_ = new THREE.PointLight(0xFF8080, 20.0, 20.0, 2.0);
+      this.light_.position.copy(origin);
+      this.life_ = life;
+      this.maxLife_ = life;
+      this.parent_.add(this.light_);
+
+      return {
+          position: p,
+          size: 2.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+  class TieFighterController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.cooldownTimer_ = 0.0;
+      this.cooldownRate_ = 0.1;
+      this.powerLevel_ = 0.0;
+
+      const x = 0.6 * 4;
+      const y1 = 0.0  * 4;
+      const z = 0.8 * 4;
+      this.offsets_ = [
+          new THREE.Vector3(-x, y1, -z),
+          new THREE.Vector3(x, y1, -z),
+      ];
+      this.shots_ = [];
+      this.offsetIndex_ = 0;
+    }
+
+    Destroy() {
+      this.blasterFX_.Destroy();
+      this.blasterFX_ = null;
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('player.fire', (m) => this.OnFire_(m));
+    }
+
+    InitEntity() {
+      // this.fireFX_ = new particle_system.ParticleSystem({
+      //     camera: params.camera,
+      //     parent: params.scene,
+      //     texture: './resources/textures/fire.png',
+      // });
+
+      const group = this.GetComponent('RenderComponent').group_;
+      this.blasterFX_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: group,
+          texture: './resources/textures/fx/blaster.jpg',
+      });
+    }
+
+    SetupFlashFX_(index) {
+      const group = this.GetComponent('RenderComponent').group_;
+      const emitter = new FlashFXEmitter(this.offsets_[index], group);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.5, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x80FF80));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x6AA84F));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.25, 2.0);
+      emitter.sizeSpline_.AddPoint(1.0, 0.25);
+      emitter.SetEmissionRate(0);
+      emitter.blend_ = 0.0;  
+      this.blasterFX_.AddEmitter(emitter);
+      emitter.AddParticles(1);
+    }
+
+    OnFire_() {
+      if (this.cooldownTimer_ > 0.0) {
+        return;
+      }
+
+      if (this.powerLevel_ < 0.4) {
+        return;
+      }
+
+      this.powerLevel_ = Math.max(this.powerLevel_ - 0.4, 0.0);
+
+      this.cooldownTimer_ = this.cooldownRate_;
+      this.offsetIndex_ = (this.offsetIndex_ + 1) % 2;
+
+      const fx = this.FindEntity('fx').GetComponent('BlasterSystem');
+      const p1 = fx.CreateParticle();
+      p1.Start = this.offsets_[this.offsetIndex_].clone();
+      p1.Start.applyQuaternion(this.Parent.Quaternion);
+      p1.Start.add(this.Parent.Position);
+      p1.End = p1.Start.clone();
+      p1.Velocity = this.Parent.Forward.clone().multiplyScalar(2000.0);
+      p1.Length = 50.0;
+      p1.Colours = [
+          new THREE.Color(0.5, 4.0, 0.5), new THREE.Color(0.0, 0.0, 0.0)];
+      p1.Life = 5.0;
+      p1.TotalLife = 5.0;
+      p1.Width = 2.5;
+
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.LoadSound('./resources/sounds/', 'laser.ogg', (s) => {
+        const group = this.GetComponent('RenderComponent').group_;
+        group.add(s);
+        s.play();  
+      });
+      
+      this.shots_.push(p1);
+      this.SetupFlashFX_(this.offsetIndex_);
+    }
+
+    UpdateShots_() {
+      this.shots_ = this.shots_.filter(p => {
+        return p.Life > 0.0;
+      });
+
+      const physics = this.FindEntity('physics').GetComponent('AmmoJSController');
+      for (let s of this.shots_) {
+        const hits = physics.RayTest(s.Start, s.End);
+        for (let h of hits) {
+          if (h.name == this.Parent.Name) {
+            continue;
+          }
+          const e = this.FindEntity(h.name);
+          e.Broadcast({topic: 'player.hit', value: this.params_.blasterStrength});
+          s.Life = 0.0;
+
+          const explosion = this.FindEntity('spawners').GetComponent('TinyExplosionSpawner')
+          explosion.Spawn(h.position);    
+        }
+      }
+    }
+
+    Update(timeElapsed) {
+      this.cooldownTimer_ = Math.max(this.cooldownTimer_ - timeElapsed, 0.0);
+      this.powerLevel_ = Math.min(this.powerLevel_ + timeElapsed, 4.0);
+
+      this.UpdateShots_();
+      this.blasterFX_.Update(timeElapsed);
+    }
+  };
+
+  return {
+    TieFighterController: TieFighterController,
+  };
+})();

+ 266 - 0
src/turret-controller.js

@@ -0,0 +1,266 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from './entity.js';
+
+
+export const turret_controller = (() => {
+
+  class FlashFXEmitter extends particle_system.ParticleEmitter {
+    constructor(offset, parent) {
+      super();
+      this.offset_ = offset;
+      this.parent_ = parent;
+      this.blend_ = 0.0;
+      this.light_ = null;
+      this.life_ = 1.0;
+      this.maxLife_ = 1.0;
+    }
+
+    OnDestroy() {
+      this.light_.parent.remove(this.light_);
+    }
+
+    OnUpdate_(timeElapsed) {
+      if (!this.light_) {
+        return;
+      }
+      this.life_ = Math.max(0.0, this.life_ - timeElapsed);
+      this.light_.intensity = 20.0 * (this.life_ / this.maxLife_);
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const origin = this.offset_.clone();
+
+      const life = 0.2;
+      const p = origin;
+
+      const d = new THREE.Vector3(0, 0, 0);
+
+      this.light_ = new THREE.PointLight(0x80FF80, 1.0, 50.0, 2.0);
+      this.light_.position.copy(origin);
+      this.life_ = life;
+      this.maxLife_ = life;
+      this.parent_.add(this.light_);
+
+      return {
+          position: p,
+          size: 2.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+  const _MAX_POWER = 4.0;
+  const _POWER_PER_SHOT = 2.0;
+  const _MAX_TARGET_DISTANCE = 500;
+  const _MAX_ANGLE = 0.707;
+
+  const _TMP_M0 = new THREE.Matrix4();
+  const _TMP_Q0 = new THREE.Quaternion();
+
+  class TurretController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.cooldownTimer_ = 0.0;
+      this.cooldownRate_ = 0.2;
+      this.powerLevel_ = _MAX_POWER;
+
+      this.offset_ = new THREE.Vector3(0, 0, -18),
+      this.shots_ = [];
+      this.target_ = null;
+    }
+
+    Destroy() {
+      this.blasterFX_.Destroy();
+      this.blasterFX_ = null;
+    }
+
+    InitEntity() {
+      const group = this.GetComponent('RenderComponent').group_;
+      this.blasterFX_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: group,
+          texture: './resources/textures/fx/blaster.jpg',
+      });
+      this.up_ = this.Parent.Quaternion.clone();
+
+      // DEMO
+      // const _TMP_M1 = new THREE.Matrix4();
+
+      // _TMP_M0.makeRotationFromQuaternion(this.up_);
+      // _TMP_M0.multiply(_TMP_M1.makeRotationX((Math.random() * 2 - 1) * 0.25));
+      // _TMP_M0.multiply(_TMP_M1.makeRotationY((Math.random() * 2 - 1) * 0.25));
+      // _TMP_M0.multiply(_TMP_M1.makeRotationZ((Math.random() * 2 - 1) * 0.25));
+      
+      // _TMP_Q0.setFromRotationMatrix(_TMP_M0);
+
+      // this.Parent.SetQuaternion(_TMP_Q0);
+    }
+
+    SetupFlashFX_() {
+      const group = this.GetComponent('RenderComponent').group_;
+      const emitter = new FlashFXEmitter(this.offset_, group);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.5, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x80FF80));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x6AA84F));
+
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.25, 5.0);
+      emitter.sizeSpline_.AddPoint(1.0, 0.25);
+      emitter.SetEmissionRate(0);
+      emitter.blend_ = 0.0;  
+      this.blasterFX_.AddEmitter(emitter);
+      emitter.AddParticles(1);
+    }
+
+    Fire_() {
+      if (this.cooldownTimer_ > 0.0) {
+        return;
+      }
+
+      if (this.powerLevel_ < _POWER_PER_SHOT) {
+        return;
+      }
+
+      this.powerLevel_ = Math.max(this.powerLevel_ - _POWER_PER_SHOT, 0.0);
+
+      this.cooldownTimer_ = this.cooldownRate_;
+
+      const fx = this.FindEntity('fx').GetComponent('BlasterSystem');
+      const p1 = fx.CreateParticle();
+      p1.Start = this.offset_.clone();
+      p1.Start.applyQuaternion(this.Parent.Quaternion);
+      p1.Start.add(this.Parent.Position);
+      p1.End = p1.Start.clone();
+      p1.Velocity = this.Parent.Forward.clone().multiplyScalar(2000.0);
+      p1.Length = 50.0;
+      p1.Colours = [
+          new THREE.Color(0.5, 4.0, 0.5), new THREE.Color(0.0, 0.0, 0.0)];
+      p1.Life = 2.0;
+      p1.TotalLife = 2.0;
+      p1.Width = 1.5;
+
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.LoadSound('./resources/sounds/', 'laser.ogg', (s) => {
+        const group = this.GetComponent('RenderComponent').group_;
+        group.add(s);
+        s.play();  
+      });
+
+      this.shots_.push(p1);
+      this.SetupFlashFX_();
+    }
+
+    UpdateShots_() {
+      this.shots_ = this.shots_.filter(p => {
+        return p.Life > 0.0;
+      });
+
+      const physics = this.FindEntity('physics').GetComponent('AmmoJSController');
+      for (let s of this.shots_) {
+        const hits = physics.RayTest(s.Start, s.End);
+        for (let h of hits) {
+          if (h.name == this.Parent.Name) {
+            continue;
+          }
+          const e = this.FindEntity(h.name);
+          e.Broadcast({topic: 'player.hit', value: this.params_.blasterStrength});
+          s.Life = 0.0;
+
+          const explosion = this.FindEntity('spawners').GetComponent('TinyExplosionSpawner')
+          explosion.Spawn(h.position);
+        }
+      }
+    }
+
+    AcquireTarget_() {
+      const pos = this.Parent.Position;
+      const colliders = this.params_.grid.FindNear(
+          [pos.x, pos.z], [1000, 1000]).filter(
+          c => c.entity.Attributes.team == 'allies'
+      );
+
+      if (colliders.length == 0) {
+        return;
+      }
+
+      this.target_ = colliders[0].entity;
+    }
+
+    TrackTargets_(timeElapsed) {
+      if (!this.target_) {
+        this.AcquireTarget_();
+        return;
+      }
+
+      if (this.target_.Position.distanceTo(this.Parent.Position) > _MAX_TARGET_DISTANCE) {
+        this.target_ = null;
+        return;
+      }
+
+      if (this.target_.IsDead) {
+        this.target_ = null;
+        return;
+      }
+
+
+      _TMP_M0.lookAt(
+        this.Parent.Position, this.target_.Position, THREE.Object3D.DefaultUp);
+      _TMP_Q0.setFromRotationMatrix(_TMP_M0);
+
+      const angle = _TMP_Q0.dot(this.up_);
+      if (Math.abs(angle) < _MAX_ANGLE) {
+        this.target_ = null;
+        return;
+      }
+
+      const t = 1.0 - Math.pow(0.25, timeElapsed);
+      this.Parent.Quaternion.slerp(_TMP_Q0, t);
+      this.Parent.SetQuaternion(this.Parent.Quaternion);
+      
+      if (this.powerLevel_ >= _POWER_PER_SHOT) {
+        // Meh
+        if (Math.random() < 0.1) {
+          this.Fire_();
+        }
+      }
+    }
+
+    Update(timeElapsed) {     
+      // DEMO
+      // if (Math.random() < 0.005) {
+      //   this.Fire_();
+      // } 
+      this.cooldownTimer_ = Math.max(this.cooldownTimer_ - timeElapsed, 0.0);
+      this.powerLevel_ = Math.min(
+          this.powerLevel_ + timeElapsed, _MAX_POWER);
+
+      this.TrackTargets_(timeElapsed);
+      this.UpdateShots_();
+
+      this.blasterFX_.Update(timeElapsed);
+    }
+  };
+
+  return {
+    TurretController: TurretController,
+  };
+})();

+ 77 - 0
src/ui-controller.js

@@ -0,0 +1,77 @@
+import {entity} from './entity.js';
+
+
+export const ui_controller = (() => {
+
+  const _PHRASES = [
+    [' ', 5],
+    ['SimonDev: All wings report in.', 10],
+    ['O-Boy: I am a leaf on the wind...', 10],
+    ['O-Boy: Watch how I fly.', 10],
+    ["SimonDev: Hey uhhh don't forget to subscribe.", 10],
+    ["Jinjie: I'm coming in hot!", 10],
+    ["SimonDev: Please subscribe, I need subscribers.", 10],
+    ["Zozo: Moya... come in.", 10],
+    ["Zozo: It's a ship, a LIVING ship.", 10],
+    ["SimonDev: Also, contribute to my Patreon.", 10],
+    ['Kai: I am Kai, last of the Brunnen-G.', 10],
+    ["SimonDev: Really need a steady supply of coffee and beer. And groceries.", 10],
+    ['Kai: Today is my day of death. The day our story begins.', 10],
+    ["SimonDev: Yeah so, Patreon and subscribe.", 10],
+    ['Kai: The dead do not contribute to Patreon.', 10],
+    ['SimonDev: Shutup Meg.', 10],
+  ];
+
+  class UIController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.timeout_ = 0.0;
+      this.textArea_ = document.getElementById('chat-ui-text-area');
+      this.text_ = null;
+    }
+
+    AddText(txt) {
+      if (this.text_) {
+        this.text_ = null;
+      }
+
+      this.text_ = document.createElement('DIV');
+      this.text_.className = 'chat-text';
+      this.text_.innerText = txt;
+      this.text_.classList.toggle('fadeOut');
+
+      this.textArea_.appendChild(this.text_);
+
+      const dead = [];
+      for (let i = 0; i < this.textArea_.children.length; ++i) {
+        const s = window.getComputedStyle(this.textArea_.children[i]);
+        if (s.visibility == 'hidden') {
+          dead.push(this.textArea_.children[i]);
+        }
+      }
+      for (let d of dead) {
+        this.textArea_.removeChild(d);
+      }
+    }
+
+    Update(timeElapsed) {
+      if (_PHRASES.length == 0) {
+        return;
+      }
+
+      this.timeout_ -= timeElapsed;
+      if (this.timeout_ < 0) {
+        const [phrase, timeout] = _PHRASES.shift();
+        this.timeout_ = timeout;
+        this.AddText(phrase);
+        return;
+      }
+    }
+  };
+
+  return {
+    UIController: UIController,
+  };
+
+})();

+ 212 - 0
src/xwing-controller.js

@@ -0,0 +1,212 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from './entity.js';
+
+
+export const xwing_controller = (() => {
+
+  class FlashFXEmitter extends particle_system.ParticleEmitter {
+    constructor(offset, parent) {
+      super();
+      this.offset_ = offset;
+      this.parent_ = parent;
+      this.blend_ = 0.0;
+      this.light_ = null;
+      this.life_ = 1.0;
+      this.maxLife_ = 1.0;
+    }
+
+    OnDestroy() {
+      this.light_.parent.remove(this.light_);
+    }
+
+    OnUpdate_(timeElapsed) {
+      if (!this.light_) {
+        return;
+      }
+      this.life_ = Math.max(0.0, this.life_ - timeElapsed);
+      this.light_.intensity = 20.0 * (this.life_ / this.maxLife_);
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const origin = this.offset_.clone();
+
+      const life = 0.2;
+      const p = origin;
+
+      const d = new THREE.Vector3(0, 0, 0);
+
+      // DEMO
+      this.light_ = new THREE.PointLight(0xFF8080, 20.0, 20.0, 2.0);
+      this.light_.position.copy(origin);
+      this.life_ = life;
+      this.maxLife_ = life;
+      this.parent_.add(this.light_);
+
+      return {
+          position: p,
+          size: 2.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+  class XWingController extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+      this.cooldownTimer_ = 0.0;
+      this.cooldownRate_ = 0.075;
+      this.powerLevel_ = 0.0;
+
+      const x = 2.35 * 4;
+      const y1 = 1.95 * 4;
+      const y2 = -0.5 * 4;
+      const z = 0.65 * 4;
+      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),
+      ];
+      for (let i = 0; i < this.offsets_.length; ++i) {
+        this.offsets_[i].add(this.params_.offset);
+      }
+      this.offsetIndex_ = 0;
+      this.shots_ = [];
+    }
+
+    Destroy() {
+      this.blasterFX_.Destroy();
+      this.blasterFX_ = null;
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('player.fire', (m) => this.OnFire_(m));
+    }
+
+    InitEntity() {
+      const group = this.GetComponent('RenderComponent').group_;
+      this.blasterFX_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: group,
+          texture: './resources/textures/fx/blaster.jpg',
+      });
+
+      this.spotlight_ = new THREE.SpotLight(
+          0xFFFFFF, 5.0, 200, Math.PI / 2, 0.5);
+      this.spotlight_.position.set(0, 0, -5);
+      this.spotlight_.target.position.set(0, 0, -6);
+
+      group.add(this.spotlight_);
+      group.add(this.spotlight_.target);
+    }
+
+    SetupFlashFX_(index) {
+      const group = this.GetComponent('RenderComponent').group_;
+      const emitter = new FlashFXEmitter(this.offsets_[index], group);
+      emitter.alphaSpline_.AddPoint(0.0, 0.0);
+      emitter.alphaSpline_.AddPoint(0.5, 1.0);
+      emitter.alphaSpline_.AddPoint(1.0, 0.0);
+      
+      emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0xFF4040));
+      emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0xA86A4F));
+      
+      emitter.sizeSpline_.AddPoint(0.0, 0.5);
+      emitter.sizeSpline_.AddPoint(0.25, 2.0);
+      emitter.sizeSpline_.AddPoint(1.0, 0.25);
+      emitter.SetEmissionRate(0);
+      emitter.blend_ = 0.0;  
+      this.blasterFX_.AddEmitter(emitter);
+      emitter.AddParticles(1);
+    }
+
+    OnFire_() {
+      if (this.cooldownTimer_ > 0.0) {
+        return;
+      }
+
+      if (this.powerLevel_ < 0.2) {
+        return;
+      }
+
+      this.powerLevel_ = Math.max(this.powerLevel_ - 0.2, 0.0);
+
+      this.cooldownTimer_ = this.cooldownRate_;
+      this.offsetIndex_ = (this.offsetIndex_ + 1) % this.offsets_.length;
+
+      const fx = this.FindEntity('fx').GetComponent('BlasterSystem');
+      const p1 = fx.CreateParticle();
+      p1.Start = this.offsets_[this.offsetIndex_].clone();
+      p1.Start.applyQuaternion(this.Parent.Quaternion);
+      p1.Start.add(this.Parent.Position);
+      p1.End = p1.Start.clone();
+      p1.Velocity = this.Parent.Forward.clone().multiplyScalar(2000.0);
+      p1.Length = 50.0;
+      p1.Colours = [
+          new THREE.Color(4.0, 0.5, 0.5), new THREE.Color(0.0, 0.0, 0.0)];
+      p1.Life = 5.0;
+      p1.TotalLife = 5.0;
+      p1.Width = 2.5;
+
+      const loader = this.FindEntity('loader').GetComponent('LoadController');
+      loader.LoadSound('./resources/sounds/', 'laser.ogg', (s) => {
+        const group = this.GetComponent('RenderComponent').group_;
+        group.add(s);
+        s.play();  
+      });
+      
+      this.shots_.push(p1);
+      this.SetupFlashFX_(this.offsetIndex_);
+    }
+
+    UpdateShots_() {
+      this.shots_ = this.shots_.filter(p => {
+        return p.Life > 0.0;
+      });
+
+      const physics = this.FindEntity('physics').GetComponent('AmmoJSController');
+      for (let s of this.shots_) {
+        const hits = physics.RayTest(s.Start, s.End);
+        for (let h of hits) {
+          if (h.name == this.Parent.Name) {
+            continue;
+          }
+          const e = this.FindEntity(h.name);
+          e.Broadcast({topic: 'player.hit', value: this.params_.blasterStrength});
+          s.Life = 0.0;
+
+          const explosion = this.FindEntity('spawners').GetComponent('TinyExplosionSpawner')
+          explosion.Spawn(h.position);    
+        }
+      }
+    }
+
+    Update(timeElapsed) {
+      this.cooldownTimer_ = Math.max(this.cooldownTimer_ - timeElapsed, 0.0);
+      this.powerLevel_ = Math.min(this.powerLevel_ + timeElapsed, 4.0);
+
+      this.blasterFX_.Update(timeElapsed);
+
+      this.UpdateShots_();
+    }
+  };
+
+  return {
+    XWingController: XWingController,
+  };
+})();

+ 121 - 0
src/xwing-effects.js

@@ -0,0 +1,121 @@
+import {THREE} from './three-defs.js';
+
+import {particle_system} from "./particle-system.js";
+import {entity} from "./entity.js";
+
+export const xwing_effect = (() => {
+
+  class FireFXEmitter extends particle_system.ParticleEmitter {
+    constructor(offset, parent) {
+      super();
+      this.offset_ = offset;
+      this.parent_ = parent;
+      this.blend_ = 0.0;
+    }
+
+    OnUpdate_() {
+    }
+
+    AddParticles(num) {
+      for (let i = 0; i < num; ++i) {
+        this.particles_.push(this.CreateParticle_());
+      }
+    }
+
+    CreateParticle_() {
+      const origin = this.offset_.clone();
+
+      const life = (Math.random() * 0.85 + 0.15) * 0.2;
+      const p = origin;
+
+      const d = new THREE.Vector3(0, 0, 10);
+
+      return {
+          position: p,
+          size: (Math.random() * 0.5 + 0.5) * 1.0,
+          colour: new THREE.Color(),
+          alpha: 1.0,
+          life: life,
+          maxLife: life,
+          rotation: Math.random() * 2.0 * Math.PI,
+          velocity: d,
+          blend: this.blend_,
+          drag: 1.0,
+      };
+    }
+  };
+
+
+  class XWingEffects extends entity.Component {
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    InitEntity() {
+      // this.fireFX_ = new particle_system.ParticleSystem({
+      //     camera: params.camera,
+      //     parent: params.scene,
+      //     texture: './resources/textures/fire.png',
+      // });
+
+      const group = this.GetComponent('RenderComponent').group_;
+      this.blasterFX_ = new particle_system.ParticleSystem({
+          camera: this.params_.camera,
+          parent: group,
+          texture: './resources/textures/fx/fire.png',
+      });
+
+      const x = 0.8 * 4;
+      const y1 = 1.65 * 4;
+      const y2 = -0.75 * 4;
+      const z = -2.7 * 4;
+      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),
+      ];
+      for (let i = 0; i < this.offsets_.length; ++i) {
+        this.offsets_[i].add(this.params_.offset);
+      }
+      this.offsetIndex_ = 0;
+
+      this.SetupFireFX_();
+    }
+
+    Destroy() {
+      this.blasterFX_.Destroy();
+      this.blasterFX_ = null;
+    }
+
+    SetupFireFX_() {
+      for (let i = 0; i < 4; ++i) {
+        const emitter = new FireFXEmitter(this.offsets_[i], this.Parent);
+        emitter.alphaSpline_.AddPoint(0.0, 0.0);
+        emitter.alphaSpline_.AddPoint(0.7, 1.0);
+        emitter.alphaSpline_.AddPoint(1.0, 0.0);
+        
+        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0xbb2909));
+        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x701a08));
+        
+        emitter.sizeSpline_.AddPoint(0.0, 0.5);
+        emitter.sizeSpline_.AddPoint(0.25, 2.0);
+        emitter.sizeSpline_.AddPoint(0.75, 0.5);
+        emitter.sizeSpline_.AddPoint(1.0, 0.25);
+        emitter.SetEmissionRate(500);
+        emitter.blend_ = 0.0;  
+        this.blasterFX_.AddEmitter(emitter);
+        emitter.AddParticles(10);
+      }
+    }
+
+    Update(timeElapsed) {
+      this.blasterFX_.Update(timeElapsed);
+    }
+  }
+  
+  return {
+    XWingEffects: XWingEffects,
+  };
+})();