├── 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 |
--------------------------------------------------------------------------------