├── LICENSE ├── base.css ├── index.html ├── resources ├── simplex-noise.png ├── space-negx.jpg ├── space-negy.jpg ├── space-negz.jpg ├── space-posx.jpg ├── space-posy.jpg └── space-posz.jpg └── src ├── camera-track.js ├── controls.js ├── demo.js ├── game.js ├── graphics.js ├── main.js ├── math.js ├── 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) 2021 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/simplex-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/simplex-noise.png -------------------------------------------------------------------------------- /resources/space-negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negx.jpg -------------------------------------------------------------------------------- /resources/space-negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negy.jpg -------------------------------------------------------------------------------- /resources/space-negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negz.jpg -------------------------------------------------------------------------------- /resources/space-posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posx.jpg -------------------------------------------------------------------------------- /resources/space-posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posy.jpg -------------------------------------------------------------------------------- /resources/space-posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posz.jpg -------------------------------------------------------------------------------- /src/camera-track.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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.125/build/three.module.js'; 2 | import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/controls/PointerLockControls.js'; 3 | import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/three@0.125/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(12, 12, 12); 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: 12, 71 | }; 72 | 73 | const rollup = this._params.gui.addFolder('Camera.FPS'); 74 | rollup.add(this._params.guiParams.camera, "acceleration_x", 4.0, 24.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 -= 2 ** this._acceleration.z * timeInSeconds; 223 | } 224 | if (this._move.backward) { 225 | this._velocity.z += 2 ** this._acceleration.z * timeInSeconds; 226 | } 227 | if (this._move.left) { 228 | this._velocity.x -= 2 ** this._acceleration.x * timeInSeconds; 229 | } 230 | if (this._move.right) { 231 | this._velocity.x += 2 ** this._acceleration.x * timeInSeconds; 232 | } 233 | if (this._move.up) { 234 | this._velocity.y += 2 ** this._acceleration.y * timeInSeconds; 235 | } 236 | if (this._move.down) { 237 | this._velocity.y -= 2 ** 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.125/build/three.module.js'; 2 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.125/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.125/build/three.module.js'; 2 | import Stats from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/libs/stats.module.js'; 3 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/WebGL.js'; 4 | 5 | import {RenderPass} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/postprocessing/RenderPass.js'; 6 | import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/postprocessing/ShaderPass.js'; 7 | import {CopyShader} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/shaders/CopyShader.js'; 8 | import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/shaders/FXAAShader.js'; 9 | import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/three@0.125/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 | antialias: false, 54 | }); 55 | this._threejs.outputEncoding = THREE.LinearEncoding; 56 | this._threejs.setPixelRatio(window.devicePixelRatio); 57 | this._threejs.setSize(window.innerWidth, window.innerHeight); 58 | this._threejs.autoClear = false; 59 | 60 | const target = document.getElementById('target'); 61 | target.appendChild(this._threejs.domElement); 62 | 63 | this._stats = new Stats(); 64 | // target.appendChild(this._stats.dom); 65 | 66 | window.addEventListener('resize', () => { 67 | this._OnWindowResize(); 68 | }, false); 69 | 70 | const fov = 60; 71 | const aspect = 1920 / 1080; 72 | const near = 0.1; 73 | const far = 10000000.0; 74 | this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far); 75 | this.camera_.position.set(75, 20, 0); 76 | 77 | this.scene_ = new THREE.Scene(); 78 | this.scene_.background = new THREE.Color(0xaaaaaa); 79 | 80 | const renderPass = new RenderPass(this.scene_, this.camera_); 81 | const fxaaPass = new ShaderPass(FXAAShader); 82 | // const depthPass = new ShaderPass(scattering_shader.Shader); 83 | 84 | // this._depthPass = depthPass; 85 | 86 | this.composer_ = new EffectComposer(this._threejs); 87 | this.composer_.addPass(renderPass); 88 | this.composer_.addPass(fxaaPass); 89 | //this.composer_.addPass(depthPass); 90 | 91 | const params = { 92 | minFilter: THREE.NearestFilter, 93 | magFilter: THREE.NearestFilter, 94 | format: THREE.RGBAFormat, 95 | type: THREE.FloatType, 96 | generateMipmaps: false, 97 | }; 98 | 99 | this._target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, params); 100 | this._target.stencilBuffer = false; 101 | this._target.depthBuffer = true; 102 | this._target.depthTexture = new THREE.DepthTexture(); 103 | this._target.depthTexture.format = THREE.DepthFormat; 104 | this._target.depthTexture.type = THREE.FloatType; 105 | this._target.outputEncoding = THREE.LinearEncoding; 106 | 107 | this._threejs.setRenderTarget(this._target); 108 | 109 | const logDepthBufFC = 2.0 / ( Math.log(this.camera_.far + 1.0) / Math.LN2); 110 | 111 | this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 112 | this._depthPass = new THREE.ShaderMaterial( { 113 | vertexShader: scattering_shader.VS, 114 | fragmentShader: scattering_shader.PS, 115 | uniforms: { 116 | cameraNear: { value: this.Camera.near }, 117 | cameraFar: { value: this.Camera.far }, 118 | cameraPosition: { value: this.Camera.position }, 119 | cameraForward: { value: null }, 120 | tDiffuse: { value: null }, 121 | tDepth: { value: null }, 122 | inverseProjection: { value: null }, 123 | inverseView: { value: null }, 124 | planetPosition: { value: null }, 125 | planetRadius: { value: null }, 126 | atmosphereRadius: { value: null }, 127 | logDepthBufFC: { value: logDepthBufFC }, 128 | } 129 | } ); 130 | var postPlane = new THREE.PlaneBufferGeometry( 2, 2 ); 131 | var postQuad = new THREE.Mesh( postPlane, this._depthPass ); 132 | this._postScene = new THREE.Scene(); 133 | this._postScene.add( postQuad ); 134 | 135 | this._CreateLights(); 136 | 137 | return true; 138 | } 139 | 140 | 141 | _CreateLights() { 142 | let light = new THREE.DirectionalLight(0xFFFFFF, 1); 143 | light.position.set(100, 100, -100); 144 | light.target.position.set(0, 0, 0); 145 | light.castShadow = false; 146 | this.scene_.add(light); 147 | 148 | light = new THREE.DirectionalLight(0x404040, 1); 149 | light.position.set(100, 100, -100); 150 | light.target.position.set(0, 0, 0); 151 | light.castShadow = false; 152 | this.scene_.add(light); 153 | 154 | light = new THREE.DirectionalLight(0x404040, 1); 155 | light.position.set(100, 100, -100); 156 | light.target.position.set(0, 0, 0); 157 | light.castShadow = false; 158 | this.scene_.add(light); 159 | 160 | light = new THREE.DirectionalLight(0x202040, 1); 161 | light.position.set(100, -100, 100); 162 | light.target.position.set(0, 0, 0); 163 | light.castShadow = false; 164 | this.scene_.add(light); 165 | 166 | light = new THREE.AmbientLight(0xFFFFFF, 1.0); 167 | this.scene_.add(light); 168 | } 169 | 170 | _OnWindowResize() { 171 | this.camera_.aspect = window.innerWidth / window.innerHeight; 172 | this.camera_.updateProjectionMatrix(); 173 | this._threejs.setSize(window.innerWidth, window.innerHeight); 174 | this.composer_.setSize(window.innerWidth, window.innerHeight); 175 | this._target.setSize(window.innerWidth, window.innerHeight); 176 | } 177 | 178 | get Scene() { 179 | return this.scene_; 180 | } 181 | 182 | get Camera() { 183 | return this.camera_; 184 | } 185 | 186 | Render(timeInSeconds) { 187 | this._threejs.setRenderTarget(this._target); 188 | 189 | this._threejs.clear(); 190 | this._threejs.render(this.scene_, this.camera_); 191 | //this.composer_.render(); 192 | 193 | this._threejs.setRenderTarget( null ); 194 | 195 | const forward = new THREE.Vector3(); 196 | this.camera_.getWorldDirection(forward); 197 | 198 | this._depthPass.uniforms.inverseProjection.value = this.camera_.projectionMatrixInverse; 199 | this._depthPass.uniforms.inverseView.value = this.camera_.matrixWorld; 200 | this._depthPass.uniforms.tDiffuse.value = this._target.texture; 201 | this._depthPass.uniforms.tDepth.value = this._target.depthTexture; 202 | this._depthPass.uniforms.cameraNear.value = this.camera_.near; 203 | this._depthPass.uniforms.cameraFar.value = this.camera_.far; 204 | this._depthPass.uniforms.cameraPosition.value = this.camera_.position; 205 | this._depthPass.uniforms.cameraForward.value = forward; 206 | this._depthPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0); 207 | this._depthPass.uniformsNeedUpdate = true; 208 | 209 | this._threejs.render( this._postScene, this._postCamera ); 210 | 211 | this._stats.update(); 212 | } 213 | } 214 | 215 | return { 216 | Graphics: _Graphics, 217 | GetPixel: _GetPixel, 218 | GetImageData: _GetImageData, 219 | }; 220 | })(); 221 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js'; 2 | import {GUI} from 'https://cdn.jsdelivr.net/npm/three@0.125/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.graphics_.Camera.position.set(355898.9978932907, -16169.249553939484, -181920.2108868533); 19 | this.graphics_.Camera.quaternion.set(0.3525209450519473, 0.6189868049149101, -0.58773147927222, 0.38360921119467495); 20 | // this.graphics_.Camera.position.set(283679.0800079606, -314104.8959314113, -71872.13040166264); 21 | // this.graphics_.Camera.quaternion.set(0.4461797373759622, 0.5541566632843257, -0.523697972824766, 0.46858773751767197); 22 | 23 | this.graphics_.Camera.position.set(357183.28155512916, -19402.113225302386, -182320.80530987142); 24 | this.graphics_.Camera.quaternion.set(0.2511776691104541, 0.6998229958650649, -0.48248862753627253, 0.46299274000447177); 25 | 26 | this._AddEntity('_terrain', new terrain.TerrainChunkManager({ 27 | camera: this.graphics_.Camera, 28 | scene: this.graphics_.Scene, 29 | scattering: this.graphics_._depthPass, 30 | gui: this._gui, 31 | guiParams: this._guiParams, 32 | game: this}), 1.0); 33 | 34 | this._AddEntity('_controls', new controls.FPSControls({ 35 | camera: this.graphics_.Camera, 36 | scene: this.graphics_.Scene, 37 | domElement: this.graphics_._threejs.domElement, 38 | gui: this._gui, 39 | guiParams: this._guiParams}), 0.0); 40 | 41 | this._totalTime = 0; 42 | 43 | this._LoadBackground(); 44 | } 45 | 46 | _CreateGUI() { 47 | this._guiParams = { 48 | general: { 49 | }, 50 | }; 51 | this._gui = new GUI(); 52 | 53 | const generalRollup = this._gui.addFolder('General'); 54 | this._gui.close(); 55 | } 56 | 57 | _LoadBackground() { 58 | this.graphics_.Scene.background = new THREE.Color(0x000000); 59 | const loader = new THREE.CubeTextureLoader(); 60 | const texture = loader.load([ 61 | './resources/space-posx.jpg', 62 | './resources/space-negx.jpg', 63 | './resources/space-posy.jpg', 64 | './resources/space-negy.jpg', 65 | './resources/space-posz.jpg', 66 | './resources/space-negz.jpg', 67 | ]); 68 | texture.encoding = THREE.sRGBEncoding; 69 | this.graphics_.Scene.background = texture; 70 | } 71 | 72 | _OnStep(timeInSeconds) { 73 | } 74 | } 75 | 76 | 77 | function _Main() { 78 | _APP = new ProceduralTerrain_Demo(); 79 | } 80 | 81 | _Main(); 82 | -------------------------------------------------------------------------------- /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 {simplex} from './simplex-noise.js'; 2 | 3 | export const noise = (function() { 4 | 5 | class _NoiseGenerator { 6 | constructor(params) { 7 | this._params = params; 8 | this._Init(); 9 | } 10 | 11 | _Init() { 12 | this._noise = new simplex.SimplexNoise(this._params.seed); 13 | } 14 | 15 | Get(x, y, z) { 16 | const G = 2.0 ** (-this._params.persistence); 17 | const xs = x / this._params.scale; 18 | const ys = y / this._params.scale; 19 | const zs = z / this._params.scale; 20 | const noiseFunc = this._noise; 21 | 22 | let amplitude = 1.0; 23 | let frequency = 1.0; 24 | let normalization = 0; 25 | let total = 0; 26 | for (let o = 0; o < this._params.octaves; o++) { 27 | const noiseValue = noiseFunc.noise3D( 28 | xs * frequency, ys * frequency, zs * frequency) * 0.5 + 0.5; 29 | 30 | total += noiseValue * amplitude; 31 | normalization += amplitude; 32 | amplitude *= G; 33 | frequency *= this._params.lacunarity; 34 | } 35 | total /= normalization; 36 | return Math.pow( 37 | total, this._params.exponentiation) * this._params.height; 38 | } 39 | } 40 | 41 | return { 42 | Noise: _NoiseGenerator 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /src/quadtree.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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 0 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 1 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 2 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 3 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 4 41 | m = new THREE.Matrix4(); 42 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r)); 43 | transforms.push(m); 44 | 45 | // -Z 5 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 i = 0; i < transforms.length; ++i) { 52 | const t = transforms[i]; 53 | this.sides_.push({ 54 | transform: t.clone(), 55 | quadtree: new QuadTree({ 56 | side: i, 57 | size: r, 58 | min_node_size: params.min_node_size, 59 | max_node_size: params.max_node_size, 60 | localToWorld: t, 61 | worldToLocal: t.clone().invert() 62 | }), 63 | }); 64 | } 65 | 66 | this.BuildRootNeighbourInfo_(); 67 | } 68 | 69 | BuildRootNeighbourInfo_() { 70 | const _FindClosestNeighbour = (edgeMidpoint, otherNodes) => { 71 | const neighbours = [...otherNodes].sort((a, b) => { 72 | return a.sphereCenter.distanceTo(edgeMidpoint) - b.sphereCenter.distanceTo(edgeMidpoint); 73 | }); 74 | const test = [...otherNodes].map(c => { 75 | return c.sphereCenter.distanceTo(edgeMidpoint); 76 | }).sort((a, b) => a - b); 77 | return neighbours[0]; 78 | }; 79 | 80 | const nodes = this.sides_.map(s => s.quadtree.root_); 81 | 82 | for (let i = 0; i < 6; ++i) { 83 | const node = nodes[i]; 84 | const edgeMidpoints = [ 85 | node.GetLeftEdgeMidpoint(), 86 | node.GetTopEdgeMidpoint(), 87 | node.GetRightEdgeMidpoint(), 88 | node.GetBottomEdgeMidpoint(), 89 | ]; 90 | const otherNodes = nodes.filter(n => n.side != node.side); 91 | 92 | const neighbours = edgeMidpoints.map(p => _FindClosestNeighbour(p, otherNodes)); 93 | node.neighbours = neighbours.map(n => nodes[n.side]); 94 | } 95 | } 96 | 97 | GetChildren() { 98 | const children = []; 99 | 100 | for (let s of this.sides_) { 101 | const side = { 102 | transform: s.transform, 103 | children: s.quadtree.GetChildren(), 104 | } 105 | children.push(side); 106 | } 107 | return children; 108 | } 109 | 110 | Insert(pos) { 111 | for (let s of this.sides_) { 112 | s.quadtree.Insert(pos); 113 | } 114 | } 115 | 116 | BuildNeighbours() { 117 | let queue = []; 118 | for (let s of this.sides_) { 119 | queue.push(s.quadtree.root_); 120 | } 121 | 122 | while (queue.length > 0) { 123 | const node = queue.shift(); 124 | 125 | this.sides_[node.side].quadtree.BuildNeighbours_Child_(node); 126 | 127 | for (let c of node.children) { 128 | queue.push(c); 129 | } 130 | } 131 | } 132 | } 133 | 134 | const LEFT = 0; 135 | const TOP = 1; 136 | const RIGHT = 2; 137 | const BOTTOM = 3; 138 | 139 | const TOP_LEFT = 2; 140 | const TOP_RIGHT = 3; 141 | const BOTTOM_LEFT = 0; 142 | const BOTTOM_RIGHT = 1; 143 | 144 | class Node { 145 | constructor(params) { 146 | } 147 | 148 | GetNeighbour(side) { 149 | return this.neighbours[side]; 150 | } 151 | 152 | GetClosestChild(node) { 153 | const children = [...this.children].sort((a, b) => { 154 | return a.sphereCenter.distanceTo(node.sphereCenter) - b.sphereCenter.distanceTo(node.sphereCenter); 155 | }); 156 | const test = [...this.children].map(c => { 157 | return c.sphereCenter.distanceTo(node.sphereCenter); 158 | }).sort((a, b) => a - b); 159 | return children[0]; 160 | } 161 | 162 | GetChild(pos) { 163 | return this.children[pos]; 164 | } 165 | 166 | GetClosestChildrenSharingEdge(edgePoint) { 167 | if (this.children.length == 0) { 168 | const edgePointLocal = edgePoint.clone().applyMatrix4(this.tree.worldToLocal); 169 | if (edgePointLocal.x == this.bounds.min.x || edgePointLocal.x == this.bounds.max.x || 170 | edgePointLocal.y == this.bounds.min.y || edgePointLocal.y == this.bounds.max.y) { 171 | return [this]; 172 | } 173 | return []; 174 | } 175 | 176 | const matches = []; 177 | for (let i = 0; i < this.children.length; ++i) { 178 | const child = this.children[i]; 179 | 180 | matches.push(...child.GetClosestChildrenSharingEdge(edgePoint)); 181 | } 182 | return matches; 183 | } 184 | 185 | GetLeftEdgeMidpoint() { 186 | const v = new THREE.Vector3(this.bounds.min.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0); 187 | v.applyMatrix4(this.localToWorld); 188 | return v; 189 | } 190 | 191 | GetRightEdgeMidpoint() { 192 | const v = new THREE.Vector3(this.bounds.max.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0); 193 | v.applyMatrix4(this.localToWorld); 194 | return v; 195 | } 196 | 197 | GetTopEdgeMidpoint() { 198 | const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.max.y, 0); 199 | v.applyMatrix4(this.localToWorld); 200 | return v; 201 | } 202 | 203 | GetBottomEdgeMidpoint() { 204 | const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.min.y, 0); 205 | v.applyMatrix4(this.localToWorld); 206 | return v; 207 | } 208 | }; 209 | 210 | class QuadTree { 211 | constructor(params) { 212 | const s = params.size; 213 | const b = new THREE.Box3( 214 | new THREE.Vector3(-s, -s, 0), 215 | new THREE.Vector3(s, s, 0)); 216 | this.root_ = new Node(); 217 | this.root_.side = params.side; 218 | this.root_.bounds = b; 219 | this.root_.children = []; 220 | this.root_.parent = null; 221 | this.root_.tree = this; 222 | this.root_.center = b.getCenter(new THREE.Vector3()); 223 | this.root_.sphereCenter = b.getCenter(new THREE.Vector3()); 224 | this.root_.localToWorld = params.localToWorld; 225 | this.root_.size = b.getSize(new THREE.Vector3()); 226 | this.root_.root = true; 227 | this.root_.neighbours = [null, null, null, null]; 228 | 229 | this._params = params; 230 | this.worldToLocal = params.worldToLocal; 231 | this.root_.sphereCenter = this.root_.center.clone(); 232 | this.root_.sphereCenter.applyMatrix4(this._params.localToWorld); 233 | this.root_.sphereCenter.normalize(); 234 | this.root_.sphereCenter.multiplyScalar(this._params.size); 235 | } 236 | 237 | GetChildren() { 238 | const children = []; 239 | this._GetChildren(this.root_, children); 240 | return children; 241 | } 242 | 243 | _GetChildren(node, target) { 244 | if (node.children.length == 0) { 245 | target.push(node); 246 | return; 247 | } 248 | 249 | for (let c of node.children) { 250 | this._GetChildren(c, target); 251 | } 252 | } 253 | 254 | BuildNeighbours_Child_(node) { 255 | const children = node.children; 256 | if (children.length == 0) { 257 | const hx = (node.bounds.max.x + node.bounds.min.x) * 0.5; 258 | const hy = (node.bounds.max.y + node.bounds.min.y) * 0.5; 259 | const nx = node.bounds.min.x; 260 | const ny = node.bounds.min.y; 261 | const px = node.bounds.max.x; 262 | const py = node.bounds.max.y; 263 | const b1 = new THREE.Vector3(nx, hy, 0); 264 | const b2 = new THREE.Vector3(hx, py, 0); 265 | const b3 = new THREE.Vector3(px, hy, 0); 266 | const b4 = new THREE.Vector3(hx, ny, 0); 267 | 268 | return; 269 | } 270 | 271 | if (node.center.x == 375000 && node.center.y == -125000 && node.side == 1 && node.size.x == 50000) { 272 | let a = 0; 273 | } 274 | if (node.root && node.side == 1) { 275 | let a = 0; 276 | } 277 | if (node.center.x == 200000 && node.center.y == -200000 && node.side == 1) { 278 | let a =0; 279 | } 280 | // Bottom left 281 | let leftNeighbour = node.GetNeighbour(LEFT); 282 | if (leftNeighbour.children.length > 0) { 283 | if (leftNeighbour.side != node.side) { 284 | leftNeighbour = leftNeighbour.GetClosestChild(children[0]); 285 | } else { 286 | leftNeighbour = leftNeighbour.GetChild(BOTTOM_RIGHT); 287 | } 288 | } 289 | 290 | let bottomNeighbour = node.GetNeighbour(BOTTOM); 291 | if (bottomNeighbour.children.length > 0) { 292 | if (bottomNeighbour.side != node.side) { 293 | bottomNeighbour = bottomNeighbour.GetClosestChild(children[0]); 294 | } else { 295 | bottomNeighbour = bottomNeighbour.GetChild(TOP_LEFT); 296 | } 297 | } 298 | children[0].neighbours = [leftNeighbour, children[TOP_LEFT], children[BOTTOM_RIGHT], bottomNeighbour]; 299 | 300 | // Bottom right 301 | let rightNeighbour = node.GetNeighbour(RIGHT); 302 | if (rightNeighbour.children.length > 0) { 303 | if (rightNeighbour.side != node.side) { 304 | rightNeighbour = rightNeighbour.GetClosestChild(children[1]); 305 | } else { 306 | rightNeighbour = rightNeighbour.GetChild(BOTTOM_LEFT); 307 | } 308 | } 309 | 310 | bottomNeighbour = node.GetNeighbour(BOTTOM); 311 | if (bottomNeighbour.children.length > 0) { 312 | if (bottomNeighbour.side != node.side) { 313 | bottomNeighbour = bottomNeighbour.GetClosestChild(children[1]); 314 | } else { 315 | bottomNeighbour = bottomNeighbour.GetChild(TOP_RIGHT); 316 | } 317 | } 318 | children[1].neighbours = [children[BOTTOM_LEFT], children[TOP_RIGHT], rightNeighbour, bottomNeighbour]; 319 | 320 | // Top left 321 | leftNeighbour = node.GetNeighbour(LEFT); 322 | if (leftNeighbour.children.length > 0) { 323 | if (leftNeighbour.side != node.side) { 324 | leftNeighbour = leftNeighbour.GetClosestChild(children[2]); 325 | } else { 326 | leftNeighbour = leftNeighbour.GetChild(TOP_RIGHT); 327 | } 328 | } 329 | 330 | let topNeighbour = node.GetNeighbour(TOP); 331 | if (topNeighbour.children.length > 0) { 332 | if (topNeighbour.side != node.side) { 333 | topNeighbour = topNeighbour.GetClosestChild(children[2]); 334 | } else { 335 | topNeighbour = topNeighbour.GetChild(BOTTOM_LEFT); 336 | } 337 | } 338 | children[2].neighbours = [leftNeighbour, topNeighbour, children[TOP_RIGHT], children[BOTTOM_LEFT]]; 339 | 340 | // Top right 341 | topNeighbour = node.GetNeighbour(TOP); 342 | if (topNeighbour.children.length > 0) { 343 | if (topNeighbour.side != node.side) { 344 | topNeighbour = topNeighbour.GetClosestChild(children[3]); 345 | } else { 346 | topNeighbour = topNeighbour.GetChild(BOTTOM_RIGHT); 347 | } 348 | } 349 | 350 | rightNeighbour = node.GetNeighbour(RIGHT); 351 | if (rightNeighbour.children.length > 0) { 352 | if (rightNeighbour.side != node.side) { 353 | rightNeighbour = rightNeighbour.GetClosestChild(children[3]); 354 | } else { 355 | rightNeighbour = rightNeighbour.GetChild(TOP_LEFT); 356 | } 357 | } 358 | children[3].neighbours = [children[TOP_LEFT], topNeighbour, rightNeighbour, children[BOTTOM_RIGHT]]; 359 | } 360 | 361 | Insert(pos) { 362 | this._Insert(this.root_, pos); 363 | } 364 | 365 | _Insert(child, pos) { 366 | // hack 367 | const distToChild = this._DistanceToChild(child, pos); 368 | 369 | if ((distToChild < child.size.x * 1.0 && child.size.x > this._params.min_node_size)) { 370 | child.children = this._CreateChildren(child); 371 | 372 | for (let c of child.children) { 373 | this._Insert(c, pos); 374 | } 375 | } 376 | } 377 | 378 | _DistanceToChild(child, pos) { 379 | return child.sphereCenter.distanceTo(pos); 380 | } 381 | 382 | _CreateChildren(child) { 383 | const midpoint = child.bounds.getCenter(new THREE.Vector3()); 384 | 385 | // Bottom left 386 | const b1 = new THREE.Box3(child.bounds.min, midpoint); 387 | 388 | // Bottom right 389 | const b2 = new THREE.Box3( 390 | new THREE.Vector3(midpoint.x, child.bounds.min.y, 0), 391 | new THREE.Vector3(child.bounds.max.x, midpoint.y, 0)); 392 | 393 | // Top left 394 | const b3 = new THREE.Box3( 395 | new THREE.Vector3(child.bounds.min.x, midpoint.y, 0), 396 | new THREE.Vector3(midpoint.x, child.bounds.max.y, 0)); 397 | 398 | // Top right 399 | const b4 = new THREE.Box3(midpoint, child.bounds.max); 400 | 401 | const children = [b1, b2, b3, b4].map( 402 | b => { 403 | return { 404 | side: child.side, 405 | bounds: b, 406 | children: [], 407 | parent: child, 408 | center: b.getCenter(new THREE.Vector3()), 409 | size: b.getSize(new THREE.Vector3()) 410 | }; 411 | }); 412 | 413 | const nodes = []; 414 | for (let c of children) { 415 | c.sphereCenter = c.center.clone(); 416 | c.sphereCenter.applyMatrix4(this._params.localToWorld); 417 | c.sphereCenter.normalize() 418 | c.sphereCenter.multiplyScalar(this._params.size); 419 | 420 | const n = new Node(); 421 | n.side = child.side; 422 | n.bounds = c.bounds; 423 | n.children = []; 424 | n.parent = child; 425 | n.tree = this; 426 | n.center = c.center; 427 | n.sphereCenter = c.sphereCenter; 428 | n.size = c.size; 429 | n.localToWorld = child.localToWorld; 430 | n.neighbours = [null, null, null, null]; 431 | nodes.push(n); 432 | } 433 | 434 | return nodes; 435 | } 436 | } 437 | 438 | return { 439 | QuadTree: QuadTree, 440 | CubeQuadTree: CubeQuadTree, 441 | } 442 | })(); 443 | -------------------------------------------------------------------------------- /src/scattering-shader.js: -------------------------------------------------------------------------------- 1 | 2 | export const scattering_shader = (function() { 3 | 4 | const _VS = ` 5 | 6 | #define saturate(a) clamp( a, 0.0, 1.0 ) 7 | 8 | out vec2 vUv; 9 | 10 | void main() { 11 | vUv = uv; 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | } 14 | `; 15 | 16 | 17 | const _PS = ` 18 | #include 19 | 20 | #define saturate(a) clamp( a, 0.0, 1.0 ) 21 | 22 | #define PI 3.141592 23 | #define PRIMARY_STEP_COUNT 16 24 | #define LIGHT_STEP_COUNT 8 25 | 26 | 27 | in vec2 vUv; 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 | uniform float logDepthBufFC; 42 | 43 | vec3 _ScreenToWorld(vec3 posS) { 44 | 45 | float depthValue = posS.z; 46 | float v_depth = pow(2.0, depthValue / (logDepthBufFC * 0.5)); 47 | float z_view = v_depth - 1.0; 48 | 49 | vec4 posCLIP = vec4(posS.xy * 2.0 - 1.0, 0.0, 1.0); 50 | vec4 posVS = inverseProjection * posCLIP; 51 | 52 | posVS = vec4(posVS.xyz / posVS.w, 1.0); 53 | posVS.xyz = normalize(posVS.xyz) * z_view; 54 | 55 | vec4 posWS = inverseView * posVS; 56 | 57 | return posWS.xyz; 58 | } 59 | 60 | vec3 _ScreenToWorld_Normal(vec3 pos) { 61 | vec3 posS = pos; 62 | 63 | vec4 posP = vec4(posS.xyz * 2.0 - 1.0, 1.0); 64 | vec4 posVS = inverseProjection * posP; 65 | 66 | posVS = vec4((posVS.xyz / posVS.w), 1.0); 67 | vec4 posWS = inverseView * posVS; 68 | 69 | return posWS.xyz; 70 | } 71 | 72 | // source: https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs 73 | vec3 RRTAndODTFit( vec3 v ) { 74 | vec3 a = v * ( v + 0.0245786 ) - 0.000090537; 75 | vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; 76 | return a / b; 77 | } 78 | // this implementation of ACES is modified to accommodate a brighter viewing environment. 79 | // the scale factor of 1/0.6 is subjective. see discussion in #19621. 80 | vec3 ACESFilmicToneMapping( vec3 color ) { 81 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT 82 | const mat3 ACESInputMat = mat3( 83 | vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source 84 | vec3( 0.35458, 0.90834, 0.13383 ), 85 | vec3( 0.04823, 0.01566, 0.83777 ) 86 | ); 87 | // ODT_SAT => XYZ => D60_2_D65 => sRGB 88 | const mat3 ACESOutputMat = mat3( 89 | vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source 90 | vec3( -0.53108, 1.10813, -0.07276 ), 91 | vec3( -0.07367, -0.00605, 1.07602 ) 92 | ); 93 | color *= 1.0 / 0.6; 94 | color = ACESInputMat * color; 95 | // Apply RRT and ODT 96 | color = RRTAndODTFit( color ); 97 | color = ACESOutputMat * color; 98 | // Clamp to [0, 1] 99 | return saturate( color ); 100 | } 101 | 102 | float _SoftLight(float a, float b) { 103 | return (b < 0.5 ? 104 | (2.0 * a * b + a * a * (1.0 - 2.0 * b)) : 105 | (2.0 * a * (1.0 - b) + sqrt(a) * (2.0 * b - 1.0)) 106 | ); 107 | } 108 | 109 | vec3 _SoftLight(vec3 a, vec3 b) { 110 | return vec3( 111 | _SoftLight(a.x, b.x), 112 | _SoftLight(a.y, b.y), 113 | _SoftLight(a.z, b.z) 114 | ); 115 | } 116 | 117 | bool _RayIntersectsSphere( 118 | vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius, out float t0, out float t1) { 119 | vec3 oc = rayStart - sphereCenter; 120 | float a = dot(rayDir, rayDir); 121 | float b = 2.0 * dot(oc, rayDir); 122 | float c = dot(oc, oc) - sphereRadius * sphereRadius; 123 | float d = b * b - 4.0 * a * c; 124 | 125 | // Also skip single point of contact 126 | if (d <= 0.0) { 127 | return false; 128 | } 129 | 130 | float r0 = (-b - sqrt(d)) / (2.0 * a); 131 | float r1 = (-b + sqrt(d)) / (2.0 * a); 132 | 133 | t0 = min(r0, r1); 134 | t1 = max(r0, r1); 135 | 136 | return (t1 >= 0.0); 137 | } 138 | 139 | 140 | vec3 _SampleLightRay( 141 | vec3 origin, vec3 sunDir, float planetScale, float planetRadius, float totalRadius, 142 | float rayleighScale, float mieScale, float absorptionHeightMax, float absorptionFalloff) { 143 | 144 | float t0, t1; 145 | _RayIntersectsSphere(origin, sunDir, planetPosition, totalRadius, t0, t1); 146 | 147 | float actualLightStepSize = (t1 - t0) / float(LIGHT_STEP_COUNT); 148 | float virtualLightStepSize = actualLightStepSize * planetScale; 149 | float lightStepPosition = 0.0; 150 | 151 | vec3 opticalDepthLight = vec3(0.0); 152 | 153 | for (int j = 0; j < LIGHT_STEP_COUNT; j++) { 154 | vec3 currentLightSamplePosition = origin + sunDir * (lightStepPosition + actualLightStepSize * 0.5); 155 | 156 | // Calculate the optical depths and accumulate 157 | float currentHeight = length(currentLightSamplePosition) - planetRadius; 158 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualLightStepSize; 159 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualLightStepSize; 160 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff)); 161 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualLightStepSize; 162 | 163 | opticalDepthLight += vec3( 164 | currentOpticalDepthRayleigh, 165 | currentOpticalDepthMie, 166 | currentOpticalDepthOzone); 167 | 168 | lightStepPosition += actualLightStepSize; 169 | } 170 | 171 | return opticalDepthLight; 172 | } 173 | 174 | void _ComputeScattering( 175 | vec3 worldSpacePos, vec3 rayDirection, vec3 rayOrigin, vec3 sunDir, 176 | out vec3 scatteringColour, out vec3 scatteringOpacity) { 177 | 178 | vec3 betaRayleigh = vec3(5.5e-6, 13.0e-6, 22.4e-6); 179 | float betaMie = 21e-6; 180 | vec3 betaAbsorption = vec3(2.04e-5, 4.97e-5, 1.95e-6); 181 | float g = 0.76; 182 | float sunIntensity = 40.0; 183 | 184 | float planetRadius = planetRadius; 185 | float atmosphereRadius = atmosphereRadius - planetRadius; 186 | float totalRadius = planetRadius + atmosphereRadius; 187 | 188 | float referencePlanetRadius = 6371000.0; 189 | float referenceAtmosphereRadius = 100000.0; 190 | float referenceTotalRadius = referencePlanetRadius + referenceAtmosphereRadius; 191 | float referenceRatio = referencePlanetRadius / referenceAtmosphereRadius; 192 | 193 | float scaleRatio = planetRadius / atmosphereRadius; 194 | float planetScale = referencePlanetRadius / planetRadius; 195 | float atmosphereScale = scaleRatio / referenceRatio; 196 | float maxDist = distance(worldSpacePos, rayOrigin); 197 | 198 | float rayleighScale = 8500.0 / (planetScale * atmosphereScale); 199 | float mieScale = 1200.0 / (planetScale * atmosphereScale); 200 | float absorptionHeightMax = 32000.0 * (planetScale * atmosphereScale); 201 | float absorptionFalloff = 3000.0 / (planetScale * atmosphereScale);; 202 | 203 | float mu = dot(rayDirection, sunDir); 204 | float mumu = mu * mu; 205 | float gg = g * g; 206 | float phaseRayleigh = 3.0 / (16.0 * PI) * (1.0 + mumu); 207 | 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)); 208 | 209 | // Early out if ray doesn't intersect atmosphere. 210 | float t0, t1; 211 | if (!_RayIntersectsSphere(rayOrigin, rayDirection, planetPosition, totalRadius, t0, t1)) { 212 | scatteringOpacity = vec3(1.0); 213 | return; 214 | } 215 | 216 | // Clip the ray between the camera and potentially the planet surface. 217 | t0 = max(0.0, t0); 218 | t1 = min(maxDist, t1); 219 | 220 | float actualPrimaryStepSize = (t1 - t0) / float(PRIMARY_STEP_COUNT); 221 | float virtualPrimaryStepSize = actualPrimaryStepSize * planetScale; 222 | float primaryStepPosition = 0.0; 223 | 224 | vec3 accumulatedRayleigh = vec3(0.0); 225 | vec3 accumulatedMie = vec3(0.0); 226 | vec3 opticalDepth = vec3(0.0); 227 | 228 | // Take N steps along primary ray 229 | for (int i = 0; i < PRIMARY_STEP_COUNT; i++) { 230 | vec3 currentPrimarySamplePosition = rayOrigin + rayDirection * ( 231 | primaryStepPosition + actualPrimaryStepSize * 0.5); 232 | 233 | float currentHeight = max(0.0, length(currentPrimarySamplePosition) - planetRadius); 234 | 235 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualPrimaryStepSize; 236 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualPrimaryStepSize; 237 | 238 | // Taken from https://www.shadertoy.com/view/wlBXWK 239 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff)); 240 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualPrimaryStepSize; 241 | 242 | opticalDepth += vec3(currentOpticalDepthRayleigh, currentOpticalDepthMie, currentOpticalDepthOzone); 243 | 244 | // Sample light ray and accumulate optical depth. 245 | vec3 opticalDepthLight = _SampleLightRay( 246 | currentPrimarySamplePosition, sunDir, 247 | planetScale, planetRadius, totalRadius, 248 | rayleighScale, mieScale, absorptionHeightMax, absorptionFalloff); 249 | 250 | vec3 r = ( 251 | betaRayleigh * (opticalDepth.x + opticalDepthLight.x) + 252 | betaMie * (opticalDepth.y + opticalDepthLight.y) + 253 | betaAbsorption * (opticalDepth.z + opticalDepthLight.z)); 254 | vec3 attn = exp(-r); 255 | 256 | accumulatedRayleigh += currentOpticalDepthRayleigh * attn; 257 | accumulatedMie += currentOpticalDepthMie * attn; 258 | 259 | primaryStepPosition += actualPrimaryStepSize; 260 | } 261 | 262 | scatteringColour = sunIntensity * (phaseRayleigh * betaRayleigh * accumulatedRayleigh + phaseMie * betaMie * accumulatedMie); 263 | scatteringOpacity = exp( 264 | -(betaMie * opticalDepth.y + betaRayleigh * opticalDepth.x + betaAbsorption * opticalDepth.z)); 265 | } 266 | 267 | vec3 _ApplyGroundFog( 268 | in vec3 rgb, 269 | float distToPoint, 270 | float height, 271 | in vec3 worldSpacePos, 272 | in vec3 rayOrigin, 273 | in vec3 rayDir, 274 | in vec3 sunDir) 275 | { 276 | vec3 up = normalize(rayOrigin); 277 | 278 | float skyAmt = dot(up, rayDir) * 0.25 + 0.75; 279 | skyAmt = saturate(skyAmt); 280 | skyAmt *= skyAmt; 281 | 282 | vec3 DARK_BLUE = vec3(0.1, 0.2, 0.3); 283 | vec3 LIGHT_BLUE = vec3(0.5, 0.6, 0.7); 284 | vec3 DARK_ORANGE = vec3(0.7, 0.4, 0.05); 285 | vec3 BLUE = vec3(0.5, 0.6, 0.7); 286 | vec3 YELLOW = vec3(1.0, 0.9, 0.7); 287 | 288 | vec3 fogCol = mix(DARK_BLUE, LIGHT_BLUE, skyAmt); 289 | float sunAmt = max(dot(rayDir, sunDir), 0.0); 290 | fogCol = mix(fogCol, YELLOW, pow(sunAmt, 16.0)); 291 | 292 | float be = 0.0025; 293 | float fogAmt = (1.0 - exp(-distToPoint * be)); 294 | 295 | // Sun 296 | sunAmt = 0.5 * saturate(pow(sunAmt, 256.0)); 297 | 298 | return mix(rgb, fogCol, fogAmt) + sunAmt * YELLOW; 299 | } 300 | 301 | vec3 _ApplySpaceFog( 302 | in vec3 rgb, 303 | in float distToPoint, 304 | in float height, 305 | in vec3 worldSpacePos, 306 | in vec3 rayOrigin, 307 | in vec3 rayDir, 308 | in vec3 sunDir) 309 | { 310 | float atmosphereThickness = (atmosphereRadius - planetRadius); 311 | 312 | float t0 = -1.0; 313 | float t1 = -1.0; 314 | 315 | // This is a hack since the world mesh has seams that we haven't fixed yet. 316 | if (_RayIntersectsSphere( 317 | rayOrigin, rayDir, planetPosition, planetRadius, t0, t1)) { 318 | if (distToPoint > t0) { 319 | distToPoint = t0; 320 | worldSpacePos = rayOrigin + t0 * rayDir; 321 | } 322 | } 323 | 324 | if (!_RayIntersectsSphere( 325 | rayOrigin, rayDir, planetPosition, planetRadius + atmosphereThickness * 5.0, t0, t1)) { 326 | return rgb * 0.5; 327 | } 328 | 329 | // Figure out a better way to do this 330 | float silhouette = saturate((distToPoint - 10000.0) / 10000.0); 331 | 332 | // Glow around planet 333 | float scaledDistanceToSurface = 0.0; 334 | 335 | // Calculate the closest point between ray direction and planet. Use a point in front of the 336 | // camera to force differences as you get closer to planet. 337 | vec3 fakeOrigin = rayOrigin + rayDir * atmosphereThickness; 338 | float t = max(0.0, dot(rayDir, planetPosition - fakeOrigin) / dot(rayDir, rayDir)); 339 | vec3 pb = fakeOrigin + t * rayDir; 340 | 341 | scaledDistanceToSurface = saturate((distance(pb, planetPosition) - planetRadius) / atmosphereThickness); 342 | scaledDistanceToSurface = smoothstep(0.0, 1.0, 1.0 - scaledDistanceToSurface); 343 | //scaledDistanceToSurface = smoothstep(0.0, 1.0, scaledDistanceToSurface); 344 | 345 | float scatteringFactor = scaledDistanceToSurface * silhouette; 346 | 347 | // Fog on surface 348 | t0 = max(0.0, t0); 349 | t1 = min(distToPoint, t1); 350 | 351 | vec3 intersectionPoint = rayOrigin + t1 * rayDir; 352 | vec3 normalAtIntersection = normalize(intersectionPoint); 353 | 354 | float distFactor = exp(-distToPoint * 0.0005 / (atmosphereThickness)); 355 | float fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection)); 356 | fresnel = smoothstep(0.0, 1.0, fresnel); 357 | 358 | float extinctionFactor = saturate(fresnel * distFactor) * (1.0 - silhouette); 359 | 360 | // Front/Back Lighting 361 | vec3 BLUE = vec3(0.5, 0.6, 0.75); 362 | vec3 YELLOW = vec3(1.0, 0.9, 0.7); 363 | vec3 RED = vec3(0.035, 0.0, 0.0); 364 | 365 | float NdotL = dot(normalAtIntersection, sunDir); 366 | float wrap = 0.5; 367 | float NdotL_wrap = max(0.0, (NdotL + wrap) / (1.0 + wrap)); 368 | float RdotS = max(0.0, dot(rayDir, sunDir)); 369 | float sunAmount = RdotS; 370 | 371 | vec3 backLightingColour = YELLOW * 0.1; 372 | vec3 frontLightingColour = mix(BLUE, YELLOW, pow(sunAmount, 32.0)); 373 | 374 | vec3 fogColour = mix(backLightingColour, frontLightingColour, NdotL_wrap); 375 | 376 | extinctionFactor *= NdotL_wrap; 377 | 378 | // Sun 379 | float specular = pow((RdotS + 0.5) / (1.0 + 0.5), 64.0); 380 | 381 | fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection)); 382 | fresnel *= fresnel; 383 | 384 | float sunFactor = (length(pb) - planetRadius) / (atmosphereThickness * 5.0); 385 | sunFactor = (1.0 - saturate(sunFactor)); 386 | sunFactor *= sunFactor; 387 | sunFactor *= sunFactor; 388 | sunFactor *= specular * fresnel; 389 | 390 | vec3 baseColour = mix(rgb, fogColour, extinctionFactor); 391 | vec3 litColour = baseColour + _SoftLight(fogColour * scatteringFactor + YELLOW * sunFactor, baseColour); 392 | vec3 blendedColour = mix(baseColour, fogColour, scatteringFactor); 393 | blendedColour += blendedColour + _SoftLight(YELLOW * sunFactor, blendedColour); 394 | return mix(litColour, blendedColour, scaledDistanceToSurface * 0.25); 395 | } 396 | 397 | vec3 _ApplyFog( 398 | in vec3 rgb, 399 | in float distToPoint, 400 | in float height, 401 | in vec3 worldSpacePos, 402 | in vec3 rayOrigin, 403 | in vec3 rayDir, 404 | in vec3 sunDir) 405 | { 406 | float distToPlanet = max(0.0, length(rayOrigin) - planetRadius); 407 | float atmosphereThickness = (atmosphereRadius - planetRadius); 408 | 409 | vec3 groundCol = _ApplyGroundFog( 410 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir); 411 | vec3 spaceCol = _ApplySpaceFog( 412 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir); 413 | 414 | float blendFactor = saturate(distToPlanet / (atmosphereThickness * 0.5)); 415 | 416 | blendFactor = smoothstep(0.0, 1.0, blendFactor); 417 | blendFactor = smoothstep(0.0, 1.0, blendFactor); 418 | 419 | return mix(groundCol, spaceCol, blendFactor); 420 | } 421 | 422 | void main() { 423 | float z = texture2D(tDepth, vUv).x; 424 | vec3 posWS = _ScreenToWorld(vec3(vUv, z)); 425 | float dist = length(posWS - cameraPosition); 426 | float height = max(0.0, length(cameraPosition) - planetRadius); 427 | vec3 cameraDirection = normalize(posWS - cameraPosition); 428 | 429 | 430 | vec3 diffuse = texture2D(tDiffuse, vUv).xyz; 431 | vec3 lightDir = normalize(vec3(1, 1, -1)); 432 | 433 | // diffuse = _ApplyFog(diffuse, dist, height, posWS, cameraPosition, cameraDirection, lightDir); 434 | 435 | vec3 scatteringColour = vec3(0.0); 436 | vec3 scatteringOpacity = vec3(1.0, 1.0, 1.0); 437 | _ComputeScattering( 438 | posWS, cameraDirection, cameraPosition, 439 | lightDir, scatteringColour, scatteringOpacity 440 | ); 441 | 442 | // diffuse = diffuse * scatteringOpacity + scatteringColour; 443 | // diffuse = ACESFilmicToneMapping(diffuse); 444 | diffuse = pow(diffuse, vec3(1.0 / 2.0)); 445 | 446 | gl_FragColor.rgb = diffuse; 447 | gl_FragColor.a = 1.0; 448 | } 449 | `; 450 | 451 | 452 | const _Shader = { 453 | uniforms: { 454 | "tDiffuse": { value: null }, 455 | "tDepth": { value: null }, 456 | "cameraNear": { value: 0.0 }, 457 | "cameraFar": { value: 0.0 }, 458 | }, 459 | vertexShader: _VS, 460 | fragmentShader: _PS, 461 | }; 462 | 463 | return { 464 | Shader: _Shader, 465 | VS: _VS, 466 | PS: _PS, 467 | }; 468 | })(); -------------------------------------------------------------------------------- /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.125/build/three.module.js'; 2 | 3 | import {Sky} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/objects/Sky.js'; 4 | import {Water} from 'https://cdn.jsdelivr.net/npm/three@0.125/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.125/build/three.module.js'; 2 | 3 | import {noise} from './noise.js'; 4 | import {texture_splatter} from './texture-splatter.js' ; 5 | import {math} from './math.js'; 6 | 7 | 8 | const _D = new THREE.Vector3(); 9 | const _D1 = new THREE.Vector3(); 10 | const _D2 = new THREE.Vector3(); 11 | const _P = new THREE.Vector3(); 12 | const _P1 = new THREE.Vector3(); 13 | const _P2 = new THREE.Vector3(); 14 | const _P3 = new THREE.Vector3(); 15 | const _H = new THREE.Vector3(); 16 | const _W = new THREE.Vector3(); 17 | const _S = new THREE.Vector3(); 18 | const _C = new THREE.Vector3(); 19 | 20 | const _N = new THREE.Vector3(); 21 | const _N1 = new THREE.Vector3(); 22 | const _N2 = new THREE.Vector3(); 23 | const _N3 = new THREE.Vector3(); 24 | 25 | 26 | class _TerrainBuilderThreadedWorker { 27 | constructor() { 28 | } 29 | 30 | Init(params) { 31 | this.cachedParams_ = {...params}; 32 | this.params_ = params; 33 | this.params_.offset = new THREE.Vector3(...params.offset); 34 | this.params_.origin = new THREE.Vector3(...params.origin); 35 | this.params_.noise = new noise.Noise(params.noiseParams); 36 | this.params_.heightGenerators = [ 37 | new texture_splatter.HeightGenerator( 38 | this.params_.noise, params.offset, 39 | params.heightGeneratorsParams.min, params.heightGeneratorsParams.max) 40 | ]; 41 | 42 | this.params_.biomeGenerator = new noise.Noise(params.biomesParams); 43 | this.params_.colourNoise = new noise.Noise(params.colourNoiseParams); 44 | this.params_.colourGenerator = new texture_splatter.TextureSplatter( 45 | { 46 | biomeGenerator: this.params_.biomeGenerator, 47 | colourNoise: this.params_.colourNoise 48 | }); 49 | } 50 | 51 | _GenerateHeight(v) { 52 | return this.params_.heightGenerators[0].Get(v.x, v.y, v.z)[0]; 53 | } 54 | 55 | GenerateNormals_(positions, indices) { 56 | const normals = new Array(positions.length).fill(0.0); 57 | for (let i = 0, n = indices.length; i < n; i+= 3) { 58 | const i1 = indices[i] * 3; 59 | const i2 = indices[i+1] * 3; 60 | const i3 = indices[i+2] * 3; 61 | 62 | _N1.fromArray(positions, i1); 63 | _N2.fromArray(positions, i2); 64 | _N3.fromArray(positions, i3); 65 | 66 | _D1.subVectors(_N3, _N2); 67 | _D2.subVectors(_N1, _N2); 68 | _D1.cross(_D2); 69 | 70 | normals[i1] += _D1.x; 71 | normals[i2] += _D1.x; 72 | normals[i3] += _D1.x; 73 | 74 | normals[i1+1] += _D1.y; 75 | normals[i2+1] += _D1.y; 76 | normals[i3+1] += _D1.y; 77 | 78 | normals[i1+2] += _D1.z; 79 | normals[i2+2] += _D1.z; 80 | normals[i3+2] += _D1.z; 81 | } 82 | return normals; 83 | } 84 | 85 | GenerateIndices_() { 86 | const resolution = this.params_.resolution + 2; 87 | const indices = []; 88 | for (let i = 0; i < resolution; i++) { 89 | for (let j = 0; j < resolution; j++) { 90 | indices.push( 91 | i * (resolution + 1) + j, 92 | (i + 1) * (resolution + 1) + j + 1, 93 | i * (resolution + 1) + j + 1); 94 | indices.push( 95 | (i + 1) * (resolution + 1) + j, 96 | (i + 1) * (resolution + 1) + j + 1, 97 | i * (resolution + 1) + j); 98 | } 99 | } 100 | return indices; 101 | } 102 | 103 | _ComputeNormal_CentralDifference(xp, yp, stepSize) { 104 | const localToWorld = this.params_.worldMatrix; 105 | const radius = this.params_.radius; 106 | const offset = this.params_.offset; 107 | const width = this.params_.width; 108 | const half = width / 2; 109 | const resolution = this.params_.resolution + 2; 110 | const effectiveResolution = resolution - 2; 111 | 112 | // Compute position 113 | const _ComputeWSPosition = (xpos, ypos) => { 114 | const xp = width * xpos; 115 | const yp = width * ypos; 116 | _P.set(xp - half, yp - half, radius); 117 | _P.add(offset); 118 | _P.normalize(); 119 | _D.copy(_P); 120 | _D.transformDirection(localToWorld); 121 | 122 | _P.multiplyScalar(radius); 123 | _P.z -= radius; 124 | _P.applyMatrix4(localToWorld); 125 | 126 | // Purturb height along z-vector 127 | const height = this._GenerateHeight(_P); 128 | _H.copy(_D); 129 | _H.multiplyScalar(height); 130 | _P.add(_H); 131 | 132 | return _P; 133 | }; 134 | 135 | const _ComputeWSPositionFromWS = (pos) => { 136 | _P.copy(pos); 137 | _P.normalize(); 138 | _D.copy(_P); 139 | _P.multiplyScalar(radius); 140 | 141 | // Purturb height along z-vector 142 | const height = this._GenerateHeight(_P); 143 | _H.copy(_D); 144 | _H.multiplyScalar(height); 145 | _P.add(_H); 146 | 147 | return _P; 148 | }; 149 | 150 | const _SphericalToCartesian = (theta, phi) => { 151 | const x = (Math.sin(theta) * Math.cos(phi)); 152 | const y = (Math.sin(theta) * Math.sin(phi)); 153 | const z = (Math.cos(theta)); 154 | _P.set(x, y, z); 155 | _P.multiplyScalar(radius); 156 | const height = this._GenerateHeight(_P); 157 | _P.set(x, y, z); 158 | _P.multiplyScalar(height + radius); 159 | return _P; 160 | }; 161 | 162 | // 163 | _P3.copy(_ComputeWSPosition(xp, yp)); 164 | _D.copy(_P3); 165 | _D.normalize(); 166 | 167 | const phi = Math.atan2(_D.y, _D.x); 168 | const theta = Math.atan2((_D.x * _D.x + _D.y * _D.y) ** 0.5, _D.z); 169 | 170 | _P1.copy(_ComputeWSPosition(xp, yp)); 171 | _P2.copy(_SphericalToCartesian(theta, phi)); 172 | 173 | // Fixme - Fixed size right now, calculate an appropriate delta 174 | const delta = 0.001; 175 | 176 | _P1.copy(_SphericalToCartesian(theta - delta, phi)); 177 | _P2.copy(_SphericalToCartesian(theta + delta, phi)); 178 | _D1.subVectors(_P1, _P2); 179 | _D1.normalize(); 180 | 181 | _P1.copy(_SphericalToCartesian(theta, phi - delta)); 182 | _P2.copy(_SphericalToCartesian(theta, phi + delta)); 183 | _D2.subVectors(_P1, _P2); 184 | _D2.normalize(); 185 | 186 | 187 | _P1.copy(_D1); 188 | _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution); 189 | _P2.copy(_P1); 190 | _P2.multiplyScalar(-1) 191 | _P1.add(_P3); 192 | _P2.add(_P3); 193 | _P1.copy(_ComputeWSPositionFromWS(_P1)); 194 | _P2.copy(_ComputeWSPositionFromWS(_P2)); 195 | _D1.subVectors(_P1, _P2); 196 | _D1.normalize(); 197 | 198 | _P1.copy(_D2); 199 | _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution); 200 | _P2.copy(_P1); 201 | _P2.multiplyScalar(-1) 202 | _P1.add(_P3); 203 | _P2.add(_P3); 204 | _P1.copy(_ComputeWSPositionFromWS(_P1)); 205 | _P2.copy(_ComputeWSPositionFromWS(_P2)); 206 | _D2.subVectors(_P1, _P2); 207 | _D2.normalize(); 208 | 209 | _D1.cross(_D2); 210 | 211 | return _D1; 212 | } 213 | 214 | RebuildEdgeNormals_(normals) { 215 | const resolution = this.params_.resolution + 2; 216 | const effectiveResolution = resolution - 2; 217 | 218 | let x = 1; 219 | for (let z = 1; z <= resolution-1; z+=1) { 220 | const i = x * (resolution + 1) + z; 221 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1)) 222 | normals[i * 3 + 0] = _N.x; 223 | normals[i * 3 + 1] = _N.y; 224 | normals[i * 3 + 2] = _N.z; 225 | } 226 | 227 | let z = resolution - 1; 228 | for (let x = 1; x <= resolution-1; x+=1) { 229 | const i = (x) * (resolution + 1) + z; 230 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1)) 231 | normals[i * 3 + 0] = _N.x; 232 | normals[i * 3 + 1] = _N.y; 233 | normals[i * 3 + 2] = _N.z; 234 | } 235 | 236 | x = resolution - 1; 237 | for (let z = 1; z <= resolution-1; z+=1) { 238 | const i = x * (resolution + 1) + z; 239 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1)) 240 | normals[i * 3 + 0] = _N.x; 241 | normals[i * 3 + 1] = _N.y; 242 | normals[i * 3 + 2] = _N.z; 243 | } 244 | 245 | z = 1; 246 | for (let x = 1; x <= resolution-1; x+=1) { 247 | const i = (x) * (resolution + 1) + z; 248 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1)) 249 | normals[i * 3 + 0] = _N.x; 250 | normals[i * 3 + 1] = _N.y; 251 | normals[i * 3 + 2] = _N.z; 252 | } 253 | } 254 | 255 | FixEdgesToMatchNeighbours_(positions, normals, colours) { 256 | const resolution = this.params_.resolution + 2; 257 | const effectiveResolution = resolution - 2; 258 | 259 | if (this.params_.neighbours[0] > 1) { 260 | const x = 1; 261 | const stride = this.params_.neighbours[0]; 262 | for (let z = 1; z <= resolution-1; z+=1) { 263 | const i = x * (resolution + 1) + z; 264 | // colours[i * 3 + 0] = 0; 265 | // colours[i * 3 + 1] = 0; 266 | // colours[i * 3 + 2] = 1; 267 | 268 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride)) 269 | normals[i * 3 + 0] = _N.x; 270 | normals[i * 3 + 1] = _N.y; 271 | normals[i * 3 + 2] = _N.z; 272 | } 273 | for (let z = 1; z <= resolution-1-stride; z+=stride) { 274 | const i1 = x * (resolution + 1) + z; 275 | const i2 = x * (resolution + 1) + (z + stride); 276 | 277 | for (let s = 1; s < stride; ++s) { 278 | const i = x * (resolution + 1) + z + s; 279 | const p = s / stride; 280 | for (let j = 0; j < 3; ++j) { 281 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]); 282 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]); 283 | } 284 | // colours[i * 3 + 0] = 0; 285 | // colours[i * 3 + 1] = 1; 286 | // colours[i * 3 + 2] = 0; 287 | } 288 | } 289 | } 290 | 291 | if (this.params_.neighbours[1] > 1) { 292 | const z = resolution - 1; 293 | const stride = this.params_.neighbours[1]; 294 | for (let x = 1; x <= resolution-1; x+=1) { 295 | const i = (x) * (resolution + 1) + z; 296 | // colours[i * 3 + 0] = 0; 297 | // colours[i * 3 + 1] = 0; 298 | // colours[i * 3 + 2] = 1; 299 | 300 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride)) 301 | normals[i * 3 + 0] = _N.x; 302 | normals[i * 3 + 1] = _N.y; 303 | normals[i * 3 + 2] = _N.z; 304 | } 305 | for (let x = 1; x <= resolution-1-stride; x+=stride) { 306 | const i1 = (x) * (resolution + 1) + z; 307 | const i2 = (x + stride) * (resolution + 1) + z; 308 | 309 | for (let s = 1; s < stride; ++s) { 310 | const i = (x + s) * (resolution + 1) + z; 311 | const p = s / stride; 312 | for (let j = 0; j < 3; ++j) { 313 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]); 314 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]); 315 | } 316 | // colours[i * 3 + 0] = 1; 317 | // colours[i * 3 + 1] = 1; 318 | // colours[i * 3 + 2] = 0; 319 | } 320 | } 321 | } 322 | 323 | if (this.params_.neighbours[2] > 1) { 324 | const x = resolution - 1; 325 | const stride = this.params_.neighbours[2]; 326 | for (let z = 1; z <= resolution-1; z+=1) { 327 | const i = x * (resolution + 1) + z; 328 | // colours[i * 3 + 0] = 0; 329 | // colours[i * 3 + 1] = 0; 330 | // colours[i * 3 + 2] = 1; 331 | 332 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride)) 333 | normals[i * 3 + 0] = _N.x; 334 | normals[i * 3 + 1] = _N.y; 335 | normals[i * 3 + 2] = _N.z; 336 | } 337 | for (let z = 1; z <= resolution-1-stride; z+=stride) { 338 | const i1 = x * (resolution + 1) + z; 339 | const i2 = x * (resolution + 1) + (z + stride); 340 | 341 | for (let s = 1; s < stride; ++s) { 342 | const i = x * (resolution + 1) + z + s; 343 | const p = s / stride; 344 | for (let j = 0; j < 3; ++j) { 345 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]); 346 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]); 347 | } 348 | // colours[i * 3 + 0] = 0; 349 | // colours[i * 3 + 1] = 1; 350 | // colours[i * 3 + 2] = 1; 351 | } 352 | } 353 | } 354 | 355 | if (this.params_.neighbours[3] > 1) { 356 | const z = 1; 357 | const stride = this.params_.neighbours[3]; 358 | for (let x = 1; x <= resolution-1; x+=1) { 359 | const i = (x) * (resolution + 1) + z; 360 | // colours[i * 3 + 0] = 0; 361 | // colours[i * 3 + 1] = 0; 362 | // colours[i * 3 + 2] = 1; 363 | 364 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride)) 365 | normals[i * 3 + 0] = _N.x; 366 | normals[i * 3 + 1] = _N.y; 367 | normals[i * 3 + 2] = _N.z; 368 | } 369 | for (let x = 1; x <= resolution-1-stride; x+=stride) { 370 | const i1 = (x) * (resolution + 1) + z; 371 | const i2 = (x + stride) * (resolution + 1) + z; 372 | 373 | for (let s = 1; s < stride; ++s) { 374 | const i = (x + s) * (resolution + 1) + z; 375 | const p = s / stride; 376 | for (let j = 0; j < 3; ++j) { 377 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]); 378 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]); 379 | } 380 | // colours[i * 3 + 0] = 1; 381 | // colours[i * 3 + 1] = 0; 382 | // colours[i * 3 + 2] = 0; 383 | } 384 | } 385 | } 386 | } 387 | 388 | FixEdgeSkirt_(positions, up, normals) { 389 | const resolution = this.params_.resolution + 2; 390 | 391 | const _ApplyFix = (x, y, xp, yp) => { 392 | const skirtIndex = x * (resolution + 1) + y; 393 | const proxyIndex = xp * (resolution + 1) + yp; 394 | 395 | _P.fromArray(positions, proxyIndex * 3); 396 | _D.fromArray(up, proxyIndex * 3); 397 | _D.multiplyScalar(0); 398 | _P.add(_D); 399 | positions[skirtIndex * 3 + 0] = _P.x; 400 | positions[skirtIndex * 3 + 1] = _P.y; 401 | positions[skirtIndex * 3 + 2] = _P.z; 402 | 403 | // Normal will be fucked, copy it from proxy point 404 | normals[skirtIndex * 3 + 0] = normals[proxyIndex * 3 + 0]; 405 | normals[skirtIndex * 3 + 1] = normals[proxyIndex * 3 + 1]; 406 | normals[skirtIndex * 3 + 2] = normals[proxyIndex * 3 + 2]; 407 | }; 408 | 409 | for (let y = 0; y <= resolution; ++y) { 410 | _ApplyFix(0, y, 1, y); 411 | } 412 | for (let y = 0; y <= resolution; ++y) { 413 | _ApplyFix(resolution, y, resolution - 1, y); 414 | } 415 | for (let x = 0; x <= resolution; ++x) { 416 | _ApplyFix(x, 0, x, 1); 417 | } 418 | for (let x = 0; x <= resolution; ++x) { 419 | _ApplyFix(x, resolution, x, resolution - 1); 420 | } 421 | } 422 | 423 | NormalizeNormals_(normals) { 424 | for (let i = 0, n = normals.length; i < n; i+=3) { 425 | _N.fromArray(normals, i); 426 | _N.normalize(); 427 | normals[i] = _N.x; 428 | normals[i+1] = _N.y; 429 | normals[i+2] = _N.z; 430 | } 431 | } 432 | 433 | RebuildEdgePositions_(positions) { 434 | const localToWorld = this.params_.worldMatrix; 435 | const resolution = this.params_.resolution + 2; 436 | const radius = this.params_.radius; 437 | const offset = this.params_.offset; 438 | const origin = this.params_.origin; 439 | const width = this.params_.width; 440 | const half = width / 2; 441 | const effectiveResolution = resolution - 2; 442 | 443 | const _ComputeOriginOffsetPosition = (xpos, ypos) => { 444 | const xp = width * xpos; 445 | const yp = width * ypos; 446 | _P.set(xp - half, yp - half, radius); 447 | _P.add(offset); 448 | _P.normalize(); 449 | _D.copy(_P); 450 | _D.transformDirection(localToWorld); 451 | 452 | _P.multiplyScalar(radius); 453 | _P.z -= radius; 454 | _P.applyMatrix4(localToWorld); 455 | 456 | // Keep the absolute world space position to sample noise 457 | _W.copy(_P); 458 | 459 | // Move the position relative to the origin 460 | _P.sub(origin); 461 | 462 | // Purturb height along z-vector 463 | const height = this._GenerateHeight(_W); 464 | _H.copy(_D); 465 | _H.multiplyScalar(height); 466 | _P.add(_H); 467 | 468 | return _P; 469 | } 470 | 471 | let x = 1; 472 | for (let z = 1; z <= resolution-1; z++) { 473 | const i = x * (resolution + 1) + z; 474 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution); 475 | positions[i * 3 + 0] = p.x; 476 | positions[i * 3 + 1] = p.y; 477 | positions[i * 3 + 2] = p.z; 478 | } 479 | 480 | let z = resolution - 1; 481 | for (let x = 1; x <= resolution-1; x++) { 482 | const i = (x) * (resolution + 1) + z; 483 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution); 484 | positions[i * 3 + 0] = p.x; 485 | positions[i * 3 + 1] = p.y; 486 | positions[i * 3 + 2] = p.z; 487 | } 488 | 489 | x = resolution - 1; 490 | for (let z = 1; z <= resolution-1; z++) { 491 | const i = x * (resolution + 1) + z; 492 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution); 493 | positions[i * 3 + 0] = p.x; 494 | positions[i * 3 + 1] = p.y; 495 | positions[i * 3 + 2] = p.z; 496 | } 497 | 498 | z = 1; 499 | for (let x = 1; x <= resolution-1; x++) { 500 | const i = (x) * (resolution + 1) + z; 501 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution); 502 | positions[i * 3 + 0] = p.x; 503 | positions[i * 3 + 1] = p.y; 504 | positions[i * 3 + 2] = p.z; 505 | } 506 | } 507 | 508 | Rebuild() { 509 | const positions = []; 510 | const up = []; 511 | const coords = []; 512 | 513 | const localToWorld = this.params_.worldMatrix; 514 | const resolution = this.params_.resolution + 2; 515 | const radius = this.params_.radius; 516 | const offset = this.params_.offset; 517 | const origin = this.params_.origin; 518 | const width = this.params_.width; 519 | const half = width / 2; 520 | const effectiveResolution = resolution - 2; 521 | 522 | for (let x = -1; x <= effectiveResolution + 1; x++) { 523 | const xp = width * x / effectiveResolution; 524 | for (let y = -1; y <= effectiveResolution + 1; y++) { 525 | const yp = width * y / effectiveResolution; 526 | 527 | // Compute position 528 | _P.set(xp - half, yp - half, radius); 529 | _P.add(offset); 530 | _P.normalize(); 531 | _D.copy(_P); 532 | _D.transformDirection(localToWorld); 533 | 534 | _P.multiplyScalar(radius); 535 | _P.z -= radius; 536 | _P.applyMatrix4(localToWorld); 537 | 538 | // Keep the absolute world space position to sample noise 539 | _W.copy(_P); 540 | 541 | // Move the position relative to the origin 542 | _P.sub(origin); 543 | 544 | // Purturb height along z-vector 545 | const height = this._GenerateHeight(_W); 546 | _H.copy(_D); 547 | _H.multiplyScalar(height); 548 | _P.add(_H); 549 | 550 | positions.push(_P.x, _P.y, _P.z); 551 | 552 | _C.copy(_W); 553 | _C.add(_H); 554 | coords.push(_C.x, _C.y, _C.z); 555 | 556 | _S.set(_W.x, _W.y, height); 557 | 558 | up.push(_D.x, _D.y, _D.z); 559 | } 560 | } 561 | 562 | const colours = new Array(positions.length).fill(1.0); 563 | 564 | // Generate indices 565 | const indices = this.GenerateIndices_(); 566 | const normals = this.GenerateNormals_(positions, indices); 567 | 568 | this.RebuildEdgePositions_(positions); 569 | this.RebuildEdgeNormals_(normals); 570 | this.FixEdgesToMatchNeighbours_(positions, normals, colours); 571 | this.FixEdgeSkirt_(positions, up, normals); 572 | this.NormalizeNormals_(normals); 573 | 574 | const bytesInFloat32 = 4; 575 | const bytesInInt32 = 4; 576 | const positionsArray = new Float32Array( 577 | new SharedArrayBuffer(bytesInFloat32 * positions.length)); 578 | const coloursArray = new Float32Array( 579 | new SharedArrayBuffer(bytesInFloat32 * colours.length)); 580 | const normalsArray = new Float32Array( 581 | new SharedArrayBuffer(bytesInFloat32 * normals.length)); 582 | const coordsArray = new Float32Array( 583 | new SharedArrayBuffer(bytesInFloat32 * coords.length)); 584 | const indicesArray = new Uint32Array( 585 | new SharedArrayBuffer(bytesInInt32 * indices.length)); 586 | 587 | positionsArray.set(positions, 0); 588 | coloursArray.set(colours, 0); 589 | normalsArray.set(normals, 0); 590 | coordsArray.set(coords, 0); 591 | indicesArray.set(indices, 0); 592 | 593 | return { 594 | positions: positionsArray, 595 | colours: coloursArray, 596 | normals: normalsArray, 597 | coords: coordsArray, 598 | indices: indicesArray, 599 | }; 600 | } 601 | 602 | QuickRebuild(mesh) { 603 | const positions = mesh.positions; 604 | const normals = mesh.normals; 605 | const colours = mesh.colours; 606 | const up = []; 607 | const indices = mesh.indices; 608 | 609 | const localToWorld = this.params_.worldMatrix; 610 | const resolution = this.params_.resolution + 2; 611 | const radius = this.params_.radius; 612 | const offset = this.params_.offset; 613 | const origin = this.params_.origin; 614 | const width = this.params_.width; 615 | const half = width / 2; 616 | const effectiveResolution = resolution - 2; 617 | 618 | colours.fill(1.0); 619 | 620 | this.RebuildEdgePositions_(positions); 621 | this.RebuildEdgeNormals_(normals); 622 | this.FixEdgesToMatchNeighbours_(positions, normals, colours); 623 | this.FixEdgeSkirt_(positions, up, normals); 624 | this.NormalizeNormals_(normals); 625 | 626 | return mesh; 627 | } 628 | } 629 | 630 | const _CHUNK = new _TerrainBuilderThreadedWorker(); 631 | 632 | self.onmessage = (msg) => { 633 | if (msg.data.subject == 'build_chunk') { 634 | _CHUNK.Init(msg.data.params); 635 | 636 | const rebuiltData = _CHUNK.Rebuild(); 637 | self.postMessage({subject: 'build_chunk_result', data: rebuiltData}); 638 | } else if (msg.data.subject == 'rebuild_chunk') { 639 | _CHUNK.Init(msg.data.params); 640 | 641 | const rebuiltData = _CHUNK.QuickRebuild(msg.data.mesh); 642 | self.postMessage({subject: 'quick_rebuild_chunk_result', data: rebuiltData}); 643 | } 644 | }; -------------------------------------------------------------------------------- /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 | } else if (msg.subject == 'quick_rebuild_chunk_result') { 90 | chunk.QuickRebuildMeshFromData(msg.data); 91 | } 92 | } 93 | 94 | AllocateChunk(params) { 95 | const w = params.width; 96 | 97 | if (!(w in this.pool_)) { 98 | this.pool_[w] = []; 99 | } 100 | 101 | let c = null; 102 | if (this.pool_[w].length > 0) { 103 | c = this.pool_[w].pop(); 104 | c.params_ = params; 105 | } else { 106 | c = new terrain_chunk.TerrainChunk(params); 107 | } 108 | 109 | c.Hide(); 110 | 111 | const threadedParams = { 112 | noiseParams: params.noiseParams, 113 | colourNoiseParams: params.colourNoiseParams, 114 | biomesParams: params.biomesParams, 115 | colourGeneratorParams: params.colourGeneratorParams, 116 | heightGeneratorsParams: params.heightGeneratorsParams, 117 | width: params.width, 118 | neighbours: params.neighbours, 119 | offset: params.offset.toArray(), 120 | origin: params.origin.toArray(), 121 | radius: params.radius, 122 | resolution: params.resolution, 123 | worldMatrix: params.transform, 124 | }; 125 | 126 | const msg = { 127 | subject: 'build_chunk', 128 | params: threadedParams, 129 | }; 130 | 131 | this.workerPool_.Enqueue(msg, (m) => { 132 | this._OnResult(c, m); 133 | }); 134 | 135 | return c; 136 | } 137 | 138 | RetireChunks(chunks) { 139 | this.old_.push(...chunks); 140 | } 141 | 142 | _RecycleChunks(chunks) { 143 | for (let c of chunks) { 144 | if (!(c.chunk.params_.width in this.pool_)) { 145 | this.pool_[c.chunk.params_.width] = []; 146 | } 147 | 148 | c.chunk.Destroy(); 149 | } 150 | } 151 | 152 | get Busy() { 153 | return this.workerPool_.Busy; 154 | } 155 | 156 | Rebuild(chunks) { 157 | for (let k in chunks) { 158 | this.workerPool_.Enqueue(chunks[k].chunk.params_); 159 | } 160 | } 161 | 162 | QuickRebuild(chunks) { 163 | for (let k in chunks) { 164 | const chunk = chunks[k]; 165 | const params = chunk.chunk.params_; 166 | 167 | const threadedParams = { 168 | noiseParams: params.noiseParams, 169 | colourNoiseParams: params.colourNoiseParams, 170 | biomesParams: params.biomesParams, 171 | colourGeneratorParams: params.colourGeneratorParams, 172 | heightGeneratorsParams: params.heightGeneratorsParams, 173 | width: params.width, 174 | neighbours: params.neighbours, 175 | offset: params.offset.toArray(), 176 | origin: params.origin.toArray(), 177 | radius: params.radius, 178 | resolution: params.resolution, 179 | worldMatrix: params.transform, 180 | }; 181 | 182 | const msg = { 183 | subject: 'rebuild_chunk', 184 | params: threadedParams, 185 | mesh: chunk.chunk.rebuildData_, 186 | }; 187 | 188 | this.workerPool_.Enqueue(msg, (m) => { 189 | this._OnResult(chunk.chunk, m); 190 | }); 191 | } 192 | } 193 | 194 | Update() { 195 | if (!this.Busy) { 196 | this._RecycleChunks(this.old_); 197 | this.old_ = []; 198 | } 199 | } 200 | } 201 | 202 | return { 203 | TerrainChunkRebuilder_Threaded: _TerrainChunkRebuilder_Threaded 204 | } 205 | })(); 206 | -------------------------------------------------------------------------------- /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.125/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.mesh_); 14 | } 15 | 16 | Hide() { 17 | this.mesh_.visible = false; 18 | } 19 | 20 | Show() { 21 | this.mesh_.visible = true; 22 | } 23 | 24 | _Init(params) { 25 | this.geometry_ = new THREE.BufferGeometry(); 26 | this.mesh_ = new THREE.Mesh(this.geometry_, params.material); 27 | this.mesh_.castShadow = false; 28 | this.mesh_.receiveShadow = true; 29 | this.mesh_.frustumCulled = false; 30 | this.params_.group.add(this.mesh_); 31 | this.Reinit(params); 32 | } 33 | 34 | Update(cameraPosition) { 35 | this.mesh_.position.copy(this.params_.origin); 36 | this.mesh_.position.sub(cameraPosition); 37 | } 38 | 39 | Reinit(params) { 40 | this.params_ = params; 41 | this.mesh_.position.set(0, 0, 0); 42 | } 43 | 44 | SetWireframe(b) { 45 | this.mesh_.material.wireframe = b; 46 | } 47 | 48 | RebuildMeshFromData(data) { 49 | this.geometry_.setAttribute( 50 | 'position', new THREE.Float32BufferAttribute(data.positions, 3)); 51 | this.geometry_.setAttribute( 52 | 'color', new THREE.Float32BufferAttribute(data.colours, 3)); 53 | this.geometry_.setAttribute( 54 | 'normal', new THREE.Float32BufferAttribute(data.normals, 3)); 55 | this.geometry_.setAttribute( 56 | 'coords', new THREE.Float32BufferAttribute(data.coords, 3)); 57 | this.geometry_.setIndex( 58 | new THREE.BufferAttribute(data.indices, 1)); 59 | this.rebuildData_ = data; 60 | this.geometry_.attributes.position.needsUpdate = true; 61 | this.geometry_.attributes.normal.needsUpdate = true; 62 | this.geometry_.attributes.color.needsUpdate = true; 63 | this.geometry_.attributes.coords.needsUpdate = true; 64 | } 65 | 66 | QuickRebuildMeshFromData(data) { 67 | this.geometry_.attributes.position.array.set(data.positions, 0) 68 | this.geometry_.attributes.normal.array.set(data.normals, 0) 69 | this.geometry_.attributes.color.array.set(data.colours, 0) 70 | this.geometry_.attributes.position.needsUpdate = true; 71 | this.geometry_.attributes.normal.needsUpdate = true; 72 | this.geometry_.attributes.color.needsUpdate = true; 73 | } 74 | } 75 | 76 | return { 77 | TerrainChunk: TerrainChunk 78 | } 79 | })(); -------------------------------------------------------------------------------- /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: 20000.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 | 21 | // Outputs 22 | out vec4 vColor; 23 | out vec3 vNormal; 24 | out vec3 vCoords; 25 | out vec3 vVSPos; 26 | out vec3 vRepeatingCoords; 27 | out float vFragDepth; 28 | 29 | #define saturate(a) clamp( a, 0.0, 1.0 ) 30 | 31 | void main(){ 32 | mat4 terrainMatrix = mat4( 33 | viewMatrix[0], 34 | viewMatrix[1], 35 | viewMatrix[2], 36 | vec4(0.0, 0.0, 0.0, 1.0)); 37 | 38 | gl_Position = projectionMatrix * terrainMatrix * modelMatrix * vec4(position, 1.0); 39 | 40 | vNormal = normal; 41 | 42 | vColor = vec4(color, 1); 43 | vCoords = (modelMatrix * vec4(position, 1.0)).xyz + cameraPosition; 44 | vVSPos = (terrainMatrix * modelMatrix * vec4(position, 1.0)).xyz; 45 | 46 | vec3 pos = coords; 47 | float p = 32768.0; 48 | float a = 1024.0; 49 | vRepeatingCoords = (4.0 * a / p) * abs(mod(pos, p) - p * 0.5); 50 | 51 | vFragDepth = 1.0 + gl_Position.w; 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 | uniform float logDepthBufFC; 70 | 71 | in vec4 vColor; 72 | in vec3 vNormal; 73 | in vec3 vCoords; 74 | in vec3 vRepeatingCoords; 75 | in vec3 vVSPos; 76 | in float vFragDepth; 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 | vec4 _CalculateLighting( 95 | vec3 lightDirection, vec3 lightColour, vec3 worldSpaceNormal, vec3 viewDirection) { 96 | float NdotL = saturate(dot(worldSpaceNormal, lightDirection)); 97 | // return vec4(lightColour * diffuse, 0.0); 98 | 99 | vec3 H = normalize(lightDirection + viewDirection); 100 | float NdotH = dot(worldSpaceNormal, H); 101 | float specular = saturate(pow(NdotH, 8.0)); 102 | 103 | return vec4(lightColour * NdotL, specular * NdotL); 104 | } 105 | 106 | vec4 _ComputeLighting(vec3 worldSpaceNormal, vec3 sunDir, vec3 viewDirection) { 107 | // Hardcoded, whee! 108 | vec4 lighting; 109 | 110 | lighting += _CalculateLighting( 111 | sunDir, vec3(1.0, 1.0, 1.0), worldSpaceNormal, viewDirection); 112 | // lighting += _CalculateLighting( 113 | // vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection); 114 | 115 | // lighting += vec4(0.15, 0.15, 0.15, 0.0); 116 | 117 | return lighting; 118 | } 119 | 120 | vec4 _TerrainBlend_4(vec4 samples[4]) { 121 | float depth = 0.2; 122 | float ma = max( 123 | samples[0].w, 124 | max( 125 | samples[1].w, 126 | max(samples[2].w, samples[3].w))) - depth; 127 | 128 | float b1 = max(samples[0].w - ma, 0.0); 129 | float b2 = max(samples[1].w - ma, 0.0); 130 | float b3 = max(samples[2].w - ma, 0.0); 131 | float b4 = max(samples[3].w - ma, 0.0); 132 | 133 | vec4 numer = ( 134 | samples[0] * b1 + samples[1] * b2 + 135 | samples[2] * b3 + samples[3] * b4); 136 | float denom = (b1 + b2 + b3 + b4); 137 | return numer / denom; 138 | } 139 | 140 | vec4 _TerrainBlend_4_lerp(vec4 samples[4]) { 141 | return ( 142 | samples[0] * samples[0].w + samples[1] * samples[1].w + 143 | samples[2] * samples[2].w + samples[3] * samples[3].w); 144 | } 145 | 146 | // Lifted from https://www.shadertoy.com/view/Xtl3zf 147 | vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) { 148 | float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup 149 | float l = k*8.0; 150 | float f = fract(l); 151 | 152 | float ia = floor(l+0.5); // suslik's method (see comments) 153 | float ib = floor(l); 154 | f = min(f, 1.0-f)*2.0; 155 | 156 | vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash 157 | vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash 158 | 159 | vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z)); 160 | vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z)); 161 | 162 | return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz))); 163 | } 164 | 165 | vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 166 | vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 167 | vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 168 | vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 169 | 170 | vec3 weights = abs(normal.xyz); 171 | weights = weights / (weights.x + weights.y + weights.z); 172 | 173 | return dx * weights.x + dy * weights.y + dz * weights.z; 174 | } 175 | 176 | vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 177 | // Tangent Reconstruction 178 | // Triplanar uvs 179 | vec2 uvX = pos.zy; // x facing plane 180 | vec2 uvY = pos.xz; // y facing plane 181 | vec2 uvZ = pos.xy; // z facing plane 182 | // Tangent space normal maps 183 | vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 184 | vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 185 | vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 186 | 187 | vec3 weights = abs(normal.xyz); 188 | weights = weights / (weights.x + weights.y + weights.z); 189 | 190 | // Get the sign (-1 or 1) of the surface normal 191 | vec3 axis = sign(normal); 192 | // Construct tangent to world matrices for each axis 193 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 194 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 195 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 196 | 197 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 198 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 199 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 200 | 201 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 202 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 203 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 204 | 205 | // Apply tangent to world matrix and triblend 206 | // Using clamp() because the cross products may be NANs 207 | vec3 worldNormal = normalize( 208 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 209 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 210 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z 211 | ); 212 | return vec4(worldNormal, 0.0); 213 | } 214 | 215 | vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 216 | vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 217 | vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 218 | vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 219 | 220 | vec3 weights = abs(normal.xyz); 221 | weights = weights / (weights.x + weights.y + weights.z); 222 | 223 | return dx * weights.x + dy * weights.y + dz * weights.z; 224 | } 225 | 226 | vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 227 | vec2 uvx = pos.zy; 228 | vec2 uvy = pos.xz; 229 | vec2 uvz = pos.xy; 230 | vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 231 | vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 232 | vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 233 | 234 | vec3 weights = abs(normal.xyz); 235 | weights *= weights; 236 | weights = weights / (weights.x + weights.y + weights.z); 237 | 238 | vec3 axis = sign(normal); 239 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 240 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 241 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 242 | 243 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 244 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 245 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 246 | 247 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 248 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 249 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 250 | 251 | vec3 worldNormal = normalize( 252 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 253 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 254 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z); 255 | return vec4(worldNormal, 0.0); 256 | } 257 | 258 | void main() { 259 | vec3 worldPosition = vCoords; 260 | vec3 eyeDirection = normalize(worldPosition - cameraPosition); 261 | vec3 sunDir = normalize(vec3(1, 1, -1)); 262 | vec3 worldSpaceNormal = normalize(vNormal); 263 | 264 | // Bit of a hack to remove lighting on dark side of planet 265 | vec3 diffuse = vec3(0.75); 266 | vec3 planetNormal = normalize(worldPosition); 267 | float planetLighting = saturate(dot(planetNormal, sunDir)); 268 | 269 | vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection); 270 | vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse + lighting.w * 0.1; 271 | // vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25); 272 | 273 | finalColour *= lighting.xyz; 274 | finalColour = lighting.xyz; 275 | // finalColour = vColor.xyz; 276 | 277 | out_FragColor = vec4(finalColour, 1); 278 | gl_FragDepth = log2(vFragDepth) * logDepthBufFC * 0.5; 279 | } 280 | 281 | `; 282 | 283 | return { 284 | VS: _VS, 285 | PS: _PS, 286 | }; 287 | })(); 288 | -------------------------------------------------------------------------------- /src/terrain.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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 | this.builder_ = new terrain_builder_threaded.TerrainChunkRebuilder_Threaded(); 23 | // this.builder_ = new terrainbuilder_.TerrainChunkRebuilder(); 24 | 25 | this.LoadTextures_(); 26 | 27 | this.InitNoise_(params); 28 | this.InitBiomes_(params); 29 | this.InitTerrain_(params); 30 | } 31 | 32 | LoadTextures_() { 33 | const loader = new THREE.TextureLoader(); 34 | 35 | const noiseTexture = loader.load('./resources/simplex-noise.png'); 36 | noiseTexture.wrapS = THREE.RepeatWrapping; 37 | noiseTexture.wrapT = THREE.RepeatWrapping; 38 | 39 | this.material_ = new THREE.RawShaderMaterial({ 40 | uniforms: { 41 | diffuseMap: { 42 | }, 43 | normalMap: { 44 | }, 45 | noiseMap: { 46 | value: noiseTexture 47 | }, 48 | logDepthBufFC: { 49 | value: 2.0 / (Math.log(this.params_.camera.far + 1.0) / Math.LN2), 50 | } 51 | }, 52 | vertexShader: terrain_shader.VS, 53 | fragmentShader: terrain_shader.PS, 54 | side: THREE.FrontSide 55 | }); 56 | } 57 | 58 | InitNoise_(params) { 59 | params.guiParams.noise = { 60 | octaves: 13, 61 | persistence: 0.5, 62 | lacunarity: 1.6, 63 | exponentiation: 7.5, 64 | height: terrain_constants.NOISE_HEIGHT, 65 | scale: terrain_constants.NOISE_SCALE, 66 | seed: 1 67 | }; 68 | 69 | const onNoiseChanged = () => { 70 | this.builder_.Rebuild(this.chunks_); 71 | }; 72 | 73 | const noiseRollup = params.gui.addFolder('Terrain.Noise'); 74 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange( 75 | onNoiseChanged); 76 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange( 77 | onNoiseChanged); 78 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange( 79 | onNoiseChanged); 80 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange( 81 | onNoiseChanged); 82 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange( 83 | onNoiseChanged); 84 | noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange( 85 | onNoiseChanged); 86 | 87 | this.noise_ = new noise.Noise(params.guiParams.noise); 88 | this.noiseParams_ = params.guiParams.noise; 89 | 90 | params.guiParams.heightmap = { 91 | height: 16, 92 | }; 93 | 94 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap'); 95 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange( 96 | onNoiseChanged); 97 | } 98 | 99 | InitBiomes_(params) { 100 | params.guiParams.biomes = { 101 | octaves: 2, 102 | persistence: 0.5, 103 | lacunarity: 2.0, 104 | scale: 2048.0, 105 | noiseType: 'simplex', 106 | seed: 2, 107 | exponentiation: 1, 108 | height: 1.0 109 | }; 110 | 111 | const onNoiseChanged = () => { 112 | this.builder_.Rebuild(this.chunks_); 113 | }; 114 | 115 | const noiseRollup = params.gui.addFolder('Terrain.Biomes'); 116 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange( 117 | onNoiseChanged); 118 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange( 119 | onNoiseChanged); 120 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange( 121 | onNoiseChanged); 122 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange( 123 | onNoiseChanged); 124 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange( 125 | onNoiseChanged); 126 | 127 | this.biomes_ = new noise.Noise(params.guiParams.biomes); 128 | this.biomesParams_ = params.guiParams.biomes; 129 | 130 | const colourParams = { 131 | octaves: 1, 132 | persistence: 0.5, 133 | lacunarity: 2.0, 134 | exponentiation: 1.0, 135 | scale: 256.0, 136 | noiseType: 'simplex', 137 | seed: 2, 138 | height: 1.0, 139 | }; 140 | this.colourNoise_ = new noise.Noise(colourParams); 141 | this.colourNoiseParams_ = colourParams; 142 | } 143 | 144 | InitTerrain_(params) { 145 | params.guiParams.terrain = { 146 | wireframe: false, 147 | fixedCamera: false, 148 | }; 149 | 150 | this.groups_ = [...new Array(6)].map(_ => new THREE.Group()); 151 | params.scene.add(...this.groups_); 152 | 153 | const terrainRollup = params.gui.addFolder('Terrain'); 154 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => { 155 | for (let k in this.chunks_) { 156 | this.chunks_[k].chunk.SetWireframe(params.guiParams.terrain.wireframe); 157 | } 158 | }); 159 | 160 | terrainRollup.add(params.guiParams.terrain, "fixedCamera"); 161 | 162 | this.chunks_ = {}; 163 | this.params_ = params; 164 | } 165 | 166 | _CreateTerrainChunk(group, groupTransform, offset, cameraPosition, width, neighbours, resolution) { 167 | const params = { 168 | group: group, 169 | transform: groupTransform, 170 | material: this.material_, 171 | width: width, 172 | offset: offset, 173 | origin: cameraPosition.clone(), 174 | radius: terrain_constants.PLANET_RADIUS, 175 | resolution: resolution, 176 | neighbours: neighbours, 177 | biomeGenerator: this.biomes_, 178 | colourGenerator: new texture_splatter.TextureSplatter( 179 | {biomeGenerator: this.biomes_, colourNoise: this.colourNoise_}), 180 | heightGenerators: [new texture_splatter.HeightGenerator( 181 | this.noise_, offset, 100000, 100000 + 1)], 182 | noiseParams: this.noiseParams_, 183 | colourNoiseParams: this.colourNoiseParams_, 184 | biomesParams: this.biomesParams_, 185 | colourGeneratorParams: { 186 | biomeGeneratorParams: this.biomesParams_, 187 | colourNoiseParams: this.colourNoiseParams_, 188 | }, 189 | heightGeneratorsParams: { 190 | min: 100000, 191 | max: 100000 + 1, 192 | } 193 | }; 194 | 195 | return this.builder_.AllocateChunk(params); 196 | } 197 | 198 | Update(_) { 199 | const cameraPosition = this.params_.camera.position.clone(); 200 | if (this.params_.guiParams.terrain.fixedCamera) { 201 | cameraPosition.copy(this.cachedCamera_); 202 | } else { 203 | this.cachedCamera_ = cameraPosition.clone(); 204 | } 205 | 206 | this.builder_.Update(); 207 | if (!this.builder_.Busy) { 208 | for (let k in this.chunks_) { 209 | this.chunks_[k].chunk.Show(); 210 | } 211 | this.UpdateVisibleChunks_Quadtree_(cameraPosition); 212 | } 213 | 214 | for (let k in this.chunks_) { 215 | this.chunks_[k].chunk.Update(this.params_.camera.position); 216 | } 217 | for (let c of this.builder_.old_) { 218 | c.chunk.Update(this.params_.camera.position); 219 | } 220 | 221 | this.params_.scattering.uniforms.planetRadius.value = terrain_constants.PLANET_RADIUS; 222 | this.params_.scattering.uniforms.atmosphereRadius.value = terrain_constants.PLANET_RADIUS * 1.01; 223 | } 224 | 225 | UpdateVisibleChunks_Quadtree_(cameraPosition) { 226 | function _Key(c) { 227 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']'; 228 | } 229 | 230 | const q = new quadtree.CubeQuadTree({ 231 | radius: terrain_constants.PLANET_RADIUS, 232 | min_node_size: terrain_constants.QT_MIN_CELL_SIZE, 233 | max_node_size: terrain_constants.QT_MAX_CELL_SIZE, 234 | }); 235 | q.Insert(cameraPosition); 236 | q.BuildNeighbours(); 237 | 238 | const sides = q.GetChildren(); 239 | 240 | let newTerrainChunks = {}; 241 | const center = new THREE.Vector3(); 242 | const dimensions = new THREE.Vector3(); 243 | 244 | const _Child = (c) => { 245 | c.bounds.getCenter(center); 246 | c.bounds.getSize(dimensions); 247 | 248 | const child = { 249 | index: c.side, 250 | group: this.groups_[c.side], 251 | transform: sides[c.side].transform, 252 | position: [center.x, center.y, center.z], 253 | bounds: c.bounds, 254 | size: dimensions.x, 255 | neighbours: c.neighbours.map(n => n.size.x / c.size.x), 256 | neighboursOriginal: c.neighbours, 257 | }; 258 | return child; 259 | }; 260 | 261 | for (let i = 0; i < sides.length; i++) { 262 | for (let c of sides[i].children) { 263 | const child = _Child(c); 264 | const k = _Key(child); 265 | 266 | const left = c.neighbours[0].GetClosestChildrenSharingEdge(c.GetLeftEdgeMidpoint()); 267 | const top = c.neighbours[1].GetClosestChildrenSharingEdge(c.GetTopEdgeMidpoint()); 268 | const right = c.neighbours[2].GetClosestChildrenSharingEdge(c.GetRightEdgeMidpoint()); 269 | const bottom = c.neighbours[3].GetClosestChildrenSharingEdge(c.GetBottomEdgeMidpoint()); 270 | 271 | child.neighbourKeys = [...left, ...top, ...right, ...bottom].map(n => _Key(_Child(n))); 272 | child.debug = [left, top, right, bottom]; 273 | 274 | newTerrainChunks[k] = child; 275 | } 276 | } 277 | 278 | 279 | const allChunks = newTerrainChunks; 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 | if (0) { 285 | const partialRebuilds = {}; 286 | 287 | for (let k in difference) { 288 | for (let n of difference[k].neighbourKeys) { 289 | if (n in this.chunks_) { 290 | partialRebuilds[n] = newTerrainChunks[n]; 291 | } 292 | } 293 | } 294 | for (let k in partialRebuilds) { 295 | if (k in intersection) { 296 | recycle.push(this.chunks_[k]); 297 | delete intersection[k]; 298 | difference[k] = allChunks[k]; 299 | } 300 | } 301 | } 302 | 303 | this.builder_.RetireChunks(recycle); 304 | 305 | newTerrainChunks = intersection; 306 | 307 | const partialRebuilds = {}; 308 | 309 | for (let k in difference) { 310 | const [xp, yp, zp] = difference[k].position; 311 | 312 | const offset = new THREE.Vector3(xp, yp, zp); 313 | newTerrainChunks[k] = { 314 | position: [xp, zp], 315 | chunk: this._CreateTerrainChunk( 316 | difference[k].group, difference[k].transform, 317 | offset, cameraPosition, difference[k].size, difference[k].neighbours, 318 | terrain_constants.QT_MIN_CELL_RESOLUTION), 319 | }; 320 | 321 | for (let n of difference[k].neighbourKeys) { 322 | if (n in this.chunks_) { 323 | partialRebuilds[n] = intersection[n]; 324 | partialRebuilds[n].chunk.params_.neighbours = allChunks[n].neighbours; 325 | } 326 | } 327 | } 328 | 329 | this.builder_.QuickRebuild(partialRebuilds); 330 | 331 | this.chunks_ = newTerrainChunks; 332 | } 333 | } 334 | 335 | return { 336 | TerrainChunkManager: TerrainChunkManager 337 | } 338 | })(); 339 | -------------------------------------------------------------------------------- /src/texture-splatter.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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.125/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 | diffuse.encoding = THREE.sRGBEncoding; 66 | 67 | atlas.atlas = diffuse; 68 | } 69 | 70 | this.onLoad(); 71 | } 72 | 73 | LoadAtlas_(atlas, names) { 74 | this.textures_[atlas] = { 75 | textures: names.map(n => this.loader_.load(n)) 76 | }; 77 | } 78 | } 79 | }; 80 | })(); 81 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------