├── 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 ├── controls.js ├── demo.js ├── game.js ├── graphics.js ├── main.js ├── math.js ├── noise.js ├── perlin-noise.js ├── quadtree.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_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/dirt_01_diffuse-1024.png -------------------------------------------------------------------------------- /resources/dirt_01_normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/dirt_01_normal-1024.jpg -------------------------------------------------------------------------------- /resources/grass1-albedo-512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/grass1-albedo-512.jpg -------------------------------------------------------------------------------- /resources/grass1-albedo3-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/grass1-albedo3-1024.png -------------------------------------------------------------------------------- /resources/grass1-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/grass1-normal-1024.jpg -------------------------------------------------------------------------------- /resources/rock-snow-ice-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/rock-snow-ice-albedo-1024.png -------------------------------------------------------------------------------- /resources/rock-snow-ice-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/rock-snow-ice-normal-1024.jpg -------------------------------------------------------------------------------- /resources/rough-wet-cobble-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/rough-wet-cobble-albedo-1024.png -------------------------------------------------------------------------------- /resources/rough-wet-cobble-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/rough-wet-cobble-normal-1024.jpg -------------------------------------------------------------------------------- /resources/sandy-rocks1-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/sandy-rocks1-albedo-1024.png -------------------------------------------------------------------------------- /resources/sandy-rocks1-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/sandy-rocks1-normal-1024.jpg -------------------------------------------------------------------------------- /resources/sandyground-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/sandyground-albedo-1024.png -------------------------------------------------------------------------------- /resources/sandyground-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/sandyground-normal-1024.jpg -------------------------------------------------------------------------------- /resources/simplex-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/simplex-noise.png -------------------------------------------------------------------------------- /resources/snow-packed-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/snow-packed-albedo-1024.png -------------------------------------------------------------------------------- /resources/snow-packed-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/snow-packed-normal-1024.jpg -------------------------------------------------------------------------------- /resources/space-negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-negx.jpg -------------------------------------------------------------------------------- /resources/space-negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-negy.jpg -------------------------------------------------------------------------------- /resources/space-negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-negz.jpg -------------------------------------------------------------------------------- /resources/space-posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-posx.jpg -------------------------------------------------------------------------------- /resources/space-posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-posy.jpg -------------------------------------------------------------------------------- /resources/space-posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/space-posz.jpg -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-albedo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/worn-bumpy-rock-albedo-1024.png -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-albedo-512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/worn-bumpy-rock-albedo-512.jpg -------------------------------------------------------------------------------- /resources/worn-bumpy-rock-normal-1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part5/2b37d1a5393db2215b339d432b757099432d1289/resources/worn-bumpy-rock-normal-1024.jpg -------------------------------------------------------------------------------- /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(4000, 4000, 4000); 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: 4000, 65 | }; 66 | 67 | const rollup = this._params.gui.addFolder('Camera.FPS'); 68 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.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.y = 0; 242 | forward.normalize(); 243 | 244 | const updown = new THREE.Vector3(0, 1, 0); 245 | 246 | const sideways = new THREE.Vector3(1, 0, 0); 247 | sideways.applyQuaternion(controlObject.quaternion); 248 | sideways.normalize(); 249 | 250 | sideways.multiplyScalar(this._velocity.x * timeInSeconds); 251 | updown.multiplyScalar(this._velocity.y * timeInSeconds); 252 | forward.multiplyScalar(this._velocity.z * timeInSeconds); 253 | 254 | controlObject.position.add(forward); 255 | controlObject.position.add(sideways); 256 | controlObject.position.add(updown); 257 | 258 | oldPosition.copy(controlObject.position); 259 | } 260 | } 261 | 262 | return { 263 | FPSControls: _FPSControls, 264 | OrbitControls: _OrbitControls, 265 | }; 266 | })(); 267 | -------------------------------------------------------------------------------- /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 | 6 | export const graphics = (function() { 7 | 8 | function _GetImageData(image) { 9 | const canvas = document.createElement('canvas'); 10 | canvas.width = image.width; 11 | canvas.height = image.height; 12 | 13 | const context = canvas.getContext( '2d' ); 14 | context.drawImage(image, 0, 0); 15 | 16 | return context.getImageData(0, 0, image.width, image.height); 17 | } 18 | 19 | function _GetPixel(imagedata, x, y) { 20 | const position = (x + imagedata.width * y) * 4; 21 | const data = imagedata.data; 22 | return { 23 | r: data[position], 24 | g: data[position + 1], 25 | b: data[position + 2], 26 | a: data[position + 3] 27 | }; 28 | } 29 | 30 | class _Graphics { 31 | constructor(game) { 32 | } 33 | 34 | Initialize() { 35 | if (!WEBGL.isWebGL2Available()) { 36 | return false; 37 | } 38 | 39 | const canvas = document.createElement('canvas'); 40 | const context = canvas.getContext('webgl2', {alpha: false}); 41 | 42 | this._threejs = new THREE.WebGLRenderer({ 43 | canvas: canvas, 44 | context: context, 45 | antialias: true, 46 | }); 47 | this._threejs.setPixelRatio(window.devicePixelRatio); 48 | this._threejs.setSize(window.innerWidth, window.innerHeight); 49 | 50 | const target = document.getElementById('target'); 51 | target.appendChild(this._threejs.domElement); 52 | 53 | this._stats = new Stats(); 54 | //target.appendChild(this._stats.dom); 55 | 56 | window.addEventListener('resize', () => { 57 | this._OnWindowResize(); 58 | }, false); 59 | 60 | const fov = 60; 61 | const aspect = 1920 / 1080; 62 | const near = 1; 63 | const far = 100000.0; 64 | this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 65 | this._camera.position.set(75, 20, 0); 66 | 67 | this._scene = new THREE.Scene(); 68 | this._scene.background = new THREE.Color(0xaaaaaa); 69 | 70 | this._CreateLights(); 71 | 72 | return true; 73 | } 74 | 75 | _CreateLights() { 76 | let light = new THREE.DirectionalLight(0xFFFFFF, 1, 100); 77 | light.position.set(-100, 100, -100); 78 | light.target.position.set(0, 0, 0); 79 | light.castShadow = false; 80 | this._scene.add(light); 81 | 82 | light = new THREE.DirectionalLight(0x404040, 1, 100); 83 | light.position.set(100, 100, -100); 84 | light.target.position.set(0, 0, 0); 85 | light.castShadow = false; 86 | this._scene.add(light); 87 | 88 | light = new THREE.DirectionalLight(0x404040, 1, 100); 89 | light.position.set(100, 100, -100); 90 | light.target.position.set(0, 0, 0); 91 | light.castShadow = false; 92 | this._scene.add(light); 93 | 94 | light = new THREE.DirectionalLight(0x101040, 1, 100); 95 | light.position.set(100, -100, 100); 96 | light.target.position.set(0, 0, 0); 97 | light.castShadow = false; 98 | this._scene.add(light); 99 | } 100 | 101 | _OnWindowResize() { 102 | this._camera.aspect = window.innerWidth / window.innerHeight; 103 | this._camera.updateProjectionMatrix(); 104 | this._threejs.setSize(window.innerWidth, window.innerHeight); 105 | } 106 | 107 | get Scene() { 108 | return this._scene; 109 | } 110 | 111 | get Camera() { 112 | return this._camera; 113 | } 114 | 115 | Render(timeInSeconds) { 116 | this._threejs.render(this._scene, this._camera); 117 | this._stats.update(); 118 | } 119 | } 120 | 121 | return { 122 | Graphics: _Graphics, 123 | GetPixel: _GetPixel, 124 | GetImageData: _GetImageData, 125 | }; 126 | })(); 127 | -------------------------------------------------------------------------------- /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 {sky} from './sky.js'; 6 | import {terrain} from './terrain.js'; 7 | import {textures} from './textures.js'; 8 | 9 | 10 | let _APP = null; 11 | 12 | 13 | 14 | class ProceduralTerrain_Demo extends game.Game { 15 | constructor() { 16 | super(); 17 | } 18 | 19 | _OnInitialize() { 20 | this._CreateGUI(); 21 | 22 | this._userCamera = new THREE.Object3D(); 23 | this._userCamera.position.set(4100, 0, 0); 24 | this._graphics.Camera.position.set(3853, -609, -1509); 25 | this._graphics.Camera.quaternion.set(0.403, 0.59, -0.549, 0.432); 26 | 27 | this._graphics.Camera.position.set(1412, -1674, -3848); 28 | this._graphics.Camera.quaternion.set(0.1004, 0.7757, -0.6097, 0.1278); 29 | 30 | this._entities['_terrain'] = new terrain.TerrainChunkManager({ 31 | camera: this._graphics.Camera, 32 | scene: this._graphics.Scene, 33 | gui: this._gui, 34 | guiParams: this._guiParams, 35 | game: this 36 | }); 37 | 38 | this._entities['_controls'] = new controls.FPSControls({ 39 | camera: this._graphics.Camera, 40 | scene: this._graphics.Scene, 41 | domElement: this._graphics._threejs.domElement, 42 | gui: this._gui, 43 | guiParams: this._guiParams, 44 | }); 45 | 46 | // this._entities['_controls'] = new controls.OrbitControls({ 47 | // camera: this._graphics.Camera, 48 | // scene: this._graphics.Scene, 49 | // domElement: this._graphics._threejs.domElement, 50 | // gui: this._gui, 51 | // guiParams: this._guiParams, 52 | // }); 53 | 54 | this._focusMesh = new THREE.Mesh( 55 | new THREE.SphereGeometry(25, 32, 32), 56 | new THREE.MeshBasicMaterial({ 57 | color: 0xFFFFFF 58 | })); 59 | this._focusMesh.castShadow = true; 60 | this._focusMesh.receiveShadow = true; 61 | //this._graphics.Scene.add(this._focusMesh); 62 | 63 | this._totalTime = 0; 64 | 65 | this._LoadBackground(); 66 | } 67 | 68 | _CreateGUI() { 69 | this._guiParams = { 70 | general: { 71 | }, 72 | }; 73 | this._gui = new GUI(); 74 | 75 | const generalRollup = this._gui.addFolder('General'); 76 | this._gui.close(); 77 | } 78 | 79 | _LoadBackground() { 80 | this._graphics.Scene.background = new THREE.Color(0x000000); 81 | const loader = new THREE.CubeTextureLoader(); 82 | const texture = loader.load([ 83 | './resources/space-posx.jpg', 84 | './resources/space-negx.jpg', 85 | './resources/space-posy.jpg', 86 | './resources/space-negy.jpg', 87 | './resources/space-posz.jpg', 88 | './resources/space-negz.jpg', 89 | ]); 90 | this._graphics._scene.background = texture; 91 | } 92 | 93 | _OnStep(timeInSeconds) { 94 | this._totalTime += timeInSeconds; 95 | 96 | const x = Math.cos(this._totalTime * 0.025) * 4100; 97 | const y = Math.sin(this._totalTime * 0.025) * 4100; 98 | this._userCamera.position.set(x, 0, y); 99 | 100 | this._focusMesh.position.copy(this._userCamera.position); 101 | } 102 | } 103 | 104 | 105 | function _Main() { 106 | _APP = new ProceduralTerrain_Demo(); 107 | } 108 | 109 | _Main(); 110 | -------------------------------------------------------------------------------- /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.25 && 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/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 | for (let i = 0, n = indices.length; i < n; i+=3) { 160 | const splats = []; 161 | const i1 = indices[i] * 3; 162 | const i2 = indices[i+1] * 3; 163 | const i3 = indices[i+2] * 3; 164 | const indexes = [i1, i2, i3]; 165 | for (let j = 0; j < 3; j++) { 166 | const j1 = indexes[j]; 167 | _P.fromArray(wsPositions, j1); 168 | _N.fromArray(normals, j1); 169 | _D.fromArray(up, j1); 170 | const s = this._params.colourGenerator.GetSplat(_P, _N, _D); 171 | splats.push(s); 172 | } 173 | 174 | const splatStrengths = {}; 175 | for (let k in splats[0]) { 176 | splatStrengths[k] = {key: k, strength: 0.0}; 177 | } 178 | for (let curSplat of splats) { 179 | for (let k in curSplat) { 180 | splatStrengths[k].strength += curSplat[k].strength; 181 | } 182 | } 183 | 184 | let typeValues = Object.values(splatStrengths); 185 | typeValues.sort((a, b) => { 186 | if (a.strength < b.strength) { 187 | return 1; 188 | } 189 | if (a.strength > b.strength) { 190 | return -1; 191 | } 192 | return 0; 193 | }); 194 | 195 | const w1 = indices[i] * 4; 196 | const w2 = indices[i+1] * 4; 197 | const w3 = indices[i+2] * 4; 198 | 199 | for (let s = 0; s < 3; s++) { 200 | let total = ( 201 | splats[s][typeValues[0].key].strength + 202 | splats[s][typeValues[1].key].strength + 203 | splats[s][typeValues[2].key].strength + 204 | splats[s][typeValues[3].key].strength); 205 | const normalization = 1.0 / total; 206 | 207 | splats[s][typeValues[0].key].strength *= normalization; 208 | splats[s][typeValues[1].key].strength *= normalization; 209 | splats[s][typeValues[2].key].strength *= normalization; 210 | splats[s][typeValues[3].key].strength *= normalization; 211 | } 212 | 213 | weights1.push(splats[0][typeValues[3].key].index); 214 | weights1.push(splats[0][typeValues[2].key].index); 215 | weights1.push(splats[0][typeValues[1].key].index); 216 | weights1.push(splats[0][typeValues[0].key].index); 217 | 218 | weights1.push(splats[1][typeValues[3].key].index); 219 | weights1.push(splats[1][typeValues[2].key].index); 220 | weights1.push(splats[1][typeValues[1].key].index); 221 | weights1.push(splats[1][typeValues[0].key].index); 222 | 223 | weights1.push(splats[2][typeValues[3].key].index); 224 | weights1.push(splats[2][typeValues[2].key].index); 225 | weights1.push(splats[2][typeValues[1].key].index); 226 | weights1.push(splats[2][typeValues[0].key].index); 227 | 228 | weights2.push(splats[0][typeValues[3].key].strength); 229 | weights2.push(splats[0][typeValues[2].key].strength); 230 | weights2.push(splats[0][typeValues[1].key].strength); 231 | weights2.push(splats[0][typeValues[0].key].strength); 232 | 233 | weights2.push(splats[1][typeValues[3].key].strength); 234 | weights2.push(splats[1][typeValues[2].key].strength); 235 | weights2.push(splats[1][typeValues[1].key].strength); 236 | weights2.push(splats[1][typeValues[0].key].strength); 237 | 238 | weights2.push(splats[2][typeValues[3].key].strength); 239 | weights2.push(splats[2][typeValues[2].key].strength); 240 | weights2.push(splats[2][typeValues[1].key].strength); 241 | weights2.push(splats[2][typeValues[0].key].strength); 242 | } 243 | yield; 244 | 245 | function _Unindex(src, stride) { 246 | const dst = []; 247 | for (let i = 0, n = indices.length; i < n; i+= 3) { 248 | const i1 = indices[i] * stride; 249 | const i2 = indices[i+1] * stride; 250 | const i3 = indices[i+2] * stride; 251 | 252 | for (let j = 0; j < stride; j++) { 253 | dst.push(src[i1 + j]); 254 | } 255 | for (let j = 0; j < stride; j++) { 256 | dst.push(src[i2 + j]); 257 | } 258 | for (let j = 0; j < stride; j++) { 259 | dst.push(src[i3 + j]); 260 | } 261 | } 262 | return dst; 263 | } 264 | 265 | const uiPositions = _Unindex(positions, 3); 266 | yield; 267 | 268 | const uiColours = _Unindex(colors, 3); 269 | yield; 270 | 271 | const uiNormals = _Unindex(normals, 3); 272 | yield; 273 | 274 | const uiTangents = _Unindex(tangents, 4); 275 | yield; 276 | 277 | const uiUVs = _Unindex(uvs, 2); 278 | yield; 279 | 280 | const uiWeights1 = weights1; 281 | const uiWeights2 = weights2; 282 | 283 | this._geometry.setAttribute( 284 | 'position', new THREE.Float32BufferAttribute(uiPositions, 3)); 285 | this._geometry.setAttribute( 286 | 'color', new THREE.Float32BufferAttribute(uiColours, 3)); 287 | this._geometry.setAttribute( 288 | 'normal', new THREE.Float32BufferAttribute(uiNormals, 3)); 289 | this._geometry.setAttribute( 290 | 'tangent', new THREE.Float32BufferAttribute(uiTangents, 4)); 291 | this._geometry.setAttribute( 292 | 'weights1', new THREE.Float32BufferAttribute(uiWeights1, 4)); 293 | this._geometry.setAttribute( 294 | 'weights2', new THREE.Float32BufferAttribute(uiWeights2, 4)); 295 | this._geometry.setAttribute( 296 | 'uv', new THREE.Float32BufferAttribute(uiUVs, 2)); 297 | } 298 | } 299 | 300 | return { 301 | TerrainChunk: TerrainChunk 302 | } 303 | })(); 304 | -------------------------------------------------------------------------------- /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 = 100.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.25, 1.25, 1.25), worldSpaceNormal, viewDirection); 114 | lighting += _CalculateLighting( 115 | -sunDir, vec3(0.75, 0.75, 1.0), worldSpaceNormal, viewDirection); 116 | lighting += _CalculateLighting( 117 | vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection); 118 | 119 | return lighting; 120 | } 121 | 122 | vec4 _TerrainBlend_4(vec4 samples[4]) { 123 | float depth = 0.2; 124 | float ma = max( 125 | samples[0].w, 126 | max( 127 | samples[1].w, 128 | max(samples[2].w, samples[3].w))) - depth; 129 | 130 | float b1 = max(samples[0].w - ma, 0.0); 131 | float b2 = max(samples[1].w - ma, 0.0); 132 | float b3 = max(samples[2].w - ma, 0.0); 133 | float b4 = max(samples[3].w - ma, 0.0); 134 | 135 | vec4 numer = ( 136 | samples[0] * b1 + samples[1] * b2 + 137 | samples[2] * b3 + samples[3] * b4); 138 | float denom = (b1 + b2 + b3 + b4); 139 | return numer / denom; 140 | } 141 | 142 | vec4 _TerrainBlend_4_lerp(vec4 samples[4]) { 143 | return ( 144 | samples[0] * samples[0].w + samples[1] * samples[1].w + 145 | samples[2] * samples[2].w + samples[3] * samples[3].w); 146 | } 147 | 148 | // Lifted from https://www.shadertoy.com/view/Xtl3zf 149 | vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) { 150 | float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup 151 | float l = k*8.0; 152 | float f = fract(l); 153 | 154 | float ia = floor(l+0.5); // suslik's method (see comments) 155 | float ib = floor(l); 156 | f = min(f, 1.0-f)*2.0; 157 | 158 | vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash 159 | vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash 160 | 161 | vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z)); 162 | vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z)); 163 | 164 | return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz))); 165 | } 166 | 167 | vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 168 | vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 169 | vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 170 | vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 171 | 172 | vec3 weights = abs(normal.xyz); 173 | weights = weights / (weights.x + weights.y + weights.z); 174 | 175 | return dx * weights.x + dy * weights.y + dz * weights.z; 176 | } 177 | 178 | vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 179 | // Tangent Reconstruction 180 | // Triplanar uvs 181 | vec2 uvX = pos.zy; // x facing plane 182 | vec2 uvY = pos.xz; // y facing plane 183 | vec2 uvZ = pos.xy; // z facing plane 184 | // Tangent space normal maps 185 | vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 186 | vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 187 | vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 188 | 189 | vec3 weights = abs(normal.xyz); 190 | weights = weights / (weights.x + weights.y + weights.z); 191 | 192 | // Get the sign (-1 or 1) of the surface normal 193 | vec3 axis = sign(normal); 194 | // Construct tangent to world matrices for each axis 195 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 196 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 197 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 198 | 199 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 200 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 201 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 202 | 203 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 204 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 205 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 206 | 207 | // Apply tangent to world matrix and triblend 208 | // Using clamp() because the cross products may be NANs 209 | vec3 worldNormal = normalize( 210 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 211 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 212 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z 213 | ); 214 | return vec4(worldNormal, 0.0); 215 | } 216 | 217 | vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 218 | vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice)); 219 | vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice)); 220 | vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice)); 221 | 222 | vec3 weights = abs(normal.xyz); 223 | weights = weights / (weights.x + weights.y + weights.z); 224 | 225 | return dx * weights.x + dy * weights.y + dz * weights.z; 226 | } 227 | 228 | vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) { 229 | vec2 uvx = pos.zy; 230 | vec2 uvy = pos.xz; 231 | vec2 uvz = pos.xy; 232 | vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 233 | vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 234 | vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1); 235 | 236 | vec3 weights = abs(normal.xyz); 237 | weights *= weights; 238 | weights = weights / (weights.x + weights.y + weights.z); 239 | 240 | vec3 axis = sign(normal); 241 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0))); 242 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x; 243 | mat3 tbnX = mat3(tangentX, bitangentX, normal); 244 | 245 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y))); 246 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y; 247 | mat3 tbnY = mat3(tangentY, bitangentY, normal); 248 | 249 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0))); 250 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z; 251 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal); 252 | 253 | vec3 worldNormal = normalize( 254 | clamp(tbnX * tx, -1.0, 1.0) * weights.x + 255 | clamp(tbnY * ty, -1.0, 1.0) * weights.y + 256 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z); 257 | return vec4(worldNormal, 0.0); 258 | } 259 | 260 | void main() { 261 | vec3 worldPosition = (modelMatrix * vec4(vPosition, 1)).xyz; 262 | vec3 eyeDirection = normalize(worldPosition - cameraPosition); 263 | vec3 sunDir = normalize(vec3(1, 1, -1)); 264 | 265 | float heights[4] = float[4](1.0, 1.0, 2.0, 4.0); 266 | float weightIndices[4] = float[4](vWeights1.x, vWeights1.y, vWeights1.z, vWeights1.w); 267 | float weightValues[4] = float[4](vWeights2.x, vWeights2.y, vWeights2.z, vWeights2.w); 268 | 269 | // TRIPLANAR SPLATTING w/ NORMALS & UVS 270 | vec3 worldSpaceNormal = (modelMatrix * vec4(vNormal, 0.0)).xyz; 271 | vec4 diffuseSamples[4]; 272 | vec4 normalSamples[4]; 273 | 274 | for (int i = 0; i < 4; ++i) { 275 | vec4 d = _Triplanar_UV( 276 | worldPosition, worldSpaceNormal, weightIndices[i], diffuseMap); 277 | vec4 n = _TriplanarN_UV( 278 | worldPosition, worldSpaceNormal, weightIndices[i], normalMap); 279 | 280 | d.w *= weightValues[i]; 281 | n.w = d.w; 282 | 283 | diffuseSamples[i] = d; 284 | normalSamples[i] = n; 285 | } 286 | 287 | vec4 diffuseBlended = _TerrainBlend_4(diffuseSamples); 288 | vec4 normalBlended = _TerrainBlend_4(normalSamples); 289 | 290 | vec3 diffuse = diffuseBlended.xyz; 291 | worldSpaceNormal = normalize(normalBlended.xyz); 292 | 293 | vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection); 294 | vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse; 295 | finalColour *= lighting.xyz; 296 | 297 | out_FragColor = vec4(_ACESFilmicToneMapping(finalColour), 1); 298 | } 299 | 300 | `; 301 | 302 | return { 303 | VS: _VS, 304 | PS: _PS, 305 | }; 306 | })(); 307 | -------------------------------------------------------------------------------- /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 | // Cross-blended Hypsometric Tints 57 | // http://www.shadedrelief.com/hypso/hypso.html 58 | class HyposemetricTints { 59 | constructor(params) { 60 | const _colourLerp = (t, p0, p1) => { 61 | const c = p0.clone(); 62 | 63 | return c.lerp(p1, t); 64 | }; 65 | this._colourSpline = [ 66 | new spline.LinearSpline(_colourLerp), 67 | new spline.LinearSpline(_colourLerp) 68 | ]; 69 | 70 | // Arid 71 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d)); 72 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc)); 73 | this._colourSpline[0].AddPoint(1.0, _SNOW); 74 | 75 | // Humid 76 | this._colourSpline[1].AddPoint(0.0, _ApplyWeightsOREST_BOREAL); 77 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c)); 78 | this._colourSpline[1].AddPoint(1.0, _SNOW); 79 | 80 | this._oceanSpline = new spline.LinearSpline(_colourLerp); 81 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN); 82 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN); 83 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN); 84 | 85 | this._params = params; 86 | } 87 | 88 | Get(x, y, z) { 89 | const m = this._params.biomeGenerator.Get(x, y, z); 90 | const h = z / 100.0; 91 | 92 | if (h < 0.05) { 93 | return this._oceanSpline.Get(h); 94 | } 95 | 96 | const c1 = this._colourSpline[0].Get(h); 97 | const c2 = this._colourSpline[1].Get(h); 98 | 99 | return c1.lerp(c2, m); 100 | } 101 | } 102 | 103 | 104 | class TextureSplatter { 105 | constructor(params) { 106 | const _colourLerp = (t, p0, p1) => { 107 | const c = p0.clone(); 108 | 109 | return c.lerp(p1, t); 110 | }; 111 | this._colourSpline = [ 112 | new spline.LinearSpline(_colourLerp), 113 | new spline.LinearSpline(_colourLerp) 114 | ]; 115 | 116 | // Arid 117 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d)); 118 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc)); 119 | this._colourSpline[0].AddPoint(1.0, _SNOW); 120 | 121 | // Humid 122 | this._colourSpline[1].AddPoint(0.0, _ApplyWeightsOREST_BOREAL); 123 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c)); 124 | this._colourSpline[1].AddPoint(1.0, _SNOW); 125 | 126 | this._oceanSpline = new spline.LinearSpline(_colourLerp); 127 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN); 128 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN); 129 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN); 130 | 131 | this._params = params; 132 | } 133 | 134 | _BaseColour(x, y, z) { 135 | const m = this._params.biomeGenerator.Get(x, y, z); 136 | const h = math.sat(z / 100.0); 137 | 138 | const c1 = this._colourSpline[0].Get(h); 139 | const c2 = this._colourSpline[1].Get(h); 140 | 141 | let c = c1.lerp(c2, m); 142 | 143 | if (h < 0.1) { 144 | c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05)); 145 | } 146 | return c; 147 | } 148 | 149 | _Colour(x, y, z) { 150 | const c = this._BaseColour(x, y, z); 151 | const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0; 152 | 153 | c.offsetHSL(0.0, 0.0, r * 0.01); 154 | return c; 155 | } 156 | 157 | _GetTextureWeights(p, n, up) { 158 | const m = this._params.biomeGenerator.Get(p.x, p.y, p.z); 159 | const h = p.z / 100.0; 160 | 161 | const types = { 162 | dirt: {index: 0, strength: 0.0}, 163 | grass: {index: 1, strength: 0.0}, 164 | gravel: {index: 2, strength: 0.0}, 165 | rock: {index: 3, strength: 0.0}, 166 | snow: {index: 4, strength: 0.0}, 167 | snowrock: {index: 5, strength: 0.0}, 168 | cobble: {index: 6, strength: 0.0}, 169 | sandyrock: {index: 7, strength: 0.0}, 170 | }; 171 | 172 | function _ApplyWeights(dst, v, m) { 173 | for (let k in types) { 174 | types[k].strength *= m; 175 | } 176 | types[dst].strength = v; 177 | }; 178 | 179 | types.grass.strength = 1.0; 180 | _ApplyWeights('gravel', 1.0 - m, m); 181 | 182 | if (h < 0.2) { 183 | const s = 1.0 - math.sat((h - 0.1) / 0.05); 184 | _ApplyWeights('cobble', s, 1.0 - s); 185 | 186 | if (h < 0.1) { 187 | const s = 1.0 - math.sat((h - 0.05) / 0.05); 188 | _ApplyWeights('sandyrock', s, 1.0 - s); 189 | } 190 | } else { 191 | if (h > 0.125) { 192 | const s = (math.sat((h - 0.125) / 1.25)); 193 | _ApplyWeights('rock', s, 1.0 - s); 194 | } 195 | 196 | if (h > 1.5) { 197 | const s = math.sat((h - 0.75) / 2.0); 198 | _ApplyWeights('snow', s, 1.0 - s); 199 | } 200 | } 201 | 202 | // In case nothing gets set. 203 | types.dirt.strength = 0.01; 204 | 205 | let total = 0.0; 206 | for (let k in types) { 207 | total += types[k].strength; 208 | } 209 | if (total < 0.01) { 210 | const a = 0; 211 | } 212 | const normalization = 1.0 / total; 213 | 214 | for (let k in types) { 215 | types[k].strength / normalization; 216 | } 217 | 218 | return types; 219 | } 220 | 221 | GetColour(position) { 222 | return this._Colour(position.x, position.y, position.z); 223 | } 224 | 225 | GetSplat(position, normal, up) { 226 | return this._GetTextureWeights(position, normal, up); 227 | } 228 | } 229 | 230 | 231 | class FixedColourGenerator { 232 | constructor(params) { 233 | this._params = params; 234 | } 235 | 236 | Get() { 237 | return this._params.colour; 238 | } 239 | } 240 | 241 | 242 | 243 | class TerrainChunkRebuilder { 244 | constructor(params) { 245 | this._pool = {}; 246 | this._params = params; 247 | this._Reset(); 248 | } 249 | 250 | AllocateChunk(params) { 251 | const w = params.width; 252 | 253 | if (!(w in this._pool)) { 254 | this._pool[w] = []; 255 | } 256 | 257 | let c = null; 258 | if (this._pool[w].length > 0) { 259 | c = this._pool[w].pop(); 260 | c._params = params; 261 | } else { 262 | c = new terrain_chunk.TerrainChunk(params); 263 | } 264 | 265 | c.Hide(); 266 | 267 | this._queued.push(c); 268 | 269 | return c; 270 | } 271 | 272 | _RecycleChunks(chunks) { 273 | for (let c of chunks) { 274 | if (!(c.chunk._params.width in this._pool)) { 275 | this._pool[c.chunk._params.width] = []; 276 | } 277 | 278 | c.chunk.Destroy(); 279 | } 280 | } 281 | 282 | _Reset() { 283 | this._active = null; 284 | this._queued = []; 285 | this._old = []; 286 | this._new = []; 287 | } 288 | 289 | get Busy() { 290 | return this._active || this._queued.length > 0; 291 | } 292 | 293 | Rebuild(chunks) { 294 | if (this.Busy) { 295 | return; 296 | } 297 | for (let k in chunks) { 298 | this._queued.push(chunks[k].chunk); 299 | } 300 | } 301 | 302 | Update() { 303 | if (this._active) { 304 | const r = this._active.next(); 305 | if (r.done) { 306 | this._active = null; 307 | } 308 | } else { 309 | const b = this._queued.pop(); 310 | if (b) { 311 | this._active = b._Rebuild(); 312 | this._new.push(b); 313 | } 314 | } 315 | 316 | if (this._active) { 317 | return; 318 | } 319 | 320 | if (!this._queued.length) { 321 | this._RecycleChunks(this._old); 322 | for (let b of this._new) { 323 | b.Show(); 324 | } 325 | this._Reset(); 326 | } 327 | } 328 | } 329 | 330 | class TerrainChunkManager { 331 | constructor(params) { 332 | this._Init(params); 333 | } 334 | 335 | _Init(params) { 336 | this._params = params; 337 | 338 | const loader = new THREE.TextureLoader(); 339 | 340 | const noiseTexture = loader.load('./resources/simplex-noise.png'); 341 | noiseTexture.wrapS = THREE.RepeatWrapping; 342 | noiseTexture.wrapT = THREE.RepeatWrapping; 343 | 344 | const diffuse = new textures.TextureAtlas(params); 345 | diffuse.Load('diffuse', [ 346 | './resources/dirt_01_diffuse-1024.png', 347 | './resources/grass1-albedo3-1024.png', 348 | './resources/sandyground-albedo-1024.png', 349 | './resources/worn-bumpy-rock-albedo-1024.png', 350 | './resources/rock-snow-ice-albedo-1024.png', 351 | './resources/snow-packed-albedo-1024.png', 352 | './resources/rough-wet-cobble-albedo-1024.png', 353 | './resources/sandy-rocks1-albedo-1024.png', 354 | ]); 355 | diffuse.onLoad = () => { 356 | this._material.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas; 357 | }; 358 | 359 | const normal = new textures.TextureAtlas(params); 360 | normal.Load('normal', [ 361 | './resources/dirt_01_normal-1024.jpg', 362 | './resources/grass1-normal-1024.jpg', 363 | './resources/sandyground-normal-1024.jpg', 364 | './resources/worn-bumpy-rock-normal-1024.jpg', 365 | './resources/rock-snow-ice-normal-1024.jpg', 366 | './resources/snow-packed-normal-1024.jpg', 367 | './resources/rough-wet-cobble-normal-1024.jpg', 368 | './resources/sandy-rocks1-normal-1024.jpg', 369 | ]); 370 | normal.onLoad = () => { 371 | this._material.uniforms.normalMap.value = normal.Info['normal'].atlas; 372 | }; 373 | 374 | this._material = new THREE.MeshStandardMaterial({ 375 | wireframe: false, 376 | wireframeLinewidth: 1, 377 | color: 0xFFFFFF, 378 | side: THREE.FrontSide, 379 | vertexColors: THREE.VertexColors, 380 | // normalMap: texture, 381 | }); 382 | 383 | this._material = new THREE.RawShaderMaterial({ 384 | uniforms: { 385 | diffuseMap: { 386 | }, 387 | normalMap: { 388 | }, 389 | noiseMap: { 390 | value: noiseTexture 391 | }, 392 | }, 393 | vertexShader: terrain_shader.VS, 394 | fragmentShader: terrain_shader.PS, 395 | side: THREE.FrontSide 396 | }); 397 | 398 | this._builder = new TerrainChunkRebuilder(); 399 | 400 | this._InitNoise(params); 401 | this._InitBiomes(params); 402 | this._InitTerrain(params); 403 | } 404 | 405 | _InitNoise(params) { 406 | params.guiParams.noise = { 407 | octaves: 10, 408 | persistence: 0.5, 409 | lacunarity: 1.6, 410 | exponentiation: 7.5, 411 | height: 900.0, 412 | scale: 1800.0, 413 | seed: 1 414 | }; 415 | 416 | const onNoiseChanged = () => { 417 | this._builder.Rebuild(this._chunks); 418 | }; 419 | 420 | const noiseRollup = params.gui.addFolder('Terrain.Noise'); 421 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange( 422 | onNoiseChanged); 423 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange( 424 | onNoiseChanged); 425 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange( 426 | onNoiseChanged); 427 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange( 428 | onNoiseChanged); 429 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange( 430 | onNoiseChanged); 431 | noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange( 432 | onNoiseChanged); 433 | 434 | this._noise = new noise.Noise(params.guiParams.noise); 435 | 436 | params.guiParams.heightmap = { 437 | height: 16, 438 | }; 439 | 440 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap'); 441 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange( 442 | onNoiseChanged); 443 | } 444 | 445 | _InitBiomes(params) { 446 | params.guiParams.biomes = { 447 | octaves: 2, 448 | persistence: 0.5, 449 | lacunarity: 2.0, 450 | scale: 2048.0, 451 | noiseType: 'simplex', 452 | seed: 2, 453 | exponentiation: 1, 454 | height: 1.0 455 | }; 456 | 457 | const onNoiseChanged = () => { 458 | this._builder.Rebuild(this._chunks); 459 | }; 460 | 461 | const noiseRollup = params.gui.addFolder('Terrain.Biomes'); 462 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange( 463 | onNoiseChanged); 464 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange( 465 | onNoiseChanged); 466 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange( 467 | onNoiseChanged); 468 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange( 469 | onNoiseChanged); 470 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange( 471 | onNoiseChanged); 472 | 473 | this._biomes = new noise.Noise(params.guiParams.biomes); 474 | 475 | const colourParams = { 476 | octaves: 1, 477 | persistence: 0.5, 478 | lacunarity: 2.0, 479 | exponentiation: 1.0, 480 | scale: 256.0, 481 | noiseType: 'simplex', 482 | seed: 2, 483 | height: 1.0, 484 | }; 485 | this._colourNoise = new noise.Noise(colourParams); 486 | } 487 | 488 | _InitTerrain(params) { 489 | params.guiParams.terrain= { 490 | wireframe: false, 491 | }; 492 | 493 | this._groups = [...new Array(6)].map(_ => new THREE.Group()); 494 | params.scene.add(...this._groups); 495 | 496 | const terrainRollup = params.gui.addFolder('Terrain'); 497 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => { 498 | for (let k in this._chunks) { 499 | this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe; 500 | } 501 | }); 502 | 503 | this._chunks = {}; 504 | this._params = params; 505 | } 506 | 507 | _CellIndex(p) { 508 | const xp = p.x + _MIN_CELL_SIZE * 0.5; 509 | const yp = p.z + _MIN_CELL_SIZE * 0.5; 510 | const x = Math.floor(xp / _MIN_CELL_SIZE); 511 | const z = Math.floor(yp / _MIN_CELL_SIZE); 512 | return [x, z]; 513 | } 514 | 515 | _CreateTerrainChunk(group, offset, width, resolution) { 516 | const params = { 517 | group: group, 518 | material: this._material, 519 | width: width, 520 | offset: offset, 521 | radius: _PLANET_RADIUS, 522 | resolution: resolution, 523 | biomeGenerator: this._biomes, 524 | colourGenerator: new TextureSplatter({biomeGenerator: this._biomes, colourNoise: this._colourNoise}), 525 | heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)], 526 | }; 527 | 528 | return this._builder.AllocateChunk(params); 529 | } 530 | 531 | Update(_) { 532 | this._builder.Update(); 533 | if (!this._builder.Busy) { 534 | this._UpdateVisibleChunks_Quadtree(); 535 | } 536 | } 537 | 538 | _UpdateVisibleChunks_Quadtree() { 539 | function _Key(c) { 540 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']'; 541 | } 542 | 543 | const q = new quadtree.CubeQuadTree({ 544 | radius: _PLANET_RADIUS, 545 | min_node_size: _MIN_CELL_SIZE, 546 | }); 547 | q.Insert(this._params.camera.position); 548 | 549 | const sides = q.GetChildren(); 550 | 551 | let newTerrainChunks = {}; 552 | const center = new THREE.Vector3(); 553 | const dimensions = new THREE.Vector3(); 554 | for (let i = 0; i < sides.length; i++) { 555 | this._groups[i].matrix = sides[i].transform; 556 | this._groups[i].matrixAutoUpdate = false; 557 | for (let c of sides[i].children) { 558 | c.bounds.getCenter(center); 559 | c.bounds.getSize(dimensions); 560 | 561 | const child = { 562 | index: i, 563 | group: this._groups[i], 564 | position: [center.x, center.y, center.z], 565 | bounds: c.bounds, 566 | size: dimensions.x, 567 | }; 568 | 569 | const k = _Key(child); 570 | newTerrainChunks[k] = child; 571 | } 572 | } 573 | 574 | const intersection = utils.DictIntersection(this._chunks, newTerrainChunks); 575 | const difference = utils.DictDifference(newTerrainChunks, this._chunks); 576 | const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks)); 577 | 578 | this._builder._old.push(...recycle); 579 | 580 | newTerrainChunks = intersection; 581 | 582 | for (let k in difference) { 583 | const [xp, yp, zp] = difference[k].position; 584 | 585 | const offset = new THREE.Vector3(xp, yp, zp); 586 | newTerrainChunks[k] = { 587 | position: [xp, zp], 588 | chunk: this._CreateTerrainChunk( 589 | difference[k].group, offset, difference[k].size, _MIN_CELL_RESOLUTION), 590 | }; 591 | } 592 | 593 | this._chunks = newTerrainChunks; 594 | } 595 | } 596 | 597 | return { 598 | TerrainChunkManager: TerrainChunkManager 599 | } 600 | })(); 601 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------