├── LICENSE
├── base.css
├── index.html
├── resources
├── simplex-noise.png
├── space-negx.jpg
├── space-negy.jpg
├── space-negz.jpg
├── space-posx.jpg
├── space-posy.jpg
└── space-posz.jpg
└── src
├── camera-track.js
├── controls.js
├── demo.js
├── game.js
├── graphics.js
├── main.js
├── math.js
├── noise.js
├── quadtree.js
├── scattering-shader.js
├── simplex-noise.js
├── sky.js
├── spline.js
├── terrain-builder-threaded-worker.js
├── terrain-builder-threaded.js
├── terrain-builder.js
├── terrain-chunk.js
├── terrain-constants.js
├── terrain-shader.js
├── terrain.js
├── texture-splatter.js
├── textures.js
└── utils.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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/simplex-noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/simplex-noise.png
--------------------------------------------------------------------------------
/resources/space-negx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negx.jpg
--------------------------------------------------------------------------------
/resources/space-negy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negy.jpg
--------------------------------------------------------------------------------
/resources/space-negz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-negz.jpg
--------------------------------------------------------------------------------
/resources/space-posx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posx.jpg
--------------------------------------------------------------------------------
/resources/space-posy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posy.jpg
--------------------------------------------------------------------------------
/resources/space-posz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part10/09448f7cf6fa98a624cb50999109cafd378295a8/resources/space-posz.jpg
--------------------------------------------------------------------------------
/src/camera-track.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 | import {spline} from './spline.js';
4 |
5 |
6 | export const camera_track = (function() {
7 |
8 | class _CameraTrack {
9 | constructor(params) {
10 | this._params = params;
11 | this._currentTime = 0.0;
12 |
13 | const lerp = (t, p1, p2) => {
14 | const p = new THREE.Vector3().lerpVectors(p1.pos, p2.pos, t);
15 | const q = p1.rot.clone().slerp(p2.rot, t);
16 |
17 | return {pos: p, rot: q};
18 | };
19 | this._spline = new spline.LinearSpline(lerp);
20 |
21 | for (let p of params.points) {
22 | this._spline.AddPoint(p.time, p.data);
23 | }
24 | }
25 |
26 | Update(timeInSeconds) {
27 | this._currentTime += timeInSeconds;
28 |
29 | const r = this._spline.Get(this._currentTime);
30 |
31 | this._params.camera.position.copy(r.pos);
32 | this._params.camera.quaternion.copy(r.rot);
33 | }
34 | };
35 |
36 | return {
37 | CameraTrack: _CameraTrack,
38 | };
39 | })();
40 |
--------------------------------------------------------------------------------
/src/controls.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 | import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/controls/PointerLockControls.js';
3 | import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/three@0.125/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(12, 12, 12);
49 |
50 | this._SetupPointerLock();
51 |
52 | this._controls = new PointerLockControls(
53 | params.camera, document.body);
54 | params.scene.add(this._controls.getObject());
55 |
56 | const controlObject = this._controls.getObject();
57 | this._position = new THREE.Vector3();
58 | this._rotation = new THREE.Quaternion();
59 | this._position.copy(controlObject.position);
60 | this._rotation.copy(controlObject.quaternion);
61 |
62 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
63 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
64 |
65 | this._InitGUI();
66 | }
67 |
68 | _InitGUI() {
69 | this._params.guiParams.camera = {
70 | acceleration_x: 12,
71 | };
72 |
73 | const rollup = this._params.gui.addFolder('Camera.FPS');
74 | rollup.add(this._params.guiParams.camera, "acceleration_x", 4.0, 24.0).onChange(
75 | () => {
76 | this._acceleration.set(
77 | this._params.guiParams.camera.acceleration_x,
78 | this._params.guiParams.camera.acceleration_x,
79 | this._params.guiParams.camera.acceleration_x);
80 | });
81 | }
82 |
83 | _onKeyDown(event) {
84 | switch (event.keyCode) {
85 | case 38: // up
86 | case 87: // w
87 | this._move.forward = true;
88 | break;
89 | case 37: // left
90 | case 65: // a
91 | this._move.left = true;
92 | break;
93 | case 40: // down
94 | case 83: // s
95 | this._move.backward = true;
96 | break;
97 | case 39: // right
98 | case 68: // d
99 | this._move.right = true;
100 | break;
101 | case 33: // PG_UP
102 | this._move.up = true;
103 | break;
104 | case 34: // PG_DOWN
105 | this._move.down = true;
106 | break;
107 | }
108 | }
109 |
110 | _onKeyUp(event) {
111 | switch(event.keyCode) {
112 | case 38: // up
113 | case 87: // w
114 | this._move.forward = false;
115 | break;
116 | case 37: // left
117 | case 65: // a
118 | this._move.left = false;
119 | break;
120 | case 40: // down
121 | case 83: // s
122 | this._move.backward = false;
123 | break;
124 | case 39: // right
125 | case 68: // d
126 | this._move.right = false;
127 | break;
128 | case 33: // PG_UP
129 | this._move.up = false;
130 | break;
131 | case 34: // PG_DOWN
132 | this._move.down = false;
133 | break;
134 | }
135 | }
136 |
137 | _SetupPointerLock() {
138 | const hasPointerLock = (
139 | 'pointerLockElement' in document ||
140 | 'mozPointerLockElement' in document ||
141 | 'webkitPointerLockElement' in document);
142 | if (hasPointerLock) {
143 | const lockChange = (event) => {
144 | if (document.pointerLockElement === document.body ||
145 | document.mozPointerLockElement === document.body ||
146 | document.webkitPointerLockElement === document.body ) {
147 | this._enabled = true;
148 | this._controls.enabled = true;
149 | } else {
150 | this._controls.enabled = false;
151 | }
152 | };
153 | const lockError = (event) => {
154 | console.log(event);
155 | };
156 |
157 | document.addEventListener('pointerlockchange', lockChange, false);
158 | document.addEventListener('webkitpointerlockchange', lockChange, false);
159 | document.addEventListener('mozpointerlockchange', lockChange, false);
160 | document.addEventListener('pointerlockerror', lockError, false);
161 | document.addEventListener('mozpointerlockerror', lockError, false);
162 | document.addEventListener('webkitpointerlockerror', lockError, false);
163 |
164 | document.getElementById('target').addEventListener('click', (event) => {
165 | document.body.requestPointerLock = (
166 | document.body.requestPointerLock ||
167 | document.body.mozRequestPointerLock ||
168 | document.body.webkitRequestPointerLock);
169 |
170 | if (/Firefox/i.test(navigator.userAgent)) {
171 | const fullScreenChange = (event) => {
172 | if (document.fullscreenElement === document.body ||
173 | document.mozFullscreenElement === document.body ||
174 | document.mozFullScreenElement === document.body) {
175 | document.removeEventListener('fullscreenchange', fullScreenChange);
176 | document.removeEventListener('mozfullscreenchange', fullScreenChange);
177 | document.body.requestPointerLock();
178 | }
179 | };
180 | document.addEventListener(
181 | 'fullscreenchange', fullScreenChange, false);
182 | document.addEventListener(
183 | 'mozfullscreenchange', fullScreenChange, false);
184 | document.body.requestFullscreen = (
185 | document.body.requestFullscreen ||
186 | document.body.mozRequestFullscreen ||
187 | document.body.mozRequestFullScreen ||
188 | document.body.webkitRequestFullscreen);
189 | document.body.requestFullscreen();
190 | } else {
191 | document.body.requestPointerLock();
192 | }
193 | }, false);
194 | }
195 | }
196 |
197 | _FindIntersections(boxes, position) {
198 | const sphere = new THREE.Sphere(position, this._radius);
199 |
200 | const intersections = boxes.filter(b => {
201 | return sphere.intersectsBox(b);
202 | });
203 |
204 | return intersections;
205 | }
206 |
207 | Update(timeInSeconds) {
208 | if (!this._enabled) {
209 | return;
210 | }
211 |
212 | const frameDecceleration = new THREE.Vector3(
213 | this._velocity.x * this._decceleration.x,
214 | this._velocity.y * this._decceleration.y,
215 | this._velocity.z * this._decceleration.z
216 | );
217 | frameDecceleration.multiplyScalar(timeInSeconds);
218 |
219 | this._velocity.add(frameDecceleration);
220 |
221 | if (this._move.forward) {
222 | this._velocity.z -= 2 ** this._acceleration.z * timeInSeconds;
223 | }
224 | if (this._move.backward) {
225 | this._velocity.z += 2 ** this._acceleration.z * timeInSeconds;
226 | }
227 | if (this._move.left) {
228 | this._velocity.x -= 2 ** this._acceleration.x * timeInSeconds;
229 | }
230 | if (this._move.right) {
231 | this._velocity.x += 2 ** this._acceleration.x * timeInSeconds;
232 | }
233 | if (this._move.up) {
234 | this._velocity.y += 2 ** this._acceleration.y * timeInSeconds;
235 | }
236 | if (this._move.down) {
237 | this._velocity.y -= 2 ** this._acceleration.y * timeInSeconds;
238 | }
239 |
240 | const controlObject = this._controls.getObject();
241 |
242 | const oldPosition = new THREE.Vector3();
243 | oldPosition.copy(controlObject.position);
244 |
245 | const forward = new THREE.Vector3(0, 0, 1);
246 | forward.applyQuaternion(controlObject.quaternion);
247 | forward.normalize();
248 |
249 | const updown = new THREE.Vector3(0, 1, 0);
250 |
251 | const sideways = new THREE.Vector3(1, 0, 0);
252 | sideways.applyQuaternion(controlObject.quaternion);
253 | sideways.normalize();
254 |
255 | sideways.multiplyScalar(this._velocity.x * timeInSeconds);
256 | updown.multiplyScalar(this._velocity.y * timeInSeconds);
257 | forward.multiplyScalar(this._velocity.z * timeInSeconds);
258 |
259 | controlObject.position.add(forward);
260 | controlObject.position.add(sideways);
261 | controlObject.position.add(updown);
262 |
263 | // this._position.lerp(controlObject.position, 0.15);
264 | this._rotation.slerp(controlObject.quaternion, 0.15);
265 |
266 | // controlObject.position.copy(this._position);
267 | controlObject.quaternion.copy(this._rotation);
268 | }
269 | };
270 |
271 | class _ShipControls {
272 | constructor(params) {
273 | this._Init(params);
274 | }
275 |
276 | _Init(params) {
277 | this._params = params;
278 | this._radius = 2;
279 | this._enabled = false;
280 | this._move = {
281 | forward: false,
282 | backward: false,
283 | left: false,
284 | right: false,
285 | up: false,
286 | down: false,
287 | rocket: false,
288 | };
289 | this._velocity = new THREE.Vector3(0, 0, 0);
290 | this._decceleration = new THREE.Vector3(-0.001, -0.0001, -1);
291 | this._acceleration = new THREE.Vector3(100, 0.1, 25000);
292 |
293 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
294 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
295 |
296 | this._InitGUI();
297 | }
298 |
299 | _InitGUI() {
300 | this._params.guiParams.camera = {
301 | acceleration_x: 100,
302 | acceleration_y: 0.1,
303 | };
304 |
305 | const rollup = this._params.gui.addFolder('Camera.Ship');
306 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange(
307 | () => {
308 | this._acceleration.x = this._params.guiParams.camera.acceleration_x;
309 | });
310 | rollup.add(this._params.guiParams.camera, "acceleration_y", 0.001, 0.1).onChange(
311 | () => {
312 | this._acceleration.y = this._params.guiParams.camera.acceleration_y;
313 | });
314 | }
315 |
316 | _onKeyDown(event) {
317 | switch (event.keyCode) {
318 | case 87: // w
319 | this._move.forward = true;
320 | break;
321 | case 65: // a
322 | this._move.left = true;
323 | break;
324 | case 83: // s
325 | this._move.backward = true;
326 | break;
327 | case 68: // d
328 | this._move.right = true;
329 | break;
330 | case 33: // PG_UP
331 | this._acceleration.x *= 1.1;
332 | break;
333 | case 34: // PG_DOWN
334 | this._acceleration.x *= 0.8;
335 | break;
336 | case 32: // SPACE
337 | this._move.rocket = true;
338 | break;
339 | case 38: // up
340 | case 37: // left
341 | case 40: // down
342 | case 39: // right
343 | break;
344 | }
345 | }
346 |
347 | _onKeyUp(event) {
348 | switch(event.keyCode) {
349 | case 87: // w
350 | this._move.forward = false;
351 | break;
352 | case 65: // a
353 | this._move.left = false;
354 | break;
355 | case 83: // s
356 | this._move.backward = false;
357 | break;
358 | case 68: // d
359 | this._move.right = false;
360 | break;
361 | case 33: // PG_UP
362 | break;
363 | case 34: // PG_DOWN
364 | break;
365 | case 32: // SPACE
366 | this._move.rocket = false;
367 | break;
368 | case 38: // up
369 | case 37: // left
370 | case 40: // down
371 | case 39: // right
372 | break;
373 | }
374 | }
375 |
376 | Update(timeInSeconds) {
377 | const frameDecceleration = new THREE.Vector3(
378 | this._velocity.x * this._decceleration.x,
379 | this._velocity.y * this._decceleration.y,
380 | this._velocity.z * this._decceleration.z
381 | );
382 | frameDecceleration.multiplyScalar(timeInSeconds);
383 |
384 | this._velocity.add(frameDecceleration);
385 |
386 | const controlObject = this._params.camera;
387 | const _Q = new THREE.Quaternion();
388 | const _A = new THREE.Vector3();
389 | const _R = controlObject.quaternion.clone();
390 |
391 | if (this._move.forward) {
392 | _A.set(1, 0, 0);
393 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
394 | _R.multiply(_Q);
395 | }
396 | if (this._move.backward) {
397 | _A.set(1, 0, 0);
398 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
399 | _R.multiply(_Q);
400 | }
401 | if (this._move.left) {
402 | _A.set(0, 0, 1);
403 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
404 | _R.multiply(_Q);
405 | }
406 | if (this._move.right) {
407 | _A.set(0, 0, 1);
408 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
409 | _R.multiply(_Q);
410 | }
411 | if (this._move.rocket) {
412 | this._velocity.z -= this._acceleration.x * timeInSeconds;
413 | }
414 |
415 | controlObject.quaternion.copy(_R);
416 |
417 | const oldPosition = new THREE.Vector3();
418 | oldPosition.copy(controlObject.position);
419 |
420 | const forward = new THREE.Vector3(0, 0, 1);
421 | forward.applyQuaternion(controlObject.quaternion);
422 | //forward.y = 0;
423 | forward.normalize();
424 |
425 | const updown = new THREE.Vector3(0, 1, 0);
426 |
427 | const sideways = new THREE.Vector3(1, 0, 0);
428 | sideways.applyQuaternion(controlObject.quaternion);
429 | sideways.normalize();
430 |
431 | sideways.multiplyScalar(this._velocity.x * timeInSeconds);
432 | updown.multiplyScalar(this._velocity.y * timeInSeconds);
433 | forward.multiplyScalar(this._velocity.z * timeInSeconds);
434 |
435 | controlObject.position.add(forward);
436 | controlObject.position.add(sideways);
437 | controlObject.position.add(updown);
438 |
439 | oldPosition.copy(controlObject.position);
440 | }
441 | };
442 |
443 | return {
444 | ShipControls: _ShipControls,
445 | FPSControls: _FPSControls,
446 | OrbitControls: _OrbitControls,
447 | };
448 | })();
449 |
--------------------------------------------------------------------------------
/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.125/build/three.module.js';
2 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.125/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 | _AddEntity(name, entity, priority) {
44 | this._entities[name] = {entity: entity, priority: priority};
45 | }
46 |
47 | _StepEntities(timeInSeconds) {
48 | const sortedEntities = Object.values(this._entities);
49 |
50 | sortedEntities.sort((a, b) => {
51 | return a.priority - b.priority;
52 | })
53 |
54 | for (let s of sortedEntities) {
55 | s.entity.Update(timeInSeconds);
56 | }
57 | }
58 |
59 | _Render(timeInMS) {
60 | const timeInSeconds = Math.min(timeInMS * 0.001, this._minFrameTime);
61 |
62 | this._OnStep(timeInSeconds);
63 | this._StepEntities(timeInSeconds);
64 | this.graphics_.Render(timeInSeconds);
65 |
66 | this._RAF();
67 | }
68 | }
69 | };
70 | })();
71 |
--------------------------------------------------------------------------------
/src/graphics.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 | import Stats from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/libs/stats.module.js';
3 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/WebGL.js';
4 |
5 | import {RenderPass} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/postprocessing/RenderPass.js';
6 | import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/postprocessing/ShaderPass.js';
7 | import {CopyShader} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/shaders/CopyShader.js';
8 | import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/shaders/FXAAShader.js';
9 | import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/postprocessing/EffectComposer.js';
10 |
11 | import {scattering_shader} from './scattering-shader.js';
12 |
13 |
14 | export const graphics = (function() {
15 |
16 | function _GetImageData(image) {
17 | const canvas = document.createElement('canvas');
18 | canvas.width = image.width;
19 | canvas.height = image.height;
20 |
21 | const context = canvas.getContext( '2d' );
22 | context.drawImage(image, 0, 0);
23 |
24 | return context.getImageData(0, 0, image.width, image.height);
25 | }
26 |
27 | function _GetPixel(imagedata, x, y) {
28 | const position = (x + imagedata.width * y) * 4;
29 | const data = imagedata.data;
30 | return {
31 | r: data[position],
32 | g: data[position + 1],
33 | b: data[position + 2],
34 | a: data[position + 3]
35 | };
36 | }
37 |
38 | class _Graphics {
39 | constructor(game) {
40 | }
41 |
42 | Initialize() {
43 | if (!WEBGL.isWebGL2Available()) {
44 | return false;
45 | }
46 |
47 | const canvas = document.createElement('canvas');
48 | const context = canvas.getContext('webgl2', {alpha: false});
49 |
50 | this._threejs = new THREE.WebGLRenderer({
51 | canvas: canvas,
52 | context: context,
53 | antialias: false,
54 | });
55 | this._threejs.outputEncoding = THREE.LinearEncoding;
56 | this._threejs.setPixelRatio(window.devicePixelRatio);
57 | this._threejs.setSize(window.innerWidth, window.innerHeight);
58 | this._threejs.autoClear = false;
59 |
60 | const target = document.getElementById('target');
61 | target.appendChild(this._threejs.domElement);
62 |
63 | this._stats = new Stats();
64 | // target.appendChild(this._stats.dom);
65 |
66 | window.addEventListener('resize', () => {
67 | this._OnWindowResize();
68 | }, false);
69 |
70 | const fov = 60;
71 | const aspect = 1920 / 1080;
72 | const near = 0.1;
73 | const far = 10000000.0;
74 | this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
75 | this.camera_.position.set(75, 20, 0);
76 |
77 | this.scene_ = new THREE.Scene();
78 | this.scene_.background = new THREE.Color(0xaaaaaa);
79 |
80 | const renderPass = new RenderPass(this.scene_, this.camera_);
81 | const fxaaPass = new ShaderPass(FXAAShader);
82 | // const depthPass = new ShaderPass(scattering_shader.Shader);
83 |
84 | // this._depthPass = depthPass;
85 |
86 | this.composer_ = new EffectComposer(this._threejs);
87 | this.composer_.addPass(renderPass);
88 | this.composer_.addPass(fxaaPass);
89 | //this.composer_.addPass(depthPass);
90 |
91 | const params = {
92 | minFilter: THREE.NearestFilter,
93 | magFilter: THREE.NearestFilter,
94 | format: THREE.RGBAFormat,
95 | type: THREE.FloatType,
96 | generateMipmaps: false,
97 | };
98 |
99 | this._target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, params);
100 | this._target.stencilBuffer = false;
101 | this._target.depthBuffer = true;
102 | this._target.depthTexture = new THREE.DepthTexture();
103 | this._target.depthTexture.format = THREE.DepthFormat;
104 | this._target.depthTexture.type = THREE.FloatType;
105 | this._target.outputEncoding = THREE.LinearEncoding;
106 |
107 | this._threejs.setRenderTarget(this._target);
108 |
109 | const logDepthBufFC = 2.0 / ( Math.log(this.camera_.far + 1.0) / Math.LN2);
110 |
111 | this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
112 | this._depthPass = new THREE.ShaderMaterial( {
113 | vertexShader: scattering_shader.VS,
114 | fragmentShader: scattering_shader.PS,
115 | uniforms: {
116 | cameraNear: { value: this.Camera.near },
117 | cameraFar: { value: this.Camera.far },
118 | cameraPosition: { value: this.Camera.position },
119 | cameraForward: { value: null },
120 | tDiffuse: { value: null },
121 | tDepth: { value: null },
122 | inverseProjection: { value: null },
123 | inverseView: { value: null },
124 | planetPosition: { value: null },
125 | planetRadius: { value: null },
126 | atmosphereRadius: { value: null },
127 | logDepthBufFC: { value: logDepthBufFC },
128 | }
129 | } );
130 | var postPlane = new THREE.PlaneBufferGeometry( 2, 2 );
131 | var postQuad = new THREE.Mesh( postPlane, this._depthPass );
132 | this._postScene = new THREE.Scene();
133 | this._postScene.add( postQuad );
134 |
135 | this._CreateLights();
136 |
137 | return true;
138 | }
139 |
140 |
141 | _CreateLights() {
142 | let light = new THREE.DirectionalLight(0xFFFFFF, 1);
143 | light.position.set(100, 100, -100);
144 | light.target.position.set(0, 0, 0);
145 | light.castShadow = false;
146 | this.scene_.add(light);
147 |
148 | light = new THREE.DirectionalLight(0x404040, 1);
149 | light.position.set(100, 100, -100);
150 | light.target.position.set(0, 0, 0);
151 | light.castShadow = false;
152 | this.scene_.add(light);
153 |
154 | light = new THREE.DirectionalLight(0x404040, 1);
155 | light.position.set(100, 100, -100);
156 | light.target.position.set(0, 0, 0);
157 | light.castShadow = false;
158 | this.scene_.add(light);
159 |
160 | light = new THREE.DirectionalLight(0x202040, 1);
161 | light.position.set(100, -100, 100);
162 | light.target.position.set(0, 0, 0);
163 | light.castShadow = false;
164 | this.scene_.add(light);
165 |
166 | light = new THREE.AmbientLight(0xFFFFFF, 1.0);
167 | this.scene_.add(light);
168 | }
169 |
170 | _OnWindowResize() {
171 | this.camera_.aspect = window.innerWidth / window.innerHeight;
172 | this.camera_.updateProjectionMatrix();
173 | this._threejs.setSize(window.innerWidth, window.innerHeight);
174 | this.composer_.setSize(window.innerWidth, window.innerHeight);
175 | this._target.setSize(window.innerWidth, window.innerHeight);
176 | }
177 |
178 | get Scene() {
179 | return this.scene_;
180 | }
181 |
182 | get Camera() {
183 | return this.camera_;
184 | }
185 |
186 | Render(timeInSeconds) {
187 | this._threejs.setRenderTarget(this._target);
188 |
189 | this._threejs.clear();
190 | this._threejs.render(this.scene_, this.camera_);
191 | //this.composer_.render();
192 |
193 | this._threejs.setRenderTarget( null );
194 |
195 | const forward = new THREE.Vector3();
196 | this.camera_.getWorldDirection(forward);
197 |
198 | this._depthPass.uniforms.inverseProjection.value = this.camera_.projectionMatrixInverse;
199 | this._depthPass.uniforms.inverseView.value = this.camera_.matrixWorld;
200 | this._depthPass.uniforms.tDiffuse.value = this._target.texture;
201 | this._depthPass.uniforms.tDepth.value = this._target.depthTexture;
202 | this._depthPass.uniforms.cameraNear.value = this.camera_.near;
203 | this._depthPass.uniforms.cameraFar.value = this.camera_.far;
204 | this._depthPass.uniforms.cameraPosition.value = this.camera_.position;
205 | this._depthPass.uniforms.cameraForward.value = forward;
206 | this._depthPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0);
207 | this._depthPass.uniformsNeedUpdate = true;
208 |
209 | this._threejs.render( this._postScene, this._postCamera );
210 |
211 | this._stats.update();
212 | }
213 | }
214 |
215 | return {
216 | Graphics: _Graphics,
217 | GetPixel: _GetPixel,
218 | GetImageData: _GetImageData,
219 | };
220 | })();
221 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 | import {GUI} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/libs/dat.gui.module.js';
3 | import {controls} from './controls.js';
4 | import {game} from './game.js';
5 | import {terrain} from './terrain.js';
6 |
7 | let _APP = null;
8 |
9 |
10 | class ProceduralTerrain_Demo extends game.Game {
11 | constructor() {
12 | super();
13 | }
14 |
15 | _OnInitialize() {
16 | this._CreateGUI();
17 |
18 | this.graphics_.Camera.position.set(355898.9978932907, -16169.249553939484, -181920.2108868533);
19 | this.graphics_.Camera.quaternion.set(0.3525209450519473, 0.6189868049149101, -0.58773147927222, 0.38360921119467495);
20 | // this.graphics_.Camera.position.set(283679.0800079606, -314104.8959314113, -71872.13040166264);
21 | // this.graphics_.Camera.quaternion.set(0.4461797373759622, 0.5541566632843257, -0.523697972824766, 0.46858773751767197);
22 |
23 | this.graphics_.Camera.position.set(357183.28155512916, -19402.113225302386, -182320.80530987142);
24 | this.graphics_.Camera.quaternion.set(0.2511776691104541, 0.6998229958650649, -0.48248862753627253, 0.46299274000447177);
25 |
26 | this._AddEntity('_terrain', new terrain.TerrainChunkManager({
27 | camera: this.graphics_.Camera,
28 | scene: this.graphics_.Scene,
29 | scattering: this.graphics_._depthPass,
30 | gui: this._gui,
31 | guiParams: this._guiParams,
32 | game: this}), 1.0);
33 |
34 | this._AddEntity('_controls', new controls.FPSControls({
35 | camera: this.graphics_.Camera,
36 | scene: this.graphics_.Scene,
37 | domElement: this.graphics_._threejs.domElement,
38 | gui: this._gui,
39 | guiParams: this._guiParams}), 0.0);
40 |
41 | this._totalTime = 0;
42 |
43 | this._LoadBackground();
44 | }
45 |
46 | _CreateGUI() {
47 | this._guiParams = {
48 | general: {
49 | },
50 | };
51 | this._gui = new GUI();
52 |
53 | const generalRollup = this._gui.addFolder('General');
54 | this._gui.close();
55 | }
56 |
57 | _LoadBackground() {
58 | this.graphics_.Scene.background = new THREE.Color(0x000000);
59 | const loader = new THREE.CubeTextureLoader();
60 | const texture = loader.load([
61 | './resources/space-posx.jpg',
62 | './resources/space-negx.jpg',
63 | './resources/space-posy.jpg',
64 | './resources/space-negy.jpg',
65 | './resources/space-posz.jpg',
66 | './resources/space-negz.jpg',
67 | ]);
68 | texture.encoding = THREE.sRGBEncoding;
69 | this.graphics_.Scene.background = texture;
70 | }
71 |
72 | _OnStep(timeInSeconds) {
73 | }
74 | }
75 |
76 |
77 | function _Main() {
78 | _APP = new ProceduralTerrain_Demo();
79 | }
80 |
81 | _Main();
82 |
--------------------------------------------------------------------------------
/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 {simplex} from './simplex-noise.js';
2 |
3 | export const noise = (function() {
4 |
5 | class _NoiseGenerator {
6 | constructor(params) {
7 | this._params = params;
8 | this._Init();
9 | }
10 |
11 | _Init() {
12 | this._noise = new simplex.SimplexNoise(this._params.seed);
13 | }
14 |
15 | Get(x, y, z) {
16 | const G = 2.0 ** (-this._params.persistence);
17 | const xs = x / this._params.scale;
18 | const ys = y / this._params.scale;
19 | const zs = z / this._params.scale;
20 | const noiseFunc = this._noise;
21 |
22 | let amplitude = 1.0;
23 | let frequency = 1.0;
24 | let normalization = 0;
25 | let total = 0;
26 | for (let o = 0; o < this._params.octaves; o++) {
27 | const noiseValue = noiseFunc.noise3D(
28 | xs * frequency, ys * frequency, zs * frequency) * 0.5 + 0.5;
29 |
30 | total += noiseValue * amplitude;
31 | normalization += amplitude;
32 | amplitude *= G;
33 | frequency *= this._params.lacunarity;
34 | }
35 | total /= normalization;
36 | return Math.pow(
37 | total, this._params.exponentiation) * this._params.height;
38 | }
39 | }
40 |
41 | return {
42 | Noise: _NoiseGenerator
43 | }
44 | })();
45 |
--------------------------------------------------------------------------------
/src/quadtree.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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 0
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 1
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 2
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 3
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 4
41 | m = new THREE.Matrix4();
42 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r));
43 | transforms.push(m);
44 |
45 | // -Z 5
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 i = 0; i < transforms.length; ++i) {
52 | const t = transforms[i];
53 | this.sides_.push({
54 | transform: t.clone(),
55 | quadtree: new QuadTree({
56 | side: i,
57 | size: r,
58 | min_node_size: params.min_node_size,
59 | max_node_size: params.max_node_size,
60 | localToWorld: t,
61 | worldToLocal: t.clone().invert()
62 | }),
63 | });
64 | }
65 |
66 | this.BuildRootNeighbourInfo_();
67 | }
68 |
69 | BuildRootNeighbourInfo_() {
70 | const _FindClosestNeighbour = (edgeMidpoint, otherNodes) => {
71 | const neighbours = [...otherNodes].sort((a, b) => {
72 | return a.sphereCenter.distanceTo(edgeMidpoint) - b.sphereCenter.distanceTo(edgeMidpoint);
73 | });
74 | const test = [...otherNodes].map(c => {
75 | return c.sphereCenter.distanceTo(edgeMidpoint);
76 | }).sort((a, b) => a - b);
77 | return neighbours[0];
78 | };
79 |
80 | const nodes = this.sides_.map(s => s.quadtree.root_);
81 |
82 | for (let i = 0; i < 6; ++i) {
83 | const node = nodes[i];
84 | const edgeMidpoints = [
85 | node.GetLeftEdgeMidpoint(),
86 | node.GetTopEdgeMidpoint(),
87 | node.GetRightEdgeMidpoint(),
88 | node.GetBottomEdgeMidpoint(),
89 | ];
90 | const otherNodes = nodes.filter(n => n.side != node.side);
91 |
92 | const neighbours = edgeMidpoints.map(p => _FindClosestNeighbour(p, otherNodes));
93 | node.neighbours = neighbours.map(n => nodes[n.side]);
94 | }
95 | }
96 |
97 | GetChildren() {
98 | const children = [];
99 |
100 | for (let s of this.sides_) {
101 | const side = {
102 | transform: s.transform,
103 | children: s.quadtree.GetChildren(),
104 | }
105 | children.push(side);
106 | }
107 | return children;
108 | }
109 |
110 | Insert(pos) {
111 | for (let s of this.sides_) {
112 | s.quadtree.Insert(pos);
113 | }
114 | }
115 |
116 | BuildNeighbours() {
117 | let queue = [];
118 | for (let s of this.sides_) {
119 | queue.push(s.quadtree.root_);
120 | }
121 |
122 | while (queue.length > 0) {
123 | const node = queue.shift();
124 |
125 | this.sides_[node.side].quadtree.BuildNeighbours_Child_(node);
126 |
127 | for (let c of node.children) {
128 | queue.push(c);
129 | }
130 | }
131 | }
132 | }
133 |
134 | const LEFT = 0;
135 | const TOP = 1;
136 | const RIGHT = 2;
137 | const BOTTOM = 3;
138 |
139 | const TOP_LEFT = 2;
140 | const TOP_RIGHT = 3;
141 | const BOTTOM_LEFT = 0;
142 | const BOTTOM_RIGHT = 1;
143 |
144 | class Node {
145 | constructor(params) {
146 | }
147 |
148 | GetNeighbour(side) {
149 | return this.neighbours[side];
150 | }
151 |
152 | GetClosestChild(node) {
153 | const children = [...this.children].sort((a, b) => {
154 | return a.sphereCenter.distanceTo(node.sphereCenter) - b.sphereCenter.distanceTo(node.sphereCenter);
155 | });
156 | const test = [...this.children].map(c => {
157 | return c.sphereCenter.distanceTo(node.sphereCenter);
158 | }).sort((a, b) => a - b);
159 | return children[0];
160 | }
161 |
162 | GetChild(pos) {
163 | return this.children[pos];
164 | }
165 |
166 | GetClosestChildrenSharingEdge(edgePoint) {
167 | if (this.children.length == 0) {
168 | const edgePointLocal = edgePoint.clone().applyMatrix4(this.tree.worldToLocal);
169 | if (edgePointLocal.x == this.bounds.min.x || edgePointLocal.x == this.bounds.max.x ||
170 | edgePointLocal.y == this.bounds.min.y || edgePointLocal.y == this.bounds.max.y) {
171 | return [this];
172 | }
173 | return [];
174 | }
175 |
176 | const matches = [];
177 | for (let i = 0; i < this.children.length; ++i) {
178 | const child = this.children[i];
179 |
180 | matches.push(...child.GetClosestChildrenSharingEdge(edgePoint));
181 | }
182 | return matches;
183 | }
184 |
185 | GetLeftEdgeMidpoint() {
186 | const v = new THREE.Vector3(this.bounds.min.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0);
187 | v.applyMatrix4(this.localToWorld);
188 | return v;
189 | }
190 |
191 | GetRightEdgeMidpoint() {
192 | const v = new THREE.Vector3(this.bounds.max.x, (this.bounds.max.y + this.bounds.min.y) * 0.5, 0);
193 | v.applyMatrix4(this.localToWorld);
194 | return v;
195 | }
196 |
197 | GetTopEdgeMidpoint() {
198 | const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.max.y, 0);
199 | v.applyMatrix4(this.localToWorld);
200 | return v;
201 | }
202 |
203 | GetBottomEdgeMidpoint() {
204 | const v = new THREE.Vector3((this.bounds.max.x + this.bounds.min.x) * 0.5, this.bounds.min.y, 0);
205 | v.applyMatrix4(this.localToWorld);
206 | return v;
207 | }
208 | };
209 |
210 | class QuadTree {
211 | constructor(params) {
212 | const s = params.size;
213 | const b = new THREE.Box3(
214 | new THREE.Vector3(-s, -s, 0),
215 | new THREE.Vector3(s, s, 0));
216 | this.root_ = new Node();
217 | this.root_.side = params.side;
218 | this.root_.bounds = b;
219 | this.root_.children = [];
220 | this.root_.parent = null;
221 | this.root_.tree = this;
222 | this.root_.center = b.getCenter(new THREE.Vector3());
223 | this.root_.sphereCenter = b.getCenter(new THREE.Vector3());
224 | this.root_.localToWorld = params.localToWorld;
225 | this.root_.size = b.getSize(new THREE.Vector3());
226 | this.root_.root = true;
227 | this.root_.neighbours = [null, null, null, null];
228 |
229 | this._params = params;
230 | this.worldToLocal = params.worldToLocal;
231 | this.root_.sphereCenter = this.root_.center.clone();
232 | this.root_.sphereCenter.applyMatrix4(this._params.localToWorld);
233 | this.root_.sphereCenter.normalize();
234 | this.root_.sphereCenter.multiplyScalar(this._params.size);
235 | }
236 |
237 | GetChildren() {
238 | const children = [];
239 | this._GetChildren(this.root_, children);
240 | return children;
241 | }
242 |
243 | _GetChildren(node, target) {
244 | if (node.children.length == 0) {
245 | target.push(node);
246 | return;
247 | }
248 |
249 | for (let c of node.children) {
250 | this._GetChildren(c, target);
251 | }
252 | }
253 |
254 | BuildNeighbours_Child_(node) {
255 | const children = node.children;
256 | if (children.length == 0) {
257 | const hx = (node.bounds.max.x + node.bounds.min.x) * 0.5;
258 | const hy = (node.bounds.max.y + node.bounds.min.y) * 0.5;
259 | const nx = node.bounds.min.x;
260 | const ny = node.bounds.min.y;
261 | const px = node.bounds.max.x;
262 | const py = node.bounds.max.y;
263 | const b1 = new THREE.Vector3(nx, hy, 0);
264 | const b2 = new THREE.Vector3(hx, py, 0);
265 | const b3 = new THREE.Vector3(px, hy, 0);
266 | const b4 = new THREE.Vector3(hx, ny, 0);
267 |
268 | return;
269 | }
270 |
271 | if (node.center.x == 375000 && node.center.y == -125000 && node.side == 1 && node.size.x == 50000) {
272 | let a = 0;
273 | }
274 | if (node.root && node.side == 1) {
275 | let a = 0;
276 | }
277 | if (node.center.x == 200000 && node.center.y == -200000 && node.side == 1) {
278 | let a =0;
279 | }
280 | // Bottom left
281 | let leftNeighbour = node.GetNeighbour(LEFT);
282 | if (leftNeighbour.children.length > 0) {
283 | if (leftNeighbour.side != node.side) {
284 | leftNeighbour = leftNeighbour.GetClosestChild(children[0]);
285 | } else {
286 | leftNeighbour = leftNeighbour.GetChild(BOTTOM_RIGHT);
287 | }
288 | }
289 |
290 | let bottomNeighbour = node.GetNeighbour(BOTTOM);
291 | if (bottomNeighbour.children.length > 0) {
292 | if (bottomNeighbour.side != node.side) {
293 | bottomNeighbour = bottomNeighbour.GetClosestChild(children[0]);
294 | } else {
295 | bottomNeighbour = bottomNeighbour.GetChild(TOP_LEFT);
296 | }
297 | }
298 | children[0].neighbours = [leftNeighbour, children[TOP_LEFT], children[BOTTOM_RIGHT], bottomNeighbour];
299 |
300 | // Bottom right
301 | let rightNeighbour = node.GetNeighbour(RIGHT);
302 | if (rightNeighbour.children.length > 0) {
303 | if (rightNeighbour.side != node.side) {
304 | rightNeighbour = rightNeighbour.GetClosestChild(children[1]);
305 | } else {
306 | rightNeighbour = rightNeighbour.GetChild(BOTTOM_LEFT);
307 | }
308 | }
309 |
310 | bottomNeighbour = node.GetNeighbour(BOTTOM);
311 | if (bottomNeighbour.children.length > 0) {
312 | if (bottomNeighbour.side != node.side) {
313 | bottomNeighbour = bottomNeighbour.GetClosestChild(children[1]);
314 | } else {
315 | bottomNeighbour = bottomNeighbour.GetChild(TOP_RIGHT);
316 | }
317 | }
318 | children[1].neighbours = [children[BOTTOM_LEFT], children[TOP_RIGHT], rightNeighbour, bottomNeighbour];
319 |
320 | // Top left
321 | leftNeighbour = node.GetNeighbour(LEFT);
322 | if (leftNeighbour.children.length > 0) {
323 | if (leftNeighbour.side != node.side) {
324 | leftNeighbour = leftNeighbour.GetClosestChild(children[2]);
325 | } else {
326 | leftNeighbour = leftNeighbour.GetChild(TOP_RIGHT);
327 | }
328 | }
329 |
330 | let topNeighbour = node.GetNeighbour(TOP);
331 | if (topNeighbour.children.length > 0) {
332 | if (topNeighbour.side != node.side) {
333 | topNeighbour = topNeighbour.GetClosestChild(children[2]);
334 | } else {
335 | topNeighbour = topNeighbour.GetChild(BOTTOM_LEFT);
336 | }
337 | }
338 | children[2].neighbours = [leftNeighbour, topNeighbour, children[TOP_RIGHT], children[BOTTOM_LEFT]];
339 |
340 | // Top right
341 | topNeighbour = node.GetNeighbour(TOP);
342 | if (topNeighbour.children.length > 0) {
343 | if (topNeighbour.side != node.side) {
344 | topNeighbour = topNeighbour.GetClosestChild(children[3]);
345 | } else {
346 | topNeighbour = topNeighbour.GetChild(BOTTOM_RIGHT);
347 | }
348 | }
349 |
350 | rightNeighbour = node.GetNeighbour(RIGHT);
351 | if (rightNeighbour.children.length > 0) {
352 | if (rightNeighbour.side != node.side) {
353 | rightNeighbour = rightNeighbour.GetClosestChild(children[3]);
354 | } else {
355 | rightNeighbour = rightNeighbour.GetChild(TOP_LEFT);
356 | }
357 | }
358 | children[3].neighbours = [children[TOP_LEFT], topNeighbour, rightNeighbour, children[BOTTOM_RIGHT]];
359 | }
360 |
361 | Insert(pos) {
362 | this._Insert(this.root_, pos);
363 | }
364 |
365 | _Insert(child, pos) {
366 | // hack
367 | const distToChild = this._DistanceToChild(child, pos);
368 |
369 | if ((distToChild < child.size.x * 1.0 && child.size.x > this._params.min_node_size)) {
370 | child.children = this._CreateChildren(child);
371 |
372 | for (let c of child.children) {
373 | this._Insert(c, pos);
374 | }
375 | }
376 | }
377 |
378 | _DistanceToChild(child, pos) {
379 | return child.sphereCenter.distanceTo(pos);
380 | }
381 |
382 | _CreateChildren(child) {
383 | const midpoint = child.bounds.getCenter(new THREE.Vector3());
384 |
385 | // Bottom left
386 | const b1 = new THREE.Box3(child.bounds.min, midpoint);
387 |
388 | // Bottom right
389 | const b2 = new THREE.Box3(
390 | new THREE.Vector3(midpoint.x, child.bounds.min.y, 0),
391 | new THREE.Vector3(child.bounds.max.x, midpoint.y, 0));
392 |
393 | // Top left
394 | const b3 = new THREE.Box3(
395 | new THREE.Vector3(child.bounds.min.x, midpoint.y, 0),
396 | new THREE.Vector3(midpoint.x, child.bounds.max.y, 0));
397 |
398 | // Top right
399 | const b4 = new THREE.Box3(midpoint, child.bounds.max);
400 |
401 | const children = [b1, b2, b3, b4].map(
402 | b => {
403 | return {
404 | side: child.side,
405 | bounds: b,
406 | children: [],
407 | parent: child,
408 | center: b.getCenter(new THREE.Vector3()),
409 | size: b.getSize(new THREE.Vector3())
410 | };
411 | });
412 |
413 | const nodes = [];
414 | for (let c of children) {
415 | c.sphereCenter = c.center.clone();
416 | c.sphereCenter.applyMatrix4(this._params.localToWorld);
417 | c.sphereCenter.normalize()
418 | c.sphereCenter.multiplyScalar(this._params.size);
419 |
420 | const n = new Node();
421 | n.side = child.side;
422 | n.bounds = c.bounds;
423 | n.children = [];
424 | n.parent = child;
425 | n.tree = this;
426 | n.center = c.center;
427 | n.sphereCenter = c.sphereCenter;
428 | n.size = c.size;
429 | n.localToWorld = child.localToWorld;
430 | n.neighbours = [null, null, null, null];
431 | nodes.push(n);
432 | }
433 |
434 | return nodes;
435 | }
436 | }
437 |
438 | return {
439 | QuadTree: QuadTree,
440 | CubeQuadTree: CubeQuadTree,
441 | }
442 | })();
443 |
--------------------------------------------------------------------------------
/src/scattering-shader.js:
--------------------------------------------------------------------------------
1 |
2 | export const scattering_shader = (function() {
3 |
4 | const _VS = `
5 |
6 | #define saturate(a) clamp( a, 0.0, 1.0 )
7 |
8 | out vec2 vUv;
9 |
10 | void main() {
11 | vUv = uv;
12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
13 | }
14 | `;
15 |
16 |
17 | const _PS = `
18 | #include
19 |
20 | #define saturate(a) clamp( a, 0.0, 1.0 )
21 |
22 | #define PI 3.141592
23 | #define PRIMARY_STEP_COUNT 16
24 | #define LIGHT_STEP_COUNT 8
25 |
26 |
27 | in vec2 vUv;
28 |
29 | uniform sampler2D tDiffuse;
30 | uniform sampler2D tDepth;
31 | uniform float cameraNear;
32 | uniform float cameraFar;
33 | uniform vec3 cameraForward;
34 | uniform mat4 inverseProjection;
35 | uniform mat4 inverseView;
36 |
37 | uniform vec3 planetPosition;
38 | uniform float planetRadius;
39 | uniform float atmosphereRadius;
40 |
41 | uniform float logDepthBufFC;
42 |
43 | vec3 _ScreenToWorld(vec3 posS) {
44 |
45 | float depthValue = posS.z;
46 | float v_depth = pow(2.0, depthValue / (logDepthBufFC * 0.5));
47 | float z_view = v_depth - 1.0;
48 |
49 | vec4 posCLIP = vec4(posS.xy * 2.0 - 1.0, 0.0, 1.0);
50 | vec4 posVS = inverseProjection * posCLIP;
51 |
52 | posVS = vec4(posVS.xyz / posVS.w, 1.0);
53 | posVS.xyz = normalize(posVS.xyz) * z_view;
54 |
55 | vec4 posWS = inverseView * posVS;
56 |
57 | return posWS.xyz;
58 | }
59 |
60 | vec3 _ScreenToWorld_Normal(vec3 pos) {
61 | vec3 posS = pos;
62 |
63 | vec4 posP = vec4(posS.xyz * 2.0 - 1.0, 1.0);
64 | vec4 posVS = inverseProjection * posP;
65 |
66 | posVS = vec4((posVS.xyz / posVS.w), 1.0);
67 | vec4 posWS = inverseView * posVS;
68 |
69 | return posWS.xyz;
70 | }
71 |
72 | // source: https://github.com/selfshadow/ltc_code/blob/master/webgl/shaders/ltc/ltc_blit.fs
73 | vec3 RRTAndODTFit( vec3 v ) {
74 | vec3 a = v * ( v + 0.0245786 ) - 0.000090537;
75 | vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;
76 | return a / b;
77 | }
78 | // this implementation of ACES is modified to accommodate a brighter viewing environment.
79 | // the scale factor of 1/0.6 is subjective. see discussion in #19621.
80 | vec3 ACESFilmicToneMapping( vec3 color ) {
81 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
82 | const mat3 ACESInputMat = mat3(
83 | vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source
84 | vec3( 0.35458, 0.90834, 0.13383 ),
85 | vec3( 0.04823, 0.01566, 0.83777 )
86 | );
87 | // ODT_SAT => XYZ => D60_2_D65 => sRGB
88 | const mat3 ACESOutputMat = mat3(
89 | vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source
90 | vec3( -0.53108, 1.10813, -0.07276 ),
91 | vec3( -0.07367, -0.00605, 1.07602 )
92 | );
93 | color *= 1.0 / 0.6;
94 | color = ACESInputMat * color;
95 | // Apply RRT and ODT
96 | color = RRTAndODTFit( color );
97 | color = ACESOutputMat * color;
98 | // Clamp to [0, 1]
99 | return saturate( color );
100 | }
101 |
102 | float _SoftLight(float a, float b) {
103 | return (b < 0.5 ?
104 | (2.0 * a * b + a * a * (1.0 - 2.0 * b)) :
105 | (2.0 * a * (1.0 - b) + sqrt(a) * (2.0 * b - 1.0))
106 | );
107 | }
108 |
109 | vec3 _SoftLight(vec3 a, vec3 b) {
110 | return vec3(
111 | _SoftLight(a.x, b.x),
112 | _SoftLight(a.y, b.y),
113 | _SoftLight(a.z, b.z)
114 | );
115 | }
116 |
117 | bool _RayIntersectsSphere(
118 | vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius, out float t0, out float t1) {
119 | vec3 oc = rayStart - sphereCenter;
120 | float a = dot(rayDir, rayDir);
121 | float b = 2.0 * dot(oc, rayDir);
122 | float c = dot(oc, oc) - sphereRadius * sphereRadius;
123 | float d = b * b - 4.0 * a * c;
124 |
125 | // Also skip single point of contact
126 | if (d <= 0.0) {
127 | return false;
128 | }
129 |
130 | float r0 = (-b - sqrt(d)) / (2.0 * a);
131 | float r1 = (-b + sqrt(d)) / (2.0 * a);
132 |
133 | t0 = min(r0, r1);
134 | t1 = max(r0, r1);
135 |
136 | return (t1 >= 0.0);
137 | }
138 |
139 |
140 | vec3 _SampleLightRay(
141 | vec3 origin, vec3 sunDir, float planetScale, float planetRadius, float totalRadius,
142 | float rayleighScale, float mieScale, float absorptionHeightMax, float absorptionFalloff) {
143 |
144 | float t0, t1;
145 | _RayIntersectsSphere(origin, sunDir, planetPosition, totalRadius, t0, t1);
146 |
147 | float actualLightStepSize = (t1 - t0) / float(LIGHT_STEP_COUNT);
148 | float virtualLightStepSize = actualLightStepSize * planetScale;
149 | float lightStepPosition = 0.0;
150 |
151 | vec3 opticalDepthLight = vec3(0.0);
152 |
153 | for (int j = 0; j < LIGHT_STEP_COUNT; j++) {
154 | vec3 currentLightSamplePosition = origin + sunDir * (lightStepPosition + actualLightStepSize * 0.5);
155 |
156 | // Calculate the optical depths and accumulate
157 | float currentHeight = length(currentLightSamplePosition) - planetRadius;
158 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualLightStepSize;
159 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualLightStepSize;
160 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
161 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualLightStepSize;
162 |
163 | opticalDepthLight += vec3(
164 | currentOpticalDepthRayleigh,
165 | currentOpticalDepthMie,
166 | currentOpticalDepthOzone);
167 |
168 | lightStepPosition += actualLightStepSize;
169 | }
170 |
171 | return opticalDepthLight;
172 | }
173 |
174 | void _ComputeScattering(
175 | vec3 worldSpacePos, vec3 rayDirection, vec3 rayOrigin, vec3 sunDir,
176 | out vec3 scatteringColour, out vec3 scatteringOpacity) {
177 |
178 | vec3 betaRayleigh = vec3(5.5e-6, 13.0e-6, 22.4e-6);
179 | float betaMie = 21e-6;
180 | vec3 betaAbsorption = vec3(2.04e-5, 4.97e-5, 1.95e-6);
181 | float g = 0.76;
182 | float sunIntensity = 40.0;
183 |
184 | float planetRadius = planetRadius;
185 | float atmosphereRadius = atmosphereRadius - planetRadius;
186 | float totalRadius = planetRadius + atmosphereRadius;
187 |
188 | float referencePlanetRadius = 6371000.0;
189 | float referenceAtmosphereRadius = 100000.0;
190 | float referenceTotalRadius = referencePlanetRadius + referenceAtmosphereRadius;
191 | float referenceRatio = referencePlanetRadius / referenceAtmosphereRadius;
192 |
193 | float scaleRatio = planetRadius / atmosphereRadius;
194 | float planetScale = referencePlanetRadius / planetRadius;
195 | float atmosphereScale = scaleRatio / referenceRatio;
196 | float maxDist = distance(worldSpacePos, rayOrigin);
197 |
198 | float rayleighScale = 8500.0 / (planetScale * atmosphereScale);
199 | float mieScale = 1200.0 / (planetScale * atmosphereScale);
200 | float absorptionHeightMax = 32000.0 * (planetScale * atmosphereScale);
201 | float absorptionFalloff = 3000.0 / (planetScale * atmosphereScale);;
202 |
203 | float mu = dot(rayDirection, sunDir);
204 | float mumu = mu * mu;
205 | float gg = g * g;
206 | float phaseRayleigh = 3.0 / (16.0 * PI) * (1.0 + mumu);
207 | float phaseMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg));
208 |
209 | // Early out if ray doesn't intersect atmosphere.
210 | float t0, t1;
211 | if (!_RayIntersectsSphere(rayOrigin, rayDirection, planetPosition, totalRadius, t0, t1)) {
212 | scatteringOpacity = vec3(1.0);
213 | return;
214 | }
215 |
216 | // Clip the ray between the camera and potentially the planet surface.
217 | t0 = max(0.0, t0);
218 | t1 = min(maxDist, t1);
219 |
220 | float actualPrimaryStepSize = (t1 - t0) / float(PRIMARY_STEP_COUNT);
221 | float virtualPrimaryStepSize = actualPrimaryStepSize * planetScale;
222 | float primaryStepPosition = 0.0;
223 |
224 | vec3 accumulatedRayleigh = vec3(0.0);
225 | vec3 accumulatedMie = vec3(0.0);
226 | vec3 opticalDepth = vec3(0.0);
227 |
228 | // Take N steps along primary ray
229 | for (int i = 0; i < PRIMARY_STEP_COUNT; i++) {
230 | vec3 currentPrimarySamplePosition = rayOrigin + rayDirection * (
231 | primaryStepPosition + actualPrimaryStepSize * 0.5);
232 |
233 | float currentHeight = max(0.0, length(currentPrimarySamplePosition) - planetRadius);
234 |
235 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualPrimaryStepSize;
236 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualPrimaryStepSize;
237 |
238 | // Taken from https://www.shadertoy.com/view/wlBXWK
239 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
240 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualPrimaryStepSize;
241 |
242 | opticalDepth += vec3(currentOpticalDepthRayleigh, currentOpticalDepthMie, currentOpticalDepthOzone);
243 |
244 | // Sample light ray and accumulate optical depth.
245 | vec3 opticalDepthLight = _SampleLightRay(
246 | currentPrimarySamplePosition, sunDir,
247 | planetScale, planetRadius, totalRadius,
248 | rayleighScale, mieScale, absorptionHeightMax, absorptionFalloff);
249 |
250 | vec3 r = (
251 | betaRayleigh * (opticalDepth.x + opticalDepthLight.x) +
252 | betaMie * (opticalDepth.y + opticalDepthLight.y) +
253 | betaAbsorption * (opticalDepth.z + opticalDepthLight.z));
254 | vec3 attn = exp(-r);
255 |
256 | accumulatedRayleigh += currentOpticalDepthRayleigh * attn;
257 | accumulatedMie += currentOpticalDepthMie * attn;
258 |
259 | primaryStepPosition += actualPrimaryStepSize;
260 | }
261 |
262 | scatteringColour = sunIntensity * (phaseRayleigh * betaRayleigh * accumulatedRayleigh + phaseMie * betaMie * accumulatedMie);
263 | scatteringOpacity = exp(
264 | -(betaMie * opticalDepth.y + betaRayleigh * opticalDepth.x + betaAbsorption * opticalDepth.z));
265 | }
266 |
267 | vec3 _ApplyGroundFog(
268 | in vec3 rgb,
269 | float distToPoint,
270 | float height,
271 | in vec3 worldSpacePos,
272 | in vec3 rayOrigin,
273 | in vec3 rayDir,
274 | in vec3 sunDir)
275 | {
276 | vec3 up = normalize(rayOrigin);
277 |
278 | float skyAmt = dot(up, rayDir) * 0.25 + 0.75;
279 | skyAmt = saturate(skyAmt);
280 | skyAmt *= skyAmt;
281 |
282 | vec3 DARK_BLUE = vec3(0.1, 0.2, 0.3);
283 | vec3 LIGHT_BLUE = vec3(0.5, 0.6, 0.7);
284 | vec3 DARK_ORANGE = vec3(0.7, 0.4, 0.05);
285 | vec3 BLUE = vec3(0.5, 0.6, 0.7);
286 | vec3 YELLOW = vec3(1.0, 0.9, 0.7);
287 |
288 | vec3 fogCol = mix(DARK_BLUE, LIGHT_BLUE, skyAmt);
289 | float sunAmt = max(dot(rayDir, sunDir), 0.0);
290 | fogCol = mix(fogCol, YELLOW, pow(sunAmt, 16.0));
291 |
292 | float be = 0.0025;
293 | float fogAmt = (1.0 - exp(-distToPoint * be));
294 |
295 | // Sun
296 | sunAmt = 0.5 * saturate(pow(sunAmt, 256.0));
297 |
298 | return mix(rgb, fogCol, fogAmt) + sunAmt * YELLOW;
299 | }
300 |
301 | vec3 _ApplySpaceFog(
302 | in vec3 rgb,
303 | in float distToPoint,
304 | in float height,
305 | in vec3 worldSpacePos,
306 | in vec3 rayOrigin,
307 | in vec3 rayDir,
308 | in vec3 sunDir)
309 | {
310 | float atmosphereThickness = (atmosphereRadius - planetRadius);
311 |
312 | float t0 = -1.0;
313 | float t1 = -1.0;
314 |
315 | // This is a hack since the world mesh has seams that we haven't fixed yet.
316 | if (_RayIntersectsSphere(
317 | rayOrigin, rayDir, planetPosition, planetRadius, t0, t1)) {
318 | if (distToPoint > t0) {
319 | distToPoint = t0;
320 | worldSpacePos = rayOrigin + t0 * rayDir;
321 | }
322 | }
323 |
324 | if (!_RayIntersectsSphere(
325 | rayOrigin, rayDir, planetPosition, planetRadius + atmosphereThickness * 5.0, t0, t1)) {
326 | return rgb * 0.5;
327 | }
328 |
329 | // Figure out a better way to do this
330 | float silhouette = saturate((distToPoint - 10000.0) / 10000.0);
331 |
332 | // Glow around planet
333 | float scaledDistanceToSurface = 0.0;
334 |
335 | // Calculate the closest point between ray direction and planet. Use a point in front of the
336 | // camera to force differences as you get closer to planet.
337 | vec3 fakeOrigin = rayOrigin + rayDir * atmosphereThickness;
338 | float t = max(0.0, dot(rayDir, planetPosition - fakeOrigin) / dot(rayDir, rayDir));
339 | vec3 pb = fakeOrigin + t * rayDir;
340 |
341 | scaledDistanceToSurface = saturate((distance(pb, planetPosition) - planetRadius) / atmosphereThickness);
342 | scaledDistanceToSurface = smoothstep(0.0, 1.0, 1.0 - scaledDistanceToSurface);
343 | //scaledDistanceToSurface = smoothstep(0.0, 1.0, scaledDistanceToSurface);
344 |
345 | float scatteringFactor = scaledDistanceToSurface * silhouette;
346 |
347 | // Fog on surface
348 | t0 = max(0.0, t0);
349 | t1 = min(distToPoint, t1);
350 |
351 | vec3 intersectionPoint = rayOrigin + t1 * rayDir;
352 | vec3 normalAtIntersection = normalize(intersectionPoint);
353 |
354 | float distFactor = exp(-distToPoint * 0.0005 / (atmosphereThickness));
355 | float fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
356 | fresnel = smoothstep(0.0, 1.0, fresnel);
357 |
358 | float extinctionFactor = saturate(fresnel * distFactor) * (1.0 - silhouette);
359 |
360 | // Front/Back Lighting
361 | vec3 BLUE = vec3(0.5, 0.6, 0.75);
362 | vec3 YELLOW = vec3(1.0, 0.9, 0.7);
363 | vec3 RED = vec3(0.035, 0.0, 0.0);
364 |
365 | float NdotL = dot(normalAtIntersection, sunDir);
366 | float wrap = 0.5;
367 | float NdotL_wrap = max(0.0, (NdotL + wrap) / (1.0 + wrap));
368 | float RdotS = max(0.0, dot(rayDir, sunDir));
369 | float sunAmount = RdotS;
370 |
371 | vec3 backLightingColour = YELLOW * 0.1;
372 | vec3 frontLightingColour = mix(BLUE, YELLOW, pow(sunAmount, 32.0));
373 |
374 | vec3 fogColour = mix(backLightingColour, frontLightingColour, NdotL_wrap);
375 |
376 | extinctionFactor *= NdotL_wrap;
377 |
378 | // Sun
379 | float specular = pow((RdotS + 0.5) / (1.0 + 0.5), 64.0);
380 |
381 | fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
382 | fresnel *= fresnel;
383 |
384 | float sunFactor = (length(pb) - planetRadius) / (atmosphereThickness * 5.0);
385 | sunFactor = (1.0 - saturate(sunFactor));
386 | sunFactor *= sunFactor;
387 | sunFactor *= sunFactor;
388 | sunFactor *= specular * fresnel;
389 |
390 | vec3 baseColour = mix(rgb, fogColour, extinctionFactor);
391 | vec3 litColour = baseColour + _SoftLight(fogColour * scatteringFactor + YELLOW * sunFactor, baseColour);
392 | vec3 blendedColour = mix(baseColour, fogColour, scatteringFactor);
393 | blendedColour += blendedColour + _SoftLight(YELLOW * sunFactor, blendedColour);
394 | return mix(litColour, blendedColour, scaledDistanceToSurface * 0.25);
395 | }
396 |
397 | vec3 _ApplyFog(
398 | in vec3 rgb,
399 | in float distToPoint,
400 | in float height,
401 | in vec3 worldSpacePos,
402 | in vec3 rayOrigin,
403 | in vec3 rayDir,
404 | in vec3 sunDir)
405 | {
406 | float distToPlanet = max(0.0, length(rayOrigin) - planetRadius);
407 | float atmosphereThickness = (atmosphereRadius - planetRadius);
408 |
409 | vec3 groundCol = _ApplyGroundFog(
410 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
411 | vec3 spaceCol = _ApplySpaceFog(
412 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
413 |
414 | float blendFactor = saturate(distToPlanet / (atmosphereThickness * 0.5));
415 |
416 | blendFactor = smoothstep(0.0, 1.0, blendFactor);
417 | blendFactor = smoothstep(0.0, 1.0, blendFactor);
418 |
419 | return mix(groundCol, spaceCol, blendFactor);
420 | }
421 |
422 | void main() {
423 | float z = texture2D(tDepth, vUv).x;
424 | vec3 posWS = _ScreenToWorld(vec3(vUv, z));
425 | float dist = length(posWS - cameraPosition);
426 | float height = max(0.0, length(cameraPosition) - planetRadius);
427 | vec3 cameraDirection = normalize(posWS - cameraPosition);
428 |
429 |
430 | vec3 diffuse = texture2D(tDiffuse, vUv).xyz;
431 | vec3 lightDir = normalize(vec3(1, 1, -1));
432 |
433 | // diffuse = _ApplyFog(diffuse, dist, height, posWS, cameraPosition, cameraDirection, lightDir);
434 |
435 | vec3 scatteringColour = vec3(0.0);
436 | vec3 scatteringOpacity = vec3(1.0, 1.0, 1.0);
437 | _ComputeScattering(
438 | posWS, cameraDirection, cameraPosition,
439 | lightDir, scatteringColour, scatteringOpacity
440 | );
441 |
442 | // diffuse = diffuse * scatteringOpacity + scatteringColour;
443 | // diffuse = ACESFilmicToneMapping(diffuse);
444 | diffuse = pow(diffuse, vec3(1.0 / 2.0));
445 |
446 | gl_FragColor.rgb = diffuse;
447 | gl_FragColor.a = 1.0;
448 | }
449 | `;
450 |
451 |
452 | const _Shader = {
453 | uniforms: {
454 | "tDiffuse": { value: null },
455 | "tDepth": { value: null },
456 | "cameraNear": { value: 0.0 },
457 | "cameraFar": { value: 0.0 },
458 | },
459 | vertexShader: _VS,
460 | fragmentShader: _PS,
461 | };
462 |
463 | return {
464 | Shader: _Shader,
465 | VS: _VS,
466 | PS: _PS,
467 | };
468 | })();
--------------------------------------------------------------------------------
/src/simplex-noise.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A fast javascript implementation of simplex noise by Jonas Wagner
3 |
4 | Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java.
5 | Which is based on example code by Stefan Gustavson (stegu@itn.liu.se).
6 | With Optimisations by Peter Eastman (peastman@drizzle.stanford.edu).
7 | Better rank ordering method by Stefan Gustavson in 2012.
8 |
9 |
10 | Copyright (c) 2018 Jonas Wagner
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining a copy
13 | of this software and associated documentation files (the "Software"), to deal
14 | in the Software without restriction, including without limitation the rights
15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 | copies of the Software, and to permit persons to whom the Software is
17 | furnished to do so, subject to the following conditions:
18 |
19 | The above copyright notice and this permission notice shall be included in all
20 | copies or substantial portions of the Software.
21 |
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 | SOFTWARE.
29 | */
30 | // (function() {
31 |
32 | export const simplex = (function() {
33 |
34 | 'use strict';
35 |
36 | var F2 = 0.5 * (Math.sqrt(3.0) - 1.0);
37 | var G2 = (3.0 - Math.sqrt(3.0)) / 6.0;
38 | var F3 = 1.0 / 3.0;
39 | var G3 = 1.0 / 6.0;
40 | var F4 = (Math.sqrt(5.0) - 1.0) / 4.0;
41 | var G4 = (5.0 - Math.sqrt(5.0)) / 20.0;
42 |
43 | function SimplexNoise(randomOrSeed) {
44 | var random;
45 | if (typeof randomOrSeed == 'function') {
46 | random = randomOrSeed;
47 | }
48 | else if (randomOrSeed) {
49 | random = alea(randomOrSeed);
50 | } else {
51 | random = Math.random;
52 | }
53 | this.p = buildPermutationTable(random);
54 | this.perm = new Uint8Array(512);
55 | this.permMod12 = new Uint8Array(512);
56 | for (var i = 0; i < 512; i++) {
57 | this.perm[i] = this.p[i & 255];
58 | this.permMod12[i] = this.perm[i] % 12;
59 | }
60 |
61 | }
62 | SimplexNoise.prototype = {
63 | grad3: new Float32Array([1, 1, 0,
64 | -1, 1, 0,
65 | 1, -1, 0,
66 |
67 | -1, -1, 0,
68 | 1, 0, 1,
69 | -1, 0, 1,
70 |
71 | 1, 0, -1,
72 | -1, 0, -1,
73 | 0, 1, 1,
74 |
75 | 0, -1, 1,
76 | 0, 1, -1,
77 | 0, -1, -1]),
78 | grad4: new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
79 | 0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1,
80 | 1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1,
81 | -1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1,
82 | 1, 1, 0, 1, 1, 1, 0, -1, 1, -1, 0, 1, 1, -1, 0, -1,
83 | -1, 1, 0, 1, -1, 1, 0, -1, -1, -1, 0, 1, -1, -1, 0, -1,
84 | 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 0,
85 | -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 0]),
86 | noise2D: function(xin, yin) {
87 | var permMod12 = this.permMod12;
88 | var perm = this.perm;
89 | var grad3 = this.grad3;
90 | var n0 = 0; // Noise contributions from the three corners
91 | var n1 = 0;
92 | var n2 = 0;
93 | // Skew the input space to determine which simplex cell we're in
94 | var s = (xin + yin) * F2; // Hairy factor for 2D
95 | var i = Math.floor(xin + s);
96 | var j = Math.floor(yin + s);
97 | var t = (i + j) * G2;
98 | var X0 = i - t; // Unskew the cell origin back to (x,y) space
99 | var Y0 = j - t;
100 | var x0 = xin - X0; // The x,y distances from the cell origin
101 | var y0 = yin - Y0;
102 | // For the 2D case, the simplex shape is an equilateral triangle.
103 | // Determine which simplex we are in.
104 | var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
105 | if (x0 > y0) {
106 | i1 = 1;
107 | j1 = 0;
108 | } // lower triangle, XY order: (0,0)->(1,0)->(1,1)
109 | else {
110 | i1 = 0;
111 | j1 = 1;
112 | } // upper triangle, YX order: (0,0)->(0,1)->(1,1)
113 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
114 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
115 | // c = (3-sqrt(3))/6
116 | var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
117 | var y1 = y0 - j1 + G2;
118 | var x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords
119 | var y2 = y0 - 1.0 + 2.0 * G2;
120 | // Work out the hashed gradient indices of the three simplex corners
121 | var ii = i & 255;
122 | var jj = j & 255;
123 | // Calculate the contribution from the three corners
124 | var t0 = 0.5 - x0 * x0 - y0 * y0;
125 | if (t0 >= 0) {
126 | var gi0 = permMod12[ii + perm[jj]] * 3;
127 | t0 *= t0;
128 | n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
129 | }
130 | var t1 = 0.5 - x1 * x1 - y1 * y1;
131 | if (t1 >= 0) {
132 | var gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3;
133 | t1 *= t1;
134 | n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
135 | }
136 | var t2 = 0.5 - x2 * x2 - y2 * y2;
137 | if (t2 >= 0) {
138 | var gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3;
139 | t2 *= t2;
140 | n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
141 | }
142 | // Add contributions from each corner to get the final noise value.
143 | // The result is scaled to return values in the interval [-1,1].
144 | return 70.0 * (n0 + n1 + n2);
145 | },
146 | // 3D simplex noise
147 | noise3D: function(xin, yin, zin) {
148 | var permMod12 = this.permMod12;
149 | var perm = this.perm;
150 | var grad3 = this.grad3;
151 | var n0, n1, n2, n3; // Noise contributions from the four corners
152 | // Skew the input space to determine which simplex cell we're in
153 | var s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D
154 | var i = Math.floor(xin + s);
155 | var j = Math.floor(yin + s);
156 | var k = Math.floor(zin + s);
157 | var t = (i + j + k) * G3;
158 | var X0 = i - t; // Unskew the cell origin back to (x,y,z) space
159 | var Y0 = j - t;
160 | var Z0 = k - t;
161 | var x0 = xin - X0; // The x,y,z distances from the cell origin
162 | var y0 = yin - Y0;
163 | var z0 = zin - Z0;
164 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron.
165 | // Determine which simplex we are in.
166 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords
167 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords
168 | if (x0 >= y0) {
169 | if (y0 >= z0) {
170 | i1 = 1;
171 | j1 = 0;
172 | k1 = 0;
173 | i2 = 1;
174 | j2 = 1;
175 | k2 = 0;
176 | } // X Y Z order
177 | else if (x0 >= z0) {
178 | i1 = 1;
179 | j1 = 0;
180 | k1 = 0;
181 | i2 = 1;
182 | j2 = 0;
183 | k2 = 1;
184 | } // X Z Y order
185 | else {
186 | i1 = 0;
187 | j1 = 0;
188 | k1 = 1;
189 | i2 = 1;
190 | j2 = 0;
191 | k2 = 1;
192 | } // Z X Y order
193 | }
194 | else { // x0 y0) rankx++;
301 | else ranky++;
302 | if (x0 > z0) rankx++;
303 | else rankz++;
304 | if (x0 > w0) rankx++;
305 | else rankw++;
306 | if (y0 > z0) ranky++;
307 | else rankz++;
308 | if (y0 > w0) ranky++;
309 | else rankw++;
310 | if (z0 > w0) rankz++;
311 | else rankw++;
312 | var i1, j1, k1, l1; // The integer offsets for the second simplex corner
313 | var i2, j2, k2, l2; // The integer offsets for the third simplex corner
314 | var i3, j3, k3, l3; // The integer offsets for the fourth simplex corner
315 | // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.
316 | // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0;
321 | j1 = ranky >= 3 ? 1 : 0;
322 | k1 = rankz >= 3 ? 1 : 0;
323 | l1 = rankw >= 3 ? 1 : 0;
324 | // Rank 2 denotes the second largest coordinate.
325 | i2 = rankx >= 2 ? 1 : 0;
326 | j2 = ranky >= 2 ? 1 : 0;
327 | k2 = rankz >= 2 ? 1 : 0;
328 | l2 = rankw >= 2 ? 1 : 0;
329 | // Rank 1 denotes the second smallest coordinate.
330 | i3 = rankx >= 1 ? 1 : 0;
331 | j3 = ranky >= 1 ? 1 : 0;
332 | k3 = rankz >= 1 ? 1 : 0;
333 | l3 = rankw >= 1 ? 1 : 0;
334 | // The fifth corner has all coordinate offsets = 1, so no need to compute that.
335 | var x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords
336 | var y1 = y0 - j1 + G4;
337 | var z1 = z0 - k1 + G4;
338 | var w1 = w0 - l1 + G4;
339 | var x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords
340 | var y2 = y0 - j2 + 2.0 * G4;
341 | var z2 = z0 - k2 + 2.0 * G4;
342 | var w2 = w0 - l2 + 2.0 * G4;
343 | var x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords
344 | var y3 = y0 - j3 + 3.0 * G4;
345 | var z3 = z0 - k3 + 3.0 * G4;
346 | var w3 = w0 - l3 + 3.0 * G4;
347 | var x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords
348 | var y4 = y0 - 1.0 + 4.0 * G4;
349 | var z4 = z0 - 1.0 + 4.0 * G4;
350 | var w4 = w0 - 1.0 + 4.0 * G4;
351 | // Work out the hashed gradient indices of the five simplex corners
352 | var ii = i & 255;
353 | var jj = j & 255;
354 | var kk = k & 255;
355 | var ll = l & 255;
356 | // Calculate the contribution from the five corners
357 | var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0;
358 | if (t0 < 0) n0 = 0.0;
359 | else {
360 | var gi0 = (perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32) * 4;
361 | t0 *= t0;
362 | n0 = t0 * t0 * (grad4[gi0] * x0 + grad4[gi0 + 1] * y0 + grad4[gi0 + 2] * z0 + grad4[gi0 + 3] * w0);
363 | }
364 | var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1;
365 | if (t1 < 0) n1 = 0.0;
366 | else {
367 | var gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32) * 4;
368 | t1 *= t1;
369 | n1 = t1 * t1 * (grad4[gi1] * x1 + grad4[gi1 + 1] * y1 + grad4[gi1 + 2] * z1 + grad4[gi1 + 3] * w1);
370 | }
371 | var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2;
372 | if (t2 < 0) n2 = 0.0;
373 | else {
374 | var gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32) * 4;
375 | t2 *= t2;
376 | n2 = t2 * t2 * (grad4[gi2] * x2 + grad4[gi2 + 1] * y2 + grad4[gi2 + 2] * z2 + grad4[gi2 + 3] * w2);
377 | }
378 | var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3;
379 | if (t3 < 0) n3 = 0.0;
380 | else {
381 | var gi3 = (perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32) * 4;
382 | t3 *= t3;
383 | n3 = t3 * t3 * (grad4[gi3] * x3 + grad4[gi3 + 1] * y3 + grad4[gi3 + 2] * z3 + grad4[gi3 + 3] * w3);
384 | }
385 | var t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4;
386 | if (t4 < 0) n4 = 0.0;
387 | else {
388 | var gi4 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32) * 4;
389 | t4 *= t4;
390 | n4 = t4 * t4 * (grad4[gi4] * x4 + grad4[gi4 + 1] * y4 + grad4[gi4 + 2] * z4 + grad4[gi4 + 3] * w4);
391 | }
392 | // Sum up and scale the result to cover the range [-1,1]
393 | return 27.0 * (n0 + n1 + n2 + n3 + n4);
394 | }
395 | };
396 |
397 | function buildPermutationTable(random) {
398 | var i;
399 | var p = new Uint8Array(256);
400 | for (i = 0; i < 256; i++) {
401 | p[i] = i;
402 | }
403 | for (i = 0; i < 255; i++) {
404 | var r = i + ~~(random() * (256 - i));
405 | var aux = p[i];
406 | p[i] = p[r];
407 | p[r] = aux;
408 | }
409 | return p;
410 | }
411 | SimplexNoise._buildPermutationTable = buildPermutationTable;
412 |
413 | function alea() {
414 | // Johannes Baagøe , 2010
415 | var s0 = 0;
416 | var s1 = 0;
417 | var s2 = 0;
418 | var c = 1;
419 |
420 | var mash = masher();
421 | s0 = mash(' ');
422 | s1 = mash(' ');
423 | s2 = mash(' ');
424 |
425 | for (var i = 0; i < arguments.length; i++) {
426 | s0 -= mash(arguments[i]);
427 | if (s0 < 0) {
428 | s0 += 1;
429 | }
430 | s1 -= mash(arguments[i]);
431 | if (s1 < 0) {
432 | s1 += 1;
433 | }
434 | s2 -= mash(arguments[i]);
435 | if (s2 < 0) {
436 | s2 += 1;
437 | }
438 | }
439 | mash = null;
440 | return function() {
441 | var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
442 | s0 = s1;
443 | s1 = s2;
444 | return s2 = t - (c = t | 0);
445 | };
446 | }
447 | function masher() {
448 | var n = 0xefc8249d;
449 | return function(data) {
450 | data = data.toString();
451 | for (var i = 0; i < data.length; i++) {
452 | n += data.charCodeAt(i);
453 | var h = 0.02519603282416938 * n;
454 | n = h >>> 0;
455 | h -= n;
456 | h *= n;
457 | n = h >>> 0;
458 | h -= n;
459 | n += h * 0x100000000; // 2^32
460 | }
461 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
462 | };
463 | }
464 |
465 | // // amd
466 | // if (typeof define !== 'undefined' && define.amd) define(function() {return SimplexNoise;});
467 | // // common js
468 | // if (typeof exports !== 'undefined') exports.SimplexNoise = SimplexNoise;
469 | // // browser
470 | // else if (typeof window !== 'undefined') window.SimplexNoise = SimplexNoise;
471 | // // nodejs
472 | // if (typeof module !== 'undefined') {
473 | // module.exports = SimplexNoise;
474 | // }
475 | return {
476 | SimplexNoise: SimplexNoise
477 | };
478 |
479 | })();
--------------------------------------------------------------------------------
/src/sky.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 | import {Sky} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/objects/Sky.js';
4 | import {Water} from 'https://cdn.jsdelivr.net/npm/three@0.125/examples/jsm/objects/Water.js';
5 |
6 |
7 | export const sky = (function() {
8 |
9 | class TerrainSky {
10 | constructor(params) {
11 | this._params = params;
12 | this._Init(params);
13 | }
14 |
15 | _Init(params) {
16 | const waterGeometry = new THREE.PlaneBufferGeometry(10000, 10000, 100, 100);
17 |
18 | this._water = new Water(
19 | waterGeometry,
20 | {
21 | textureWidth: 2048,
22 | textureHeight: 2048,
23 | waterNormals: new THREE.TextureLoader().load( 'resources/waternormals.jpg', function ( texture ) {
24 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
25 | } ),
26 | alpha: 0.5,
27 | sunDirection: new THREE.Vector3(1, 0, 0),
28 | sunColor: 0xffffff,
29 | waterColor: 0x001e0f,
30 | distortionScale: 0.0,
31 | fog: undefined
32 | }
33 | );
34 | // this._water.rotation.x = - Math.PI / 2;
35 | // this._water.position.y = 4;
36 |
37 | this._sky = new Sky();
38 | this._sky.scale.setScalar(10000);
39 |
40 | this._group = new THREE.Group();
41 | //this._group.add(this._water);
42 | this._group.add(this._sky);
43 |
44 | params.scene.add(this._group);
45 |
46 | params.guiParams.sky = {
47 | turbidity: 10.0,
48 | rayleigh: 2,
49 | mieCoefficient: 0.005,
50 | mieDirectionalG: 0.8,
51 | luminance: 1,
52 | };
53 |
54 | params.guiParams.sun = {
55 | inclination: 0.31,
56 | azimuth: 0.25,
57 | };
58 |
59 | const onShaderChange = () => {
60 | for (let k in params.guiParams.sky) {
61 | this._sky.material.uniforms[k].value = params.guiParams.sky[k];
62 | }
63 | for (let k in params.guiParams.general) {
64 | this._sky.material.uniforms[k].value = params.guiParams.general[k];
65 | }
66 | };
67 |
68 | const onSunChange = () => {
69 | var theta = Math.PI * (params.guiParams.sun.inclination - 0.5);
70 | var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5);
71 |
72 | const sunPosition = new THREE.Vector3();
73 | sunPosition.x = Math.cos(phi);
74 | sunPosition.y = Math.sin(phi) * Math.sin(theta);
75 | sunPosition.z = Math.sin(phi) * Math.cos(theta);
76 |
77 | this._sky.material.uniforms['sunPosition'].value.copy(sunPosition);
78 | this._water.material.uniforms['sunDirection'].value.copy(sunPosition.normalize());
79 | };
80 |
81 | const skyRollup = params.gui.addFolder('Sky');
82 | skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange(
83 | onShaderChange);
84 | skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange(
85 | onShaderChange);
86 | skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange(
87 | onShaderChange);
88 | skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange(
89 | onShaderChange);
90 | skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange(
91 | onShaderChange);
92 |
93 | const sunRollup = params.gui.addFolder('Sun');
94 | sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange(
95 | onSunChange);
96 | sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange(
97 | onSunChange);
98 |
99 | onShaderChange();
100 | onSunChange();
101 | }
102 |
103 | Update(timeInSeconds) {
104 | this._water.material.uniforms['time'].value += timeInSeconds;
105 |
106 | this._group.position.x = this._params.camera.position.x;
107 | this._group.position.z = this._params.camera.position.z;
108 | }
109 | }
110 |
111 |
112 | return {
113 | TerrainSky: TerrainSky
114 | }
115 | })();
116 |
--------------------------------------------------------------------------------
/src/spline.js:
--------------------------------------------------------------------------------
1 | export const spline = (function() {
2 |
3 | class _CubicHermiteSpline {
4 | constructor(lerp) {
5 | this._points = [];
6 | this._lerp = lerp;
7 | }
8 |
9 | AddPoint(t, d) {
10 | this._points.push([t, d]);
11 | }
12 |
13 | Get(t) {
14 | let p1 = 0;
15 |
16 | for (let i = 0; i < this._points.length; i++) {
17 | if (this._points[i][0] >= t) {
18 | break;
19 | }
20 | p1 = i;
21 | }
22 |
23 | const p0 = Math.max(0, p1 - 1);
24 | const p2 = Math.min(this._points.length - 1, p1 + 1);
25 | const p3 = Math.min(this._points.length - 1, p1 + 2);
26 |
27 | if (p1 == p2) {
28 | return this._points[p1][1];
29 | }
30 |
31 | return this._lerp(
32 | (t - this._points[p1][0]) / (
33 | this._points[p2][0] - this._points[p1][0]),
34 | this._points[p0][1], this._points[p1][1],
35 | this._points[p2][1], this._points[p3][1]);
36 | }
37 | };
38 |
39 | class _LinearSpline {
40 | constructor(lerp) {
41 | this._points = [];
42 | this._lerp = lerp;
43 | }
44 |
45 | AddPoint(t, d) {
46 | this._points.push([t, d]);
47 | }
48 |
49 | Get(t) {
50 | let p1 = 0;
51 |
52 | for (let i = 0; i < this._points.length; i++) {
53 | if (this._points[i][0] >= t) {
54 | break;
55 | }
56 | p1 = i;
57 | }
58 |
59 | const p2 = Math.min(this._points.length - 1, p1 + 1);
60 |
61 | if (p1 == p2) {
62 | return this._points[p1][1];
63 | }
64 |
65 | return this._lerp(
66 | (t - this._points[p1][0]) / (
67 | this._points[p2][0] - this._points[p1][0]),
68 | this._points[p1][1], this._points[p2][1]);
69 | }
70 | }
71 |
72 | return {
73 | CubicHermiteSpline: _CubicHermiteSpline,
74 | LinearSpline: _LinearSpline,
75 | };
76 | })();
77 |
--------------------------------------------------------------------------------
/src/terrain-builder-threaded-worker.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 | import {noise} from './noise.js';
4 | import {texture_splatter} from './texture-splatter.js' ;
5 | import {math} from './math.js';
6 |
7 |
8 | const _D = new THREE.Vector3();
9 | const _D1 = new THREE.Vector3();
10 | const _D2 = new THREE.Vector3();
11 | const _P = new THREE.Vector3();
12 | const _P1 = new THREE.Vector3();
13 | const _P2 = new THREE.Vector3();
14 | const _P3 = new THREE.Vector3();
15 | const _H = new THREE.Vector3();
16 | const _W = new THREE.Vector3();
17 | const _S = new THREE.Vector3();
18 | const _C = new THREE.Vector3();
19 |
20 | const _N = new THREE.Vector3();
21 | const _N1 = new THREE.Vector3();
22 | const _N2 = new THREE.Vector3();
23 | const _N3 = new THREE.Vector3();
24 |
25 |
26 | class _TerrainBuilderThreadedWorker {
27 | constructor() {
28 | }
29 |
30 | Init(params) {
31 | this.cachedParams_ = {...params};
32 | this.params_ = params;
33 | this.params_.offset = new THREE.Vector3(...params.offset);
34 | this.params_.origin = new THREE.Vector3(...params.origin);
35 | this.params_.noise = new noise.Noise(params.noiseParams);
36 | this.params_.heightGenerators = [
37 | new texture_splatter.HeightGenerator(
38 | this.params_.noise, params.offset,
39 | params.heightGeneratorsParams.min, params.heightGeneratorsParams.max)
40 | ];
41 |
42 | this.params_.biomeGenerator = new noise.Noise(params.biomesParams);
43 | this.params_.colourNoise = new noise.Noise(params.colourNoiseParams);
44 | this.params_.colourGenerator = new texture_splatter.TextureSplatter(
45 | {
46 | biomeGenerator: this.params_.biomeGenerator,
47 | colourNoise: this.params_.colourNoise
48 | });
49 | }
50 |
51 | _GenerateHeight(v) {
52 | return this.params_.heightGenerators[0].Get(v.x, v.y, v.z)[0];
53 | }
54 |
55 | GenerateNormals_(positions, indices) {
56 | const normals = new Array(positions.length).fill(0.0);
57 | for (let i = 0, n = indices.length; i < n; i+= 3) {
58 | const i1 = indices[i] * 3;
59 | const i2 = indices[i+1] * 3;
60 | const i3 = indices[i+2] * 3;
61 |
62 | _N1.fromArray(positions, i1);
63 | _N2.fromArray(positions, i2);
64 | _N3.fromArray(positions, i3);
65 |
66 | _D1.subVectors(_N3, _N2);
67 | _D2.subVectors(_N1, _N2);
68 | _D1.cross(_D2);
69 |
70 | normals[i1] += _D1.x;
71 | normals[i2] += _D1.x;
72 | normals[i3] += _D1.x;
73 |
74 | normals[i1+1] += _D1.y;
75 | normals[i2+1] += _D1.y;
76 | normals[i3+1] += _D1.y;
77 |
78 | normals[i1+2] += _D1.z;
79 | normals[i2+2] += _D1.z;
80 | normals[i3+2] += _D1.z;
81 | }
82 | return normals;
83 | }
84 |
85 | GenerateIndices_() {
86 | const resolution = this.params_.resolution + 2;
87 | const indices = [];
88 | for (let i = 0; i < resolution; i++) {
89 | for (let j = 0; j < resolution; j++) {
90 | indices.push(
91 | i * (resolution + 1) + j,
92 | (i + 1) * (resolution + 1) + j + 1,
93 | i * (resolution + 1) + j + 1);
94 | indices.push(
95 | (i + 1) * (resolution + 1) + j,
96 | (i + 1) * (resolution + 1) + j + 1,
97 | i * (resolution + 1) + j);
98 | }
99 | }
100 | return indices;
101 | }
102 |
103 | _ComputeNormal_CentralDifference(xp, yp, stepSize) {
104 | const localToWorld = this.params_.worldMatrix;
105 | const radius = this.params_.radius;
106 | const offset = this.params_.offset;
107 | const width = this.params_.width;
108 | const half = width / 2;
109 | const resolution = this.params_.resolution + 2;
110 | const effectiveResolution = resolution - 2;
111 |
112 | // Compute position
113 | const _ComputeWSPosition = (xpos, ypos) => {
114 | const xp = width * xpos;
115 | const yp = width * ypos;
116 | _P.set(xp - half, yp - half, radius);
117 | _P.add(offset);
118 | _P.normalize();
119 | _D.copy(_P);
120 | _D.transformDirection(localToWorld);
121 |
122 | _P.multiplyScalar(radius);
123 | _P.z -= radius;
124 | _P.applyMatrix4(localToWorld);
125 |
126 | // Purturb height along z-vector
127 | const height = this._GenerateHeight(_P);
128 | _H.copy(_D);
129 | _H.multiplyScalar(height);
130 | _P.add(_H);
131 |
132 | return _P;
133 | };
134 |
135 | const _ComputeWSPositionFromWS = (pos) => {
136 | _P.copy(pos);
137 | _P.normalize();
138 | _D.copy(_P);
139 | _P.multiplyScalar(radius);
140 |
141 | // Purturb height along z-vector
142 | const height = this._GenerateHeight(_P);
143 | _H.copy(_D);
144 | _H.multiplyScalar(height);
145 | _P.add(_H);
146 |
147 | return _P;
148 | };
149 |
150 | const _SphericalToCartesian = (theta, phi) => {
151 | const x = (Math.sin(theta) * Math.cos(phi));
152 | const y = (Math.sin(theta) * Math.sin(phi));
153 | const z = (Math.cos(theta));
154 | _P.set(x, y, z);
155 | _P.multiplyScalar(radius);
156 | const height = this._GenerateHeight(_P);
157 | _P.set(x, y, z);
158 | _P.multiplyScalar(height + radius);
159 | return _P;
160 | };
161 |
162 | //
163 | _P3.copy(_ComputeWSPosition(xp, yp));
164 | _D.copy(_P3);
165 | _D.normalize();
166 |
167 | const phi = Math.atan2(_D.y, _D.x);
168 | const theta = Math.atan2((_D.x * _D.x + _D.y * _D.y) ** 0.5, _D.z);
169 |
170 | _P1.copy(_ComputeWSPosition(xp, yp));
171 | _P2.copy(_SphericalToCartesian(theta, phi));
172 |
173 | // Fixme - Fixed size right now, calculate an appropriate delta
174 | const delta = 0.001;
175 |
176 | _P1.copy(_SphericalToCartesian(theta - delta, phi));
177 | _P2.copy(_SphericalToCartesian(theta + delta, phi));
178 | _D1.subVectors(_P1, _P2);
179 | _D1.normalize();
180 |
181 | _P1.copy(_SphericalToCartesian(theta, phi - delta));
182 | _P2.copy(_SphericalToCartesian(theta, phi + delta));
183 | _D2.subVectors(_P1, _P2);
184 | _D2.normalize();
185 |
186 |
187 | _P1.copy(_D1);
188 | _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution);
189 | _P2.copy(_P1);
190 | _P2.multiplyScalar(-1)
191 | _P1.add(_P3);
192 | _P2.add(_P3);
193 | _P1.copy(_ComputeWSPositionFromWS(_P1));
194 | _P2.copy(_ComputeWSPositionFromWS(_P2));
195 | _D1.subVectors(_P1, _P2);
196 | _D1.normalize();
197 |
198 | _P1.copy(_D2);
199 | _P1.multiplyScalar(-0.5*width*stepSize/effectiveResolution);
200 | _P2.copy(_P1);
201 | _P2.multiplyScalar(-1)
202 | _P1.add(_P3);
203 | _P2.add(_P3);
204 | _P1.copy(_ComputeWSPositionFromWS(_P1));
205 | _P2.copy(_ComputeWSPositionFromWS(_P2));
206 | _D2.subVectors(_P1, _P2);
207 | _D2.normalize();
208 |
209 | _D1.cross(_D2);
210 |
211 | return _D1;
212 | }
213 |
214 | RebuildEdgeNormals_(normals) {
215 | const resolution = this.params_.resolution + 2;
216 | const effectiveResolution = resolution - 2;
217 |
218 | let x = 1;
219 | for (let z = 1; z <= resolution-1; z+=1) {
220 | const i = x * (resolution + 1) + z;
221 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
222 | normals[i * 3 + 0] = _N.x;
223 | normals[i * 3 + 1] = _N.y;
224 | normals[i * 3 + 2] = _N.z;
225 | }
226 |
227 | let z = resolution - 1;
228 | for (let x = 1; x <= resolution-1; x+=1) {
229 | const i = (x) * (resolution + 1) + z;
230 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
231 | normals[i * 3 + 0] = _N.x;
232 | normals[i * 3 + 1] = _N.y;
233 | normals[i * 3 + 2] = _N.z;
234 | }
235 |
236 | x = resolution - 1;
237 | for (let z = 1; z <= resolution-1; z+=1) {
238 | const i = x * (resolution + 1) + z;
239 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
240 | normals[i * 3 + 0] = _N.x;
241 | normals[i * 3 + 1] = _N.y;
242 | normals[i * 3 + 2] = _N.z;
243 | }
244 |
245 | z = 1;
246 | for (let x = 1; x <= resolution-1; x+=1) {
247 | const i = (x) * (resolution + 1) + z;
248 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, 1))
249 | normals[i * 3 + 0] = _N.x;
250 | normals[i * 3 + 1] = _N.y;
251 | normals[i * 3 + 2] = _N.z;
252 | }
253 | }
254 |
255 | FixEdgesToMatchNeighbours_(positions, normals, colours) {
256 | const resolution = this.params_.resolution + 2;
257 | const effectiveResolution = resolution - 2;
258 |
259 | if (this.params_.neighbours[0] > 1) {
260 | const x = 1;
261 | const stride = this.params_.neighbours[0];
262 | for (let z = 1; z <= resolution-1; z+=1) {
263 | const i = x * (resolution + 1) + z;
264 | // colours[i * 3 + 0] = 0;
265 | // colours[i * 3 + 1] = 0;
266 | // colours[i * 3 + 2] = 1;
267 |
268 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
269 | normals[i * 3 + 0] = _N.x;
270 | normals[i * 3 + 1] = _N.y;
271 | normals[i * 3 + 2] = _N.z;
272 | }
273 | for (let z = 1; z <= resolution-1-stride; z+=stride) {
274 | const i1 = x * (resolution + 1) + z;
275 | const i2 = x * (resolution + 1) + (z + stride);
276 |
277 | for (let s = 1; s < stride; ++s) {
278 | const i = x * (resolution + 1) + z + s;
279 | const p = s / stride;
280 | for (let j = 0; j < 3; ++j) {
281 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
282 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
283 | }
284 | // colours[i * 3 + 0] = 0;
285 | // colours[i * 3 + 1] = 1;
286 | // colours[i * 3 + 2] = 0;
287 | }
288 | }
289 | }
290 |
291 | if (this.params_.neighbours[1] > 1) {
292 | const z = resolution - 1;
293 | const stride = this.params_.neighbours[1];
294 | for (let x = 1; x <= resolution-1; x+=1) {
295 | const i = (x) * (resolution + 1) + z;
296 | // colours[i * 3 + 0] = 0;
297 | // colours[i * 3 + 1] = 0;
298 | // colours[i * 3 + 2] = 1;
299 |
300 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
301 | normals[i * 3 + 0] = _N.x;
302 | normals[i * 3 + 1] = _N.y;
303 | normals[i * 3 + 2] = _N.z;
304 | }
305 | for (let x = 1; x <= resolution-1-stride; x+=stride) {
306 | const i1 = (x) * (resolution + 1) + z;
307 | const i2 = (x + stride) * (resolution + 1) + z;
308 |
309 | for (let s = 1; s < stride; ++s) {
310 | const i = (x + s) * (resolution + 1) + z;
311 | const p = s / stride;
312 | for (let j = 0; j < 3; ++j) {
313 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
314 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
315 | }
316 | // colours[i * 3 + 0] = 1;
317 | // colours[i * 3 + 1] = 1;
318 | // colours[i * 3 + 2] = 0;
319 | }
320 | }
321 | }
322 |
323 | if (this.params_.neighbours[2] > 1) {
324 | const x = resolution - 1;
325 | const stride = this.params_.neighbours[2];
326 | for (let z = 1; z <= resolution-1; z+=1) {
327 | const i = x * (resolution + 1) + z;
328 | // colours[i * 3 + 0] = 0;
329 | // colours[i * 3 + 1] = 0;
330 | // colours[i * 3 + 2] = 1;
331 |
332 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
333 | normals[i * 3 + 0] = _N.x;
334 | normals[i * 3 + 1] = _N.y;
335 | normals[i * 3 + 2] = _N.z;
336 | }
337 | for (let z = 1; z <= resolution-1-stride; z+=stride) {
338 | const i1 = x * (resolution + 1) + z;
339 | const i2 = x * (resolution + 1) + (z + stride);
340 |
341 | for (let s = 1; s < stride; ++s) {
342 | const i = x * (resolution + 1) + z + s;
343 | const p = s / stride;
344 | for (let j = 0; j < 3; ++j) {
345 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
346 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
347 | }
348 | // colours[i * 3 + 0] = 0;
349 | // colours[i * 3 + 1] = 1;
350 | // colours[i * 3 + 2] = 1;
351 | }
352 | }
353 | }
354 |
355 | if (this.params_.neighbours[3] > 1) {
356 | const z = 1;
357 | const stride = this.params_.neighbours[3];
358 | for (let x = 1; x <= resolution-1; x+=1) {
359 | const i = (x) * (resolution + 1) + z;
360 | // colours[i * 3 + 0] = 0;
361 | // colours[i * 3 + 1] = 0;
362 | // colours[i * 3 + 2] = 1;
363 |
364 | _N.copy(this._ComputeNormal_CentralDifference((x-1) / effectiveResolution, (z-1) / effectiveResolution, stride))
365 | normals[i * 3 + 0] = _N.x;
366 | normals[i * 3 + 1] = _N.y;
367 | normals[i * 3 + 2] = _N.z;
368 | }
369 | for (let x = 1; x <= resolution-1-stride; x+=stride) {
370 | const i1 = (x) * (resolution + 1) + z;
371 | const i2 = (x + stride) * (resolution + 1) + z;
372 |
373 | for (let s = 1; s < stride; ++s) {
374 | const i = (x + s) * (resolution + 1) + z;
375 | const p = s / stride;
376 | for (let j = 0; j < 3; ++j) {
377 | positions[i * 3 + j] = math.lerp(p, positions[i1 * 3 + j], positions[i2 * 3 + j]);
378 | normals[i * 3 + j] = math.lerp(p, normals[i1 * 3 + j], normals[i2 * 3 + j]);
379 | }
380 | // colours[i * 3 + 0] = 1;
381 | // colours[i * 3 + 1] = 0;
382 | // colours[i * 3 + 2] = 0;
383 | }
384 | }
385 | }
386 | }
387 |
388 | FixEdgeSkirt_(positions, up, normals) {
389 | const resolution = this.params_.resolution + 2;
390 |
391 | const _ApplyFix = (x, y, xp, yp) => {
392 | const skirtIndex = x * (resolution + 1) + y;
393 | const proxyIndex = xp * (resolution + 1) + yp;
394 |
395 | _P.fromArray(positions, proxyIndex * 3);
396 | _D.fromArray(up, proxyIndex * 3);
397 | _D.multiplyScalar(0);
398 | _P.add(_D);
399 | positions[skirtIndex * 3 + 0] = _P.x;
400 | positions[skirtIndex * 3 + 1] = _P.y;
401 | positions[skirtIndex * 3 + 2] = _P.z;
402 |
403 | // Normal will be fucked, copy it from proxy point
404 | normals[skirtIndex * 3 + 0] = normals[proxyIndex * 3 + 0];
405 | normals[skirtIndex * 3 + 1] = normals[proxyIndex * 3 + 1];
406 | normals[skirtIndex * 3 + 2] = normals[proxyIndex * 3 + 2];
407 | };
408 |
409 | for (let y = 0; y <= resolution; ++y) {
410 | _ApplyFix(0, y, 1, y);
411 | }
412 | for (let y = 0; y <= resolution; ++y) {
413 | _ApplyFix(resolution, y, resolution - 1, y);
414 | }
415 | for (let x = 0; x <= resolution; ++x) {
416 | _ApplyFix(x, 0, x, 1);
417 | }
418 | for (let x = 0; x <= resolution; ++x) {
419 | _ApplyFix(x, resolution, x, resolution - 1);
420 | }
421 | }
422 |
423 | NormalizeNormals_(normals) {
424 | for (let i = 0, n = normals.length; i < n; i+=3) {
425 | _N.fromArray(normals, i);
426 | _N.normalize();
427 | normals[i] = _N.x;
428 | normals[i+1] = _N.y;
429 | normals[i+2] = _N.z;
430 | }
431 | }
432 |
433 | RebuildEdgePositions_(positions) {
434 | const localToWorld = this.params_.worldMatrix;
435 | const resolution = this.params_.resolution + 2;
436 | const radius = this.params_.radius;
437 | const offset = this.params_.offset;
438 | const origin = this.params_.origin;
439 | const width = this.params_.width;
440 | const half = width / 2;
441 | const effectiveResolution = resolution - 2;
442 |
443 | const _ComputeOriginOffsetPosition = (xpos, ypos) => {
444 | const xp = width * xpos;
445 | const yp = width * ypos;
446 | _P.set(xp - half, yp - half, radius);
447 | _P.add(offset);
448 | _P.normalize();
449 | _D.copy(_P);
450 | _D.transformDirection(localToWorld);
451 |
452 | _P.multiplyScalar(radius);
453 | _P.z -= radius;
454 | _P.applyMatrix4(localToWorld);
455 |
456 | // Keep the absolute world space position to sample noise
457 | _W.copy(_P);
458 |
459 | // Move the position relative to the origin
460 | _P.sub(origin);
461 |
462 | // Purturb height along z-vector
463 | const height = this._GenerateHeight(_W);
464 | _H.copy(_D);
465 | _H.multiplyScalar(height);
466 | _P.add(_H);
467 |
468 | return _P;
469 | }
470 |
471 | let x = 1;
472 | for (let z = 1; z <= resolution-1; z++) {
473 | const i = x * (resolution + 1) + z;
474 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
475 | positions[i * 3 + 0] = p.x;
476 | positions[i * 3 + 1] = p.y;
477 | positions[i * 3 + 2] = p.z;
478 | }
479 |
480 | let z = resolution - 1;
481 | for (let x = 1; x <= resolution-1; x++) {
482 | const i = (x) * (resolution + 1) + z;
483 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
484 | positions[i * 3 + 0] = p.x;
485 | positions[i * 3 + 1] = p.y;
486 | positions[i * 3 + 2] = p.z;
487 | }
488 |
489 | x = resolution - 1;
490 | for (let z = 1; z <= resolution-1; z++) {
491 | const i = x * (resolution + 1) + z;
492 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
493 | positions[i * 3 + 0] = p.x;
494 | positions[i * 3 + 1] = p.y;
495 | positions[i * 3 + 2] = p.z;
496 | }
497 |
498 | z = 1;
499 | for (let x = 1; x <= resolution-1; x++) {
500 | const i = (x) * (resolution + 1) + z;
501 | const p = _ComputeOriginOffsetPosition((x-1) / effectiveResolution, (z-1) / effectiveResolution);
502 | positions[i * 3 + 0] = p.x;
503 | positions[i * 3 + 1] = p.y;
504 | positions[i * 3 + 2] = p.z;
505 | }
506 | }
507 |
508 | Rebuild() {
509 | const positions = [];
510 | const up = [];
511 | const coords = [];
512 |
513 | const localToWorld = this.params_.worldMatrix;
514 | const resolution = this.params_.resolution + 2;
515 | const radius = this.params_.radius;
516 | const offset = this.params_.offset;
517 | const origin = this.params_.origin;
518 | const width = this.params_.width;
519 | const half = width / 2;
520 | const effectiveResolution = resolution - 2;
521 |
522 | for (let x = -1; x <= effectiveResolution + 1; x++) {
523 | const xp = width * x / effectiveResolution;
524 | for (let y = -1; y <= effectiveResolution + 1; y++) {
525 | const yp = width * y / effectiveResolution;
526 |
527 | // Compute position
528 | _P.set(xp - half, yp - half, radius);
529 | _P.add(offset);
530 | _P.normalize();
531 | _D.copy(_P);
532 | _D.transformDirection(localToWorld);
533 |
534 | _P.multiplyScalar(radius);
535 | _P.z -= radius;
536 | _P.applyMatrix4(localToWorld);
537 |
538 | // Keep the absolute world space position to sample noise
539 | _W.copy(_P);
540 |
541 | // Move the position relative to the origin
542 | _P.sub(origin);
543 |
544 | // Purturb height along z-vector
545 | const height = this._GenerateHeight(_W);
546 | _H.copy(_D);
547 | _H.multiplyScalar(height);
548 | _P.add(_H);
549 |
550 | positions.push(_P.x, _P.y, _P.z);
551 |
552 | _C.copy(_W);
553 | _C.add(_H);
554 | coords.push(_C.x, _C.y, _C.z);
555 |
556 | _S.set(_W.x, _W.y, height);
557 |
558 | up.push(_D.x, _D.y, _D.z);
559 | }
560 | }
561 |
562 | const colours = new Array(positions.length).fill(1.0);
563 |
564 | // Generate indices
565 | const indices = this.GenerateIndices_();
566 | const normals = this.GenerateNormals_(positions, indices);
567 |
568 | this.RebuildEdgePositions_(positions);
569 | this.RebuildEdgeNormals_(normals);
570 | this.FixEdgesToMatchNeighbours_(positions, normals, colours);
571 | this.FixEdgeSkirt_(positions, up, normals);
572 | this.NormalizeNormals_(normals);
573 |
574 | const bytesInFloat32 = 4;
575 | const bytesInInt32 = 4;
576 | const positionsArray = new Float32Array(
577 | new SharedArrayBuffer(bytesInFloat32 * positions.length));
578 | const coloursArray = new Float32Array(
579 | new SharedArrayBuffer(bytesInFloat32 * colours.length));
580 | const normalsArray = new Float32Array(
581 | new SharedArrayBuffer(bytesInFloat32 * normals.length));
582 | const coordsArray = new Float32Array(
583 | new SharedArrayBuffer(bytesInFloat32 * coords.length));
584 | const indicesArray = new Uint32Array(
585 | new SharedArrayBuffer(bytesInInt32 * indices.length));
586 |
587 | positionsArray.set(positions, 0);
588 | coloursArray.set(colours, 0);
589 | normalsArray.set(normals, 0);
590 | coordsArray.set(coords, 0);
591 | indicesArray.set(indices, 0);
592 |
593 | return {
594 | positions: positionsArray,
595 | colours: coloursArray,
596 | normals: normalsArray,
597 | coords: coordsArray,
598 | indices: indicesArray,
599 | };
600 | }
601 |
602 | QuickRebuild(mesh) {
603 | const positions = mesh.positions;
604 | const normals = mesh.normals;
605 | const colours = mesh.colours;
606 | const up = [];
607 | const indices = mesh.indices;
608 |
609 | const localToWorld = this.params_.worldMatrix;
610 | const resolution = this.params_.resolution + 2;
611 | const radius = this.params_.radius;
612 | const offset = this.params_.offset;
613 | const origin = this.params_.origin;
614 | const width = this.params_.width;
615 | const half = width / 2;
616 | const effectiveResolution = resolution - 2;
617 |
618 | colours.fill(1.0);
619 |
620 | this.RebuildEdgePositions_(positions);
621 | this.RebuildEdgeNormals_(normals);
622 | this.FixEdgesToMatchNeighbours_(positions, normals, colours);
623 | this.FixEdgeSkirt_(positions, up, normals);
624 | this.NormalizeNormals_(normals);
625 |
626 | return mesh;
627 | }
628 | }
629 |
630 | const _CHUNK = new _TerrainBuilderThreadedWorker();
631 |
632 | self.onmessage = (msg) => {
633 | if (msg.data.subject == 'build_chunk') {
634 | _CHUNK.Init(msg.data.params);
635 |
636 | const rebuiltData = _CHUNK.Rebuild();
637 | self.postMessage({subject: 'build_chunk_result', data: rebuiltData});
638 | } else if (msg.data.subject == 'rebuild_chunk') {
639 | _CHUNK.Init(msg.data.params);
640 |
641 | const rebuiltData = _CHUNK.QuickRebuild(msg.data.mesh);
642 | self.postMessage({subject: 'quick_rebuild_chunk_result', data: rebuiltData});
643 | }
644 | };
--------------------------------------------------------------------------------
/src/terrain-builder-threaded.js:
--------------------------------------------------------------------------------
1 |
2 | import {terrain_chunk} from './terrain-chunk.js';
3 |
4 |
5 | export const terrain_builder_threaded = (function() {
6 |
7 | const _NUM_WORKERS = 7;
8 |
9 | let _IDs = 0;
10 |
11 | class WorkerThread {
12 | constructor(s) {
13 | this.worker_ = new Worker(s, {type: 'module'});
14 | this.worker_.onmessage = (e) => {
15 | this._OnMessage(e);
16 | };
17 | this._resolve = null;
18 | this._id = _IDs++;
19 | }
20 |
21 | _OnMessage(e) {
22 | const resolve = this._resolve;
23 | this._resolve = null;
24 | resolve(e.data);
25 | }
26 |
27 | get id() {
28 | return this._id;
29 | }
30 |
31 | postMessage(s, resolve) {
32 | this._resolve = resolve;
33 | this.worker_.postMessage(s);
34 | }
35 | }
36 |
37 | class WorkerThreadPool {
38 | constructor(sz, entry) {
39 | this.workers_ = [...Array(sz)].map(_ => new WorkerThread(entry));
40 | this.free_ = [...this.workers_];
41 | this.busy_ = {};
42 | this.queue_ = [];
43 | }
44 |
45 | get length() {
46 | return this.workers_.length;
47 | }
48 |
49 | get Busy() {
50 | return this.queue_.length > 0 || Object.keys(this.busy_).length > 0;
51 | }
52 |
53 | Enqueue(workItem, resolve) {
54 | this.queue_.push([workItem, resolve]);
55 | this._PumpQueue();
56 | }
57 |
58 | _PumpQueue() {
59 | while (this.free_.length > 0 && this.queue_.length > 0) {
60 | const w = this.free_.pop();
61 | this.busy_[w.id] = w;
62 |
63 | const [workItem, workResolve] = this.queue_.shift();
64 |
65 | w.postMessage(workItem, (v) => {
66 | delete this.busy_[w.id];
67 | this.free_.push(w);
68 | workResolve(v);
69 | this._PumpQueue();
70 | });
71 | }
72 | }
73 | }
74 |
75 | class _TerrainChunkRebuilder_Threaded {
76 | constructor(params) {
77 | this.pool_ = {};
78 | this.old_ = [];
79 |
80 | this.workerPool_ = new WorkerThreadPool(
81 | _NUM_WORKERS, 'src/terrain-builder-threaded-worker.js');
82 |
83 | this.params_ = params;
84 | }
85 |
86 | _OnResult(chunk, msg) {
87 | if (msg.subject == 'build_chunk_result') {
88 | chunk.RebuildMeshFromData(msg.data);
89 | } else if (msg.subject == 'quick_rebuild_chunk_result') {
90 | chunk.QuickRebuildMeshFromData(msg.data);
91 | }
92 | }
93 |
94 | AllocateChunk(params) {
95 | const w = params.width;
96 |
97 | if (!(w in this.pool_)) {
98 | this.pool_[w] = [];
99 | }
100 |
101 | let c = null;
102 | if (this.pool_[w].length > 0) {
103 | c = this.pool_[w].pop();
104 | c.params_ = params;
105 | } else {
106 | c = new terrain_chunk.TerrainChunk(params);
107 | }
108 |
109 | c.Hide();
110 |
111 | const threadedParams = {
112 | noiseParams: params.noiseParams,
113 | colourNoiseParams: params.colourNoiseParams,
114 | biomesParams: params.biomesParams,
115 | colourGeneratorParams: params.colourGeneratorParams,
116 | heightGeneratorsParams: params.heightGeneratorsParams,
117 | width: params.width,
118 | neighbours: params.neighbours,
119 | offset: params.offset.toArray(),
120 | origin: params.origin.toArray(),
121 | radius: params.radius,
122 | resolution: params.resolution,
123 | worldMatrix: params.transform,
124 | };
125 |
126 | const msg = {
127 | subject: 'build_chunk',
128 | params: threadedParams,
129 | };
130 |
131 | this.workerPool_.Enqueue(msg, (m) => {
132 | this._OnResult(c, m);
133 | });
134 |
135 | return c;
136 | }
137 |
138 | RetireChunks(chunks) {
139 | this.old_.push(...chunks);
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 | get Busy() {
153 | return this.workerPool_.Busy;
154 | }
155 |
156 | Rebuild(chunks) {
157 | for (let k in chunks) {
158 | this.workerPool_.Enqueue(chunks[k].chunk.params_);
159 | }
160 | }
161 |
162 | QuickRebuild(chunks) {
163 | for (let k in chunks) {
164 | const chunk = chunks[k];
165 | const params = chunk.chunk.params_;
166 |
167 | const threadedParams = {
168 | noiseParams: params.noiseParams,
169 | colourNoiseParams: params.colourNoiseParams,
170 | biomesParams: params.biomesParams,
171 | colourGeneratorParams: params.colourGeneratorParams,
172 | heightGeneratorsParams: params.heightGeneratorsParams,
173 | width: params.width,
174 | neighbours: params.neighbours,
175 | offset: params.offset.toArray(),
176 | origin: params.origin.toArray(),
177 | radius: params.radius,
178 | resolution: params.resolution,
179 | worldMatrix: params.transform,
180 | };
181 |
182 | const msg = {
183 | subject: 'rebuild_chunk',
184 | params: threadedParams,
185 | mesh: chunk.chunk.rebuildData_,
186 | };
187 |
188 | this.workerPool_.Enqueue(msg, (m) => {
189 | this._OnResult(chunk.chunk, m);
190 | });
191 | }
192 | }
193 |
194 | Update() {
195 | if (!this.Busy) {
196 | this._RecycleChunks(this.old_);
197 | this.old_ = [];
198 | }
199 | }
200 | }
201 |
202 | return {
203 | TerrainChunkRebuilder_Threaded: _TerrainChunkRebuilder_Threaded
204 | }
205 | })();
206 |
--------------------------------------------------------------------------------
/src/terrain-builder.js:
--------------------------------------------------------------------------------
1 | import {terrain_chunk} from './terrain-chunk.js';
2 |
3 |
4 | export const terrain_builder = (function() {
5 |
6 | class _TerrainChunkRebuilder {
7 | constructor(params) {
8 | this._pool = {};
9 | this._params = params;
10 | this._Reset();
11 | }
12 |
13 | AllocateChunk(params) {
14 | const w = params.width;
15 |
16 | if (!(w in this._pool)) {
17 | this._pool[w] = [];
18 | }
19 |
20 | let c = null;
21 | if (this._pool[w].length > 0) {
22 | c = this._pool[w].pop();
23 | c._params = params;
24 | } else {
25 | c = new terrain_chunk.TerrainChunk(params);
26 | }
27 |
28 | c.Hide();
29 |
30 | this._queued.push(c);
31 |
32 | return c;
33 | }
34 |
35 | RetireChunks(chunks) {
36 | this._old.push(...chunks);
37 | }
38 |
39 | _RecycleChunks(chunks) {
40 | for (let c of chunks) {
41 | if (!(c.chunk._params.width in this._pool)) {
42 | this._pool[c.chunk._params.width] = [];
43 | }
44 |
45 | c.chunk.Destroy();
46 | }
47 | }
48 |
49 | _Reset() {
50 | this._active = null;
51 | this._queued = [];
52 | this._old = [];
53 | this._new = [];
54 | }
55 |
56 | get Busy() {
57 | return this._active || this._queued.length > 0;
58 | }
59 |
60 | Rebuild(chunks) {
61 | if (this.Busy) {
62 | return;
63 | }
64 | for (let k in chunks) {
65 | this._queued.push(chunks[k].chunk);
66 | }
67 | }
68 |
69 | Update() {
70 | if (this._active) {
71 | const r = this._active.next();
72 | if (r.done) {
73 | this._active = null;
74 | }
75 | } else {
76 | const b = this._queued.pop();
77 | if (b) {
78 | this._active = b._Rebuild();
79 | this._new.push(b);
80 | }
81 | }
82 |
83 | if (this._active) {
84 | return;
85 | }
86 |
87 | if (!this._queued.length) {
88 | this._RecycleChunks(this._old);
89 | for (let b of this._new) {
90 | b.Show();
91 | }
92 | this._Reset();
93 | }
94 | }
95 | }
96 |
97 | return {
98 | TerrainChunkRebuilder: _TerrainChunkRebuilder
99 | }
100 | })();
101 |
--------------------------------------------------------------------------------
/src/terrain-chunk.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/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.mesh_);
14 | }
15 |
16 | Hide() {
17 | this.mesh_.visible = false;
18 | }
19 |
20 | Show() {
21 | this.mesh_.visible = true;
22 | }
23 |
24 | _Init(params) {
25 | this.geometry_ = new THREE.BufferGeometry();
26 | this.mesh_ = new THREE.Mesh(this.geometry_, params.material);
27 | this.mesh_.castShadow = false;
28 | this.mesh_.receiveShadow = true;
29 | this.mesh_.frustumCulled = false;
30 | this.params_.group.add(this.mesh_);
31 | this.Reinit(params);
32 | }
33 |
34 | Update(cameraPosition) {
35 | this.mesh_.position.copy(this.params_.origin);
36 | this.mesh_.position.sub(cameraPosition);
37 | }
38 |
39 | Reinit(params) {
40 | this.params_ = params;
41 | this.mesh_.position.set(0, 0, 0);
42 | }
43 |
44 | SetWireframe(b) {
45 | this.mesh_.material.wireframe = b;
46 | }
47 |
48 | RebuildMeshFromData(data) {
49 | this.geometry_.setAttribute(
50 | 'position', new THREE.Float32BufferAttribute(data.positions, 3));
51 | this.geometry_.setAttribute(
52 | 'color', new THREE.Float32BufferAttribute(data.colours, 3));
53 | this.geometry_.setAttribute(
54 | 'normal', new THREE.Float32BufferAttribute(data.normals, 3));
55 | this.geometry_.setAttribute(
56 | 'coords', new THREE.Float32BufferAttribute(data.coords, 3));
57 | this.geometry_.setIndex(
58 | new THREE.BufferAttribute(data.indices, 1));
59 | this.rebuildData_ = data;
60 | this.geometry_.attributes.position.needsUpdate = true;
61 | this.geometry_.attributes.normal.needsUpdate = true;
62 | this.geometry_.attributes.color.needsUpdate = true;
63 | this.geometry_.attributes.coords.needsUpdate = true;
64 | }
65 |
66 | QuickRebuildMeshFromData(data) {
67 | this.geometry_.attributes.position.array.set(data.positions, 0)
68 | this.geometry_.attributes.normal.array.set(data.normals, 0)
69 | this.geometry_.attributes.color.array.set(data.colours, 0)
70 | this.geometry_.attributes.position.needsUpdate = true;
71 | this.geometry_.attributes.normal.needsUpdate = true;
72 | this.geometry_.attributes.color.needsUpdate = true;
73 | }
74 | }
75 |
76 | return {
77 | TerrainChunk: TerrainChunk
78 | }
79 | })();
--------------------------------------------------------------------------------
/src/terrain-constants.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export const terrain_constants = (function() {
4 | return {
5 | QT_MIN_CELL_SIZE: 25,
6 | QT_MIN_CELL_RESOLUTION: 48,
7 | PLANET_RADIUS: 400000.0,
8 |
9 | NOISE_HEIGHT: 20000.0,
10 | NOISE_SCALE: 18000.0,
11 | }
12 | })();
13 |
--------------------------------------------------------------------------------
/src/terrain-shader.js:
--------------------------------------------------------------------------------
1 | export const terrain_shader = (function() {
2 |
3 | const _VS = `#version 300 es
4 |
5 | precision highp float;
6 |
7 | uniform mat4 modelMatrix;
8 | uniform mat4 modelViewMatrix;
9 | uniform mat4 viewMatrix;
10 | uniform mat4 projectionMatrix;
11 | uniform vec3 cameraPosition;
12 | uniform float fogDensity;
13 | uniform vec3 cloudScale;
14 |
15 | // Attributes
16 | in vec3 position;
17 | in vec3 normal;
18 | in vec3 coords;
19 | in vec3 color;
20 |
21 | // Outputs
22 | out vec4 vColor;
23 | out vec3 vNormal;
24 | out vec3 vCoords;
25 | out vec3 vVSPos;
26 | out vec3 vRepeatingCoords;
27 | out float vFragDepth;
28 |
29 | #define saturate(a) clamp( a, 0.0, 1.0 )
30 |
31 | void main(){
32 | mat4 terrainMatrix = mat4(
33 | viewMatrix[0],
34 | viewMatrix[1],
35 | viewMatrix[2],
36 | vec4(0.0, 0.0, 0.0, 1.0));
37 |
38 | gl_Position = projectionMatrix * terrainMatrix * modelMatrix * vec4(position, 1.0);
39 |
40 | vNormal = normal;
41 |
42 | vColor = vec4(color, 1);
43 | vCoords = (modelMatrix * vec4(position, 1.0)).xyz + cameraPosition;
44 | vVSPos = (terrainMatrix * modelMatrix * vec4(position, 1.0)).xyz;
45 |
46 | vec3 pos = coords;
47 | float p = 32768.0;
48 | float a = 1024.0;
49 | vRepeatingCoords = (4.0 * a / p) * abs(mod(pos, p) - p * 0.5);
50 |
51 | vFragDepth = 1.0 + gl_Position.w;
52 | }
53 | `;
54 |
55 |
56 | const _PS = `#version 300 es
57 |
58 | precision highp float;
59 | precision highp int;
60 | precision highp sampler2DArray;
61 |
62 | uniform sampler2DArray normalMap;
63 | uniform sampler2DArray diffuseMap;
64 | uniform sampler2D noiseMap;
65 |
66 | uniform mat4 modelMatrix;
67 | uniform mat4 modelViewMatrix;
68 | uniform vec3 cameraPosition;
69 | uniform float logDepthBufFC;
70 |
71 | in vec4 vColor;
72 | in vec3 vNormal;
73 | in vec3 vCoords;
74 | in vec3 vRepeatingCoords;
75 | in vec3 vVSPos;
76 | in float vFragDepth;
77 |
78 | out vec4 out_FragColor;
79 |
80 | #define saturate(a) clamp( a, 0.0, 1.0 )
81 |
82 | const float _TRI_SCALE = 10.0;
83 |
84 | float sum( vec3 v ) { return v.x+v.y+v.z; }
85 |
86 | vec4 hash4( vec2 p ) {
87 | return fract(
88 | sin(vec4(1.0+dot(p,vec2(37.0,17.0)),
89 | 2.0+dot(p,vec2(11.0,47.0)),
90 | 3.0+dot(p,vec2(41.0,29.0)),
91 | 4.0+dot(p,vec2(23.0,31.0))))*103.0);
92 | }
93 |
94 | vec4 _CalculateLighting(
95 | vec3 lightDirection, vec3 lightColour, vec3 worldSpaceNormal, vec3 viewDirection) {
96 | float NdotL = saturate(dot(worldSpaceNormal, lightDirection));
97 | // return vec4(lightColour * diffuse, 0.0);
98 |
99 | vec3 H = normalize(lightDirection + viewDirection);
100 | float NdotH = dot(worldSpaceNormal, H);
101 | float specular = saturate(pow(NdotH, 8.0));
102 |
103 | return vec4(lightColour * NdotL, specular * NdotL);
104 | }
105 |
106 | vec4 _ComputeLighting(vec3 worldSpaceNormal, vec3 sunDir, vec3 viewDirection) {
107 | // Hardcoded, whee!
108 | vec4 lighting;
109 |
110 | lighting += _CalculateLighting(
111 | sunDir, vec3(1.0, 1.0, 1.0), worldSpaceNormal, viewDirection);
112 | // lighting += _CalculateLighting(
113 | // vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection);
114 |
115 | // lighting += vec4(0.15, 0.15, 0.15, 0.0);
116 |
117 | return lighting;
118 | }
119 |
120 | vec4 _TerrainBlend_4(vec4 samples[4]) {
121 | float depth = 0.2;
122 | float ma = max(
123 | samples[0].w,
124 | max(
125 | samples[1].w,
126 | max(samples[2].w, samples[3].w))) - depth;
127 |
128 | float b1 = max(samples[0].w - ma, 0.0);
129 | float b2 = max(samples[1].w - ma, 0.0);
130 | float b3 = max(samples[2].w - ma, 0.0);
131 | float b4 = max(samples[3].w - ma, 0.0);
132 |
133 | vec4 numer = (
134 | samples[0] * b1 + samples[1] * b2 +
135 | samples[2] * b3 + samples[3] * b4);
136 | float denom = (b1 + b2 + b3 + b4);
137 | return numer / denom;
138 | }
139 |
140 | vec4 _TerrainBlend_4_lerp(vec4 samples[4]) {
141 | return (
142 | samples[0] * samples[0].w + samples[1] * samples[1].w +
143 | samples[2] * samples[2].w + samples[3] * samples[3].w);
144 | }
145 |
146 | // Lifted from https://www.shadertoy.com/view/Xtl3zf
147 | vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) {
148 | float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup
149 | float l = k*8.0;
150 | float f = fract(l);
151 |
152 | float ia = floor(l+0.5); // suslik's method (see comments)
153 | float ib = floor(l);
154 | f = min(f, 1.0-f)*2.0;
155 |
156 | vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
157 | vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
158 |
159 | vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z));
160 | vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z));
161 |
162 | return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz)));
163 | }
164 |
165 | vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
166 | vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
167 | vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
168 | vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
169 |
170 | vec3 weights = abs(normal.xyz);
171 | weights = weights / (weights.x + weights.y + weights.z);
172 |
173 | return dx * weights.x + dy * weights.y + dz * weights.z;
174 | }
175 |
176 | vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
177 | // Tangent Reconstruction
178 | // Triplanar uvs
179 | vec2 uvX = pos.zy; // x facing plane
180 | vec2 uvY = pos.xz; // y facing plane
181 | vec2 uvZ = pos.xy; // z facing plane
182 | // Tangent space normal maps
183 | vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
184 | vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
185 | vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
186 |
187 | vec3 weights = abs(normal.xyz);
188 | weights = weights / (weights.x + weights.y + weights.z);
189 |
190 | // Get the sign (-1 or 1) of the surface normal
191 | vec3 axis = sign(normal);
192 | // Construct tangent to world matrices for each axis
193 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
194 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
195 | mat3 tbnX = mat3(tangentX, bitangentX, normal);
196 |
197 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
198 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
199 | mat3 tbnY = mat3(tangentY, bitangentY, normal);
200 |
201 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
202 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
203 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
204 |
205 | // Apply tangent to world matrix and triblend
206 | // Using clamp() because the cross products may be NANs
207 | vec3 worldNormal = normalize(
208 | clamp(tbnX * tx, -1.0, 1.0) * weights.x +
209 | clamp(tbnY * ty, -1.0, 1.0) * weights.y +
210 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z
211 | );
212 | return vec4(worldNormal, 0.0);
213 | }
214 |
215 | vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
216 | vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
217 | vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
218 | vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
219 |
220 | vec3 weights = abs(normal.xyz);
221 | weights = weights / (weights.x + weights.y + weights.z);
222 |
223 | return dx * weights.x + dy * weights.y + dz * weights.z;
224 | }
225 |
226 | vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
227 | vec2 uvx = pos.zy;
228 | vec2 uvy = pos.xz;
229 | vec2 uvz = pos.xy;
230 | vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
231 | vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
232 | vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
233 |
234 | vec3 weights = abs(normal.xyz);
235 | weights *= weights;
236 | weights = weights / (weights.x + weights.y + weights.z);
237 |
238 | vec3 axis = sign(normal);
239 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
240 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
241 | mat3 tbnX = mat3(tangentX, bitangentX, normal);
242 |
243 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
244 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
245 | mat3 tbnY = mat3(tangentY, bitangentY, normal);
246 |
247 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
248 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
249 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
250 |
251 | vec3 worldNormal = normalize(
252 | clamp(tbnX * tx, -1.0, 1.0) * weights.x +
253 | clamp(tbnY * ty, -1.0, 1.0) * weights.y +
254 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z);
255 | return vec4(worldNormal, 0.0);
256 | }
257 |
258 | void main() {
259 | vec3 worldPosition = vCoords;
260 | vec3 eyeDirection = normalize(worldPosition - cameraPosition);
261 | vec3 sunDir = normalize(vec3(1, 1, -1));
262 | vec3 worldSpaceNormal = normalize(vNormal);
263 |
264 | // Bit of a hack to remove lighting on dark side of planet
265 | vec3 diffuse = vec3(0.75);
266 | vec3 planetNormal = normalize(worldPosition);
267 | float planetLighting = saturate(dot(planetNormal, sunDir));
268 |
269 | vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection);
270 | vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse + lighting.w * 0.1;
271 | // vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25);
272 |
273 | finalColour *= lighting.xyz;
274 | finalColour = lighting.xyz;
275 | // finalColour = vColor.xyz;
276 |
277 | out_FragColor = vec4(finalColour, 1);
278 | gl_FragDepth = log2(vFragDepth) * logDepthBufFC * 0.5;
279 | }
280 |
281 | `;
282 |
283 | return {
284 | VS: _VS,
285 | PS: _PS,
286 | };
287 | })();
288 |
--------------------------------------------------------------------------------
/src/terrain.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 | import {noise} from './noise.js';
4 | import {quadtree} from './quadtree.js';
5 | import {terrain_shader} from './terrain-shader.js';
6 | import {terrain_builder_threaded} from './terrain-builder-threaded.js';
7 | import {terrain_constants} from './terrain-constants.js';
8 | import {texture_splatter} from './texture-splatter.js';
9 | import {textures} from './textures.js';
10 | import {utils} from './utils.js';
11 |
12 | export const terrain = (function() {
13 |
14 | class TerrainChunkManager {
15 | constructor(params) {
16 | this._Init(params);
17 | }
18 |
19 | _Init(params) {
20 | this.params_ = params;
21 |
22 | this.builder_ = new terrain_builder_threaded.TerrainChunkRebuilder_Threaded();
23 | // this.builder_ = new terrainbuilder_.TerrainChunkRebuilder();
24 |
25 | this.LoadTextures_();
26 |
27 | this.InitNoise_(params);
28 | this.InitBiomes_(params);
29 | this.InitTerrain_(params);
30 | }
31 |
32 | LoadTextures_() {
33 | const loader = new THREE.TextureLoader();
34 |
35 | const noiseTexture = loader.load('./resources/simplex-noise.png');
36 | noiseTexture.wrapS = THREE.RepeatWrapping;
37 | noiseTexture.wrapT = THREE.RepeatWrapping;
38 |
39 | this.material_ = new THREE.RawShaderMaterial({
40 | uniforms: {
41 | diffuseMap: {
42 | },
43 | normalMap: {
44 | },
45 | noiseMap: {
46 | value: noiseTexture
47 | },
48 | logDepthBufFC: {
49 | value: 2.0 / (Math.log(this.params_.camera.far + 1.0) / Math.LN2),
50 | }
51 | },
52 | vertexShader: terrain_shader.VS,
53 | fragmentShader: terrain_shader.PS,
54 | side: THREE.FrontSide
55 | });
56 | }
57 |
58 | InitNoise_(params) {
59 | params.guiParams.noise = {
60 | octaves: 13,
61 | persistence: 0.5,
62 | lacunarity: 1.6,
63 | exponentiation: 7.5,
64 | height: terrain_constants.NOISE_HEIGHT,
65 | scale: terrain_constants.NOISE_SCALE,
66 | seed: 1
67 | };
68 |
69 | const onNoiseChanged = () => {
70 | this.builder_.Rebuild(this.chunks_);
71 | };
72 |
73 | const noiseRollup = params.gui.addFolder('Terrain.Noise');
74 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange(
75 | onNoiseChanged);
76 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange(
77 | onNoiseChanged);
78 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange(
79 | onNoiseChanged);
80 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange(
81 | onNoiseChanged);
82 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange(
83 | onNoiseChanged);
84 | noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange(
85 | onNoiseChanged);
86 |
87 | this.noise_ = new noise.Noise(params.guiParams.noise);
88 | this.noiseParams_ = params.guiParams.noise;
89 |
90 | params.guiParams.heightmap = {
91 | height: 16,
92 | };
93 |
94 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap');
95 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange(
96 | onNoiseChanged);
97 | }
98 |
99 | InitBiomes_(params) {
100 | params.guiParams.biomes = {
101 | octaves: 2,
102 | persistence: 0.5,
103 | lacunarity: 2.0,
104 | scale: 2048.0,
105 | noiseType: 'simplex',
106 | seed: 2,
107 | exponentiation: 1,
108 | height: 1.0
109 | };
110 |
111 | const onNoiseChanged = () => {
112 | this.builder_.Rebuild(this.chunks_);
113 | };
114 |
115 | const noiseRollup = params.gui.addFolder('Terrain.Biomes');
116 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange(
117 | onNoiseChanged);
118 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange(
119 | onNoiseChanged);
120 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange(
121 | onNoiseChanged);
122 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange(
123 | onNoiseChanged);
124 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange(
125 | onNoiseChanged);
126 |
127 | this.biomes_ = new noise.Noise(params.guiParams.biomes);
128 | this.biomesParams_ = params.guiParams.biomes;
129 |
130 | const colourParams = {
131 | octaves: 1,
132 | persistence: 0.5,
133 | lacunarity: 2.0,
134 | exponentiation: 1.0,
135 | scale: 256.0,
136 | noiseType: 'simplex',
137 | seed: 2,
138 | height: 1.0,
139 | };
140 | this.colourNoise_ = new noise.Noise(colourParams);
141 | this.colourNoiseParams_ = colourParams;
142 | }
143 |
144 | InitTerrain_(params) {
145 | params.guiParams.terrain = {
146 | wireframe: false,
147 | fixedCamera: false,
148 | };
149 |
150 | this.groups_ = [...new Array(6)].map(_ => new THREE.Group());
151 | params.scene.add(...this.groups_);
152 |
153 | const terrainRollup = params.gui.addFolder('Terrain');
154 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => {
155 | for (let k in this.chunks_) {
156 | this.chunks_[k].chunk.SetWireframe(params.guiParams.terrain.wireframe);
157 | }
158 | });
159 |
160 | terrainRollup.add(params.guiParams.terrain, "fixedCamera");
161 |
162 | this.chunks_ = {};
163 | this.params_ = params;
164 | }
165 |
166 | _CreateTerrainChunk(group, groupTransform, offset, cameraPosition, width, neighbours, resolution) {
167 | const params = {
168 | group: group,
169 | transform: groupTransform,
170 | material: this.material_,
171 | width: width,
172 | offset: offset,
173 | origin: cameraPosition.clone(),
174 | radius: terrain_constants.PLANET_RADIUS,
175 | resolution: resolution,
176 | neighbours: neighbours,
177 | biomeGenerator: this.biomes_,
178 | colourGenerator: new texture_splatter.TextureSplatter(
179 | {biomeGenerator: this.biomes_, colourNoise: this.colourNoise_}),
180 | heightGenerators: [new texture_splatter.HeightGenerator(
181 | this.noise_, offset, 100000, 100000 + 1)],
182 | noiseParams: this.noiseParams_,
183 | colourNoiseParams: this.colourNoiseParams_,
184 | biomesParams: this.biomesParams_,
185 | colourGeneratorParams: {
186 | biomeGeneratorParams: this.biomesParams_,
187 | colourNoiseParams: this.colourNoiseParams_,
188 | },
189 | heightGeneratorsParams: {
190 | min: 100000,
191 | max: 100000 + 1,
192 | }
193 | };
194 |
195 | return this.builder_.AllocateChunk(params);
196 | }
197 |
198 | Update(_) {
199 | const cameraPosition = this.params_.camera.position.clone();
200 | if (this.params_.guiParams.terrain.fixedCamera) {
201 | cameraPosition.copy(this.cachedCamera_);
202 | } else {
203 | this.cachedCamera_ = cameraPosition.clone();
204 | }
205 |
206 | this.builder_.Update();
207 | if (!this.builder_.Busy) {
208 | for (let k in this.chunks_) {
209 | this.chunks_[k].chunk.Show();
210 | }
211 | this.UpdateVisibleChunks_Quadtree_(cameraPosition);
212 | }
213 |
214 | for (let k in this.chunks_) {
215 | this.chunks_[k].chunk.Update(this.params_.camera.position);
216 | }
217 | for (let c of this.builder_.old_) {
218 | c.chunk.Update(this.params_.camera.position);
219 | }
220 |
221 | this.params_.scattering.uniforms.planetRadius.value = terrain_constants.PLANET_RADIUS;
222 | this.params_.scattering.uniforms.atmosphereRadius.value = terrain_constants.PLANET_RADIUS * 1.01;
223 | }
224 |
225 | UpdateVisibleChunks_Quadtree_(cameraPosition) {
226 | function _Key(c) {
227 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']';
228 | }
229 |
230 | const q = new quadtree.CubeQuadTree({
231 | radius: terrain_constants.PLANET_RADIUS,
232 | min_node_size: terrain_constants.QT_MIN_CELL_SIZE,
233 | max_node_size: terrain_constants.QT_MAX_CELL_SIZE,
234 | });
235 | q.Insert(cameraPosition);
236 | q.BuildNeighbours();
237 |
238 | const sides = q.GetChildren();
239 |
240 | let newTerrainChunks = {};
241 | const center = new THREE.Vector3();
242 | const dimensions = new THREE.Vector3();
243 |
244 | const _Child = (c) => {
245 | c.bounds.getCenter(center);
246 | c.bounds.getSize(dimensions);
247 |
248 | const child = {
249 | index: c.side,
250 | group: this.groups_[c.side],
251 | transform: sides[c.side].transform,
252 | position: [center.x, center.y, center.z],
253 | bounds: c.bounds,
254 | size: dimensions.x,
255 | neighbours: c.neighbours.map(n => n.size.x / c.size.x),
256 | neighboursOriginal: c.neighbours,
257 | };
258 | return child;
259 | };
260 |
261 | for (let i = 0; i < sides.length; i++) {
262 | for (let c of sides[i].children) {
263 | const child = _Child(c);
264 | const k = _Key(child);
265 |
266 | const left = c.neighbours[0].GetClosestChildrenSharingEdge(c.GetLeftEdgeMidpoint());
267 | const top = c.neighbours[1].GetClosestChildrenSharingEdge(c.GetTopEdgeMidpoint());
268 | const right = c.neighbours[2].GetClosestChildrenSharingEdge(c.GetRightEdgeMidpoint());
269 | const bottom = c.neighbours[3].GetClosestChildrenSharingEdge(c.GetBottomEdgeMidpoint());
270 |
271 | child.neighbourKeys = [...left, ...top, ...right, ...bottom].map(n => _Key(_Child(n)));
272 | child.debug = [left, top, right, bottom];
273 |
274 | newTerrainChunks[k] = child;
275 | }
276 | }
277 |
278 |
279 | const allChunks = newTerrainChunks;
280 | const intersection = utils.DictIntersection(this.chunks_, newTerrainChunks);
281 | const difference = utils.DictDifference(newTerrainChunks, this.chunks_);
282 | const recycle = Object.values(utils.DictDifference(this.chunks_, newTerrainChunks));
283 |
284 | if (0) {
285 | const partialRebuilds = {};
286 |
287 | for (let k in difference) {
288 | for (let n of difference[k].neighbourKeys) {
289 | if (n in this.chunks_) {
290 | partialRebuilds[n] = newTerrainChunks[n];
291 | }
292 | }
293 | }
294 | for (let k in partialRebuilds) {
295 | if (k in intersection) {
296 | recycle.push(this.chunks_[k]);
297 | delete intersection[k];
298 | difference[k] = allChunks[k];
299 | }
300 | }
301 | }
302 |
303 | this.builder_.RetireChunks(recycle);
304 |
305 | newTerrainChunks = intersection;
306 |
307 | const partialRebuilds = {};
308 |
309 | for (let k in difference) {
310 | const [xp, yp, zp] = difference[k].position;
311 |
312 | const offset = new THREE.Vector3(xp, yp, zp);
313 | newTerrainChunks[k] = {
314 | position: [xp, zp],
315 | chunk: this._CreateTerrainChunk(
316 | difference[k].group, difference[k].transform,
317 | offset, cameraPosition, difference[k].size, difference[k].neighbours,
318 | terrain_constants.QT_MIN_CELL_RESOLUTION),
319 | };
320 |
321 | for (let n of difference[k].neighbourKeys) {
322 | if (n in this.chunks_) {
323 | partialRebuilds[n] = intersection[n];
324 | partialRebuilds[n].chunk.params_.neighbours = allChunks[n].neighbours;
325 | }
326 | }
327 | }
328 |
329 | this.builder_.QuickRebuild(partialRebuilds);
330 |
331 | this.chunks_ = newTerrainChunks;
332 | }
333 | }
334 |
335 | return {
336 | TerrainChunkManager: TerrainChunkManager
337 | }
338 | })();
339 |
--------------------------------------------------------------------------------
/src/texture-splatter.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 | import {math} from './math.js';
4 | import {spline} from './spline.js';
5 | import {terrain_constants} from './terrain-constants.js';
6 |
7 |
8 | export const texture_splatter = (function() {
9 |
10 | const _HEIGHT_NORMALIZATION = terrain_constants.NOISE_HEIGHT / 10.0;
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_BOREAL = new THREE.Color(0x29c100);
19 |
20 | const _GREEN = new THREE.Color(0x80FF80);
21 | const _RED = new THREE.Color(0xFF8080);
22 | const _BLACK = new THREE.Color(0x000000);
23 |
24 |
25 | class FixedHeightGenerator {
26 | constructor() {}
27 |
28 | Get() {
29 | return [50, 1];
30 | }
31 | }
32 |
33 |
34 | class FixedColourGenerator {
35 | constructor(params) {
36 | this._params = params;
37 | }
38 |
39 | Get() {
40 | return this._params.colour;
41 | }
42 | }
43 |
44 |
45 | class HeightGenerator {
46 | constructor(generator, position, minRadius, maxRadius) {
47 | this._position = position.clone();
48 | this._radius = [minRadius, maxRadius];
49 | this._generator = generator;
50 | }
51 |
52 | Get(x, y, z) {
53 | return [this._generator.Get(x, y, z), 1];
54 | }
55 | }
56 |
57 |
58 | class TextureSplatter {
59 | constructor(params) {
60 | const _colourLerp = (t, p0, p1) => {
61 | const c = p0.clone();
62 |
63 | return c.lerp(p1, t);
64 | };
65 | this._colourSpline = [
66 | new spline.LinearSpline(_colourLerp),
67 | new spline.LinearSpline(_colourLerp)
68 | ];
69 |
70 | // Arid
71 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d));
72 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc));
73 | this._colourSpline[0].AddPoint(1.0, _SNOW);
74 |
75 | // Humid
76 | this._colourSpline[1].AddPoint(0.0, _FOREST_BOREAL);
77 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c));
78 | this._colourSpline[1].AddPoint(1.0, _SNOW);
79 |
80 | this._oceanSpline = new spline.LinearSpline(_colourLerp);
81 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN);
82 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN);
83 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN);
84 |
85 | this._params = params;
86 | }
87 |
88 | _BaseColour(x, y, z) {
89 | const m = this._params.biomeGenerator.Get(x, y, z);
90 | const h = math.sat(z / 100.0);
91 |
92 | const c1 = this._colourSpline[0].Get(h);
93 | const c2 = this._colourSpline[1].Get(h);
94 |
95 | let c = c1.lerp(c2, m);
96 |
97 | if (h < 0.1) {
98 | c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05));
99 | }
100 | return c;
101 | }
102 |
103 | _Colour(x, y, z) {
104 | const c = this._BaseColour(x, y, z);
105 | const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0;
106 |
107 | c.offsetHSL(0.0, 0.0, r * 0.01);
108 | return c;
109 | }
110 |
111 | _GetTextureWeights(p, n, up) {
112 | const m = this._params.biomeGenerator.Get(p.x, p.y, p.z);
113 | const h = p.z / _HEIGHT_NORMALIZATION;
114 |
115 | const types = {
116 | dirt: {index: 0, strength: 0.0},
117 | grass: {index: 1, strength: 0.0},
118 | gravel: {index: 2, strength: 0.0},
119 | rock: {index: 3, strength: 0.0},
120 | snow: {index: 4, strength: 0.0},
121 | snowrock: {index: 5, strength: 0.0},
122 | cobble: {index: 6, strength: 0.0},
123 | sandyrock: {index: 7, strength: 0.0},
124 | };
125 |
126 | function _ApplyWeights(dst, v, m) {
127 | for (let k in types) {
128 | types[k].strength *= m;
129 | }
130 | types[dst].strength = v;
131 | };
132 |
133 | types.grass.strength = 1.0;
134 | _ApplyWeights('gravel', 1.0 - m, m);
135 |
136 | if (h < 0.2) {
137 | const s = 1.0 - math.sat((h - 0.1) / 0.05);
138 | _ApplyWeights('cobble', s, 1.0 - s);
139 |
140 | if (h < 0.1) {
141 | const s = 1.0 - math.sat((h - 0.05) / 0.05);
142 | _ApplyWeights('sandyrock', s, 1.0 - s);
143 | }
144 | } else {
145 | if (h > 0.125) {
146 | const s = (math.sat((h - 0.125) / 1.25));
147 | _ApplyWeights('rock', s, 1.0 - s);
148 | }
149 |
150 | if (h > 1.5) {
151 | const s = math.sat((h - 0.75) / 2.0);
152 | _ApplyWeights('snow', s, 1.0 - s);
153 | }
154 | }
155 |
156 | // In case nothing gets set.
157 | types.dirt.strength = 0.01;
158 |
159 | let total = 0.0;
160 | for (let k in types) {
161 | total += types[k].strength;
162 | }
163 | if (total < 0.01) {
164 | const a = 0;
165 | }
166 | const normalization = 1.0 / total;
167 |
168 | for (let k in types) {
169 | types[k].strength / normalization;
170 | }
171 |
172 | return types;
173 | }
174 |
175 | GetColour(position) {
176 | return this._Colour(position.x, position.y, position.z);
177 | }
178 |
179 | GetSplat(position, normal, up) {
180 | return this._GetTextureWeights(position, normal, up);
181 | }
182 | }
183 |
184 | return {
185 | HeightGenerator: HeightGenerator,
186 | TextureSplatter: TextureSplatter,
187 | }
188 | })();
189 |
--------------------------------------------------------------------------------
/src/textures.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.125/build/three.module.js';
2 |
3 |
4 | export const textures = (function() {
5 |
6 | // Taken from https://github.com/mrdoob/three.js/issues/758
7 | function _GetImageData( image ) {
8 | var canvas = document.createElement('canvas');
9 | canvas.width = image.width;
10 | canvas.height = image.height;
11 |
12 | var context = canvas.getContext('2d');
13 | context.drawImage( image, 0, 0 );
14 |
15 | return context.getImageData( 0, 0, image.width, image.height );
16 | }
17 |
18 | return {
19 | TextureAtlas: class {
20 | constructor(params) {
21 | this.game_ = params.game;
22 | this.Create_();
23 | this.onLoad = () => {};
24 | }
25 |
26 | Load(atlas, names) {
27 | this.LoadAtlas_(atlas, names);
28 | }
29 |
30 | Create_() {
31 | this.manager_ = new THREE.LoadingManager();
32 | this.loader_ = new THREE.TextureLoader(this.manager_);
33 | this.textures_ = {};
34 |
35 | this.manager_.onLoad = () => {
36 | this.OnLoad_();
37 | };
38 | }
39 |
40 | get Info() {
41 | return this.textures_;
42 | }
43 |
44 | OnLoad_() {
45 | for (let k in this.textures_) {
46 | const atlas = this.textures_[k];
47 | const data = new Uint8Array(atlas.textures.length * 4 * 1024 * 1024);
48 |
49 | for (let t = 0; t < atlas.textures.length; t++) {
50 | const curTexture = atlas.textures[t];
51 | const curData = _GetImageData(curTexture.image);
52 | const offset = t * (4 * 1024 * 1024);
53 |
54 | data.set(curData.data, offset);
55 | }
56 |
57 | const diffuse = new THREE.DataTexture2DArray(data, 1024, 1024, atlas.textures.length);
58 | diffuse.format = THREE.RGBAFormat;
59 | diffuse.type = THREE.UnsignedByteType;
60 | diffuse.minFilter = THREE.LinearMipMapLinearFilter;
61 | diffuse.magFilter = THREE.LinearFilter;
62 | diffuse.wrapS = THREE.RepeatWrapping;
63 | diffuse.wrapT = THREE.RepeatWrapping;
64 | diffuse.generateMipmaps = true;
65 | diffuse.encoding = THREE.sRGBEncoding;
66 |
67 | atlas.atlas = diffuse;
68 | }
69 |
70 | this.onLoad();
71 | }
72 |
73 | LoadAtlas_(atlas, names) {
74 | this.textures_[atlas] = {
75 | textures: names.map(n => this.loader_.load(n))
76 | };
77 | }
78 | }
79 | };
80 | })();
81 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------