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