├── LICENSE ├── base.css ├── index.html ├── resources ├── heightmap-hi.png └── heightmap-simondev.jpg └── src ├── controls.js ├── game.js ├── graphics.js ├── main.js ├── math.js ├── noise.js ├── perlin-noise.js ├── quadtree.js ├── spline.js ├── terrain-chunk.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/heightmap-hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part4/97ffc7ecb1624b274416e071fd8926165ca59d7a/resources/heightmap-hi.png -------------------------------------------------------------------------------- /resources/heightmap-simondev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part4/97ffc7ecb1624b274416e071fd8926165ca59d7a/resources/heightmap-simondev.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(250, 100, 250); 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: 250, 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/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 = 100000.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(0xFFFFFF, 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, 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 | light = new THREE.DirectionalLight(0x404040, 1, 100); 84 | light.position.set(100, 100, -100); 85 | light.target.position.set(0, 0, 0); 86 | light.castShadow = false; 87 | this._scene.add(light); 88 | 89 | light = new THREE.DirectionalLight(0x101040, 1, 100); 90 | light.position.set(100, -100, 100); 91 | light.target.position.set(0, 0, 0); 92 | light.castShadow = false; 93 | this._scene.add(light); 94 | } 95 | 96 | _OnWindowResize() { 97 | this._camera.aspect = window.innerWidth / window.innerHeight; 98 | this._camera.updateProjectionMatrix(); 99 | this._threejs.setSize(window.innerWidth, window.innerHeight); 100 | } 101 | 102 | get Scene() { 103 | return this._scene; 104 | } 105 | 106 | get Camera() { 107 | return this._camera; 108 | } 109 | 110 | Render(timeInSeconds) { 111 | this._threejs.render(this._scene, this._camera); 112 | this._stats.update(); 113 | } 114 | } 115 | 116 | return { 117 | Graphics: _Graphics, 118 | GetPixel: _GetPixel, 119 | GetImageData: _GetImageData, 120 | }; 121 | })(); 122 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | import {GUI} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/dat.gui.module.js'; 3 | import {controls} from './controls.js'; 4 | import {game} from './game.js'; 5 | import {terrain} from './terrain.js'; 6 | import {textures} from './textures.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._textures = new textures.TextureAtlas(this); 20 | this._textures.onLoad = () => {}; 21 | this._CreateGUI(); 22 | 23 | this._userCamera = new THREE.Object3D(); 24 | this._userCamera.position.set(4100, 0, 0); 25 | this._graphics.Camera.position.set(7000, 7000, 7000); 26 | 27 | this._entities['_terrain'] = new terrain.TerrainChunkManager({ 28 | camera: this._userCamera, 29 | scene: this._graphics.Scene, 30 | gui: this._gui, 31 | guiParams: this._guiParams, 32 | }); 33 | 34 | this._entities['_controls'] = new controls.OrbitControls({ 35 | camera: this._graphics.Camera, 36 | scene: this._graphics.Scene, 37 | domElement: this._graphics._threejs.domElement, 38 | gui: this._gui, 39 | guiParams: this._guiParams, 40 | }); 41 | 42 | this._focusMesh = new THREE.Mesh( 43 | new THREE.SphereGeometry(25, 32, 32), 44 | new THREE.MeshBasicMaterial({ 45 | color: 0xFFFFFF 46 | })); 47 | this._focusMesh.castShadow = true; 48 | this._focusMesh.receiveShadow = true; 49 | this._graphics.Scene.add(this._focusMesh); 50 | 51 | this._totalTime = 0; 52 | 53 | this._LoadBackground(); 54 | } 55 | 56 | _CreateGUI() { 57 | this._guiParams = { 58 | general: { 59 | }, 60 | }; 61 | this._gui = new GUI(); 62 | 63 | const generalRollup = this._gui.addFolder('General'); 64 | this._gui.close(); 65 | } 66 | 67 | _LoadBackground() { 68 | this._graphics.Scene.background = new THREE.Color(0x000000); 69 | } 70 | 71 | _OnStep(timeInSeconds) { 72 | this._totalTime += timeInSeconds; 73 | 74 | const x = Math.cos(this._totalTime * 0.025) * 4100; 75 | const y = Math.sin(this._totalTime * 0.025) * 4100; 76 | this._userCamera.position.set(x, 0, y); 77 | 78 | this._focusMesh.position.copy(this._userCamera.position); 79 | } 80 | } 81 | 82 | 83 | function _Main() { 84 | _APP = new ProceduralTerrain_Demo(); 85 | } 86 | 87 | _Main(); 88 | -------------------------------------------------------------------------------- /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 | total += noiseValue * amplitude; 34 | normalization += amplitude; 35 | amplitude *= G; 36 | frequency *= this._params.lacunarity; 37 | } 38 | total /= normalization; 39 | return Math.pow( 40 | total, this._params.exponentiation) * this._params.height; 41 | } 42 | } 43 | 44 | return { 45 | Noise: _NoiseGenerator 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /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/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 indices = []; 57 | 58 | const localToWorld = this._params.group.matrix; 59 | const resolution = this._params.resolution; 60 | const radius = this._params.radius; 61 | const offset = this._params.offset; 62 | const width = this._params.width; 63 | const half = width / 2; 64 | 65 | for (let x = 0; x < resolution + 1; x++) { 66 | const xp = width * x / resolution; 67 | for (let y = 0; y < resolution + 1; y++) { 68 | const yp = width * y / resolution; 69 | 70 | // Compute position 71 | _P.set(xp - half, yp - half, radius); 72 | _P.add(offset); 73 | _P.normalize(); 74 | _D.copy(_P); 75 | _P.multiplyScalar(radius); 76 | _P.z -= radius; 77 | 78 | // Compute a world space position to sample noise 79 | _W.copy(_P); 80 | _W.applyMatrix4(localToWorld); 81 | 82 | const height = this._GenerateHeight(_W); 83 | const color = this._params.colourGenerator.Get(_W.x, _W.y, height); 84 | 85 | // Purturb height along z-vector 86 | _H.copy(_D); 87 | _H.multiplyScalar(height); 88 | _P.add(_H); 89 | 90 | positions.push(_P.x, _P.y, _P.z); 91 | colors.push(color.r, color.g, color.b); 92 | normals.push(_D.x, _D.y, _D.z); 93 | tangents.push(1, 0, 0, 1); 94 | uvs.push(_P.x / 10, _P.y / 10); 95 | } 96 | } 97 | yield; 98 | 99 | for (let i = 0; i < resolution; i++) { 100 | for (let j = 0; j < resolution; j++) { 101 | indices.push( 102 | i * (resolution + 1) + j, 103 | (i + 1) * (resolution + 1) + j + 1, 104 | i * (resolution + 1) + j + 1); 105 | indices.push( 106 | (i + 1) * (resolution + 1) + j, 107 | (i + 1) * (resolution + 1) + j + 1, 108 | i * (resolution + 1) + j); 109 | } 110 | } 111 | yield; 112 | 113 | for (let i = 0, n = indices.length; i < n; i+= 3) { 114 | const i1 = indices[i] * 3; 115 | const i2 = indices[i+1] * 3; 116 | const i3 = indices[i+2] * 3; 117 | 118 | _N1.fromArray(positions, i1); 119 | _N2.fromArray(positions, i2); 120 | _N3.fromArray(positions, i3); 121 | 122 | _D1.subVectors(_N3, _N2); 123 | _D2.subVectors(_N1, _N2); 124 | _D1.cross(_D2); 125 | 126 | normals[i1] += _D1.x; 127 | normals[i2] += _D1.x; 128 | normals[i3] += _D1.x; 129 | 130 | normals[i1+1] += _D1.y; 131 | normals[i2+1] += _D1.y; 132 | normals[i3+1] += _D1.y; 133 | 134 | normals[i1+2] += _D1.z; 135 | normals[i2+2] += _D1.z; 136 | normals[i3+2] += _D1.z; 137 | } 138 | yield; 139 | 140 | for (let i = 0, n = normals.length; i < n; i+=3) { 141 | _N.fromArray(normals, i); 142 | _N.normalize(); 143 | normals[i] = _N.x; 144 | normals[i+1] = _N.y; 145 | normals[i+2] = _N.z; 146 | } 147 | yield; 148 | 149 | this._geometry.setAttribute( 150 | 'position', new THREE.Float32BufferAttribute(positions, 3)); 151 | this._geometry.setAttribute( 152 | 'color', new THREE.Float32BufferAttribute(colors, 3)); 153 | this._geometry.setAttribute( 154 | 'normal', new THREE.Float32BufferAttribute(normals, 3)); 155 | this._geometry.setAttribute( 156 | 'tangent', new THREE.Float32BufferAttribute(tangents, 4)); 157 | this._geometry.setAttribute( 158 | 'uv', new THREE.Float32BufferAttribute(uvs, 2)); 159 | this._geometry.setIndex( 160 | new THREE.BufferAttribute(new Uint32Array(indices), 1)); 161 | } 162 | } 163 | 164 | return { 165 | TerrainChunk: TerrainChunk 166 | } 167 | })(); 168 | -------------------------------------------------------------------------------- /src/terrain.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js'; 2 | 3 | import {noise} from './noise.js'; 4 | import {quadtree} from './quadtree.js'; 5 | import {spline} from './spline.js'; 6 | import {terrain_chunk} from './terrain-chunk.js'; 7 | import {utils} from './utils.js'; 8 | 9 | 10 | export const terrain = (function() { 11 | 12 | const _WHITE = new THREE.Color(0x808080); 13 | 14 | const _DEEP_OCEAN = new THREE.Color(0x20020FF); 15 | const _SHALLOW_OCEAN = new THREE.Color(0x8080FF); 16 | const _BEACH = new THREE.Color(0xd9d592); 17 | const _SNOW = new THREE.Color(0xFFFFFF); 18 | const _FOREST_TROPICAL = new THREE.Color(0x4f9f0f); 19 | const _FOREST_TEMPERATE = new THREE.Color(0x2b960e); 20 | const _FOREST_BOREAL = new THREE.Color(0x29c100); 21 | 22 | const _GREEN = new THREE.Color(0x80FF80); 23 | const _RED = new THREE.Color(0xFF8080); 24 | const _BLACK = new THREE.Color(0x000000); 25 | 26 | const _MIN_CELL_SIZE = 500; 27 | const _MIN_CELL_RESOLUTION = 128; 28 | const _PLANET_RADIUS = 4000; 29 | 30 | 31 | class HeightGenerator { 32 | constructor(generator, position, minRadius, maxRadius) { 33 | this._position = position.clone(); 34 | this._radius = [minRadius, maxRadius]; 35 | this._generator = generator; 36 | } 37 | 38 | Get(x, y, z) { 39 | return [this._generator.Get(x, y, z), 1]; 40 | } 41 | } 42 | 43 | 44 | class FixedHeightGenerator { 45 | constructor() {} 46 | 47 | Get() { 48 | return [50, 1]; 49 | } 50 | } 51 | 52 | 53 | // Cross-blended Hypsometric Tints 54 | // http://www.shadedrelief.com/hypso/hypso.html 55 | class HyposemetricTints { 56 | constructor(params) { 57 | const _colourLerp = (t, p0, p1) => { 58 | const c = p0.clone(); 59 | 60 | return c.lerp(p1, t); 61 | }; 62 | this._colourSpline = [ 63 | new spline.LinearSpline(_colourLerp), 64 | new spline.LinearSpline(_colourLerp) 65 | ]; 66 | 67 | // Arid 68 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d)); 69 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc)); 70 | this._colourSpline[0].AddPoint(1.0, _SNOW); 71 | 72 | // Humid 73 | this._colourSpline[1].AddPoint(0.0, _FOREST_BOREAL); 74 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c)); 75 | this._colourSpline[1].AddPoint(1.0, _SNOW); 76 | 77 | this._oceanSpline = new spline.LinearSpline(_colourLerp); 78 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN); 79 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN); 80 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN); 81 | 82 | this._params = params; 83 | } 84 | 85 | Get(x, y, z) { 86 | const m = this._params.biomeGenerator.Get(x, y, z); 87 | const h = z / 100.0; 88 | 89 | if (h < 0.05) { 90 | return this._oceanSpline.Get(h); 91 | } 92 | 93 | const c1 = this._colourSpline[0].Get(h); 94 | const c2 = this._colourSpline[1].Get(h); 95 | 96 | return c1.lerp(c2, m); 97 | } 98 | } 99 | 100 | 101 | class FixedColourGenerator { 102 | constructor(params) { 103 | this._params = params; 104 | } 105 | 106 | Get() { 107 | return this._params.colour; 108 | } 109 | } 110 | 111 | 112 | 113 | class TerrainChunkRebuilder { 114 | constructor(params) { 115 | this._pool = {}; 116 | this._params = params; 117 | this._Reset(); 118 | } 119 | 120 | AllocateChunk(params) { 121 | const w = params.width; 122 | 123 | if (!(w in this._pool)) { 124 | this._pool[w] = []; 125 | } 126 | 127 | let c = null; 128 | if (this._pool[w].length > 0) { 129 | c = this._pool[w].pop(); 130 | c._params = params; 131 | } else { 132 | c = new terrain_chunk.TerrainChunk(params); 133 | } 134 | 135 | c.Hide(); 136 | 137 | this._queued.push(c); 138 | 139 | return c; 140 | } 141 | 142 | _RecycleChunks(chunks) { 143 | for (let c of chunks) { 144 | if (!(c.chunk._params.width in this._pool)) { 145 | this._pool[c.chunk._params.width] = []; 146 | } 147 | 148 | c.chunk.Destroy(); 149 | } 150 | } 151 | 152 | _Reset() { 153 | this._active = null; 154 | this._queued = []; 155 | this._old = []; 156 | this._new = []; 157 | } 158 | 159 | get Busy() { 160 | return this._active || this._queued.length > 0; 161 | } 162 | 163 | Rebuild(chunks) { 164 | if (this.Busy) { 165 | return; 166 | } 167 | for (let k in chunks) { 168 | this._queued.push(chunks[k].chunk); 169 | } 170 | } 171 | 172 | Update() { 173 | if (this._active) { 174 | const r = this._active.next(); 175 | if (r.done) { 176 | this._active = null; 177 | } 178 | } else { 179 | const b = this._queued.pop(); 180 | if (b) { 181 | this._active = b._Rebuild(); 182 | this._new.push(b); 183 | } 184 | } 185 | 186 | if (this._active) { 187 | return; 188 | } 189 | 190 | if (!this._queued.length) { 191 | this._RecycleChunks(this._old); 192 | for (let b of this._new) { 193 | b.Show(); 194 | } 195 | this._Reset(); 196 | } 197 | } 198 | } 199 | 200 | class TerrainChunkManager { 201 | constructor(params) { 202 | this._Init(params); 203 | } 204 | 205 | _Init(params) { 206 | this._params = params; 207 | 208 | this._material = new THREE.MeshStandardMaterial({ 209 | wireframe: false, 210 | wireframeLinewidth: 1, 211 | color: 0xFFFFFF, 212 | side: THREE.FrontSide, 213 | vertexColors: THREE.VertexColors, 214 | }); 215 | this._builder = new TerrainChunkRebuilder(); 216 | 217 | this._InitNoise(params); 218 | this._InitBiomes(params); 219 | this._InitTerrain(params); 220 | } 221 | 222 | _InitNoise(params) { 223 | params.guiParams.noise = { 224 | octaves: 13, 225 | persistence: 0.707, 226 | lacunarity: 1.8, 227 | exponentiation: 4.5, 228 | height: 300.0, 229 | scale: 1100.0, 230 | seed: 1 231 | }; 232 | 233 | const onNoiseChanged = () => { 234 | this._builder.Rebuild(this._chunks); 235 | }; 236 | 237 | const noiseRollup = params.gui.addFolder('Terrain.Noise'); 238 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange( 239 | onNoiseChanged); 240 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange( 241 | onNoiseChanged); 242 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange( 243 | onNoiseChanged); 244 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange( 245 | onNoiseChanged); 246 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange( 247 | onNoiseChanged); 248 | noiseRollup.add(params.guiParams.noise, "height", 0, 512).onChange( 249 | onNoiseChanged); 250 | 251 | this._noise = new noise.Noise(params.guiParams.noise); 252 | 253 | params.guiParams.heightmap = { 254 | height: 16, 255 | }; 256 | 257 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap'); 258 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange( 259 | onNoiseChanged); 260 | } 261 | 262 | _InitBiomes(params) { 263 | params.guiParams.biomes = { 264 | octaves: 2, 265 | persistence: 0.5, 266 | lacunarity: 2.0, 267 | exponentiation: 3.9, 268 | scale: 2048.0, 269 | noiseType: 'simplex', 270 | seed: 2, 271 | exponentiation: 1, 272 | height: 1 273 | }; 274 | 275 | const onNoiseChanged = () => { 276 | this._builder.Rebuild(this._chunks); 277 | }; 278 | 279 | const noiseRollup = params.gui.addFolder('Terrain.Biomes'); 280 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange( 281 | onNoiseChanged); 282 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange( 283 | onNoiseChanged); 284 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange( 285 | onNoiseChanged); 286 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange( 287 | onNoiseChanged); 288 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange( 289 | onNoiseChanged); 290 | 291 | this._biomes = new noise.Noise(params.guiParams.biomes); 292 | } 293 | 294 | _InitTerrain(params) { 295 | params.guiParams.terrain= { 296 | wireframe: false, 297 | }; 298 | 299 | this._groups = [...new Array(6)].map(_ => new THREE.Group()); 300 | params.scene.add(...this._groups); 301 | 302 | const terrainRollup = params.gui.addFolder('Terrain'); 303 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => { 304 | for (let k in this._chunks) { 305 | this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe; 306 | } 307 | }); 308 | 309 | this._chunks = {}; 310 | this._params = params; 311 | } 312 | 313 | _CellIndex(p) { 314 | const xp = p.x + _MIN_CELL_SIZE * 0.5; 315 | const yp = p.z + _MIN_CELL_SIZE * 0.5; 316 | const x = Math.floor(xp / _MIN_CELL_SIZE); 317 | const z = Math.floor(yp / _MIN_CELL_SIZE); 318 | return [x, z]; 319 | } 320 | 321 | _CreateTerrainChunk(group, offset, width, resolution) { 322 | const params = { 323 | group: group, 324 | material: this._material, 325 | width: width, 326 | offset: offset, 327 | radius: _PLANET_RADIUS, 328 | resolution: resolution, 329 | biomeGenerator: this._biomes, 330 | colourGenerator: new HyposemetricTints({biomeGenerator: this._biomes}), 331 | heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)], 332 | }; 333 | 334 | return this._builder.AllocateChunk(params); 335 | } 336 | 337 | Update(_) { 338 | this._builder.Update(); 339 | if (!this._builder.Busy) { 340 | this._UpdateVisibleChunks_Quadtree(); 341 | } 342 | } 343 | 344 | _UpdateVisibleChunks_Quadtree() { 345 | function _Key(c) { 346 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']'; 347 | } 348 | 349 | const q = new quadtree.CubeQuadTree({ 350 | radius: _PLANET_RADIUS, 351 | min_node_size: _MIN_CELL_SIZE, 352 | }); 353 | q.Insert(this._params.camera.position); 354 | 355 | const sides = q.GetChildren(); 356 | 357 | let newTerrainChunks = {}; 358 | const center = new THREE.Vector3(); 359 | const dimensions = new THREE.Vector3(); 360 | for (let i = 0; i < sides.length; i++) { 361 | this._groups[i].matrix = sides[i].transform; 362 | this._groups[i].matrixAutoUpdate = false; 363 | for (let c of sides[i].children) { 364 | c.bounds.getCenter(center); 365 | c.bounds.getSize(dimensions); 366 | 367 | const child = { 368 | index: i, 369 | group: this._groups[i], 370 | position: [center.x, center.y, center.z], 371 | bounds: c.bounds, 372 | size: dimensions.x, 373 | }; 374 | 375 | const k = _Key(child); 376 | newTerrainChunks[k] = child; 377 | } 378 | } 379 | 380 | const intersection = utils.DictIntersection(this._chunks, newTerrainChunks); 381 | const difference = utils.DictDifference(newTerrainChunks, this._chunks); 382 | const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks)); 383 | 384 | this._builder._old.push(...recycle); 385 | 386 | newTerrainChunks = intersection; 387 | 388 | for (let k in difference) { 389 | const [xp, yp, zp] = difference[k].position; 390 | 391 | const offset = new THREE.Vector3(xp, yp, zp); 392 | newTerrainChunks[k] = { 393 | position: [xp, zp], 394 | chunk: this._CreateTerrainChunk( 395 | difference[k].group, offset, difference[k].size, _MIN_CELL_RESOLUTION), 396 | }; 397 | } 398 | 399 | this._chunks = newTerrainChunks; 400 | } 401 | } 402 | 403 | return { 404 | TerrainChunkManager: TerrainChunkManager 405 | } 406 | })(); 407 | -------------------------------------------------------------------------------- /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 | return { 6 | // Originally I planned to do texture atlasing, then got lazy. 7 | TextureAtlas: class { 8 | constructor(game) { 9 | this._game = game; 10 | this._Create(game); 11 | this.onLoad = () => {}; 12 | } 13 | 14 | _Create(game) { 15 | this._manager = new THREE.LoadingManager(); 16 | this._loader = new THREE.TextureLoader(this._manager); 17 | this._textures = {}; 18 | 19 | this._manager.onLoad = () => { 20 | this._OnLoad(); 21 | }; 22 | 23 | this._game = game; 24 | } 25 | 26 | get Info() { 27 | return this._textures; 28 | } 29 | 30 | _OnLoad() { 31 | this.onLoad(); 32 | } 33 | 34 | _LoadType(name, textureNames, offset, colourRange) { 35 | this._textures[name] = { 36 | colourRange: colourRange, 37 | uvOffset: [ 38 | offset.x, 39 | offset.y, 40 | ], 41 | textures: textureNames.map(n => this._loader.load(n)) 42 | }; 43 | if (this._textures[name].textures.length > 1) { 44 | } else { 45 | const caps = this._game._graphics._threejs.capabilities; 46 | const aniso = caps.getMaxAnisotropy(); 47 | 48 | this._textures[name].texture = this._textures[name].textures[0]; 49 | this._textures[name].texture.minFilter = THREE.LinearMipMapLinearFilter; 50 | this._textures[name].texture.magFilter = THREE.NearestFilter; 51 | this._textures[name].texture.wrapS = THREE.RepeatWrapping; 52 | this._textures[name].texture.wrapT = THREE.RepeatWrapping; 53 | this._textures[name].texture.anisotropy = aniso; 54 | } 55 | } 56 | } 57 | }; 58 | })(); 59 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------