Bläddra i källkod

First commit.

Simon 1 år sedan
förälder
incheckning
caf2af47f4
66 ändrade filer med 7913 tillägg och 1 borttagningar
  1. 2 0
      .gitignore
  2. 15 1
      README.md
  3. 17 0
      base.css
  4. 16 0
      index.html
  5. 2292 0
      package-lock.json
  6. 25 0
      package.json
  7. 1 0
      resources/characters/README.txt
  8. BIN
      resources/characters/guard.glb
  9. BIN
      resources/characters/paladin.glb
  10. BIN
      resources/characters/ybot.glb
  11. 1 0
      resources/models/README.txt
  12. BIN
      resources/models/mountain.glb
  13. 67 0
      resources/shaders/bugs-lighting-model-fsh.glsl
  14. 116 0
      resources/shaders/bugs-lighting-model-vsh.glsl
  15. 105 0
      resources/shaders/common.glsl
  16. 143 0
      resources/shaders/grass-lighting-model-fsh.glsl
  17. 232 0
      resources/shaders/grass-lighting-model-vsh.glsl
  18. 17 0
      resources/shaders/header.glsl
  19. 127 0
      resources/shaders/lighting-model-fsh.glsl
  20. 57 0
      resources/shaders/lighting-model-vsh.glsl
  21. 312 0
      resources/shaders/noise.glsl
  22. 44 0
      resources/shaders/oklab.glsl
  23. 109 0
      resources/shaders/phong-lighting-model-fsh.glsl
  24. 44 0
      resources/shaders/phong-lighting-model-vsh.glsl
  25. 14 0
      resources/shaders/sky-lighting-model-fsh.glsl
  26. 15 0
      resources/shaders/sky-lighting-model-vsh.glsl
  27. 54 0
      resources/shaders/sky.glsl
  28. 149 0
      resources/shaders/terrain-lighting-model-fsh.glsl
  29. 84 0
      resources/shaders/terrain-lighting-model-vsh.glsl
  30. 162 0
      resources/shaders/water-lighting-model-fsh.glsl
  31. 46 0
      resources/shaders/water-lighting-model-vsh.glsl
  32. 26 0
      resources/shaders/water-texture-fsh.glsl
  33. 12 0
      resources/shaders/water-texture-vsh.glsl
  34. 21 0
      resources/shaders/wind-lighting-model-fsh.glsl
  35. 81 0
      resources/shaders/wind-lighting-model-vsh.glsl
  36. BIN
      resources/textures/butterfly.png
  37. BIN
      resources/textures/dust.png
  38. BIN
      resources/textures/grass.png
  39. BIN
      resources/textures/grid.png
  40. BIN
      resources/textures/moth.png
  41. BIN
      resources/textures/terrain.png
  42. BIN
      resources/textures/whitesquare.png
  43. 63 0
      src/base/entity-manager.js
  44. 352 0
      src/base/entity.js
  45. 260 0
      src/base/load-controller.js
  46. 99 0
      src/base/math.js
  47. 9 0
      src/base/passes.js
  48. 180 0
      src/base/render-component.js
  49. 8 0
      src/base/render-order.js
  50. 175 0
      src/base/render/bugs-component.js
  51. 280 0
      src/base/render/grass-component.js
  52. 49 0
      src/base/render/light-component.js
  53. 245 0
      src/base/render/terrain-component.js
  54. 99 0
      src/base/render/water-component.js
  55. 162 0
      src/base/render/wind-component.js
  56. 35 0
      src/base/three-defs.js
  57. 555 0
      src/base/threejs-component.js
  58. 99 0
      src/demo-builder.js
  59. 99 0
      src/game/player-entity.js
  60. 141 0
      src/game/player-input.js
  61. 50 0
      src/game/render/render-sky-component.js
  62. 284 0
      src/game/render/shaders.js
  63. 65 0
      src/game/spawners.js
  64. 79 0
      src/game/third-person-camera.js
  65. 97 0
      src/main.js
  66. 22 0
      vite.config.js

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*node_modules/*
+*dist/*

+ 15 - 1
README.md

@@ -1 +1,15 @@
-# Quick_Grass
+# Quick_Grass
+
+Hi there, this is a cleaned up version of the source code for my video [How do Major Video Games Render Grass?](https://youtu.be/bp7REZBV4P4)
+
+If you're not already a subscriber, check out my channel: [SimonDev](https://www.youtube.com/channel/UCEwhtpXrg5MmwlH04ANpL8A)
+
+It's an implementation based on the GDC presentation for [Ghost of Tsushima's grass](https://www.gdcvault.com/play/1027033/Advanced-Graphics-Summit-Procedural-Grass)
+
+
+If you'd like to help choose the next video, consider support me on [Patreon](https://www.patreon.com/simondevyt)
+
+
+Lastly, this is released under the MIT license, so do whatever you want. If you do happen to use it in a project, I'd appreciate a shout-out or support, although you're under no obligation to do so.
+
+Cheers

+ 17 - 0
base.css

@@ -0,0 +1,17 @@
+@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500;900&display=swap');
+
+body {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  background: #000000;
+  margin: 0;
+  padding: 0;
+  overscroll-behavior: none;
+}
+
+.container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}

+ 16 - 0
index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>SimonDev Grass Thing</title>
+  <link rel="stylesheet" type="text/css" href="base.css">
+  <link rel="shortcut icon" href="#">
+  <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+</head>
+<body>
+  <script src="https://cdn.jsdelivr.net/gh/kripken/ammo.js@HEAD/builds/ammo.js"></script>
+  <script src="./src/main.js" type="module">
+  </script>
+  <div class="container" id="container">
+  </div>
+</body>
+</html>

+ 2292 - 0
package-lock.json

@@ -0,0 +1,2292 @@
+{
+  "name": "package",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "package",
+      "version": "0.0.0",
+      "dependencies": {
+        "@dimforge/rapier3d": "^0.11.2",
+        "mersenne-twister": "^1.1.0",
+        "n8ao": "^1.6.8",
+        "stats.js": "^0.17.0",
+        "three": "^0.156.0",
+        "vite-plugin-solid": "^2.7.0",
+        "vite-plugin-top-level-await": "^1.3.0",
+        "vite-plugin-wasm": "^3.2.2"
+      },
+      "devDependencies": {
+        "gltf-pipeline": "^4.1.0",
+        "vite": "^4.4.9"
+      }
+    },
+    "node_modules/@ampproject/remapping": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
+      "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.0",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.22.13",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+      "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+      "dependencies": {
+        "@babel/highlight": "^7.22.13",
+        "chalk": "^2.4.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.22.9",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz",
+      "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.22.17",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.17.tgz",
+      "integrity": "sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==",
+      "dependencies": {
+        "@ampproject/remapping": "^2.2.0",
+        "@babel/code-frame": "^7.22.13",
+        "@babel/generator": "^7.22.15",
+        "@babel/helper-compilation-targets": "^7.22.15",
+        "@babel/helper-module-transforms": "^7.22.17",
+        "@babel/helpers": "^7.22.15",
+        "@babel/parser": "^7.22.16",
+        "@babel/template": "^7.22.15",
+        "@babel/traverse": "^7.22.17",
+        "@babel/types": "^7.22.17",
+        "convert-source-map": "^1.7.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
+      "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
+      "dependencies": {
+        "@babel/types": "^7.22.15",
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jsesc": "^2.5.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
+      "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
+      "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+      "dependencies": {
+        "@babel/compat-data": "^7.22.9",
+        "@babel/helper-validator-option": "^7.22.15",
+        "browserslist": "^4.21.9",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-create-class-features-plugin": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
+      "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.22.5",
+        "@babel/helper-environment-visitor": "^7.22.5",
+        "@babel/helper-function-name": "^7.22.5",
+        "@babel/helper-member-expression-to-functions": "^7.22.15",
+        "@babel/helper-optimise-call-expression": "^7.22.5",
+        "@babel/helper-replace-supers": "^7.22.9",
+        "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-environment-visitor": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
+      "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-function-name": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
+      "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+      "dependencies": {
+        "@babel/template": "^7.22.5",
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-hoist-variables": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+      "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-member-expression-to-functions": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz",
+      "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==",
+      "dependencies": {
+        "@babel/types": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "dependencies": {
+        "@babel/types": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.22.17",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz",
+      "integrity": "sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==",
+      "dependencies": {
+        "@babel/helper-environment-visitor": "^7.22.5",
+        "@babel/helper-module-imports": "^7.22.15",
+        "@babel/helper-simple-access": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "@babel/helper-validator-identifier": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-optimise-call-expression": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
+      "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
+      "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-replace-supers": {
+      "version": "7.22.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz",
+      "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==",
+      "dependencies": {
+        "@babel/helper-environment-visitor": "^7.22.5",
+        "@babel/helper-member-expression-to-functions": "^7.22.5",
+        "@babel/helper-optimise-call-expression": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-simple-access": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
+      "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
+      "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-split-export-declaration": {
+      "version": "7.22.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+      "dependencies": {
+        "@babel/types": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz",
+      "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
+      "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz",
+      "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==",
+      "dependencies": {
+        "@babel/template": "^7.22.15",
+        "@babel/traverse": "^7.22.15",
+        "@babel/types": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/highlight": {
+      "version": "7.22.13",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz",
+      "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.22.5",
+        "chalk": "^2.4.2",
+        "js-tokens": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.22.16",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
+      "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-jsx": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
+      "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-syntax-typescript": {
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz",
+      "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-modules-commonjs": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz",
+      "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==",
+      "dependencies": {
+        "@babel/helper-module-transforms": "^7.22.15",
+        "@babel/helper-plugin-utils": "^7.22.5",
+        "@babel/helper-simple-access": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-typescript": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz",
+      "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==",
+      "dependencies": {
+        "@babel/helper-annotate-as-pure": "^7.22.5",
+        "@babel/helper-create-class-features-plugin": "^7.22.15",
+        "@babel/helper-plugin-utils": "^7.22.5",
+        "@babel/plugin-syntax-typescript": "^7.22.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/preset-typescript": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.15.tgz",
+      "integrity": "sha512-HblhNmh6yM+cU4VwbBRpxFhxsTdfS1zsvH9W+gEjD0ARV9+8B4sNfpI6GuhePti84nuvhiwKS539jKPFHskA9A==",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.22.5",
+        "@babel/helper-validator-option": "^7.22.15",
+        "@babel/plugin-syntax-jsx": "^7.22.5",
+        "@babel/plugin-transform-modules-commonjs": "^7.22.15",
+        "@babel/plugin-transform-typescript": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.22.15",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+      "dependencies": {
+        "@babel/code-frame": "^7.22.13",
+        "@babel/parser": "^7.22.15",
+        "@babel/types": "^7.22.15"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.22.17",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.17.tgz",
+      "integrity": "sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==",
+      "dependencies": {
+        "@babel/code-frame": "^7.22.13",
+        "@babel/generator": "^7.22.15",
+        "@babel/helper-environment-visitor": "^7.22.5",
+        "@babel/helper-function-name": "^7.22.5",
+        "@babel/helper-hoist-variables": "^7.22.5",
+        "@babel/helper-split-export-declaration": "^7.22.6",
+        "@babel/parser": "^7.22.16",
+        "@babel/types": "^7.22.17",
+        "debug": "^4.1.0",
+        "globals": "^11.1.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.22.17",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz",
+      "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.22.5",
+        "@babel/helper-validator-identifier": "^7.22.15",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@cesium/engine": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-4.0.0.tgz",
+      "integrity": "sha512-r9hQfSQ+Ob8+TEkFEx5gB2f06Sm5VC/k/Ypk34XjS4PiZqktS1K/UcHlmQLhMpsNQqXMx2sd/Rx+SRjyiL4qBQ==",
+      "dev": true,
+      "dependencies": {
+        "@tweenjs/tween.js": "^21.0.0",
+        "@zip.js/zip.js": "2.4.x",
+        "autolinker": "^4.0.0",
+        "bitmap-sdf": "^1.0.3",
+        "dompurify": "^3.0.2",
+        "draco3d": "^1.5.1",
+        "earcut": "^2.2.4",
+        "grapheme-splitter": "^1.0.4",
+        "jsep": "^1.3.8",
+        "kdbush": "^4.0.1",
+        "ktx-parse": "^0.6.0",
+        "lerc": "^2.0.0",
+        "mersenne-twister": "^1.1.0",
+        "meshoptimizer": "^0.19.0",
+        "pako": "^2.0.4",
+        "protobufjs": "^7.1.0",
+        "rbush": "^3.0.1",
+        "topojson-client": "^3.1.0",
+        "urijs": "^1.19.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@cesium/widgets": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-4.0.0.tgz",
+      "integrity": "sha512-4UT4jNmu9ScR8GUZ+wPDjPCI8LSuJDs7sbVfFR0CjGmfP3VqXIacNQduTI3RCRzes5u+Po23dxdIMtFMSycY4g==",
+      "dev": true,
+      "dependencies": {
+        "@cesium/engine": "^4.0.0",
+        "nosleep.js": "^0.12.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@dimforge/rapier3d": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/@dimforge/rapier3d/-/rapier3d-0.11.2.tgz",
+      "integrity": "sha512-B+AKkPmtJxED3goMTGU8v0ju8hUAUQGLgghzCos4G4OeN9X+mJ5lfN2xtNA0n8tJRJk2YfsMk9BOj/6AN89Acg=="
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+      "cpu": [
+        "loong64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.4.15",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+      "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@protobufjs/aspromise": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+      "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/codegen": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+      "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/eventemitter": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+      "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/fetch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+      "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+      "dev": true,
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.1",
+        "@protobufjs/inquire": "^1.1.0"
+      }
+    },
+    "node_modules/@protobufjs/float": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+      "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/inquire": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+      "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/path": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+      "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/pool": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+      "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+      "dev": true
+    },
+    "node_modules/@protobufjs/utf8": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+      "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+      "dev": true
+    },
+    "node_modules/@rollup/plugin-virtual": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.1.tgz",
+      "integrity": "sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==",
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0||^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@swc/core": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.83.tgz",
+      "integrity": "sha512-PccHDgGQlFjpExgJxH91qA3a4aifR+axCFJ4RieCoiI0m5gURE4nBhxzTBY5YU/YKTBmPO8Gc5Q6inE3+NquWg==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "@swc/types": "^0.1.4"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/swc"
+      },
+      "optionalDependencies": {
+        "@swc/core-darwin-arm64": "1.3.83",
+        "@swc/core-darwin-x64": "1.3.83",
+        "@swc/core-linux-arm-gnueabihf": "1.3.83",
+        "@swc/core-linux-arm64-gnu": "1.3.83",
+        "@swc/core-linux-arm64-musl": "1.3.83",
+        "@swc/core-linux-x64-gnu": "1.3.83",
+        "@swc/core-linux-x64-musl": "1.3.83",
+        "@swc/core-win32-arm64-msvc": "1.3.83",
+        "@swc/core-win32-ia32-msvc": "1.3.83",
+        "@swc/core-win32-x64-msvc": "1.3.83"
+      },
+      "peerDependencies": {
+        "@swc/helpers": "^0.5.0"
+      },
+      "peerDependenciesMeta": {
+        "@swc/helpers": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@swc/core-darwin-arm64": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.83.tgz",
+      "integrity": "sha512-Plz2IKeveVLivbXTSCC3OZjD2MojyKYllhPrn9RotkDIZEFRYJZtW5/Ik1tJW/2rzu5HVKuGYrDKdScVVTbOxQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-darwin-x64": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.83.tgz",
+      "integrity": "sha512-FBGVg5IPF/8jQ6FbK60iDUHjv0H5+LwfpJHKH6wZnRaYWFtm7+pzYgreLu3NTsm3m7/1a7t0+7KURwBGUaJCCw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-linux-arm-gnueabihf": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.83.tgz",
+      "integrity": "sha512-EZcsuRYhGkzofXtzwDjuuBC/suiX9s7zeg2YYXOVjWwyebb6BUhB1yad3mcykFQ20rTLO9JUyIaiaMYDHGobqw==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-linux-arm64-gnu": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.83.tgz",
+      "integrity": "sha512-khI41szLHrCD/cFOcN4p2SYvZgHjhhHlcMHz5BksRrDyteSJKu0qtWRZITVom0N/9jWoAleoFhMnFTUs0H8IWA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-linux-arm64-musl": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.83.tgz",
+      "integrity": "sha512-zgT7yNOdbjHcGAwvys79mbfNLK65KBlPJWzeig+Yk7I8TVzmaQge7B6ZS/gwF9/p+8TiLYo/tZ5aF2lqlgdSVw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-linux-x64-gnu": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.83.tgz",
+      "integrity": "sha512-x+mH0Y3NC/G0YNlFmGi3vGD4VOm7IPDhh+tGrx6WtJp0BsShAbOpxtfU885rp1QweZe4qYoEmGqiEjE2WrPIdA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-linux-x64-musl": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.83.tgz",
+      "integrity": "sha512-s5AYhAOmetUwUZwS5g9qb92IYgNHHBGiY2mTLImtEgpAeBwe0LPDj6WrujxCBuZnaS55mKRLLOuiMZE5TpjBNA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-win32-arm64-msvc": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.83.tgz",
+      "integrity": "sha512-yw2rd/KVOGs95lRRB+killLWNaO1dy4uVa8Q3/4wb5txlLru07W1m041fZLzwOg/1Sh0TMjJgGxj0XHGR3ZXhQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-win32-ia32-msvc": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.83.tgz",
+      "integrity": "sha512-POW+rgZ6KWqBpwPGIRd2/3pcf46P+UrKBm4HLt5IwbHvekJ4avIM8ixJa9kK0muJNVJcDpaZgxaU1ELxtJ1j8w==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/core-win32-x64-msvc": {
+      "version": "1.3.83",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.83.tgz",
+      "integrity": "sha512-CiWQtkFnZElXQUalaHp+Wacw0Jd+24ncRYhqaJ9YKnEQP1H82CxIIuQqLM8IFaLpn5dpY6SgzaeubWF46hjcLA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@swc/types": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.4.tgz",
+      "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg=="
+    },
+    "node_modules/@tweenjs/tween.js": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
+      "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==",
+      "dev": true
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
+      "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.6.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+      "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+      "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.20.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz",
+      "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==",
+      "dependencies": {
+        "@babel/types": "^7.20.7"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.6.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
+      "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==",
+      "devOptional": true
+    },
+    "node_modules/@zip.js/zip.js": {
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.26.tgz",
+      "integrity": "sha512-I9HBO3BHIxEMQmltmHM3iqUW6IHqi3gsL9wTSXvHTRpOrA6q2OxtR58EDSaOGjHhDVJ+wIOAxZyKq2x00AVmqw==",
+      "dev": true
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/autolinker": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.0.0.tgz",
+      "integrity": "sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      }
+    },
+    "node_modules/babel-plugin-jsx-dom-expressions": {
+      "version": "0.36.10",
+      "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.36.10.tgz",
+      "integrity": "sha512-QA2k/14WGw+RgcGGnEuLWwnu4em6CGhjeXtjvgOYyFHYS2a+CzPeaVQHDOlfuiBcjq/3hWMspHMIMnPEOIzdBg==",
+      "dependencies": {
+        "@babel/helper-module-imports": "7.18.6",
+        "@babel/plugin-syntax-jsx": "^7.18.6",
+        "@babel/types": "^7.20.7",
+        "html-entities": "2.3.3",
+        "validate-html-nesting": "^1.2.1"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.20.12"
+      }
+    },
+    "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
+      "version": "7.18.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+      "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+      "dependencies": {
+        "@babel/types": "^7.18.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/babel-preset-solid": {
+      "version": "1.7.7",
+      "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.7.7.tgz",
+      "integrity": "sha512-tdxVzx3kgcIjNXAOmGRbzIhFBPeJjSakiN9yM+IYdL/+LtXNnbGqb0Va5tJb8Sjbk+QVEriovCyuzB5T7jeTvg==",
+      "dependencies": {
+        "babel-plugin-jsx-dom-expressions": "^0.36.10"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/bitmap-sdf": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz",
+      "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==",
+      "dev": true
+    },
+    "node_modules/bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+      "dev": true
+    },
+    "node_modules/browserslist": {
+      "version": "4.21.10",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
+      "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001517",
+        "electron-to-chromium": "^1.4.477",
+        "node-releases": "^2.0.13",
+        "update-browserslist-db": "^1.0.11"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001532",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz",
+      "integrity": "sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ]
+    },
+    "node_modules/cesium": {
+      "version": "1.109.0",
+      "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.109.0.tgz",
+      "integrity": "sha512-dyxZnu2/8MScAfaKZJny0mksZOVoJkBiiyEG2EQviG6F37Lm2QcJYoF4ExWogiKgYYI0t2DuzVKAENmejQ95iQ==",
+      "dev": true,
+      "dependencies": {
+        "@cesium/engine": "^4.0.0",
+        "@cesium/widgets": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+    },
+    "node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true
+    },
+    "node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+    },
+    "node_modules/csstype": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
+      "peer": true
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/dompurify": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
+      "integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==",
+      "dev": true
+    },
+    "node_modules/draco3d": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.6.tgz",
+      "integrity": "sha512-+3NaRjWktb5r61ZFoDejlykPEFKT5N/LkbXsaddlw6xNSXBanUYpFc2AXXpbJDilPHazcSreU/DpQIaxfX0NfQ==",
+      "dev": true
+    },
+    "node_modules/earcut": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
+      "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
+      "dev": true
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.4.513",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz",
+      "integrity": "sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "node_modules/esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.1.1",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
+      "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/gltf-pipeline": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/gltf-pipeline/-/gltf-pipeline-4.1.0.tgz",
+      "integrity": "sha512-QVxs+9Sv1AIP0FKtDXfKgxUVtm3YOeXfe9WFZz2duOenAjw4WBa/x+bohFQ0dvFikFZiV/pJ6MZSYEFOYC8hMA==",
+      "dev": true,
+      "dependencies": {
+        "bluebird": "^3.7.2",
+        "cesium": "^1.86.1",
+        "draco3d": "^1.4.3",
+        "fs-extra": "^11.0.0",
+        "mime": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "yargs": "^17.2.1"
+      },
+      "bin": {
+        "gltf-pipeline": "bin/gltf-pipeline.js"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true
+    },
+    "node_modules/grapheme-splitter": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+      "dev": true
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/html-entities": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
+      "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "4.1.15",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz",
+      "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==",
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+    },
+    "node_modules/jsep": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.8.tgz",
+      "integrity": "sha512-qofGylTGgYj9gZFsHuyWAN4jr35eJ66qJCK4eKDnldohuUoQFbU3iZn2zjvEbd9wOAhP9Wx5DsAAduTyE1PSWQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10.16.0"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dev": true,
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/kdbush": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+      "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+      "dev": true
+    },
+    "node_modules/ktx-parse": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.6.0.tgz",
+      "integrity": "sha512-hYOJUI86N9+YPm0M3t8hVzW9t5FnFFibRalZCrqHs/qM2eNziqQzBtAaF0ErgkXm8F+5uE8CjPUYr32vWlXLkQ==",
+      "dev": true
+    },
+    "node_modules/lerc": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/lerc/-/lerc-2.0.0.tgz",
+      "integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==",
+      "dev": true
+    },
+    "node_modules/long": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+      "dev": true
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/merge-anything": {
+      "version": "5.1.7",
+      "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
+      "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
+      "dependencies": {
+        "is-what": "^4.1.8"
+      },
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/mersenne-twister": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz",
+      "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA=="
+    },
+    "node_modules/meshoptimizer": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.19.0.tgz",
+      "integrity": "sha512-58qz5Qc/6Geu8Ib3bBWERE5R7pM5ErrJVo16fAtu6ryxVaE3VAtM/u2vurDxaq8AGZ3yWxuM/DnylTga5a4XCQ==",
+      "dev": true
+    },
+    "node_modules/mime": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+      "dev": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/n8ao": {
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.6.8.tgz",
+      "integrity": "sha512-3xaBaoMIplgPdBK+9mZefa8stWEoA2673h2734wYMxm/hUkMLENMhzymDe+WZueFQq93ly4xpl5s1NJrQBzFOQ==",
+      "peerDependencies": {
+        "postprocessing": ">=6.30.0",
+        "three": ">=0.137"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.13",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+    },
+    "node_modules/nosleep.js": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz",
+      "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==",
+      "dev": true
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "dev": true
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+    },
+    "node_modules/postcss": {
+      "version": "8.4.29",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
+      "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postprocessing": {
+      "version": "6.33.2",
+      "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.33.2.tgz",
+      "integrity": "sha512-xGirHyjArISGVfmjCwXyvuhZm9JpLxEkjdE+ZOSq+7SmSetqFfdpaGfkEjbbFxWShwgwXWmgtcPxvrg9BP+r8g==",
+      "peer": true,
+      "engines": {
+        "node": ">= 0.13.2"
+      },
+      "peerDependencies": {
+        "three": ">= 0.138.0 < 0.158.0"
+      }
+    },
+    "node_modules/protobufjs": {
+      "version": "7.2.5",
+      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz",
+      "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==",
+      "dev": true,
+      "hasInstallScript": true,
+      "dependencies": {
+        "@protobufjs/aspromise": "^1.1.2",
+        "@protobufjs/base64": "^1.1.2",
+        "@protobufjs/codegen": "^2.0.4",
+        "@protobufjs/eventemitter": "^1.1.0",
+        "@protobufjs/fetch": "^1.1.0",
+        "@protobufjs/float": "^1.0.2",
+        "@protobufjs/inquire": "^1.1.0",
+        "@protobufjs/path": "^1.1.2",
+        "@protobufjs/pool": "^1.1.0",
+        "@protobufjs/utf8": "^1.1.0",
+        "@types/node": ">=13.7.0",
+        "long": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/quickselect": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
+      "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
+      "dev": true
+    },
+    "node_modules/rbush": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
+      "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
+      "dev": true,
+      "dependencies": {
+        "quickselect": "^2.0.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "3.29.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.0.tgz",
+      "integrity": "sha512-nszM8DINnx1vSS+TpbWKMkxem0CDWk3cSit/WWCBVs9/JZ1I/XLwOsiUglYuYReaeWWSsW9kge5zE5NZtf/a4w==",
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=14.18.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/seroval": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
+      "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==",
+      "peer": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/solid-js": {
+      "version": "1.7.11",
+      "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.11.tgz",
+      "integrity": "sha512-JkuvsHt8jqy7USsy9xJtT18aF9r2pFO+GB8JQ2XGTvtF49rGTObB46iebD25sE3qVNvIbwglXOXdALnJq9IHtQ==",
+      "peer": true,
+      "dependencies": {
+        "csstype": "^3.1.0",
+        "seroval": "^0.5.0"
+      }
+    },
+    "node_modules/solid-refresh": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.5.3.tgz",
+      "integrity": "sha512-Otg5it5sjOdZbQZJnvo99TEBAr6J7PQ5AubZLNU6szZzg3RQQ5MX04oteBIIGDs0y2Qv8aXKm9e44V8z+UnFdw==",
+      "dependencies": {
+        "@babel/generator": "^7.21.1",
+        "@babel/helper-module-imports": "^7.18.6",
+        "@babel/types": "^7.21.2"
+      },
+      "peerDependencies": {
+        "solid-js": "^1.3"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stats.js": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+      "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/three": {
+      "version": "0.156.1",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.156.1.tgz",
+      "integrity": "sha512-kP7H0FK9d/k6t/XvQ9FO6i+QrePoDcNhwl0I02+wmUJRNSLCUIDMcfObnzQvxb37/0Uc9TDT0T1HgsRRrO6SYQ=="
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/topojson-client": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
+      "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
+      "dev": true,
+      "dependencies": {
+        "commander": "2"
+      },
+      "bin": {
+        "topo2geo": "bin/topo2geo",
+        "topomerge": "bin/topomerge",
+        "topoquantize": "bin/topoquantize"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+      "dev": true
+    },
+    "node_modules/universalify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+      "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/urijs": {
+      "version": "1.19.11",
+      "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
+      "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
+      "dev": true
+    },
+    "node_modules/uuid": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/validate-html-nesting": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz",
+      "integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="
+    },
+    "node_modules/vite": {
+      "version": "4.4.9",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
+      "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
+      "dependencies": {
+        "esbuild": "^0.18.10",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-plugin-solid": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.7.0.tgz",
+      "integrity": "sha512-avp/Jl5zOp/Itfo67xtDB2O61U7idviaIp4mLsjhCa13PjKNasz+IID0jYTyqUp9SFx6/PmBr6v4KgDppqompg==",
+      "dependencies": {
+        "@babel/core": "^7.20.5",
+        "@babel/preset-typescript": "^7.18.6",
+        "@types/babel__core": "^7.1.20",
+        "babel-preset-solid": "^1.7.2",
+        "merge-anything": "^5.1.4",
+        "solid-refresh": "^0.5.0",
+        "vitefu": "^0.2.3"
+      },
+      "peerDependencies": {
+        "solid-js": "^1.7.2",
+        "vite": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "node_modules/vite-plugin-top-level-await": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.3.1.tgz",
+      "integrity": "sha512-55M1h4NAwkrpxPNOJIBzKZFihqLUzIgnElLSmPNPMR2Fn9+JHKaNg3sVX1Fq+VgvuBksQYxiD3OnwQAUu7kaPQ==",
+      "dependencies": {
+        "@rollup/plugin-virtual": "^3.0.1",
+        "@swc/core": "^1.3.10",
+        "uuid": "^9.0.0"
+      },
+      "peerDependencies": {
+        "vite": ">=2.8"
+      }
+    },
+    "node_modules/vite-plugin-wasm": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.2.2.tgz",
+      "integrity": "sha512-cdbBUNR850AEoMd5nvLmnyeq63CSfoP1ctD/L2vLk/5+wsgAPlAVAzUK5nGKWO/jtehNlrSSHLteN+gFQw7VOA==",
+      "peerDependencies": {
+        "vite": "^2 || ^3 || ^4"
+      }
+    },
+    "node_modules/vitefu": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz",
+      "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==",
+      "peerDependencies": {
+        "vite": "^3.0.0 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    }
+  }
+}

+ 25 - 0
package.json

@@ -0,0 +1,25 @@
+{
+  "name": "package",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "gltf-pipeline": "^4.1.0",
+    "vite": "^4.4.9"
+  },
+  "dependencies": {
+    "@dimforge/rapier3d": "^0.11.2",
+    "mersenne-twister": "^1.1.0",
+    "n8ao": "^1.6.8",
+    "stats.js": "^0.17.0",
+    "three": "^0.156.0",
+    "vite-plugin-solid": "^2.7.0",
+    "vite-plugin-top-level-await": "^1.3.0",
+    "vite-plugin-wasm": "^3.2.2"
+  }
+}

+ 1 - 0
resources/characters/README.txt

@@ -0,0 +1 @@
+Taken from https://www.mixamo.com/

BIN
resources/characters/guard.glb


BIN
resources/characters/paladin.glb


BIN
resources/characters/ybot.glb


+ 1 - 0
resources/models/README.txt

@@ -0,0 +1 @@
+These mountains were made from rocks from https://quaternius.com/

BIN
resources/models/mountain.glb


+ 67 - 0
resources/shaders/bugs-lighting-model-fsh.glsl

@@ -0,0 +1,67 @@
+#define PHONG
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform vec3 specular;
+uniform float shininess;
+uniform float opacity;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_pars_fragment>
+#include <fog_pars_fragment>
+#include <bsdfs>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+#include <lights_phong_pars_fragment>
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <specularmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+varying vec3 vWorldNormal;
+
+uniform sampler2D bugsTexture;
+
+void main() {
+	#include <clipping_planes_fragment>
+	vec4 diffuseColor = vec4( diffuse, opacity );
+
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <specularmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+	#include <emissivemap_fragment>
+	#include <lights_phong_fragment>
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
+
+	#include <envmap_fragment>
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	#include <fog_fragment>
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 116 - 0
resources/shaders/bugs-lighting-model-vsh.glsl

@@ -0,0 +1,116 @@
+
+
+#define PHONG
+varying vec3 vViewPosition;
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <envmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+varying vec3 vWorldNormal;
+
+uniform vec2 bugsSize;
+uniform vec4 bugsParams;
+uniform float time;
+
+uniform sampler2D heightmap;
+uniform vec3 heightmapParams;
+
+attribute vec3 offset;
+
+
+void main() {
+  #include <uv_vertex>
+  #include <color_vertex>
+  #include <morphcolor_vertex>
+  // #include <beginnormal_vertex>
+
+  vec3 objectNormal = vec3(0.0, 1.0, 0.0);
+#ifdef USE_TANGENT
+  vec3 objectTangent = vec3( tangent.xyz );
+#endif
+
+  // #include <begin_vertex>
+
+vec3 transformed = vec3( position );
+#ifdef USE_ALPHAHASH
+	vPosition = vec3( position );
+#endif
+
+  vec4 bugHashVal = hash42(offset.xz);
+
+  float BUG_SCALE = mix(0.35, 0.55, bugHashVal.z);
+  transformed *= BUG_SCALE;
+
+  const float FLAP_SPEED = 20.0;
+  float flapTimeSample = time * FLAP_SPEED + bugHashVal.x * 100.0;
+  transformed.y += mix(0.0, sin(flapTimeSample), abs(position.x)) * BUG_SCALE;
+  transformed.x *= abs(cos(flapTimeSample));
+
+  float TIME_PERIOD = 20.0;
+  float repeatingTime = TIME_PERIOD * 0.5 - abs(mod(time, TIME_PERIOD) - TIME_PERIOD * 0.5);
+
+  float height = noise11(time * 3.0 + bugHashVal.x * 100.0);
+  // transformed.y += height * 0.5;
+
+  // Loop
+  float loopTime = time * 0.5 + bugHashVal.x * 123.23;
+  float loopSize = 2.0;
+  vec3 bugsOffset = vec3(sin(loopTime) * loopSize, height * 0.125, cos(loopTime) * loopSize) + offset;
+
+  // Forward
+  transformed = rotateY(-loopTime + PI / 2.0) * transformed;
+  transformed += bugsOffset;
+
+  // Center
+  vec3 bugCenter = offset;
+
+  vec3 bugsWorldPos = (modelMatrix * vec4(bugCenter, 1.0)).xyz;
+  vec2 heightmapUV = vec2(
+      remap(bugsWorldPos.x, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 0.0, 1.0),
+      remap(bugsWorldPos.z, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 1.0, 0.0));
+  float terrainHeight = texture2D(heightmap, heightmapUV).x * heightmapParams.x - heightmapParams.y;
+  transformed.y += terrainHeight;
+
+  if (terrainHeight < -11.0) {
+    transformed.y -= 1000.0;
+  }
+
+  objectNormal = normal;
+
+  #include <morphnormal_vertex>
+  #include <skinbase_vertex>
+  #include <skinnormal_vertex>
+  #include <defaultnormal_vertex>
+  #include <normal_vertex>
+
+  #include <morphtarget_vertex>
+  #include <skinning_vertex>
+  #include <displacementmap_vertex>
+
+  // #include <project_vertex>
+  vec4 mvPosition = vec4( transformed, 1.0 );
+#ifdef USE_INSTANCING
+	mvPosition = instanceMatrix * mvPosition;
+#endif
+  mvPosition = modelViewMatrix * mvPosition;
+  gl_Position = projectionMatrix * mvPosition;
+
+  #include <logdepthbuf_vertex>
+  #include <clipping_planes_vertex>
+  vViewPosition = - mvPosition.xyz;
+  #include <worldpos_vertex>
+  #include <envmap_vertex>
+  #include <shadowmap_vertex>
+  #include <fog_vertex>
+
+  vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz;
+}

+ 105 - 0
resources/shaders/common.glsl

@@ -0,0 +1,105 @@
+// #define PI 3.14159265359
+
+
+float saturate(float x) {
+  return clamp(x, 0.0, 1.0);
+}
+
+vec2 saturate2(vec2 x) {
+  return clamp(x, vec2(0.0), vec2(1.0));
+}
+
+vec3 saturate3(vec3 x) {
+  return clamp(x, vec3(0.0), vec3(1.0));
+}
+
+
+float linearstep(float minValue, float maxValue, float v) {
+  return clamp((v - minValue) / (maxValue - minValue), 0.0, 1.0);
+}
+
+float inverseLerp(float minValue, float maxValue, float v) {
+  return (v - minValue) / (maxValue - minValue);
+}
+
+float inverseLerpSat(float minValue, float maxValue, float v) {
+  return saturate((v - minValue) / (maxValue - minValue));
+}
+
+float remap(float v, float inMin, float inMax, float outMin, float outMax) {
+  float t = inverseLerp(inMin, inMax, v);
+  return mix(outMin, outMax, t);
+}
+
+vec3 LINEAR_TO_GAMMA(vec3 value) {
+  vec3 colour = pow(value, vec3(1.0 / 2.2));
+
+	return colour;
+}
+
+vec3 GAMMA_TO_LINEAR(vec3 value) {
+  vec3 colour = pow(value, vec3(2.2));
+
+	return colour;
+}
+
+
+float easeOut(float x, float t) {
+	return 1.0 - pow(1.0 - x, t);
+}
+
+float easeIn(float x, float t) {
+	return pow(x, t);
+}
+
+
+mat2 rotate2D(float angle) {
+  float s = sin(angle);
+  float c = cos(angle);
+  return mat2(c, -s, s, c);
+}
+
+mat3 rotateX(float theta) {
+    float c = cos(theta);
+    float s = sin(theta);
+    return mat3(
+        vec3(1, 0, 0),
+        vec3(0, c, -s),
+        vec3(0, s, c)
+    );
+}
+
+// Rotation matrix around the Y axis.
+mat3 rotateY(float theta) {
+    float c = cos(theta);
+    float s = sin(theta);
+    return mat3(
+        vec3(c, 0, s),
+        vec3(0, 1, 0),
+        vec3(-s, 0, c)
+    );
+}
+
+// Rotation matrix around the Z axis.
+mat3 rotateZ(float theta) {
+    float c = cos(theta);
+    float s = sin(theta);
+    return mat3(
+        vec3(c, -s, 0),
+        vec3(s, c, 0),
+        vec3(0, 0, 1)
+    );
+}
+
+mat3 rotateAxis(vec3 axis, float angle) {
+  axis = normalize(axis);
+  float s = sin(angle);
+  float c = cos(angle);
+  float oc = 1.0 - c;
+
+  return mat3(
+    oc * axis.x * axis.x + c,           oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,
+    oc * axis.x * axis.y + axis.z * s,  oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,
+    oc * axis.z * axis.x - axis.y * s,  oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c
+  );
+}

+ 143 - 0
resources/shaders/grass-lighting-model-fsh.glsl

@@ -0,0 +1,143 @@
+#define PHONG
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform vec3 specular;
+uniform float shininess;
+uniform float opacity;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_pars_fragment>
+#include <fog_pars_fragment>
+#include <bsdfs>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+// #include <lights_phong_pars_fragment>
+
+uniform sampler2D grassTexture;
+uniform vec3 grassLODColour;
+uniform float time;
+uniform mat3 normalMatrix;
+
+varying vec3 vGrassColour;
+varying vec4 vGrassParams;
+varying vec3 vNormal2;
+varying vec3 vWorldPosition;
+
+varying vec3 vViewPosition;
+struct BlinnPhongMaterial {
+	vec3 diffuseColor;
+	vec3 specularColor;
+	float specularShininess;
+	float specularStrength;
+};
+
+
+void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+	float wrap = 0.5;
+	float dotNL = saturate( (dot( geometry.normal, directLight.direction ) + wrap) / (1.0 + wrap) );
+	vec3 irradiance = dotNL * directLight.color;
+	reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
+	reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularShininess ) * material.specularStrength;
+
+  // Backscatter fakery
+	wrap = 0.5;
+  float backLight = saturate((dot(geometry.viewDir, -directLight.direction) + wrap) / (1.0 + wrap));
+  float falloff = 0.5;//mix(0.5, pow(1.0 - saturate(dot(geometry.viewDir, geometry.normal)), 2.0), 1.0) * 0.5;
+  vec3 scatter = directLight.color * pow(backLight, 1.0) * falloff *  BRDF_Lambert(material.diffuseColor);
+
+  reflectedLight.indirectDiffuse += scatter * (1.0 - vGrassParams.z);
+}
+void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+	reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
+}
+#define RE_Direct				      RE_Direct_BlinnPhong
+#define RE_IndirectDiffuse		RE_IndirectDiffuse_BlinnPhong
+
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <specularmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+
+void main() {
+	vec3 viewDir = normalize(cameraPosition - vWorldPosition);
+
+	#include <clipping_planes_fragment>
+	vec4 diffuseColor = vec4( diffuse, opacity );
+
+  // Grass
+  float heightPercent = vGrassParams.x;
+  float height = vGrassParams.y;
+	float lodFadeIn = vGrassParams.z;
+	float lodFadeOut = 1.0 - lodFadeIn;
+
+  float grassMiddle = mix(
+			smoothstep(abs(vGrassParams.w - 0.5), 0.0, 0.1), 1.0, lodFadeIn);
+
+  float isSandy = saturate(linearstep(-11.0, -14.0, height));
+
+	float density = 1.0 - isSandy;
+
+	// Density is in the range [0, 1]
+	// 0 being no grass
+	// 1 being full grass
+	float aoForDensity = mix(1.0, 0.25, density);
+  float ao = mix(aoForDensity, 1.0, easeIn(heightPercent, 2.0));
+
+  diffuseColor.rgb *= vGrassColour;
+	diffuseColor.rgb *= mix(0.85, 1.0, grassMiddle);
+  diffuseColor.rgb *= ao;
+
+
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <specularmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+
+	vec3 normal2 = normalize(vNormal2);
+	normal = normalize(mix(vNormal, normal2, vGrassParams.w));
+
+	#include <emissivemap_fragment>
+	// #include <lights_phong_fragment>
+
+  BlinnPhongMaterial material;
+  material.diffuseColor = diffuseColor.rgb;
+  material.specularColor = specular;
+
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
+
+	#include <envmap_fragment>
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	// #include <fog_fragment>
+
+	gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth);
+
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 232 - 0
resources/shaders/grass-lighting-model-vsh.glsl

@@ -0,0 +1,232 @@
+
+
+#define PHONG
+varying vec3 vViewPosition;
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <envmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+varying vec3 vWorldNormal;
+varying vec3 vGrassColour;
+varying vec4 vGrassParams;
+varying vec3 vNormal2;
+varying vec3 vWorldPosition;
+
+uniform vec2 grassSize;
+uniform vec4 grassParams;
+uniform vec4 grassDraw;
+uniform float time;
+uniform sampler2D heightmap;
+uniform vec4 heightParams;
+uniform vec3 playerPos;
+uniform mat4 viewMatrixInverse;
+
+attribute float vertIndex;
+
+
+void main() {
+  #include <uv_vertex>
+  #include <color_vertex>
+  #include <morphcolor_vertex>
+  #include <beginnormal_vertex>
+  #include <begin_vertex>
+
+  vec3 grassOffset = vec3(position.x, 0.0, position.y);
+
+  // Blade world position
+  vec3 grassBladeWorldPos = (modelMatrix * vec4(grassOffset, 1.0)).xyz;
+  vec2 heightmapUV = vec2(
+      remap(grassBladeWorldPos.x, -heightParams.x * 0.5, heightParams.x * 0.5, 0.0, 1.0),
+      remap(grassBladeWorldPos.z, -heightParams.x * 0.5, heightParams.x * 0.5, 1.0, 0.0));
+  vec4 heightmapSample = texture2D(heightmap, heightmapUV);
+  grassBladeWorldPos.y += heightmapSample.x * grassParams.z - grassParams.w;
+
+  float heightmapSampleHeight = 1.0;//mix(0.5, 1.0, heightmapSample.y);
+
+  vec4 hashVal1 = hash42(vec2(grassBladeWorldPos.x, grassBladeWorldPos.z));
+
+  float highLODOut = smoothstep(grassDraw.x * 0.5, grassDraw.x, distance(cameraPosition, grassBladeWorldPos));
+// hack lod
+  float lodFadeIn = smoothstep(grassDraw.x, grassDraw.y, distance(cameraPosition, grassBladeWorldPos));
+
+  // Check terrain type, maybe don't allow grass blade
+  float isSandy = linearstep(-11.0, -14.0, grassBladeWorldPos.y);
+  float grassAllowedHash = hashVal1.w - isSandy;
+  float isGrassAllowed = step(0.0, grassAllowedHash);
+
+  float randomAngle = hashVal1.x * 2.0 * 3.14159;
+  float randomShade = remap(hashVal1.y, -1.0, 1.0, 0.5, 1.0);
+  float randomHeight = remap(hashVal1.z, 0.0, 1.0, 0.75, 1.5) * mix(1.0, 0.0, lodFadeIn) * isGrassAllowed * heightmapSampleHeight;
+  float randomWidth = (1.0 - isSandy) * heightmapSampleHeight;
+  float randomLean = remap(hashVal1.w, 0.0, 1.0, 0.1, 0.4);
+
+  // HACK
+  // randomHeight = 1.0;
+  // randomShade = 1.0;
+  // randomWidth = 1.0;
+
+  vec2 hashGrassColour = hash22(vec2(grassBladeWorldPos.x, grassBladeWorldPos.z));
+  float leanAnimation = noise12(vec2(time * 0.35) + grassBladeWorldPos.xz * 137.423) * 0.1;
+
+  float GRASS_SEGMENTS = grassParams.x;
+  float GRASS_VERTICES = grassParams.y;
+
+  // Figure out vertex id, > GRASS_VERTICES is back side
+  float vertID = mod(float(vertIndex), GRASS_VERTICES);
+
+  // 1 = front, -1 = back
+  float zSide = -(floor(vertIndex / GRASS_VERTICES) * 2.0 - 1.0);
+
+  // 0 = left, 1 = right
+  float xSide = mod(vertID, 2.0);
+
+  float heightPercent = (vertID - xSide) / (GRASS_SEGMENTS * 2.0);
+
+  float grassTotalHeight = grassSize.y * randomHeight;
+  float grassTotalWidthHigh = easeOut(1.0 - heightPercent, 2.0);
+  float grassTotalWidthLow = 1.0 - heightPercent;
+  float grassTotalWidth = grassSize.x * mix(grassTotalWidthHigh, grassTotalWidthLow, highLODOut) * randomWidth;
+
+// hack lod
+  // grassTotalWidth = grassSize.x * randomWidth;
+
+  // Shift verts
+  float x = (xSide - 0.5) * grassTotalWidth;
+  float y = heightPercent * grassTotalHeight;
+
+  float windDir = noise12(grassBladeWorldPos.xz * 0.05 + 0.05 * time);
+  float windNoiseSample = noise12(grassBladeWorldPos.xz * 0.25 + time * 1.0);
+  float windLeanAngle = remap(windNoiseSample, -1.0, 1.0, 0.25, 1.0);
+  windLeanAngle = easeIn(windLeanAngle, 2.0) * 1.25;
+  vec3 windAxis = vec3(cos(windDir), 0.0, sin(windDir));
+
+  windLeanAngle *= heightPercent;
+
+  float distToPlayer = distance(grassBladeWorldPos.xz, playerPos.xz);
+  float playerFalloff = smoothstep(2.5, 1.0, distToPlayer);
+  float playerLeanAngle = mix(0.0, 0.2, playerFalloff * linearstep(0.5, 0.0, windLeanAngle));
+  vec3 grassToPlayer = normalize(vec3(playerPos.x, 0.0, playerPos.z) - vec3(grassBladeWorldPos.x, 0.0, grassBladeWorldPos.z));
+  vec3 playerLeanAxis = vec3(grassToPlayer.z, 0, -grassToPlayer.x);
+
+// hack lod
+  // randomLean = 0.0;
+  // windLeanAngle = 0.0;
+  // leanAnimation = 0.0;
+
+  randomLean += leanAnimation;
+
+  float easedHeight = mix(easeIn(heightPercent, 2.0), 1.0, highLODOut);
+  float curveAmount = -randomLean * easedHeight;
+
+  float ncurve1 = -randomLean * easedHeight;
+  vec3 n1 = vec3(0.0, (heightPercent + 0.01), 0.0);
+  n1 = rotateX(ncurve1) * n1;
+
+  float ncurve2 = -randomLean * easedHeight * 0.9;
+  vec3 n2 = vec3(0.0, (heightPercent + 0.01) * 0.9, 0.0);
+  n2 = rotateX(ncurve2) * n2;
+
+  vec3 ncurve = normalize(n1 - n2);
+
+  mat3 grassMat = rotateAxis(playerLeanAxis, playerLeanAngle) * rotateAxis(windAxis, windLeanAngle) * rotateY(randomAngle);
+
+  vec3 grassFaceNormal = vec3(0.0, 0.0, 1.0);
+  grassFaceNormal = grassMat * grassFaceNormal;
+  grassFaceNormal *= zSide;
+
+  vec3 grassVertexNormal = vec3(0.0, -ncurve.z, ncurve.y);
+  vec3 grassVertexNormal1 = rotateY(PI * 0.3 * zSide) * grassVertexNormal;
+  vec3 grassVertexNormal2 = rotateY(PI * -0.3 * zSide) * grassVertexNormal;
+
+  grassVertexNormal1 = grassMat * grassVertexNormal1;
+  grassVertexNormal1 *= zSide;
+
+  grassVertexNormal2 = grassMat * grassVertexNormal2;
+  grassVertexNormal2 *= zSide;
+
+  vec3 grassVertexPosition = vec3(x, y, 0.0);
+  grassVertexPosition = rotateX(curveAmount) * grassVertexPosition;
+  grassVertexPosition = grassMat * grassVertexPosition;
+
+  grassVertexPosition += grassOffset;
+
+  vec3 b1 = GAMMA_TO_LINEAR(vec3(0.02, 0.075, 0.01));
+  vec3 b2 = GAMMA_TO_LINEAR(vec3(0.025, 0.1, 0.01));
+  vec3 t1 = GAMMA_TO_LINEAR(vec3(0.25, 0.5, 0.15));
+  vec3 t2 = GAMMA_TO_LINEAR(vec3(0.3, 0.6, 0.2));
+
+  vec3 baseColour = mix(b1, b2, hashGrassColour.x);
+  vec3 tipColour = mix(t1, t2, hashGrassColour.y);
+  vec3 highLODColour = mix(baseColour, tipColour, easeIn(heightPercent, 4.0)) * randomShade;
+  vec3 lowLODColour = mix(b1, t1, heightPercent);
+  vGrassColour = mix(highLODColour, lowLODColour, highLODOut);
+  vGrassParams = vec4(heightPercent, grassBladeWorldPos.y, highLODOut, xSide);
+  
+  const float SKY_RATIO = 0.25;
+  // TODO: Grab terrain normal
+  vec3 UP = vec3(0.0, 1.0, 0.0);
+  // float skyFadeIn = smoothstep(grassDraw.x * 0.5, grassDraw.x, distance(cameraPosition, grassBladeWorldPos)) * SKY_RATIO;
+  float skyFadeIn = (1.0 - highLODOut) * SKY_RATIO;
+  vec3 normal1 = normalize(mix(UP, grassVertexNormal1, skyFadeIn));
+  vec3 normal2 = normalize(mix(UP, grassVertexNormal2, skyFadeIn));
+
+  transformed = grassVertexPosition;
+  transformed.y += grassBladeWorldPos.y;
+
+  vec3 cameraWorldLeft = (viewMatrixInverse * vec4(-1.0, 0.0, 0.0, 0.0)).xyz;
+
+  vec3 viewDir = normalize(cameraPosition - grassBladeWorldPos);
+  vec3 viewDirXZ = normalize(vec3(viewDir.x, 0.0, viewDir.z));
+
+  vec3 grassFaceNormalXZ = normalize(vec3(grassFaceNormal.x, 0.0, grassFaceNormal.z));
+
+  float viewDotNormal = saturate(dot(grassFaceNormal, viewDirXZ));
+  float viewSpaceThickenFactor = easeOut(1.0 - viewDotNormal, 4.0) * smoothstep(0.0, 0.2, viewDotNormal);
+
+  objectNormal = grassVertexNormal1;
+
+  #include <morphnormal_vertex>
+  #include <skinbase_vertex>
+  #include <skinnormal_vertex>
+
+  #include <defaultnormal_vertex>
+  #include <normal_vertex>
+
+  vNormal = normalize(normalMatrix * normal1);
+  vNormal2 = normalize(normalMatrix * normal2);
+
+  #include <morphtarget_vertex>
+  #include <skinning_vertex>
+  #include <displacementmap_vertex>
+
+  // #include <project_vertex>
+  vec4 mvPosition = vec4( transformed, 1.0 );
+#ifdef USE_INSTANCING
+	mvPosition = instanceMatrix * mvPosition;
+#endif
+  mvPosition = modelViewMatrix * mvPosition;
+
+  // HACK
+  mvPosition.x += viewSpaceThickenFactor * (xSide - 0.5) * grassTotalWidth * 0.5 * zSide;
+
+  gl_Position = projectionMatrix * mvPosition;
+
+  #include <logdepthbuf_vertex>
+  #include <clipping_planes_vertex>
+  vViewPosition = - mvPosition.xyz;
+  #include <worldpos_vertex>
+  #include <envmap_vertex>
+  #include <shadowmap_vertex>
+  #include <fog_vertex>
+
+  vWorldPosition = worldPosition.xyz;
+}

+ 17 - 0
resources/shaders/header.glsl

@@ -0,0 +1,17 @@
+
+
+vec3 COLOUR_LIGHT_BLUE = vec3(0.42, 0.65, 0.85);
+vec3 COLOUR_LIGHT_GREEN = vec3(0.25, 1.0, 0.25);
+vec3 COLOUR_PALE_GREEN = vec3(0.42, 0.85, 0.65);
+vec3 COLOUR_LIGHT_PURPLE = vec3(0.85, 0.25, 0.85);
+vec3 COLOUR_BRIGHT_PINK = vec3(1.0, 0.5, 0.5);
+
+vec3 COLOUR_BRIGHT_RED = vec3(1.0, 0.1, 0.02);
+vec3 COLOUR_BRIGHT_BLUE = vec3(0.01, 0.2, 1.0);
+vec3 COLOUR_BRIGHT_GREEN = vec3(0.01, 1.0, 0.2);
+vec3 COLOUR_PALE_BLUE = vec3(0.42, 0.65, 0.85);
+vec3 COLOUR_LIGHT_YELLOW = vec3(1.0, 1.0, 0.25);
+
+
+
+#define USE_OKLAB

+ 127 - 0
resources/shaders/lighting-model-fsh.glsl

@@ -0,0 +1,127 @@
+#define STANDARD
+#ifdef PHYSICAL
+	#define IOR
+	#define USE_SPECULAR
+#endif
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform float roughness;
+uniform float metalness;
+uniform float opacity;
+#ifdef IOR
+	uniform float ior;
+#endif
+#ifdef USE_SPECULAR
+	uniform float specularIntensity;
+	uniform vec3 specularColor;
+	#ifdef USE_SPECULAR_COLORMAP
+		uniform sampler2D specularColorMap;
+	#endif
+	#ifdef USE_SPECULAR_INTENSITYMAP
+		uniform sampler2D specularIntensityMap;
+	#endif
+#endif
+#ifdef USE_CLEARCOAT
+	uniform float clearcoat;
+	uniform float clearcoatRoughness;
+#endif
+#ifdef USE_IRIDESCENCE
+	uniform float iridescence;
+	uniform float iridescenceIOR;
+	uniform float iridescenceThicknessMinimum;
+	uniform float iridescenceThicknessMaximum;
+#endif
+#ifdef USE_SHEEN
+	uniform vec3 sheenColor;
+	uniform float sheenRoughness;
+	#ifdef USE_SHEEN_COLORMAP
+		uniform sampler2D sheenColorMap;
+	#endif
+	#ifdef USE_SHEEN_ROUGHNESSMAP
+		uniform sampler2D sheenRoughnessMap;
+	#endif
+#endif
+#ifdef USE_ANISOTROPY
+	uniform vec2 anisotropyVector;
+	#ifdef USE_ANISOTROPYMAP
+		uniform sampler2D anisotropyMap;
+	#endif
+#endif
+varying vec3 vViewPosition;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <iridescence_fragment>
+#include <cube_uv_reflection_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_physical_pars_fragment>
+#include <fog_pars_fragment>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+#include <lights_physical_pars_fragment>
+#include <transmission_pars_fragment>
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <clearcoat_pars_fragment>
+#include <iridescence_pars_fragment>
+#include <roughnessmap_pars_fragment>
+#include <metalnessmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+varying vec3 vWorldNormal;
+
+void main() {
+	#include <clipping_planes_fragment>
+	vec4 diffuseColor = vec4( diffuse, opacity );
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <roughnessmap_fragment>
+	#include <metalnessmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+	#include <clearcoat_normal_fragment_begin>
+	#include <clearcoat_normal_fragment_maps>
+	#include <emissivemap_fragment>
+	#include <lights_physical_fragment>
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;
+	vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;
+	#include <transmission_fragment>
+	vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;
+	#ifdef USE_SHEEN
+		float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );
+		outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular;
+	#endif
+	#ifdef USE_CLEARCOAT
+		float dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );
+		vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );
+		outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;
+	#endif
+
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	#include <fog_fragment>
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 57 - 0
resources/shaders/lighting-model-vsh.glsl

@@ -0,0 +1,57 @@
+#define STANDARD
+varying vec3 vViewPosition;
+#ifdef USE_TRANSMISSION
+	varying vec3 vWorldPosition;
+#endif
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+varying vec3 vWorldNormal;
+
+void main() {
+	#include <uv_vertex>
+	#include <color_vertex>
+	#include <morphcolor_vertex>
+	#include <beginnormal_vertex>
+	#include <morphnormal_vertex>
+	#include <skinbase_vertex>
+	#include <skinnormal_vertex>
+	#include <defaultnormal_vertex>
+	#include <normal_vertex>
+	#include <begin_vertex>
+	#include <morphtarget_vertex>
+	#include <skinning_vertex>
+	#include <displacementmap_vertex>
+	#include <project_vertex>
+	#include <logdepthbuf_vertex>
+	#include <clipping_planes_vertex>
+
+	vViewPosition = - mvPosition.xyz;
+
+	// #include <worldpos_vertex>
+#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0
+	vec4 worldPosition = vec4( transformed, 1.0 );
+	#ifdef USE_INSTANCING
+		worldPosition = instanceMatrix * worldPosition;
+	#endif
+	worldPosition = modelMatrix * worldPosition;
+#endif
+
+	#include <shadowmap_vertex>
+	#include <fog_vertex>
+#ifdef USE_TRANSMISSION
+	vWorldPosition = worldPosition.xyz;
+#endif
+
+	vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz;
+	// vWorldNormal = normal.xyz;
+}

+ 312 - 0
resources/shaders/noise.glsl

@@ -0,0 +1,312 @@
+// Virtually all of these were taken from: https://www.shadertoy.com/view/ttc3zr
+
+uvec4 murmurHash42(uvec2 src) {
+    const uint M = 0x5bd1e995u;
+    uvec4 h = uvec4(1190494759u, 2147483647u, 3559788179u, 179424673u);
+    src *= M; src ^= src>>24u; src *= M;
+    h *= M; h ^= src.x; h *= M; h ^= src.y;
+    h ^= h>>13u; h *= M; h ^= h>>15u;
+    return h;
+}
+
+uint murmurHash11(uint src) {
+  const uint M = 0x5bd1e995u;
+  uint h = 1190494759u;
+  src *= M; src ^= src>>24u; src *= M;
+  h *= M; h ^= src;
+  h ^= h>>13u; h *= M; h ^= h>>15u;
+  return h;
+}
+
+uint murmurHash12(uvec2 src) {
+  const uint M = 0x5bd1e995u;
+  uint h = 1190494759u;
+  src *= M; src ^= src>>24u; src *= M;
+  h *= M; h ^= src.x; h *= M; h ^= src.y;
+  h ^= h>>13u; h *= M; h ^= h>>15u;
+  return h;
+}
+
+uint murmurHash13(uvec3 src) {
+    const uint M = 0x5bd1e995u;
+    uint h = 1190494759u;
+    src *= M; src ^= src>>24u; src *= M;
+    h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z;
+    h ^= h>>13u; h *= M; h ^= h>>15u;
+    return h;
+}
+
+uvec2 murmurHash22(uvec2 src) {
+  const uint M = 0x5bd1e995u;
+  uvec2 h = uvec2(1190494759u, 2147483647u);
+  src *= M; src ^= src>>24u; src *= M;
+  h *= M; h ^= src.x; h *= M; h ^= src.y;
+  h ^= h>>13u; h *= M; h ^= h>>15u;
+  return h;
+}
+
+uvec2 murmurHash21(uint src) {
+  const uint M = 0x5bd1e995u;
+  uvec2 h = uvec2(1190494759u, 2147483647u);
+  src *= M; src ^= src>>24u; src *= M;
+  h *= M; h ^= src;
+  h ^= h>>13u; h *= M; h ^= h>>15u;
+  return h;
+}
+
+uvec2 murmurHash23(uvec3 src) {
+    const uint M = 0x5bd1e995u;
+    uvec2 h = uvec2(1190494759u, 2147483647u);
+    src *= M; src ^= src>>24u; src *= M;
+    h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z;
+    h ^= h>>13u; h *= M; h ^= h>>15u;
+    return h;
+}
+
+uvec3 murmurHash31(uint src) {
+    const uint M = 0x5bd1e995u;
+    uvec3 h = uvec3(1190494759u, 2147483647u, 3559788179u);
+    src *= M; src ^= src>>24u; src *= M;
+    h *= M; h ^= src;
+    h ^= h>>13u; h *= M; h ^= h>>15u;
+    return h;
+}
+
+uvec3 murmurHash33(uvec3 src) {
+  const uint M = 0x5bd1e995u;
+  uvec3 h = uvec3(1190494759u, 2147483647u, 3559788179u);
+  src *= M; src ^= src>>24u; src *= M;
+  h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z;
+  h ^= h>>13u; h *= M; h ^= h>>15u;
+  return h;
+}
+
+// 3 outputs, 3 inputs
+vec3 hash33(vec3 src) {
+  uvec3 h = murmurHash33(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 1 output, 1 input
+float hash11(float src) {
+  uint h = murmurHash11(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 1 output, 2 inputs
+float hash12(vec2 src) {
+  uint h = murmurHash12(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 1 output, 3 inputs
+float hash13(vec3 src) {
+    uint h = murmurHash13(floatBitsToUint(src));
+    return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 2 outputs, 1 input
+vec2 hash21(float src) {
+  uvec2 h = murmurHash21(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 3 outputs, 1 input
+vec3 hash31(float src) {
+    uvec3 h = murmurHash31(floatBitsToUint(src));
+    return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 2 outputs, 2 inputs
+vec2 hash22(vec2 src) {
+  uvec2 h = murmurHash22(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+// 4 outputs, 2 inputs
+vec4 hash42(vec2 src) {
+  uvec4 h = murmurHash42(floatBitsToUint(src));
+  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+
+// 2 outputs, 3 inputs
+vec2 hash23(vec3 src) {
+    uvec2 h = murmurHash23(floatBitsToUint(src));
+    return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
+}
+
+float noise11(float p) {
+  float i = floor(p);
+
+  float f = fract(p);
+  float u = smoothstep(0.0, 1.0, f);
+
+	float val = mix( hash11(i + 0.0),
+                   hash11(i + 1.0), u);
+  return val * 2.0 - 1.0;
+}
+
+float noise12(vec2 p) {
+  vec2 i = floor(p);
+
+  vec2 f = fract(p);
+  vec2 u = smoothstep(vec2(0.0), vec2(1.0), f);
+
+	float val = mix( mix( hash12( i + vec2(0.0, 0.0) ), 
+                        hash12( i + vec2(1.0, 0.0) ), u.x),
+                   mix( hash12( i + vec2(0.0, 1.0) ), 
+                        hash12( i + vec2(1.0, 1.0) ), u.x), u.y);
+  return val * 2.0 - 1.0;
+}
+
+float noise13(vec3 x) {
+  vec3 i = floor(x);
+  vec3 f = fract(x);
+  f = f*f*(3.0-2.0*f);
+
+  return mix(mix(mix( hash13(i+vec3(0.0, 0.0, 0.0)), 
+                      hash13(i+vec3(1.0, 0.0, 0.0)),f.x),
+                 mix( hash13(i+vec3(0.0, 1.0, 0.0)), 
+                      hash13(i+vec3(1.0, 1.0, 0.0)),f.x),f.y),
+             mix(mix( hash13(i+vec3(0.0, 0.0, 1.0)), 
+                      hash13(i+vec3(1.0, 0.0, 1.0)),f.x),
+                 mix( hash13(i+vec3(0.0, 1.0, 1.0)), 
+                      hash13(i+vec3(1.0, 1.0, 1.0)),f.x),f.y),f.z);
+}
+
+vec2 noise23(vec3 x) {
+  vec3 i = floor(x);
+  vec3 f = fract(x);
+  f = f*f*(3.0-2.0*f);
+
+  return mix(mix(mix( hash23(i+vec3(0.0, 0.0, 0.0)), 
+                      hash23(i+vec3(1.0, 0.0, 0.0)),f.x),
+                 mix( hash23(i+vec3(0.0, 1.0, 0.0)), 
+                      hash23(i+vec3(1.0, 1.0, 0.0)),f.x),f.y),
+             mix(mix( hash23(i+vec3(0.0, 0.0, 1.0)), 
+                      hash23(i+vec3(1.0, 0.0, 1.0)),f.x),
+                 mix( hash23(i+vec3(0.0, 1.0, 1.0)), 
+                      hash23(i+vec3(1.0, 1.0, 1.0)),f.x),f.y),f.z);
+}
+
+vec2 noise22(vec2 p) {
+	vec2 i = floor(p);
+
+	vec2 f = fract(p);
+	vec2 u = smoothstep(vec2(0.0), vec2(1.0), f);
+
+	vec2 val = mix( mix( hash22( i + vec2(0.0, 0.0) ), 
+											 hash22( i + vec2(1.0, 0.0) ), u.x),
+									mix( hash22( i + vec2(0.0, 1.0) ), 
+											 hash22( i + vec2(1.0, 1.0) ), u.x), u.y);
+	return val * 2.0 - 1.0;
+}
+
+// The MIT License
+// Copyright © 2017 Inigo Quilez
+// 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.youtube.com/c/InigoQuilez
+// https://iquilezles.org/
+vec4 noised_1_3(vec3 x) {
+  vec3 i = floor(x);
+  vec3 f = fract(x);
+    
+  // quintic interpolant
+  vec3 u = f*f*f*(f*(f*6.0-15.0)+10.0);
+  vec3 du = 30.0*f*f*(f*(f-2.0)+1.0);
+    
+  // gradients
+  vec3 ga = hash33( i+vec3(0.0,0.0,0.0) ) * 2.0 - 1.0;
+  vec3 gb = hash33( i+vec3(1.0,0.0,0.0) ) * 2.0 - 1.0;
+  vec3 gc = hash33( i+vec3(0.0,1.0,0.0) ) * 2.0 - 1.0;
+  vec3 gd = hash33( i+vec3(1.0,1.0,0.0) ) * 2.0 - 1.0;
+  vec3 ge = hash33( i+vec3(0.0,0.0,1.0) ) * 2.0 - 1.0;
+	vec3 gf = hash33( i+vec3(1.0,0.0,1.0) ) * 2.0 - 1.0;
+  vec3 gg = hash33( i+vec3(0.0,1.0,1.0) ) * 2.0 - 1.0;
+  vec3 gh = hash33( i+vec3(1.0,1.0,1.0) ) * 2.0 - 1.0;
+    
+  // projections
+  float va = dot( ga, f-vec3(0.0,0.0,0.0) );
+  float vb = dot( gb, f-vec3(1.0,0.0,0.0) );
+  float vc = dot( gc, f-vec3(0.0,1.0,0.0) );
+  float vd = dot( gd, f-vec3(1.0,1.0,0.0) );
+  float ve = dot( ge, f-vec3(0.0,0.0,1.0) );
+  float vf = dot( gf, f-vec3(1.0,0.0,1.0) );
+  float vg = dot( gg, f-vec3(0.0,1.0,1.0) );
+  float vh = dot( gh, f-vec3(1.0,1.0,1.0) );
+	
+  // interpolations
+  return vec4( va + u.x*(vb-va) + u.y*(vc-va) + u.z*(ve-va) + u.x*u.y*(va-vb-vc+vd) + u.y*u.z*(va-vc-ve+vg) + u.z*u.x*(va-vb-ve+vf) + (-va+vb+vc-vd+ve-vf-vg+vh)*u.x*u.y*u.z,    // value
+                ga + u.x*(gb-ga) + u.y*(gc-ga) + u.z*(ge-ga) + u.x*u.y*(ga-gb-gc+gd) + u.y*u.z*(ga-gc-ge+gg) + u.z*u.x*(ga-gb-ge+gf) + (-ga+gb+gc-gd+ge-gf-gg+gh)*u.x*u.y*u.z +   // derivatives
+                du * (vec3(vb,vc,ve) - va + u.yzx*vec3(va-vb-vc+vd,va-vc-ve+vg,va-vb-ve+vf) + u.zxy*vec3(va-vb-ve+vf,va-vb-vc+vd,va-vc-ve+vg) + u.yzx*u.zxy*(-va+vb+vc-vd+ve-vf-vg+vh) ));
+}
+
+float FBM_1_2(vec2 p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 1.0;
+  float frequency = 1.0;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise12(p * frequency);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    frequency *= lacunarity;
+  }
+
+  total /= normalization;
+  total = total * 0.5 + 0.5;
+
+  return total;
+}
+
+float FBM_1_3(vec3 p, int octaves, float persistence, float lacunarity) {
+  float amplitude = 1.0;
+  float frequency = 1.0;
+  float total = 0.0;
+  float normalization = 0.0;
+
+  for (int i = 0; i < octaves; ++i) {
+    float noiseValue = noise13(p * frequency);
+    total += noiseValue * amplitude;
+    normalization += amplitude;
+    amplitude *= persistence;
+    frequency *= lacunarity;
+  }
+
+  total /= normalization;
+  total = total * 0.5 + 0.5;
+
+  return total;
+}
+
+const mat3 m3  = mat3( 0.00,  0.80,  0.60,
+                      -0.80,  0.36, -0.48,
+                      -0.60, -0.48,  0.64 );
+const mat3 m3i = mat3( 0.00, -0.80, -0.60,
+                       0.80,  0.36, -0.48,
+                       0.60, -0.48,  0.64 );
+
+vec4 FBM_D_1_4(in vec3 x, int octaves) {
+  float f = 1.98;  // could be 2.0
+  float s = 0.49;  // could be 0.5
+  float a = 0.0;
+  float b = 0.5;
+  vec3  d = vec3(0.0);
+  mat3  m = mat3(
+      1.0,0.0,0.0,
+      0.0,1.0,0.0,
+      0.0,0.0,1.0);
+  for( int i=0; i < octaves; i++ )
+  {
+      vec4 n = noised_1_3(x);
+      a += b*n.x;          // accumulate values
+      d += b*m*n.yzw;      // accumulate derivatives
+      b *= s;
+      x = f*m3*x;
+      m = f*m3i*m;
+  }
+  return vec4( a, d );
+}

+ 44 - 0
resources/shaders/oklab.glsl

@@ -0,0 +1,44 @@
+/////////////////////////////////////////////////////////////////////////
+//
+// OKLab stuff, mostly based off https://www.shadertoy.com/view/ttcyRS
+//
+/////////////////////////////////////////////////////////////////////////
+
+const mat3 kLMStoCONE = mat3(
+    4.0767245293, -1.2681437731, -0.0041119885,
+    -3.3072168827, 2.6093323231, -0.7034763098,
+    0.2307590544, -0.3411344290,  1.7068625689);
+
+const mat3 kCONEtoLMS = mat3(
+    0.4121656120, 0.2118591070, 0.0883097947,
+    0.5362752080, 0.6807189584, 0.2818474174,
+    0.0514575653, 0.1074065790, 0.6302613616);
+
+vec3 rgbToOklab(vec3 c) {
+  vec3 lms = kCONEtoLMS * c;
+
+  return sign(lms)*pow(abs(lms), vec3(0.3333333333333));
+}
+
+vec3 oklabToRGB(vec3 c) {
+  vec3 lms = c;
+  
+  return kLMStoCONE * (lms * lms * lms);
+}
+
+
+#ifndef USE_OKLAB
+#define col3 vec3
+#else
+vec3 col3(float r, float g, float b) {
+  return rgbToOklab(vec3(r, g, b));
+}
+
+vec3 col3(vec3 v) {
+  return rgbToOklab(v);
+}
+
+vec3 col3(float v) {
+  return rgbToOklab(vec3(v));
+}
+#endif

+ 109 - 0
resources/shaders/phong-lighting-model-fsh.glsl

@@ -0,0 +1,109 @@
+#define PHONG
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform vec3 specular;
+uniform float shininess;
+uniform float opacity;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_pars_fragment>
+#include <fog_pars_fragment>
+#include <bsdfs>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+
+varying vec3 vViewPosition;
+struct BlinnPhongMaterial {
+	vec3 diffuseColor;
+	vec3 specularColor;
+	float specularShininess;
+	float specularStrength;
+};
+void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+	float dotNL = saturate( dot( geometry.normal, directLight.direction ) );
+	vec3 irradiance = dotNL * directLight.color;
+	reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
+	reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularShininess ) * material.specularStrength;
+
+  // Backscatter
+  float backLight = saturate(dot(geometry.viewDir, -directLight.direction));
+  float falloff = pow(1.0 - saturate(dot(geometry.viewDir, geometry.normal)), 2.0);
+  vec3 scatter = directLight.color * pow(backLight, 4.0) * falloff *  BRDF_Lambert(material.diffuseColor);
+
+  // reflectedLight.indirectDiffuse += scatter * 2.0;
+}
+void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+	reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
+}
+#define RE_Direct				      RE_Direct_BlinnPhong
+#define RE_IndirectDiffuse		RE_IndirectDiffuse_BlinnPhong
+
+
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <specularmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPosition;
+
+void main() {
+	#include <clipping_planes_fragment>
+
+	vec3 viewDir = normalize(cameraPosition - vWorldPosition);
+
+	vec4 diffuseColor = vec4( diffuse, opacity );
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <specularmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+	#include <emissivemap_fragment>
+
+	// #include <lights_phong_fragment>
+  BlinnPhongMaterial material;
+  material.diffuseColor = diffuseColor.rgb;
+  material.specularColor = specular;
+  material.specularShininess = shininess;
+  material.specularStrength = specularStrength;
+
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
+
+  // outgoingLight = normalize(vWorldNormal);
+	// outgoingLight = reflectedLight.directSpecular;
+
+	#include <envmap_fragment>
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	// #include <fog_fragment>
+
+	gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth);
+
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 44 - 0
resources/shaders/phong-lighting-model-vsh.glsl

@@ -0,0 +1,44 @@
+#define PHONG
+varying vec3 vViewPosition;
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <envmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPosition;
+
+void main() {
+	#include <uv_vertex>
+	#include <color_vertex>
+	#include <morphcolor_vertex>
+	#include <beginnormal_vertex>
+	#include <morphnormal_vertex>
+	#include <skinbase_vertex>
+	#include <skinnormal_vertex>
+	#include <defaultnormal_vertex>
+	#include <normal_vertex>
+	#include <begin_vertex>
+	#include <morphtarget_vertex>
+	#include <skinning_vertex>
+	#include <displacementmap_vertex>
+	#include <project_vertex>
+	#include <logdepthbuf_vertex>
+	#include <clipping_planes_vertex>
+	vViewPosition = - mvPosition.xyz;
+	#include <worldpos_vertex>
+	#include <envmap_vertex>
+	#include <shadowmap_vertex>
+	#include <fog_vertex>
+
+  vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz;
+	vWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;
+}

+ 14 - 0
resources/shaders/sky-lighting-model-fsh.glsl

@@ -0,0 +1,14 @@
+uniform float time;
+
+
+varying vec3 vWorldPosition;
+varying vec3 vWorldNormal;
+
+
+
+void main() {
+  vec3 viewDir = normalize(vWorldPosition - cameraPosition);
+  vec3 colour = CalculateSkyLighting(viewDir, vWorldNormal);
+
+  gl_FragColor = vec4(colour, 1.0);
+}

+ 15 - 0
resources/shaders/sky-lighting-model-vsh.glsl

@@ -0,0 +1,15 @@
+varying vec3 vWorldPosition;
+varying vec3 vWorldNormal;
+varying vec2 vUv;
+
+
+void main() {
+  vec4 localSpacePosition = vec4(position, 1.0);
+  vec4 worldPosition = modelMatrix * localSpacePosition;
+
+  vWorldPosition = worldPosition.xyz;
+  vWorldNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
+  vUv = uv;
+
+  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+}

+ 54 - 0
resources/shaders/sky.glsl

@@ -0,0 +1,54 @@
+
+vec3 SKY_lighterBlue = vec3(0.39, 0.57, 0.86) * 0.35;
+vec3 SKY_midBlue = vec3(0.1, 0.11, 0.1) * 0.5;
+vec3 SKY_darkerBlue = vec3(0.0);
+vec3 SKY_SUN_COLOUR = vec3(0.5);
+vec3 SKY_SUN_GLOW_COLOUR = vec3(0.15, 0.2, 0.25);
+vec3 SKY_FOG_GLOW_COLOUR = vec3(vec3(0.75, 0.75, 1.0) * 0.15);
+float SKY_POWER = 16.0;
+float SUN_POWER = 128.0;
+float SKY_DARK_POWER = 2.0;
+float SKY_fogScatterDensity = 0.0002;
+float SKY_fogExtinctionDensity = 0.006;
+vec3 SUN_DIR = vec3(-1.0, 0.45, 1.0);
+
+// This is just a bunch of nonsense since I didn't want to implement a full
+// sky model. It's just a simple gradient with a sun and some fog.
+vec3 CalculateSkyLighting(vec3 viewDir, vec3 normalDir) {
+  vec3 lighterBlue = col3(GAMMA_TO_LINEAR(SKY_lighterBlue));
+  vec3 midBlue = col3(GAMMA_TO_LINEAR(SKY_midBlue));
+  vec3 darkerBlue = col3(GAMMA_TO_LINEAR(SKY_darkerBlue));
+
+  vec3 SUN_COLOUR = col3(GAMMA_TO_LINEAR(SKY_SUN_COLOUR));
+  vec3 SUN_GLOW_COLOUR = col3(GAMMA_TO_LINEAR(SKY_SUN_GLOW_COLOUR));
+
+  float viewDirY = linearstep(-0.01, 1.0, viewDir.y);
+
+  float skyGradientMixFactor = saturate(viewDirY);
+  vec3 skyGradient = mix(darkerBlue, lighterBlue, exp(-sqrt(saturate(viewDirY)) * 2.0));
+
+  vec3 sunDir = normalize(SUN_DIR);
+  float mu = 1.0 - saturate(dot(viewDir, sunDir));
+
+  vec3 colour = skyGradient + SUN_GLOW_COLOUR * saturate(exp(-sqrt(mu) * 10.0)) * 0.75;
+  colour += SUN_COLOUR * smoothstep(0.9997, 0.9998, 1.0 - mu);
+
+  colour = oklabToRGB(colour);
+
+  return colour;
+}
+
+vec3 CalculateSkyFog(vec3 normalDir) {
+  return CalculateSkyLighting(normalDir, normalDir);
+}
+
+vec3 CalculateFog(vec3 baseColour, vec3 viewDir, float sceneDepth) {
+	vec3 fogSkyColour = CalculateSkyFog(-viewDir);
+	float fogDepth = sceneDepth * sceneDepth;
+
+	float fogScatterFactor = exp(-SKY_fogScatterDensity * SKY_fogScatterDensity * fogDepth);
+	float fogExtinctionFactor = exp(-SKY_fogExtinctionDensity * SKY_fogExtinctionDensity * fogDepth);
+
+	vec3 finalColour = baseColour * fogExtinctionFactor + fogSkyColour * (1.0 - fogScatterFactor);
+  return finalColour;
+}

+ 149 - 0
resources/shaders/terrain-lighting-model-fsh.glsl

@@ -0,0 +1,149 @@
+#define STANDARD
+#ifdef PHYSICAL
+	#define IOR
+	#define USE_SPECULAR
+#endif
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform float roughness;
+uniform float metalness;
+uniform float opacity;
+#ifdef IOR
+	uniform float ior;
+#endif
+#ifdef USE_SPECULAR
+	uniform float specularIntensity;
+	uniform vec3 specularColor;
+	#ifdef USE_SPECULAR_COLORMAP
+		uniform sampler2D specularColorMap;
+	#endif
+	#ifdef USE_SPECULAR_INTENSITYMAP
+		uniform sampler2D specularIntensityMap;
+	#endif
+#endif
+#ifdef USE_CLEARCOAT
+	uniform float clearcoat;
+	uniform float clearcoatRoughness;
+#endif
+#ifdef USE_IRIDESCENCE
+	uniform float iridescence;
+	uniform float iridescenceIOR;
+	uniform float iridescenceThicknessMinimum;
+	uniform float iridescenceThicknessMaximum;
+#endif
+#ifdef USE_SHEEN
+	uniform vec3 sheenColor;
+	uniform float sheenRoughness;
+	#ifdef USE_SHEEN_COLORMAP
+		uniform sampler2D sheenColorMap;
+	#endif
+	#ifdef USE_SHEEN_ROUGHNESSMAP
+		uniform sampler2D sheenRoughnessMap;
+	#endif
+#endif
+#ifdef USE_ANISOTROPY
+	uniform vec2 anisotropyVector;
+	#ifdef USE_ANISOTROPYMAP
+		uniform sampler2D anisotropyMap;
+	#endif
+#endif
+varying vec3 vViewPosition;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <iridescence_fragment>
+#include <cube_uv_reflection_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_physical_pars_fragment>
+#include <fog_pars_fragment>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+#include <lights_physical_pars_fragment>
+#include <transmission_pars_fragment>
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <clearcoat_pars_fragment>
+#include <iridescence_pars_fragment>
+#include <roughnessmap_pars_fragment>
+#include <metalnessmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPosition;
+varying vec3 vTerrainColour;
+
+uniform sampler2D grid;
+
+
+void main() {
+	#include <clipping_planes_fragment>
+	
+  vec3 viewDir = normalize(cameraPosition - vWorldPosition);
+
+	vec4 diffuseColor = vec4( diffuse, opacity );
+	diffuseColor.rgb *= vTerrainColour;
+	float grid1 = texture(grid, vWorldPosition.xz * 0.1).r;
+	float grid2 = texture(grid, vWorldPosition.xz * 1.0).r;
+
+	float gridHash1 = hash12(floor(vWorldPosition.xz * 1.0));
+
+	vec3 gridColour = mix(vec3(0.5 + remap(gridHash1, 0.0, 1.0, -0.2, 0.2)), vec3(0.0625), grid2);
+	gridColour = mix(gridColour, vec3(0.0), grid1);
+
+	// diffuseColor.rgb = gridColour;
+
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <roughnessmap_fragment>
+	#include <metalnessmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+	#include <clearcoat_normal_fragment_begin>
+	#include <clearcoat_normal_fragment_maps>
+	#include <emissivemap_fragment>
+	#include <lights_physical_fragment>
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;
+	vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;
+	#include <transmission_fragment>
+	vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;
+	#ifdef USE_SHEEN
+		float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );
+		outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular;
+	#endif
+	#ifdef USE_CLEARCOAT
+		float dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );
+		vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );
+		outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;
+	#endif
+
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	// #include <fog_fragment>
+
+	gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth);
+
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 84 - 0
resources/shaders/terrain-lighting-model-vsh.glsl

@@ -0,0 +1,84 @@
+#define STANDARD
+varying vec3 vViewPosition;
+#ifdef USE_TRANSMISSION
+	varying vec3 vWorldPosition;
+#endif
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+uniform sampler2D heightmap;
+uniform vec4 heightParams;
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPosition;
+varying vec3 vTerrainColour;
+
+
+void main() {
+	#include <uv_vertex>
+	#include <color_vertex>
+	#include <morphcolor_vertex>
+	#include <beginnormal_vertex>
+	#include <morphnormal_vertex>
+	#include <skinbase_vertex>
+	#include <skinnormal_vertex>
+	#include <defaultnormal_vertex>
+	#include <normal_vertex>
+	#include <begin_vertex>
+
+  vec4 heightSample = texture2D(heightmap, uv);
+  float height = heightSample.x * heightParams.z - heightParams.w;
+
+  vec3 terrainWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;
+  float distToVertex = distance(cameraPosition, terrainWorldPos);
+
+  float isSandy = linearstep(-11.0, -14.0, height);
+
+  vec2 hashGrassColour = hash22(vec2(position.x, position.z));
+  // vec3 baseColour = mix(vec3(0.02, 0.2, 0.01), vec3(0.025, 0.1, 0.01), hashGrassColour.x);
+  // vec3 tipColour = mix(vec3(0.5, 0.7, 0.1), vec3(0.4, 0.5, 0.025), hashGrassColour.y);
+  // vec3 tipColour = vec3(0.2, 0.35, 0.05);
+  // vec3 baseColour = vec3(0.02, 0.2, 0.01);
+  // vec3 tipColour = vec3(0.5, 0.5, 0.1);
+
+  vec3 baseColour = GAMMA_TO_LINEAR(vec3(0.05, 0.2, 0.01));
+  vec3 tipColour = GAMMA_TO_LINEAR(vec3(0.3, 0.3, 0.1));
+
+  float aoDist = smoothstep(25.0, 50.0, distToVertex);
+  float colourDist = smoothstep(50.0, 100.0, distToVertex);
+  float ao = mix(0.125, 1.0, aoDist);
+  ao = mix(ao, 1.0, isSandy);
+
+  vec3 SAND_COLOUR = GAMMA_TO_LINEAR(vec3(0.6, 0.4, 0.2));
+
+  vTerrainColour = mix(baseColour, tipColour, colourDist);
+  vTerrainColour = mix(vTerrainColour, SAND_COLOUR, smoothstep(-11.0, -14.0, height));
+  vTerrainColour *= ao;
+
+
+	#include <morphtarget_vertex>
+	#include <skinning_vertex>
+	#include <displacementmap_vertex>
+	#include <project_vertex>
+	#include <logdepthbuf_vertex>
+	#include <clipping_planes_vertex>
+	vViewPosition = - mvPosition.xyz;
+	#include <worldpos_vertex>
+	#include <shadowmap_vertex>
+	#include <fog_vertex>
+#ifdef USE_TRANSMISSION
+	vWorldPosition = worldPosition.xyz;
+#endif
+
+  vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz;
+	vWorldPosition = worldPosition.xyz;
+}

+ 162 - 0
resources/shaders/water-lighting-model-fsh.glsl

@@ -0,0 +1,162 @@
+#define PHONG
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform vec3 specular;
+uniform float shininess;
+uniform float opacity;
+#include <common>
+#include <packing>
+#include <dithering_pars_fragment>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <map_pars_fragment>
+#include <alphamap_pars_fragment>
+#include <alphatest_pars_fragment>
+#include <alphahash_pars_fragment>
+#include <aomap_pars_fragment>
+#include <lightmap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <envmap_common_pars_fragment>
+#include <envmap_pars_fragment>
+#include <fog_pars_fragment>
+#include <bsdfs>
+#include <lights_pars_begin>
+#include <normal_pars_fragment>
+#include <lights_phong_pars_fragment>
+#include <shadowmap_pars_fragment>
+#include <bumpmap_pars_fragment>
+#include <normalmap_pars_fragment>
+#include <specularmap_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+uniform mat4 projectionMatrix;
+uniform mat4 inverseProjectionMatrix;
+
+uniform sampler2D colourTexture;
+uniform vec2 resolution;
+uniform float time;
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPos;
+
+vec2 ViewToScreen(vec3 pos) {
+	vec4 clipPos = projectionMatrix * vec4(pos, 1.0);
+	vec3 ndcPos = clipPos.xyz / clipPos.w;
+	return ndcPos.xy * 0.5 + 0.5;
+}
+
+vec3 ScreenToView(vec2 uv) {
+	vec2 ndcPos = uv * 2.0 - 1.0;
+	vec4 clipPos = vec4(ndcPos, 0.0, 1.0);
+	vec4 viewPos = inverse(projectionMatrix) * clipPos;
+	return vec3(viewPos.xy / viewPos.w, 1.0);
+}
+
+// TODO: This was lazily done to just get it working.
+// Do not use this for anything other than this demo.
+vec4 TraceRay(vec3 rayWorldOrigin, vec3 rayWorldDir) {
+	const int MAX_COUNT = 32;
+
+	vec3 rayViewPos = (viewMatrix * vec4(rayWorldOrigin, 1.0)).xyz;
+	vec3 rayViewDir = (viewMatrix * vec4(rayWorldDir, 0.0)).xyz;
+
+	vec3 rayPos = rayViewPos;
+	vec3 rayDir = rayViewDir;
+
+	float dist = 0.01;
+	for (int i = 0; i < MAX_COUNT; i++) {
+		rayPos += rayDir * dist;
+		dist *= 1.5;
+
+		vec2 coords = ViewToScreen(rayPos);
+		float depthAtCoord = texture(colourTexture, coords).w;
+		float rayDepth = -rayPos.z;
+
+		if (depthAtCoord < rayDepth) {
+			if (depthAtCoord < -rayViewPos.z) {
+				continue;
+			}
+			if (coords.y < 0.0 || coords.y > 1.0) {
+				break;
+			}
+			return vec4(texture(colourTexture, coords).xyz, 1.0);
+		}
+	}
+	return vec4(0.0);
+}
+
+vec3 WaterNormal2(vec3 pos, float falloff) {
+	vec3 noiseNormal = FBM_D_1_4(vec3(pos.xz * 0.4, time * 0.8), 1).yzw;
+
+	return normalize(vec3(0.0, 1.0, 0.0) + noiseNormal * 0.5 * falloff);
+}
+
+void main() {
+	#include <clipping_planes_fragment>
+	vec4 diffuseColor = vec4( diffuse, opacity );
+
+	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
+	vec3 totalEmissiveRadiance = emissive;
+	#include <logdepthbuf_fragment>
+	#include <map_fragment>
+	#include <color_fragment>
+	#include <alphamap_fragment>
+	#include <alphatest_fragment>
+	#include <alphahash_fragment>
+	#include <specularmap_fragment>
+	#include <normal_fragment_begin>
+	#include <normal_fragment_maps>
+	#include <emissivemap_fragment>
+
+	#include <lights_phong_fragment>
+
+	#include <lights_fragment_begin>
+	#include <lights_fragment_maps>
+	#include <lights_fragment_end>
+	#include <aomap_fragment>
+	vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
+
+  vec2 coords = gl_FragCoord.xy / resolution;
+  float sceneZ = texture(colourTexture, coords).w;
+  float waterZ = vViewPosition.z;
+	float waterDepth = sceneZ - waterZ;
+  float waterFalloff = smoothstep(0.0, 2.0, waterDepth);
+
+	float dist = distance(cameraPosition, vWorldPos);
+	vec3 viewDir = normalize(cameraPosition - vWorldPos);
+	// vec3 waterNormal = WaterNormal(vWorldPos, sceneZ);
+
+	float waterNormalFalloff = pow(saturate(dot(vec3(0.0, 1.0, 0.0), viewDir)), 2.0);// * easeIn(linearstep(100.0, 0.0, dist), 3.0);
+	vec3 waterNormal = WaterNormal2(vWorldPos, waterNormalFalloff);
+
+
+	float fresnel = pow(saturate(dot(waterNormal, viewDir)), 1.0);
+
+	vec3 reflectedDir = reflect(-viewDir, waterNormal);
+	vec3 refractDir = refract(-viewDir, waterNormal, 1.0 / 1.33);
+
+	vec4 tracedReflection = TraceRay(vWorldPos, reflectedDir);
+	vec3 tracedSky = CalculateSkyLighting(reflectedDir, viewDir);
+	float edgeFalloff = smoothstep(0.5, 0.3, abs(coords.x - 0.5));
+	edgeFalloff = remap(edgeFalloff, 0.0, 1.0, 0.25, 1.0);
+	vec3 reflectedColour = mix(tracedSky, tracedReflection.xyz, tracedReflection.w * edgeFalloff);
+  vec4 colourSample = texture(colourTexture, coords + noise23(vec3(vWorldPos.xz, time)) * 0.05 * waterNormalFalloff);
+	// vec4 tracedRefraction = TraceRay(vWorldPos, refractDir);
+
+	vec3 waterColour = mix(colourSample.xyz, vec3(0.2, 0.2, 0.5), waterFalloff);
+	vec4 froth = vec4(vec3(1.0), remap(noise13(vec3(vWorldPos.xz * 10.0, time * 2.0)), -1.0, 1.0, 0.0, 1.0)) * smoothstep(0.25, 0.0, waterDepth);
+	waterColour = mix(waterColour, froth.xyz, froth.w);
+
+  outgoingLight = mix(reflectedColour, waterColour, fresnel);
+	outgoingLight = mix(colourSample.xyz, outgoingLight, smoothstep(0.0, 0.1, waterDepth));
+
+
+	#include <envmap_fragment>
+	#include <opaque_fragment>
+	#include <tonemapping_fragment>
+	#include <colorspace_fragment>
+	#include <fog_fragment>
+	#include <premultiplied_alpha_fragment>
+	#include <dithering_fragment>
+}

+ 46 - 0
resources/shaders/water-lighting-model-vsh.glsl

@@ -0,0 +1,46 @@
+#define PHONG
+varying vec3 vViewPosition;
+#include <common>
+#include <uv_pars_vertex>
+#include <displacementmap_pars_vertex>
+#include <envmap_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <normal_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <shadowmap_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+varying vec3 vWorldNormal;
+varying vec3 vWorldPos;
+
+void main() {
+	#include <uv_vertex>
+	#include <color_vertex>
+	#include <morphcolor_vertex>
+	#include <beginnormal_vertex>
+
+	#include <morphnormal_vertex>
+	#include <skinbase_vertex>
+	#include <skinnormal_vertex>
+	#include <defaultnormal_vertex>
+	#include <normal_vertex>
+	#include <begin_vertex>
+
+	#include <morphtarget_vertex>
+	#include <skinning_vertex>
+	#include <displacementmap_vertex>
+	#include <project_vertex>
+	#include <logdepthbuf_vertex>
+	#include <clipping_planes_vertex>
+	vViewPosition = - mvPosition.xyz;
+	#include <worldpos_vertex>
+	#include <envmap_vertex>
+	#include <shadowmap_vertex>
+	#include <fog_vertex>
+
+	vWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
+	vWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;
+}

+ 26 - 0
resources/shaders/water-texture-fsh.glsl

@@ -0,0 +1,26 @@
+#include <common>
+#include <packing>
+
+uniform sampler2D colourTexture;
+uniform sampler2D depthTexture;
+uniform vec3 nearFar;
+
+varying vec2 vUvs;
+
+float GetDepth(float depthSample) {
+  float nf = nearFar.x;
+  float f_sub_n = nearFar.y;
+  float f = nearFar.z;
+
+  float z_final = depthSample;
+  return nf / (f_sub_n * z_final - f);
+}
+
+void main() {
+  vec4 colourSample = texture(colourTexture, vUvs);
+  float depthSample = texture(depthTexture, vUvs).r;
+
+  depthSample = -GetDepth(depthSample);
+
+  gl_FragColor = vec4(colourSample.xyz, depthSample);
+}

+ 12 - 0
resources/shaders/water-texture-vsh.glsl

@@ -0,0 +1,12 @@
+
+varying vec2 vUvs;
+
+
+void main() {
+  vec4 localSpacePosition = vec4(position, 1.0);
+  vec4 worldPosition = modelMatrix * localSpacePosition;
+
+  vUvs = uv;
+
+  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+}

+ 21 - 0
resources/shaders/wind-lighting-model-fsh.glsl

@@ -0,0 +1,21 @@
+#define PHONG
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform vec3 specular;
+uniform float shininess;
+uniform float opacity;
+
+
+varying vec2 vUVs;
+varying float vWindParams;
+
+uniform sampler2D diffuseTexture;
+
+void main() {
+	vec4 colour = texture(diffuseTexture, vUVs).xyzx;
+
+	colour.xyz *= vec3(0.5);
+	colour.w *= vWindParams;
+
+	gl_FragColor = colour;
+}

+ 81 - 0
resources/shaders/wind-lighting-model-vsh.glsl

@@ -0,0 +1,81 @@
+
+
+#define PHONG
+
+
+varying vec2 vUVs;
+varying float vWindParams;
+
+uniform vec2 dustSize;
+uniform float time;
+
+uniform sampler2D heightmap;
+uniform vec3 heightmapParams;
+
+attribute vec3 offset;
+
+
+const float PI = 3.1415926535897932384626433832795;
+
+void main() {
+
+vec3 transformed = vec3( position );
+#ifdef USE_ALPHAHASH
+	vPosition = vec3( position );
+#endif
+
+  {
+    vec3 baseWorldPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + offset;
+    float hashSample = hash12(baseWorldPosition.xz);
+
+    float hashedTime = time + hashSample * 100.0;
+
+    float windDir = noise12(baseWorldPosition.xz * 0.05 + 0.5 * time);
+    // float windNoiseSample = noise12(grassBladeWorldPos.xz * 0.25 + time * 1.1);
+    // float windLeanAngle = saturate(remap(windNoiseSample, -1.0, 1.0, 0.25, 1.0));
+    // windLeanAngle = easeIn(windLeanAngle, 2.0) * 1.5;
+    vec3 windAxis = vec3(sin(windDir), 0.0, -cos(windDir));
+
+    const float TIME_REPEAT_PERIOD = 4.0;
+    float repeatingTime = mod(hashedTime, TIME_REPEAT_PERIOD);
+    float fadeInOut = (
+        smoothstep(0.0, TIME_REPEAT_PERIOD * 0.25, repeatingTime) *
+        smoothstep(TIME_REPEAT_PERIOD, TIME_REPEAT_PERIOD * 0.75, repeatingTime));
+
+    vec3 windOffset = offset + windAxis * repeatingTime * 5.0;
+
+    vec3 scaledPosition = position;
+    scaledPosition.xy *= dustSize;
+
+    vec3 scaledOffsetPosition = scaledPosition + windOffset;
+
+    vec3 worldPosition = (modelMatrix * vec4(scaledOffsetPosition, 1.0)).xyz;
+
+    vec3 z = normalize(cameraPosition - worldPosition);
+    vec3 x = normalize(cross(vec3(0.0, 1.0, 0.0), z));
+    vec3 y = normalize(cross(z, x));
+    mat3 alignMatrix = mat3(x, y, z);
+    transformed = alignMatrix * scaledPosition + windOffset;
+
+    vec2 heightmapUV = vec2(
+        remap(worldPosition.x, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 0.0, 1.0),
+        remap(worldPosition.z, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 1.0, 0.0));
+    float terrainHeight = texture2D(heightmap, heightmapUV).x * heightmapParams.x - heightmapParams.y;
+    transformed.y += terrainHeight;
+
+    vWindParams = fadeInOut;
+
+    float randomAngle = remap(hashSample, 0.0, 1.0, 0.0, 2.0 * PI);
+    vUVs = rotate2D(randomAngle) * uv;
+
+    // vec3 worldCenter = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + windOffset;
+    // vec3 viewCenter = normalize(worldCenter - cameraPosition);
+    // vec3 viewXZ = -normalize(vec3(viewCenter.x, 0.0, viewCenter.z));
+
+    // float i = floor(16.0 * (atan(viewXZ.z, viewXZ.x) + PI) / (2.0 * PI));
+    // float j = floor(16.0 * offset.w / (2.0 * PI));
+    // vBillboardLayer = vec2(i * 16.0 + j, smoothstep(350.0, 300.0, dist));
+  }
+
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
+}

BIN
resources/textures/butterfly.png


BIN
resources/textures/dust.png


BIN
resources/textures/grass.png


BIN
resources/textures/grid.png


BIN
resources/textures/moth.png


BIN
resources/textures/terrain.png


BIN
resources/textures/whitesquare.png


+ 63 - 0
src/base/entity-manager.js

@@ -0,0 +1,63 @@
+import * as entity from './entity.js';
+import * as passes from "./passes.js";
+
+
+const ROOT_ = '__root__';
+
+export class EntityManager {
+  static #instance_ = null;
+
+  #root_;
+  #entitiesMap_;
+
+  static Init() {
+    this.#instance_ = new EntityManager();
+    this.#instance_.#CreateRoot_();
+    return this.#instance_;
+  }
+
+  static get Instance() {
+    return this.#instance_;
+  }
+
+  constructor() {
+    this.#entitiesMap_ = {};
+    this.#root_ = null;
+  }
+
+  #CreateRoot_() {
+    this.#root_ = new entity.Entity(ROOT_);
+    this.#root_.Init();
+  }
+
+  Remove(n) {
+    delete this.#entitiesMap_[n];
+  }
+
+  Get(n) {
+    return this.#entitiesMap_[n];
+  }
+
+  Add(child, parent) {
+    this.#entitiesMap_[child.Name] = child;
+
+    // Root check
+    if (child.ID == this.#root_.ID) {
+      parent = null;
+    } else {
+      parent = parent ? parent : this.#root_;
+    }
+
+    child.SetParent(parent);
+  }
+
+  Update(timeElapsed) {
+    for (let i = passes.Passes.PASSES_MIN; i <= passes.Passes.PASSES_MAX; i = i << 1) {
+      this.UpdatePass_(timeElapsed, i);
+    }
+  }
+
+  UpdatePass_(timeElapsedS, pass) {
+    this.#root_.Update(timeElapsedS, pass);
+  }
+}

+ 352 - 0
src/base/entity.js

@@ -0,0 +1,352 @@
+import {THREE} from './three-defs.js';
+
+import * as entity_manager from './entity-manager.js';
+import * as passes from './passes.js';
+
+
+const SCALE_1_ = new THREE.Vector3(1, 1, 1);
+
+let IDS_ = 0;
+
+export class Entity {
+  constructor(name) {
+    IDS_ += 1;
+
+    this.id_ = IDS_;
+    this.name_ = name ? name : this.GenerateName_();
+    this.components_ = {};
+    this.attributes_ = {};
+
+    this.transform_ = new THREE.Matrix4();
+    this.transform_.identity();
+    this.worldTransform_ = new THREE.Matrix4();
+    this.worldTransform_.identity();
+
+    this.position_ = new THREE.Vector3();
+    this.rotation_ = new THREE.Quaternion();
+
+    this.handlers_ = {};
+    this.parent_ = null;
+    this.dead_ = false;
+    this.active_ = true;
+
+    this.childrenActive_ = [];
+    this.children_ = [];
+  }
+
+  Destroy_() {
+    for (let c of this.children_) {
+      c.Destroy_();
+    }
+    for (let k in this.components_) {
+      this.components_[k].Destroy();
+    }
+    this.childrenActive_ = [];
+    this.children_ = [];
+    this.components_ = {};
+    this.parent_ = null;
+    this.handlers_ = {};
+    this.Manager.Remove(this.name_);
+  }
+
+  GenerateName_() {
+    return '__name__' + this.id_;
+  }
+
+  RegisterHandler_(n, h) {
+    if (!(n in this.handlers_)) {
+      this.handlers_[n] = [];
+    }
+    this.handlers_[n].push(h);
+  }
+
+  UnregisterHandler_(n, h) {
+    this.handlers_[n] = this.handlers_[n].filter(c => c != h);
+  }
+
+  AddChild_(e) {
+    this.children_.push(e);
+    this.RefreshActiveChildren_();
+  }
+
+  RemoveChild_(e) {
+    this.children_ = this.children_.filter(c => c != e);
+    this.RefreshActiveChildren_();
+  }
+
+  SetParent(p) {
+    if (this.parent_) {
+      this.parent_.RemoveChild_(this);
+    }
+
+    this.parent_ = p;
+
+    if (this.parent_) {
+      this.parent_.AddChild_(this);
+    }
+  }
+
+  get Name() {
+    return this.name_;
+  }
+
+  get ID() {
+    return this.id_;
+  }
+
+  get Manager() {
+    return entity_manager.EntityManager.Instance;
+  }
+
+  get Parent() {
+    return this.parent_;
+  }
+
+  get Attributes() {
+    return this.attributes_;
+  }
+
+  get Children() {
+    return [...this.children_];
+  }
+
+  get IsDead() {
+    return this.dead_;
+  }
+
+  get IsActive() {
+    return this.active_;
+  }
+
+  RefreshActiveChildren_() {
+    this.childrenActive_ = this.children_.filter(c => c.IsActive);
+  }
+
+  SetActive(active) {
+    this.active_ = active;
+    if (this.parent_) {
+      this.parent_.RefreshActiveChildren_();
+    }
+  }
+
+  SetDead() {
+    this.dead_ = true;
+  }
+
+  AddComponent(c) {
+    c.SetParent(this);
+    this.components_[c.NAME] = c;
+
+    c.InitComponent();
+  }
+
+  Init(parent) {
+    this.Manager.Add(this, parent);
+    this.InitEntity_();
+  }
+
+  InitEntity_() {
+    for (let k in this.components_) {
+      this.components_[k].InitEntity();
+    }
+    this.SetActive(this.active_);
+  }
+
+  GetComponent(n) {
+    return this.components_[n];
+  }
+
+  FindEntity(name) {
+    return this.Manager.Get(name);
+  }
+
+  FindChild(name, recursive) {
+    let result = null;
+
+    for (let i = 0, n = this.children_.length; i < n; ++i) {
+      if (this.children_[i].Name == name) {
+        result = this.children_[i];
+        break;
+      }
+
+      if (recursive) {
+        result = this.children_[i].FindChild(name, recursive);
+        if (result) {
+          break;
+        }
+      }
+    }
+    return result;
+  }
+
+  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.transform_.compose(this.position_, this.rotation_, SCALE_1_);
+    this.Broadcast({
+      topic: 'update.position',
+      value: this.position_,
+    });
+  }
+
+  SetQuaternion(r) {
+    this.rotation_.copy(r);
+    this.transform_.compose(this.position_, this.rotation_, SCALE_1_);
+    this.Broadcast({
+      topic: 'update.rotation',
+      value: this.rotation_,
+    });
+  }
+
+  get Transform() {
+    return this.transform_;
+  }
+
+  get WorldTransform() {
+    const m = this.worldTransform_.copy(this.transform_);
+    if (this.parent_) {
+      m.multiply(this.parent_.Transform);
+    }
+    return m;
+  }
+
+  GetWorldPosition(target) {
+    target.setFromMatrixPosition(this.WorldTransform);
+    return target;
+  }
+
+  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 Left() {
+    const forward = new THREE.Vector3(-1, 0, 0);
+    forward.applyQuaternion(this.rotation_);
+    return forward;
+  }
+
+  get Up() {
+    const forward = new THREE.Vector3(0, 1, 0);
+    forward.applyQuaternion(this.rotation_);
+    return forward;
+  }
+
+  UpdateComponents_(timeElapsed, pass) {
+    for (let k in this.components_) {
+      const c = this.components_[k];
+      if (c.Pass == pass) {
+        c.Update(timeElapsed);
+      }
+    }
+  }
+
+  UpdateChildren_(timeElapsed, pass) {
+    const dead = [];
+    const alive = [];
+    for (let i = 0; i < this.childrenActive_.length; ++i) {
+      const e = this.childrenActive_[i];
+
+      e.Update(timeElapsed, pass);
+
+      if (e.IsDead) {
+        dead.push(e);
+      } else {
+        alive.push(e);
+      }
+    }
+
+    let hasDead = false;
+    for (let i = 0; i < dead.length; ++i) {
+      const e = dead[i];
+
+      e.Destroy_();
+      hasDead = true;
+    }
+
+    if (hasDead) {
+      this.children_ = this.children_.filter(c => !c.IsDead);
+      this.RefreshActiveChildren_();
+    }
+  }
+
+  Update(timeElapsed, pass) {
+    this.UpdateComponents_(timeElapsed, pass);
+    this.UpdateChildren_(timeElapsed, pass);
+  }
+};
+
+
+export class Component {
+  get NAME() {
+    console.error('Unnamed Component: ' + this.constructor.name);
+    return '__UNNAMED__';
+  }
+
+  constructor() {
+    this.parent_ = null;
+    this.pass_ = passes.Passes.DEFAULT;
+  }
+
+  Destroy() {}
+  InitComponent() {}
+  InitEntity() {}
+  Update(timeElapsed) {}
+
+  SetParent(parent) {
+    this.parent_ = parent;
+  }
+
+  SetPass(pass) {
+      this.pass_ = pass;
+  }
+
+  get Pass() {
+    return this.pass_;
+  }
+
+  GetComponent(name) {
+    return this.parent_.GetComponent(name);
+  }
+
+  get Manager() {
+    return this.Parent.Manager;
+  }
+
+  get Parent() {
+    return this.parent_;
+  }
+
+  FindEntity(name) {
+    return this.Manager.Get(name);
+  }
+
+  Broadcast(m) {
+    this.parent_.Broadcast(m);
+  }
+
+  RegisterHandler_(name, cb) {
+    this.parent_.RegisterHandler_(name, cb);
+  }
+};

+ 260 - 0
src/base/load-controller.js

@@ -0,0 +1,260 @@
+import {THREE, FBXLoader, GLTFLoader, SkeletonUtils} from './three-defs.js';
+
+import * as entity from "./entity.js";
+import * as shaders from '../game/render/shaders.js'
+
+
+export const load_controller = (() => {
+
+  class LoadController extends entity.Component {
+    static CLASS_NAME = 'LoadController';
+
+    get NAME() {
+      return LoadController.CLASS_NAME;
+    }
+
+    constructor() {
+      super();
+
+      this.textures_ = {};
+      this.models_ = {};
+      this.sounds_ = {};
+      this.playing_ = [];
+    }
+
+    AddModel(model, path, name) {
+      const group = new THREE.Group();
+      group.add(model);
+
+      const fullName = path + name;
+      this.models_[fullName] = {
+        asset: { scene: group, animations: [] },
+        queue: null
+    };
+    }
+
+    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);
+      }
+    }
+
+    #FinalizeLoad_(group) {
+      const threejsController = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+      group.traverse((obj) => {
+          if (!(obj instanceof THREE.Mesh)) {
+            return;
+          }
+
+          let materials = (
+              obj.material instanceof Array ?
+                  obj.material : [obj.material]);
+
+          if (obj.geometry) {
+            obj.geometry.computeBoundingBox();
+          }
+
+          for (let mat of materials) {
+              if (mat) {
+              }
+          }
+      });
+  }
+
+    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 {
+        const fullName = path + name;
+        if (this.models_[fullName]) {
+          const clone = this.models_[fullName].asset.scene.clone();
+
+          this.#FinalizeLoad_(clone);
+          
+          onLoad({scene: clone});
+          return;
+        }
+        // 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 = SkeletonUtils.clone(this.models_[name].asset);
+            q(clone);
+          }
+        });
+      } else if (this.models_[name].asset == null) {
+        this.models_[name].queue.push(onLoad);
+      } else {
+        const clone = SkeletonUtils.clone(this.models_[name].asset);
+        onLoad(clone);
+      }
+    }
+
+    #ConvertToGameMaterial_(group) {
+      const threejsController = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+      group.traverse((obj) => {
+        if (!(obj instanceof THREE.Mesh)) {
+            return;
+        }
+
+        let materials = (
+            obj.material instanceof Array ?
+                obj.material : [obj.material]);
+
+        for (let mat of materials) {
+            if (mat instanceof THREE.MeshStandardMaterial) {
+              // mat.metalness = 0.0;
+              // obj.material = new shaders.GamePBRMaterial('TREE');
+              obj.material.copy(mat);
+              obj.material = new shaders.GameMaterial('PHONG');
+              // obj.material = new THREE.MeshStandardMaterial();
+              obj.material.map = mat.map;
+              obj.material.color = mat.color;
+              obj.material.normalMap = mat.normalMap;
+              obj.material.vertexColors = mat.vertexColors;
+              obj.material.alphaTest = mat.alphaTest;
+              // obj.material.copy(mat);
+            }
+        }
+    });
+    }
+
+    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;
+
+          this.#ConvertToGameMaterial_(glb.scene);
+
+          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);
+          }
+        });
+      } 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);
+      }
+    }
+
+    LoadSkinnedGLB(path, name, onLoad) {
+      if (!(name in this.models_)) {
+        const loader = new GLTFLoader();
+        loader.setPath(path);
+
+        this.models_[name] = {loader: loader, asset: null, queue: [onLoad]};
+        this.models_[name].loader.load(name, (glb) => {
+          this.models_[name].asset = glb;
+
+          glb.scene.traverse(c => {
+            // HAHAHAH
+            c.frustumCulled = false;
+            // Apparently this doesn't work, so just disable frustum culling.
+            // Bugs... so many bugs...
+
+            // if (c.geometry) {
+            //   // Just make our own, super crappy, super big box
+            //   c.geometry.boundingBox = new THREE.Box3(
+            //       new THREE.Vector3(-50, -50, -50),
+            //       new THREE.Vector3(50, 50, 50));
+            //   c.geometry.boundingSphere = new THREE.Sphere();
+            //   c.geometry.boundingBox.getBoundingSphere(c.geometry.boundingSphere);
+            // }
+          });
+
+          const queue = this.models_[name].queue;
+          this.models_[name].queue = null;
+          for (let q of queue) {
+            const clone = {...glb};
+            clone.scene = SkeletonUtils.clone(clone.scene);
+
+            q(clone);
+          }
+        });
+      } else if (this.models_[name].asset == null) {
+        this.models_[name].queue.push(onLoad);
+      } else {
+        const clone = {...this.models_[name].asset};
+        clone.scene = SkeletonUtils.clone(clone.scene);
+
+        onLoad(clone);
+      }
+    }
+
+    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,
+  };
+})();

+ 99 - 0
src/base/math.js

@@ -0,0 +1,99 @@
+// 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;
+//     },
+//   };
+// })();
+
+
+import MersenneTwister from 'mersenne-twister';
+
+
+let RNG_ = new MersenneTwister();
+
+export function set_seed(seed) {
+  RNG_ = new MersenneTwister(seed);
+}
+
+export function clamp(x, a, b) {
+  return Math.min(Math.max(x, a), b);
+}
+
+export function sat(x) {
+  return Math.min(Math.max(x, 0.0), 1.0);
+}
+
+export function in_range(x, a, b) {
+  return x >= a && x <= b;
+}
+
+export function easeOut(x, t) {
+  return 1.0 - Math.pow(1.0 - x, t);
+}
+
+export function easeIn(x, t) {
+  return Math.pow(x, t);
+}
+
+export function rand_range(a, b) {
+  return RNG_.random() * (b - a) + a;
+}
+
+export function rand_normalish() {
+  const r = RNG_.random() + RNG_.random() + RNG_.random() + RNG_.random();
+  return (r / 4.0) * 2.0 - 1;
+}
+
+export function rand_int(a, b) {
+  return Math.round(RNG_.random() * (b - a) + a);
+}
+
+export function lerp(x, a, b) {
+  return x * (b - a) + a;
+}
+
+export function smoothstep(edge0, edge1, x) {
+  const t = sat((x - edge0) / (edge1 - edge0));
+  return t * t * (3.0 - 2.0 * t);
+}
+
+export function smootherstep(edge0, edge1, x) {
+  const t = sat((x - edge0) / (edge1 - edge0));
+  return (t * t * t * (t * (t * 6 - 15) + 10));
+}

+ 9 - 0
src/base/passes.js

@@ -0,0 +1,9 @@
+export const Passes = {
+  PASSES_MIN: 1 << 0,
+
+  INPUT     : 1 << 0,
+  CAMERA    : 1 << 1,
+  DEFAULT   : 1 << 2,
+
+  PASSES_MAX: 1 << 2,
+};

+ 180 - 0
src/base/render-component.js

@@ -0,0 +1,180 @@
+import {THREE} from './three-defs.js';
+
+import * as entity from './entity.js';
+
+
+export const render_component = (() => {
+
+  class RenderComponent extends entity.Component {
+    static CLASS_NAME = 'RenderComponent';
+
+    get NAME() {
+      return RenderComponent.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+      this.group_ = new THREE.Group();
+      this.target_ = null;
+      this.offset_ = null;
+      this.params_ = params;
+    }
+
+    Destroy() {
+      this.group_.traverse(c => {
+        if (c.material) {
+          c.material.dispose();
+        }
+        if (c.geometry) {
+          c.geometry.dispose();
+        }
+      });
+      this.group_.removeFromParent();
+    }
+
+    InitEntity() {
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      threejs.AddSceneObject(this.group_);
+
+      this.Parent.Attributes.Render = {
+        group: this.group_,
+      };
+
+      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.scene);
+      });
+    }
+
+    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.copy(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;
+            }
+          }
+        }
+
+        c.castShadow = true;
+        c.receiveShadow = true;
+
+        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;
+        }
+      });
+
+      this.Broadcast({
+          topic: 'render.loaded',
+          value: this.target_,
+      });
+    }
+
+    Update(timeInSeconds) {
+    }
+  };
+
+
+  return {
+      RenderComponent: RenderComponent,
+  };
+
+})();

+ 8 - 0
src/base/render-order.js

@@ -0,0 +1,8 @@
+export const render_order = (() => {
+  return {
+    DEFAULT: 0,
+    DECALS: 1,
+    SHIELDS: 2,
+    PARTICLES: 3,
+  };
+})();

+ 175 - 0
src/base/render/bugs-component.js

@@ -0,0 +1,175 @@
+import {THREE} from '../three-defs.js';
+
+import * as shaders from '../../game/render/shaders.js';
+
+import * as entity from "../entity.js";
+
+import * as terrain_component from './terrain-component.js';
+import MersenneTwister from 'mersenne-twister';
+
+
+class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute {
+
+	constructor( array, itemSize, normalized, meshPerAttribute = 1 ) {
+
+		super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute );
+
+		this.isFloat16BufferAttribute = true;
+	}
+};
+
+
+const NUM_BUGS = 8;
+const NUM_SEGMENTS = 2;
+const NUM_VERTICES = (NUM_SEGMENTS + 1) * 2;
+const BUG_SPAWN_RANGE = 40.0;
+const BUG_MAX_DIST = 100.0;
+
+const M_TMP = new THREE.Matrix4();
+const AABB_TMP = new THREE.Box3();
+
+
+export class BugsComponent extends entity.Component {
+  static CLASS_NAME = 'BugsComponent';
+
+  get NAME() {
+    return BugsComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #meshes_;
+  #group_;
+  #totalTime_;
+  #material_;
+  #geometry_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#meshes_ = [];
+    this.#group_ = new THREE.Group();
+    this.#totalTime_ = 0;
+    this.#geometry_ = null;
+  }
+
+  Destroy() {
+    for (let m of this.#meshes_) {
+      m.removeFromParent();
+    }
+    this.#group_.removeFromParent();
+  }
+
+  #CreateGeometry_() {
+    const rng = new MersenneTwister(1);
+
+    const offsets = new Uint16Array(NUM_BUGS * 3);
+    for (let i = 0; i < NUM_BUGS; ++i) {
+      offsets[i*3 + 0] = THREE.DataUtils.toHalfFloat((rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2));
+      offsets[i*3 + 1] = THREE.DataUtils.toHalfFloat(rng.random() * 1.0 + 2.0);
+      offsets[i*3 + 2] = THREE.DataUtils.toHalfFloat((rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2));
+    }
+
+    const plane = new THREE.PlaneGeometry(1, 1, 2, 1);
+
+    const geo = new THREE.InstancedBufferGeometry();
+    geo.instanceCount = NUM_BUGS;
+    geo.setAttribute('position', plane.attributes.position);
+    geo.setAttribute('uv', plane.attributes.uv);
+    geo.setAttribute('normal', plane.attributes.normal);
+    geo.setAttribute('offset', new InstancedFloat16BufferAttribute(offsets, 3));
+    geo.setIndex(plane.index);
+    geo.rotateX(-Math.PI / 2);
+    geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), BUG_SPAWN_RANGE);
+
+    return geo;
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+    this.#geometry_ = this.#CreateGeometry_();
+
+    const textureLoader = new THREE.TextureLoader();
+    const albedo = textureLoader.load('./resources/textures/' + 'moth.png');
+    albedo.colorSpace = THREE.SRGBColorSpace;
+
+    // // this.#grassMaterialLow_.setVec4('grassDraw', new THREE.Vector4(
+    // //     GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0));
+    this.#material_ = new shaders.GameMaterial('BUGS');
+    this.#material_.setVec2('bugsSize', new THREE.Vector2(0.5, 1.25));
+    this.#material_.setVec4('bugsParams', new THREE.Vector4(
+        NUM_SEGMENTS, NUM_VERTICES, 0, 0));
+    this.#material_.setTexture('heightmap', this.#params_.heightmap);
+    this.#material_.setVec3('heightmapParams', new THREE.Vector3(
+        this.#params_.height, this.#params_.offset, this.#params_.dims));
+    this.#material_.map = albedo;
+    this.#material_.shininess = 0;
+    this.#material_.alphaTest = 0.5;
+    this.#material_.side = THREE.DoubleSide;
+
+    threejs.AddSceneObject(this.#group_);
+  }
+
+  #CreateMesh_() {
+    const m = new THREE.Mesh(this.#geometry_, this.#material_);
+    m.receiveShadow = true;
+    m.castShadow = false;
+    m.visible = true;
+
+    this.#meshes_.push(m);
+    this.#group_.add(m);
+
+    return m;
+  }
+
+  Update(timeElapsed) {
+    this.#totalTime_ += timeElapsed;
+
+    this.#material_.setFloat('time', this.#totalTime_);
+
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    const camera = threejs.Camera;
+    const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse));
+
+    const meshes = [...this.#meshes_];
+
+    const baseCellPos = camera.position.clone();
+    baseCellPos.divideScalar(BUG_SPAWN_RANGE);
+    baseCellPos.floor();
+    baseCellPos.multiplyScalar(BUG_SPAWN_RANGE);
+
+    // This is dumb and slow
+    for (let c of this.#group_.children) {
+      c.visible = false;
+    }
+
+    const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME);
+
+    const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z);
+
+    for (let x = -3; x < 3; x++) {
+      for (let z = -3; z < 3; z++) {
+        // Current cell
+        const currentCell = new THREE.Vector3(
+            baseCellPos.x + x * BUG_SPAWN_RANGE, 0, baseCellPos.z + z * BUG_SPAWN_RANGE);
+        currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z);
+
+        AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(BUG_SPAWN_RANGE, 100, BUG_SPAWN_RANGE));
+        const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ);
+        if (distToCell > BUG_MAX_DIST) {
+          continue;
+        }
+
+        if (!frustum.intersectsBox(AABB_TMP)) {
+          continue;
+        }
+
+        const m = meshes.length > 0 ? meshes.pop() : this.#CreateMesh_();
+        m.position.copy(currentCell);
+        m.position.y = 0;
+        m.visible = true;
+      }
+    }
+  }
+}

+ 280 - 0
src/base/render/grass-component.js

@@ -0,0 +1,280 @@
+import {THREE} from '../three-defs.js';
+
+import * as shaders from '../../game/render/shaders.js';
+
+import * as entity from "../entity.js";
+
+import * as terrain_component from './terrain-component.js';
+import * as math from '../math.js';
+
+
+
+class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute {
+
+	constructor( array, itemSize, normalized, meshPerAttribute = 1 ) {
+
+		super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute );
+
+		this.isFloat16BufferAttribute = true;
+	}
+};
+
+const M_TMP = new THREE.Matrix4();
+const S_TMP = new THREE.Sphere();
+const AABB_TMP = new THREE.Box3();
+
+
+const NUM_GRASS = (32 * 32) * 3;
+const GRASS_SEGMENTS_LOW = 1;
+const GRASS_SEGMENTS_HIGH = 6;
+const GRASS_VERTICES_LOW = (GRASS_SEGMENTS_LOW + 1) * 2;
+const GRASS_VERTICES_HIGH = (GRASS_SEGMENTS_HIGH + 1) * 2;
+const GRASS_LOD_DIST = 15;
+const GRASS_MAX_DIST = 100;
+
+const GRASS_PATCH_SIZE = 5 * 2;
+
+const GRASS_WIDTH = 0.1;
+const GRASS_HEIGHT = 1.5;
+
+
+
+export class GrassComponent extends entity.Component {
+  static CLASS_NAME = 'GrassComponent';
+
+  get NAME() {
+    return GrassComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #meshesLow_;
+  #meshesHigh_;
+  #group_;
+  #totalTime_;
+  #grassMaterialLow_;
+  #grassMaterialHigh_;
+  #geometryLow_;
+  #geometryHigh_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#meshesLow_ = [];
+    this.#meshesHigh_ = [];
+    this.#group_ = new THREE.Group();
+    this.#group_.name = "GRASS";
+    this.#totalTime_ = 0;
+    this.#grassMaterialLow_ = null;
+    this.#grassMaterialHigh_ = null;
+    this.#geometryLow_ = null;
+    this.#geometryHigh_ = null;
+  }
+
+  Destroy() {
+    for (let m of this.#meshesLow_) {
+      m.removeFromParent();
+    }
+    for (let m of this.#meshesHigh_) {
+      m.removeFromParent();
+    }
+    this.#group_.removeFromParent();
+  }
+
+  #CreateGeometry_(segments) {
+    math.set_seed(0);
+
+    const VERTICES = (segments + 1) * 2;
+
+    const indices = [];
+    for (let i = 0; i < segments; ++i) {
+      const vi = i * 2;
+      indices[i*12+0] = vi + 0;
+      indices[i*12+1] = vi + 1;
+      indices[i*12+2] = vi + 2;
+
+      indices[i*12+3] = vi + 2;
+      indices[i*12+4] = vi + 1;
+      indices[i*12+5] = vi + 3;
+
+      const fi = VERTICES + vi;
+      indices[i*12+6] = fi + 2;
+      indices[i*12+7] = fi + 1;
+      indices[i*12+8] = fi + 0;
+
+      indices[i*12+9]  = fi + 3;
+      indices[i*12+10] = fi + 1;
+      indices[i*12+11] = fi + 2;
+    }
+
+    const offsets = [];
+    for (let i = 0; i < NUM_GRASS; ++i) {
+      offsets.push(math.rand_range(-GRASS_PATCH_SIZE * 0.5, GRASS_PATCH_SIZE * 0.5));
+      offsets.push(math.rand_range(-GRASS_PATCH_SIZE * 0.5, GRASS_PATCH_SIZE * 0.5));
+      offsets.push(0);
+    }
+
+    const offsetsData = offsets.map(THREE.DataUtils.toHalfFloat);
+
+    const vertID = new Uint8Array(VERTICES*2);
+    for (let i = 0; i < VERTICES*2; ++i) {
+      vertID[i] = i;
+    }
+
+    const geo = new THREE.InstancedBufferGeometry();
+    geo.instanceCount = NUM_GRASS;
+    geo.setAttribute('vertIndex', new THREE.Uint8BufferAttribute(vertID, 1));
+    geo.setAttribute('position', new InstancedFloat16BufferAttribute(offsetsData, 3));
+    geo.setIndex(indices);
+    geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 1 + GRASS_PATCH_SIZE * 2);
+
+    return geo;
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+    this.#grassMaterialLow_ = new shaders.GameMaterial('GRASS');
+    this.#grassMaterialHigh_ = new shaders.GameMaterial('GRASS');
+    this.#grassMaterialLow_.side = THREE.FrontSide;
+    this.#grassMaterialHigh_.side = THREE.FrontSide;
+
+    this.#geometryLow_ = this.#CreateGeometry_(GRASS_SEGMENTS_LOW);
+    this.#geometryHigh_ = this.#CreateGeometry_(GRASS_SEGMENTS_HIGH);
+
+    this.#grassMaterialLow_.setVec2('grassSize', new THREE.Vector2(GRASS_WIDTH, GRASS_HEIGHT));
+    this.#grassMaterialLow_.setVec4('grassParams', new THREE.Vector4(
+        GRASS_SEGMENTS_LOW, GRASS_VERTICES_LOW, this.#params_.height, this.#params_.offset));
+    this.#grassMaterialLow_.setVec4('grassDraw', new THREE.Vector4(
+        GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0));
+    this.#grassMaterialLow_.setTexture('heightmap', this.#params_.heightmap);
+    this.#grassMaterialLow_.setVec4('heightParams', new THREE.Vector4(this.#params_.dims, 0, 0, 0))
+    this.#grassMaterialLow_.setVec3('grassLODColour', new THREE.Vector3(0, 0, 1));
+    this.#grassMaterialLow_.alphaTest = 0.5;
+
+    this.#grassMaterialHigh_.setVec2('grassSize', new THREE.Vector2(GRASS_WIDTH, GRASS_HEIGHT));
+    this.#grassMaterialHigh_.setVec4('grassParams', new THREE.Vector4(
+        GRASS_SEGMENTS_HIGH, GRASS_VERTICES_HIGH, this.#params_.height, this.#params_.offset));
+    this.#grassMaterialHigh_.setVec4('grassDraw', new THREE.Vector4(
+        GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0));
+    this.#grassMaterialHigh_.setTexture('heightmap', this.#params_.heightmap);
+    this.#grassMaterialHigh_.setVec4('heightParams', new THREE.Vector4(this.#params_.dims, 0, 0, 0))
+    this.#grassMaterialHigh_.setVec3('grassLODColour', new THREE.Vector3(1, 0, 0));
+    this.#grassMaterialHigh_.alphaTest = 0.5;
+
+    threejs.AddSceneObject(this.#group_);
+  }
+
+  #CreateMesh_(distToCell) {
+    const meshes = distToCell > GRASS_LOD_DIST ? this.#meshesLow_ : this.#meshesHigh_;
+    if (meshes.length > 1000) {
+      console.log('crap')
+      return null;
+    }
+
+    const geo = distToCell > GRASS_LOD_DIST ? this.#geometryLow_ : this.#geometryHigh_;
+    const mat = distToCell > GRASS_LOD_DIST ? this.#grassMaterialLow_ : this.#grassMaterialHigh_;
+
+    const m = new THREE.Mesh(geo, mat);
+    m.position.set(0, 0, 0);
+    m.receiveShadow = true;
+    m.castShadow = false;
+    m.visible = false;
+
+    meshes.push(m);
+    this.#group_.add(m);
+    return m;
+  }
+
+  Update(timeElapsed) {
+    this.#totalTime_ += timeElapsed;
+
+    this.#grassMaterialLow_.setFloat('time', this.#totalTime_);
+    this.#grassMaterialHigh_.setFloat('time', this.#totalTime_);
+
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    const camera = threejs.Camera;
+    const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse));
+
+    const meshesLow = [...this.#meshesLow_];
+    const meshesHigh = [...this.#meshesHigh_];
+
+    const baseCellPos = camera.position.clone();
+    baseCellPos.divideScalar(GRASS_PATCH_SIZE);
+    baseCellPos.floor();
+    baseCellPos.multiplyScalar(GRASS_PATCH_SIZE);
+
+    // This is dumb and slow
+    for (let c of this.#group_.children) {
+      c.visible = false;
+    }
+
+    const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME);
+
+    const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z);
+    const playerPos = this.FindEntity('player').Position;
+
+    this.#grassMaterialHigh_.setVec3('playerPos', playerPos);
+    // this.#grassMaterialHigh_.setVec3('cameraPos', camera.position);
+    this.#grassMaterialHigh_.setMatrix('viewMatrixInverse', camera.matrixWorld);
+    this.#grassMaterialLow_.setMatrix('viewMatrixInverse', camera.matrixWorld);
+    // this.#grassMaterialLow_.setVec3('cameraPos', camera.position);
+
+
+    // const playerCellPos = this.FindEntity('player').Position.clone();
+    // playerCellPos.divideScalar(GRASS_PATCH_SIZE);
+    // playerCellPos.round();
+    // playerCellPos.multiplyScalar(GRASS_PATCH_SIZE);
+    // const playerCellPos = new THREE.Vector3();
+
+    // const m = meshesHigh.length > 0 ? meshesHigh.pop() : this.#CreateMesh_(0);
+    // m.position.copy(playerCellPos);
+    // m.position.y = 0;
+    // m.visible = true;
+
+    let totalGrass = 0;
+    let totalVerts = 0;
+
+    for (let x = -16; x < 16; x++) {
+      for (let z = -16; z < 16; z++) {
+        // Current cell
+        const currentCell = new THREE.Vector3(
+            baseCellPos.x + x * GRASS_PATCH_SIZE, 0, baseCellPos.z + z * GRASS_PATCH_SIZE);
+        currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z);
+
+        AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(GRASS_PATCH_SIZE, 1000, GRASS_PATCH_SIZE));
+        const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ);
+        if (distToCell > GRASS_MAX_DIST) {
+          continue;
+        }
+
+        if (x == 0 && z == 0) {
+          let a = 0;
+        }
+
+        if (!frustum.intersectsBox(AABB_TMP)) {
+          continue;
+        }
+
+        if (distToCell > GRASS_LOD_DIST) {
+          const m = meshesLow.length > 0 ? meshesLow.pop() : this.#CreateMesh_(distToCell);
+          m.position.copy(currentCell);
+          m.position.y = 0;
+          m.visible = true;
+          totalVerts += GRASS_VERTICES_LOW;
+        } else {
+          const m = meshesHigh.length > 0 ? meshesHigh.pop() : this.#CreateMesh_(distToCell);
+          m.position.copy(currentCell);
+          m.position.y = 0;
+          m.visible = true;
+          totalVerts += GRASS_VERTICES_HIGH;
+        }
+        totalGrass += 1;
+      }
+    }
+
+    totalGrass *= NUM_GRASS;
+    totalVerts *= NUM_GRASS;
+    // console.log('total grass: ' + totalGrass + ' total verts: ' + totalVerts);
+  }
+}

+ 49 - 0
src/base/render/light-component.js

@@ -0,0 +1,49 @@
+import {THREE} from '../three-defs.js';
+
+import * as entity from "../entity.js";
+
+
+export class LightComponent extends entity.Component {
+  static CLASS_NAME = 'LightComponent';
+
+  get NAME() {
+    return LightComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #light_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#light_ = null;
+  }
+
+  Destroy() {
+    this.light_.removeFromParent();
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    if (this.#params_.hemi) {
+      const params = this.#params_.hemi;
+      this.#light_ = new THREE.HemisphereLight(params.upColour, params.downColour, params.intensity);
+      threejs.AddSceneObject(this.#light_);
+    }
+  }
+
+  Update(timeElapsed) {
+    const player = this.FindEntity('player');
+    if (!player) {
+      return;
+    }
+    const pos = player.Position;
+
+    if (this.sky_) {
+      this.sky_.material.uniforms.time.value += timeElapsed;
+    }
+
+    this.sky_.position.copy(pos);
+  }
+}

+ 245 - 0
src/base/render/terrain-component.js

@@ -0,0 +1,245 @@
+import {THREE, Float32ToFloat16} from '../three-defs.js';
+
+import * as shaders from '../../game/render/shaders.js';
+
+import * as entity from "../entity.js";
+import * as math from '../math.js';
+
+import { render_component } from '../render-component.js';
+import * as grass_component from './grass-component.js';
+import * as bugs_component from './bugs-component.js';
+import * as wind_component from './wind-component.js';
+import * as water_component from './water-component.js';
+
+
+function GetImageData_(image) {
+  const canvas = document.createElement('canvas');
+  canvas.width = image.width;
+  canvas.height = image.height;
+
+  const context = canvas.getContext( '2d' );
+  context.drawImage(image, 0, 0);
+
+  return context.getImageData(0, 0, image.width, image.height);
+}
+
+class Heightmap {
+  constructor(params, img) {
+    this.params_ = params;
+    this.data_ = GetImageData_(img);
+  }
+
+  Get(x, y) {
+    const _GetPixelAsFloat = (x, y) => {
+      const position = (x + this.data_.width * y) * 4;
+      const data = this.data_.data;
+      return data[position] / 255.0;
+    }
+
+    // Bilinear filter
+    const offset = this.params_.offset;
+    const dimensions = this.params_.dimensions;
+
+    const xf = math.sat((x - offset.x) / dimensions.x);
+    const yf = 1.0 - math.sat((y - offset.y) / dimensions.y);
+    const w = this.data_.width - 1;
+    const h = this.data_.height - 1;
+
+    const x1 = Math.floor(xf * w);
+    const y1 = Math.floor(yf * h);
+    const x2 = math.clamp(x1 + 1, 0, w);
+    const y2 = math.clamp(y1 + 1, 0, h);
+
+    const xp = xf * w - x1;
+    const yp = yf * h - y1;
+
+    const p11 = _GetPixelAsFloat(x1, y1);
+    const p21 = _GetPixelAsFloat(x2, y1);
+    const p12 = _GetPixelAsFloat(x1, y2);
+    const p22 = _GetPixelAsFloat(x2, y2);
+
+    const px1 = math.lerp(xp, p11, p21);
+    const px2 = math.lerp(xp, p12, p22);
+
+    return math.lerp(yp, px1, px2) * this.params_.height;
+  }
+}
+
+
+const TERRAIN_HEIGHT = 75;
+const TERRAIN_OFFSET = 50;
+
+// const TERRAIN_HEIGHT = 0;
+// const TERRAIN_OFFSET = 0;
+
+const TERRAIN_DIMS = 2000;
+
+export class TerrainComponent extends entity.Component {
+  static CLASS_NAME = 'TerrainComponent';
+
+  get NAME() {
+    return TerrainComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #heightmap_;
+  #mesh_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#heightmap_ = null;
+    this.#mesh_ = null;
+  }
+
+  Destroy() {
+    this.#mesh_.removeFromParent();
+  }
+
+  GetHeight(x, y) {
+    const xn = (x + TERRAIN_DIMS * 0.5) / TERRAIN_DIMS;
+    const yn = 1 - (y + TERRAIN_DIMS * 0.5) / TERRAIN_DIMS;
+    return this.#heightmap_.Get(xn, yn) - TERRAIN_OFFSET;
+  }
+
+  IsReady() {
+    return this.#heightmap_ != null;
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    const geometry = new THREE.PlaneGeometry(TERRAIN_DIMS, TERRAIN_DIMS, 256, 256);
+
+    const textureLoader = new THREE.TextureLoader();
+    textureLoader.load(
+        './resources/textures/' + 'terrain.png',
+        (heightmapTexture) => {
+      const heightmapGenerator = new Heightmap({
+          dimensions: new THREE.Vector2(1.0, 1.0),
+          offset: new THREE.Vector2(0.0, 0.0),
+          height: TERRAIN_HEIGHT
+        }, heightmapTexture.image);
+
+      this.#heightmap_ = heightmapGenerator;
+
+      const positions = geometry.attributes.position;
+      const uv = geometry.attributes.uv;
+      for (let i = 0; i < positions.count; i++) {
+        const h = heightmapGenerator.Get(uv.array[i*2+0], uv.array[i*2+1]) - TERRAIN_OFFSET;
+        positions.array[i*3+2] = h;
+      }
+
+      geometry.computeVertexNormals();
+      geometry.computeTangents();
+
+      const position16 = Float32ToFloat16(geometry.attributes.position.array);
+      const normal16 = Float32ToFloat16(geometry.attributes.normal.array);
+      const tangent16 = Float32ToFloat16(geometry.attributes.tangent.array);
+      const uv16 = Float32ToFloat16(geometry.attributes.uv.array);
+
+      geometry.setAttribute('position', new THREE.Float16BufferAttribute(position16, 3));
+      geometry.setAttribute('normal', new THREE.Float16BufferAttribute(normal16, 3));
+      geometry.setAttribute('tangent', new THREE.Float16BufferAttribute(tangent16, 3));
+      geometry.setAttribute('uv', new THREE.Float16BufferAttribute(uv16, 2));
+      geometry.rotateX(-Math.PI / 2);
+
+      heightmapTexture.colorSpace = THREE.LinearSRGBColorSpace;
+
+      const LOAD_ = (name) => {
+        const albedo = textureLoader.load('./resources/textures/' + name);
+        albedo.magFilter = THREE.LinearFilter;
+        albedo.minFilter = THREE.LinearMipMapLinearFilter;
+        albedo.wrapS = THREE.RepeatWrapping;
+        albedo.wrapT = THREE.RepeatWrapping;
+        albedo.anisotropy = 16;
+        albedo.repeat.set(40, 40);
+        return albedo; 
+      }
+
+      // const grassAlbedo = LOAD_('wispy-grass-meadow_albedo.png');
+      const grid = LOAD_('grid.png');
+      grid.anisotropy = 16;
+      grid.repeat.set(1, 1);
+
+      const terrainMaterial = new shaders.GamePBRMaterial('TERRAIN', {});
+      terrainMaterial.setTexture('heightmap', heightmapTexture);
+      terrainMaterial.setTexture('grid', grid);
+      terrainMaterial.setVec4('heightParams', new THREE.Vector4(TERRAIN_DIMS, TERRAIN_DIMS, TERRAIN_HEIGHT, TERRAIN_OFFSET));
+
+      this.#mesh_ = new THREE.Mesh(geometry, terrainMaterial);
+      this.#mesh_.position.set(0, 0, 0);
+      this.#mesh_.receiveShadow = true;
+      this.#mesh_.castShadow = false;
+  
+      threejs.AddSceneObject(this.#mesh_);
+  
+      this.Broadcast({
+        topic: 'render.loaded',
+        value: this.#mesh_,
+      });
+
+      
+      const mountain = new entity.Entity();
+      mountain.AddComponent(new render_component.RenderComponent({
+        resourcePath: './resources/models/',
+        resourceName: 'mountain.glb',
+        scale: new THREE.Vector3(1, 1, 1),
+        emissive: new THREE.Color(0x000000),
+        color: new THREE.Color(0xFFFFFF),
+        receiveShadow: false,
+        castShadow: false,
+      }));
+
+      mountain.SetPosition(new THREE.Vector3(0, -100, 0));
+      mountain.SetActive(false);
+      mountain.Init();
+
+      const water = new entity.Entity();
+      water.AddComponent(new water_component.WaterComponent({
+        terrain: this.Parent,
+        height: TERRAIN_HEIGHT,
+        offset: TERRAIN_OFFSET,
+        heightmap: heightmapTexture
+      }));
+      water.SetActive(true);
+      water.Init(this.Parent);
+
+      const grass = new entity.Entity();
+      grass.AddComponent(new grass_component.GrassComponent({
+        terrain: this.Parent,
+        height: TERRAIN_HEIGHT,
+        offset: TERRAIN_OFFSET,
+        dims: TERRAIN_DIMS,
+        heightmap: heightmapTexture
+      }));
+      grass.SetActive(true);
+      grass.Init(this.Parent);
+
+      const bugs = new entity.Entity();
+      bugs.AddComponent(new bugs_component.BugsComponent({
+        terrain: this.Parent,
+        height: TERRAIN_HEIGHT,
+        offset: TERRAIN_OFFSET,
+        dims: TERRAIN_DIMS,
+        heightmap: heightmapTexture
+      }));
+      bugs.SetActive(true);
+      bugs.Init(this.Parent);
+
+      const wind = new entity.Entity();
+      wind.AddComponent(new wind_component.WindComponent({
+        terrain: this.Parent,
+        height: TERRAIN_HEIGHT,
+        offset: TERRAIN_OFFSET,
+        dims: TERRAIN_DIMS,
+        heightmap: heightmapTexture
+      }));
+      wind.SetActive(true);
+      wind.Init(this.Parent);
+    });
+  }
+
+  Update(timeElapsed) {
+  }
+}

+ 99 - 0
src/base/render/water-component.js

@@ -0,0 +1,99 @@
+import {THREE} from '../three-defs.js';
+
+import * as shaders from '../../game/render/shaders.js';
+
+import * as entity from "../entity.js";
+
+
+class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute {
+
+	constructor( array, itemSize, normalized, meshPerAttribute = 1 ) {
+
+		super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute );
+
+		this.isFloat16BufferAttribute = true;
+	}
+};
+
+
+const NUM_BUGS = 6;
+const NUM_SEGMENTS = 2;
+const NUM_VERTICES = (NUM_SEGMENTS + 1) * 2;
+const BUG_SPAWN_RANGE = 40.0;
+const BUG_MAX_DIST = 100.0;
+
+const M_TMP = new THREE.Matrix4();
+const AABB_TMP = new THREE.Box3();
+
+
+export class WaterComponent extends entity.Component {
+  static CLASS_NAME = 'WaterComponent';
+
+  get NAME() {
+    return WaterComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #mesh_;
+  #group_;
+  #totalTime_;
+  #material_;
+  #geometry_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#mesh_ = [];
+    this.#group_ = new THREE.Group();
+    this.#totalTime_ = 0;
+    this.#geometry_ = null;
+  }
+
+  Destroy() {
+    this.#mesh_.removeFromParent();
+    this.#group_.removeFromParent();
+  }
+
+  #CreateGeometry_() {
+    const plane = new THREE.PlaneGeometry(2000, 2000, 1, 1);
+    plane.rotateX(-Math.PI / 2);
+
+    return plane;
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+    this.#geometry_ = this.#CreateGeometry_();
+    this.#material_ = new shaders.GameMaterial('WATER');
+    this.#material_.depthWrite = false;
+    this.#material_.depthTest = true;
+    this.#mesh_ = this.#CreateMesh_();
+    this.#mesh_.position.y = -14.0;
+
+    this.#group_.add(this.#mesh_);
+
+    threejs.AddSceneObject(this.#group_, {pass: 'water'});
+  }
+
+  #CreateMesh_() {
+    const m = new THREE.Mesh(this.#geometry_, this.#material_);
+    m.receiveShadow = true;
+    m.castShadow = false;
+    m.visible = true;
+
+    return m;
+  }
+
+  Update(timeElapsed) {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+    this.#totalTime_ += timeElapsed;
+
+    this.#material_.setFloat('time', this.#totalTime_);
+    this.#material_.setVec2('resolution', new THREE.Vector2(window.innerWidth, window.innerHeight));
+    this.#material_.setTexture('colourTexture', threejs.WaterTexture);
+    this.#material_.setMatrix('inverseProjectMatrix', threejs.Camera.projectionMatrixInverse);
+  }
+}

+ 162 - 0
src/base/render/wind-component.js

@@ -0,0 +1,162 @@
+import {THREE} from '../three-defs.js';
+
+import * as shaders from '../../game/render/shaders.js';
+
+import * as entity from "../entity.js";
+
+import * as terrain_component from './terrain-component.js';
+import MersenneTwister from 'mersenne-twister';
+
+
+
+const NUM_BUGS = 8;
+const BUG_SPAWN_RANGE = 20.0;
+const BUG_MAX_DIST = 50.0;
+
+const M_TMP = new THREE.Matrix4();
+const AABB_TMP = new THREE.Box3();
+
+
+export class WindComponent extends entity.Component {
+  static CLASS_NAME = 'WindComponent';
+
+  get NAME() {
+    return WindComponent.CLASS_NAME;
+  }
+
+  #params_;
+  #meshes_;
+  #group_;
+  #totalTime_;
+  #material_;
+  #geometry_;
+
+  constructor(params) {
+    super();
+
+    this.#params_ = params;
+    this.#meshes_ = [];
+    this.#group_ = new THREE.Group();
+    this.#totalTime_ = 0;
+    this.#geometry_ = null;
+  }
+
+  Destroy() {
+    for (let m of this.#meshes_) {
+      m.removeFromParent();
+    }
+    this.#group_.removeFromParent();
+  }
+
+  #CreateGeometry_() {
+    const rng = new MersenneTwister(1);
+
+    const offsets = new Float32Array(NUM_BUGS * 3);
+    for (let i = 0; i < NUM_BUGS; ++i) {
+      offsets[i*3 + 0] = (rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2);
+      offsets[i*3 + 1] = rng.random() * 1.0 + 2.0;
+      offsets[i*3 + 2] = (rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2);
+    }
+
+    const plane = new THREE.PlaneGeometry(1, 1, 1, 1);
+
+    const geo = new THREE.InstancedBufferGeometry();
+    geo.instanceCount = NUM_BUGS;
+    geo.setAttribute('position', plane.attributes.position);
+    geo.setAttribute('uv', plane.attributes.uv);
+    geo.setAttribute('normal', plane.attributes.normal);
+    geo.setAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3));
+    geo.setIndex(plane.index);
+    geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), BUG_SPAWN_RANGE);
+
+    return geo;
+  }
+
+  InitEntity() {
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+
+    this.#geometry_ = this.#CreateGeometry_();
+
+    const textureLoader = new THREE.TextureLoader();
+    const albedo = textureLoader.load('./resources/textures/' + 'dust.png');
+    albedo.colorSpace = THREE.SRGBColorSpace;
+
+    this.#material_ = new shaders.ShaderMaterial('WIND', {
+      uniforms: {
+        time: { value: 0.0 },
+        diffuseTexture: { value: albedo },
+        dustSize: { value: new THREE.Vector2(0.4, 0.4) },
+        heightmap: { value: this.#params_.heightmap },
+        heightmapParams: { value: new THREE.Vector3(this.#params_.height, this.#params_.offset, this.#params_.dims) },
+      }
+    });
+    this.#material_.transparent = true;
+    this.#material_.side = THREE.DoubleSide;
+    this.#material_.depthWrite = false;
+    this.#material_.depthTest = true;
+
+    threejs.AddSceneObject(this.#group_, {pass: 'transparent'});
+  }
+
+  #CreateMesh_() {
+    const m = new THREE.Mesh(this.#geometry_, this.#material_);
+    m.receiveShadow = false;
+    m.castShadow = false;
+    m.visible = true;
+
+    this.#meshes_.push(m);
+    this.#group_.add(m);
+
+    return m;
+  }
+
+  Update(timeElapsed) {
+    this.#totalTime_ += timeElapsed;
+
+    this.#material_.uniforms.time.value = this.#totalTime_;
+
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    const camera = threejs.Camera;
+    const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse));
+
+    const meshes = [...this.#meshes_];
+
+    const baseCellPos = camera.position.clone();
+    baseCellPos.divideScalar(BUG_SPAWN_RANGE);
+    baseCellPos.floor();
+    baseCellPos.multiplyScalar(BUG_SPAWN_RANGE);
+
+    // This is dumb and slow
+    for (let c of this.#group_.children) {
+      c.visible = false;
+    }
+
+    const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME);
+
+    const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z);
+
+    for (let x = -3; x < 3; x++) {
+      for (let z = -3; z < 3; z++) {
+        // Current cell
+        const currentCell = new THREE.Vector3(
+            baseCellPos.x + x * BUG_SPAWN_RANGE, 0, baseCellPos.z + z * BUG_SPAWN_RANGE);
+        currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z);
+
+        AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(BUG_SPAWN_RANGE, 100, BUG_SPAWN_RANGE));
+        const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ);
+        if (distToCell > BUG_MAX_DIST) {
+          continue;
+        }
+
+        if (!frustum.intersectsBox(AABB_TMP)) {
+          continue;
+        }
+
+        const m = meshes.length > 0 ? meshes.pop() : this.#CreateMesh_();
+        m.position.copy(currentCell);
+        m.position.y = 0;
+        m.visible = true;
+      }
+    }
+  }
+}

+ 35 - 0
src/base/three-defs.js

@@ -0,0 +1,35 @@
+
+import * as THREE from 'three';
+
+import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
+import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
+import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
+import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js';
+
+import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js';
+import {ACESFilmicToneMappingShader} from 'three/addons/shaders/ACESFilmicToneMappingShader.js';
+import {FXAAShader} from 'three/addons/shaders/FXAAShader.js';
+
+import {FBXLoader} from 'three/addons/loaders/FBXLoader.js';
+import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
+
+import { CSM } from 'three/addons/csm/CSM.js';
+import { CSMShader } from 'three/addons/csm/CSMShader.js';
+
+import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
+import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+export function Float32ToFloat16(data) {
+  const data16 = new Uint16Array(data.length);
+  for (let i = 0; i < data.length; i++) {
+    data16[i] = THREE.DataUtils.toHalfFloat(data[i]);
+  }
+  return data16;
+}
+
+export {
+  THREE, EffectComposer, ShaderPass, GammaCorrectionShader, ACESFilmicToneMappingShader,
+  RenderPass, FXAAShader, UnrealBloomPass,
+  FBXLoader, GLTFLoader, SkeletonUtils, BufferGeometryUtils,
+  CSM, CSMShader,
+};

+ 555 - 0
src/base/threejs-component.js

@@ -0,0 +1,555 @@
+import { THREE, RenderPass, ShaderPass, FXAAShader, ACESFilmicToneMappingShader } from './three-defs.js';
+import {N8AOPass} from "n8ao";
+
+import * as Stats from 'stats.js';
+
+import * as entity from "./entity.js";
+import * as light_component from './render/light-component.js';
+import * as shaders from '../game/render/shaders.js';
+
+
+const HEMI_UP = new THREE.Color().setHex(0x7CBFFF, THREE.SRGBColorSpace);
+const HEMI_DOWN = new THREE.Color().setHex(0xE5BCFF, THREE.SRGBColorSpace);
+const HEMI_INTENSITY = 0.25;
+const LIGHT_INTENSITY = 0.7;
+const LIGHT_COLOUR = new THREE.Color().setRGB(0.52, 0.66, 0.99, THREE.SRGBColorSpace);
+const LIGHT_FAR = 1000.0;
+
+
+const GammaCorrectionShader2 = {
+	name: 'GammaCorrectionShader2',
+	uniforms: {
+		'tDiffuse': { value: null }
+	},
+	vertexShader: /* glsl */`
+		varying vec2 vUv;
+
+		void main() {
+			vUv = uv;
+			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+		}`,
+	fragmentShader: /* glsl */`
+		uniform sampler2D tDiffuse;
+		varying vec2 vUv;
+
+    float inverseLerp(float minValue, float maxValue, float v) {
+      return (v - minValue) / (maxValue - minValue);
+    }
+    
+    float remap(float v, float inMin, float inMax, float outMin, float outMax) {
+      float t = inverseLerp(inMin, inMax, v);
+      return mix(outMin, outMax, t);
+    }
+
+    vec3 vignette(vec2 uvs) {
+      float v1 = smoothstep(0.5, 0.3, abs(uvs.x - 0.5));
+      float v2 = smoothstep(0.5, 0.3, abs(uvs.y - 0.5));
+      float v = v1 * v2;
+      v = pow(v, 0.125);
+      v = remap(v, 0.0, 1.0, 0.4, 1.0);
+      return vec3(v);
+    }
+
+		void main() {
+			vec4 tex = texture2D( tDiffuse, vUv );
+
+      tex = LinearTosRGB(tex);
+      tex.rgb *= vignette(vUv);
+
+			gl_FragColor = tex;
+		}`
+};
+
+const Copy2 = {
+
+	name: 'Copy2',
+
+	uniforms: {
+
+		'tDiffuse': { value: null }
+
+	},
+
+	vertexShader: /* glsl */`
+
+		varying vec2 vUv;
+
+		void main() {
+
+			vUv = uv;
+			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+
+		}`,
+
+	fragmentShader: /* glsl */`
+
+		uniform sampler2D tDiffuse;
+
+		varying vec2 vUv;
+
+		void main() {
+
+			vec4 tex = texture2D( tDiffuse, vUv );
+
+			gl_FragColor = tex;
+
+		}`
+
+};
+
+
+class FakeCSM {
+  constructor() {
+    this.lights = [{
+      color: new THREE.Color(0xFFFFFF),
+      lightDirection: new THREE.Vector3(1, 0, 0),
+    }];
+    this.lightDirection = new THREE.Vector3(1, 0, 0);
+  }
+  setupMaterial() {}
+  updateFrustums() {}
+  update() {}
+
+}
+
+export const threejs_component = (() => {
+
+  class ThreeJSController extends entity.Component {
+    static CLASS_NAME = 'ThreeJSController';
+
+    get NAME() {
+      return ThreeJSController.CLASS_NAME;
+    }
+
+    #threejs_;
+    #csm_;
+
+    #ssaoPass_;
+    #opaqueScene_;
+    #opaquePass_;
+    #waterScene_;
+    #waterPass_;
+
+    #opaqueCamera_;
+    #waterCamera_;
+
+    #transparentScene_;
+    #transparentPass_;
+    #transparentCamera_;
+
+    #waterTexturePass_;
+
+    #fxaaPass_;
+    #acesPass_;
+    #gammaPass_;
+    #copyPass_;
+
+    #grassTimingAvg_;
+
+    constructor() {
+      super();
+
+      this.#threejs_ = null;
+      this.#ssaoPass_ = null;
+      this.#opaqueScene_ = null;
+      this.#opaquePass_ = null;
+      this.#opaqueCamera_ = null;
+      this.#waterScene_ = null;
+      this.#waterCamera_ = null;
+      this.#waterPass_ = null;
+      this.#waterScene_ = null;
+      this.#waterCamera_ = null;
+      this.#waterPass_ = null;
+      this.#waterTexturePass_ = null;
+      this.#fxaaPass_ = null;
+      this.#acesPass_ = null;
+      this.#gammaPass_ = null;
+      this.#copyPass_ = null;
+      this.#csm_ = null;
+      this.grassTimingAvg_ = 0;
+      this.timerQuery = null;
+    }
+
+    InitEntity() {
+      shaders.SetThreeJS(this);
+
+      this.#threejs_ = new THREE.WebGLRenderer({
+        antialias: false,
+        powerPreference: 'high-performance',
+      });
+      this.#threejs_.shadowMap.enabled = true;
+      this.#threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
+      this.#threejs_.setSize(window.innerWidth, window.innerHeight);
+      this.#threejs_.domElement.id = 'threejs';
+      this.#threejs_.outputColorSpace = THREE.LinearSRGBColorSpace;
+  
+      document.getElementById('container').appendChild(this.#threejs_.domElement);
+  
+      window.addEventListener('resize', () => {
+        this.onWindowResize_();
+      }, false);
+
+      const fov = 60;
+      const aspect = 1920 / 1080;
+      const near = 0.1;
+      const far = 10000.0;
+      this.#opaqueCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this.#opaqueCamera_.position.set(20, 5, 15);
+
+      this.#waterCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+
+      this.#opaqueScene_ = new THREE.Scene();
+      this.#opaqueScene_.add(this.#opaqueCamera_);
+
+      this.#waterScene_ = new THREE.Scene();
+      this.#waterScene_.add(this.#waterCamera_);
+
+      this.#transparentScene_ = new THREE.Scene();
+      this.#transparentCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
+      this.#transparentScene_.add(this.#transparentCamera_);
+
+      this.listener_ = new THREE.AudioListener();
+      this.#opaqueCamera_.add(this.listener_);
+
+      this.uiCamera_ = new THREE.OrthographicCamera(
+          -1, 1, 1, -1, 1, 1000);
+      this.uiScene_ = new THREE.Scene();
+  
+      this.#opaqueScene_.fog = new THREE.FogExp2(0xDFE9F3, 0.0001);
+      this.#opaqueScene_.fog.color.setRGB(0.45, 0.8, 1.0, THREE.SRGBColorSpace);
+
+      let light = new THREE.DirectionalLight(0xFFFFFF, LIGHT_INTENSITY);
+      light.position.set(-20, 20, 20);
+      light.target.position.set(0, 0, 0);
+      light.color.copy(LIGHT_COLOUR);
+
+      this.#csm_ = new FakeCSM();
+
+      // VIDEO HACK
+      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 = 100.0;
+      light.shadow.camera.left = 32;
+      light.shadow.camera.right = -32;
+      light.shadow.camera.top = 32;
+      light.shadow.camera.bottom = -32;
+      this.#opaqueScene_.add(light);
+      this.#opaqueScene_.add(light.target);
+
+      const lightDir = light.position.clone();
+      lightDir.normalize();
+      lightDir.multiplyScalar(-1);
+
+      const csmFar = LIGHT_FAR;
+      // this.#csm_ = new CSM({
+      //   maxFar: csmFar,
+      //   fade: true,
+      //   cascades: 6,
+      //   mode: 'practical',
+      //   parent: this.#opaqueScene_,
+      //   shadowMapSize: 2048,
+      //   lightIntensity: LIGHT_INTENSITY,
+      //   lightNear: 1.0,
+      //   lightFar: csmFar,
+      //   lightDirection: lightDir,
+      //   camera: this.#opaqueCamera_
+      // });
+      // this.#csm_.fade = true;
+
+      for (let i = 0; i < this.#csm_.lights.length; i++) {
+        this.#csm_.lights[i].color.copy(LIGHT_COLOUR);
+      }
+
+      this.#csm_.updateFrustums();
+
+      this.sun_ = light;
+
+      const waterParams = {
+        type: THREE.HalfFloatType,
+        magFilter: THREE.NearestFilter,
+        minFilter: THREE.NearestFilter,
+        wrapS: THREE.ClampToEdgeWrapping,
+        wrapT: THREE.ClampToEdgeWrapping,
+        generateMipmaps: false,
+      };
+      this.waterBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, waterParams);
+      this.waterBuffer_.stencilBuffer = false;
+
+      const bufferParams = {
+        type: THREE.HalfFloatType,
+        magFilter: THREE.LinearFilter,
+        minFilter: THREE.LinearFilter,
+        wrapS: THREE.ClampToEdgeWrapping,
+        wrapT: THREE.ClampToEdgeWrapping,
+        generateMipmaps: false,
+      };
+      this.readBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, bufferParams);
+      this.readBuffer_.stencilBuffer = false;
+      this.readBuffer_.depthTexture = new THREE.DepthTexture();
+      this.readBuffer_.depthTexture.format = THREE.DepthStencilFormat;
+      this.readBuffer_.depthTexture.type = THREE.UnsignedInt248Type;
+
+      this.writeBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, bufferParams);
+      this.writeBuffer_.stencilBuffer = false;
+      this.writeBuffer_.depthTexture = new THREE.DepthTexture();
+      this.writeBuffer_.depthTexture.format = THREE.DepthStencilFormat;
+      this.writeBuffer_.depthTexture.type = THREE.UnsignedInt248Type;
+
+      this.#opaquePass_ = new RenderPass(this.#opaqueScene_, this.#opaqueCamera_);
+      this.#ssaoPass_ = new N8AOPass(this.#opaqueScene_, this.#opaqueCamera_, this.writeBuffer_.width, this.writeBuffer_.height);
+      this.#ssaoPass_.configuration.aoRadius = 3.0;
+      this.#ssaoPass_.configuration.distanceFalloff = 0.25;
+      this.#ssaoPass_.configuration.intensity = 5.0;
+      this.#ssaoPass_.configuration.color = new THREE.Color(0, 0, 0);
+      // this.#ssaoPass_.configuration.halfRes = true;
+      this.#ssaoPass_.beautyRenderTarget.dispose();
+      this.#ssaoPass_.beautyRenderTarget = this.writeBuffer_;
+      this.#ssaoPass_.setQualityMode("High");
+
+      this.#waterPass_ = new RenderPass(this.#waterScene_, this.#opaqueCamera_);
+      this.#transparentPass_ = new RenderPass(this.#transparentScene_, this.#transparentCamera_);
+
+      const f = this.#opaqueCamera_.far;
+      const n = this.#opaqueCamera_.near;
+      const shader = new shaders.ShaderMaterial('WATER-TEXTURE', {
+        uniforms: {
+          colourTexture: { value: null },
+          depthTexture: { value: null },
+          nearFar: { value: new THREE.Vector3(f * n, f - n, f) },
+        }
+      });
+
+      this.#waterTexturePass_ = new ShaderPass(shader);
+      this.#fxaaPass_ = new ShaderPass(FXAAShader);
+      this.#acesPass_ = new ShaderPass(ACESFilmicToneMappingShader);
+      this.#gammaPass_ = new ShaderPass(GammaCorrectionShader2);
+      this.#copyPass_ = new ShaderPass(Copy2);
+
+      const hemiLight = new entity.Entity();
+      hemiLight.AddComponent(new light_component.LightComponent({
+          hemi: {
+              // upColour: 0x7CBFFF,
+              // downColour: 0xFFE5BC,
+              upColour: HEMI_UP,
+              downColour: HEMI_DOWN,
+              intensity: HEMI_INTENSITY,
+          }
+      }));
+      hemiLight.SetActive(false);
+      hemiLight.Init(this.Parent);
+
+      this.grassStats_ = new Stats.Panel('Grass MS', '#0f0', '#020');
+      this.stats_ = new Stats();
+      this.stats_.addPanel(this.grassStats_);
+      this.stats_.showPanel(0);
+      document.body.appendChild(this.stats_.dom);
+
+      this.onWindowResize_();
+    }
+
+    get Scene() {
+      return this.#opaqueScene_;
+    }
+
+    get Camera() {
+      return this.#opaqueCamera_;
+    }
+
+    get WaterTexture() {
+      return this.waterBuffer_.texture;
+    }
+
+    get WaterDepthTexture() {
+      return this.waterBuffer_.texture;
+    }
+
+    getMaxAnisotropy() {
+      return this.#threejs_.capabilities.getMaxAnisotropy();
+    }
+
+    onWindowResize_() {
+      const w = window.innerWidth;
+      const h = window.innerHeight
+      this.#opaqueCamera_.aspect = w / h;
+      this.#opaqueCamera_.updateProjectionMatrix();
+
+      this.#waterCamera_.aspect = this.#opaqueCamera_.aspect;
+      this.#waterCamera_.updateProjectionMatrix();
+
+      this.#transparentCamera_.aspect = this.#opaqueCamera_.aspect;
+      this.#transparentCamera_.updateProjectionMatrix();
+  
+      this.#threejs_.setSize(w, h);
+      // this.composer_.setSize(window.innerWidth, window.innerHeight);
+
+      this.waterBuffer_.setSize(w, h);
+      this.writeBuffer_.setSize(w, h);
+      this.readBuffer_.setSize(w, h);
+      // this.csm_.updateFrustums();
+
+      this.#ssaoPass_.setSize(w, h);
+
+      this.#waterTexturePass_.setSize(w, h);
+
+      this.#fxaaPass_.material.uniforms['resolution'].value.x = 1 / w;
+      this.#fxaaPass_.material.uniforms['resolution'].value.y = 1 / h;
+
+      this.#csm_.updateFrustums();
+    }
+
+    swapBuffers_() {
+      const tmp = this.writeBuffer_;
+      this.writeBuffer_ = this.readBuffer_;
+      this.readBuffer_ = tmp;
+    }
+
+    SetupMaterial(material) {
+      this.#csm_.setupMaterial(material);
+    }
+
+    AddSceneObject(obj, params) {
+      params = params || {};
+
+      if (params.pass == 'water') {
+        this.#waterScene_.add(obj);
+      } else if (params.pass == 'transparent') {
+        this.#transparentScene_.add(obj);
+      } else {
+        this.#opaqueScene_.add(obj);
+      }
+    }
+
+    Render(timeElapsedS) {
+      this.#waterCamera_.position.copy(this.#opaqueCamera_.position);
+      this.#waterCamera_.quaternion.copy(this.#opaqueCamera_.quaternion);
+
+      this.#transparentCamera_.position.copy(this.#opaqueCamera_.position);
+      this.#transparentCamera_.quaternion.copy(this.#opaqueCamera_.quaternion);
+      
+      this.stats_.begin();
+
+      this.#threejs_.autoClear = true;
+      this.#threejs_.autoClearColor = true;
+      this.#threejs_.autoClearDepth = true;
+      this.#threejs_.autoClearStencil = true;
+      this.#threejs_.setRenderTarget(this.writeBuffer_);
+      this.#threejs_.clear();
+      this.#threejs_.setRenderTarget(this.readBuffer_);
+      this.#threejs_.clear();
+      this.#threejs_.setRenderTarget(null);
+
+      const gl = this.#threejs_.getContext();
+      const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
+      if (this.timerQuery === null) {
+        this.timerQuery = gl.createQuery();
+        gl.beginQuery(ext.TIME_ELAPSED_EXT, this.timerQuery);
+      }
+
+      this.#opaquePass_.renderToScreen = false;
+      this.#opaquePass_.render(this.#threejs_, null, this.writeBuffer_, timeElapsedS, false);
+      this.writeBuffer_.ACTIVE_HAS_OPAQUE = true; 
+      this.readBuffer_.ACTIVE_HAS_OPAQUE = false; 
+      this.swapBuffers_();
+
+      this.#threejs_.autoClear = false;
+      this.#threejs_.autoClearColor = false;
+      this.#threejs_.autoClearDepth = false;
+      this.#threejs_.autoClearStencil = false;
+
+      if (this.timerQuery !== null) {
+        gl.endQuery(ext.TIME_ELAPSED_EXT);
+        gl.flush();
+        const available = gl.getQueryParameter(this.timerQuery, gl.QUERY_RESULT_AVAILABLE);
+        if (available) {
+          const elapsedTimeInNs = gl.getQueryParameter(this.timerQuery, gl.QUERY_RESULT);
+          const elapsedTimeInMs = elapsedTimeInNs / 1000000;
+          this.grassTimingAvg_ = this.grassTimingAvg_ * 0.9 + elapsedTimeInMs * 0.1;
+          // console.log(`Render time: ${this.grassTimingAvg_}ms`);
+          this.grassStats_.update(elapsedTimeInMs, 10);
+          this.timerQuery = null;
+        }
+      }
+
+      this.#ssaoPass_.clear = false;
+      this.#ssaoPass_.renderToScreen = false;
+      this.#ssaoPass_.beautyRenderTarget = this.readBuffer_;
+      this.#ssaoPass_.configuration.autoRenderBeauty = false;
+      this.#ssaoPass_.configuration.intensity = 5.0;
+      // this.#ssaoPass_.setDisplayMode("Split");
+      this.#ssaoPass_.render(this.#threejs_, this.writeBuffer_, null, timeElapsedS, false);
+      this.writeBuffer_.ACTIVE_HAS_SSAO_OPAQUE = true; 
+      this.readBuffer_.ACTIVE_HAS_SSAO_OPAQUE = false; 
+      this.swapBuffers_();
+
+      // SSAO buffer has colour, but other one has depth, which I want to reuse
+      // Swapping them should work, but doesn't, and I don't feel like figuring out why.
+      this.#copyPass_.renderToScreen = false;
+      this.#copyPass_.clear = false;
+      this.#copyPass_.material.depthWrite = false;
+      this.#copyPass_.material.depthTest = false;
+      this.#copyPass_.render(this.#threejs_, this.writeBuffer_, this.readBuffer_, timeElapsedS, false);
+      this.writeBuffer_.ACTIVE_HAS_FINAL_OPAQUE = true; 
+      this.readBuffer_.ACTIVE_HAS_FINAL_OPAQUE = false; 
+
+      this.#waterTexturePass_.clear = false;
+      this.#waterTexturePass_.renderToScreen = false;
+      this.#waterTexturePass_.material.uniforms.colourTexture.value = this.writeBuffer_.texture;
+      this.#waterTexturePass_.material.uniforms.depthTexture.value = this.writeBuffer_.depthTexture;
+      this.#waterTexturePass_.render(this.#threejs_, this.waterBuffer_, null, timeElapsedS, false);
+
+      this.#waterPass_.clear = false;
+      this.#waterPass_.render(this.#threejs_, this.null, this.writeBuffer_, timeElapsedS, false);
+
+      this.#transparentPass_.renderToScreen = false;
+      this.#transparentPass_.clear = false;
+      this.#transparentPass_.render(this.#threejs_, null, this.writeBuffer_, timeElapsedS, false);
+      this.writeBuffer_.ACTIVE_HAS_WATER = true;
+      this.readBuffer_.ACTIVE_HAS_WATER = false;
+      this.swapBuffers_();
+
+      this.#fxaaPass_.clear = false;
+      this.#fxaaPass_.render(this.#threejs_, this.writeBuffer_, this.readBuffer_, timeElapsedS, false);
+      this.swapBuffers_();
+
+      // SHADERPASS SWAPS ORDER OF READ/WRITE BUFFERS
+      this.#acesPass_.clear = false;
+      this.#acesPass_.material.uniforms.exposure.value = 1.0;
+      this.#acesPass_.material.depthTest = false;
+      this.#acesPass_.material.depthWrite = false;
+      this.#acesPass_.render(this.#threejs_, this.writeBuffer_, this.readBuffer_, timeElapsedS, false);
+      this.writeBuffer_.ACTIVE_HAS_ACES = true;
+      this.readBuffer_.ACTIVE_HAS_ACES = false;
+      this.swapBuffers_();
+
+      this.#gammaPass_.clear = false;
+      this.#gammaPass_.renderToScreen = true;
+      this.#gammaPass_.render(this.#threejs_, null, this.readBuffer_, timeElapsedS, false);
+
+      this.stats_.end();
+    }
+
+    Update(timeElapsed) {
+      const player = this.FindEntity('player');
+      if (!player) {
+        return;
+      }
+      const pos = player.Position;
+
+      this.#csm_.update();
+  
+      this.sun_.position.copy(pos);
+      this.sun_.position.add(new THREE.Vector3(-10, 40, 10));
+      this.sun_.target.position.copy(pos);
+      this.sun_.updateMatrixWorld();
+      this.sun_.target.updateMatrixWorld();
+    }
+  }
+
+  return {
+      ThreeJSController: ThreeJSController,
+  };
+})();

