├── LICENSE ├── base.css ├── index.html ├── resources ├── README.txt ├── dirt_01_diffuse-1024.png ├── dirt_01_normal-1024.jpg ├── grass1-albedo-512.jpg ├── grass1-albedo3-1024.png ├── grass1-normal-1024.jpg ├── rock-snow-ice-albedo-1024.png ├── rock-snow-ice-normal-1024.jpg ├── rough-wet-cobble-albedo-1024.png ├── rough-wet-cobble-normal-1024.jpg ├── sandy-rocks1-albedo-1024.png ├── sandy-rocks1-normal-1024.jpg ├── sandyground-albedo-1024.png ├── sandyground-normal-1024.jpg ├── simplex-noise.png ├── snow-packed-albedo-1024.png ├── snow-packed-normal-1024.jpg ├── space-negx.jpg ├── space-negy.jpg ├── space-negz.jpg ├── space-posx.jpg ├── space-posy.jpg ├── space-posz.jpg ├── worn-bumpy-rock-albedo-1024.png ├── worn-bumpy-rock-albedo-512.jpg └── worn-bumpy-rock-normal-1024.jpg └── src ├── camera-track.js ├── controls.js ├── demo.js ├── game.js ├── graphics.js ├── main.js ├── math.js ├── noise.js ├── perlin-noise.js ├── quadtree.js ├── scattering-shader.js ├── simplex-noise.js ├── sky.js ├── spline.js ├── terrain-builder-threaded-worker.js ├── terrain-builder-threaded.js ├── terrain-builder.js ├── terrain-chunk.js ├── terrain-constants.js ├── terrain-shader.js ├── terrain.js ├── texture-splatter.js ├── textures.js └── utils.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 simondevyoutube 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base.css: -------------------------------------------------------------------------------- 1 | .header { 2 | font-size: 3em; 3 | color: white; 4 | background: #404040; 5 | text-align: center; 6 | height: 2.5em; 7 | text-shadow: 4px 4px 4px black; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | #error { 14 | font-size: 2em; 15 | color: red; 16 | height: 50px; 17 | text-shadow: 2px 2px 2px black; 18 | margin: 2em; 19 | display: none; 20 | } 21 | 22 | .container { 23 | width: 100% !important; 24 | height: 100% !important; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-direction: column; 29 | position: absolute; 30 | } 31 | 32 | .visible { 33 | display: block; 34 | } 35 | 36 | #target { 37 | width: 100% !important; 38 | height: 100% !important; 39 | position: absolute; 40 | } 41 | 42 | body { 43 | background: #000000; 44 | margin: 0; 45 | padding: 0; 46 | overscroll-behavior: none; 47 | } 48 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Procedural Planet 5 | 6 | 7 | 8 |
9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/README.txt: -------------------------------------------------------------------------------- 1 | Most of these textures were taken from freepbr.com or https://opengameart.org/content/36-free-ground-textures-diffuse-normals. 2 | 3 | They were all 2kx2k so they've been resaved as 1k. 4 | -------------------------------------------------------------------------------- /resources/dirt_01_diffuse-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/dirt_01_diffuse-1024.png -------------------------------------------------------------------------------- /resources/dirt_01_normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/dirt_01_normal-1024.jpg -------------------------------------------------------------------------------- /resources/grass1-albedo-512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/grass1-albedo-512.jpg -------------------------------------------------------------------------------- /resources/grass1-albedo3-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/grass1-albedo3-1024.png -------------------------------------------------------------------------------- /resources/grass1-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/grass1-normal-1024.jpg -------------------------------------------------------------------------------- /resources/rock-snow-ice-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/rock-snow-ice-albedo-1024.png -------------------------------------------------------------------------------- /resources/rock-snow-ice-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/rock-snow-ice-normal-1024.jpg -------------------------------------------------------------------------------- /resources/rough-wet-cobble-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/rough-wet-cobble-albedo-1024.png -------------------------------------------------------------------------------- /resources/rough-wet-cobble-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/rough-wet-cobble-normal-1024.jpg -------------------------------------------------------------------------------- /resources/sandy-rocks1-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/sandy-rocks1-albedo-1024.png -------------------------------------------------------------------------------- /resources/sandy-rocks1-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/sandy-rocks1-normal-1024.jpg -------------------------------------------------------------------------------- /resources/sandyground-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/sandyground-albedo-1024.png -------------------------------------------------------------------------------- /resources/sandyground-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/sandyground-normal-1024.jpg -------------------------------------------------------------------------------- /resources/simplex-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/simplex-noise.png -------------------------------------------------------------------------------- /resources/snow-packed-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/snow-packed-albedo-1024.png -------------------------------------------------------------------------------- /resources/snow-packed-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/snow-packed-normal-1024.jpg -------------------------------------------------------------------------------- /resources/space-negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-negx.jpg -------------------------------------------------------------------------------- /resources/space-negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-negy.jpg -------------------------------------------------------------------------------- /resources/space-negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-negz.jpg -------------------------------------------------------------------------------- /resources/space-posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-posx.jpg -------------------------------------------------------------------------------- /resources/space-posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-posy.jpg -------------------------------------------------------------------------------- /resources/space-posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/space-posz.jpg -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/worn-bumpy-rock-albedo-1024.png -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-albedo-512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/worn-bumpy-rock-albedo-512.jpg -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part8/eaed204709a716d560739111b04930759b07c223/resources/worn-bumpy-rock-normal-1024.jpg -------------------------------------------------------------------------------- /src/camera-track.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {spline} from './spline.js'; 4 | 5 | 6 | export const camera_track = (function() { 7 | 8 | class _CameraTrack { 9 | constructor(params) { 10 | this._params = params; 11 | this._currentTime = 0.0; 12 | 13 | const lerp = (t, p1, p2) => { 14 | const p = new THREE.Vector3().lerpVectors(p1.pos, p2.pos, t); 15 | const q = p1.rot.clone().slerp(p2.rot, t); 16 | 17 | return {pos: p, rot: q}; 18 | }; 19 | this._spline = new spline.LinearSpline(lerp); 20 | 21 | for (let p of params.points) { 22 | this._spline.AddPoint(p.time, p.data); 23 | } 24 | } 25 | 26 | Update(timeInSeconds) { 27 | this._currentTime += timeInSeconds; 28 | 29 | const r = this._spline.Get(this._currentTime); 30 | 31 | this._params.camera.position.copy(r.pos); 32 | this._params.camera.quaternion.copy(r.rot); 33 | } 34 | }; 35 | 36 | return { 37 | CameraTrack: _CameraTrack, 38 | }; 39 | })(); 40 | -------------------------------------------------------------------------------- /src/controls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/controls/PointerLockControls.js'; 3 | import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/controls/OrbitControls.js'; 4 | 5 | 6 | export const controls = (function() { 7 | 8 | class _OrbitControls { 9 | constructor(params) { 10 | this._params = params; 11 | this._Init(params); 12 | } 13 | 14 | _Init(params) { 15 | this._controls = new OrbitControls(params.camera, params.domElement); 16 | this._controls.target.set(0, 0, 0); 17 | this._controls.update(); 18 | } 19 | 20 | Update() { 21 | } 22 | } 23 | 24 | // FPSControls was adapted heavily from a threejs example. Movement control 25 | // and collision detection was completely rewritten, but credit to original 26 | // class for the setup code. 27 | class _FPSControls { 28 | constructor(params) { 29 | this._cells = params.cells; 30 | this._Init(params); 31 | } 32 | 33 | _Init(params) { 34 | this._params = params; 35 | this._radius = 2; 36 | this._enabled = false; 37 | this._move = { 38 | forward: false, 39 | backward: false, 40 | left: false, 41 | right: false, 42 | up: false, 43 | down: false, 44 | }; 45 | this._standing = true; 46 | this._velocity = new THREE.Vector3(0, 0, 0); 47 | this._decceleration = new THREE.Vector3(-10, -10, -10); 48 | this._acceleration = new THREE.Vector3(5000, 5000, 5000); 49 | 50 | this._SetupPointerLock(); 51 | 52 | this._controls = new PointerLockControls( 53 | params.camera, document.body); 54 | params.scene.add(this._controls.getObject()); 55 | 56 | const controlObject = this._controls.getObject(); 57 | this._position = new THREE.Vector3(); 58 | this._rotation = new THREE.Quaternion(); 59 | this._position.copy(controlObject.position); 60 | this._rotation.copy(controlObject.quaternion); 61 | 62 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false); 63 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false); 64 | 65 | this._InitGUI(); 66 | } 67 | 68 | _InitGUI() { 69 | this._params.guiParams.camera = { 70 | acceleration_x: 5000, 71 | }; 72 | 73 | const rollup = this._params.gui.addFolder('Camera.FPS'); 74 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 500000.0).onChange( 75 | () => { 76 | this._acceleration.set( 77 | this._params.guiParams.camera.acceleration_x, 78 | this._params.guiParams.camera.acceleration_x, 79 | this._params.guiParams.camera.acceleration_x); 80 | }); 81 | } 82 | 83 | _onKeyDown(event) { 84 | switch (event.keyCode) { 85 | case 38: // up 86 | case 87: // w 87 | this._move.forward = true; 88 | break; 89 | case 37: // left 90 | case 65: // a 91 | this._move.left = true; 92 | break; 93 | case 40: // down 94 | case 83: // s 95 | this._move.backward = true; 96 | break; 97 | case 39: // right 98 | case 68: // d 99 | this._move.right = true; 100 | break; 101 | case 33: // PG_UP 102 | this._move.up = true; 103 | break; 104 | case 34: // PG_DOWN 105 | this._move.down = true; 106 | break; 107 | } 108 | } 109 | 110 | _onKeyUp(event) { 111 | switch(event.keyCode) { 112 | case 38: // up 113 | case 87: // w 114 | this._move.forward = false; 115 | break; 116 | case 37: // left 117 | case 65: // a 118 | this._move.left = false; 119 | break; 120 | case 40: // down 121 | case 83: // s 122 | this._move.backward = false; 123 | break; 124 | case 39: // right 125 | case 68: // d 126 | this._move.right = false; 127 | break; 128 | case 33: // PG_UP 129 | this._move.up = false; 130 | break; 131 | case 34: // PG_DOWN 132 | this._move.down = false; 133 | break; 134 | } 135 | } 136 | 137 | _SetupPointerLock() { 138 | const hasPointerLock = ( 139 | 'pointerLockElement' in document || 140 | 'mozPointerLockElement' in document || 141 | 'webkitPointerLockElement' in document); 142 | if (hasPointerLock) { 143 | const lockChange = (event) => { 144 | if (document.pointerLockElement === document.body || 145 | document.mozPointerLockElement === document.body || 146 | document.webkitPointerLockElement === document.body ) { 147 | this._enabled = true; 148 | this._controls.enabled = true; 149 | } else { 150 | this._controls.enabled = false; 151 | } 152 | }; 153 | const lockError = (event) => { 154 | console.log(event); 155 | }; 156 | 157 | document.addEventListener('pointerlockchange', lockChange, false); 158 | document.addEventListener('webkitpointerlockchange', lockChange, false); 159 | document.addEventListener('mozpointerlockchange', lockChange, false); 160 | document.addEventListener('pointerlockerror', lockError, false); 161 | document.addEventListener('mozpointerlockerror', lockError, false); 162 | document.addEventListener('webkitpointerlockerror', lockError, false); 163 | 164 | document.getElementById('target').addEventListener('click', (event) => { 165 | document.body.requestPointerLock = ( 166 | document.body.requestPointerLock || 167 | document.body.mozRequestPointerLock || 168 | document.body.webkitRequestPointerLock); 169 | 170 | if (/Firefox/i.test(navigator.userAgent)) { 171 | const fullScreenChange = (event) => { 172 | if (document.fullscreenElement === document.body || 173 | document.mozFullscreenElement === document.body || 174 | document.mozFullScreenElement === document.body) { 175 | document.removeEventListener('fullscreenchange', fullScreenChange); 176 | document.removeEventListener('mozfullscreenchange', fullScreenChange); 177 | document.body.requestPointerLock(); 178 | } 179 | }; 180 | document.addEventListener( 181 | 'fullscreenchange', fullScreenChange, false); 182 | document.addEventListener( 183 | 'mozfullscreenchange', fullScreenChange, false); 184 | document.body.requestFullscreen = ( 185 | document.body.requestFullscreen || 186 | document.body.mozRequestFullscreen || 187 | document.body.mozRequestFullScreen || 188 | document.body.webkitRequestFullscreen); 189 | document.body.requestFullscreen(); 190 | } else { 191 | document.body.requestPointerLock(); 192 | } 193 | }, false); 194 | } 195 | } 196 | 197 | _FindIntersections(boxes, position) { 198 | const sphere = new THREE.Sphere(position, this._radius); 199 | 200 | const intersections = boxes.filter(b => { 201 | return sphere.intersectsBox(b); 202 | }); 203 | 204 | return intersections; 205 | } 206 | 207 | Update(timeInSeconds) { 208 | if (!this._enabled) { 209 | return; 210 | } 211 | 212 | const frameDecceleration = new THREE.Vector3( 213 | this._velocity.x * this._decceleration.x, 214 | this._velocity.y * this._decceleration.y, 215 | this._velocity.z * this._decceleration.z 216 | ); 217 | frameDecceleration.multiplyScalar(timeInSeconds); 218 | 219 | this._velocity.add(frameDecceleration); 220 | 221 | if (this._move.forward) { 222 | this._velocity.z -= this._acceleration.z * timeInSeconds; 223 | } 224 | if (this._move.backward) { 225 | this._velocity.z += this._acceleration.z * timeInSeconds; 226 | } 227 | if (this._move.left) { 228 | this._velocity.x -= this._acceleration.x * timeInSeconds; 229 | } 230 | if (this._move.right) { 231 | this._velocity.x += this._acceleration.x * timeInSeconds; 232 | } 233 | if (this._move.up) { 234 | this._velocity.y += this._acceleration.y * timeInSeconds; 235 | } 236 | if (this._move.down) { 237 | this._velocity.y -= this._acceleration.y * timeInSeconds; 238 | } 239 | 240 | const controlObject = this._controls.getObject(); 241 | 242 | const oldPosition = new THREE.Vector3(); 243 | oldPosition.copy(controlObject.position); 244 | 245 | const forward = new THREE.Vector3(0, 0, 1); 246 | forward.applyQuaternion(controlObject.quaternion); 247 | forward.normalize(); 248 | 249 | const updown = new THREE.Vector3(0, 1, 0); 250 | 251 | const sideways = new THREE.Vector3(1, 0, 0); 252 | sideways.applyQuaternion(controlObject.quaternion); 253 | sideways.normalize(); 254 | 255 | sideways.multiplyScalar(this._velocity.x * timeInSeconds); 256 | updown.multiplyScalar(this._velocity.y * timeInSeconds); 257 | forward.multiplyScalar(this._velocity.z * timeInSeconds); 258 | 259 | controlObject.position.add(forward); 260 | controlObject.position.add(sideways); 261 | controlObject.position.add(updown); 262 | 263 | // this._position.lerp(controlObject.position, 0.15); 264 | this._rotation.slerp(controlObject.quaternion, 0.15); 265 | 266 | // controlObject.position.copy(this._position); 267 | controlObject.quaternion.copy(this._rotation); 268 | } 269 | }; 270 | 271 | class _ShipControls { 272 | constructor(params) { 273 | this._Init(params); 274 | } 275 | 276 | _Init(params) { 277 | this._params = params; 278 | this._radius = 2; 279 | this._enabled = false; 280 | this._move = { 281 | forward: false, 282 | backward: false, 283 | left: false, 284 | right: false, 285 | up: false, 286 | down: false, 287 | rocket: false, 288 | }; 289 | this._velocity = new THREE.Vector3(0, 0, 0); 290 | this._decceleration = new THREE.Vector3(-0.001, -0.0001, -1); 291 | this._acceleration = new THREE.Vector3(100, 0.1, 25000); 292 | 293 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false); 294 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false); 295 | 296 | this._InitGUI(); 297 | } 298 | 299 | _InitGUI() { 300 | this._params.guiParams.camera = { 301 | acceleration_x: 100, 302 | acceleration_y: 0.1, 303 | }; 304 | 305 | const rollup = this._params.gui.addFolder('Camera.Ship'); 306 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange( 307 | () => { 308 | this._acceleration.x = this._params.guiParams.camera.acceleration_x; 309 | }); 310 | rollup.add(this._params.guiParams.camera, "acceleration_y", 0.001, 0.1).onChange( 311 | () => { 312 | this._acceleration.y = this._params.guiParams.camera.acceleration_y; 313 | }); 314 | } 315 | 316 | _onKeyDown(event) { 317 | switch (event.keyCode) { 318 | case 87: // w 319 | this._move.forward = true; 320 | break; 321 | case 65: // a 322 | this._move.left = true; 323 | break; 324 | case 83: // s 325 | this._move.backward = true; 326 | break; 327 | case 68: // d 328 | this._move.right = true; 329 | break; 330 | case 33: // PG_UP 331 | this._acceleration.x *= 1.1; 332 | break; 333 | case 34: // PG_DOWN 334 | this._acceleration.x *= 0.8; 335 | break; 336 | case 32: // SPACE 337 | this._move.rocket = true; 338 | break; 339 | case 38: // up 340 | case 37: // left 341 | case 40: // down 342 | case 39: // right 343 | break; 344 | } 345 | } 346 | 347 | _onKeyUp(event) { 348 | switch(event.keyCode) { 349 | case 87: // w 350 | this._move.forward = false; 351 | break; 352 | case 65: // a 353 | this._move.left = false; 354 | break; 355 | case 83: // s 356 | this._move.backward = false; 357 | break; 358 | case 68: // d 359 | this._move.right = false; 360 | break; 361 | case 33: // PG_UP 362 | break; 363 | case 34: // PG_DOWN 364 | break; 365 | case 32: // SPACE 366 | this._move.rocket = false; 367 | break; 368 | case 38: // up 369 | case 37: // left 370 | case 40: // down 371 | case 39: // right 372 | break; 373 | } 374 | } 375 | 376 | Update(timeInSeconds) { 377 | const frameDecceleration = new THREE.Vector3( 378 | this._velocity.x * this._decceleration.x, 379 | this._velocity.y * this._decceleration.y, 380 | this._velocity.z * this._decceleration.z 381 | ); 382 | frameDecceleration.multiplyScalar(timeInSeconds); 383 | 384 | this._velocity.add(frameDecceleration); 385 | 386 | const controlObject = this._params.camera; 387 | const _Q = new THREE.Quaternion(); 388 | const _A = new THREE.Vector3(); 389 | const _R = controlObject.quaternion.clone(); 390 | 391 | if (this._move.forward) { 392 | _A.set(1, 0, 0); 393 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y); 394 | _R.multiply(_Q); 395 | } 396 | if (this._move.backward) { 397 | _A.set(1, 0, 0); 398 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y); 399 | _R.multiply(_Q); 400 | } 401 | if (this._move.left) { 402 | _A.set(0, 0, 1); 403 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y); 404 | _R.multiply(_Q); 405 | } 406 | if (this._move.right) { 407 | _A.set(0, 0, 1); 408 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y); 409 | _R.multiply(_Q); 410 | } 411 | if (this._move.rocket) { 412 | this._velocity.z -= this._acceleration.x * timeInSeconds; 413 | } 414 | 415 | controlObject.quaternion.copy(_R); 416 | 417 | const oldPosition = new THREE.Vector3(); 418 | oldPosition.copy(controlObject.position); 419 | 420 | const forward = new THREE.Vector3(0, 0, 1); 421 | forward.applyQuaternion(controlObject.quaternion); 422 | //forward.y = 0; 423 | forward.normalize(); 424 | 425 | const updown = new THREE.Vector3(0, 1, 0); 426 | 427 | const sideways = new THREE.Vector3(1, 0, 0); 428 | sideways.applyQuaternion(controlObject.quaternion); 429 | sideways.normalize(); 430 | 431 | sideways.multiplyScalar(this._velocity.x * timeInSeconds); 432 | updown.multiplyScalar(this._velocity.y * timeInSeconds); 433 | forward.multiplyScalar(this._velocity.z * timeInSeconds); 434 | 435 | controlObject.position.add(forward); 436 | controlObject.position.add(sideways); 437 | controlObject.position.add(updown); 438 | 439 | oldPosition.copy(controlObject.position); 440 | } 441 | }; 442 | 443 | return { 444 | ShipControls: _ShipControls, 445 | FPSControls: _FPSControls, 446 | OrbitControls: _OrbitControls, 447 | }; 448 | })(); 449 | -------------------------------------------------------------------------------- /src/demo.js: -------------------------------------------------------------------------------- 1 | import {game} from './game.js'; 2 | import {graphics} from './graphics.js'; 3 | import {math} from './math.js'; 4 | import {noise} from './noise.js'; 5 | 6 | 7 | window.onload = function() { 8 | function _Perlin() { 9 | const canvas = document.getElementById("canvas"); 10 | const context = canvas.getContext("2d"); 11 | 12 | const imgData = context.createImageData(canvas.width, canvas.height); 13 | 14 | const params = { 15 | scale: 32, 16 | noiseType: 'simplex', 17 | persistence: 0.5, 18 | octaves: 1, 19 | lacunarity: 1, 20 | exponentiation: 1, 21 | height: 255 22 | }; 23 | const noiseGen = new noise.Noise(params); 24 | 25 | for (let x = 0; x < canvas.width; x++) { 26 | for (let y = 0; y < canvas.height; y++) { 27 | const pixelIndex = (y * canvas.width + x) * 4; 28 | 29 | const n = noiseGen.Get(x, y); 30 | 31 | imgData.data[pixelIndex] = n; 32 | imgData.data[pixelIndex+1] = n; 33 | imgData.data[pixelIndex+2] = n; 34 | imgData.data[pixelIndex+3] = 255; 35 | } 36 | } 37 | 38 | context.putImageData(imgData, 0, 0); 39 | } 40 | 41 | 42 | function _Randomness() { 43 | const canvas = document.getElementById("canvas"); 44 | const context = canvas.getContext("2d"); 45 | 46 | const imgData = context.createImageData(canvas.width, canvas.height); 47 | 48 | const params = { 49 | scale: 32, 50 | noiseType: 'simplex', 51 | persistence: 0.5, 52 | octaves: 1, 53 | lacunarity: 2, 54 | exponentiation: 1, 55 | height: 1 56 | }; 57 | const noiseGen = new noise.Noise(params); 58 | let foo = ''; 59 | 60 | for (let x = 0; x < canvas.width; x++) { 61 | for (let y = 0; y < canvas.height; y++) { 62 | const pixelIndex = (y * canvas.width + x) * 4; 63 | 64 | const n = noiseGen.Get(x, y); 65 | if (x == 0) { 66 | foo += n + '\n'; 67 | } 68 | 69 | imgData.data[pixelIndex] = n; 70 | imgData.data[pixelIndex+1] = n; 71 | imgData.data[pixelIndex+2] = n; 72 | imgData.data[pixelIndex+3] = 255; 73 | } 74 | } 75 | console.log(foo); 76 | 77 | context.putImageData(imgData, 0, 0); 78 | } 79 | 80 | _Randomness(); 81 | 82 | }; 83 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/WebGL.js'; 3 | import {graphics} from './graphics.js'; 4 | 5 | 6 | export const game = (function() { 7 | return { 8 | Game: class { 9 | constructor() { 10 | this._Initialize(); 11 | } 12 | 13 | _Initialize() { 14 | this._graphics = new graphics.Graphics(this); 15 | if (!this._graphics.Initialize()) { 16 | this._DisplayError('WebGL2 is not available.'); 17 | return; 18 | } 19 | 20 | this._previousRAF = null; 21 | this._minFrameTime = 1.0 / 10.0; 22 | this._entities = {}; 23 | 24 | this._OnInitialize(); 25 | this._RAF(); 26 | } 27 | 28 | _DisplayError(errorText) { 29 | const error = document.getElementById('error'); 30 | error.innerText = errorText; 31 | } 32 | 33 | _RAF() { 34 | requestAnimationFrame((t) => { 35 | if (this._previousRAF === null) { 36 | this._previousRAF = t; 37 | } 38 | this._Render(t - this._previousRAF); 39 | this._previousRAF = t; 40 | }); 41 | } 42 | 43 | _AddEntity(name, entity, priority) { 44 | this._entities[name] = {entity: entity, priority: priority}; 45 | } 46 | 47 | _StepEntities(timeInSeconds) { 48 | const sortedEntities = Object.values(this._entities); 49 | 50 | sortedEntities.sort((a, b) => { 51 | return a.priority - b.priority; 52 | }) 53 | 54 | for (let s of sortedEntities) { 55 | s.entity.Update(timeInSeconds); 56 | } 57 | } 58 | 59 | _Render(timeInMS) { 60 | const timeInSeconds = Math.min(timeInMS * 0.001, this._minFrameTime); 61 | 62 | this._OnStep(timeInSeconds); 63 | this._StepEntities(timeInSeconds); 64 | this._graphics.Render(timeInSeconds); 65 | 66 | this._RAF(); 67 | } 68 | } 69 | }; 70 | })(); 71 | -------------------------------------------------------------------------------- /src/graphics.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | import Stats from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/stats.module.js'; 3 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/WebGL.js'; 4 | 5 | import {RenderPass} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/postprocessing/RenderPass.js'; 6 | import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/postprocessing/ShaderPass.js'; 7 | import {CopyShader} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/shaders/CopyShader.js'; 8 | import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/shaders/FXAAShader.js'; 9 | import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/postprocessing/EffectComposer.js'; 10 | 11 | import {scattering_shader} from './scattering-shader.js'; 12 | 13 | 14 | export const graphics = (function() { 15 | 16 | function _GetImageData(image) { 17 | const canvas = document.createElement('canvas'); 18 | canvas.width = image.width; 19 | canvas.height = image.height; 20 | 21 | const context = canvas.getContext( '2d' ); 22 | context.drawImage(image, 0, 0); 23 | 24 | return context.getImageData(0, 0, image.width, image.height); 25 | } 26 | 27 | function _GetPixel(imagedata, x, y) { 28 | const position = (x + imagedata.width * y) * 4; 29 | const data = imagedata.data; 30 | return { 31 | r: data[position], 32 | g: data[position + 1], 33 | b: data[position + 2], 34 | a: data[position + 3] 35 | }; 36 | } 37 | 38 | class _Graphics { 39 | constructor(game) { 40 | } 41 | 42 | Initialize() { 43 | if (!WEBGL.isWebGL2Available()) { 44 | return false; 45 | } 46 | 47 | const canvas = document.createElement('canvas'); 48 | const context = canvas.getContext('webgl2', {alpha: false}); 49 | 50 | this._threejs = new THREE.WebGLRenderer({ 51 | canvas: canvas, 52 | context: context, 53 | }); 54 | this._threejs.setPixelRatio(window.devicePixelRatio); 55 | this._threejs.setSize(window.innerWidth, window.innerHeight); 56 | this._threejs.autoClear = false; 57 | 58 | const target = document.getElementById('target'); 59 | target.appendChild(this._threejs.domElement); 60 | 61 | this._stats = new Stats(); 62 | // target.appendChild(this._stats.dom); 63 | 64 | window.addEventListener('resize', () => { 65 | this._OnWindowResize(); 66 | }, false); 67 | 68 | const fov = 60; 69 | const aspect = 1920 / 1080; 70 | const near = 1.0; 71 | const far = 1000000.0; 72 | this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 73 | this._camera.position.set(75, 20, 0); 74 | 75 | this._scene = new THREE.Scene(); 76 | this._scene.background = new THREE.Color(0xaaaaaa); 77 | 78 | const renderPass = new RenderPass(this._scene, this._camera); 79 | const fxaaPass = new ShaderPass(FXAAShader); 80 | // const depthPass = new ShaderPass(scattering_shader.Shader); 81 | 82 | // this._depthPass = depthPass; 83 | 84 | this._composer = new EffectComposer(this._threejs); 85 | this._composer.addPass(renderPass); 86 | this._composer.addPass(fxaaPass); 87 | //this._composer.addPass(depthPass); 88 | 89 | this._target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight); 90 | this._target.texture.format = THREE.RGBFormat; 91 | this._target.texture.minFilter = THREE.NearestFilter; 92 | this._target.texture.magFilter = THREE.NearestFilter; 93 | this._target.texture.generateMipmaps = false; 94 | this._target.stencilBuffer = false; 95 | this._target.depthBuffer = true; 96 | this._target.depthTexture = new THREE.DepthTexture(); 97 | this._target.depthTexture.format = THREE.DepthFormat; 98 | this._target.depthTexture.type = THREE.FloatType; 99 | 100 | this._threejs.setRenderTarget(this._target); 101 | 102 | this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 103 | this._depthPass = new THREE.ShaderMaterial( { 104 | vertexShader: scattering_shader.VS, 105 | fragmentShader: scattering_shader.PS, 106 | uniforms: { 107 | cameraNear: { value: this.Camera.near }, 108 | cameraFar: { value: this.Camera.far }, 109 | cameraPosition: { value: this.Camera.position }, 110 | cameraForward: { value: null }, 111 | tDiffuse: { value: null }, 112 | tDepth: { value: null }, 113 | inverseProjection: { value: null }, 114 | inverseView: { value: null }, 115 | planetPosition: { value: null }, 116 | planetRadius: { value: null }, 117 | atmosphereRadius: { value: null }, 118 | } 119 | } ); 120 | var postPlane = new THREE.PlaneBufferGeometry( 2, 2 ); 121 | var postQuad = new THREE.Mesh( postPlane, this._depthPass ); 122 | this._postScene = new THREE.Scene(); 123 | this._postScene.add( postQuad ); 124 | 125 | this._CreateLights(); 126 | 127 | return true; 128 | } 129 | 130 | 131 | _CreateLights() { 132 | let light = new THREE.DirectionalLight(0xFFFFFF, 1); 133 | light.position.set(100, 100, -100); 134 | light.target.position.set(0, 0, 0); 135 | light.castShadow = false; 136 | this._scene.add(light); 137 | 138 | light = new THREE.DirectionalLight(0x404040, 1); 139 | light.position.set(100, 100, -100); 140 | light.target.position.set(0, 0, 0); 141 | light.castShadow = false; 142 | this._scene.add(light); 143 | 144 | light = new THREE.DirectionalLight(0x404040, 1); 145 | light.position.set(100, 100, -100); 146 | light.target.position.set(0, 0, 0); 147 | light.castShadow = false; 148 | this._scene.add(light); 149 | 150 | light = new THREE.DirectionalLight(0x202040, 1); 151 | light.position.set(100, -100, 100); 152 | light.target.position.set(0, 0, 0); 153 | light.castShadow = false; 154 | this._scene.add(light); 155 | 156 | light = new THREE.AmbientLight(0xFFFFFF, 1.0); 157 | this._scene.add(light); 158 | } 159 | 160 | _OnWindowResize() { 161 | this._camera.aspect = window.innerWidth / window.innerHeight; 162 | this._camera.updateProjectionMatrix(); 163 | this._threejs.setSize(window.innerWidth, window.innerHeight); 164 | this._composer.setSize(window.innerWidth, window.innerHeight); 165 | this._target.setSize(window.innerWidth, window.innerHeight); 166 | } 167 | 168 | get Scene() { 169 | return this._scene; 170 | } 171 | 172 | get Camera() { 173 | return this._camera; 174 | } 175 | 176 | Render(timeInSeconds) { 177 | this._threejs.setRenderTarget(this._target); 178 | 179 | this._threejs.clear(); 180 | this._threejs.render(this._scene, this._camera); 181 | //this._composer.render(); 182 | 183 | this._threejs.setRenderTarget( null ); 184 | 185 | const forward = new THREE.Vector3(); 186 | this._camera.getWorldDirection(forward); 187 | 188 | this._depthPass.uniforms.inverseProjection.value = this._camera.projectionMatrixInverse; 189 | this._depthPass.uniforms.inverseView.value = this._camera.matrixWorld; 190 | this._depthPass.uniforms.tDiffuse.value = this._target.texture; 191 | this._depthPass.uniforms.tDepth.value = this._target.depthTexture; 192 | this._depthPass.uniforms.cameraNear.value = this._camera.near; 193 | this._depthPass.uniforms.cameraFar.value = this._camera.far; 194 | this._depthPass.uniforms.cameraPosition.value = this._camera.position; 195 | this._depthPass.uniforms.cameraForward.value = forward; 196 | this._depthPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0); 197 | this._depthPass.uniformsNeedUpdate = true; 198 | 199 | this._threejs.render( this._postScene, this._postCamera ); 200 | 201 | this._stats.update(); 202 | } 203 | } 204 | 205 | return { 206 | Graphics: _Graphics, 207 | GetPixel: _GetPixel, 208 | GetImageData: _GetImageData, 209 | }; 210 | })(); 211 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | import {GUI} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/dat.gui.module.js'; 3 | import {controls} from './controls.js'; 4 | import {game} from './game.js'; 5 | import {terrain} from './terrain.js'; 6 | 7 | let _APP = null; 8 | 9 | 10 | class ProceduralTerrain_Demo extends game.Game { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | _OnInitialize() { 16 | this._CreateGUI(); 17 | 18 | this._userCamera = new THREE.Object3D(); 19 | this._userCamera.position.set(4100, 0, 0); 20 | this._graphics.Camera.position.set(3853, -609, -1509); 21 | this._graphics.Camera.quaternion.set(0.403, 0.59, -0.549, 0.432); 22 | 23 | this._graphics.Camera.position.set(1412, -1674, -3848); 24 | this._graphics.Camera.quaternion.set(0.1004, 0.7757, -0.6097, 0.1278); 25 | 26 | this._graphics.Camera.position.set(338810.64, -181800.18, -110710.19); 27 | this._graphics.Camera.quaternion.set(0.4275, 0.5715, -0.5616, 0.4183); 28 | // this._graphics.Camera.position.set(1367.34, -570.00, -3716.39); 29 | // this._graphics.Camera.quaternion.set(0.1287, 0.6021, -0.7815, 0.0992); 30 | 31 | this._graphics.Camera.position.set(338784.59, -181584.12, -110698.22); 32 | this._graphics.Camera.quaternion.set(0.4684, 0.5026, -0.6334, 0.3554); 33 | 34 | this._graphics.Camera.position.set(341783.13615310675, -225077.36034169965, -109485.53388079848); 35 | this._graphics.Camera.quaternion.set(0.39608344608128787, 0.5873476594328535, -0.5523018098171056, 0.4394352529916893); 36 | 37 | this._AddEntity('_terrain', new terrain.TerrainChunkManager({ 38 | camera: this._graphics.Camera, 39 | scene: this._graphics.Scene, 40 | scattering: this._graphics._depthPass, 41 | gui: this._gui, 42 | guiParams: this._guiParams, 43 | game: this}), 1.0); 44 | 45 | this._AddEntity('_controls', new controls.FPSControls({ 46 | camera: this._graphics.Camera, 47 | scene: this._graphics.Scene, 48 | domElement: this._graphics._threejs.domElement, 49 | gui: this._gui, 50 | guiParams: this._guiParams}), 0.0); 51 | 52 | // this._AddEntity('_controls', new controls.ShipControls({ 53 | // camera: this._graphics.Camera, 54 | // scene: this._graphics.Scene, 55 | // domElement: this._graphics._threejs.domElement, 56 | // gui: this._gui, 57 | // guiParams: this._guiParams, 58 | // }), 0.0); 59 | 60 | this._totalTime = 0; 61 | 62 | this._LoadBackground(); 63 | } 64 | 65 | _CreateGUI() { 66 | this._guiParams = { 67 | general: { 68 | }, 69 | }; 70 | this._gui = new GUI(); 71 | 72 | const generalRollup = this._gui.addFolder('General'); 73 | this._gui.close(); 74 | } 75 | 76 | _LoadBackground() { 77 | this._graphics.Scene.background = new THREE.Color(0x000000); 78 | const loader = new THREE.CubeTextureLoader(); 79 | const texture = loader.load([ 80 | './resources/space-posx.jpg', 81 | './resources/space-negx.jpg', 82 | './resources/space-posy.jpg', 83 | './resources/space-negy.jpg', 84 | './resources/space-posz.jpg', 85 | './resources/space-negz.jpg', 86 | ]); 87 | this._graphics._scene.background = texture; 88 | } 89 | 90 | _OnStep(timeInSeconds) { 91 | } 92 | } 93 | 94 | 95 | function _Main() { 96 | _APP = new ProceduralTerrain_Demo(); 97 | } 98 | 99 | _Main(); 100 | -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | export const math = (function() { 2 | return { 3 | rand_range: function(a, b) { 4 | return Math.random() * (b - a) + a; 5 | }, 6 | 7 | rand_normalish: function() { 8 | const r = Math.random() + Math.random() + Math.random() + Math.random(); 9 | return (r / 4.0) * 2.0 - 1; 10 | }, 11 | 12 | rand_int: function(a, b) { 13 | return Math.round(Math.random() * (b - a) + a); 14 | }, 15 | 16 | lerp: function(x, a, b) { 17 | return x * (b - a) + a; 18 | }, 19 | 20 | smoothstep: function(x, a, b) { 21 | x = x * x * (3.0 - 2.0 * x); 22 | return x * (b - a) + a; 23 | }, 24 | 25 | smootherstep: function(x, a, b) { 26 | x = x * x * x * (x * (x * 6 - 15) + 10); 27 | return x * (b - a) + a; 28 | }, 29 | 30 | clamp: function(x, a, b) { 31 | return Math.min(Math.max(x, a), b); 32 | }, 33 | 34 | sat: function(x) { 35 | return Math.min(Math.max(x, 0.0), 1.0); 36 | }, 37 | }; 38 | })(); 39 | -------------------------------------------------------------------------------- /src/noise.js: -------------------------------------------------------------------------------- 1 | //import 'https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.js'; 2 | //import perlin from 'https://cdn.jsdelivr.net/gh/mikechambers/es6-perlin-module/perlin.js'; 3 | import perlin from './perlin-noise.js'; 4 | import {simplex} from './simplex-noise.js'; 5 | 6 | import {math} from './math.js'; 7 | 8 | export const noise = (function() { 9 | 10 | class _NoiseGenerator { 11 | constructor(params) { 12 | this._params = params; 13 | this._Init(); 14 | } 15 | 16 | _Init() { 17 | this._noise = new simplex.SimplexNoise(this._params.seed); 18 | } 19 | 20 | Get(x, y, z) { 21 | const G = 2.0 ** (-this._params.persistence); 22 | const xs = x / this._params.scale; 23 | const ys = y / this._params.scale; 24 | const zs = z / this._params.scale; 25 | const noiseFunc = this._noise; 26 | 27 | let amplitude = 1.0; 28 | let frequency = 1.0; 29 | let normalization = 0; 30 | let total = 0; 31 | for (let o = 0; o < this._params.octaves; o++) { 32 | const noiseValue = noiseFunc.noise3D( 33 | xs * frequency, ys * frequency, zs * frequency) * 0.5 + 0.5; 34 | 35 | total += noiseValue * amplitude; 36 | normalization += amplitude; 37 | amplitude *= G; 38 | frequency *= this._params.lacunarity; 39 | } 40 | total /= normalization; 41 | return Math.pow( 42 | total, this._params.exponentiation) * this._params.height; 43 | } 44 | } 45 | 46 | return { 47 | Noise: _NoiseGenerator 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /src/perlin-noise.js: -------------------------------------------------------------------------------- 1 | // noise1234 2 | // 3 | // Author: Stefan Gustavson, 2003-2005 4 | // Contact: stefan.gustavson@liu.se 5 | // 6 | // This code was GPL licensed until February 2011. 7 | // As the original author of this code, I hereby 8 | // release it into the public domain. 9 | // Please feel free to use it for whatever you want. 10 | // Credit is appreciated where appropriate, and I also 11 | // appreciate being told where this code finds any use, 12 | // but you may do as you like. 13 | 14 | //Ported to JavaScript by Mike mikechambers 15 | //http://www.mikechambers.com 16 | // 17 | // Note, all return values are scaled to be between 0 and 1 18 | // 19 | //From original C at: 20 | //https://github.com/stegu/perlin-noise 21 | //https://github.com/stegu/perlin-noise/blob/master/src/noise1234.c 22 | 23 | /* 24 | * This implementation is "Improved Noise" as presented by 25 | * Ken Perlin at Siggraph 2002. The 3D function is a direct port 26 | * of his Java reference code which was once publicly available 27 | * on www.noisemachine.com (although I cleaned it up, made it 28 | * faster and made the code more readable), but the 1D, 2D and 29 | * 4D functions were implemented from scratch by me. 30 | * 31 | * This is a backport to C of my improved noise class in C++ 32 | * which was included in the Aqsis renderer project. 33 | * It is highly reusable without source code modifications. 34 | * 35 | */ 36 | 37 | // This is the new and improved, C(2) continuous interpolant 38 | function fade(t) { 39 | return ( t * t * t * ( t * ( t * 6 - 15 ) + 10 ) ); 40 | } 41 | 42 | function lerp(t, a, b) { 43 | return ((a) + (t)*((b)-(a))); 44 | } 45 | 46 | 47 | //--------------------------------------------------------------------- 48 | // Static data 49 | 50 | /* 51 | * Permutation table. This is just a random jumble of all numbers 0-255, 52 | * repeated twice to avoid wrapping the index at 255 for each lookup. 53 | * This needs to be exactly the same for all instances on all platforms, 54 | * so it's easiest to just keep it as static explicit data. 55 | * This also removes the need for any initialisation of this class. 56 | * 57 | * Note that making this an int[] instead of a char[] might make the 58 | * code run faster on platforms with a high penalty for unaligned single 59 | * byte addressing. Intel x86 is generally single-byte-friendly, but 60 | * some other CPUs are faster with 4-aligned reads. 61 | * However, a char[] is smaller, which avoids cache trashing, and that 62 | * is probably the most important aspect on most architectures. 63 | * This array is accessed a *lot* by the noise functions. 64 | * A vector-valued noise over 3D accesses it 96 times, and a 65 | * float-valued 4D noise 64 times. We want this to fit in the cache! 66 | */ 67 | const perm = [151,160,137,91,90,15, 68 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 69 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 70 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 71 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 72 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 73 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 74 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 75 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 76 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 77 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 78 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 79 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, 80 | 151,160,137,91,90,15, 81 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 82 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 83 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 84 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 85 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 86 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 87 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 88 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 89 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 90 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 91 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 92 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 93 | ]; 94 | 95 | //--------------------------------------------------------------------- 96 | 97 | /* 98 | * Helper functions to compute gradients-dot-residualvectors (1D to 4D) 99 | * Note that these generate gradients of more than unit length. To make 100 | * a close match with the value range of classic Perlin noise, the final 101 | * noise values need to be rescaled. To match the RenderMan noise in a 102 | * statistical sense, the approximate scaling values (empirically 103 | * determined from test renderings) are: 104 | * 1D noise needs rescaling with 0.188 105 | * 2D noise needs rescaling with 0.507 106 | * 3D noise needs rescaling with 0.936 107 | * 4D noise needs rescaling with 0.87 108 | */ 109 | 110 | function grad1( hash, x ) { 111 | let h = hash & 15; 112 | let grad = 1.0 + (h & 7); // Gradient value 1.0, 2.0, ..., 8.0 113 | if (h&8) grad = -grad; // and a random sign for the gradient 114 | return ( grad * x ); // Multiply the gradient with the distance 115 | } 116 | 117 | function grad2( hash, x, y ) { 118 | let h = hash & 7; // Convert low 3 bits of hash code 119 | let u = h<4 ? x : y; // into 8 simple gradient directions, 120 | let v = h<4 ? y : x; // and compute the dot product with (x,y). 121 | return ((h&1)? -u : u) + ((h&2)? -2.0*v : 2.0*v); 122 | } 123 | 124 | function grad3( hash, x, y , z ) { 125 | let h = hash & 15; // Convert low 4 bits of hash code into 12 simple 126 | let u = h<8 ? x : y; // gradient directions, and compute dot product. 127 | let v = h<4 ? y : h==12||h==14 ? x : z; // Fix repeats at h = 12 to 15 128 | return ((h&1)? -u : u) + ((h&2)? -v : v); 129 | } 130 | 131 | function grad4( hash, x, y, z, t ) { 132 | let h = hash & 31; // Convert low 5 bits of hash code into 32 simple 133 | let u = h<24 ? x : y; // gradient directions, and compute dot product. 134 | let v = h<16 ? y : z; 135 | let w = h<8 ? z : t; 136 | return ((h&1)? -u : u) + ((h&2)? -v : v) + ((h&4)? -w : w); 137 | } 138 | 139 | //--------------------------------------------------------------------- 140 | /** 1D float Perlin noise, SL "noise()" 141 | */ 142 | export function noise1( x ) 143 | { 144 | let ix0, ix1; 145 | let fx0, fx1; 146 | let s, n0, n1; 147 | 148 | ix0 = Math.floor( x ); // Integer part of x 149 | fx0 = x - ix0; // Fractional part of x 150 | fx1 = fx0 - 1.0; 151 | ix1 = ( ix0+1 ) & 0xff; 152 | ix0 = ix0 & 0xff; // Wrap to 0..255 153 | 154 | s = fade( fx0 ); 155 | 156 | n0 = grad1( perm[ ix0 ], fx0 ); 157 | n1 = grad1( perm[ ix1 ], fx1 ); 158 | return scale(0.188 * ( lerp( s, n0, n1 ) )); 159 | } 160 | 161 | //--------------------------------------------------------------------- 162 | /** 1D float Perlin periodic noise, SL "pnoise()" 163 | */ 164 | export function pnoise1( x, px ) 165 | { 166 | let ix0, ix1; 167 | let fx0, fx1; 168 | let s, n0, n1; 169 | 170 | ix0 = Math.floor( x ); // Integer part of x 171 | fx0 = x - ix0; // Fractional part of x 172 | fx1 = fx0 - 1.0; 173 | ix1 = (( ix0 + 1 ) % px) & 0xff; // Wrap to 0..px-1 *and* wrap to 0..255 174 | ix0 = ( ix0 % px ) & 0xff; // (because px might be greater than 256) 175 | 176 | s = fade( fx0 ); 177 | 178 | n0 = grad1( perm[ ix0 ], fx0 ); 179 | n1 = grad1( perm[ ix1 ], fx1 ); 180 | return scale(0.188 * ( lerp( s, n0, n1 ) )); 181 | } 182 | 183 | 184 | //--------------------------------------------------------------------- 185 | /** 2D float Perlin noise. 186 | */ 187 | export function noise2( x, y ) 188 | { 189 | let ix0, iy0, ix1, iy1; 190 | let fx0, fy0, fx1, fy1; 191 | let s, t, nx0, nx1, n0, n1; 192 | 193 | ix0 = Math.floor( x ); // Integer part of x 194 | iy0 = Math.floor( y ); // Integer part of y 195 | fx0 = x - ix0; // Fractional part of x 196 | fy0 = y - iy0; // Fractional part of y 197 | fx1 = fx0 - 1.0; 198 | fy1 = fy0 - 1.0; 199 | ix1 = (ix0 + 1) & 0xff; // Wrap to 0..255 200 | iy1 = (iy0 + 1) & 0xff; 201 | ix0 = ix0 & 0xff; 202 | iy0 = iy0 & 0xff; 203 | 204 | t = fade( fy0 ); 205 | s = fade( fx0 ); 206 | 207 | nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0); 208 | nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1); 209 | n0 = lerp( t, nx0, nx1 ); 210 | 211 | nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0); 212 | nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1); 213 | n1 = lerp(t, nx0, nx1); 214 | 215 | return scale(0.507 * ( lerp( s, n0, n1 ) )); 216 | } 217 | 218 | //--------------------------------------------------------------------- 219 | /** 2D float Perlin periodic noise. 220 | */ 221 | export function pnoise2( x, y, px, py ) 222 | { 223 | let ix0, iy0, ix1, iy1; 224 | let fx0, fy0, fx1, fy1; 225 | let s, t, nx0, nx1, n0, n1; 226 | 227 | ix0 = Math.floor( x ); // Integer part of x 228 | iy0 = Math.floor( y ); // Integer part of y 229 | fx0 = x - ix0; // Fractional part of x 230 | fy0 = y - iy0; // Fractional part of y 231 | fx1 = fx0 - 1.0; 232 | fy1 = fy0 - 1.0; 233 | ix1 = (( ix0 + 1 ) % px) & 0xff; // Wrap to 0..px-1 and wrap to 0..255 234 | iy1 = (( iy0 + 1 ) % py) & 0xff; // Wrap to 0..py-1 and wrap to 0..255 235 | ix0 = ( ix0 % px ) & 0xff; 236 | iy0 = ( iy0 % py ) & 0xff; 237 | 238 | t = fade( fy0 ); 239 | s = fade( fx0 ); 240 | 241 | nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0); 242 | nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1); 243 | n0 = lerp( t, nx0, nx1 ); 244 | 245 | nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0); 246 | nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1); 247 | n1 = lerp(t, nx0, nx1); 248 | 249 | return scale(0.507 * ( lerp( s, n0, n1 ) )); 250 | } 251 | 252 | 253 | //--------------------------------------------------------------------- 254 | /** 3D float Perlin noise. 255 | */ 256 | export function noise3( x, y, z ) 257 | { 258 | let ix0, iy0, ix1, iy1, iz0, iz1; 259 | let fx0, fy0, fz0, fx1, fy1, fz1; 260 | let s, t, r; 261 | let nxy0, nxy1, nx0, nx1, n0, n1; 262 | 263 | ix0 = Math.floor( x ); // Integer part of x 264 | iy0 = Math.floor( y ); // Integer part of y 265 | iz0 = Math.floor( z ); // Integer part of z 266 | fx0 = x - ix0; // Fractional part of x 267 | fy0 = y - iy0; // Fractional part of y 268 | fz0 = z - iz0; // Fractional part of z 269 | fx1 = fx0 - 1.0; 270 | fy1 = fy0 - 1.0; 271 | fz1 = fz0 - 1.0; 272 | ix1 = ( ix0 + 1 ) & 0xff; // Wrap to 0..255 273 | iy1 = ( iy0 + 1 ) & 0xff; 274 | iz1 = ( iz0 + 1 ) & 0xff; 275 | ix0 = ix0 & 0xff; 276 | iy0 = iy0 & 0xff; 277 | iz0 = iz0 & 0xff; 278 | 279 | r = fade( fz0 ); 280 | t = fade( fy0 ); 281 | s = fade( fx0 ); 282 | 283 | nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0); 284 | nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1); 285 | nx0 = lerp( r, nxy0, nxy1 ); 286 | 287 | nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0); 288 | nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1); 289 | nx1 = lerp( r, nxy0, nxy1 ); 290 | 291 | n0 = lerp( t, nx0, nx1 ); 292 | 293 | nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0); 294 | nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1); 295 | nx0 = lerp( r, nxy0, nxy1 ); 296 | 297 | nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0); 298 | nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1); 299 | nx1 = lerp( r, nxy0, nxy1 ); 300 | 301 | n1 = lerp( t, nx0, nx1 ); 302 | 303 | return scale(0.936 * ( lerp( s, n0, n1 ) )); 304 | } 305 | 306 | //--------------------------------------------------------------------- 307 | /** 3D float Perlin periodic noise. 308 | */ 309 | export function pnoise3( x, y, z, px, py, pz ) 310 | { 311 | let ix0, iy0, ix1, iy1, iz0, iz1; 312 | let fx0, fy0, fz0, fx1, fy1, fz1; 313 | let s, t, r; 314 | let nxy0, nxy1, nx0, nx1, n0, n1; 315 | 316 | ix0 = Math.floor( x ); // Integer part of x 317 | iy0 = Math.floor( y ); // Integer part of y 318 | iz0 = Math.floor( z ); // Integer part of z 319 | fx0 = x - ix0; // Fractional part of x 320 | fy0 = y - iy0; // Fractional part of y 321 | fz0 = z - iz0; // Fractional part of z 322 | fx1 = fx0 - 1.0; 323 | fy1 = fy0 - 1.0; 324 | fz1 = fz0 - 1.0; 325 | ix1 = (( ix0 + 1 ) % px ) & 0xff; // Wrap to 0..px-1 and wrap to 0..255 326 | iy1 = (( iy0 + 1 ) % py ) & 0xff; // Wrap to 0..py-1 and wrap to 0..255 327 | iz1 = (( iz0 + 1 ) % pz ) & 0xff; // Wrap to 0..pz-1 and wrap to 0..255 328 | ix0 = ( ix0 % px ) & 0xff; 329 | iy0 = ( iy0 % py ) & 0xff; 330 | iz0 = ( iz0 % pz ) & 0xff; 331 | 332 | r = fade( fz0 ); 333 | t = fade( fy0 ); 334 | s = fade( fx0 ); 335 | 336 | nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0); 337 | nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1); 338 | nx0 = lerp( r, nxy0, nxy1 ); 339 | 340 | nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0); 341 | nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1); 342 | nx1 = lerp( r, nxy0, nxy1 ); 343 | 344 | n0 = lerp( t, nx0, nx1 ); 345 | 346 | nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0); 347 | nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1); 348 | nx0 = lerp( r, nxy0, nxy1 ); 349 | 350 | nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0); 351 | nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1); 352 | nx1 = lerp( r, nxy0, nxy1 ); 353 | 354 | n1 = lerp( t, nx0, nx1 ); 355 | 356 | return scale(0.936 * ( lerp( s, n0, n1 ) )); 357 | } 358 | 359 | 360 | //--------------------------------------------------------------------- 361 | /** 4D float Perlin noise. 362 | */ 363 | 364 | export function noise4( x, y, z, w ) 365 | { 366 | let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1; 367 | let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1; 368 | let s, t, r, q; 369 | let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1; 370 | 371 | ix0 = Math.floor( x ); // Integer part of x 372 | iy0 = Math.floor( y ); // Integer part of y 373 | iz0 = Math.floor( z ); // Integer part of y 374 | iw0 = Math.floor( w ); // Integer part of w 375 | fx0 = x - ix0; // Fractional part of x 376 | fy0 = y - iy0; // Fractional part of y 377 | fz0 = z - iz0; // Fractional part of z 378 | fw0 = w - iw0; // Fractional part of w 379 | fx1 = fx0 - 1.0; 380 | fy1 = fy0 - 1.0; 381 | fz1 = fz0 - 1.0; 382 | fw1 = fw0 - 1.0; 383 | ix1 = ( ix0 + 1 ) & 0xff; // Wrap to 0..255 384 | iy1 = ( iy0 + 1 ) & 0xff; 385 | iz1 = ( iz0 + 1 ) & 0xff; 386 | iw1 = ( iw0 + 1 ) & 0xff; 387 | ix0 = ix0 & 0xff; 388 | iy0 = iy0 & 0xff; 389 | iz0 = iz0 & 0xff; 390 | iw0 = iw0 & 0xff; 391 | 392 | q = fade( fw0 ); 393 | r = fade( fz0 ); 394 | t = fade( fy0 ); 395 | s = fade( fx0 ); 396 | 397 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0); 398 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1); 399 | nxy0 = lerp( q, nxyz0, nxyz1 ); 400 | 401 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0); 402 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1); 403 | nxy1 = lerp( q, nxyz0, nxyz1 ); 404 | 405 | nx0 = lerp ( r, nxy0, nxy1 ); 406 | 407 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0); 408 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1); 409 | nxy0 = lerp( q, nxyz0, nxyz1 ); 410 | 411 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0); 412 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1); 413 | nxy1 = lerp( q, nxyz0, nxyz1 ); 414 | 415 | nx1 = lerp ( r, nxy0, nxy1 ); 416 | 417 | n0 = lerp( t, nx0, nx1 ); 418 | 419 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0); 420 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1); 421 | nxy0 = lerp( q, nxyz0, nxyz1 ); 422 | 423 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0); 424 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1); 425 | nxy1 = lerp( q, nxyz0, nxyz1 ); 426 | 427 | nx0 = lerp ( r, nxy0, nxy1 ); 428 | 429 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0); 430 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1); 431 | nxy0 = lerp( q, nxyz0, nxyz1 ); 432 | 433 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0); 434 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1); 435 | nxy1 = lerp( q, nxyz0, nxyz1 ); 436 | 437 | nx1 = lerp ( r, nxy0, nxy1 ); 438 | 439 | n1 = lerp( t, nx0, nx1 ); 440 | 441 | return scale(0.87 * ( lerp( s, n0, n1 ) )); 442 | } 443 | 444 | //--------------------------------------------------------------------- 445 | /** 4D float Perlin periodic noise. 446 | */ 447 | 448 | export function pnoise4( x, y, z, w, 449 | px, py, pz, pw ) 450 | { 451 | let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1; 452 | let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1; 453 | let s, t, r, q; 454 | let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1; 455 | 456 | ix0 = Math.floor( x ); // Integer part of x 457 | iy0 = Math.floor( y ); // Integer part of y 458 | iz0 = Math.floor( z ); // Integer part of y 459 | iw0 = Math.floor( w ); // Integer part of w 460 | fx0 = x - ix0; // Fractional part of x 461 | fy0 = y - iy0; // Fractional part of y 462 | fz0 = z - iz0; // Fractional part of z 463 | fw0 = w - iw0; // Fractional part of w 464 | fx1 = fx0 - 1.0; 465 | fy1 = fy0 - 1.0; 466 | fz1 = fz0 - 1.0; 467 | fw1 = fw0 - 1.0; 468 | ix1 = (( ix0 + 1 ) % px ) & 0xff; // Wrap to 0..px-1 and wrap to 0..255 469 | iy1 = (( iy0 + 1 ) % py ) & 0xff; // Wrap to 0..py-1 and wrap to 0..255 470 | iz1 = (( iz0 + 1 ) % pz ) & 0xff; // Wrap to 0..pz-1 and wrap to 0..255 471 | iw1 = (( iw0 + 1 ) % pw ) & 0xff; // Wrap to 0..pw-1 and wrap to 0..255 472 | ix0 = ( ix0 % px ) & 0xff; 473 | iy0 = ( iy0 % py ) & 0xff; 474 | iz0 = ( iz0 % pz ) & 0xff; 475 | iw0 = ( iw0 % pw ) & 0xff; 476 | 477 | q = fade( fw0 ); 478 | r = fade( fz0 ); 479 | t = fade( fy0 ); 480 | s = fade( fx0 ); 481 | 482 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0); 483 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1); 484 | nxy0 = lerp( q, nxyz0, nxyz1 ); 485 | 486 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0); 487 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1); 488 | nxy1 = lerp( q, nxyz0, nxyz1 ); 489 | 490 | nx0 = lerp ( r, nxy0, nxy1 ); 491 | 492 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0); 493 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1); 494 | nxy0 = lerp( q, nxyz0, nxyz1 ); 495 | 496 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0); 497 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1); 498 | nxy1 = lerp( q, nxyz0, nxyz1 ); 499 | 500 | nx1 = lerp ( r, nxy0, nxy1 ); 501 | 502 | n0 = lerp( t, nx0, nx1 ); 503 | 504 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0); 505 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1); 506 | nxy0 = lerp( q, nxyz0, nxyz1 ); 507 | 508 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0); 509 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1); 510 | nxy1 = lerp( q, nxyz0, nxyz1 ); 511 | 512 | nx0 = lerp ( r, nxy0, nxy1 ); 513 | 514 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0); 515 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1); 516 | nxy0 = lerp( q, nxyz0, nxyz1 ); 517 | 518 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0); 519 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1); 520 | nxy1 = lerp( q, nxyz0, nxyz1 ); 521 | 522 | nx1 = lerp ( r, nxy0, nxy1 ); 523 | 524 | n1 = lerp( t, nx0, nx1 ); 525 | 526 | return scale(0.87 * ( lerp( s, n0, n1 ) )); 527 | } 528 | 529 | function scale(n) { 530 | return (1 + n) / 2; 531 | } 532 | 533 | export default function noise(x, y, z, w) { 534 | 535 | switch(arguments.length) { 536 | case 1: 537 | return noise1(x); //todo: move these to perlin functions 538 | break; 539 | case 2: 540 | return noise2(x, y); //todo: move these to perlin functions 541 | break; 542 | case 3: 543 | return noise3(x, y, z); 544 | case 3: 545 | return noise4(x, y, z, w); 546 | break; 547 | } 548 | } 549 | 550 | //--------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/quadtree.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | 4 | export const quadtree = (function() { 5 | 6 | class CubeQuadTree { 7 | constructor(params) { 8 | this._params = params; 9 | this._sides = []; 10 | 11 | const r = params.radius; 12 | let m; 13 | 14 | const transforms = []; 15 | 16 | // +Y 17 | m = new THREE.Matrix4(); 18 | m.makeRotationX(-Math.PI / 2); 19 | m.premultiply(new THREE.Matrix4().makeTranslation(0, r, 0)); 20 | transforms.push(m); 21 | 22 | // -Y 23 | m = new THREE.Matrix4(); 24 | m.makeRotationX(Math.PI / 2); 25 | m.premultiply(new THREE.Matrix4().makeTranslation(0, -r, 0)); 26 | transforms.push(m); 27 | 28 | // +X 29 | m = new THREE.Matrix4(); 30 | m.makeRotationY(Math.PI / 2); 31 | m.premultiply(new THREE.Matrix4().makeTranslation(r, 0, 0)); 32 | transforms.push(m); 33 | 34 | // -X 35 | m = new THREE.Matrix4(); 36 | m.makeRotationY(-Math.PI / 2); 37 | m.premultiply(new THREE.Matrix4().makeTranslation(-r, 0, 0)); 38 | transforms.push(m); 39 | 40 | // +Z 41 | m = new THREE.Matrix4(); 42 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r)); 43 | transforms.push(m); 44 | 45 | // -Z 46 | m = new THREE.Matrix4(); 47 | m.makeRotationY(Math.PI); 48 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, -r)); 49 | transforms.push(m); 50 | 51 | for (let t of transforms) { 52 | this._sides.push({ 53 | transform: t.clone(), 54 | worldToLocal: t.clone().getInverse(t), 55 | quadtree: new QuadTree({ 56 | size: r, 57 | min_node_size: params.min_node_size, 58 | localToWorld: t 59 | }), 60 | }); 61 | } 62 | } 63 | 64 | GetChildren() { 65 | const children = []; 66 | 67 | for (let s of this._sides) { 68 | const side = { 69 | transform: s.transform, 70 | children: s.quadtree.GetChildren(), 71 | } 72 | children.push(side); 73 | } 74 | return children; 75 | } 76 | 77 | Insert(pos) { 78 | for (let s of this._sides) { 79 | s.quadtree.Insert(pos); 80 | } 81 | } 82 | } 83 | 84 | class QuadTree { 85 | constructor(params) { 86 | const s = params.size; 87 | const b = new THREE.Box3( 88 | new THREE.Vector3(-s, -s, 0), 89 | new THREE.Vector3(s, s, 0)); 90 | this._root = { 91 | bounds: b, 92 | children: [], 93 | center: b.getCenter(new THREE.Vector3()), 94 | sphereCenter: b.getCenter(new THREE.Vector3()), 95 | size: b.getSize(new THREE.Vector3()), 96 | root: true, 97 | }; 98 | 99 | this._params = params; 100 | this._root.sphereCenter = this._root.center.clone(); 101 | this._root.sphereCenter.applyMatrix4(this._params.localToWorld); 102 | this._root.sphereCenter.normalize(); 103 | this._root.sphereCenter.multiplyScalar(this._params.size); 104 | } 105 | 106 | GetChildren() { 107 | const children = []; 108 | this._GetChildren(this._root, children); 109 | return children; 110 | } 111 | 112 | _GetChildren(node, target) { 113 | if (node.children.length == 0) { 114 | target.push(node); 115 | return; 116 | } 117 | 118 | for (let c of node.children) { 119 | this._GetChildren(c, target); 120 | } 121 | } 122 | 123 | Insert(pos) { 124 | this._Insert(this._root, pos); 125 | } 126 | 127 | _Insert(child, pos) { 128 | const distToChild = this._DistanceToChild(child, pos); 129 | 130 | if (distToChild < child.size.x * 1.0 && child.size.x > this._params.min_node_size) { 131 | child.children = this._CreateChildren(child); 132 | 133 | for (let c of child.children) { 134 | this._Insert(c, pos); 135 | } 136 | } 137 | } 138 | 139 | _DistanceToChild(child, pos) { 140 | return child.sphereCenter.distanceTo(pos); 141 | } 142 | 143 | _CreateChildren(child) { 144 | const midpoint = child.bounds.getCenter(new THREE.Vector3()); 145 | 146 | // Bottom left 147 | const b1 = new THREE.Box3(child.bounds.min, midpoint); 148 | 149 | // Bottom right 150 | const b2 = new THREE.Box3( 151 | new THREE.Vector3(midpoint.x, child.bounds.min.y, 0), 152 | new THREE.Vector3(child.bounds.max.x, midpoint.y, 0)); 153 | 154 | // Top left 155 | const b3 = new THREE.Box3( 156 | new THREE.Vector3(child.bounds.min.x, midpoint.y, 0), 157 | new THREE.Vector3(midpoint.x, child.bounds.max.y, 0)); 158 | 159 | // Top right 160 | const b4 = new THREE.Box3(midpoint, child.bounds.max); 161 | 162 | const children = [b1, b2, b3, b4].map( 163 | b => { 164 | return { 165 | bounds: b, 166 | children: [], 167 | center: b.getCenter(new THREE.Vector3()), 168 | size: b.getSize(new THREE.Vector3()) 169 | }; 170 | }); 171 | 172 | for (let c of children) { 173 | c.sphereCenter = c.center.clone(); 174 | c.sphereCenter.applyMatrix4(this._params.localToWorld); 175 | c.sphereCenter.normalize() 176 | c.sphereCenter.multiplyScalar(this._params.size); 177 | } 178 | 179 | return children; 180 | } 181 | } 182 | 183 | return { 184 | QuadTree: QuadTree, 185 | CubeQuadTree: CubeQuadTree, 186 | } 187 | })(); 188 | -------------------------------------------------------------------------------- /src/scattering-shader.js: -------------------------------------------------------------------------------- 1 | export const scattering_shader = (function() { 2 | 3 | const _VS = `#version 300 es 4 | 5 | #define saturate(a) clamp( a, 0.0, 1.0 ) 6 | 7 | out vec2 vUv; 8 | 9 | void main() { 10 | vUv = uv; 11 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 12 | } 13 | `; 14 | 15 | 16 | const _PS = `#version 300 es 17 | #include 18 | 19 | #define saturate(a) clamp( a, 0.0, 1.0 ) 20 | 21 | #define PI 3.141592 22 | #define PRIMARY_STEP_COUNT 16 23 | #define LIGHT_STEP_COUNT 8 24 | 25 | 26 | in vec2 vUv; 27 | out vec4 out_FragColor; 28 | 29 | uniform sampler2D tDiffuse; 30 | uniform sampler2D tDepth; 31 | uniform float cameraNear; 32 | uniform float cameraFar; 33 | uniform vec3 cameraForward; 34 | uniform mat4 inverseProjection; 35 | uniform mat4 inverseView; 36 | 37 | uniform vec3 planetPosition; 38 | uniform float planetRadius; 39 | uniform float atmosphereRadius; 40 | 41 | 42 | vec3 _ScreenToWorld(vec3 pos) { 43 | vec4 posP = vec4(pos.xyz * 2.0 - 1.0, 1.0); 44 | 45 | vec4 posVS = inverseProjection * posP; 46 | vec4 posWS = inverseView * vec4((posVS.xyz / posVS.w), 1.0); 47 | 48 | return posWS.xyz; 49 | } 50 | 51 | 52 | float _SoftLight(float a, float b) { 53 | return (b < 0.5 ? 54 | (2.0 * a * b + a * a * (1.0 - 2.0 * b)) : 55 | (2.0 * a * (1.0 - b) + sqrt(a) * (2.0 * b - 1.0)) 56 | ); 57 | } 58 | 59 | vec3 _SoftLight(vec3 a, vec3 b) { 60 | return vec3( 61 | _SoftLight(a.x, b.x), 62 | _SoftLight(a.y, b.y), 63 | _SoftLight(a.z, b.z) 64 | ); 65 | } 66 | 67 | bool _RayIntersectsSphere( 68 | vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius, out float t0, out float t1) { 69 | vec3 oc = rayStart - sphereCenter; 70 | float a = dot(rayDir, rayDir); 71 | float b = 2.0 * dot(oc, rayDir); 72 | float c = dot(oc, oc) - sphereRadius * sphereRadius; 73 | float d = b * b - 4.0 * a * c; 74 | 75 | // Also skip single point of contact 76 | if (d <= 0.0) { 77 | return false; 78 | } 79 | 80 | float r0 = (-b - sqrt(d)) / (2.0 * a); 81 | float r1 = (-b + sqrt(d)) / (2.0 * a); 82 | 83 | t0 = min(r0, r1); 84 | t1 = max(r0, r1); 85 | 86 | return (t1 >= 0.0); 87 | } 88 | 89 | 90 | vec3 _SampleLightRay( 91 | vec3 origin, vec3 sunDir, float planetScale, float planetRadius, float totalRadius, 92 | float rayleighScale, float mieScale, float absorptionHeightMax, float absorptionFalloff) { 93 | 94 | float t0, t1; 95 | _RayIntersectsSphere(origin, sunDir, planetPosition, totalRadius, t0, t1); 96 | 97 | float actualLightStepSize = (t1 - t0) / float(LIGHT_STEP_COUNT); 98 | float virtualLightStepSize = actualLightStepSize * planetScale; 99 | float lightStepPosition = 0.0; 100 | 101 | vec3 opticalDepthLight = vec3(0.0); 102 | 103 | for (int j = 0; j < LIGHT_STEP_COUNT; j++) { 104 | vec3 currentLightSamplePosition = origin + sunDir * (lightStepPosition + actualLightStepSize * 0.5); 105 | 106 | // Calculate the optical depths and accumulate 107 | float currentHeight = length(currentLightSamplePosition) - planetRadius; 108 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualLightStepSize; 109 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualLightStepSize; 110 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff)); 111 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualLightStepSize; 112 | 113 | opticalDepthLight += vec3( 114 | currentOpticalDepthRayleigh, 115 | currentOpticalDepthMie, 116 | currentOpticalDepthOzone); 117 | 118 | lightStepPosition += actualLightStepSize; 119 | } 120 | 121 | return opticalDepthLight; 122 | } 123 | 124 | void _ComputeScattering( 125 | vec3 worldSpacePos, vec3 rayDirection, vec3 rayOrigin, vec3 sunDir, 126 | out vec3 scatteringColour, out vec3 scatteringOpacity) { 127 | 128 | vec3 betaRayleigh = vec3(5.5e-6, 13.0e-6, 22.4e-6); 129 | float betaMie = 21e-6; 130 | vec3 betaAbsorption = vec3(2.04e-5, 4.97e-5, 1.95e-6); 131 | float g = 0.76; 132 | float sunIntensity = 40.0; 133 | 134 | float planetRadius = planetRadius; 135 | float atmosphereRadius = atmosphereRadius - planetRadius; 136 | float totalRadius = planetRadius + atmosphereRadius; 137 | 138 | float referencePlanetRadius = 6371000.0; 139 | float referenceAtmosphereRadius = 100000.0; 140 | float referenceTotalRadius = referencePlanetRadius + referenceAtmosphereRadius; 141 | float referenceRatio = referencePlanetRadius / referenceAtmosphereRadius; 142 | 143 | float scaleRatio = planetRadius / atmosphereRadius; 144 | float planetScale = referencePlanetRadius / planetRadius; 145 | float atmosphereScale = scaleRatio / referenceRatio; 146 | float maxDist = distance(worldSpacePos, rayOrigin); 147 | 148 | float rayleighScale = 8500.0 / (planetScale * atmosphereScale); 149 | float mieScale = 1200.0 / (planetScale * atmosphereScale); 150 | float absorptionHeightMax = 32000.0 * (planetScale * atmosphereScale); 151 | float absorptionFalloff = 3000.0 / (planetScale * atmosphereScale);; 152 | 153 | float mu = dot(rayDirection, sunDir); 154 | float mumu = mu * mu; 155 | float gg = g * g; 156 | float phaseRayleigh = 3.0 / (16.0 * PI) * (1.0 + mumu); 157 | float phaseMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)); 158 | 159 | // Early out if ray doesn't intersect atmosphere. 160 | float t0, t1; 161 | if (!_RayIntersectsSphere(rayOrigin, rayDirection, planetPosition, totalRadius, t0, t1)) { 162 | scatteringOpacity = vec3(1.0); 163 | return; 164 | } 165 | 166 | // Clip the ray between the camera and potentially the planet surface. 167 | t0 = max(0.0, t0); 168 | t1 = min(maxDist, t1); 169 | 170 | float actualPrimaryStepSize = (t1 - t0) / float(PRIMARY_STEP_COUNT); 171 | float virtualPrimaryStepSize = actualPrimaryStepSize * planetScale; 172 | float primaryStepPosition = 0.0; 173 | 174 | vec3 accumulatedRayleigh = vec3(0.0); 175 | vec3 accumulatedMie = vec3(0.0); 176 | vec3 opticalDepth = vec3(0.0); 177 | 178 | // Take N steps along primary ray 179 | for (int i = 0; i < PRIMARY_STEP_COUNT; i++) { 180 | vec3 currentPrimarySamplePosition = rayOrigin + rayDirection * ( 181 | primaryStepPosition + actualPrimaryStepSize * 0.5); 182 | 183 | float currentHeight = max(0.0, length(currentPrimarySamplePosition) - planetRadius); 184 | 185 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualPrimaryStepSize; 186 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualPrimaryStepSize; 187 | 188 | // Taken from https://www.shadertoy.com/view/wlBXWK 189 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff)); 190 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualPrimaryStepSize; 191 | 192 | opticalDepth += vec3(currentOpticalDepthRayleigh, currentOpticalDepthMie, currentOpticalDepthOzone); 193 | 194 | // Sample light ray and accumulate optical depth. 195 | vec3 opticalDepthLight = _SampleLightRay( 196 | currentPrimarySamplePosition, sunDir, 197 | planetScale, planetRadius, totalRadius, 198 | rayleighScale, mieScale, absorptionHeightMax, absorptionFalloff); 199 | 200 | vec3 r = ( 201 | betaRayleigh * (opticalDepth.x + opticalDepthLight.x) + 202 | betaMie * (opticalDepth.y + opticalDepthLight.y) + 203 | betaAbsorption * (opticalDepth.z + opticalDepthLight.z)); 204 | vec3 attn = exp(-r); 205 | 206 | accumulatedRayleigh += currentOpticalDepthRayleigh * attn; 207 | accumulatedMie += currentOpticalDepthMie * attn; 208 | 209 | primaryStepPosition += actualPrimaryStepSize; 210 | } 211 | 212 | scatteringColour = sunIntensity * (phaseRayleigh * betaRayleigh * accumulatedRayleigh + phaseMie * betaMie * accumulatedMie); 213 | scatteringOpacity = exp( 214 | -(betaMie * opticalDepth.y + betaRayleigh * opticalDepth.x + betaAbsorption * opticalDepth.z)); 215 | } 216 | 217 | vec3 _ApplyGroundFog( 218 | in vec3 rgb, 219 | float distToPoint, 220 | float height, 221 | in vec3 worldSpacePos, 222 | in vec3 rayOrigin, 223 | in vec3 rayDir, 224 | in vec3 sunDir) 225 | { 226 | vec3 up = normalize(rayOrigin); 227 | 228 | float skyAmt = dot(up, rayDir) * 0.25 + 0.75; 229 | skyAmt = saturate(skyAmt); 230 | skyAmt *= skyAmt; 231 | 232 | vec3 DARK_BLUE = vec3(0.1, 0.2, 0.3); 233 | vec3 LIGHT_BLUE = vec3(0.5, 0.6, 0.7); 234 | vec3 DARK_ORANGE = vec3(0.7, 0.4, 0.05); 235 | vec3 BLUE = vec3(0.5, 0.6, 0.7); 236 | vec3 YELLOW = vec3(1.0, 0.9, 0.7); 237 | 238 | vec3 fogCol = mix(DARK_BLUE, LIGHT_BLUE, skyAmt); 239 | float sunAmt = max(dot(rayDir, sunDir), 0.0); 240 | fogCol = mix(fogCol, YELLOW, pow(sunAmt, 16.0)); 241 | 242 | float be = 0.0025; 243 | float fogAmt = (1.0 - exp(-distToPoint * be)); 244 | 245 | // Sun 246 | sunAmt = 0.5 * saturate(pow(sunAmt, 256.0)); 247 | 248 | return mix(rgb, fogCol, fogAmt) + sunAmt * YELLOW; 249 | } 250 | 251 | vec3 _ApplySpaceFog( 252 | in vec3 rgb, 253 | in float distToPoint, 254 | in float height, 255 | in vec3 worldSpacePos, 256 | in vec3 rayOrigin, 257 | in vec3 rayDir, 258 | in vec3 sunDir) 259 | { 260 | float atmosphereThickness = (atmosphereRadius - planetRadius); 261 | 262 | float t0 = -1.0; 263 | float t1 = -1.0; 264 | 265 | // This is a hack since the world mesh has seams that we haven't fixed yet. 266 | if (_RayIntersectsSphere( 267 | rayOrigin, rayDir, planetPosition, planetRadius, t0, t1)) { 268 | if (distToPoint > t0) { 269 | distToPoint = t0; 270 | worldSpacePos = rayOrigin + t0 * rayDir; 271 | } 272 | } 273 | 274 | if (!_RayIntersectsSphere( 275 | rayOrigin, rayDir, planetPosition, planetRadius + atmosphereThickness * 5.0, t0, t1)) { 276 | return rgb * 0.5; 277 | } 278 | 279 | // Figure out a better way to do this 280 | float silhouette = saturate((distToPoint - 10000.0) / 10000.0); 281 | 282 | // Glow around planet 283 | float scaledDistanceToSurface = 0.0; 284 | 285 | // Calculate the closest point between ray direction and planet. Use a point in front of the 286 | // camera to force differences as you get closer to planet. 287 | vec3 fakeOrigin = rayOrigin + rayDir * atmosphereThickness; 288 | float t = max(0.0, dot(rayDir, planetPosition - fakeOrigin) / dot(rayDir, rayDir)); 289 | vec3 pb = fakeOrigin + t * rayDir; 290 | 291 | scaledDistanceToSurface = saturate((distance(pb, planetPosition) - planetRadius) / atmosphereThickness); 292 | scaledDistanceToSurface = smoothstep(0.0, 1.0, 1.0 - scaledDistanceToSurface); 293 | //scaledDistanceToSurface = smoothstep(0.0, 1.0, scaledDistanceToSurface); 294 | 295 | float scatteringFactor = scaledDistanceToSurface * silhouette; 296 | 297 | // Fog on surface 298 | t0 = max(0.0, t0); 299 | t1 = min(distToPoint, t1); 300 | 301 | vec3 intersectionPoint = rayOrigin + t1 * rayDir; 302 | vec3 normalAtIntersection = normalize(intersectionPoint); 303 | 304 | float distFactor = exp(-distToPoint * 0.0005 / (atmosphereThickness)); 305 | float fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection)); 306 | fresnel = smoothstep(0.0, 1.0, fresnel); 307 | 308 | float extinctionFactor = saturate(fresnel * distFactor) * (1.0 - silhouette); 309 | 310 | // Front/Back Lighting 311 | vec3 BLUE = vec3(0.5, 0.6, 0.75); 312 | vec3 YELLOW = vec3(1.0, 0.9, 0.7); 313 | vec3 RED = vec3(0.035, 0.0, 0.0); 314 | 315 | float NdotL = dot(normalAtIntersection, sunDir); 316 | float wrap = 0.5; 317 | float NdotL_wrap = max(0.0, (NdotL + wrap) / (1.0 + wrap)); 318 | float RdotS = max(0.0, dot(rayDir, sunDir)); 319 | float sunAmount = RdotS; 320 | 321 | vec3 backLightingColour = YELLOW * 0.1; 322 | vec3 frontLightingColour = mix(BLUE, YELLOW, pow(sunAmount, 32.0)); 323 | 324 | vec3 fogColour = mix(backLightingColour, frontLightingColour, NdotL_wrap); 325 | 326 | extinctionFactor *= NdotL_wrap; 327 | 328 | // Sun 329 | float specular = pow((RdotS + 0.5) / (1.0 + 0.5), 64.0); 330 | 331 | fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection)); 332 | fresnel *= fresnel; 333 | 334 | float sunFactor = (length(pb) - planetRadius) / (atmosphereThickness * 5.0); 335 | sunFactor = (1.0 - saturate(sunFactor)); 336 | sunFactor *= sunFactor; 337 | sunFactor *= sunFactor; 338 | sunFactor *= specular * fresnel; 339 | 340 | vec3 baseColour = mix(rgb, fogColour, extinctionFactor); 341 | vec3 litColour = baseColour + _SoftLight(fogColour * scatteringFactor + YELLOW * sunFactor, baseColour); 342 | vec3 blendedColour = mix(baseColour, fogColour, scatteringFactor); 343 | blendedColour += blendedColour + _SoftLight(YELLOW * sunFactor, blendedColour); 344 | return mix(litColour, blendedColour, scaledDistanceToSurface * 0.25); 345 | } 346 | 347 | vec3 _ApplyFog( 348 | in vec3 rgb, 349 | in float distToPoint, 350 | in float height, 351 | in vec3 worldSpacePos, 352 | in vec3 rayOrigin, 353 | in vec3 rayDir, 354 | in vec3 sunDir) 355 | { 356 | float distToPlanet = max(0.0, length(rayOrigin) - planetRadius); 357 | float atmosphereThickness = (atmosphereRadius - planetRadius); 358 | 359 | vec3 groundCol = _ApplyGroundFog( 360 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir); 361 | vec3 spaceCol = _ApplySpaceFog( 362 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir); 363 | 364 | float blendFactor = saturate(distToPlanet / (atmosphereThickness * 0.5)); 365 | 366 | blendFactor = smoothstep(0.0, 1.0, blendFactor); 367 | blendFactor = smoothstep(0.0, 1.0, blendFactor); 368 | 369 | return mix(groundCol, spaceCol, blendFactor); 370 | } 371 | 372 | void main() { 373 | float z = texture2D(tDepth, vUv).x; 374 | vec3 posWS = _ScreenToWorld(vec3(vUv, z)); 375 | float dist = length(posWS - cameraPosition); 376 | float height = max(0.0, length(cameraPosition) - planetRadius); 377 | vec3 cameraDirection = normalize(posWS - cameraPosition); 378 | 379 | 380 | vec3 diffuse = texture2D(tDiffuse, vUv).xyz; 381 | vec3 lightDir = normalize(vec3(1, 1, -1)); 382 | 383 | // diffuse = _ApplyFog(diffuse, dist, height, posWS, cameraPosition, cameraDirection, lightDir); 384 | 385 | vec3 scatteringColour = vec3(0.0); 386 | vec3 scatteringOpacity = vec3(1.0, 1.0, 1.0); 387 | _ComputeScattering( 388 | posWS, cameraDirection, cameraPosition, 389 | lightDir, scatteringColour, scatteringOpacity 390 | ); 391 | 392 | diffuse = diffuse * scatteringOpacity + scatteringColour; 393 | diffuse = ACESFilmicToneMapping(diffuse); 394 | 395 | out_FragColor.rgb = diffuse; 396 | out_FragColor.a = 1.0; 397 | } 398 | `; 399 | 400 | 401 | const _Shader = { 402 | uniforms: { 403 | "tDiffuse": { value: null }, 404 | "tDepth": { value: null }, 405 | "cameraNear": { value: 0.0 }, 406 | "cameraFar": { value: 0.0 }, 407 | }, 408 | vertexShader: _VS, 409 | fragmentShader: _PS, 410 | }; 411 | 412 | return { 413 | Shader: _Shader, 414 | VS: _VS, 415 | PS: _PS, 416 | }; 417 | })(); 418 | -------------------------------------------------------------------------------- /src/simplex-noise.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A fast javascript implementation of simplex noise by Jonas Wagner 3 | 4 | Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java. 5 | Which is based on example code by Stefan Gustavson (stegu@itn.liu.se). 6 | With Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). 7 | Better rank ordering method by Stefan Gustavson in 2012. 8 | 9 | 10 | Copyright (c) 2018 Jonas Wagner 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | // (function() { 31 | 32 | export const simplex = (function() { 33 | 34 | 'use strict'; 35 | 36 | var F2 = 0.5 * (Math.sqrt(3.0) - 1.0); 37 | var G2 = (3.0 - Math.sqrt(3.0)) / 6.0; 38 | var F3 = 1.0 / 3.0; 39 | var G3 = 1.0 / 6.0; 40 | var F4 = (Math.sqrt(5.0) - 1.0) / 4.0; 41 | var G4 = (5.0 - Math.sqrt(5.0)) / 20.0; 42 | 43 | function SimplexNoise(randomOrSeed) { 44 | var random; 45 | if (typeof randomOrSeed == 'function') { 46 | random = randomOrSeed; 47 | } 48 | else if (randomOrSeed) { 49 | random = alea(randomOrSeed); 50 | } else { 51 | random = Math.random; 52 | } 53 | this.p = buildPermutationTable(random); 54 | this.perm = new Uint8Array(512); 55 | this.permMod12 = new Uint8Array(512); 56 | for (var i = 0; i < 512; i++) { 57 | this.perm[i] = this.p[i & 255]; 58 | this.permMod12[i] = this.perm[i] % 12; 59 | } 60 | 61 | } 62 | SimplexNoise.prototype = { 63 | grad3: new Float32Array([1, 1, 0, 64 | -1, 1, 0, 65 | 1, -1, 0, 66 | 67 | -1, -1, 0, 68 | 1, 0, 1, 69 | -1, 0, 1, 70 | 71 | 1, 0, -1, 72 | -1, 0, -1, 73 | 0, 1, 1, 74 | 75 | 0, -1, 1, 76 | 0, 1, -1, 77 | 0, -1, -1]), 78 | grad4: new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 79 | 0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 80 | 1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1, 81 | -1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1, 82 | 1, 1, 0, 1, 1, 1, 0, -1, 1, -1, 0, 1, 1, -1, 0, -1, 83 | -1, 1, 0, 1, -1, 1, 0, -1, -1, -1, 0, 1, -1, -1, 0, -1, 84 | 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 0, 85 | -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 0]), 86 | noise2D: function(xin, yin) { 87 | var permMod12 = this.permMod12; 88 | var perm = this.perm; 89 | var grad3 = this.grad3; 90 | var n0 = 0; // Noise contributions from the three corners 91 | var n1 = 0; 92 | var n2 = 0; 93 | // Skew the input space to determine which simplex cell we're in 94 | var s = (xin + yin) * F2; // Hairy factor for 2D 95 | var i = Math.floor(xin + s); 96 | var j = Math.floor(yin + s); 97 | var t = (i + j) * G2; 98 | var X0 = i - t; // Unskew the cell origin back to (x,y) space 99 | var Y0 = j - t; 100 | var x0 = xin - X0; // The x,y distances from the cell origin 101 | var y0 = yin - Y0; 102 | // For the 2D case, the simplex shape is an equilateral triangle. 103 | // Determine which simplex we are in. 104 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 105 | if (x0 > y0) { 106 | i1 = 1; 107 | j1 = 0; 108 | } // lower triangle, XY order: (0,0)->(1,0)->(1,1) 109 | else { 110 | i1 = 0; 111 | j1 = 1; 112 | } // upper triangle, YX order: (0,0)->(0,1)->(1,1) 113 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 114 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 115 | // c = (3-sqrt(3))/6 116 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 117 | var y1 = y0 - j1 + G2; 118 | var x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords 119 | var y2 = y0 - 1.0 + 2.0 * G2; 120 | // Work out the hashed gradient indices of the three simplex corners 121 | var ii = i & 255; 122 | var jj = j & 255; 123 | // Calculate the contribution from the three corners 124 | var t0 = 0.5 - x0 * x0 - y0 * y0; 125 | if (t0 >= 0) { 126 | var gi0 = permMod12[ii + perm[jj]] * 3; 127 | t0 *= t0; 128 | n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient 129 | } 130 | var t1 = 0.5 - x1 * x1 - y1 * y1; 131 | if (t1 >= 0) { 132 | var gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3; 133 | t1 *= t1; 134 | n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1); 135 | } 136 | var t2 = 0.5 - x2 * x2 - y2 * y2; 137 | if (t2 >= 0) { 138 | var gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3; 139 | t2 *= t2; 140 | n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2); 141 | } 142 | // Add contributions from each corner to get the final noise value. 143 | // The result is scaled to return values in the interval [-1,1]. 144 | return 70.0 * (n0 + n1 + n2); 145 | }, 146 | // 3D simplex noise 147 | noise3D: function(xin, yin, zin) { 148 | var permMod12 = this.permMod12; 149 | var perm = this.perm; 150 | var grad3 = this.grad3; 151 | var n0, n1, n2, n3; // Noise contributions from the four corners 152 | // Skew the input space to determine which simplex cell we're in 153 | var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D 154 | var i = Math.floor(xin + s); 155 | var j = Math.floor(yin + s); 156 | var k = Math.floor(zin + s); 157 | var t = (i + j + k) * G3; 158 | var X0 = i - t; // Unskew the cell origin back to (x,y,z) space 159 | var Y0 = j - t; 160 | var Z0 = k - t; 161 | var x0 = xin - X0; // The x,y,z distances from the cell origin 162 | var y0 = yin - Y0; 163 | var z0 = zin - Z0; 164 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 165 | // Determine which simplex we are in. 166 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 167 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 168 | if (x0 >= y0) { 169 | if (y0 >= z0) { 170 | i1 = 1; 171 | j1 = 0; 172 | k1 = 0; 173 | i2 = 1; 174 | j2 = 1; 175 | k2 = 0; 176 | } // X Y Z order 177 | else if (x0 >= z0) { 178 | i1 = 1; 179 | j1 = 0; 180 | k1 = 0; 181 | i2 = 1; 182 | j2 = 0; 183 | k2 = 1; 184 | } // X Z Y order 185 | else { 186 | i1 = 0; 187 | j1 = 0; 188 | k1 = 1; 189 | i2 = 1; 190 | j2 = 0; 191 | k2 = 1; 192 | } // Z X Y order 193 | } 194 | else { // x0 y0) rankx++; 301 | else ranky++; 302 | if (x0 > z0) rankx++; 303 | else rankz++; 304 | if (x0 > w0) rankx++; 305 | else rankw++; 306 | if (y0 > z0) ranky++; 307 | else rankz++; 308 | if (y0 > w0) ranky++; 309 | else rankw++; 310 | if (z0 > w0) rankz++; 311 | else rankw++; 312 | var i1, j1, k1, l1; // The integer offsets for the second simplex corner 313 | var i2, j2, k2, l2; // The integer offsets for the third simplex corner 314 | var i3, j3, k3, l3; // The integer offsets for the fourth simplex corner 315 | // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. 316 | // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0; 321 | j1 = ranky >= 3 ? 1 : 0; 322 | k1 = rankz >= 3 ? 1 : 0; 323 | l1 = rankw >= 3 ? 1 : 0; 324 | // Rank 2 denotes the second largest coordinate. 325 | i2 = rankx >= 2 ? 1 : 0; 326 | j2 = ranky >= 2 ? 1 : 0; 327 | k2 = rankz >= 2 ? 1 : 0; 328 | l2 = rankw >= 2 ? 1 : 0; 329 | // Rank 1 denotes the second smallest coordinate. 330 | i3 = rankx >= 1 ? 1 : 0; 331 | j3 = ranky >= 1 ? 1 : 0; 332 | k3 = rankz >= 1 ? 1 : 0; 333 | l3 = rankw >= 1 ? 1 : 0; 334 | // The fifth corner has all coordinate offsets = 1, so no need to compute that. 335 | var x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords 336 | var y1 = y0 - j1 + G4; 337 | var z1 = z0 - k1 + G4; 338 | var w1 = w0 - l1 + G4; 339 | var x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords 340 | var y2 = y0 - j2 + 2.0 * G4; 341 | var z2 = z0 - k2 + 2.0 * G4; 342 | var w2 = w0 - l2 + 2.0 * G4; 343 | var x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords 344 | var y3 = y0 - j3 + 3.0 * G4; 345 | var z3 = z0 - k3 + 3.0 * G4; 346 | var w3 = w0 - l3 + 3.0 * G4; 347 | var x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords 348 | var y4 = y0 - 1.0 + 4.0 * G4; 349 | var z4 = z0 - 1.0 + 4.0 * G4; 350 | var w4 = w0 - 1.0 + 4.0 * G4; 351 | // Work out the hashed gradient indices of the five simplex corners 352 | var ii = i & 255; 353 | var jj = j & 255; 354 | var kk = k & 255; 355 | var ll = l & 255; 356 | // Calculate the contribution from the five corners 357 | var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0; 358 | if (t0 < 0) n0 = 0.0; 359 | else { 360 | var gi0 = (perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32) * 4; 361 | t0 *= t0; 362 | n0 = t0 * t0 * (grad4[gi0] * x0 + grad4[gi0 + 1] * y0 + grad4[gi0 + 2] * z0 + grad4[gi0 + 3] * w0); 363 | } 364 | var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1; 365 | if (t1 < 0) n1 = 0.0; 366 | else { 367 | var gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32) * 4; 368 | t1 *= t1; 369 | n1 = t1 * t1 * (grad4[gi1] * x1 + grad4[gi1 + 1] * y1 + grad4[gi1 + 2] * z1 + grad4[gi1 + 3] * w1); 370 | } 371 | var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2; 372 | if (t2 < 0) n2 = 0.0; 373 | else { 374 | var gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32) * 4; 375 | t2 *= t2; 376 | n2 = t2 * t2 * (grad4[gi2] * x2 + grad4[gi2 + 1] * y2 + grad4[gi2 + 2] * z2 + grad4[gi2 + 3] * w2); 377 | } 378 | var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3; 379 | if (t3 < 0) n3 = 0.0; 380 | else { 381 | var gi3 = (perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32) * 4; 382 | t3 *= t3; 383 | n3 = t3 * t3 * (grad4[gi3] * x3 + grad4[gi3 + 1] * y3 + grad4[gi3 + 2] * z3 + grad4[gi3 + 3] * w3); 384 | } 385 | var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4; 386 | if (t4 < 0) n4 = 0.0; 387 | else { 388 | var gi4 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32) * 4; 389 | t4 *= t4; 390 | n4 = t4 * t4 * (grad4[gi4] * x4 + grad4[gi4 + 1] * y4 + grad4[gi4 + 2] * z4 + grad4[gi4 + 3] * w4); 391 | } 392 | // Sum up and scale the result to cover the range [-1,1] 393 | return 27.0 * (n0 + n1 + n2 + n3 + n4); 394 | } 395 | }; 396 | 397 | function buildPermutationTable(random) { 398 | var i; 399 | var p = new Uint8Array(256); 400 | for (i = 0; i < 256; i++) { 401 | p[i] = i; 402 | } 403 | for (i = 0; i < 255; i++) { 404 | var r = i + ~~(random() * (256 - i)); 405 | var aux = p[i]; 406 | p[i] = p[r]; 407 | p[r] = aux; 408 | } 409 | return p; 410 | } 411 | SimplexNoise._buildPermutationTable = buildPermutationTable; 412 | 413 | function alea() { 414 | // Johannes Baagøe , 2010 415 | var s0 = 0; 416 | var s1 = 0; 417 | var s2 = 0; 418 | var c = 1; 419 | 420 | var mash = masher(); 421 | s0 = mash(' '); 422 | s1 = mash(' '); 423 | s2 = mash(' '); 424 | 425 | for (var i = 0; i < arguments.length; i++) { 426 | s0 -= mash(arguments[i]); 427 | if (s0 < 0) { 428 | s0 += 1; 429 | } 430 | s1 -= mash(arguments[i]); 431 | if (s1 < 0) { 432 | s1 += 1; 433 | } 434 | s2 -= mash(arguments[i]); 435 | if (s2 < 0) { 436 | s2 += 1; 437 | } 438 | } 439 | mash = null; 440 | return function() { 441 | var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 442 | s0 = s1; 443 | s1 = s2; 444 | return s2 = t - (c = t | 0); 445 | }; 446 | } 447 | function masher() { 448 | var n = 0xefc8249d; 449 | return function(data) { 450 | data = data.toString(); 451 | for (var i = 0; i < data.length; i++) { 452 | n += data.charCodeAt(i); 453 | var h = 0.02519603282416938 * n; 454 | n = h >>> 0; 455 | h -= n; 456 | h *= n; 457 | n = h >>> 0; 458 | h -= n; 459 | n += h * 0x100000000; // 2^32 460 | } 461 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 462 | }; 463 | } 464 | 465 | // // amd 466 | // if (typeof define !== 'undefined' && define.amd) define(function() {return SimplexNoise;}); 467 | // // common js 468 | // if (typeof exports !== 'undefined') exports.SimplexNoise = SimplexNoise; 469 | // // browser 470 | // else if (typeof window !== 'undefined') window.SimplexNoise = SimplexNoise; 471 | // // nodejs 472 | // if (typeof module !== 'undefined') { 473 | // module.exports = SimplexNoise; 474 | // } 475 | return { 476 | SimplexNoise: SimplexNoise 477 | }; 478 | 479 | })(); -------------------------------------------------------------------------------- /src/sky.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {Sky} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/objects/Sky.js'; 4 | import {Water} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/objects/Water.js'; 5 | 6 | 7 | export const sky = (function() { 8 | 9 | class TerrainSky { 10 | constructor(params) { 11 | this._params = params; 12 | this._Init(params); 13 | } 14 | 15 | _Init(params) { 16 | const waterGeometry = new THREE.PlaneBufferGeometry(10000, 10000, 100, 100); 17 | 18 | this._water = new Water( 19 | waterGeometry, 20 | { 21 | textureWidth: 2048, 22 | textureHeight: 2048, 23 | waterNormals: new THREE.TextureLoader().load( 'resources/waternormals.jpg', function ( texture ) { 24 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 25 | } ), 26 | alpha: 0.5, 27 | sunDirection: new THREE.Vector3(1, 0, 0), 28 | sunColor: 0xffffff, 29 | waterColor: 0x001e0f, 30 | distortionScale: 0.0, 31 | fog: undefined 32 | } 33 | ); 34 | // this._water.rotation.x = - Math.PI / 2; 35 | // this._water.position.y = 4; 36 | 37 | this._sky = new Sky(); 38 | this._sky.scale.setScalar(10000); 39 | 40 | this._group = new THREE.Group(); 41 | //this._group.add(this._water); 42 | this._group.add(this._sky); 43 | 44 | params.scene.add(this._group); 45 | 46 | params.guiParams.sky = { 47 | turbidity: 10.0, 48 | rayleigh: 2, 49 | mieCoefficient: 0.005, 50 | mieDirectionalG: 0.8, 51 | luminance: 1, 52 | }; 53 | 54 | params.guiParams.sun = { 55 | inclination: 0.31, 56 | azimuth: 0.25, 57 | }; 58 | 59 | const onShaderChange = () => { 60 | for (let k in params.guiParams.sky) { 61 | this._sky.material.uniforms[k].value = params.guiParams.sky[k]; 62 | } 63 | for (let k in params.guiParams.general) { 64 | this._sky.material.uniforms[k].value = params.guiParams.general[k]; 65 | } 66 | }; 67 | 68 | const onSunChange = () => { 69 | var theta = Math.PI * (params.guiParams.sun.inclination - 0.5); 70 | var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5); 71 | 72 | const sunPosition = new THREE.Vector3(); 73 | sunPosition.x = Math.cos(phi); 74 | sunPosition.y = Math.sin(phi) * Math.sin(theta); 75 | sunPosition.z = Math.sin(phi) * Math.cos(theta); 76 | 77 | this._sky.material.uniforms['sunPosition'].value.copy(sunPosition); 78 | this._water.material.uniforms['sunDirection'].value.copy(sunPosition.normalize()); 79 | }; 80 | 81 | const skyRollup = params.gui.addFolder('Sky'); 82 | skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange( 83 | onShaderChange); 84 | skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange( 85 | onShaderChange); 86 | skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange( 87 | onShaderChange); 88 | skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange( 89 | onShaderChange); 90 | skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange( 91 | onShaderChange); 92 | 93 | const sunRollup = params.gui.addFolder('Sun'); 94 | sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange( 95 | onSunChange); 96 | sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange( 97 | onSunChange); 98 | 99 | onShaderChange(); 100 | onSunChange(); 101 | } 102 | 103 | Update(timeInSeconds) { 104 | this._water.material.uniforms['time'].value += timeInSeconds; 105 | 106 | this._group.position.x = this._params.camera.position.x; 107 | this._group.position.z = this._params.camera.position.z; 108 | } 109 | } 110 | 111 | 112 | return { 113 | TerrainSky: TerrainSky 114 | } 115 | })(); 116 | -------------------------------------------------------------------------------- /src/spline.js: -------------------------------------------------------------------------------- 1 | export const spline = (function() { 2 | 3 | class _CubicHermiteSpline { 4 | constructor(lerp) { 5 | this._points = []; 6 | this._lerp = lerp; 7 | } 8 | 9 | AddPoint(t, d) { 10 | this._points.push([t, d]); 11 | } 12 | 13 | Get(t) { 14 | let p1 = 0; 15 | 16 | for (let i = 0; i < this._points.length; i++) { 17 | if (this._points[i][0] >= t) { 18 | break; 19 | } 20 | p1 = i; 21 | } 22 | 23 | const p0 = Math.max(0, p1 - 1); 24 | const p2 = Math.min(this._points.length - 1, p1 + 1); 25 | const p3 = Math.min(this._points.length - 1, p1 + 2); 26 | 27 | if (p1 == p2) { 28 | return this._points[p1][1]; 29 | } 30 | 31 | return this._lerp( 32 | (t - this._points[p1][0]) / ( 33 | this._points[p2][0] - this._points[p1][0]), 34 | this._points[p0][1], this._points[p1][1], 35 | this._points[p2][1], this._points[p3][1]); 36 | } 37 | }; 38 | 39 | class _LinearSpline { 40 | constructor(lerp) { 41 | this._points = []; 42 | this._lerp = lerp; 43 | } 44 | 45 | AddPoint(t, d) { 46 | this._points.push([t, d]); 47 | } 48 | 49 | Get(t) { 50 | let p1 = 0; 51 | 52 | for (let i = 0; i < this._points.length; i++) { 53 | if (this._points[i][0] >= t) { 54 | break; 55 | } 56 | p1 = i; 57 | } 58 | 59 | const p2 = Math.min(this._points.length - 1, p1 + 1); 60 | 61 | if (p1 == p2) { 62 | return this._points[p1][1]; 63 | } 64 | 65 | return this._lerp( 66 | (t - this._points[p1][0]) / ( 67 | this._points[p2][0] - this._points[p1][0]), 68 | this._points[p1][1], this._points[p2][1]); 69 | } 70 | } 71 | 72 | return { 73 | CubicHermiteSpline: _CubicHermiteSpline, 74 | LinearSpline: _LinearSpline, 75 | }; 76 | })(); 77 | -------------------------------------------------------------------------------- /src/terrain-builder-threaded-worker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {noise} from './noise.js'; 4 | import {texture_splatter} from './texture-splatter.js' ; 5 | 6 | 7 | class _TerrainBuilderThreadedWorker { 8 | constructor() { 9 | } 10 | 11 | Init(params) { 12 | this._params = params; 13 | this._params.offset = new THREE.Vector3( 14 | params.offset[0], params.offset[1], params.offset[2]); 15 | this._params.noise = new noise.Noise(params.noiseParams); 16 | this._params.heightGenerators = [ 17 | new texture_splatter.HeightGenerator( 18 | this._params.noise, params.offset, 19 | params.heightGeneratorsParams.min, params.heightGeneratorsParams.max) 20 | ]; 21 | 22 | this._params.biomeGenerator = new noise.Noise(params.biomesParams); 23 | this._params.colourNoise = new noise.Noise(params.colourNoiseParams); 24 | this._params.colourGenerator = new texture_splatter.TextureSplatter( 25 | { 26 | biomeGenerator: this._params.biomeGenerator, 27 | colourNoise: this._params.colourNoise 28 | }); 29 | } 30 | 31 | _GenerateHeight(v) { 32 | return this._params.heightGenerators[0].Get(v.x, v.y, v.z)[0]; 33 | } 34 | 35 | Rebuild() { 36 | const _D = new THREE.Vector3(); 37 | const _D1 = new THREE.Vector3(); 38 | const _D2 = new THREE.Vector3(); 39 | const _P = new THREE.Vector3(); 40 | const _H = new THREE.Vector3(); 41 | const _W = new THREE.Vector3(); 42 | const _S = new THREE.Vector3(); 43 | const _C = new THREE.Vector3(); 44 | 45 | const _N = new THREE.Vector3(); 46 | const _N1 = new THREE.Vector3(); 47 | const _N2 = new THREE.Vector3(); 48 | const _N3 = new THREE.Vector3(); 49 | 50 | const positions = []; 51 | const colors = []; 52 | const up = []; 53 | const coords = []; 54 | const uvs = []; 55 | const weights1 = []; 56 | const weights2 = []; 57 | const indices = []; 58 | const wsPositions = []; 59 | 60 | const localToWorld = this._params.worldMatrix; 61 | const resolution = this._params.resolution; 62 | const radius = this._params.radius; 63 | const offset = this._params.offset; 64 | const origin = this._params.origin; 65 | const width = this._params.width; 66 | const half = width / 2; 67 | 68 | for (let x = 0; x < resolution + 1; x++) { 69 | const xp = width * x / resolution; 70 | for (let y = 0; y < resolution + 1; y++) { 71 | const yp = width * y / resolution; 72 | 73 | // Compute position 74 | _P.set(xp - half, yp - half, radius); 75 | _P.add(offset); 76 | _P.normalize(); 77 | _D.copy(_P); 78 | _D.transformDirection(localToWorld); 79 | 80 | _P.multiplyScalar(radius); 81 | _P.z -= radius; 82 | _P.applyMatrix4(localToWorld); 83 | 84 | // Keep the absolute world space position to sample noise 85 | _W.copy(_P); 86 | 87 | // Move the position relative to the origin 88 | _P.sub(origin); 89 | 90 | // Purturb height along z-vector 91 | const height = this._GenerateHeight(_W); 92 | _H.copy(_D); 93 | _H.multiplyScalar(height); 94 | _P.add(_H); 95 | 96 | positions.push(_P.x, _P.y, _P.z); 97 | 98 | _C.copy(_W); 99 | _C.add(_H); 100 | coords.push(_C.x, _C.y, _C.z); 101 | 102 | _S.set(_W.x, _W.y, height); 103 | 104 | const color = this._params.colourGenerator.GetColour(_S); 105 | colors.push(color.r, color.g, color.b); 106 | up.push(_D.x, _D.y, _D.z); 107 | wsPositions.push(_W.x, _W.y, height); 108 | // TODO GUI 109 | uvs.push(_P.x / 200.0, _P.y / 200.0); 110 | } 111 | } 112 | 113 | for (let i = 0; i < resolution; i++) { 114 | for (let j = 0; j < resolution; j++) { 115 | indices.push( 116 | i * (resolution + 1) + j, 117 | (i + 1) * (resolution + 1) + j + 1, 118 | i * (resolution + 1) + j + 1); 119 | indices.push( 120 | (i + 1) * (resolution + 1) + j, 121 | (i + 1) * (resolution + 1) + j + 1, 122 | i * (resolution + 1) + j); 123 | } 124 | } 125 | 126 | const normals = new Array(up.length).fill(0.0); 127 | 128 | for (let i = 0, n = indices.length; i < n; i+= 3) { 129 | const i1 = indices[i] * 3; 130 | const i2 = indices[i+1] * 3; 131 | const i3 = indices[i+2] * 3; 132 | 133 | _N1.fromArray(positions, i1); 134 | _N2.fromArray(positions, i2); 135 | _N3.fromArray(positions, i3); 136 | 137 | _D1.subVectors(_N3, _N2); 138 | _D2.subVectors(_N1, _N2); 139 | _D1.cross(_D2); 140 | 141 | normals[i1] += _D1.x; 142 | normals[i2] += _D1.x; 143 | normals[i3] += _D1.x; 144 | 145 | normals[i1+1] += _D1.y; 146 | normals[i2+1] += _D1.y; 147 | normals[i3+1] += _D1.y; 148 | 149 | normals[i1+2] += _D1.z; 150 | normals[i2+2] += _D1.z; 151 | normals[i3+2] += _D1.z; 152 | } 153 | 154 | for (let i = 0, n = normals.length; i < n; i+=3) { 155 | _N.fromArray(normals, i); 156 | _N.normalize(); 157 | normals[i] = _N.x; 158 | normals[i+1] = _N.y; 159 | normals[i+2] = _N.z; 160 | } 161 | 162 | for (let i = 0, n = indices.length; i < n; i+=3) { 163 | const splats = []; 164 | const i1 = indices[i] * 3; 165 | const i2 = indices[i+1] * 3; 166 | const i3 = indices[i+2] * 3; 167 | const indexes = [i1, i2, i3]; 168 | for (let j = 0; j < 3; j++) { 169 | const j1 = indexes[j]; 170 | _P.fromArray(wsPositions, j1); 171 | _N.fromArray(normals, j1); 172 | _D.fromArray(up, j1); 173 | const s = this._params.colourGenerator.GetSplat(_P, _N, _D); 174 | splats.push(s); 175 | } 176 | 177 | const splatStrengths = {}; 178 | for (let k in splats[0]) { 179 | splatStrengths[k] = {key: k, strength: 0.0}; 180 | } 181 | for (let curSplat of splats) { 182 | for (let k in curSplat) { 183 | splatStrengths[k].strength += curSplat[k].strength; 184 | } 185 | } 186 | 187 | let typeValues = Object.values(splatStrengths); 188 | typeValues.sort((a, b) => { 189 | if (a.strength < b.strength) { 190 | return 1; 191 | } 192 | if (a.strength > b.strength) { 193 | return -1; 194 | } 195 | return 0; 196 | }); 197 | 198 | const w1 = indices[i] * 4; 199 | const w2 = indices[i+1] * 4; 200 | const w3 = indices[i+2] * 4; 201 | 202 | for (let s = 0; s < 3; s++) { 203 | let total = ( 204 | splats[s][typeValues[0].key].strength + 205 | splats[s][typeValues[1].key].strength + 206 | splats[s][typeValues[2].key].strength + 207 | splats[s][typeValues[3].key].strength); 208 | const normalization = 1.0 / total; 209 | 210 | splats[s][typeValues[0].key].strength *= normalization; 211 | splats[s][typeValues[1].key].strength *= normalization; 212 | splats[s][typeValues[2].key].strength *= normalization; 213 | splats[s][typeValues[3].key].strength *= normalization; 214 | } 215 | 216 | weights1.push(splats[0][typeValues[3].key].index); 217 | weights1.push(splats[0][typeValues[2].key].index); 218 | weights1.push(splats[0][typeValues[1].key].index); 219 | weights1.push(splats[0][typeValues[0].key].index); 220 | 221 | weights1.push(splats[1][typeValues[3].key].index); 222 | weights1.push(splats[1][typeValues[2].key].index); 223 | weights1.push(splats[1][typeValues[1].key].index); 224 | weights1.push(splats[1][typeValues[0].key].index); 225 | 226 | weights1.push(splats[2][typeValues[3].key].index); 227 | weights1.push(splats[2][typeValues[2].key].index); 228 | weights1.push(splats[2][typeValues[1].key].index); 229 | weights1.push(splats[2][typeValues[0].key].index); 230 | 231 | weights2.push(splats[0][typeValues[3].key].strength); 232 | weights2.push(splats[0][typeValues[2].key].strength); 233 | weights2.push(splats[0][typeValues[1].key].strength); 234 | weights2.push(splats[0][typeValues[0].key].strength); 235 | 236 | weights2.push(splats[1][typeValues[3].key].strength); 237 | weights2.push(splats[1][typeValues[2].key].strength); 238 | weights2.push(splats[1][typeValues[1].key].strength); 239 | weights2.push(splats[1][typeValues[0].key].strength); 240 | 241 | weights2.push(splats[2][typeValues[3].key].strength); 242 | weights2.push(splats[2][typeValues[2].key].strength); 243 | weights2.push(splats[2][typeValues[1].key].strength); 244 | weights2.push(splats[2][typeValues[0].key].strength); 245 | } 246 | 247 | function _Unindex(src, stride) { 248 | const dst = []; 249 | for (let i = 0, n = indices.length; i < n; i+= 3) { 250 | const i1 = indices[i] * stride; 251 | const i2 = indices[i+1] * stride; 252 | const i3 = indices[i+2] * stride; 253 | 254 | for (let j = 0; j < stride; j++) { 255 | dst.push(src[i1 + j]); 256 | } 257 | for (let j = 0; j < stride; j++) { 258 | dst.push(src[i2 + j]); 259 | } 260 | for (let j = 0; j < stride; j++) { 261 | dst.push(src[i3 + j]); 262 | } 263 | } 264 | return dst; 265 | } 266 | 267 | const uiPositions = _Unindex(positions, 3); 268 | const uiColours = _Unindex(colors, 3); 269 | const uiNormals = _Unindex(normals, 3); 270 | const uiCoords = _Unindex(coords, 3); 271 | const uiUVs = _Unindex(uvs, 2); 272 | const uiWeights1 = weights1; 273 | const uiWeights2 = weights2; 274 | 275 | const bytesInFloat32 = 4; 276 | const positionsArray = new Float32Array( 277 | new SharedArrayBuffer(bytesInFloat32 * uiPositions.length)); 278 | const coloursArray = new Float32Array( 279 | new SharedArrayBuffer(bytesInFloat32 * uiColours.length)); 280 | const normalsArray = new Float32Array( 281 | new SharedArrayBuffer(bytesInFloat32 * uiNormals.length)); 282 | const coordsArray = new Float32Array( 283 | new SharedArrayBuffer(bytesInFloat32 * uiCoords.length)); 284 | const uvsArray = new Float32Array( 285 | new SharedArrayBuffer(bytesInFloat32 * uiUVs.length)); 286 | const weights1Array = new Float32Array( 287 | new SharedArrayBuffer(bytesInFloat32 * uiWeights2.length)); 288 | const weights2Array = new Float32Array( 289 | new SharedArrayBuffer(bytesInFloat32 * uiWeights2.length)); 290 | 291 | positionsArray.set(uiPositions, 0); 292 | coloursArray.set(uiColours, 0); 293 | normalsArray.set(uiNormals, 0); 294 | uvsArray.set(uiUVs, 0); 295 | coordsArray.set(uiCoords, 0); 296 | weights1Array.set(uiWeights1, 0); 297 | weights2Array.set(uiWeights2, 0); 298 | 299 | return { 300 | positions: positionsArray, 301 | colours: coloursArray, 302 | uvs: uvsArray, 303 | normals: normalsArray, 304 | coords: coordsArray, 305 | weights1: weights1Array, 306 | weights2: weights2Array, 307 | }; 308 | } 309 | } 310 | 311 | const _CHUNK = new _TerrainBuilderThreadedWorker(); 312 | 313 | self.onmessage = (msg) => { 314 | if (msg.data.subject == 'build_chunk') { 315 | _CHUNK.Init(msg.data.params); 316 | 317 | const rebuiltData = _CHUNK.Rebuild(); 318 | self.postMessage({subject: 'build_chunk_result', data: rebuiltData}); 319 | } 320 | } -------------------------------------------------------------------------------- /src/terrain-builder-threaded.js: -------------------------------------------------------------------------------- 1 | 2 | import {terrain_chunk} from './terrain-chunk.js'; 3 | 4 | 5 | export const terrain_builder_threaded = (function() { 6 | 7 | const _NUM_WORKERS = 7; 8 | 9 | let _IDs = 0; 10 | 11 | class WorkerThread { 12 | constructor(s) { 13 | this._worker = new Worker(s, {type: 'module'}); 14 | this._worker.onmessage = (e) => { 15 | this._OnMessage(e); 16 | }; 17 | this._resolve = null; 18 | this._id = _IDs++; 19 | } 20 | 21 | _OnMessage(e) { 22 | const resolve = this._resolve; 23 | this._resolve = null; 24 | resolve(e.data); 25 | } 26 | 27 | get id() { 28 | return this._id; 29 | } 30 | 31 | postMessage(s, resolve) { 32 | this._resolve = resolve; 33 | this._worker.postMessage(s); 34 | } 35 | } 36 | 37 | class WorkerThreadPool { 38 | constructor(sz, entry) { 39 | this._workers = [...Array(sz)].map(_ => new WorkerThread(entry)); 40 | this._free = [...this._workers]; 41 | this._busy = {}; 42 | this._queue = []; 43 | } 44 | 45 | get length() { 46 | return this._workers.length; 47 | } 48 | 49 | get Busy() { 50 | return this._queue.length > 0 || Object.keys(this._busy).length > 0; 51 | } 52 | 53 | Enqueue(workItem, resolve) { 54 | this._queue.push([workItem, resolve]); 55 | this._PumpQueue(); 56 | } 57 | 58 | _PumpQueue() { 59 | while (this._free.length > 0 && this._queue.length > 0) { 60 | const w = this._free.pop(); 61 | this._busy[w.id] = w; 62 | 63 | const [workItem, workResolve] = this._queue.shift(); 64 | 65 | w.postMessage(workItem, (v) => { 66 | delete this._busy[w.id]; 67 | this._free.push(w); 68 | workResolve(v); 69 | this._PumpQueue(); 70 | }); 71 | } 72 | } 73 | } 74 | 75 | class _TerrainChunkRebuilder_Threaded { 76 | constructor(params) { 77 | this._pool = {}; 78 | this._old = []; 79 | 80 | this._workerPool = new WorkerThreadPool( 81 | _NUM_WORKERS, 'src/terrain-builder-threaded-worker.js'); 82 | 83 | this._params = params; 84 | } 85 | 86 | _OnResult(chunk, msg) { 87 | if (msg.subject == 'build_chunk_result') { 88 | chunk.RebuildMeshFromData(msg.data); 89 | chunk.Show(); 90 | } 91 | } 92 | 93 | AllocateChunk(params) { 94 | const w = params.width; 95 | 96 | if (!(w in this._pool)) { 97 | this._pool[w] = []; 98 | } 99 | 100 | let c = null; 101 | if (this._pool[w].length > 0) { 102 | c = this._pool[w].pop(); 103 | c._params = params; 104 | } else { 105 | c = new terrain_chunk.TerrainChunk(params); 106 | } 107 | 108 | c.Hide(); 109 | 110 | const threadedParams = { 111 | noiseParams: params.noiseParams, 112 | colourNoiseParams: params.colourNoiseParams, 113 | biomesParams: params.biomesParams, 114 | colourGeneratorParams: params.colourGeneratorParams, 115 | heightGeneratorsParams: params.heightGeneratorsParams, 116 | width: params.width, 117 | offset: [params.offset.x, params.offset.y, params.offset.z], 118 | origin: params.origin, 119 | radius: params.radius, 120 | resolution: params.resolution, 121 | worldMatrix: params.transform, 122 | }; 123 | 124 | const msg = { 125 | subject: 'build_chunk', 126 | params: threadedParams, 127 | }; 128 | 129 | this._workerPool.Enqueue(msg, (m) => { 130 | this._OnResult(c, m); 131 | }); 132 | 133 | return c; 134 | } 135 | 136 | RetireChunks(chunks) { 137 | this._old.push(...chunks); 138 | } 139 | 140 | _RecycleChunks(chunks) { 141 | for (let c of chunks) { 142 | if (!(c.chunk._params.width in this._pool)) { 143 | this._pool[c.chunk._params.width] = []; 144 | } 145 | 146 | c.chunk.Destroy(); 147 | } 148 | } 149 | 150 | get Busy() { 151 | return this._workerPool.Busy; 152 | } 153 | 154 | Rebuild(chunks) { 155 | for (let k in chunks) { 156 | this._workerPool.Enqueue(chunks[k].chunk._params); 157 | } 158 | } 159 | 160 | Update() { 161 | if (!this.Busy) { 162 | this._RecycleChunks(this._old); 163 | this._old = []; 164 | } 165 | } 166 | } 167 | 168 | return { 169 | TerrainChunkRebuilder_Threaded: _TerrainChunkRebuilder_Threaded 170 | } 171 | })(); 172 | -------------------------------------------------------------------------------- /src/terrain-builder.js: -------------------------------------------------------------------------------- 1 | import {terrain_chunk} from './terrain-chunk.js'; 2 | 3 | 4 | export const terrain_builder = (function() { 5 | 6 | class _TerrainChunkRebuilder { 7 | constructor(params) { 8 | this._pool = {}; 9 | this._params = params; 10 | this._Reset(); 11 | } 12 | 13 | AllocateChunk(params) { 14 | const w = params.width; 15 | 16 | if (!(w in this._pool)) { 17 | this._pool[w] = []; 18 | } 19 | 20 | let c = null; 21 | if (this._pool[w].length > 0) { 22 | c = this._pool[w].pop(); 23 | c._params = params; 24 | } else { 25 | c = new terrain_chunk.TerrainChunk(params); 26 | } 27 | 28 | c.Hide(); 29 | 30 | this._queued.push(c); 31 | 32 | return c; 33 | } 34 | 35 | RetireChunks(chunks) { 36 | this._old.push(...chunks); 37 | } 38 | 39 | _RecycleChunks(chunks) { 40 | for (let c of chunks) { 41 | if (!(c.chunk._params.width in this._pool)) { 42 | this._pool[c.chunk._params.width] = []; 43 | } 44 | 45 | c.chunk.Destroy(); 46 | } 47 | } 48 | 49 | _Reset() { 50 | this._active = null; 51 | this._queued = []; 52 | this._old = []; 53 | this._new = []; 54 | } 55 | 56 | get Busy() { 57 | return this._active || this._queued.length > 0; 58 | } 59 | 60 | Rebuild(chunks) { 61 | if (this.Busy) { 62 | return; 63 | } 64 | for (let k in chunks) { 65 | this._queued.push(chunks[k].chunk); 66 | } 67 | } 68 | 69 | Update() { 70 | if (this._active) { 71 | const r = this._active.next(); 72 | if (r.done) { 73 | this._active = null; 74 | } 75 | } else { 76 | const b = this._queued.pop(); 77 | if (b) { 78 | this._active = b._Rebuild(); 79 | this._new.push(b); 80 | } 81 | } 82 | 83 | if (this._active) { 84 | return; 85 | } 86 | 87 | if (!this._queued.length) { 88 | this._RecycleChunks(this._old); 89 | for (let b of this._new) { 90 | b.Show(); 91 | } 92 | this._Reset(); 93 | } 94 | } 95 | } 96 | 97 | return { 98 | TerrainChunkRebuilder: _TerrainChunkRebuilder 99 | } 100 | })(); 101 | -------------------------------------------------------------------------------- /src/terrain-chunk.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | 4 | export const terrain_chunk = (function() { 5 | 6 | class TerrainChunk { 7 | constructor(params) { 8 | this._params = params; 9 | this._Init(params); 10 | } 11 | 12 | Destroy() { 13 | this._params.group.remove(this._plane); 14 | } 15 | 16 | Hide() { 17 | this._plane.visible = false; 18 | } 19 | 20 | Show() { 21 | this._plane.visible = true; 22 | } 23 | 24 | _Init(params) { 25 | this._geometry = new THREE.BufferGeometry(); 26 | this._plane = new THREE.Mesh(this._geometry, params.material); 27 | this._plane.castShadow = false; 28 | this._plane.receiveShadow = true; 29 | this._plane.frustumCulled = false; 30 | this._params.group.add(this._plane); 31 | this.Reinit(params); 32 | } 33 | 34 | Update(cameraPosition) { 35 | this._plane.position.copy(this._params.origin); 36 | this._plane.position.sub(cameraPosition); 37 | } 38 | 39 | Reinit(params) { 40 | this._params = params; 41 | this._plane.position.set(0, 0, 0); 42 | } 43 | 44 | RebuildMeshFromData(data) { 45 | this._geometry.setAttribute( 46 | 'position', new THREE.Float32BufferAttribute(data.positions, 3)); 47 | this._geometry.setAttribute( 48 | 'color', new THREE.Float32BufferAttribute(data.colours, 3)); 49 | this._geometry.setAttribute( 50 | 'normal', new THREE.Float32BufferAttribute(data.normals, 3)); 51 | this._geometry.setAttribute( 52 | 'coords', new THREE.Float32BufferAttribute(data.coords, 3)); 53 | this._geometry.setAttribute( 54 | 'weights1', new THREE.Float32BufferAttribute(data.weights1, 4)); 55 | this._geometry.setAttribute( 56 | 'weights2', new THREE.Float32BufferAttribute(data.weights2, 4)); 57 | } 58 | } 59 | 60 | return { 61 | TerrainChunk: TerrainChunk 62 | } 63 | })(); 64 | -------------------------------------------------------------------------------- /src/terrain-constants.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const terrain_constants = (function() { 4 | return { 5 | QT_MIN_CELL_SIZE: 25, 6 | QT_MIN_CELL_RESOLUTION: 48, 7 | PLANET_RADIUS: 400000.0, 8 | 9 | NOISE_HEIGHT: 900.0, 10 | NOISE_SCALE: 18000.0, 11 | } 12 | })(); 13 | -------------------------------------------------------------------------------- /src/terrain-shader.js: -------------------------------------------------------------------------------- 1 | export const terrain_shader = (function() { 2 | 3 | const _VS = `#version 300 es 4 | 5 | precision highp float; 6 | 7 | uniform mat4 modelMatrix; 8 | uniform mat4 modelViewMatrix; 9 | uniform mat4 viewMatrix; 10 | uniform mat4 projectionMatrix; 11 | uniform vec3 cameraPosition; 12 | uniform float fogDensity; 13 | uniform vec3 cloudScale; 14 | 15 | // Attributes 16 | in vec3 position; 17 | in vec3 normal; 18 | in vec3 coords; 19 | in vec3 color; 20 | in vec2 uv; 21 | in vec4 weights1; 22 | in vec4 weights2; 23 | 24 | // Outputs 25 | out vec2 vUV; 26 | out vec4 vColor; 27 | out vec3 vNormal; 28 | out vec3 vCoords; 29 | out vec3 vVSPos; 30 | out vec4 vWeights1; 31 | out vec4 vWeights2; 32 | 33 | #define saturate(a) clamp( a, 0.0, 1.0 ) 34 | 35 | void main(){ 36 | mat4 terrainMatrix = mat4( 37 | viewMatrix[0], 38 | viewMatrix[1], 39 | viewMatrix[2], 40 | vec4(0.0, 0.0, 0.0, 1.0)); 41 | 42 | gl_Position = projectionMatrix * terrainMatrix * modelMatrix * vec4(position, 1.0); 43 | 44 | vUV = uv; 45 | vNormal = normal; 46 | 47 | vColor = vec4(color, 1); 48 | vCoords = position.xyz; 49 | vVSPos = (terrainMatrix * modelMatrix * vec4(position, 1.0)).xyz; 50 | vWeights1 = weights1; 51 | vWeights2 = weights2; 52 | } 53 | `; 54 | 55 | 56 | const _PS = `#version 300 es 57 | 58 | precision highp float; 59 | precision highp int; 60 | precision highp sampler2DArray; 61 | 62 | uniform sampler2DArray normalMap; 63 | uniform sampler2DArray diffuseMap; 64 | uniform sampler2D noiseMap; 65 | 66 | uniform mat4 modelMatrix; 67 | uniform mat4 modelViewMatrix; 68 | uniform vec3 cameraPosition; 69 | 70 | in vec2 vUV; 71 | in vec4 vColor; 72 | in vec3 vNormal; 73 | in vec3 vCoords; 74 | in vec3 vVSPos; 75 | in vec4 vWeights1; 76 | in vec4 vWeights2; 77 | 78 | out vec4 out_FragColor; 79 | 80 | #define saturate(a) clamp( a, 0.0, 1.0 ) 81 | 82 | const float _TRI_SCALE = 10.0; 83 | 84 | float sum( vec3 v ) { return v.x+v.y+v.z; } 85 | 86 | vec4 hash4( vec2 p ) { 87 | return fract( 88 | sin(vec4(1.0+dot(p,vec2(37.0,17.0)), 89 | 2.0+dot(p,vec2(11.0,47.0)), 90 | 3.0+dot(p,vec2(41.0,29.0)), 91 | 4.0+dot(p,vec2(23.0,31.0))))*103.0); 92 | } 93 | 94 | 95 | vec3 _ACESFilmicToneMapping(vec3 x) { 96 | float a = 2.51; 97 | float b = 0.03; 98 | float c = 2.43; 99 | float d = 0.59; 100 | float e = 0.14; 101 | return saturate((x*(a*x+b))/(x*(c*x+d)+e)); 102 | } 103 | 104 | vec4 _CalculateLighting( 105 | vec3 lightDirection, vec3 lightColour, vec3 worldSpaceNormal, vec3 viewDirection) { 106 | float diffuse = saturate(dot(worldSpaceNormal, lightDirection)); 107 | return vec4(lightColour * diffuse, 0.0); 108 | 109 | // vec3 H = normalize(lightDirection + viewDirection); 110 | // float NdotH = dot(worldSpaceNormal, H); 111 | // float specular = 0.0;// saturate(pow(NdotH, 8.0)); 112 | 113 | // return vec4(lightColour * (diffuse + diffuse * specular), 0); 114 | } 115 | 116 | vec4 _ComputeLighting(vec3 worldSpaceNormal, vec3 sunDir, vec3 viewDirection) { 117 | // Hardcoded, whee! 118 | vec4 lighting; 119 | 120 | lighting += _CalculateLighting( 121 | sunDir, vec3(1.0, 1.0, 1.0), worldSpaceNormal, viewDirection); 122 | lighting += _CalculateLighting( 123 | vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection); 124 | 125 | lighting += vec4(0.15, 0.15, 0.15, 0.0); 126 | 127 | return lighting; 128 | } 129 | 130 | vec4 _TerrainBlend_4(vec4 samples[4]) { 131 | float depth = 0.2; 132 | float ma = max( 133 | samples[0].w, 134 | max( 135 | samples[1].w, 136 | max(samples[2].w, samples[3].w))) - depth; 137 | 138 | float b1 = max(samples[0].w - ma, 0.0); 139 | float b2 = max(samples[1].w - ma, 0.0); 140 | float b3 = max(samples[2].w - ma, 0.0); 141 | float b4 = max(samples[3].w - ma, 0.0); 142 | 143 | vec4 numer = ( 144 | samples[0] * b1 + samples[1] * b2 + 145 | samples[2] * b3 + samples[3] * b4); 146 | float denom = (b1 + b2 + b3 + b4); 147 | return numer / denom; 148 | } 149 | 150 | vec4 _TerrainBlend_4_lerp(vec4 samples[4]) { 151 | return ( 152 | samples[0] * samples[0].w + samples[1] * samples[1].w + 153 | samples[2] * samples[2].w + samples[3] * samples[3].w); 154 | } 155 | 156 | // Lifted from https://www.shadertoy.com/view/Xtl3zf 157 | vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) { 158 | float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup 159 | float l = k*8.0; 160 | float f = fract(l); 161 | 162 | float ia = floor(l+0.5); // suslik's method (see comments) 163 | float ib = floor(l); 164 | f = min(f, 1.0-f)*2.0; 165 | 166 | vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash 167 | vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash 168 | 169 | vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z)); 170 | vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z)); 171 | 172 | return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz))); 173 | } 174 | 175 | vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 176 | vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 177 | vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 178 | vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 179 | 180 | vec3 weights = abs(normal.xyz); 181 | weights = weights / (weights.x + weights.y + weights.z); 182 | 183 | return dx * weights.x + dy * weights.y + dz * weights.z; 184 | } 185 | 186 | vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 187 | // Tangent Reconstruction 188 | // Triplanar uvs 189 | vec2 uvX = pos.zy; // x facing plane 190 | vec2 uvY = pos.xz; // y facing plane 191 | vec2 uvZ = pos.xy; // z facing plane 192 | // Tangent space normal maps 193 | vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 194 | vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 195 | vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 196 | 197 | vec3 weights = abs(normal.xyz); 198 | weights = weights / (weights.x + weights.y + weights.z); 199 | 200 | // Get the sign (-1 or 1) of the surface normal 201 | vec3 axis = sign(normal); 202 | // Construct tangent to world matrices for each axis 203 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 204 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 205 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 206 | 207 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 208 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 209 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 210 | 211 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 212 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 213 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 214 | 215 | // Apply tangent to world matrix and triblend 216 | // Using clamp() because the cross products may be NANs 217 | vec3 worldNormal = normalize( 218 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 219 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 220 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z 221 | ); 222 | return vec4(worldNormal, 0.0); 223 | } 224 | 225 | vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 226 | vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 227 | vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 228 | vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 229 | 230 | vec3 weights = abs(normal.xyz); 231 | weights = weights / (weights.x + weights.y + weights.z); 232 | 233 | return dx * weights.x + dy * weights.y + dz * weights.z; 234 | } 235 | 236 | vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 237 | vec2 uvx = pos.zy; 238 | vec2 uvy = pos.xz; 239 | vec2 uvz = pos.xy; 240 | vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 241 | vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 242 | vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 243 | 244 | vec3 weights = abs(normal.xyz); 245 | weights *= weights; 246 | weights = weights / (weights.x + weights.y + weights.z); 247 | 248 | vec3 axis = sign(normal); 249 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 250 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 251 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 252 | 253 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 254 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 255 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 256 | 257 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 258 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 259 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 260 | 261 | vec3 worldNormal = normalize( 262 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 263 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 264 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z); 265 | return vec4(worldNormal, 0.0); 266 | } 267 | 268 | void main() { 269 | vec3 worldPosition = (modelMatrix * vec4(vCoords, 1.0)).xyz; 270 | vec3 eyeDirection = normalize(worldPosition - cameraPosition); 271 | vec3 sunDir = normalize(vec3(1, 1, -1)); 272 | 273 | float weightIndices[4] = float[4](vWeights1.x, vWeights1.y, vWeights1.z, vWeights1.w); 274 | float weightValues[4] = float[4](vWeights2.x, vWeights2.y, vWeights2.z, vWeights2.w); 275 | 276 | // TRIPLANAR SPLATTING w/ NORMALS & UVS 277 | vec3 worldSpaceNormal = normalize(vNormal); 278 | vec4 diffuseSamples[4]; 279 | vec4 normalSamples[4]; 280 | 281 | for (int i = 0; i < 4; ++i) { 282 | vec4 d = vec4(0.0); 283 | vec4 n = vec4(0.0); 284 | if (weightValues[i] > 0.0) { 285 | d = _Triplanar_UV( 286 | worldPosition, worldSpaceNormal, weightIndices[i], diffuseMap); 287 | n = _TriplanarN_UV( 288 | worldPosition, worldSpaceNormal, weightIndices[i], normalMap); 289 | 290 | d.w *= weightValues[i]; 291 | n.w = d.w; 292 | } 293 | 294 | diffuseSamples[i] = d; 295 | normalSamples[i] = n; 296 | } 297 | 298 | vec4 diffuseBlended = _TerrainBlend_4(diffuseSamples); 299 | vec4 normalBlended = _TerrainBlend_4(normalSamples); 300 | vec3 diffuse = diffuseBlended.xyz; 301 | float blend = saturate(length(vVSPos) / 1000.0); 302 | worldSpaceNormal = mix(normalize(normalBlended.xyz), worldSpaceNormal, blend); 303 | 304 | // Bit of a hack to remove lighting on dark side of planet 305 | vec3 planetNormal = normalize(worldPosition); 306 | float planetLighting = saturate(dot(planetNormal, sunDir)); 307 | 308 | vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection); 309 | vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse; 310 | // vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25); 311 | 312 | finalColour *= lighting.xyz; 313 | 314 | out_FragColor = vec4(finalColour, 1); 315 | } 316 | 317 | `; 318 | 319 | return { 320 | VS: _VS, 321 | PS: _PS, 322 | }; 323 | })(); 324 | -------------------------------------------------------------------------------- /src/terrain.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {noise} from './noise.js'; 4 | import {quadtree} from './quadtree.js'; 5 | import {terrain_shader} from './terrain-shader.js'; 6 | import {terrain_builder_threaded} from './terrain-builder-threaded.js'; 7 | import {terrain_constants} from './terrain-constants.js'; 8 | import {texture_splatter} from './texture-splatter.js'; 9 | import {textures} from './textures.js'; 10 | import {utils} from './utils.js'; 11 | 12 | export const terrain = (function() { 13 | 14 | class TerrainChunkManager { 15 | constructor(params) { 16 | this._Init(params); 17 | } 18 | 19 | _Init(params) { 20 | this._params = params; 21 | 22 | const loader = new THREE.TextureLoader(); 23 | 24 | const noiseTexture = loader.load('./resources/simplex-noise.png'); 25 | noiseTexture.wrapS = THREE.RepeatWrapping; 26 | noiseTexture.wrapT = THREE.RepeatWrapping; 27 | 28 | const diffuse = new textures.TextureAtlas(params); 29 | diffuse.Load('diffuse', [ 30 | './resources/dirt_01_diffuse-1024.png', 31 | './resources/grass1-albedo3-1024.png', 32 | './resources/sandyground-albedo-1024.png', 33 | './resources/worn-bumpy-rock-albedo-1024.png', 34 | './resources/rock-snow-ice-albedo-1024.png', 35 | './resources/snow-packed-albedo-1024.png', 36 | './resources/rough-wet-cobble-albedo-1024.png', 37 | './resources/sandy-rocks1-albedo-1024.png', 38 | ]); 39 | diffuse.onLoad = () => { 40 | this._material.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas; 41 | }; 42 | 43 | const normal = new textures.TextureAtlas(params); 44 | normal.Load('normal', [ 45 | './resources/dirt_01_normal-1024.jpg', 46 | './resources/grass1-normal-1024.jpg', 47 | './resources/sandyground-normal-1024.jpg', 48 | './resources/worn-bumpy-rock-normal-1024.jpg', 49 | './resources/rock-snow-ice-normal-1024.jpg', 50 | './resources/snow-packed-normal-1024.jpg', 51 | './resources/rough-wet-cobble-normal-1024.jpg', 52 | './resources/sandy-rocks1-normal-1024.jpg', 53 | ]); 54 | normal.onLoad = () => { 55 | this._material.uniforms.normalMap.value = normal.Info['normal'].atlas; 56 | }; 57 | 58 | this._material = new THREE.MeshStandardMaterial({ 59 | wireframe: false, 60 | wireframeLinewidth: 1, 61 | color: 0xFFFFFF, 62 | side: THREE.FrontSide, 63 | vertexColors: THREE.VertexColors, 64 | // normalMap: texture, 65 | }); 66 | 67 | this._material = new THREE.RawShaderMaterial({ 68 | uniforms: { 69 | diffuseMap: { 70 | }, 71 | normalMap: { 72 | }, 73 | noiseMap: { 74 | value: noiseTexture 75 | }, 76 | }, 77 | vertexShader: terrain_shader.VS, 78 | fragmentShader: terrain_shader.PS, 79 | side: THREE.FrontSide 80 | }); 81 | 82 | this._builder = new terrain_builder_threaded.TerrainChunkRebuilder_Threaded(); 83 | // this._builder = new terrain_builder.TerrainChunkRebuilder(); 84 | 85 | this._InitNoise(params); 86 | this._InitBiomes(params); 87 | this._InitTerrain(params); 88 | } 89 | 90 | _InitNoise(params) { 91 | params.guiParams.noise = { 92 | octaves: 13, 93 | persistence: 0.5, 94 | lacunarity: 1.6, 95 | exponentiation: 7.5, 96 | height: terrain_constants.NOISE_HEIGHT, 97 | scale: terrain_constants.NOISE_SCALE, 98 | seed: 1 99 | }; 100 | 101 | const onNoiseChanged = () => { 102 | this._builder.Rebuild(this._chunks); 103 | }; 104 | 105 | const noiseRollup = params.gui.addFolder('Terrain.Noise'); 106 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange( 107 | onNoiseChanged); 108 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange( 109 | onNoiseChanged); 110 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange( 111 | onNoiseChanged); 112 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange( 113 | onNoiseChanged); 114 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange( 115 | onNoiseChanged); 116 | noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange( 117 | onNoiseChanged); 118 | 119 | this._noise = new noise.Noise(params.guiParams.noise); 120 | this._noiseParams = params.guiParams.noise; 121 | 122 | params.guiParams.heightmap = { 123 | height: 16, 124 | }; 125 | 126 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap'); 127 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange( 128 | onNoiseChanged); 129 | } 130 | 131 | _InitBiomes(params) { 132 | params.guiParams.biomes = { 133 | octaves: 2, 134 | persistence: 0.5, 135 | lacunarity: 2.0, 136 | scale: 2048.0, 137 | noiseType: 'simplex', 138 | seed: 2, 139 | exponentiation: 1, 140 | height: 1.0 141 | }; 142 | 143 | const onNoiseChanged = () => { 144 | this._builder.Rebuild(this._chunks); 145 | }; 146 | 147 | const noiseRollup = params.gui.addFolder('Terrain.Biomes'); 148 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange( 149 | onNoiseChanged); 150 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange( 151 | onNoiseChanged); 152 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange( 153 | onNoiseChanged); 154 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange( 155 | onNoiseChanged); 156 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange( 157 | onNoiseChanged); 158 | 159 | this._biomes = new noise.Noise(params.guiParams.biomes); 160 | this._biomesParams = params.guiParams.biomes; 161 | 162 | const colourParams = { 163 | octaves: 1, 164 | persistence: 0.5, 165 | lacunarity: 2.0, 166 | exponentiation: 1.0, 167 | scale: 256.0, 168 | noiseType: 'simplex', 169 | seed: 2, 170 | height: 1.0, 171 | }; 172 | this._colourNoise = new noise.Noise(colourParams); 173 | this._colourNoiseParams = colourParams; 174 | } 175 | 176 | _InitTerrain(params) { 177 | params.guiParams.terrain= { 178 | wireframe: false, 179 | }; 180 | 181 | this._groups = [...new Array(6)].map(_ => new THREE.Group()); 182 | params.scene.add(...this._groups); 183 | 184 | const terrainRollup = params.gui.addFolder('Terrain'); 185 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => { 186 | for (let k in this._chunks) { 187 | this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe; 188 | } 189 | }); 190 | 191 | this._chunks = {}; 192 | this._params = params; 193 | } 194 | 195 | _CreateTerrainChunk(group, groupTransform, offset, width, resolution) { 196 | const params = { 197 | group: group, 198 | transform: groupTransform, 199 | material: this._material, 200 | width: width, 201 | offset: offset, 202 | origin: this._params.camera.position.clone(), 203 | radius: terrain_constants.PLANET_RADIUS, 204 | resolution: resolution, 205 | biomeGenerator: this._biomes, 206 | colourGenerator: new texture_splatter.TextureSplatter( 207 | {biomeGenerator: this._biomes, colourNoise: this._colourNoise}), 208 | heightGenerators: [new texture_splatter.HeightGenerator( 209 | this._noise, offset, 100000, 100000 + 1)], 210 | noiseParams: this._noiseParams, 211 | colourNoiseParams: this._colourNoiseParams, 212 | biomesParams: this._biomesParams, 213 | colourGeneratorParams: { 214 | biomeGeneratorParams: this._biomesParams, 215 | colourNoiseParams: this._colourNoiseParams, 216 | }, 217 | heightGeneratorsParams: { 218 | min: 100000, 219 | max: 100000 + 1, 220 | } 221 | }; 222 | 223 | return this._builder.AllocateChunk(params); 224 | } 225 | 226 | Update(_) { 227 | this._builder.Update(); 228 | if (!this._builder.Busy) { 229 | this._UpdateVisibleChunks_Quadtree(); 230 | } 231 | 232 | const cameraPosition = this._params.camera.position; 233 | 234 | for (let k in this._chunks) { 235 | this._chunks[k].chunk.Update(cameraPosition); 236 | } 237 | for (let c of this._builder._old) { 238 | c.chunk.Update(cameraPosition); 239 | } 240 | 241 | this._params.scattering.uniforms.planetRadius.value = terrain_constants.PLANET_RADIUS; 242 | this._params.scattering.uniforms.atmosphereRadius.value = terrain_constants.PLANET_RADIUS * 1.01; 243 | } 244 | 245 | _UpdateVisibleChunks_Quadtree() { 246 | function _Key(c) { 247 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']'; 248 | } 249 | 250 | const q = new quadtree.CubeQuadTree({ 251 | radius: terrain_constants.PLANET_RADIUS, 252 | min_node_size: terrain_constants.QT_MIN_CELL_SIZE, 253 | }); 254 | q.Insert(this._params.camera.position); 255 | 256 | const sides = q.GetChildren(); 257 | 258 | let newTerrainChunks = {}; 259 | const center = new THREE.Vector3(); 260 | const dimensions = new THREE.Vector3(); 261 | for (let i = 0; i < sides.length; i++) { 262 | for (let c of sides[i].children) { 263 | c.bounds.getCenter(center); 264 | c.bounds.getSize(dimensions); 265 | 266 | const child = { 267 | index: i, 268 | group: this._groups[i], 269 | transform: sides[i].transform, 270 | position: [center.x, center.y, center.z], 271 | bounds: c.bounds, 272 | size: dimensions.x, 273 | }; 274 | 275 | const k = _Key(child); 276 | newTerrainChunks[k] = child; 277 | } 278 | } 279 | 280 | const intersection = utils.DictIntersection(this._chunks, newTerrainChunks); 281 | const difference = utils.DictDifference(newTerrainChunks, this._chunks); 282 | const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks)); 283 | 284 | this._builder.RetireChunks(recycle); 285 | 286 | newTerrainChunks = intersection; 287 | 288 | for (let k in difference) { 289 | const [xp, yp, zp] = difference[k].position; 290 | 291 | const offset = new THREE.Vector3(xp, yp, zp); 292 | newTerrainChunks[k] = { 293 | position: [xp, zp], 294 | chunk: this._CreateTerrainChunk( 295 | difference[k].group, difference[k].transform, 296 | offset, difference[k].size, 297 | terrain_constants.QT_MIN_CELL_RESOLUTION), 298 | }; 299 | } 300 | 301 | this._chunks = newTerrainChunks; 302 | } 303 | } 304 | 305 | return { 306 | TerrainChunkManager: TerrainChunkManager 307 | } 308 | })(); 309 | -------------------------------------------------------------------------------- /src/texture-splatter.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {math} from './math.js'; 4 | import {spline} from './spline.js'; 5 | import {terrain_constants} from './terrain-constants.js'; 6 | 7 | 8 | export const texture_splatter = (function() { 9 | 10 | const _HEIGHT_NORMALIZATION = terrain_constants.NOISE_HEIGHT / 10.0; 11 | 12 | const _WHITE = new THREE.Color(0x808080); 13 | 14 | const _DEEP_OCEAN = new THREE.Color(0x20020FF); 15 | const _SHALLOW_OCEAN = new THREE.Color(0x8080FF); 16 | const _BEACH = new THREE.Color(0xd9d592); 17 | const _SNOW = new THREE.Color(0xFFFFFF); 18 | const _FOREST_BOREAL = new THREE.Color(0x29c100); 19 | 20 | const _GREEN = new THREE.Color(0x80FF80); 21 | const _RED = new THREE.Color(0xFF8080); 22 | const _BLACK = new THREE.Color(0x000000); 23 | 24 | 25 | class FixedHeightGenerator { 26 | constructor() {} 27 | 28 | Get() { 29 | return [50, 1]; 30 | } 31 | } 32 | 33 | 34 | class FixedColourGenerator { 35 | constructor(params) { 36 | this._params = params; 37 | } 38 | 39 | Get() { 40 | return this._params.colour; 41 | } 42 | } 43 | 44 | 45 | class HeightGenerator { 46 | constructor(generator, position, minRadius, maxRadius) { 47 | this._position = position.clone(); 48 | this._radius = [minRadius, maxRadius]; 49 | this._generator = generator; 50 | } 51 | 52 | Get(x, y, z) { 53 | return [this._generator.Get(x, y, z), 1]; 54 | } 55 | } 56 | 57 | 58 | class TextureSplatter { 59 | constructor(params) { 60 | const _colourLerp = (t, p0, p1) => { 61 | const c = p0.clone(); 62 | 63 | return c.lerp(p1, t); 64 | }; 65 | this._colourSpline = [ 66 | new spline.LinearSpline(_colourLerp), 67 | new spline.LinearSpline(_colourLerp) 68 | ]; 69 | 70 | // Arid 71 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d)); 72 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc)); 73 | this._colourSpline[0].AddPoint(1.0, _SNOW); 74 | 75 | // Humid 76 | this._colourSpline[1].AddPoint(0.0, _FOREST_BOREAL); 77 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c)); 78 | this._colourSpline[1].AddPoint(1.0, _SNOW); 79 | 80 | this._oceanSpline = new spline.LinearSpline(_colourLerp); 81 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN); 82 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN); 83 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN); 84 | 85 | this._params = params; 86 | } 87 | 88 | _BaseColour(x, y, z) { 89 | const m = this._params.biomeGenerator.Get(x, y, z); 90 | const h = math.sat(z / 100.0); 91 | 92 | const c1 = this._colourSpline[0].Get(h); 93 | const c2 = this._colourSpline[1].Get(h); 94 | 95 | let c = c1.lerp(c2, m); 96 | 97 | if (h < 0.1) { 98 | c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05)); 99 | } 100 | return c; 101 | } 102 | 103 | _Colour(x, y, z) { 104 | const c = this._BaseColour(x, y, z); 105 | const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0; 106 | 107 | c.offsetHSL(0.0, 0.0, r * 0.01); 108 | return c; 109 | } 110 | 111 | _GetTextureWeights(p, n, up) { 112 | const m = this._params.biomeGenerator.Get(p.x, p.y, p.z); 113 | const h = p.z / _HEIGHT_NORMALIZATION; 114 | 115 | const types = { 116 | dirt: {index: 0, strength: 0.0}, 117 | grass: {index: 1, strength: 0.0}, 118 | gravel: {index: 2, strength: 0.0}, 119 | rock: {index: 3, strength: 0.0}, 120 | snow: {index: 4, strength: 0.0}, 121 | snowrock: {index: 5, strength: 0.0}, 122 | cobble: {index: 6, strength: 0.0}, 123 | sandyrock: {index: 7, strength: 0.0}, 124 | }; 125 | 126 | function _ApplyWeights(dst, v, m) { 127 | for (let k in types) { 128 | types[k].strength *= m; 129 | } 130 | types[dst].strength = v; 131 | }; 132 | 133 | types.grass.strength = 1.0; 134 | _ApplyWeights('gravel', 1.0 - m, m); 135 | 136 | if (h < 0.2) { 137 | const s = 1.0 - math.sat((h - 0.1) / 0.05); 138 | _ApplyWeights('cobble', s, 1.0 - s); 139 | 140 | if (h < 0.1) { 141 | const s = 1.0 - math.sat((h - 0.05) / 0.05); 142 | _ApplyWeights('sandyrock', s, 1.0 - s); 143 | } 144 | } else { 145 | if (h > 0.125) { 146 | const s = (math.sat((h - 0.125) / 1.25)); 147 | _ApplyWeights('rock', s, 1.0 - s); 148 | } 149 | 150 | if (h > 1.5) { 151 | const s = math.sat((h - 0.75) / 2.0); 152 | _ApplyWeights('snow', s, 1.0 - s); 153 | } 154 | } 155 | 156 | // In case nothing gets set. 157 | types.dirt.strength = 0.01; 158 | 159 | let total = 0.0; 160 | for (let k in types) { 161 | total += types[k].strength; 162 | } 163 | if (total < 0.01) { 164 | const a = 0; 165 | } 166 | const normalization = 1.0 / total; 167 | 168 | for (let k in types) { 169 | types[k].strength / normalization; 170 | } 171 | 172 | return types; 173 | } 174 | 175 | GetColour(position) { 176 | return this._Colour(position.x, position.y, position.z); 177 | } 178 | 179 | GetSplat(position, normal, up) { 180 | return this._GetTextureWeights(position, normal, up); 181 | } 182 | } 183 | 184 | return { 185 | HeightGenerator: HeightGenerator, 186 | TextureSplatter: TextureSplatter, 187 | } 188 | })(); 189 | -------------------------------------------------------------------------------- /src/textures.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | 4 | export const textures = (function() { 5 | 6 | // Taken from https://github.com/mrdoob/three.js/issues/758 7 | function _GetImageData( image ) { 8 | var canvas = document.createElement('canvas'); 9 | canvas.width = image.width; 10 | canvas.height = image.height; 11 | 12 | var context = canvas.getContext('2d'); 13 | context.drawImage( image, 0, 0 ); 14 | 15 | return context.getImageData( 0, 0, image.width, image.height ); 16 | } 17 | 18 | return { 19 | TextureAtlas: class { 20 | constructor(params) { 21 | this._game = params.game; 22 | this._Create(); 23 | this.onLoad = () => {}; 24 | } 25 | 26 | Load(atlas, names) { 27 | this._LoadAtlas(atlas, names); 28 | } 29 | 30 | _Create() { 31 | this._manager = new THREE.LoadingManager(); 32 | this._loader = new THREE.TextureLoader(this._manager); 33 | this._textures = {}; 34 | 35 | this._manager.onLoad = () => { 36 | this._OnLoad(); 37 | }; 38 | } 39 | 40 | get Info() { 41 | return this._textures; 42 | } 43 | 44 | _OnLoad() { 45 | for (let k in this._textures) { 46 | const atlas = this._textures[k]; 47 | const data = new Uint8Array(atlas.textures.length * 4 * 1024 * 1024); 48 | 49 | for (let t = 0; t < atlas.textures.length; t++) { 50 | const curTexture = atlas.textures[t]; 51 | const curData = _GetImageData(curTexture.image); 52 | const offset = t * (4 * 1024 * 1024); 53 | 54 | data.set(curData.data, offset); 55 | } 56 | 57 | const diffuse = new THREE.DataTexture2DArray(data, 1024, 1024, atlas.textures.length); 58 | diffuse.format = THREE.RGBAFormat; 59 | diffuse.type = THREE.UnsignedByteType; 60 | diffuse.minFilter = THREE.LinearMipMapLinearFilter; 61 | diffuse.magFilter = THREE.LinearFilter; 62 | diffuse.wrapS = THREE.RepeatWrapping; 63 | diffuse.wrapT = THREE.RepeatWrapping; 64 | diffuse.generateMipmaps = true; 65 | 66 | const caps = this._game._graphics._threejs.capabilities; 67 | const aniso = caps.getMaxAnisotropy(); 68 | 69 | diffuse.anisotropy = 4; 70 | 71 | atlas.atlas = diffuse; 72 | } 73 | 74 | this.onLoad(); 75 | } 76 | 77 | _LoadAtlas(atlas, names) { 78 | this._textures[atlas] = { 79 | textures: names.map(n => this._loader.load(n)) 80 | }; 81 | } 82 | } 83 | }; 84 | })(); 85 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const utils = (function() { 2 | return { 3 | DictIntersection: function(dictA, dictB) { 4 | const intersection = {}; 5 | for (let k in dictB) { 6 | if (k in dictA) { 7 | intersection[k] = dictA[k]; 8 | } 9 | } 10 | return intersection 11 | }, 12 | 13 | DictDifference: function(dictA, dictB) { 14 | const diff = {...dictA}; 15 | for (let k in dictB) { 16 | delete diff[k]; 17 | } 18 | return diff; 19 | } 20 | }; 21 | })(); 22 | --------------------------------------------------------------------------------