├── LICENSE ├── base.css ├── demo.html ├── index.html ├── resources ├── heightmap-hi.png ├── heightmap-simondev.jpg ├── heightmap-test.jpg └── waternormals.jpg └── src ├── controls.js ├── demo.js ├── game.js ├── graphics.js ├── main.js ├── math.js ├── noise.js ├── quadtree.js ├── sky.js ├── spline.js ├── terrain.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 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Noise 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Procedural Terrain 5 | 6 | 7 | 8 | 9 |
10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/heightmap-hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part3/25f3563a0702d70eecb40266f578757c4f88eb5b/resources/heightmap-hi.png -------------------------------------------------------------------------------- /resources/heightmap-simondev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part3/25f3563a0702d70eecb40266f578757c4f88eb5b/resources/heightmap-simondev.jpg -------------------------------------------------------------------------------- /resources/heightmap-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part3/25f3563a0702d70eecb40266f578757c4f88eb5b/resources/heightmap-test.jpg -------------------------------------------------------------------------------- /resources/waternormals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part3/25f3563a0702d70eecb40266f578757c4f88eb5b/resources/waternormals.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 | 4 | 5 | export const controls = (function() { 6 | return { 7 | // FPSControls was adapted heavily from a threejs example. Movement control 8 | // and collision detection was completely rewritten, but credit to original 9 | // class for the setup code. 10 | FPSControls: class { 11 | constructor(params) { 12 | this._cells = params.cells; 13 | this._Init(params); 14 | } 15 | 16 | _Init(params) { 17 | this._radius = 2; 18 | this._enabled = false; 19 | this._move = { 20 | forward: false, 21 | backward: false, 22 | left: false, 23 | right: false, 24 | up: false, 25 | down: false, 26 | }; 27 | this._standing = true; 28 | this._velocity = new THREE.Vector3(0, 0, 0); 29 | this._decceleration = new THREE.Vector3(-10, -10, -10); 30 | this._acceleration = new THREE.Vector3(250, 100, 250); 31 | 32 | this._SetupPointerLock(); 33 | 34 | this._controls = new PointerLockControls( 35 | params.camera, document.body); 36 | params.scene.add(this._controls.getObject()); 37 | 38 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false); 39 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false); 40 | } 41 | 42 | _onKeyDown(event) { 43 | switch (event.keyCode) { 44 | case 38: // up 45 | case 87: // w 46 | this._move.forward = true; 47 | break; 48 | case 37: // left 49 | case 65: // a 50 | this._move.left = true; 51 | break; 52 | case 40: // down 53 | case 83: // s 54 | this._move.backward = true; 55 | break; 56 | case 39: // right 57 | case 68: // d 58 | this._move.right = true; 59 | break; 60 | case 33: // PG_UP 61 | this._move.up = true; 62 | break; 63 | case 34: // PG_DOWN 64 | this._move.down = true; 65 | break; 66 | } 67 | } 68 | 69 | _onKeyUp(event) { 70 | switch(event.keyCode) { 71 | case 38: // up 72 | case 87: // w 73 | this._move.forward = false; 74 | break; 75 | case 37: // left 76 | case 65: // a 77 | this._move.left = false; 78 | break; 79 | case 40: // down 80 | case 83: // s 81 | this._move.backward = false; 82 | break; 83 | case 39: // right 84 | case 68: // d 85 | this._move.right = false; 86 | break; 87 | case 33: // PG_UP 88 | this._move.up = false; 89 | break; 90 | case 34: // PG_DOWN 91 | this._move.down = false; 92 | break; 93 | } 94 | } 95 | 96 | _SetupPointerLock() { 97 | const hasPointerLock = ( 98 | 'pointerLockElement' in document || 99 | 'mozPointerLockElement' in document || 100 | 'webkitPointerLockElement' in document); 101 | if (hasPointerLock) { 102 | const lockChange = (event) => { 103 | if (document.pointerLockElement === document.body || 104 | document.mozPointerLockElement === document.body || 105 | document.webkitPointerLockElement === document.body ) { 106 | this._enabled = true; 107 | this._controls.enabled = true; 108 | } else { 109 | this._controls.enabled = false; 110 | } 111 | }; 112 | const lockError = (event) => { 113 | console.log(event); 114 | }; 115 | 116 | document.addEventListener('pointerlockchange', lockChange, false); 117 | document.addEventListener('webkitpointerlockchange', lockChange, false); 118 | document.addEventListener('mozpointerlockchange', lockChange, false); 119 | document.addEventListener('pointerlockerror', lockError, false); 120 | document.addEventListener('mozpointerlockerror', lockError, false); 121 | document.addEventListener('webkitpointerlockerror', lockError, false); 122 | 123 | document.getElementById('target').addEventListener('click', (event) => { 124 | document.body.requestPointerLock = ( 125 | document.body.requestPointerLock || 126 | document.body.mozRequestPointerLock || 127 | document.body.webkitRequestPointerLock); 128 | 129 | if (/Firefox/i.test(navigator.userAgent)) { 130 | const fullScreenChange = (event) => { 131 | if (document.fullscreenElement === document.body || 132 | document.mozFullscreenElement === document.body || 133 | document.mozFullScreenElement === document.body) { 134 | document.removeEventListener('fullscreenchange', fullScreenChange); 135 | document.removeEventListener('mozfullscreenchange', fullScreenChange); 136 | document.body.requestPointerLock(); 137 | } 138 | }; 139 | document.addEventListener( 140 | 'fullscreenchange', fullScreenChange, false); 141 | document.addEventListener( 142 | 'mozfullscreenchange', fullScreenChange, false); 143 | document.body.requestFullscreen = ( 144 | document.body.requestFullscreen || 145 | document.body.mozRequestFullscreen || 146 | document.body.mozRequestFullScreen || 147 | document.body.webkitRequestFullscreen); 148 | document.body.requestFullscreen(); 149 | } else { 150 | document.body.requestPointerLock(); 151 | } 152 | }, false); 153 | } 154 | } 155 | 156 | _FindIntersections(boxes, position) { 157 | const sphere = new THREE.Sphere(position, this._radius); 158 | 159 | const intersections = boxes.filter(b => { 160 | return sphere.intersectsBox(b); 161 | }); 162 | 163 | return intersections; 164 | } 165 | 166 | Update(timeInSeconds) { 167 | if (!this._enabled) { 168 | return; 169 | } 170 | 171 | const frameDecceleration = new THREE.Vector3( 172 | this._velocity.x * this._decceleration.x, 173 | this._velocity.y * this._decceleration.y, 174 | this._velocity.z * this._decceleration.z 175 | ); 176 | frameDecceleration.multiplyScalar(timeInSeconds); 177 | 178 | this._velocity.add(frameDecceleration); 179 | 180 | if (this._move.forward) { 181 | this._velocity.z -= this._acceleration.z * timeInSeconds; 182 | } 183 | if (this._move.backward) { 184 | this._velocity.z += this._acceleration.z * timeInSeconds; 185 | } 186 | if (this._move.left) { 187 | this._velocity.x -= this._acceleration.x * timeInSeconds; 188 | } 189 | if (this._move.right) { 190 | this._velocity.x += this._acceleration.x * timeInSeconds; 191 | } 192 | if (this._move.up) { 193 | this._velocity.y += this._acceleration.y * timeInSeconds; 194 | } 195 | if (this._move.down) { 196 | this._velocity.y -= this._acceleration.y * timeInSeconds; 197 | } 198 | 199 | const controlObject = this._controls.getObject(); 200 | 201 | const oldPosition = new THREE.Vector3(); 202 | oldPosition.copy(controlObject.position); 203 | 204 | const forward = new THREE.Vector3(0, 0, 1); 205 | forward.applyQuaternion(controlObject.quaternion); 206 | forward.y = 0; 207 | forward.normalize(); 208 | 209 | const updown = new THREE.Vector3(0, 1, 0); 210 | 211 | const sideways = new THREE.Vector3(1, 0, 0); 212 | sideways.applyQuaternion(controlObject.quaternion); 213 | sideways.normalize(); 214 | 215 | sideways.multiplyScalar(this._velocity.x * timeInSeconds); 216 | updown.multiplyScalar(this._velocity.y * timeInSeconds); 217 | forward.multiplyScalar(this._velocity.z * timeInSeconds); 218 | 219 | controlObject.position.add(forward); 220 | controlObject.position.add(sideways); 221 | controlObject.position.add(updown); 222 | 223 | oldPosition.copy(controlObject.position); 224 | } 225 | } 226 | }; 227 | })(); 228 | -------------------------------------------------------------------------------- /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 | this._threejs = new THREE.WebGLRenderer({ 40 | antialias: true, 41 | }); 42 | this._threejs.setPixelRatio(window.devicePixelRatio); 43 | this._threejs.setSize(window.innerWidth, window.innerHeight); 44 | 45 | const target = document.getElementById('target'); 46 | target.appendChild(this._threejs.domElement); 47 | 48 | this._stats = new Stats(); 49 | //target.appendChild(this._stats.dom); 50 | 51 | window.addEventListener('resize', () => { 52 | this._OnWindowResize(); 53 | }, false); 54 | 55 | const fov = 60; 56 | const aspect = 1920 / 1080; 57 | const near = 1; 58 | const far = 25000.0; 59 | this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 60 | this._camera.position.set(75, 20, 0); 61 | 62 | this._scene = new THREE.Scene(); 63 | this._scene.background = new THREE.Color(0xaaaaaa); 64 | 65 | this._CreateLights(); 66 | 67 | return true; 68 | } 69 | 70 | _CreateLights() { 71 | let light = new THREE.DirectionalLight(0x808080, 1, 100); 72 | light.position.set(-100, 100, -100); 73 | light.target.position.set(0, 0, 0); 74 | light.castShadow = false; 75 | this._scene.add(light); 76 | 77 | light = new THREE.DirectionalLight(0x404040, 1.5, 100); 78 | light.position.set(100, 100, -100); 79 | light.target.position.set(0, 0, 0); 80 | light.castShadow = false; 81 | this._scene.add(light); 82 | } 83 | 84 | _OnWindowResize() { 85 | this._camera.aspect = window.innerWidth / window.innerHeight; 86 | this._camera.updateProjectionMatrix(); 87 | this._threejs.setSize(window.innerWidth, window.innerHeight); 88 | } 89 | 90 | get Scene() { 91 | return this._scene; 92 | } 93 | 94 | get Camera() { 95 | return this._camera; 96 | } 97 | 98 | Render(timeInSeconds) { 99 | this._threejs.render(this._scene, this._camera); 100 | this._stats.update(); 101 | } 102 | } 103 | 104 | return { 105 | Graphics: _Graphics, 106 | GetPixel: _GetPixel, 107 | GetImageData: _GetImageData, 108 | }; 109 | })(); 110 | -------------------------------------------------------------------------------- /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 | 8 | 9 | let _APP = null; 10 | 11 | 12 | 13 | class ProceduralTerrain_Demo extends game.Game { 14 | constructor() { 15 | super(); 16 | } 17 | 18 | _OnInitialize() { 19 | this._CreateGUI(); 20 | 21 | this._userCamera = new THREE.Object3D(); 22 | this._userCamera.position.set(475, 75, 900); 23 | 24 | this._entities['_terrain'] = new terrain.TerrainChunkManager({ 25 | camera: this._userCamera, 26 | scene: this._graphics.Scene, 27 | gui: this._gui, 28 | guiParams: this._guiParams, 29 | }); 30 | 31 | this._entities['_sky'] = new sky.TerrainSky({ 32 | camera: this._graphics.Camera, 33 | scene: this._graphics.Scene, 34 | gui: this._gui, 35 | guiParams: this._guiParams, 36 | }); 37 | 38 | this._entities['_controls'] = new controls.FPSControls( 39 | { 40 | scene: this._graphics.Scene, 41 | camera: this._userCamera 42 | }); 43 | 44 | this._graphics.Camera.position.copy(this._userCamera.position); 45 | 46 | this._LoadBackground(); 47 | } 48 | 49 | _CreateGUI() { 50 | this._guiParams = { 51 | general: { 52 | }, 53 | }; 54 | this._gui = new GUI(); 55 | 56 | const generalRollup = this._gui.addFolder('General'); 57 | this._gui.close(); 58 | } 59 | 60 | _LoadBackground() { 61 | this._graphics.Scene.background = new THREE.Color(0x000000); 62 | } 63 | 64 | _OnStep(_) { 65 | this._graphics._camera.position.copy(this._userCamera.position); 66 | this._graphics._camera.quaternion.copy(this._userCamera.quaternion); 67 | } 68 | } 69 | 70 | 71 | function _Main() { 72 | _APP = new ProceduralTerrain_Demo(); 73 | } 74 | 75 | _Main(); 76 | -------------------------------------------------------------------------------- /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 | 4 | import {math} from './math.js'; 5 | 6 | export const noise = (function() { 7 | 8 | class _PerlinWrapper { 9 | constructor() { 10 | } 11 | 12 | noise2D(x, y) { 13 | return perlin(x, y) * 2.0 - 1.0; 14 | } 15 | } 16 | 17 | class _RandomWrapper { 18 | constructor() { 19 | this._values = {}; 20 | } 21 | 22 | _Rand(x, y) { 23 | const k = x + '.' + y; 24 | if (!(k in this._values)) { 25 | this._values[k] = Math.random() * 2 - 1; 26 | } 27 | return this._values[k]; 28 | } 29 | 30 | noise2D(x, y) { 31 | // Bilinear filter 32 | const x1 = Math.floor(x); 33 | const y1 = Math.floor(y); 34 | const x2 = x1 + 1; 35 | const y2 = y1 + 1; 36 | 37 | const xp = x - x1; 38 | const yp = y - y1; 39 | 40 | const p11 = this._Rand(x1, y1); 41 | const p21 = this._Rand(x2, y1); 42 | const p12 = this._Rand(x1, y2); 43 | const p22 = this._Rand(x2, y2); 44 | 45 | const px1 = math.lerp(xp, p11, p21); 46 | const px2 = math.lerp(xp, p12, p22); 47 | 48 | return math.lerp(yp, px1, px2); 49 | } 50 | } 51 | 52 | class _NoiseGenerator { 53 | constructor(params) { 54 | this._params = params; 55 | this._Init(); 56 | } 57 | 58 | _Init() { 59 | this._noise = { 60 | simplex: new SimplexNoise(this._params.seed), 61 | perlin: new _PerlinWrapper(), 62 | rand: new _RandomWrapper(), 63 | }; 64 | } 65 | 66 | Get(x, y) { 67 | const xs = x / this._params.scale; 68 | const ys = y / this._params.scale; 69 | const noiseFunc = this._noise[this._params.noiseType]; 70 | const G = 2.0 ** (-this._params.persistence); 71 | let amplitude = 1.0; 72 | let frequency = 1.0; 73 | let normalization = 0; 74 | let total = 0; 75 | for (let o = 0; o < this._params.octaves; o++) { 76 | const noiseValue = noiseFunc.noise2D( 77 | xs * frequency, ys * frequency) * 0.5 + 0.5; 78 | total += noiseValue * amplitude; 79 | normalization += amplitude; 80 | amplitude *= G; 81 | frequency *= this._params.lacunarity; 82 | } 83 | total /= normalization; 84 | return Math.pow( 85 | total, this._params.exponentiation) * this._params.height; 86 | } 87 | } 88 | 89 | return { 90 | Noise: _NoiseGenerator 91 | } 92 | })(); 93 | -------------------------------------------------------------------------------- /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 | const _MIN_NODE_SIZE = 500; 7 | 8 | class QuadTree { 9 | constructor(params) { 10 | const b = new THREE.Box2(params.min, params.max); 11 | this._root = { 12 | bounds: b, 13 | children: [], 14 | center: b.getCenter(new THREE.Vector2()), 15 | size: b.getSize(new THREE.Vector2()), 16 | }; 17 | } 18 | 19 | GetChildren() { 20 | const children = []; 21 | this._GetChildren(this._root, children); 22 | return children; 23 | } 24 | 25 | _GetChildren(node, target) { 26 | if (node.children.length == 0) { 27 | target.push(node); 28 | return; 29 | } 30 | 31 | for (let c of node.children) { 32 | this._GetChildren(c, target); 33 | } 34 | } 35 | 36 | Insert(pos) { 37 | this._Insert(this._root, new THREE.Vector2(pos.x, pos.z)); 38 | } 39 | 40 | _Insert(child, pos) { 41 | const distToChild = this._DistanceToChild(child, pos); 42 | 43 | if (distToChild < child.size.x && child.size.x > _MIN_NODE_SIZE) { 44 | child.children = this._CreateChildren(child); 45 | 46 | for (let c of child.children) { 47 | this._Insert(c, pos); 48 | } 49 | } 50 | } 51 | 52 | _DistanceToChild(child, pos) { 53 | return child.center.distanceTo(pos); 54 | } 55 | 56 | _CreateChildren(child) { 57 | const midpoint = child.bounds.getCenter(new THREE.Vector2()); 58 | 59 | // Bottom left 60 | const b1 = new THREE.Box2(child.bounds.min, midpoint); 61 | 62 | // Bottom right 63 | const b2 = new THREE.Box2( 64 | new THREE.Vector2(midpoint.x, child.bounds.min.y), 65 | new THREE.Vector2(child.bounds.max.x, midpoint.y)); 66 | 67 | // Top left 68 | const b3 = new THREE.Box2( 69 | new THREE.Vector2(child.bounds.min.x, midpoint.y), 70 | new THREE.Vector2(midpoint.x, child.bounds.max.y)); 71 | 72 | // Top right 73 | const b4 = new THREE.Box2(midpoint, child.bounds.max); 74 | 75 | const children = [b1, b2, b3, b4].map( 76 | b => { 77 | return { 78 | bounds: b, 79 | children: [], 80 | center: b.getCenter(new THREE.Vector2()), 81 | size: b.getSize(new THREE.Vector2()) 82 | }; 83 | }); 84 | 85 | return children; 86 | } 87 | } 88 | 89 | return { 90 | QuadTree: QuadTree 91 | } 92 | })(); 93 | -------------------------------------------------------------------------------- /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 | 25 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping; 26 | 27 | } ), 28 | alpha: 0.5, 29 | sunDirection: new THREE.Vector3(1, 0, 0), 30 | sunColor: 0xffffff, 31 | waterColor: 0x001e0f, 32 | distortionScale: 0.0, 33 | fog: undefined 34 | } 35 | ); 36 | this._water.rotation.x = - Math.PI / 2; 37 | this._water.position.y = 4; 38 | 39 | this._sky = new Sky(); 40 | this._sky.scale.setScalar(10000); 41 | 42 | this._group = new THREE.Group(); 43 | this._group.add(this._water); 44 | this._group.add(this._sky); 45 | 46 | params.scene.add(this._group); 47 | 48 | params.guiParams.sky = { 49 | turbidity: 10.0, 50 | rayleigh: 2, 51 | mieCoefficient: 0.005, 52 | mieDirectionalG: 0.8, 53 | luminance: 1, 54 | }; 55 | 56 | params.guiParams.sun = { 57 | inclination: 0.31, 58 | azimuth: 0.25, 59 | }; 60 | 61 | const onShaderChange = () => { 62 | for (let k in params.guiParams.sky) { 63 | this._sky.material.uniforms[k].value = params.guiParams.sky[k]; 64 | } 65 | for (let k in params.guiParams.general) { 66 | this._sky.material.uniforms[k].value = params.guiParams.general[k]; 67 | } 68 | }; 69 | 70 | const onSunChange = () => { 71 | var theta = Math.PI * (params.guiParams.sun.inclination - 0.5); 72 | var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5); 73 | 74 | const sunPosition = new THREE.Vector3(); 75 | sunPosition.x = Math.cos(phi); 76 | sunPosition.y = Math.sin(phi) * Math.sin(theta); 77 | sunPosition.z = Math.sin(phi) * Math.cos(theta); 78 | 79 | this._sky.material.uniforms['sunPosition'].value.copy(sunPosition); 80 | this._water.material.uniforms['sunDirection'].value.copy(sunPosition.normalize()); 81 | }; 82 | 83 | const skyRollup = params.gui.addFolder('Sky'); 84 | skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange( 85 | onShaderChange); 86 | skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange( 87 | onShaderChange); 88 | skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange( 89 | onShaderChange); 90 | skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange( 91 | onShaderChange); 92 | skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange( 93 | onShaderChange); 94 | 95 | const sunRollup = params.gui.addFolder('Sun'); 96 | sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange( 97 | onSunChange); 98 | sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange( 99 | onSunChange); 100 | 101 | onShaderChange(); 102 | onSunChange(); 103 | } 104 | 105 | Update(timeInSeconds) { 106 | this._water.material.uniforms['time'].value += timeInSeconds; 107 | 108 | this._group.position.x = this._params.camera.position.x; 109 | this._group.position.z = this._params.camera.position.z; 110 | } 111 | } 112 | 113 | 114 | return { 115 | TerrainSky: TerrainSky 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /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.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 {utils} from './utils.js'; 9 | 10 | 11 | export const terrain = (function() { 12 | 13 | class HeightGenerator { 14 | constructor(generator, position, minRadius, maxRadius) { 15 | this._position = position.clone(); 16 | this._radius = [minRadius, maxRadius]; 17 | this._generator = generator; 18 | } 19 | 20 | Get(x, y) { 21 | const distance = this._position.distanceTo(new THREE.Vector2(x, y)); 22 | let normalization = 1.0 - math.sat( 23 | (distance - this._radius[0]) / (this._radius[1] - this._radius[0])); 24 | normalization = normalization * normalization * (3 - 2 * normalization); 25 | 26 | return [this._generator.Get(x, y), normalization]; 27 | } 28 | } 29 | 30 | 31 | class FixedHeightGenerator { 32 | constructor() {} 33 | 34 | Get() { 35 | return [50, 1]; 36 | } 37 | } 38 | 39 | 40 | class Heightmap { 41 | constructor(params, img) { 42 | this._params = params; 43 | this._data = graphics.GetImageData(img); 44 | } 45 | 46 | Get(x, y) { 47 | const _GetPixelAsFloat = (x, y) => { 48 | const position = (x + this._data.width * y) * 4; 49 | const data = this._data.data; 50 | return data[position] / 255.0; 51 | } 52 | 53 | // Bilinear filter 54 | const offset = new THREE.Vector2(-250, -250); 55 | const dimensions = new THREE.Vector2(500, 500); 56 | 57 | const xf = 1.0 - math.sat((x - offset.x) / dimensions.x); 58 | const yf = math.sat((y - offset.y) / dimensions.y); 59 | const w = this._data.width - 1; 60 | const h = this._data.height - 1; 61 | 62 | const x1 = Math.floor(xf * w); 63 | const y1 = Math.floor(yf * h); 64 | const x2 = math.clamp(x1 + 1, 0, w); 65 | const y2 = math.clamp(y1 + 1, 0, h); 66 | 67 | const xp = xf * w - x1; 68 | const yp = yf * h - y1; 69 | 70 | const p11 = _GetPixelAsFloat(x1, y1); 71 | const p21 = _GetPixelAsFloat(x2, y1); 72 | const p12 = _GetPixelAsFloat(x1, y2); 73 | const p22 = _GetPixelAsFloat(x2, y2); 74 | 75 | const px1 = math.lerp(xp, p11, p21); 76 | const px2 = math.lerp(xp, p12, p22); 77 | 78 | return math.lerp(yp, px1, px2) * this._params.height; 79 | } 80 | } 81 | 82 | const _WHITE = new THREE.Color(0x808080); 83 | const _OCEAN = new THREE.Color(0xd9d592); 84 | const _BEACH = new THREE.Color(0xd9d592); 85 | const _SNOW = new THREE.Color(0xFFFFFF); 86 | const _FOREST_TROPICAL = new THREE.Color(0x4f9f0f); 87 | const _FOREST_TEMPERATE = new THREE.Color(0x2b960e); 88 | const _FOREST_BOREAL = new THREE.Color(0x29c100); 89 | 90 | const _GREEN = new THREE.Color(0x80FF80); 91 | const _RED = new THREE.Color(0xFF8080); 92 | const _BLACK = new THREE.Color(0x000000); 93 | 94 | const _MIN_CELL_SIZE = 500; 95 | const _FIXED_GRID_SIZE = 10; 96 | const _MIN_CELL_RESOLUTION = 64; 97 | 98 | 99 | // Cross-blended Hypsometric Tints 100 | // http://www.shadedrelief.com/hypso/hypso.html 101 | class HyposemetricTints { 102 | constructor(params) { 103 | const _colourLerp = (t, p0, p1) => { 104 | const c = p0.clone(); 105 | 106 | return c.lerpHSL(p1, t); 107 | }; 108 | this._colourSpline = [ 109 | new spline.LinearSpline(_colourLerp), 110 | new spline.LinearSpline(_colourLerp) 111 | ]; 112 | // Arid 113 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d)); 114 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc)); 115 | this._colourSpline[0].AddPoint(1.0, _SNOW); 116 | 117 | // Humid 118 | this._colourSpline[1].AddPoint(0.0, _FOREST_BOREAL); 119 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c)); 120 | this._colourSpline[1].AddPoint(1.0, _SNOW); 121 | 122 | this._params = params; 123 | } 124 | 125 | Get(x, y, z) { 126 | const m = this._params.biomeGenerator.Get(x, z); 127 | const h = y / 100.0; 128 | 129 | if (h < 0.05) { 130 | return _OCEAN; 131 | } 132 | 133 | const c1 = this._colourSpline[0].Get(h); 134 | const c2 = this._colourSpline[1].Get(h); 135 | 136 | return c1.lerpHSL(c2, m); 137 | } 138 | } 139 | 140 | 141 | class FixedColourGenerator { 142 | constructor(params) { 143 | this._params = params; 144 | } 145 | 146 | Get() { 147 | return this._params.colour; 148 | } 149 | } 150 | 151 | 152 | class TerrainChunk { 153 | constructor(params) { 154 | this._params = params; 155 | this._Init(params); 156 | } 157 | 158 | Destroy() { 159 | this._params.group.remove(this._plane); 160 | } 161 | 162 | Hide() { 163 | this._plane.visible = false; 164 | } 165 | 166 | Show() { 167 | this._plane.visible = true; 168 | } 169 | 170 | _Init(params) { 171 | const size = new THREE.Vector3(params.width, 0, params.width); 172 | 173 | this._plane = new THREE.Mesh( 174 | new THREE.PlaneGeometry(size.x, size.z, params.resolution, params.resolution), 175 | params.material); 176 | this._plane.castShadow = false; 177 | this._plane.receiveShadow = true; 178 | this._plane.rotation.x = -Math.PI / 2; 179 | this._params.group.add(this._plane); 180 | } 181 | 182 | _GenerateHeight(v) { 183 | const offset = this._params.offset; 184 | const heightPairs = []; 185 | let normalization = 0; 186 | let z = 0; 187 | for (let gen of this._params.heightGenerators) { 188 | heightPairs.push(gen.Get(v.x + offset.x, -v.y + offset.y)); 189 | normalization += heightPairs[heightPairs.length-1][1]; 190 | } 191 | 192 | if (normalization > 0) { 193 | for (let h of heightPairs) { 194 | z += h[0] * h[1] / normalization; 195 | } 196 | } 197 | 198 | return z; 199 | } 200 | 201 | *_Rebuild() { 202 | const NUM_STEPS = 2000; 203 | const colours = []; 204 | const offset = this._params.offset; 205 | let count = 0; 206 | 207 | for (let v of this._plane.geometry.vertices) { 208 | v.z = this._GenerateHeight(v); 209 | colours.push(this._params.colourGenerator.Get(v.x + offset.x, v.z, -v.y + offset.y)); 210 | 211 | count++; 212 | if (count > NUM_STEPS) { 213 | count = 0; 214 | yield; 215 | } 216 | } 217 | 218 | for (let f of this._plane.geometry.faces) { 219 | const vs = [f.a, f.b, f.c]; 220 | 221 | const vertexColours = []; 222 | for (let v of vs) { 223 | vertexColours.push(colours[v]); 224 | } 225 | f.vertexColors = vertexColours; 226 | 227 | count++; 228 | if (count > NUM_STEPS) { 229 | count = 0; 230 | yield; 231 | } 232 | } 233 | 234 | yield; 235 | this._plane.geometry.elementsNeedUpdate = true; 236 | this._plane.geometry.verticesNeedUpdate = true; 237 | this._plane.geometry.computeVertexNormals(); 238 | this._plane.position.set(offset.x, 0, offset.y); 239 | } 240 | } 241 | 242 | class TerrainChunkRebuilder { 243 | constructor(params) { 244 | this._pool = {}; 245 | this._params = params; 246 | this._Reset(); 247 | } 248 | 249 | AllocateChunk(params) { 250 | const w = params.width; 251 | 252 | if (!(w in this._pool)) { 253 | this._pool[w] = []; 254 | } 255 | 256 | let c = null; 257 | if (this._pool[w].length > 0) { 258 | c = this._pool[w].pop(); 259 | c._params = params; 260 | } else { 261 | c = new TerrainChunk(params); 262 | } 263 | 264 | c.Hide(); 265 | 266 | this._queued.push(c); 267 | 268 | return c; 269 | } 270 | 271 | _RecycleChunks(chunks) { 272 | for (let c of chunks) { 273 | if (!(c.chunk._params.width in this._pool)) { 274 | this._pool[c.chunk._params.width] = []; 275 | } 276 | 277 | c.chunk.Hide(); 278 | this._pool[c.chunk._params.width].push(c.chunk); 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; 291 | } 292 | 293 | Update2() { 294 | for (let b of this._queued) { 295 | b._Rebuild().next(); 296 | this._new.push(b); 297 | } 298 | this._queued = []; 299 | 300 | if (this._active) { 301 | return; 302 | } 303 | 304 | if (!this._queued.length) { 305 | this._RecycleChunks(this._old); 306 | for (let b of this._new) { 307 | b.Show(); 308 | } 309 | this._Reset(); 310 | } 311 | } 312 | 313 | Update() { 314 | if (this._active) { 315 | const r = this._active.next(); 316 | if (r.done) { 317 | this._active = null; 318 | } 319 | } else { 320 | const b = this._queued.pop(); 321 | if (b) { 322 | this._active = b._Rebuild(); 323 | this._new.push(b); 324 | } 325 | } 326 | 327 | if (this._active) { 328 | return; 329 | } 330 | 331 | if (!this._queued.length) { 332 | this._RecycleChunks(this._old); 333 | for (let b of this._new) { 334 | b.Show(); 335 | } 336 | this._Reset(); 337 | } 338 | } 339 | } 340 | 341 | class TerrainChunkManager { 342 | constructor(params) { 343 | this._Init(params); 344 | } 345 | 346 | _Init(params) { 347 | this._params = params; 348 | 349 | this._material = new THREE.MeshStandardMaterial({ 350 | wireframe: false, 351 | wireframeLinewidth: 1, 352 | color: 0xFFFFFF, 353 | side: THREE.FrontSide, 354 | vertexColors: THREE.VertexColors, 355 | }); 356 | this._builder = new TerrainChunkRebuilder(); 357 | 358 | this._InitNoise(params); 359 | this._InitBiomes(params); 360 | this._InitTerrain(params); 361 | } 362 | 363 | _InitNoise(params) { 364 | params.guiParams.noise = { 365 | octaves: 6, 366 | persistence: 0.707, 367 | lacunarity: 1.8, 368 | exponentiation: 4.5, 369 | height: 300.0, 370 | scale: 1100.0, 371 | noiseType: 'simplex', 372 | seed: 1 373 | }; 374 | 375 | const onNoiseChanged = () => { 376 | for (let k in this._chunks) { 377 | this._chunks[k].chunk.Rebuild(); 378 | } 379 | }; 380 | 381 | const noiseRollup = params.gui.addFolder('Terrain.Noise'); 382 | noiseRollup.add(params.guiParams.noise, "noiseType", ['simplex', 'perlin', 'rand']).onChange( 383 | onNoiseChanged); 384 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange( 385 | onNoiseChanged); 386 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange( 387 | onNoiseChanged); 388 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange( 389 | onNoiseChanged); 390 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange( 391 | onNoiseChanged); 392 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange( 393 | onNoiseChanged); 394 | noiseRollup.add(params.guiParams.noise, "height", 0, 512).onChange( 395 | onNoiseChanged); 396 | 397 | this._noise = new noise.Noise(params.guiParams.noise); 398 | 399 | params.guiParams.heightmap = { 400 | height: 16, 401 | }; 402 | 403 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap'); 404 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange( 405 | onNoiseChanged); 406 | } 407 | 408 | _InitBiomes(params) { 409 | params.guiParams.biomes = { 410 | octaves: 2, 411 | persistence: 0.5, 412 | lacunarity: 2.0, 413 | exponentiation: 3.9, 414 | scale: 2048.0, 415 | noiseType: 'simplex', 416 | seed: 2, 417 | exponentiation: 1, 418 | height: 1 419 | }; 420 | 421 | const onNoiseChanged = () => { 422 | for (let k in this._chunks) { 423 | this._chunks[k].chunk.Rebuild(); 424 | } 425 | }; 426 | 427 | const noiseRollup = params.gui.addFolder('Terrain.Biomes'); 428 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange( 429 | onNoiseChanged); 430 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange( 431 | onNoiseChanged); 432 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange( 433 | onNoiseChanged); 434 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange( 435 | onNoiseChanged); 436 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange( 437 | onNoiseChanged); 438 | 439 | this._biomes = new noise.Noise(params.guiParams.biomes); 440 | } 441 | 442 | _InitTerrain(params) { 443 | params.guiParams.terrain= { 444 | wireframe: false, 445 | }; 446 | 447 | this._group = new THREE.Group() 448 | params.scene.add(this._group); 449 | 450 | const terrainRollup = params.gui.addFolder('Terrain'); 451 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => { 452 | for (let k in this._chunks) { 453 | this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe; 454 | } 455 | }); 456 | 457 | this._chunks = {}; 458 | this._params = params; 459 | } 460 | 461 | _CellIndex(p) { 462 | const xp = p.x + _MIN_CELL_SIZE * 0.5; 463 | const yp = p.z + _MIN_CELL_SIZE * 0.5; 464 | const x = Math.floor(xp / _MIN_CELL_SIZE); 465 | const z = Math.floor(yp / _MIN_CELL_SIZE); 466 | return [x, z]; 467 | } 468 | 469 | _CreateTerrainChunk(offset, width) { 470 | const params = { 471 | group: this._group, 472 | material: this._material, 473 | width: width, 474 | offset: new THREE.Vector3(offset.x, offset.y, 0), 475 | resolution: _MIN_CELL_RESOLUTION, 476 | biomeGenerator: this._biomes, 477 | colourGenerator: new HyposemetricTints({biomeGenerator: this._biomes}), 478 | heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)], 479 | }; 480 | 481 | return this._builder.AllocateChunk(params); 482 | } 483 | 484 | Update(_) { 485 | this._builder.Update(); 486 | if (!this._builder.Busy) { 487 | this._UpdateVisibleChunks_Quadtree(); 488 | } 489 | } 490 | 491 | _UpdateVisibleChunks_Quadtree() { 492 | function _Key(c) { 493 | return c.position[0] + '/' + c.position[1] + ' [' + c.dimensions[0] + ']'; 494 | } 495 | 496 | const q = new quadtree.QuadTree({ 497 | min: new THREE.Vector2(-32000, -32000), 498 | max: new THREE.Vector2(32000, 32000), 499 | }); 500 | q.Insert(this._params.camera.position); 501 | 502 | const children = q.GetChildren(); 503 | 504 | let newTerrainChunks = {}; 505 | const center = new THREE.Vector2(); 506 | const dimensions = new THREE.Vector2(); 507 | for (let c of children) { 508 | c.bounds.getCenter(center); 509 | c.bounds.getSize(dimensions); 510 | 511 | const child = { 512 | position: [center.x, center.y], 513 | bounds: c.bounds, 514 | dimensions: [dimensions.x, dimensions.y], 515 | }; 516 | 517 | const k = _Key(child); 518 | newTerrainChunks[k] = child; 519 | } 520 | 521 | const intersection = utils.DictIntersection(this._chunks, newTerrainChunks); 522 | const difference = utils.DictDifference(newTerrainChunks, this._chunks); 523 | const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks)); 524 | 525 | this._builder._old.push(...recycle); 526 | 527 | newTerrainChunks = intersection; 528 | 529 | for (let k in difference) { 530 | const [xp, zp] = difference[k].position; 531 | 532 | const offset = new THREE.Vector2(xp, zp); 533 | newTerrainChunks[k] = { 534 | position: [xp, zp], 535 | chunk: this._CreateTerrainChunk(offset, difference[k].dimensions[0]), 536 | }; 537 | } 538 | 539 | this._chunks = newTerrainChunks; 540 | } 541 | 542 | _UpdateVisibleChunks_FixedGrid() { 543 | function _Key(xc, zc) { 544 | return xc + '/' + zc; 545 | } 546 | 547 | const [xc, zc] = this._CellIndex(this._params.camera.position); 548 | 549 | const keys = {}; 550 | 551 | for (let x = -_FIXED_GRID_SIZE; x <= _FIXED_GRID_SIZE; x++) { 552 | for (let z = -_FIXED_GRID_SIZE; z <= _FIXED_GRID_SIZE; z++) { 553 | const k = _Key(x + xc, z + zc); 554 | keys[k] = { 555 | position: [x + xc, z + zc] 556 | }; 557 | } 558 | } 559 | 560 | const difference = utils.DictDifference(keys, this._chunks); 561 | const recycle = Object.values(utils.DictDifference(this._chunks, keys)); 562 | 563 | for (let k in difference) { 564 | if (k in this._chunks) { 565 | continue; 566 | } 567 | 568 | const [xp, zp] = difference[k].position; 569 | 570 | const offset = new THREE.Vector2(xp * _MIN_CELL_SIZE, zp * _MIN_CELL_SIZE); 571 | this._chunks[k] = { 572 | position: [xc, zc], 573 | chunk: this._CreateTerrainChunk(offset, _MIN_CELL_SIZE), 574 | }; 575 | } 576 | } 577 | 578 | _UpdateVisibleChunks_Single() { 579 | function _Key(xc, zc) { 580 | return xc + '/' + zc; 581 | } 582 | 583 | // Check the camera's position. 584 | const [xc, zc] = this._CellIndex(this._params.camera.position); 585 | const newChunkKey = _Key(xc, zc); 586 | 587 | // We're still in the bounds of the previous chunk of terrain. 588 | if (newChunkKey in this._chunks) { 589 | return; 590 | } 591 | 592 | // Create a new chunk of terrain. 593 | const offset = new THREE.Vector2(xc * _MIN_CELL_SIZE, zc * _MIN_CELL_SIZE); 594 | this._chunks[newChunkKey] = { 595 | position: [xc, zc], 596 | chunk: this._CreateTerrainChunk(offset, _MIN_CELL_SIZE), 597 | }; 598 | } 599 | } 600 | 601 | return { 602 | TerrainChunkManager: TerrainChunkManager 603 | } 604 | })(); 605 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------