+ 99 - 0
src/demo-builder.js

@@ -0,0 +1,99 @@
+import {THREE} from './base/three-defs.js';
+
+import * as entity from './base/entity.js';
+import * as terrain_component from './base/render/terrain-component.js';
+import * as shaders from './game/render/shaders.js';
+
+
+export const demo_builder = (() => {
+
+  class DemoBuilder extends entity.Component {
+    static CLASS_NAME = 'DemoBuilder';
+
+    get NAME() {
+      return DemoBuilder.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+
+      this.params_ = params;
+      this.spawned_ = false;
+      this.materials_ = {};
+    }
+
+    LoadMaterial_(albedoName, normalName, roughnessName, metalnessName) {
+      const textureLoader = new THREE.TextureLoader();
+      const albedo = textureLoader.load('./resources/textures/' + albedoName);
+      albedo.anisotropy = this.FindEntity('threejs').GetComponent('ThreeJSController').getMaxAnisotropy();
+      albedo.wrapS = THREE.RepeatWrapping;
+      albedo.wrapT = THREE.RepeatWrapping;
+      albedo.colorSpace = THREE.SRGBColorSpace;
+
+      const material = new shaders.GameMaterial('PHONG', {
+        map: albedo,
+        color: 0x303030,
+      });
+
+      return material;
+    }
+
+    BuildHackModel_() {
+      this.materials_.checkerboard = this.LoadMaterial_(
+          'whitesquare.png', null, null, null);
+
+      const ground = new THREE.Mesh(
+          new THREE.BoxGeometry(1, 1, 1, 10, 10, 10),
+          this.materials_.checkerboard);
+      ground.castShadow = true;
+      ground.receiveShadow = true;
+
+      this.FindEntity('loader').GetComponent('LoadController').AddModel(ground, 'built-in.', 'ground');
+
+      const box = new THREE.Mesh(
+          new THREE.BoxGeometry(1, 1, 1, 10, 10, 10),
+          this.materials_.checkerboard);
+      box.castShadow = true;
+      box.receiveShadow = true;
+
+      this.FindEntity('loader').GetComponent('LoadController').AddModel(box, 'built-in.', 'box');
+
+      const sphere = new THREE.Mesh(
+          new THREE.SphereGeometry(1, 16, 16),
+          this.materials_.checkerboard);
+      sphere.castShadow = true;
+      sphere.receiveShadow = true;
+
+      this.FindEntity('loader').GetComponent('LoadController').AddModel(sphere, 'built-in.', 'sphere');
+
+      this.currentTime_ = 0.0;
+    }
+
+    Update(timeElapsed) {
+      this.currentTime_ += timeElapsed;
+
+      if (this.materials_.checkerboard && this.materials_.checkerboard.userData.shader) {
+        this.materials_.checkerboard.userData.shader.uniforms.iTime.value = this.currentTime_;
+        this.materials_.checkerboard.needsUpdate = true;
+      }
+
+      if (this.spawned_) {
+        return;
+      }
+
+      this.spawned_ = true;
+
+      this.BuildHackModel_();
+
+      const terrain = new entity.Entity('terrain');
+      terrain.AddComponent(new terrain_component.TerrainComponent({}));
+      terrain.SetActive(true);
+      terrain.Init();
+    }
+  };
+
+  return {
+    DemoBuilder: DemoBuilder
+  };
+
+})();

+ 99 - 0
src/game/player-entity.js

@@ -0,0 +1,99 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+
+import * as entity from '../base/entity.js';
+import {player_input} from './player-input.js';
+
+
+export const player_entity = (() => {
+
+  class BasicCharacterController extends entity.Component {
+    static CLASS_NAME = 'BasicCharacterController';
+
+    get NAME() {
+      return BasicCharacterController.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    InitEntity() {
+      this.Init_();
+    }
+
+    Init_() {
+      this.decceleration_ = new THREE.Vector3(-0.0005, -0.0001, -5.0);
+      this.acceleration_ = new THREE.Vector3(1, 0.125, 20.0);
+      this.velocity_ = new THREE.Vector3(0, 0, 0);
+      this.group_ = new THREE.Group();
+
+      // let light = new THREE.DirectionalLight(0xFFFFFF, 0.5);
+      // light.position.set(-20, 20, 20);
+      // light.target.position.set(0, 0, 0);
+      // light.intensity = 10;
+      // this.group_.add(light);
+
+      const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+      threejs.AddSceneObject(this.group_, {type: 'player'});
+
+      this.rotation_ = new THREE.Quaternion();
+      this.translation_ = new THREE.Vector3(0, 3, 0);
+
+      this.animations_ = {};
+  
+      this.LoadModels_();
+    }
+
+    InitComponent() {
+      this.RegisterHandler_('health.death', (m) => { this.OnDeath_(m); });
+      this.RegisterHandler_(
+          'update.position', (m) => { this.OnUpdatePosition_(m); });
+      this.RegisterHandler_(
+          'update.rotation', (m) => { this.OnUpdateRotation_(m); });
+    }
+
+    OnUpdatePosition_(msg) {
+      this.group_.position.copy(msg.value);
+    }
+
+    OnUpdateRotation_(msg) {
+      this.group_.quaternion.copy(msg.value);
+    }
+
+    OnDeath_(msg) {
+      this.stateMachine_.SetState('death');
+    }
+
+    LoadModels_() {
+    }
+
+    Update(timeInSeconds) {
+      const input = this.GetComponent('PlayerInput');
+
+  
+      const controlObject = this.group_;
+      const _Q = new THREE.Quaternion();
+      const _A = new THREE.Vector3();
+      const _R = controlObject.quaternion.clone();
+
+      if (input.key(player_input.KEYS.a)) {
+        _A.set(0, 1, 0);
+        _Q.setFromAxisAngle(_A, 2.0 * Math.PI * timeInSeconds * this.acceleration_.y);
+        _R.multiply(_Q);
+      }
+      if (input.key(player_input.KEYS.d)) {
+        _A.set(0, 1, 0);
+        _Q.setFromAxisAngle(_A, 2.0 * -Math.PI * timeInSeconds * this.acceleration_.y);
+        _R.multiply(_Q);
+      }
+  
+      this.Parent.SetQuaternion(_R);
+    }
+  };
+  
+  return {
+    BasicCharacterController: BasicCharacterController,
+  };
+
+})();

+ 141 - 0
src/game/player-input.js

@@ -0,0 +1,141 @@
+import * as entity from "../base/entity.js";
+import * as passes from '../base/passes.js';
+
+
+export const player_input = (() => {
+
+  const KEYS = {
+    'a': 65,
+    's': 83,
+    'w': 87,
+    'd': 68,
+    'SPACE': 32,
+    'SHIFT_L': 16,
+    'CTRL_L': 17,
+    'BACKSPACE': 8,
+  };
+
+  class PlayerInput extends entity.Component {
+    static CLASS_NAME = 'PlayerInput';
+
+    get NAME() {
+      return PlayerInput.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+  
+    InitEntity() {
+      this.current_ = {
+        leftButton: false,
+        rightButton: false,
+        mouseXDelta: 0,
+        mouseYDelta: 0,
+        mouseX: 0,
+        mouseY: 0,
+      };
+      this.previous_ = null;
+      this.keys_ = {};
+      this.previousKeys_ = {};
+      this.target_ = document;
+      this.target_.addEventListener('mousedown', (e) => this.onMouseDown_(e), false);
+      this.target_.addEventListener('mousemove', (e) => this.onMouseMove_(e), false);
+      this.target_.addEventListener('mouseup', (e) => this.onMouseUp_(e), false);
+      this.target_.addEventListener('keydown', (e) => this.onKeyDown_(e), false);
+      this.target_.addEventListener('keyup', (e) => this.onKeyUp_(e), false);
+
+      this.Parent.Attributes.Input = {
+        Keyboard: {
+          Current: this.keys_,
+          Previous: this.previousKeys_
+        },
+        Mouse: {
+          Current: this.current_,
+          Previous: this.previous_
+        },
+      };
+
+      this.SetPass(passes.Passes.INPUT);
+    }
+  
+    onMouseMove_(e) {
+      this.current_.mouseX = e.pageX - window.innerWidth / 2;
+      this.current_.mouseY = e.pageY - window.innerHeight / 2;
+
+      if (this.previous_ === null) {
+        this.previous_ = {...this.current_};
+      }
+
+      this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX;
+      this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY;
+    }
+
+    onMouseDown_(e) {
+      this.onMouseMove_(e);
+
+      switch (e.button) {
+        case 0: {
+          this.current_.leftButton = true;
+          break;
+        }
+        case 2: {
+          this.current_.rightButton = true;
+          break;
+        }
+      }
+    }
+
+    onMouseUp_(e) {
+      this.onMouseMove_(e);
+
+      switch (e.button) {
+        case 0: {
+          this.current_.leftButton = false;
+          break;
+        }
+        case 2: {
+          this.current_.rightButton = false;
+          break;
+        }
+      }
+    }
+
+    onKeyDown_(e) {
+      this.keys_[e.keyCode] = true;
+    }
+
+    onKeyUp_(e) {
+      this.keys_[e.keyCode] = false;
+    }
+
+    key(keyCode) {
+      return !!this.keys_[keyCode];
+    }
+
+    mouseLeftReleased(checkPrevious=true) {
+      return (!this.current_.leftButton && this.previous_.leftButton);
+    }
+
+    isReady() {
+      return this.previous_ !== null;
+    }
+
+    Update(_) {
+      if (this.previous_ !== null) {
+        this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX;
+        this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY;
+
+        this.previous_ = {...this.current_};
+        this.previousKeys_ = {...this.keys_};
+      }
+    }
+  };
+
+  return {
+    PlayerInput: PlayerInput,
+    KEYS: KEYS,
+  };
+
+})();

+ 50 - 0
src/game/render/render-sky-component.js

@@ -0,0 +1,50 @@
+import {THREE} from '../../base/three-defs.js';
+
+import * as entity from "../../base/entity.js";
+import * as shaders from "./shaders.js";
+
+
+export class RenderSkyComponent extends entity.Component {
+  static CLASS_NAME = 'RenderSkyComponent';
+
+  get NAME() {
+    return RenderSkyComponent.CLASS_NAME;
+  }
+
+  constructor() {
+    super();
+  }
+
+  InitEntity() {
+    const uniforms = {
+      "time": { value: 0.0 },
+    };
+
+    const skyGeo = new THREE.SphereGeometry(5000, 32, 15);
+    const skyMat = new shaders.ShaderMaterial('SKY', {
+        uniforms: uniforms,
+        side: THREE.BackSide
+    });
+
+    this.sky_ = new THREE.Mesh(skyGeo, skyMat);
+    this.sky_.castShadow = false;
+    this.sky_.receiveShadow = false;
+
+    const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController');
+    threejs.AddSceneObject(this.sky_);
+  }
+
+  Update(timeElapsed) {
+    const player = this.FindEntity('player');
+    if (!player) {
+      return;
+    }
+    const pos = player.Position;
+
+    if (this.sky_) {
+      this.sky_.material.uniforms.time.value += timeElapsed;
+    }
+
+    this.sky_.position.copy(pos);
+  }
+}

+ 284 - 0
src/game/render/shaders.js

@@ -0,0 +1,284 @@
+import { THREE, CSMShader } from '../../base/three-defs.js';
+
+
+class ShaderManager {
+  static shaderCode = {};
+  static threejs = null;
+};
+
+export function SetThreeJS(threejs) {
+  ShaderManager.threejs = threejs;
+}
+
+export async function loadShaders() {
+  const loadText = async (url) => {
+    const d = await fetch(url);
+    return await d.text();
+  };
+
+  const globalShaders = [
+    'header.glsl',
+    'common.glsl',
+    'oklab.glsl',
+    'noise.glsl',
+    'sky.glsl',
+  ];
+
+  const globalShadersCode = [];
+  for (let i = 0; i < globalShaders.length; ++i) {
+    globalShadersCode.push(await loadText('resources/shaders/' + globalShaders[i]));
+  }
+
+  const loadShader = async (url) => {
+    const d = await fetch(url);
+    let shaderCode = '';
+    for (let i = 0; i < globalShadersCode.length; ++i) {
+      shaderCode += globalShadersCode[i] + '\n';
+    }
+    return shaderCode + '\n' + await d.text();
+  }
+
+  ShaderManager.shaderCode['PHONG'] = {
+    vsh: await loadShader('resources/shaders/phong-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/phong-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['GRASS'] = {
+    vsh: await loadShader('resources/shaders/grass-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/grass-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['TERRAIN'] = {
+    vsh: await loadShader('resources/shaders/terrain-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/terrain-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['BUGS'] = {
+    vsh: await loadShader('resources/shaders/bugs-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/bugs-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['WIND'] = {
+    vsh: await loadShader('resources/shaders/wind-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/wind-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['SKY'] = {
+    vsh: await loadShader('resources/shaders/sky-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/sky-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['WATER'] = {
+    vsh: await loadShader('resources/shaders/water-lighting-model-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/water-lighting-model-fsh.glsl'),
+  };
+
+  ShaderManager.shaderCode['WATER-TEXTURE'] = {
+    vsh: await loadShader('resources/shaders/water-texture-vsh.glsl'),
+    fsh: await loadShader('resources/shaders/water-texture-fsh.glsl'),
+  };
+} 
+
+
+export class ShaderMaterial extends THREE.ShaderMaterial {
+  constructor(shaderType, parameters) {
+    parameters.vertexShader = ShaderManager.shaderCode[shaderType].vsh;
+    parameters.fragmentShader = ShaderManager.shaderCode[shaderType].fsh;
+
+    super(parameters);
+  }
+};
+
+export class GamePBRMaterial extends THREE.MeshStandardMaterial {
+
+  #uniforms_ = {};
+  #shader_ = null;
+
+  constructor(shaderType, parameters) {
+    super(parameters);
+
+    this.#shader_ = null;
+    this.#uniforms_ = {};
+
+    ShaderManager.threejs.SetupMaterial(this);
+
+    const previousCallback = this.onBeforeCompile;
+    
+    this.onBeforeCompile = (shader) => {
+        shader.fragmentShader = ShaderManager.shaderCode[shaderType].fsh;
+        shader.vertexShader = ShaderManager.shaderCode[shaderType].vsh;
+        shader.uniforms.time = { value: 0.0 };
+        shader.uniforms.playerPos = { value: new THREE.Vector3(0.0) };
+
+        for (let k in this.#uniforms_) {
+          shader.uniforms[k] = this.#uniforms_[k];
+        }
+
+        this.#shader_ = shader;
+
+        previousCallback(shader);
+    };
+
+    this.onBeforeRender = () => {
+      if (shaderType == 'BUGS') {
+        let a = 0;
+      }
+      let a = 0;
+    }
+
+    this.customProgramCacheKey = () => {
+      let uniformStr = '';
+      for (let k in this.#uniforms_) {
+        uniformStr += k + ':' + this.#uniforms_[k].value + ';';
+      }
+      return shaderType + uniformStr;
+    }
+  }
+
+  setFloat(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec2(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec3(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec4(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setMatrix(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setTexture(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setTextureArray(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+}
+
+export class GameMaterial extends THREE.MeshPhongMaterial {
+
+  #uniforms_ = {};
+  #shader_ = null;
+
+  constructor(shaderType, parameters) {
+    super(parameters);
+
+    this.#shader_ = null;
+    this.#uniforms_ = {};
+
+    ShaderManager.threejs.SetupMaterial(this);
+
+    const previousCallback = this.onBeforeCompile;
+
+    this.onBeforeCompile = (shader) => {
+        shader.fragmentShader = ShaderManager.shaderCode[shaderType].fsh;
+        shader.vertexShader = ShaderManager.shaderCode[shaderType].vsh;
+        shader.uniforms.time = { value: 0.0 };
+        shader.uniforms.playerPos = { value: new THREE.Vector3(0.0) };
+
+        for (let k in this.#uniforms_) {
+          shader.uniforms[k] = this.#uniforms_[k];
+        }
+
+        this.#shader_ = shader;
+
+        previousCallback(shader);
+    };
+
+    this.onBeforeRender = () => {
+      if (shaderType == 'BUGS') {
+        let a = 0;
+      }
+      let a = 0;
+    }
+
+    this.customProgramCacheKey = () => {
+      let uniformStr = '';
+      for (let k in this.#uniforms_) {
+        uniformStr += k + ':' + this.#uniforms_[k].value + ';';
+      }
+      return shaderType + uniformStr;
+    }
+  }
+
+  setFloat(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec2(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec3(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setVec4(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setMatrix(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setTexture(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+
+  setTextureArray(name, value) {
+    this.#uniforms_[name] = { value: value };
+    if (this.#shader_) {
+      this.#shader_.uniforms[name] = this.#uniforms_[name];
+    }
+  }
+}
+
+

+ 65 - 0
src/game/spawners.js

@@ -0,0 +1,65 @@
+import {THREE} from '../base/three-defs.js';
+
+import * as entity from '../base/entity.js';
+
+import {player_input} from './player-input.js';
+import {player_entity} from './player-entity.js'
+import {third_person_camera} from './third-person-camera.js';
+import {demo_builder} from '../demo-builder.js';
+
+
+export const spawners = (() => {
+
+  class PlayerSpawner extends entity.Component {
+    static CLASS_NAME = 'PlayerSpawner';
+
+    get NAME() {
+      return PlayerSpawner.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const player = new entity.Entity('player');
+      player.SetPosition(new THREE.Vector3(316, 15, -560));
+      player.AddComponent(new player_input.PlayerInput(this.params_));
+      player.AddComponent(new player_entity.BasicCharacterController(this.params_));
+      player.AddComponent(new third_person_camera.ThirdPersonCamera(this.params_));
+      player.Init();
+      player.SetQuaternion(new THREE.Quaternion(0, 0.448, 0, -0.892));
+
+      return player;
+    }
+  };
+
+  class DemoSpawner extends entity.Component {
+    static CLASS_NAME = 'DemoSpawner';
+
+    get NAME() {
+      return DemoSpawner.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+      this.params_ = params;
+    }
+
+    Spawn() {
+      const e = new entity.Entity();
+      e.SetPosition(new THREE.Vector3(0, 0, 0));
+      e.AddComponent(new demo_builder.DemoBuilder(this.params_));
+      e.Init();
+
+      return e;
+    }
+  };
+
+
+  return {
+    PlayerSpawner: PlayerSpawner,
+    DemoSpawner: DemoSpawner,
+  };
+})();

+ 79 - 0
src/game/third-person-camera.js

@@ -0,0 +1,79 @@
+import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
+import * as entity from '../base/entity.js';
+import * as passes from '../base/passes.js';
+import * as terrain_component from '../base/render/terrain-component.js';
+
+export const third_person_camera = (() => {
+  
+  class ThirdPersonCamera extends entity.Component {
+    static CLASS_NAME = 'ThirdPersonCamera';
+
+    get NAME() {
+      return ThirdPersonCamera.CLASS_NAME;
+    }
+
+    constructor(params) {
+      super();
+
+      this.params_ = params;
+      this.camera_ = params.camera;
+
+      this.currentPosition_ = new THREE.Vector3();
+      this.currentLookat_ = new THREE.Vector3();
+    }
+
+    InitEntity() {
+      this.SetPass(passes.Passes.CAMERA);
+    }
+
+    CalculateIdealOffset_() {
+      const idealOffset = new THREE.Vector3(0, 0.5, -8);
+      // idealOffset.multiplyScalar(10.0);
+      idealOffset.applyQuaternion(this.Parent.Quaternion);
+      idealOffset.add(this.Parent.Position);
+
+      // idealOffset.y = Math.min(idealOffset.y, height + 1.5);
+      // idealOffset.y += (this.Parent.Position.y - 1.5 + height);
+
+      return idealOffset;
+    }
+
+    CalculateIdealLookat_() {
+      const idealLookat = new THREE.Vector3(0, 1.25, 4);
+      idealLookat.applyQuaternion(this.Parent.Quaternion);
+      idealLookat.add(this.Parent.Position);
+      return idealLookat;
+    }
+
+    Update(timeElapsed) {
+      const terrain = this.FindEntity('terrain');
+      if (terrain) {
+        const terrainComponent = terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME);
+        if (!terrainComponent.IsReady()) {
+          return;
+        }
+
+        const idealOffset = this.CalculateIdealOffset_();
+        const idealLookat = this.CalculateIdealLookat_();
+  
+        const height = terrainComponent.GetHeight(idealOffset.x, idealOffset.z);
+        idealOffset.y = height + 4.25;
+
+        // const t = 0.05;
+        // const t = 4.0 * timeElapsed;
+        const t = 1.0 - Math.pow(0.0001, timeElapsed);
+  
+        this.currentPosition_.lerp(idealOffset, t);
+        this.currentLookat_.lerp(idealLookat, t);
+  
+        this.camera_.position.copy(this.currentPosition_);
+        this.camera_.lookAt(this.currentLookat_); 
+      }
+    }
+  }
+
+  return {
+    ThirdPersonCamera: ThirdPersonCamera
+  };
+
+})();

+ 97 - 0
src/main.js

@@ -0,0 +1,97 @@
+import * as entity_manager from './base/entity-manager.js';
+import * as entity from './base/entity.js';
+
+import {load_controller} from './base/load-controller.js';
+import {spawners} from './game/spawners.js';
+
+import {threejs_component} from './base/threejs-component.js';
+
+import * as render_sky_component from './game/render/render-sky-component.js';
+import * as shaders from './game/render/shaders.js';
+
+
+class QuickFPS1 {
+  constructor() {
+  }
+
+  async Init() {
+    await shaders.loadShaders();
+
+    this.Initialize_();
+  }
+
+  Initialize_() {
+    this.entityManager_ = entity_manager.EntityManager.Init();
+
+    this.OnGameStarted_();
+  }
+
+  OnGameStarted_() {
+    this.LoadControllers_();
+
+    this.previousRAF_ = null;
+    this.RAF_();
+  }
+
+  LoadControllers_() {
+    const threejs = new entity.Entity('threejs');
+    threejs.AddComponent(new threejs_component.ThreeJSController());
+    threejs.Init();
+
+    const sky = new entity.Entity();
+    sky.AddComponent(new render_sky_component.RenderSkyComponent());
+    sky.Init(threejs);
+
+    // Hack
+    this.camera_ = threejs.GetComponent('ThreeJSController').Camera;
+    this.threejs_ = threejs.GetComponent('ThreeJSController');
+
+    const loader = new entity.Entity('loader');
+    loader.AddComponent(new load_controller.LoadController());
+    loader.Init();
+
+    const basicParams = {
+      camera: this.camera_,
+    };
+
+    const spawner = new entity.Entity('spawners');
+    spawner.AddComponent(new spawners.PlayerSpawner(basicParams));
+    spawner.AddComponent(new spawners.DemoSpawner(basicParams));
+    spawner.Init();
+
+    spawner.GetComponent('PlayerSpawner').Spawn();
+    spawner.GetComponent('DemoSpawner').Spawn();
+  }
+
+  RAF_() {
+    requestAnimationFrame((t) => {
+      if (this.previousRAF_ === null) {
+        this.previousRAF_ = t;
+      } else {
+        this.Step_(t - this.previousRAF_);
+        this.previousRAF_ = t;
+      }
+
+      setTimeout(() => {
+        this.RAF_();
+      }, 1);
+    });
+  }
+
+  Step_(timeElapsed) {
+    const timeElapsedS = Math.min(1.0 / 30.0, timeElapsed * 0.001);
+
+    this.entityManager_.Update(timeElapsedS);
+
+    this.threejs_.Render(timeElapsedS);
+  }
+}
+
+
+let _APP = null;
+
+window.addEventListener('DOMContentLoaded', async () => {
+  _APP = new QuickFPS1();
+  await _APP.Init();
+});
+let a = 0;

+ 22 - 0
vite.config.js

@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import wasm from 'vite-plugin-wasm';
+import topLevelAwait from 'vite-plugin-top-level-await';
+import solidPlugin from 'vite-plugin-solid';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+    plugins: [wasm(), topLevelAwait(), solidPlugin()],
+    resolve: {
+        alias: {
+        },
+    },
+    build: {
+        sourcemap: true,
+    },
+    server: {
+        port: 5200,
+        hmr: {
+            clientPort: 5200,
+        }
+    }
+});