├── LICENSE
├── base.css
├── index.html
├── resources
├── README.txt
├── dirt_01_diffuse-1024.png
├── dirt_01_normal-1024.jpg
├── grass1-albedo-512.jpg
├── grass1-albedo3-1024.png
├── grass1-normal-1024.jpg
├── rock-snow-ice-albedo-1024.png
├── rock-snow-ice-normal-1024.jpg
├── rough-wet-cobble-albedo-1024.png
├── rough-wet-cobble-normal-1024.jpg
├── sandy-rocks1-albedo-1024.png
├── sandy-rocks1-normal-1024.jpg
├── sandyground-albedo-1024.png
├── sandyground-normal-1024.jpg
├── simplex-noise.png
├── snow-packed-albedo-1024.png
├── snow-packed-normal-1024.jpg
├── space-negx.jpg
├── space-negy.jpg
├── space-negz.jpg
├── space-posx.jpg
├── space-posy.jpg
├── space-posz.jpg
├── worn-bumpy-rock-albedo-1024.png
├── worn-bumpy-rock-albedo-512.jpg
└── worn-bumpy-rock-normal-1024.jpg
└── src
├── camera-track.js
├── controls.js
├── demo.js
├── game.js
├── graphics.js
├── main.js
├── math.js
├── noise.js
├── perlin-noise.js
├── quadtree.js
├── scattering-shader.js
├── sky.js
├── spline.js
├── terrain-chunk.js
├── terrain-shader.js
├── terrain.js
├── textures.js
└── utils.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 simondevyoutube
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/base.css:
--------------------------------------------------------------------------------
1 | .header {
2 | font-size: 3em;
3 | color: white;
4 | background: #404040;
5 | text-align: center;
6 | height: 2.5em;
7 | text-shadow: 4px 4px 4px black;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | #error {
14 | font-size: 2em;
15 | color: red;
16 | height: 50px;
17 | text-shadow: 2px 2px 2px black;
18 | margin: 2em;
19 | display: none;
20 | }
21 |
22 | .container {
23 | width: 100% !important;
24 | height: 100% !important;
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-direction: column;
29 | position: absolute;
30 | }
31 |
32 | .visible {
33 | display: block;
34 | }
35 |
36 | #target {
37 | width: 100% !important;
38 | height: 100% !important;
39 | position: absolute;
40 | }
41 |
42 | body {
43 | background: #000000;
44 | margin: 0;
45 | padding: 0;
46 | overscroll-behavior: none;
47 | }
48 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Procedural Planet
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/resources/README.txt:
--------------------------------------------------------------------------------
1 | Most of these textures were taken from freepbr.com or https://opengameart.org/content/36-free-ground-textures-diffuse-normals.
2 |
3 | They were all 2kx2k so they've been resaved as 1k.
4 |
--------------------------------------------------------------------------------
/resources/dirt_01_diffuse-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/dirt_01_diffuse-1024.png
--------------------------------------------------------------------------------
/resources/dirt_01_normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/dirt_01_normal-1024.jpg
--------------------------------------------------------------------------------
/resources/grass1-albedo-512.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/grass1-albedo-512.jpg
--------------------------------------------------------------------------------
/resources/grass1-albedo3-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/grass1-albedo3-1024.png
--------------------------------------------------------------------------------
/resources/grass1-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/grass1-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/rock-snow-ice-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/rock-snow-ice-albedo-1024.png
--------------------------------------------------------------------------------
/resources/rock-snow-ice-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/rock-snow-ice-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/rough-wet-cobble-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/rough-wet-cobble-albedo-1024.png
--------------------------------------------------------------------------------
/resources/rough-wet-cobble-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/rough-wet-cobble-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/sandy-rocks1-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/sandy-rocks1-albedo-1024.png
--------------------------------------------------------------------------------
/resources/sandy-rocks1-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/sandy-rocks1-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/sandyground-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/sandyground-albedo-1024.png
--------------------------------------------------------------------------------
/resources/sandyground-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/sandyground-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/simplex-noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/simplex-noise.png
--------------------------------------------------------------------------------
/resources/snow-packed-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/snow-packed-albedo-1024.png
--------------------------------------------------------------------------------
/resources/snow-packed-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/snow-packed-normal-1024.jpg
--------------------------------------------------------------------------------
/resources/space-negx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-negx.jpg
--------------------------------------------------------------------------------
/resources/space-negy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-negy.jpg
--------------------------------------------------------------------------------
/resources/space-negz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-negz.jpg
--------------------------------------------------------------------------------
/resources/space-posx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-posx.jpg
--------------------------------------------------------------------------------
/resources/space-posy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-posy.jpg
--------------------------------------------------------------------------------
/resources/space-posz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/space-posz.jpg
--------------------------------------------------------------------------------
/resources/worn-bumpy-rock-albedo-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/worn-bumpy-rock-albedo-1024.png
--------------------------------------------------------------------------------
/resources/worn-bumpy-rock-albedo-512.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/worn-bumpy-rock-albedo-512.jpg
--------------------------------------------------------------------------------
/resources/worn-bumpy-rock-normal-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/ProceduralTerrain_Part6/a17dc9ed4cfe3ad3cee5478bb8afa147b5c8e12b/resources/worn-bumpy-rock-normal-1024.jpg
--------------------------------------------------------------------------------
/src/camera-track.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/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.112.1/build/three.module.js';
2 | import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/controls/PointerLockControls.js';
3 | import {OrbitControls} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/controls/OrbitControls.js';
4 |
5 |
6 | export const controls = (function() {
7 |
8 | class _OrbitControls {
9 | constructor(params) {
10 | this._params = params;
11 | this._Init(params);
12 | }
13 |
14 | _Init(params) {
15 | this._controls = new OrbitControls(params.camera, params.domElement);
16 | this._controls.target.set(0, 0, 0);
17 | this._controls.update();
18 | }
19 |
20 | Update() {
21 | }
22 | }
23 |
24 | // FPSControls was adapted heavily from a threejs example. Movement control
25 | // and collision detection was completely rewritten, but credit to original
26 | // class for the setup code.
27 | class _FPSControls {
28 | constructor(params) {
29 | this._cells = params.cells;
30 | this._Init(params);
31 | }
32 |
33 | _Init(params) {
34 | this._params = params;
35 | this._radius = 2;
36 | this._enabled = false;
37 | this._move = {
38 | forward: false,
39 | backward: false,
40 | left: false,
41 | right: false,
42 | up: false,
43 | down: false,
44 | };
45 | this._standing = true;
46 | this._velocity = new THREE.Vector3(0, 0, 0);
47 | this._decceleration = new THREE.Vector3(-10, -10, -10);
48 | this._acceleration = new THREE.Vector3(50000, 50000, 50000);
49 |
50 | this._SetupPointerLock();
51 |
52 | this._controls = new PointerLockControls(
53 | params.camera, document.body);
54 | params.scene.add(this._controls.getObject());
55 |
56 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
57 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
58 |
59 | this._InitGUI();
60 | }
61 |
62 | _InitGUI() {
63 | this._params.guiParams.camera = {
64 | acceleration_x: 50000,
65 | };
66 |
67 | const rollup = this._params.gui.addFolder('Camera.FPS');
68 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 50000.0).onChange(
69 | () => {
70 | this._acceleration.set(
71 | this._params.guiParams.camera.acceleration_x,
72 | this._params.guiParams.camera.acceleration_x,
73 | this._params.guiParams.camera.acceleration_x);
74 | });
75 | }
76 |
77 | _onKeyDown(event) {
78 | switch (event.keyCode) {
79 | case 38: // up
80 | case 87: // w
81 | this._move.forward = true;
82 | break;
83 | case 37: // left
84 | case 65: // a
85 | this._move.left = true;
86 | break;
87 | case 40: // down
88 | case 83: // s
89 | this._move.backward = true;
90 | break;
91 | case 39: // right
92 | case 68: // d
93 | this._move.right = true;
94 | break;
95 | case 33: // PG_UP
96 | this._move.up = true;
97 | break;
98 | case 34: // PG_DOWN
99 | this._move.down = true;
100 | break;
101 | }
102 | }
103 |
104 | _onKeyUp(event) {
105 | switch(event.keyCode) {
106 | case 38: // up
107 | case 87: // w
108 | this._move.forward = false;
109 | break;
110 | case 37: // left
111 | case 65: // a
112 | this._move.left = false;
113 | break;
114 | case 40: // down
115 | case 83: // s
116 | this._move.backward = false;
117 | break;
118 | case 39: // right
119 | case 68: // d
120 | this._move.right = false;
121 | break;
122 | case 33: // PG_UP
123 | this._move.up = false;
124 | break;
125 | case 34: // PG_DOWN
126 | this._move.down = false;
127 | break;
128 | }
129 | }
130 |
131 | _SetupPointerLock() {
132 | const hasPointerLock = (
133 | 'pointerLockElement' in document ||
134 | 'mozPointerLockElement' in document ||
135 | 'webkitPointerLockElement' in document);
136 | if (hasPointerLock) {
137 | const lockChange = (event) => {
138 | if (document.pointerLockElement === document.body ||
139 | document.mozPointerLockElement === document.body ||
140 | document.webkitPointerLockElement === document.body ) {
141 | this._enabled = true;
142 | this._controls.enabled = true;
143 | } else {
144 | this._controls.enabled = false;
145 | }
146 | };
147 | const lockError = (event) => {
148 | console.log(event);
149 | };
150 |
151 | document.addEventListener('pointerlockchange', lockChange, false);
152 | document.addEventListener('webkitpointerlockchange', lockChange, false);
153 | document.addEventListener('mozpointerlockchange', lockChange, false);
154 | document.addEventListener('pointerlockerror', lockError, false);
155 | document.addEventListener('mozpointerlockerror', lockError, false);
156 | document.addEventListener('webkitpointerlockerror', lockError, false);
157 |
158 | document.getElementById('target').addEventListener('click', (event) => {
159 | document.body.requestPointerLock = (
160 | document.body.requestPointerLock ||
161 | document.body.mozRequestPointerLock ||
162 | document.body.webkitRequestPointerLock);
163 |
164 | if (/Firefox/i.test(navigator.userAgent)) {
165 | const fullScreenChange = (event) => {
166 | if (document.fullscreenElement === document.body ||
167 | document.mozFullscreenElement === document.body ||
168 | document.mozFullScreenElement === document.body) {
169 | document.removeEventListener('fullscreenchange', fullScreenChange);
170 | document.removeEventListener('mozfullscreenchange', fullScreenChange);
171 | document.body.requestPointerLock();
172 | }
173 | };
174 | document.addEventListener(
175 | 'fullscreenchange', fullScreenChange, false);
176 | document.addEventListener(
177 | 'mozfullscreenchange', fullScreenChange, false);
178 | document.body.requestFullscreen = (
179 | document.body.requestFullscreen ||
180 | document.body.mozRequestFullscreen ||
181 | document.body.mozRequestFullScreen ||
182 | document.body.webkitRequestFullscreen);
183 | document.body.requestFullscreen();
184 | } else {
185 | document.body.requestPointerLock();
186 | }
187 | }, false);
188 | }
189 | }
190 |
191 | _FindIntersections(boxes, position) {
192 | const sphere = new THREE.Sphere(position, this._radius);
193 |
194 | const intersections = boxes.filter(b => {
195 | return sphere.intersectsBox(b);
196 | });
197 |
198 | return intersections;
199 | }
200 |
201 | Update(timeInSeconds) {
202 | if (!this._enabled) {
203 | return;
204 | }
205 |
206 | const frameDecceleration = new THREE.Vector3(
207 | this._velocity.x * this._decceleration.x,
208 | this._velocity.y * this._decceleration.y,
209 | this._velocity.z * this._decceleration.z
210 | );
211 | frameDecceleration.multiplyScalar(timeInSeconds);
212 |
213 | this._velocity.add(frameDecceleration);
214 |
215 | if (this._move.forward) {
216 | this._velocity.z -= this._acceleration.z * timeInSeconds;
217 | }
218 | if (this._move.backward) {
219 | this._velocity.z += this._acceleration.z * timeInSeconds;
220 | }
221 | if (this._move.left) {
222 | this._velocity.x -= this._acceleration.x * timeInSeconds;
223 | }
224 | if (this._move.right) {
225 | this._velocity.x += this._acceleration.x * timeInSeconds;
226 | }
227 | if (this._move.up) {
228 | this._velocity.y += this._acceleration.y * timeInSeconds;
229 | }
230 | if (this._move.down) {
231 | this._velocity.y -= this._acceleration.y * timeInSeconds;
232 | }
233 |
234 | const controlObject = this._controls.getObject();
235 |
236 | const oldPosition = new THREE.Vector3();
237 | oldPosition.copy(controlObject.position);
238 |
239 | const forward = new THREE.Vector3(0, 0, 1);
240 | forward.applyQuaternion(controlObject.quaternion);
241 | forward.normalize();
242 |
243 | const updown = new THREE.Vector3(0, 1, 0);
244 |
245 | const sideways = new THREE.Vector3(1, 0, 0);
246 | sideways.applyQuaternion(controlObject.quaternion);
247 | sideways.normalize();
248 |
249 | sideways.multiplyScalar(this._velocity.x * timeInSeconds);
250 | updown.multiplyScalar(this._velocity.y * timeInSeconds);
251 | forward.multiplyScalar(this._velocity.z * timeInSeconds);
252 |
253 | controlObject.position.add(forward);
254 | controlObject.position.add(sideways);
255 | controlObject.position.add(updown);
256 |
257 | oldPosition.copy(controlObject.position);
258 | }
259 | };
260 |
261 | class _ShipControls {
262 | constructor(params) {
263 | this._Init(params);
264 | }
265 |
266 | _Init(params) {
267 | this._params = params;
268 | this._radius = 2;
269 | this._enabled = false;
270 | this._move = {
271 | forward: false,
272 | backward: false,
273 | left: false,
274 | right: false,
275 | up: false,
276 | down: false,
277 | rocket: false,
278 | };
279 | this._velocity = new THREE.Vector3(0, 0, 0);
280 | this._decceleration = new THREE.Vector3(-0.001, -0.0001, -1);
281 | this._acceleration = new THREE.Vector3(100, 0.1, 25000);
282 |
283 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
284 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
285 |
286 | this._InitGUI();
287 | }
288 |
289 | _InitGUI() {
290 | this._params.guiParams.camera = {
291 | acceleration_x: 100,
292 | acceleration_y: 0.1,
293 | };
294 |
295 | const rollup = this._params.gui.addFolder('Camera.Ship');
296 | rollup.add(this._params.guiParams.camera, "acceleration_x", 50.0, 25000.0).onChange(
297 | () => {
298 | this._acceleration.x = this._params.guiParams.camera.acceleration_x;
299 | });
300 | rollup.add(this._params.guiParams.camera, "acceleration_y", 0.001, 0.1).onChange(
301 | () => {
302 | this._acceleration.y = this._params.guiParams.camera.acceleration_y;
303 | });
304 | }
305 |
306 | _onKeyDown(event) {
307 | switch (event.keyCode) {
308 | case 87: // w
309 | this._move.forward = true;
310 | break;
311 | case 65: // a
312 | this._move.left = true;
313 | break;
314 | case 83: // s
315 | this._move.backward = true;
316 | break;
317 | case 68: // d
318 | this._move.right = true;
319 | break;
320 | case 33: // PG_UP
321 | this._acceleration.x *= 1.1;
322 | break;
323 | case 34: // PG_DOWN
324 | this._acceleration.x *= 0.9;
325 | break;
326 | case 32: // SPACE
327 | this._move.rocket = true;
328 | break;
329 | case 38: // up
330 | case 37: // left
331 | case 40: // down
332 | case 39: // right
333 | break;
334 | }
335 | }
336 |
337 | _onKeyUp(event) {
338 | switch(event.keyCode) {
339 | case 87: // w
340 | this._move.forward = false;
341 | break;
342 | case 65: // a
343 | this._move.left = false;
344 | break;
345 | case 83: // s
346 | this._move.backward = false;
347 | break;
348 | case 68: // d
349 | this._move.right = false;
350 | break;
351 | case 33: // PG_UP
352 | break;
353 | case 34: // PG_DOWN
354 | break;
355 | case 32: // SPACE
356 | this._move.rocket = false;
357 | break;
358 | case 38: // up
359 | case 37: // left
360 | case 40: // down
361 | case 39: // right
362 | break;
363 | }
364 | }
365 |
366 | Update(timeInSeconds) {
367 | const frameDecceleration = new THREE.Vector3(
368 | this._velocity.x * this._decceleration.x,
369 | this._velocity.y * this._decceleration.y,
370 | this._velocity.z * this._decceleration.z
371 | );
372 | frameDecceleration.multiplyScalar(timeInSeconds);
373 |
374 | this._velocity.add(frameDecceleration);
375 |
376 | const controlObject = this._params.camera;
377 | const _Q = new THREE.Quaternion();
378 | const _A = new THREE.Vector3();
379 | const _R = controlObject.quaternion.clone();
380 |
381 | if (this._move.forward) {
382 | _A.set(1, 0, 0);
383 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
384 | _R.multiply(_Q);
385 | }
386 | if (this._move.backward) {
387 | _A.set(1, 0, 0);
388 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
389 | _R.multiply(_Q);
390 | }
391 | if (this._move.left) {
392 | _A.set(0, 0, 1);
393 | _Q.setFromAxisAngle(_A, Math.PI * timeInSeconds * this._acceleration.y);
394 | _R.multiply(_Q);
395 | }
396 | if (this._move.right) {
397 | _A.set(0, 0, 1);
398 | _Q.setFromAxisAngle(_A, -Math.PI * timeInSeconds * this._acceleration.y);
399 | _R.multiply(_Q);
400 | }
401 | if (this._move.rocket) {
402 | this._velocity.z -= this._acceleration.x * timeInSeconds;
403 | }
404 |
405 | controlObject.quaternion.copy(_R);
406 |
407 | const oldPosition = new THREE.Vector3();
408 | oldPosition.copy(controlObject.position);
409 |
410 | const forward = new THREE.Vector3(0, 0, 1);
411 | forward.applyQuaternion(controlObject.quaternion);
412 | //forward.y = 0;
413 | forward.normalize();
414 |
415 | const updown = new THREE.Vector3(0, 1, 0);
416 |
417 | const sideways = new THREE.Vector3(1, 0, 0);
418 | sideways.applyQuaternion(controlObject.quaternion);
419 | sideways.normalize();
420 |
421 | sideways.multiplyScalar(this._velocity.x * timeInSeconds);
422 | updown.multiplyScalar(this._velocity.y * timeInSeconds);
423 | forward.multiplyScalar(this._velocity.z * timeInSeconds);
424 |
425 | controlObject.position.add(forward);
426 | controlObject.position.add(sideways);
427 | controlObject.position.add(updown);
428 |
429 | oldPosition.copy(controlObject.position);
430 | }
431 | };
432 |
433 | return {
434 | ShipControls: _ShipControls,
435 | FPSControls: _FPSControls,
436 | OrbitControls: _OrbitControls,
437 | };
438 | })();
439 |
--------------------------------------------------------------------------------
/src/demo.js:
--------------------------------------------------------------------------------
1 | import {game} from './game.js';
2 | import {graphics} from './graphics.js';
3 | import {math} from './math.js';
4 | import {noise} from './noise.js';
5 |
6 |
7 | window.onload = function() {
8 | function _Perlin() {
9 | const canvas = document.getElementById("canvas");
10 | const context = canvas.getContext("2d");
11 |
12 | const imgData = context.createImageData(canvas.width, canvas.height);
13 |
14 | const params = {
15 | scale: 32,
16 | noiseType: 'simplex',
17 | persistence: 0.5,
18 | octaves: 1,
19 | lacunarity: 1,
20 | exponentiation: 1,
21 | height: 255
22 | };
23 | const noiseGen = new noise.Noise(params);
24 |
25 | for (let x = 0; x < canvas.width; x++) {
26 | for (let y = 0; y < canvas.height; y++) {
27 | const pixelIndex = (y * canvas.width + x) * 4;
28 |
29 | const n = noiseGen.Get(x, y);
30 |
31 | imgData.data[pixelIndex] = n;
32 | imgData.data[pixelIndex+1] = n;
33 | imgData.data[pixelIndex+2] = n;
34 | imgData.data[pixelIndex+3] = 255;
35 | }
36 | }
37 |
38 | context.putImageData(imgData, 0, 0);
39 | }
40 |
41 |
42 | function _Randomness() {
43 | const canvas = document.getElementById("canvas");
44 | const context = canvas.getContext("2d");
45 |
46 | const imgData = context.createImageData(canvas.width, canvas.height);
47 |
48 | const params = {
49 | scale: 32,
50 | noiseType: 'simplex',
51 | persistence: 0.5,
52 | octaves: 1,
53 | lacunarity: 2,
54 | exponentiation: 1,
55 | height: 1
56 | };
57 | const noiseGen = new noise.Noise(params);
58 | let foo = '';
59 |
60 | for (let x = 0; x < canvas.width; x++) {
61 | for (let y = 0; y < canvas.height; y++) {
62 | const pixelIndex = (y * canvas.width + x) * 4;
63 |
64 | const n = noiseGen.Get(x, y);
65 | if (x == 0) {
66 | foo += n + '\n';
67 | }
68 |
69 | imgData.data[pixelIndex] = n;
70 | imgData.data[pixelIndex+1] = n;
71 | imgData.data[pixelIndex+2] = n;
72 | imgData.data[pixelIndex+3] = 255;
73 | }
74 | }
75 | console.log(foo);
76 |
77 | context.putImageData(imgData, 0, 0);
78 | }
79 |
80 | _Randomness();
81 |
82 | };
83 |
--------------------------------------------------------------------------------
/src/game.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/WebGL.js';
3 | import {graphics} from './graphics.js';
4 |
5 |
6 | export const game = (function() {
7 | return {
8 | Game: class {
9 | constructor() {
10 | this._Initialize();
11 | }
12 |
13 | _Initialize() {
14 | this._graphics = new graphics.Graphics(this);
15 | if (!this._graphics.Initialize()) {
16 | this._DisplayError('WebGL2 is not available.');
17 | return;
18 | }
19 |
20 | this._previousRAF = null;
21 | this._minFrameTime = 1.0 / 10.0;
22 | this._entities = {};
23 |
24 | this._OnInitialize();
25 | this._RAF();
26 | }
27 |
28 | _DisplayError(errorText) {
29 | const error = document.getElementById('error');
30 | error.innerText = errorText;
31 | }
32 |
33 | _RAF() {
34 | requestAnimationFrame((t) => {
35 | if (this._previousRAF === null) {
36 | this._previousRAF = t;
37 | }
38 | this._Render(t - this._previousRAF);
39 | this._previousRAF = t;
40 | });
41 | }
42 |
43 | _StepEntities(timeInSeconds) {
44 | for (let k in this._entities) {
45 | this._entities[k].Update(timeInSeconds);
46 | }
47 | }
48 |
49 | _Render(timeInMS) {
50 | const timeInSeconds = Math.min(timeInMS * 0.001, this._minFrameTime);
51 |
52 | this._OnStep(timeInSeconds);
53 | this._StepEntities(timeInSeconds);
54 | this._graphics.Render(timeInSeconds);
55 |
56 | this._RAF();
57 | }
58 | }
59 | };
60 | })();
61 |
--------------------------------------------------------------------------------
/src/graphics.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import Stats from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/stats.module.js';
3 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/WebGL.js';
4 |
5 | import {RenderPass} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/postprocessing/RenderPass.js';
6 | import {ShaderPass} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/postprocessing/ShaderPass.js';
7 | import {CopyShader} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/shaders/CopyShader.js';
8 | import {FXAAShader} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/shaders/FXAAShader.js';
9 | import {EffectComposer} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/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 | });
54 | this._threejs.setPixelRatio(window.devicePixelRatio);
55 | this._threejs.setSize(window.innerWidth, window.innerHeight);
56 | this._threejs.autoClear = false;
57 |
58 | const target = document.getElementById('target');
59 | target.appendChild(this._threejs.domElement);
60 |
61 | this._stats = new Stats();
62 | // target.appendChild(this._stats.dom);
63 |
64 | window.addEventListener('resize', () => {
65 | this._OnWindowResize();
66 | }, false);
67 |
68 | const fov = 60;
69 | const aspect = 1920 / 1080;
70 | const near = 0.1;
71 | const far = 100000.0;
72 | this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
73 | this._camera.position.set(75, 20, 0);
74 |
75 | this._scene = new THREE.Scene();
76 | this._scene.background = new THREE.Color(0xaaaaaa);
77 |
78 | const renderPass = new RenderPass(this._scene, this._camera);
79 | const fxaaPass = new ShaderPass(FXAAShader);
80 | // const depthPass = new ShaderPass(scattering_shader.Shader);
81 |
82 | // this._depthPass = depthPass;
83 |
84 | this._composer = new EffectComposer(this._threejs);
85 | this._composer.addPass(renderPass);
86 | this._composer.addPass(fxaaPass);
87 | //this._composer.addPass(depthPass);
88 |
89 | this._target = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
90 | this._target.texture.format = THREE.RGBFormat;
91 | this._target.texture.minFilter = THREE.NearestFilter;
92 | this._target.texture.magFilter = THREE.NearestFilter;
93 | this._target.texture.generateMipmaps = false;
94 | this._target.stencilBuffer = false;
95 | this._target.depthBuffer = true;
96 | this._target.depthTexture = new THREE.DepthTexture();
97 | this._target.depthTexture.format = THREE.DepthFormat;
98 | this._target.depthTexture.type = THREE.FloatType;
99 |
100 | this._threejs.setRenderTarget(this._target);
101 |
102 | this._postCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
103 | this._depthPass = new THREE.ShaderMaterial( {
104 | vertexShader: scattering_shader.VS,
105 | fragmentShader: scattering_shader.PS,
106 | uniforms: {
107 | cameraNear: { value: this.Camera.near },
108 | cameraFar: { value: this.Camera.far },
109 | cameraPosition: { value: this.Camera.position },
110 | cameraForward: { value: null },
111 | tDiffuse: { value: null },
112 | tDepth: { value: null },
113 | inverseProjection: { value: null },
114 | inverseView: { value: null },
115 | planetPosition: { value: null },
116 | planetRadius: { value: null },
117 | atmosphereRadius: { value: null },
118 | }
119 | } );
120 | var postPlane = new THREE.PlaneBufferGeometry( 2, 2 );
121 | var postQuad = new THREE.Mesh( postPlane, this._depthPass );
122 | this._postScene = new THREE.Scene();
123 | this._postScene.add( postQuad );
124 |
125 | this._CreateLights();
126 |
127 | return true;
128 | }
129 |
130 |
131 | _CreateLights() {
132 | let light = new THREE.DirectionalLight(0xFFFFFF, 1);
133 | light.position.set(100, 100, -100);
134 | light.target.position.set(0, 0, 0);
135 | light.castShadow = false;
136 | this._scene.add(light);
137 |
138 | light = new THREE.DirectionalLight(0x404040, 1);
139 | light.position.set(100, 100, -100);
140 | light.target.position.set(0, 0, 0);
141 | light.castShadow = false;
142 | this._scene.add(light);
143 |
144 | light = new THREE.DirectionalLight(0x404040, 1);
145 | light.position.set(100, 100, -100);
146 | light.target.position.set(0, 0, 0);
147 | light.castShadow = false;
148 | this._scene.add(light);
149 |
150 | light = new THREE.DirectionalLight(0x202040, 1);
151 | light.position.set(100, -100, 100);
152 | light.target.position.set(0, 0, 0);
153 | light.castShadow = false;
154 | this._scene.add(light);
155 |
156 | light = new THREE.AmbientLight(0xFFFFFF, 1.0);
157 | this._scene.add(light);
158 | }
159 |
160 | _OnWindowResize() {
161 | this._camera.aspect = window.innerWidth / window.innerHeight;
162 | this._camera.updateProjectionMatrix();
163 | this._threejs.setSize(window.innerWidth, window.innerHeight);
164 | this._composer.setSize(window.innerWidth, window.innerHeight);
165 | this._target.setSize(window.innerWidth, window.innerHeight);
166 | }
167 |
168 | get Scene() {
169 | return this._scene;
170 | }
171 |
172 | get Camera() {
173 | return this._camera;
174 | }
175 |
176 | Render(timeInSeconds) {
177 | this._threejs.setRenderTarget(this._target);
178 |
179 | this._threejs.clear();
180 | this._threejs.render(this._scene, this._camera);
181 | //this._composer.render();
182 |
183 | this._threejs.setRenderTarget( null );
184 |
185 | const forward = new THREE.Vector3();
186 | this._camera.getWorldDirection(forward);
187 |
188 | this._depthPass.uniforms.inverseProjection.value = this._camera.projectionMatrixInverse;
189 | this._depthPass.uniforms.inverseView.value = this._camera.matrixWorld;
190 | this._depthPass.uniforms.tDiffuse.value = this._target.texture;
191 | this._depthPass.uniforms.tDepth.value = this._target.depthTexture;
192 | this._depthPass.uniforms.cameraNear.value = this._camera.near;
193 | this._depthPass.uniforms.cameraFar.value = this._camera.far;
194 | this._depthPass.uniforms.cameraPosition.value = this._camera.position;
195 | this._depthPass.uniforms.cameraForward.value = forward;
196 | this._depthPass.uniforms.planetPosition.value = new THREE.Vector3(0, 0, 0);
197 | this._depthPass.uniforms.planetRadius.value = 4000.0;
198 | this._depthPass.uniforms.atmosphereRadius.value = 6000.0;
199 | this._depthPass.uniformsNeedUpdate = true;
200 |
201 | this._threejs.render( this._postScene, this._postCamera );
202 |
203 | this._stats.update();
204 | }
205 | }
206 |
207 | return {
208 | Graphics: _Graphics,
209 | GetPixel: _GetPixel,
210 | GetImageData: _GetImageData,
211 | };
212 | })();
213 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import {GUI} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/dat.gui.module.js';
3 | import {controls} from './controls.js';
4 | import {game} from './game.js';
5 | import {terrain} from './terrain.js';
6 |
7 |
8 | let _APP = null;
9 |
10 |
11 |
12 | class ProceduralTerrain_Demo extends game.Game {
13 | constructor() {
14 | super();
15 | }
16 |
17 | _OnInitialize() {
18 | this._CreateGUI();
19 |
20 | this._userCamera = new THREE.Object3D();
21 | this._userCamera.position.set(4100, 0, 0);
22 | this._graphics.Camera.position.set(3853, -609, -1509);
23 | this._graphics.Camera.quaternion.set(0.403, 0.59, -0.549, 0.432);
24 |
25 | this._graphics.Camera.position.set(1412, -1674, -3848);
26 | this._graphics.Camera.quaternion.set(0.1004, 0.7757, -0.6097, 0.1278);
27 |
28 | this._entities['_terrain'] = new terrain.TerrainChunkManager({
29 | camera: this._graphics.Camera,
30 | scene: this._graphics.Scene,
31 | gui: this._gui,
32 | guiParams: this._guiParams,
33 | game: this
34 | });
35 |
36 | // this._entities['_controls'] = new controls.OrbitControls({
37 | // camera: this._graphics.Camera,
38 | // scene: this._graphics.Scene,
39 | // domElement: this._graphics._threejs.domElement,
40 | // gui: this._gui,
41 | // guiParams: this._guiParams,
42 | // });
43 |
44 | // this._entities['_controls'] = new controls.ShipControls({
45 | // camera: this._graphics.Camera,
46 | // scene: this._graphics.Scene,
47 | // domElement: this._graphics._threejs.domElement,
48 | // gui: this._gui,
49 | // guiParams: this._guiParams,
50 | // });
51 |
52 | this._entities['_controls'] = new controls.FPSControls({
53 | camera: this._graphics.Camera,
54 | scene: this._graphics.Scene,
55 | domElement: this._graphics._threejs.domElement,
56 | gui: this._gui,
57 | guiParams: this._guiParams,
58 | });
59 |
60 | // this._focusMesh = new THREE.Mesh(
61 | // new THREE.SphereGeometry(25, 32, 32),
62 | // new THREE.MeshBasicMaterial({
63 | // color: 0xFFFFFF
64 | // }));
65 | // this._focusMesh.castShadow = true;
66 | // this._focusMesh.receiveShadow = true;
67 | //this._graphics.Scene.add(this._focusMesh);
68 |
69 | this._totalTime = 0;
70 |
71 | this._LoadBackground();
72 | }
73 |
74 | _CreateGUI() {
75 | this._guiParams = {
76 | general: {
77 | },
78 | };
79 | this._gui = new GUI();
80 |
81 | const generalRollup = this._gui.addFolder('General');
82 | this._gui.close();
83 | }
84 |
85 | _LoadBackground() {
86 | this._graphics.Scene.background = new THREE.Color(0x000000);
87 | const loader = new THREE.CubeTextureLoader();
88 | const texture = loader.load([
89 | './resources/space-posx.jpg',
90 | './resources/space-negx.jpg',
91 | './resources/space-posy.jpg',
92 | './resources/space-negy.jpg',
93 | './resources/space-posz.jpg',
94 | './resources/space-negz.jpg',
95 | ]);
96 | this._graphics._scene.background = texture;
97 | }
98 |
99 | _OnStep(timeInSeconds) {
100 | }
101 | }
102 |
103 |
104 | function _Main() {
105 | _APP = new ProceduralTerrain_Demo();
106 | }
107 |
108 | _Main();
109 |
--------------------------------------------------------------------------------
/src/math.js:
--------------------------------------------------------------------------------
1 | export const math = (function() {
2 | return {
3 | rand_range: function(a, b) {
4 | return Math.random() * (b - a) + a;
5 | },
6 |
7 | rand_normalish: function() {
8 | const r = Math.random() + Math.random() + Math.random() + Math.random();
9 | return (r / 4.0) * 2.0 - 1;
10 | },
11 |
12 | rand_int: function(a, b) {
13 | return Math.round(Math.random() * (b - a) + a);
14 | },
15 |
16 | lerp: function(x, a, b) {
17 | return x * (b - a) + a;
18 | },
19 |
20 | smoothstep: function(x, a, b) {
21 | x = x * x * (3.0 - 2.0 * x);
22 | return x * (b - a) + a;
23 | },
24 |
25 | smootherstep: function(x, a, b) {
26 | x = x * x * x * (x * (x * 6 - 15) + 10);
27 | return x * (b - a) + a;
28 | },
29 |
30 | clamp: function(x, a, b) {
31 | return Math.min(Math.max(x, a), b);
32 | },
33 |
34 | sat: function(x) {
35 | return Math.min(Math.max(x, 0.0), 1.0);
36 | },
37 | };
38 | })();
39 |
--------------------------------------------------------------------------------
/src/noise.js:
--------------------------------------------------------------------------------
1 | import 'https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.js';
2 | //import perlin from 'https://cdn.jsdelivr.net/gh/mikechambers/es6-perlin-module/perlin.js';
3 | import perlin from './perlin-noise.js';
4 |
5 | import {math} from './math.js';
6 |
7 | export const noise = (function() {
8 |
9 | class _NoiseGenerator {
10 | constructor(params) {
11 | this._params = params;
12 | this._Init();
13 | }
14 |
15 | _Init() {
16 | this._noise = new SimplexNoise(this._params.seed);
17 | }
18 |
19 | Get(x, y, z) {
20 | const G = 2.0 ** (-this._params.persistence);
21 | const xs = x / this._params.scale;
22 | const ys = y / this._params.scale;
23 | const zs = z / this._params.scale;
24 | const noiseFunc = this._noise;
25 |
26 | let amplitude = 1.0;
27 | let frequency = 1.0;
28 | let normalization = 0;
29 | let total = 0;
30 | for (let o = 0; o < this._params.octaves; o++) {
31 | const noiseValue = noiseFunc.noise3D(
32 | xs * frequency, ys * frequency, zs * frequency) * 0.5 + 0.5;
33 |
34 | total += noiseValue * amplitude;
35 | normalization += amplitude;
36 | amplitude *= G;
37 | frequency *= this._params.lacunarity;
38 | }
39 | total /= normalization;
40 | return Math.pow(
41 | total, this._params.exponentiation) * this._params.height;
42 | }
43 | }
44 |
45 | return {
46 | Noise: _NoiseGenerator
47 | }
48 | })();
49 |
--------------------------------------------------------------------------------
/src/perlin-noise.js:
--------------------------------------------------------------------------------
1 | // noise1234
2 | //
3 | // Author: Stefan Gustavson, 2003-2005
4 | // Contact: stefan.gustavson@liu.se
5 | //
6 | // This code was GPL licensed until February 2011.
7 | // As the original author of this code, I hereby
8 | // release it into the public domain.
9 | // Please feel free to use it for whatever you want.
10 | // Credit is appreciated where appropriate, and I also
11 | // appreciate being told where this code finds any use,
12 | // but you may do as you like.
13 |
14 | //Ported to JavaScript by Mike mikechambers
15 | //http://www.mikechambers.com
16 | //
17 | // Note, all return values are scaled to be between 0 and 1
18 | //
19 | //From original C at:
20 | //https://github.com/stegu/perlin-noise
21 | //https://github.com/stegu/perlin-noise/blob/master/src/noise1234.c
22 |
23 | /*
24 | * This implementation is "Improved Noise" as presented by
25 | * Ken Perlin at Siggraph 2002. The 3D function is a direct port
26 | * of his Java reference code which was once publicly available
27 | * on www.noisemachine.com (although I cleaned it up, made it
28 | * faster and made the code more readable), but the 1D, 2D and
29 | * 4D functions were implemented from scratch by me.
30 | *
31 | * This is a backport to C of my improved noise class in C++
32 | * which was included in the Aqsis renderer project.
33 | * It is highly reusable without source code modifications.
34 | *
35 | */
36 |
37 | // This is the new and improved, C(2) continuous interpolant
38 | function fade(t) {
39 | return ( t * t * t * ( t * ( t * 6 - 15 ) + 10 ) );
40 | }
41 |
42 | function lerp(t, a, b) {
43 | return ((a) + (t)*((b)-(a)));
44 | }
45 |
46 |
47 | //---------------------------------------------------------------------
48 | // Static data
49 |
50 | /*
51 | * Permutation table. This is just a random jumble of all numbers 0-255,
52 | * repeated twice to avoid wrapping the index at 255 for each lookup.
53 | * This needs to be exactly the same for all instances on all platforms,
54 | * so it's easiest to just keep it as static explicit data.
55 | * This also removes the need for any initialisation of this class.
56 | *
57 | * Note that making this an int[] instead of a char[] might make the
58 | * code run faster on platforms with a high penalty for unaligned single
59 | * byte addressing. Intel x86 is generally single-byte-friendly, but
60 | * some other CPUs are faster with 4-aligned reads.
61 | * However, a char[] is smaller, which avoids cache trashing, and that
62 | * is probably the most important aspect on most architectures.
63 | * This array is accessed a *lot* by the noise functions.
64 | * A vector-valued noise over 3D accesses it 96 times, and a
65 | * float-valued 4D noise 64 times. We want this to fit in the cache!
66 | */
67 | const perm = [151,160,137,91,90,15,
68 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
69 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
70 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
71 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
72 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
73 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
74 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
75 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
76 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
77 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
78 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
79 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,
80 | 151,160,137,91,90,15,
81 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
82 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
83 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
84 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
85 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
86 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
87 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
88 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
89 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
90 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
91 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
92 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
93 | ];
94 |
95 | //---------------------------------------------------------------------
96 |
97 | /*
98 | * Helper functions to compute gradients-dot-residualvectors (1D to 4D)
99 | * Note that these generate gradients of more than unit length. To make
100 | * a close match with the value range of classic Perlin noise, the final
101 | * noise values need to be rescaled. To match the RenderMan noise in a
102 | * statistical sense, the approximate scaling values (empirically
103 | * determined from test renderings) are:
104 | * 1D noise needs rescaling with 0.188
105 | * 2D noise needs rescaling with 0.507
106 | * 3D noise needs rescaling with 0.936
107 | * 4D noise needs rescaling with 0.87
108 | */
109 |
110 | function grad1( hash, x ) {
111 | let h = hash & 15;
112 | let grad = 1.0 + (h & 7); // Gradient value 1.0, 2.0, ..., 8.0
113 | if (h&8) grad = -grad; // and a random sign for the gradient
114 | return ( grad * x ); // Multiply the gradient with the distance
115 | }
116 |
117 | function grad2( hash, x, y ) {
118 | let h = hash & 7; // Convert low 3 bits of hash code
119 | let u = h<4 ? x : y; // into 8 simple gradient directions,
120 | let v = h<4 ? y : x; // and compute the dot product with (x,y).
121 | return ((h&1)? -u : u) + ((h&2)? -2.0*v : 2.0*v);
122 | }
123 |
124 | function grad3( hash, x, y , z ) {
125 | let h = hash & 15; // Convert low 4 bits of hash code into 12 simple
126 | let u = h<8 ? x : y; // gradient directions, and compute dot product.
127 | let v = h<4 ? y : h==12||h==14 ? x : z; // Fix repeats at h = 12 to 15
128 | return ((h&1)? -u : u) + ((h&2)? -v : v);
129 | }
130 |
131 | function grad4( hash, x, y, z, t ) {
132 | let h = hash & 31; // Convert low 5 bits of hash code into 32 simple
133 | let u = h<24 ? x : y; // gradient directions, and compute dot product.
134 | let v = h<16 ? y : z;
135 | let w = h<8 ? z : t;
136 | return ((h&1)? -u : u) + ((h&2)? -v : v) + ((h&4)? -w : w);
137 | }
138 |
139 | //---------------------------------------------------------------------
140 | /** 1D float Perlin noise, SL "noise()"
141 | */
142 | export function noise1( x )
143 | {
144 | let ix0, ix1;
145 | let fx0, fx1;
146 | let s, n0, n1;
147 |
148 | ix0 = Math.floor( x ); // Integer part of x
149 | fx0 = x - ix0; // Fractional part of x
150 | fx1 = fx0 - 1.0;
151 | ix1 = ( ix0+1 ) & 0xff;
152 | ix0 = ix0 & 0xff; // Wrap to 0..255
153 |
154 | s = fade( fx0 );
155 |
156 | n0 = grad1( perm[ ix0 ], fx0 );
157 | n1 = grad1( perm[ ix1 ], fx1 );
158 | return scale(0.188 * ( lerp( s, n0, n1 ) ));
159 | }
160 |
161 | //---------------------------------------------------------------------
162 | /** 1D float Perlin periodic noise, SL "pnoise()"
163 | */
164 | export function pnoise1( x, px )
165 | {
166 | let ix0, ix1;
167 | let fx0, fx1;
168 | let s, n0, n1;
169 |
170 | ix0 = Math.floor( x ); // Integer part of x
171 | fx0 = x - ix0; // Fractional part of x
172 | fx1 = fx0 - 1.0;
173 | ix1 = (( ix0 + 1 ) % px) & 0xff; // Wrap to 0..px-1 *and* wrap to 0..255
174 | ix0 = ( ix0 % px ) & 0xff; // (because px might be greater than 256)
175 |
176 | s = fade( fx0 );
177 |
178 | n0 = grad1( perm[ ix0 ], fx0 );
179 | n1 = grad1( perm[ ix1 ], fx1 );
180 | return scale(0.188 * ( lerp( s, n0, n1 ) ));
181 | }
182 |
183 |
184 | //---------------------------------------------------------------------
185 | /** 2D float Perlin noise.
186 | */
187 | export function noise2( x, y )
188 | {
189 | let ix0, iy0, ix1, iy1;
190 | let fx0, fy0, fx1, fy1;
191 | let s, t, nx0, nx1, n0, n1;
192 |
193 | ix0 = Math.floor( x ); // Integer part of x
194 | iy0 = Math.floor( y ); // Integer part of y
195 | fx0 = x - ix0; // Fractional part of x
196 | fy0 = y - iy0; // Fractional part of y
197 | fx1 = fx0 - 1.0;
198 | fy1 = fy0 - 1.0;
199 | ix1 = (ix0 + 1) & 0xff; // Wrap to 0..255
200 | iy1 = (iy0 + 1) & 0xff;
201 | ix0 = ix0 & 0xff;
202 | iy0 = iy0 & 0xff;
203 |
204 | t = fade( fy0 );
205 | s = fade( fx0 );
206 |
207 | nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0);
208 | nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1);
209 | n0 = lerp( t, nx0, nx1 );
210 |
211 | nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0);
212 | nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1);
213 | n1 = lerp(t, nx0, nx1);
214 |
215 | return scale(0.507 * ( lerp( s, n0, n1 ) ));
216 | }
217 |
218 | //---------------------------------------------------------------------
219 | /** 2D float Perlin periodic noise.
220 | */
221 | export function pnoise2( x, y, px, py )
222 | {
223 | let ix0, iy0, ix1, iy1;
224 | let fx0, fy0, fx1, fy1;
225 | let s, t, nx0, nx1, n0, n1;
226 |
227 | ix0 = Math.floor( x ); // Integer part of x
228 | iy0 = Math.floor( y ); // Integer part of y
229 | fx0 = x - ix0; // Fractional part of x
230 | fy0 = y - iy0; // Fractional part of y
231 | fx1 = fx0 - 1.0;
232 | fy1 = fy0 - 1.0;
233 | ix1 = (( ix0 + 1 ) % px) & 0xff; // Wrap to 0..px-1 and wrap to 0..255
234 | iy1 = (( iy0 + 1 ) % py) & 0xff; // Wrap to 0..py-1 and wrap to 0..255
235 | ix0 = ( ix0 % px ) & 0xff;
236 | iy0 = ( iy0 % py ) & 0xff;
237 |
238 | t = fade( fy0 );
239 | s = fade( fx0 );
240 |
241 | nx0 = grad2(perm[ix0 + perm[iy0]], fx0, fy0);
242 | nx1 = grad2(perm[ix0 + perm[iy1]], fx0, fy1);
243 | n0 = lerp( t, nx0, nx1 );
244 |
245 | nx0 = grad2(perm[ix1 + perm[iy0]], fx1, fy0);
246 | nx1 = grad2(perm[ix1 + perm[iy1]], fx1, fy1);
247 | n1 = lerp(t, nx0, nx1);
248 |
249 | return scale(0.507 * ( lerp( s, n0, n1 ) ));
250 | }
251 |
252 |
253 | //---------------------------------------------------------------------
254 | /** 3D float Perlin noise.
255 | */
256 | export function noise3( x, y, z )
257 | {
258 | let ix0, iy0, ix1, iy1, iz0, iz1;
259 | let fx0, fy0, fz0, fx1, fy1, fz1;
260 | let s, t, r;
261 | let nxy0, nxy1, nx0, nx1, n0, n1;
262 |
263 | ix0 = Math.floor( x ); // Integer part of x
264 | iy0 = Math.floor( y ); // Integer part of y
265 | iz0 = Math.floor( z ); // Integer part of z
266 | fx0 = x - ix0; // Fractional part of x
267 | fy0 = y - iy0; // Fractional part of y
268 | fz0 = z - iz0; // Fractional part of z
269 | fx1 = fx0 - 1.0;
270 | fy1 = fy0 - 1.0;
271 | fz1 = fz0 - 1.0;
272 | ix1 = ( ix0 + 1 ) & 0xff; // Wrap to 0..255
273 | iy1 = ( iy0 + 1 ) & 0xff;
274 | iz1 = ( iz0 + 1 ) & 0xff;
275 | ix0 = ix0 & 0xff;
276 | iy0 = iy0 & 0xff;
277 | iz0 = iz0 & 0xff;
278 |
279 | r = fade( fz0 );
280 | t = fade( fy0 );
281 | s = fade( fx0 );
282 |
283 | nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0);
284 | nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1);
285 | nx0 = lerp( r, nxy0, nxy1 );
286 |
287 | nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0);
288 | nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1);
289 | nx1 = lerp( r, nxy0, nxy1 );
290 |
291 | n0 = lerp( t, nx0, nx1 );
292 |
293 | nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0);
294 | nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1);
295 | nx0 = lerp( r, nxy0, nxy1 );
296 |
297 | nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0);
298 | nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1);
299 | nx1 = lerp( r, nxy0, nxy1 );
300 |
301 | n1 = lerp( t, nx0, nx1 );
302 |
303 | return scale(0.936 * ( lerp( s, n0, n1 ) ));
304 | }
305 |
306 | //---------------------------------------------------------------------
307 | /** 3D float Perlin periodic noise.
308 | */
309 | export function pnoise3( x, y, z, px, py, pz )
310 | {
311 | let ix0, iy0, ix1, iy1, iz0, iz1;
312 | let fx0, fy0, fz0, fx1, fy1, fz1;
313 | let s, t, r;
314 | let nxy0, nxy1, nx0, nx1, n0, n1;
315 |
316 | ix0 = Math.floor( x ); // Integer part of x
317 | iy0 = Math.floor( y ); // Integer part of y
318 | iz0 = Math.floor( z ); // Integer part of z
319 | fx0 = x - ix0; // Fractional part of x
320 | fy0 = y - iy0; // Fractional part of y
321 | fz0 = z - iz0; // Fractional part of z
322 | fx1 = fx0 - 1.0;
323 | fy1 = fy0 - 1.0;
324 | fz1 = fz0 - 1.0;
325 | ix1 = (( ix0 + 1 ) % px ) & 0xff; // Wrap to 0..px-1 and wrap to 0..255
326 | iy1 = (( iy0 + 1 ) % py ) & 0xff; // Wrap to 0..py-1 and wrap to 0..255
327 | iz1 = (( iz0 + 1 ) % pz ) & 0xff; // Wrap to 0..pz-1 and wrap to 0..255
328 | ix0 = ( ix0 % px ) & 0xff;
329 | iy0 = ( iy0 % py ) & 0xff;
330 | iz0 = ( iz0 % pz ) & 0xff;
331 |
332 | r = fade( fz0 );
333 | t = fade( fy0 );
334 | s = fade( fx0 );
335 |
336 | nxy0 = grad3(perm[ix0 + perm[iy0 + perm[iz0]]], fx0, fy0, fz0);
337 | nxy1 = grad3(perm[ix0 + perm[iy0 + perm[iz1]]], fx0, fy0, fz1);
338 | nx0 = lerp( r, nxy0, nxy1 );
339 |
340 | nxy0 = grad3(perm[ix0 + perm[iy1 + perm[iz0]]], fx0, fy1, fz0);
341 | nxy1 = grad3(perm[ix0 + perm[iy1 + perm[iz1]]], fx0, fy1, fz1);
342 | nx1 = lerp( r, nxy0, nxy1 );
343 |
344 | n0 = lerp( t, nx0, nx1 );
345 |
346 | nxy0 = grad3(perm[ix1 + perm[iy0 + perm[iz0]]], fx1, fy0, fz0);
347 | nxy1 = grad3(perm[ix1 + perm[iy0 + perm[iz1]]], fx1, fy0, fz1);
348 | nx0 = lerp( r, nxy0, nxy1 );
349 |
350 | nxy0 = grad3(perm[ix1 + perm[iy1 + perm[iz0]]], fx1, fy1, fz0);
351 | nxy1 = grad3(perm[ix1 + perm[iy1 + perm[iz1]]], fx1, fy1, fz1);
352 | nx1 = lerp( r, nxy0, nxy1 );
353 |
354 | n1 = lerp( t, nx0, nx1 );
355 |
356 | return scale(0.936 * ( lerp( s, n0, n1 ) ));
357 | }
358 |
359 |
360 | //---------------------------------------------------------------------
361 | /** 4D float Perlin noise.
362 | */
363 |
364 | export function noise4( x, y, z, w )
365 | {
366 | let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1;
367 | let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1;
368 | let s, t, r, q;
369 | let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1;
370 |
371 | ix0 = Math.floor( x ); // Integer part of x
372 | iy0 = Math.floor( y ); // Integer part of y
373 | iz0 = Math.floor( z ); // Integer part of y
374 | iw0 = Math.floor( w ); // Integer part of w
375 | fx0 = x - ix0; // Fractional part of x
376 | fy0 = y - iy0; // Fractional part of y
377 | fz0 = z - iz0; // Fractional part of z
378 | fw0 = w - iw0; // Fractional part of w
379 | fx1 = fx0 - 1.0;
380 | fy1 = fy0 - 1.0;
381 | fz1 = fz0 - 1.0;
382 | fw1 = fw0 - 1.0;
383 | ix1 = ( ix0 + 1 ) & 0xff; // Wrap to 0..255
384 | iy1 = ( iy0 + 1 ) & 0xff;
385 | iz1 = ( iz0 + 1 ) & 0xff;
386 | iw1 = ( iw0 + 1 ) & 0xff;
387 | ix0 = ix0 & 0xff;
388 | iy0 = iy0 & 0xff;
389 | iz0 = iz0 & 0xff;
390 | iw0 = iw0 & 0xff;
391 |
392 | q = fade( fw0 );
393 | r = fade( fz0 );
394 | t = fade( fy0 );
395 | s = fade( fx0 );
396 |
397 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0);
398 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1);
399 | nxy0 = lerp( q, nxyz0, nxyz1 );
400 |
401 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0);
402 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1);
403 | nxy1 = lerp( q, nxyz0, nxyz1 );
404 |
405 | nx0 = lerp ( r, nxy0, nxy1 );
406 |
407 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0);
408 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1);
409 | nxy0 = lerp( q, nxyz0, nxyz1 );
410 |
411 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0);
412 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1);
413 | nxy1 = lerp( q, nxyz0, nxyz1 );
414 |
415 | nx1 = lerp ( r, nxy0, nxy1 );
416 |
417 | n0 = lerp( t, nx0, nx1 );
418 |
419 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0);
420 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1);
421 | nxy0 = lerp( q, nxyz0, nxyz1 );
422 |
423 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0);
424 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1);
425 | nxy1 = lerp( q, nxyz0, nxyz1 );
426 |
427 | nx0 = lerp ( r, nxy0, nxy1 );
428 |
429 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0);
430 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1);
431 | nxy0 = lerp( q, nxyz0, nxyz1 );
432 |
433 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0);
434 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1);
435 | nxy1 = lerp( q, nxyz0, nxyz1 );
436 |
437 | nx1 = lerp ( r, nxy0, nxy1 );
438 |
439 | n1 = lerp( t, nx0, nx1 );
440 |
441 | return scale(0.87 * ( lerp( s, n0, n1 ) ));
442 | }
443 |
444 | //---------------------------------------------------------------------
445 | /** 4D float Perlin periodic noise.
446 | */
447 |
448 | export function pnoise4( x, y, z, w,
449 | px, py, pz, pw )
450 | {
451 | let ix0, iy0, iz0, iw0, ix1, iy1, iz1, iw1;
452 | let fx0, fy0, fz0, fw0, fx1, fy1, fz1, fw1;
453 | let s, t, r, q;
454 | let nxyz0, nxyz1, nxy0, nxy1, nx0, nx1, n0, n1;
455 |
456 | ix0 = Math.floor( x ); // Integer part of x
457 | iy0 = Math.floor( y ); // Integer part of y
458 | iz0 = Math.floor( z ); // Integer part of y
459 | iw0 = Math.floor( w ); // Integer part of w
460 | fx0 = x - ix0; // Fractional part of x
461 | fy0 = y - iy0; // Fractional part of y
462 | fz0 = z - iz0; // Fractional part of z
463 | fw0 = w - iw0; // Fractional part of w
464 | fx1 = fx0 - 1.0;
465 | fy1 = fy0 - 1.0;
466 | fz1 = fz0 - 1.0;
467 | fw1 = fw0 - 1.0;
468 | ix1 = (( ix0 + 1 ) % px ) & 0xff; // Wrap to 0..px-1 and wrap to 0..255
469 | iy1 = (( iy0 + 1 ) % py ) & 0xff; // Wrap to 0..py-1 and wrap to 0..255
470 | iz1 = (( iz0 + 1 ) % pz ) & 0xff; // Wrap to 0..pz-1 and wrap to 0..255
471 | iw1 = (( iw0 + 1 ) % pw ) & 0xff; // Wrap to 0..pw-1 and wrap to 0..255
472 | ix0 = ( ix0 % px ) & 0xff;
473 | iy0 = ( iy0 % py ) & 0xff;
474 | iz0 = ( iz0 % pz ) & 0xff;
475 | iw0 = ( iw0 % pw ) & 0xff;
476 |
477 | q = fade( fw0 );
478 | r = fade( fz0 );
479 | t = fade( fy0 );
480 | s = fade( fx0 );
481 |
482 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx0, fy0, fz0, fw0);
483 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx0, fy0, fz0, fw1);
484 | nxy0 = lerp( q, nxyz0, nxyz1 );
485 |
486 | nxyz0 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx0, fy0, fz1, fw0);
487 | nxyz1 = grad4(perm[ix0 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx0, fy0, fz1, fw1);
488 | nxy1 = lerp( q, nxyz0, nxyz1 );
489 |
490 | nx0 = lerp ( r, nxy0, nxy1 );
491 |
492 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx0, fy1, fz0, fw0);
493 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx0, fy1, fz0, fw1);
494 | nxy0 = lerp( q, nxyz0, nxyz1 );
495 |
496 | nxyz0 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx0, fy1, fz1, fw0);
497 | nxyz1 = grad4(perm[ix0 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx0, fy1, fz1, fw1);
498 | nxy1 = lerp( q, nxyz0, nxyz1 );
499 |
500 | nx1 = lerp ( r, nxy0, nxy1 );
501 |
502 | n0 = lerp( t, nx0, nx1 );
503 |
504 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw0]]]], fx1, fy0, fz0, fw0);
505 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz0 + perm[iw1]]]], fx1, fy0, fz0, fw1);
506 | nxy0 = lerp( q, nxyz0, nxyz1 );
507 |
508 | nxyz0 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw0]]]], fx1, fy0, fz1, fw0);
509 | nxyz1 = grad4(perm[ix1 + perm[iy0 + perm[iz1 + perm[iw1]]]], fx1, fy0, fz1, fw1);
510 | nxy1 = lerp( q, nxyz0, nxyz1 );
511 |
512 | nx0 = lerp ( r, nxy0, nxy1 );
513 |
514 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw0]]]], fx1, fy1, fz0, fw0);
515 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz0 + perm[iw1]]]], fx1, fy1, fz0, fw1);
516 | nxy0 = lerp( q, nxyz0, nxyz1 );
517 |
518 | nxyz0 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw0]]]], fx1, fy1, fz1, fw0);
519 | nxyz1 = grad4(perm[ix1 + perm[iy1 + perm[iz1 + perm[iw1]]]], fx1, fy1, fz1, fw1);
520 | nxy1 = lerp( q, nxyz0, nxyz1 );
521 |
522 | nx1 = lerp ( r, nxy0, nxy1 );
523 |
524 | n1 = lerp( t, nx0, nx1 );
525 |
526 | return scale(0.87 * ( lerp( s, n0, n1 ) ));
527 | }
528 |
529 | function scale(n) {
530 | return (1 + n) / 2;
531 | }
532 |
533 | export default function noise(x, y, z, w) {
534 |
535 | switch(arguments.length) {
536 | case 1:
537 | return noise1(x); //todo: move these to perlin functions
538 | break;
539 | case 2:
540 | return noise2(x, y); //todo: move these to perlin functions
541 | break;
542 | case 3:
543 | return noise3(x, y, z);
544 | case 3:
545 | return noise4(x, y, z, w);
546 | break;
547 | }
548 | }
549 |
550 | //---------------------------------------------------------------------
--------------------------------------------------------------------------------
/src/quadtree.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 |
4 | export const quadtree = (function() {
5 |
6 | class CubeQuadTree {
7 | constructor(params) {
8 | this._params = params;
9 | this._sides = [];
10 |
11 | const r = params.radius;
12 | let m;
13 |
14 | const transforms = [];
15 |
16 | // +Y
17 | m = new THREE.Matrix4();
18 | m.makeRotationX(-Math.PI / 2);
19 | m.premultiply(new THREE.Matrix4().makeTranslation(0, r, 0));
20 | transforms.push(m);
21 |
22 | // -Y
23 | m = new THREE.Matrix4();
24 | m.makeRotationX(Math.PI / 2);
25 | m.premultiply(new THREE.Matrix4().makeTranslation(0, -r, 0));
26 | transforms.push(m);
27 |
28 | // +X
29 | m = new THREE.Matrix4();
30 | m.makeRotationY(Math.PI / 2);
31 | m.premultiply(new THREE.Matrix4().makeTranslation(r, 0, 0));
32 | transforms.push(m);
33 |
34 | // -X
35 | m = new THREE.Matrix4();
36 | m.makeRotationY(-Math.PI / 2);
37 | m.premultiply(new THREE.Matrix4().makeTranslation(-r, 0, 0));
38 | transforms.push(m);
39 |
40 | // +Z
41 | m = new THREE.Matrix4();
42 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, r));
43 | transforms.push(m);
44 |
45 | // -Z
46 | m = new THREE.Matrix4();
47 | m.makeRotationY(Math.PI);
48 | m.premultiply(new THREE.Matrix4().makeTranslation(0, 0, -r));
49 | transforms.push(m);
50 |
51 | for (let t of transforms) {
52 | this._sides.push({
53 | transform: t.clone(),
54 | worldToLocal: t.clone().getInverse(t),
55 | quadtree: new QuadTree({
56 | size: r,
57 | min_node_size: params.min_node_size,
58 | localToWorld: t
59 | }),
60 | });
61 | }
62 | }
63 |
64 | GetChildren() {
65 | const children = [];
66 |
67 | for (let s of this._sides) {
68 | const side = {
69 | transform: s.transform,
70 | children: s.quadtree.GetChildren(),
71 | }
72 | children.push(side);
73 | }
74 | return children;
75 | }
76 |
77 | Insert(pos) {
78 | for (let s of this._sides) {
79 | s.quadtree.Insert(pos);
80 | }
81 | }
82 | }
83 |
84 | class QuadTree {
85 | constructor(params) {
86 | const s = params.size;
87 | const b = new THREE.Box3(
88 | new THREE.Vector3(-s, -s, 0),
89 | new THREE.Vector3(s, s, 0));
90 | this._root = {
91 | bounds: b,
92 | children: [],
93 | center: b.getCenter(new THREE.Vector3()),
94 | sphereCenter: b.getCenter(new THREE.Vector3()),
95 | size: b.getSize(new THREE.Vector3()),
96 | root: true,
97 | };
98 |
99 | this._params = params;
100 | this._root.sphereCenter = this._root.center.clone();
101 | this._root.sphereCenter.applyMatrix4(this._params.localToWorld);
102 | this._root.sphereCenter.normalize();
103 | this._root.sphereCenter.multiplyScalar(this._params.size);
104 | }
105 |
106 | GetChildren() {
107 | const children = [];
108 | this._GetChildren(this._root, children);
109 | return children;
110 | }
111 |
112 | _GetChildren(node, target) {
113 | if (node.children.length == 0) {
114 | target.push(node);
115 | return;
116 | }
117 |
118 | for (let c of node.children) {
119 | this._GetChildren(c, target);
120 | }
121 | }
122 |
123 | Insert(pos) {
124 | this._Insert(this._root, pos);
125 | }
126 |
127 | _Insert(child, pos) {
128 | const distToChild = this._DistanceToChild(child, pos);
129 |
130 | if (distToChild < child.size.x * 1.0 && child.size.x > this._params.min_node_size) {
131 | child.children = this._CreateChildren(child);
132 |
133 | for (let c of child.children) {
134 | this._Insert(c, pos);
135 | }
136 | }
137 | }
138 |
139 | _DistanceToChild(child, pos) {
140 | return child.sphereCenter.distanceTo(pos);
141 | }
142 |
143 | _CreateChildren(child) {
144 | const midpoint = child.bounds.getCenter(new THREE.Vector3());
145 |
146 | // Bottom left
147 | const b1 = new THREE.Box3(child.bounds.min, midpoint);
148 |
149 | // Bottom right
150 | const b2 = new THREE.Box3(
151 | new THREE.Vector3(midpoint.x, child.bounds.min.y, 0),
152 | new THREE.Vector3(child.bounds.max.x, midpoint.y, 0));
153 |
154 | // Top left
155 | const b3 = new THREE.Box3(
156 | new THREE.Vector3(child.bounds.min.x, midpoint.y, 0),
157 | new THREE.Vector3(midpoint.x, child.bounds.max.y, 0));
158 |
159 | // Top right
160 | const b4 = new THREE.Box3(midpoint, child.bounds.max);
161 |
162 | const children = [b1, b2, b3, b4].map(
163 | b => {
164 | return {
165 | bounds: b,
166 | children: [],
167 | center: b.getCenter(new THREE.Vector3()),
168 | size: b.getSize(new THREE.Vector3())
169 | };
170 | });
171 |
172 | for (let c of children) {
173 | c.sphereCenter = c.center.clone();
174 | c.sphereCenter.applyMatrix4(this._params.localToWorld);
175 | c.sphereCenter.normalize()
176 | c.sphereCenter.multiplyScalar(this._params.size);
177 | }
178 |
179 | return children;
180 | }
181 | }
182 |
183 | return {
184 | QuadTree: QuadTree,
185 | CubeQuadTree: CubeQuadTree,
186 | }
187 | })();
188 |
--------------------------------------------------------------------------------
/src/scattering-shader.js:
--------------------------------------------------------------------------------
1 | export const scattering_shader = (function() {
2 |
3 | const _VS = `#version 300 es
4 |
5 | #define saturate(a) clamp( a, 0.0, 1.0 )
6 |
7 | out vec2 vUv;
8 |
9 | void main() {
10 | vUv = uv;
11 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
12 | }
13 | `;
14 |
15 |
16 | const _PS = `#version 300 es
17 | #include
18 |
19 | #define saturate(a) clamp( a, 0.0, 1.0 )
20 |
21 | #define PI 3.141592
22 | #define PRIMARY_STEP_COUNT 16
23 | #define LIGHT_STEP_COUNT 8
24 |
25 |
26 | in vec2 vUv;
27 | out vec4 out_FragColor;
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 |
42 | vec3 _ScreenToWorld(vec3 pos) {
43 | vec4 posP = vec4(pos.xyz * 2.0 - 1.0, 1.0);
44 |
45 | vec4 posVS = inverseProjection * posP;
46 | vec4 posWS = inverseView * vec4((posVS.xyz / posVS.w), 1.0);
47 |
48 | return posWS.xyz;
49 | }
50 |
51 |
52 | float _SoftLight(float a, float b) {
53 | return (b < 0.5 ?
54 | (2.0 * a * b + a * a * (1.0 - 2.0 * b)) :
55 | (2.0 * a * (1.0 - b) + sqrt(a) * (2.0 * b - 1.0))
56 | );
57 | }
58 |
59 | vec3 _SoftLight(vec3 a, vec3 b) {
60 | return vec3(
61 | _SoftLight(a.x, b.x),
62 | _SoftLight(a.y, b.y),
63 | _SoftLight(a.z, b.z)
64 | );
65 | }
66 |
67 | bool _RayIntersectsSphere(
68 | vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius, out float t0, out float t1) {
69 | vec3 oc = rayStart - sphereCenter;
70 | float a = dot(rayDir, rayDir);
71 | float b = 2.0 * dot(oc, rayDir);
72 | float c = dot(oc, oc) - sphereRadius * sphereRadius;
73 | float d = b * b - 4.0 * a * c;
74 |
75 | // Also skip single point of contact
76 | if (d <= 0.0) {
77 | return false;
78 | }
79 |
80 | float r0 = (-b - sqrt(d)) / (2.0 * a);
81 | float r1 = (-b + sqrt(d)) / (2.0 * a);
82 |
83 | t0 = min(r0, r1);
84 | t1 = max(r0, r1);
85 |
86 | return (t1 >= 0.0);
87 | }
88 |
89 |
90 | vec3 _SampleLightRay(
91 | vec3 origin, vec3 sunDir, float planetScale, float planetRadius, float totalRadius,
92 | float rayleighScale, float mieScale, float absorptionHeightMax, float absorptionFalloff) {
93 |
94 | float t0, t1;
95 | _RayIntersectsSphere(origin, sunDir, planetPosition, totalRadius, t0, t1);
96 |
97 | float actualLightStepSize = (t1 - t0) / float(LIGHT_STEP_COUNT);
98 | float virtualLightStepSize = actualLightStepSize * planetScale;
99 | float lightStepPosition = 0.0;
100 |
101 | vec3 opticalDepthLight = vec3(0.0);
102 |
103 | for (int j = 0; j < LIGHT_STEP_COUNT; j++) {
104 | vec3 currentLightSamplePosition = origin + sunDir * (lightStepPosition + actualLightStepSize * 0.5);
105 |
106 | // Calculate the optical depths and accumulate
107 | float currentHeight = length(currentLightSamplePosition) - planetRadius;
108 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualLightStepSize;
109 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualLightStepSize;
110 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
111 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualLightStepSize;
112 |
113 | opticalDepthLight += vec3(
114 | currentOpticalDepthRayleigh,
115 | currentOpticalDepthMie,
116 | currentOpticalDepthOzone);
117 |
118 | lightStepPosition += actualLightStepSize;
119 | }
120 |
121 | return opticalDepthLight;
122 | }
123 |
124 | void _ComputeScattering(
125 | vec3 worldSpacePos, vec3 rayDirection, vec3 rayOrigin, vec3 sunDir,
126 | out vec3 scatteringColour, out vec3 scatteringOpacity) {
127 |
128 | vec3 betaRayleigh = vec3(5.5e-6, 13.0e-6, 22.4e-6);
129 | float betaMie = 21e-6;
130 | vec3 betaAbsorption = vec3(2.04e-5, 4.97e-5, 1.95e-6);
131 | float g = 0.76;
132 | float sunIntensity = 40.0;
133 |
134 | float planetRadius = planetRadius;
135 | float atmosphereRadius = atmosphereRadius - planetRadius;
136 | float totalRadius = planetRadius + atmosphereRadius;
137 |
138 | float referencePlanetRadius = 6371000.0;
139 | float referenceAtmosphereRadius = 100000.0;
140 | float referenceTotalRadius = referencePlanetRadius + referenceAtmosphereRadius;
141 | float referenceRatio = referencePlanetRadius / referenceAtmosphereRadius;
142 |
143 | float scaleRatio = planetRadius / atmosphereRadius;
144 | float planetScale = referencePlanetRadius / planetRadius;
145 | float atmosphereScale = scaleRatio / referenceRatio;
146 | float maxDist = distance(worldSpacePos, rayOrigin);
147 |
148 | float rayleighScale = 8500.0 / (planetScale * atmosphereScale);
149 | float mieScale = 1200.0 / (planetScale * atmosphereScale);
150 | float absorptionHeightMax = 32000.0 * (planetScale * atmosphereScale);
151 | float absorptionFalloff = 3000.0 / (planetScale * atmosphereScale);;
152 |
153 | float mu = dot(rayDirection, sunDir);
154 | float mumu = mu * mu;
155 | float gg = g * g;
156 | float phaseRayleigh = 3.0 / (16.0 * PI) * (1.0 + mumu);
157 | 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));
158 |
159 | // Early out if ray doesn't intersect atmosphere.
160 | float t0, t1;
161 | if (!_RayIntersectsSphere(rayOrigin, rayDirection, planetPosition, totalRadius, t0, t1)) {
162 | scatteringOpacity = vec3(1.0);
163 | return;
164 | }
165 |
166 | // Clip the ray between the camera and potentially the planet surface.
167 | t0 = max(0.0, t0);
168 | t1 = min(maxDist, t1);
169 |
170 | float actualPrimaryStepSize = (t1 - t0) / float(PRIMARY_STEP_COUNT);
171 | float virtualPrimaryStepSize = actualPrimaryStepSize * planetScale;
172 | float primaryStepPosition = 0.0;
173 |
174 | vec3 accumulatedRayleigh = vec3(0.0);
175 | vec3 accumulatedMie = vec3(0.0);
176 | vec3 opticalDepth = vec3(0.0);
177 |
178 | // Take N steps along primary ray
179 | for (int i = 0; i < PRIMARY_STEP_COUNT; i++) {
180 | vec3 currentPrimarySamplePosition = rayOrigin + rayDirection * (
181 | primaryStepPosition + actualPrimaryStepSize * 0.5);
182 |
183 | float currentHeight = max(0.0, length(currentPrimarySamplePosition) - planetRadius);
184 |
185 | float currentOpticalDepthRayleigh = exp(-currentHeight / rayleighScale) * virtualPrimaryStepSize;
186 | float currentOpticalDepthMie = exp(-currentHeight / mieScale) * virtualPrimaryStepSize;
187 |
188 | // Taken from https://www.shadertoy.com/view/wlBXWK
189 | float currentOpticalDepthOzone = (1.0 / cosh((absorptionHeightMax - currentHeight) / absorptionFalloff));
190 | currentOpticalDepthOzone *= currentOpticalDepthRayleigh * virtualPrimaryStepSize;
191 |
192 | opticalDepth += vec3(currentOpticalDepthRayleigh, currentOpticalDepthMie, currentOpticalDepthOzone);
193 |
194 | // Sample light ray and accumulate optical depth.
195 | vec3 opticalDepthLight = _SampleLightRay(
196 | currentPrimarySamplePosition, sunDir,
197 | planetScale, planetRadius, totalRadius,
198 | rayleighScale, mieScale, absorptionHeightMax, absorptionFalloff);
199 |
200 | vec3 r = (
201 | betaRayleigh * (opticalDepth.x + opticalDepthLight.x) +
202 | betaMie * (opticalDepth.y + opticalDepthLight.y) +
203 | betaAbsorption * (opticalDepth.z + opticalDepthLight.z));
204 | vec3 attn = exp(-r);
205 |
206 | accumulatedRayleigh += currentOpticalDepthRayleigh * attn;
207 | accumulatedMie += currentOpticalDepthMie * attn;
208 |
209 | primaryStepPosition += actualPrimaryStepSize;
210 | }
211 |
212 | scatteringColour = sunIntensity * (phaseRayleigh * betaRayleigh * accumulatedRayleigh + phaseMie * betaMie * accumulatedMie);
213 | scatteringOpacity = exp(
214 | -(betaMie * opticalDepth.y + betaRayleigh * opticalDepth.x + betaAbsorption * opticalDepth.z));
215 | }
216 |
217 | vec3 _ApplyGroundFog(
218 | in vec3 rgb,
219 | float distToPoint,
220 | float height,
221 | in vec3 worldSpacePos,
222 | in vec3 rayOrigin,
223 | in vec3 rayDir,
224 | in vec3 sunDir)
225 | {
226 | vec3 up = normalize(rayOrigin);
227 |
228 | float skyAmt = dot(up, rayDir) * 0.25 + 0.75;
229 | skyAmt = saturate(skyAmt);
230 | skyAmt *= skyAmt;
231 |
232 | vec3 DARK_BLUE = vec3(0.1, 0.2, 0.3);
233 | vec3 LIGHT_BLUE = vec3(0.5, 0.6, 0.7);
234 | vec3 DARK_ORANGE = vec3(0.7, 0.4, 0.05);
235 | vec3 BLUE = vec3(0.5, 0.6, 0.7);
236 | vec3 YELLOW = vec3(1.0, 0.9, 0.7);
237 |
238 | vec3 fogCol = mix(DARK_BLUE, LIGHT_BLUE, skyAmt);
239 | float sunAmt = max(dot(rayDir, sunDir), 0.0);
240 | fogCol = mix(fogCol, YELLOW, pow(sunAmt, 16.0));
241 |
242 | float be = 0.0025;
243 | float fogAmt = (1.0 - exp(-distToPoint * be));
244 |
245 | // Sun
246 | sunAmt = 0.5 * saturate(pow(sunAmt, 256.0));
247 |
248 | return mix(rgb, fogCol, fogAmt) + sunAmt * YELLOW;
249 | }
250 |
251 | vec3 _ApplySpaceFog(
252 | in vec3 rgb,
253 | in float distToPoint,
254 | in float height,
255 | in vec3 worldSpacePos,
256 | in vec3 rayOrigin,
257 | in vec3 rayDir,
258 | in vec3 sunDir)
259 | {
260 | float atmosphereThickness = (atmosphereRadius - planetRadius);
261 |
262 | float t0 = -1.0;
263 | float t1 = -1.0;
264 |
265 | // This is a hack since the world mesh has seams that we haven't fixed yet.
266 | if (_RayIntersectsSphere(
267 | rayOrigin, rayDir, planetPosition, planetRadius, t0, t1)) {
268 | if (distToPoint > t0) {
269 | distToPoint = t0;
270 | worldSpacePos = rayOrigin + t0 * rayDir;
271 | }
272 | }
273 |
274 | if (!_RayIntersectsSphere(
275 | rayOrigin, rayDir, planetPosition, planetRadius + atmosphereThickness * 5.0, t0, t1)) {
276 | return rgb * 0.5;
277 | }
278 |
279 | // Figure out a better way to do this
280 | float silhouette = saturate((distToPoint - 10000.0) / 10000.0);
281 |
282 | // Glow around planet
283 | float scaledDistanceToSurface = 0.0;
284 |
285 | // Calculate the closest point between ray direction and planet. Use a point in front of the
286 | // camera to force differences as you get closer to planet.
287 | vec3 fakeOrigin = rayOrigin + rayDir * atmosphereThickness;
288 | float t = max(0.0, dot(rayDir, planetPosition - fakeOrigin) / dot(rayDir, rayDir));
289 | vec3 pb = fakeOrigin + t * rayDir;
290 |
291 | scaledDistanceToSurface = saturate((distance(pb, planetPosition) - planetRadius) / atmosphereThickness);
292 | scaledDistanceToSurface = smoothstep(0.0, 1.0, 1.0 - scaledDistanceToSurface);
293 | //scaledDistanceToSurface = smoothstep(0.0, 1.0, scaledDistanceToSurface);
294 |
295 | float scatteringFactor = scaledDistanceToSurface * silhouette;
296 |
297 | // Fog on surface
298 | t0 = max(0.0, t0);
299 | t1 = min(distToPoint, t1);
300 |
301 | vec3 intersectionPoint = rayOrigin + t1 * rayDir;
302 | vec3 normalAtIntersection = normalize(intersectionPoint);
303 |
304 | float distFactor = exp(-distToPoint * 0.0005 / (atmosphereThickness));
305 | float fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
306 | fresnel = smoothstep(0.0, 1.0, fresnel);
307 |
308 | float extinctionFactor = saturate(fresnel * distFactor) * (1.0 - silhouette);
309 |
310 | // Front/Back Lighting
311 | vec3 BLUE = vec3(0.5, 0.6, 0.75);
312 | vec3 YELLOW = vec3(1.0, 0.9, 0.7);
313 | vec3 RED = vec3(0.035, 0.0, 0.0);
314 |
315 | float NdotL = dot(normalAtIntersection, sunDir);
316 | float wrap = 0.5;
317 | float NdotL_wrap = max(0.0, (NdotL + wrap) / (1.0 + wrap));
318 | float RdotS = max(0.0, dot(rayDir, sunDir));
319 | float sunAmount = RdotS;
320 |
321 | vec3 backLightingColour = YELLOW * 0.1;
322 | vec3 frontLightingColour = mix(BLUE, YELLOW, pow(sunAmount, 32.0));
323 |
324 | vec3 fogColour = mix(backLightingColour, frontLightingColour, NdotL_wrap);
325 |
326 | extinctionFactor *= NdotL_wrap;
327 |
328 | // Sun
329 | float specular = pow((RdotS + 0.5) / (1.0 + 0.5), 64.0);
330 |
331 | fresnel = 1.0 - saturate(dot(-rayDir, normalAtIntersection));
332 | fresnel *= fresnel;
333 |
334 | float sunFactor = (length(pb) - planetRadius) / (atmosphereThickness * 5.0);
335 | sunFactor = (1.0 - saturate(sunFactor));
336 | sunFactor *= sunFactor;
337 | sunFactor *= sunFactor;
338 | sunFactor *= specular * fresnel;
339 |
340 | vec3 baseColour = mix(rgb, fogColour, extinctionFactor);
341 | vec3 litColour = baseColour + _SoftLight(fogColour * scatteringFactor + YELLOW * sunFactor, baseColour);
342 | vec3 blendedColour = mix(baseColour, fogColour, scatteringFactor);
343 | blendedColour += blendedColour + _SoftLight(YELLOW * sunFactor, blendedColour);
344 | return mix(litColour, blendedColour, scaledDistanceToSurface * 0.25);
345 | }
346 |
347 | vec3 _ApplyFog(
348 | in vec3 rgb,
349 | in float distToPoint,
350 | in float height,
351 | in vec3 worldSpacePos,
352 | in vec3 rayOrigin,
353 | in vec3 rayDir,
354 | in vec3 sunDir)
355 | {
356 | float distToPlanet = max(0.0, length(rayOrigin) - planetRadius);
357 | float atmosphereThickness = (atmosphereRadius - planetRadius);
358 |
359 | vec3 groundCol = _ApplyGroundFog(
360 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
361 | vec3 spaceCol = _ApplySpaceFog(
362 | rgb, distToPoint, height, worldSpacePos, rayOrigin, rayDir, sunDir);
363 |
364 | float blendFactor = saturate(distToPlanet / (atmosphereThickness * 0.5));
365 |
366 | blendFactor = smoothstep(0.0, 1.0, blendFactor);
367 | blendFactor = smoothstep(0.0, 1.0, blendFactor);
368 |
369 | return mix(groundCol, spaceCol, blendFactor);
370 | }
371 |
372 | void main() {
373 | float z = texture2D(tDepth, vUv).x;
374 | vec3 posWS = _ScreenToWorld(vec3(vUv, z));
375 | float dist = length(posWS - cameraPosition);
376 | float height = max(0.0, length(cameraPosition) - planetRadius);
377 | vec3 cameraDirection = normalize(posWS - cameraPosition);
378 |
379 | vec3 diffuse = texture2D(tDiffuse, vUv).xyz;
380 | vec3 lightDir = normalize(vec3(1, 1, -1));
381 |
382 | diffuse = _ApplyFog(diffuse, dist, height, posWS, cameraPosition, cameraDirection, lightDir);
383 |
384 | // vec3 scatteringColour = vec3(0.0);
385 | // vec3 scatteringOpacity = vec3(1.0, 1.0, 1.0);
386 | // _ComputeScattering(
387 | // posWS, cameraDirection, cameraPosition,
388 | // lightDir, scatteringColour, scatteringOpacity
389 | // );
390 |
391 | // diffuse = diffuse * scatteringOpacity + scatteringColour;
392 |
393 | diffuse = ACESFilmicToneMapping(diffuse);
394 |
395 | out_FragColor.rgb = diffuse;
396 | out_FragColor.a = 1.0;
397 | }
398 | `;
399 |
400 |
401 | const _Shader = {
402 | uniforms: {
403 | "tDiffuse": { value: null },
404 | "tDepth": { value: null },
405 | "cameraNear": { value: 0.0 },
406 | "cameraFar": { value: 0.0 },
407 | },
408 | vertexShader: _VS,
409 | fragmentShader: _PS,
410 | };
411 |
412 | return {
413 | Shader: _Shader,
414 | VS: _VS,
415 | PS: _PS,
416 | };
417 | })();
418 |
--------------------------------------------------------------------------------
/src/sky.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 | import {Sky} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/objects/Sky.js';
4 | import {Water} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/objects/Water.js';
5 |
6 |
7 | export const sky = (function() {
8 |
9 | class TerrainSky {
10 | constructor(params) {
11 | this._params = params;
12 | this._Init(params);
13 | }
14 |
15 | _Init(params) {
16 | const waterGeometry = new THREE.PlaneBufferGeometry(10000, 10000, 100, 100);
17 |
18 | this._water = new Water(
19 | waterGeometry,
20 | {
21 | textureWidth: 2048,
22 | textureHeight: 2048,
23 | waterNormals: new THREE.TextureLoader().load( 'resources/waternormals.jpg', function ( texture ) {
24 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
25 | } ),
26 | alpha: 0.5,
27 | sunDirection: new THREE.Vector3(1, 0, 0),
28 | sunColor: 0xffffff,
29 | waterColor: 0x001e0f,
30 | distortionScale: 0.0,
31 | fog: undefined
32 | }
33 | );
34 | // this._water.rotation.x = - Math.PI / 2;
35 | // this._water.position.y = 4;
36 |
37 | this._sky = new Sky();
38 | this._sky.scale.setScalar(10000);
39 |
40 | this._group = new THREE.Group();
41 | //this._group.add(this._water);
42 | this._group.add(this._sky);
43 |
44 | params.scene.add(this._group);
45 |
46 | params.guiParams.sky = {
47 | turbidity: 10.0,
48 | rayleigh: 2,
49 | mieCoefficient: 0.005,
50 | mieDirectionalG: 0.8,
51 | luminance: 1,
52 | };
53 |
54 | params.guiParams.sun = {
55 | inclination: 0.31,
56 | azimuth: 0.25,
57 | };
58 |
59 | const onShaderChange = () => {
60 | for (let k in params.guiParams.sky) {
61 | this._sky.material.uniforms[k].value = params.guiParams.sky[k];
62 | }
63 | for (let k in params.guiParams.general) {
64 | this._sky.material.uniforms[k].value = params.guiParams.general[k];
65 | }
66 | };
67 |
68 | const onSunChange = () => {
69 | var theta = Math.PI * (params.guiParams.sun.inclination - 0.5);
70 | var phi = 2 * Math.PI * (params.guiParams.sun.azimuth - 0.5);
71 |
72 | const sunPosition = new THREE.Vector3();
73 | sunPosition.x = Math.cos(phi);
74 | sunPosition.y = Math.sin(phi) * Math.sin(theta);
75 | sunPosition.z = Math.sin(phi) * Math.cos(theta);
76 |
77 | this._sky.material.uniforms['sunPosition'].value.copy(sunPosition);
78 | this._water.material.uniforms['sunDirection'].value.copy(sunPosition.normalize());
79 | };
80 |
81 | const skyRollup = params.gui.addFolder('Sky');
82 | skyRollup.add(params.guiParams.sky, "turbidity", 0.1, 30.0).onChange(
83 | onShaderChange);
84 | skyRollup.add(params.guiParams.sky, "rayleigh", 0.1, 4.0).onChange(
85 | onShaderChange);
86 | skyRollup.add(params.guiParams.sky, "mieCoefficient", 0.0001, 0.1).onChange(
87 | onShaderChange);
88 | skyRollup.add(params.guiParams.sky, "mieDirectionalG", 0.0, 1.0).onChange(
89 | onShaderChange);
90 | skyRollup.add(params.guiParams.sky, "luminance", 0.0, 2.0).onChange(
91 | onShaderChange);
92 |
93 | const sunRollup = params.gui.addFolder('Sun');
94 | sunRollup.add(params.guiParams.sun, "inclination", 0.0, 1.0).onChange(
95 | onSunChange);
96 | sunRollup.add(params.guiParams.sun, "azimuth", 0.0, 1.0).onChange(
97 | onSunChange);
98 |
99 | onShaderChange();
100 | onSunChange();
101 | }
102 |
103 | Update(timeInSeconds) {
104 | this._water.material.uniforms['time'].value += timeInSeconds;
105 |
106 | this._group.position.x = this._params.camera.position.x;
107 | this._group.position.z = this._params.camera.position.z;
108 | }
109 | }
110 |
111 |
112 | return {
113 | TerrainSky: TerrainSky
114 | }
115 | })();
116 |
--------------------------------------------------------------------------------
/src/spline.js:
--------------------------------------------------------------------------------
1 | export const spline = (function() {
2 |
3 | class _CubicHermiteSpline {
4 | constructor(lerp) {
5 | this._points = [];
6 | this._lerp = lerp;
7 | }
8 |
9 | AddPoint(t, d) {
10 | this._points.push([t, d]);
11 | }
12 |
13 | Get(t) {
14 | let p1 = 0;
15 |
16 | for (let i = 0; i < this._points.length; i++) {
17 | if (this._points[i][0] >= t) {
18 | break;
19 | }
20 | p1 = i;
21 | }
22 |
23 | const p0 = Math.max(0, p1 - 1);
24 | const p2 = Math.min(this._points.length - 1, p1 + 1);
25 | const p3 = Math.min(this._points.length - 1, p1 + 2);
26 |
27 | if (p1 == p2) {
28 | return this._points[p1][1];
29 | }
30 |
31 | return this._lerp(
32 | (t - this._points[p1][0]) / (
33 | this._points[p2][0] - this._points[p1][0]),
34 | this._points[p0][1], this._points[p1][1],
35 | this._points[p2][1], this._points[p3][1]);
36 | }
37 | };
38 |
39 | class _LinearSpline {
40 | constructor(lerp) {
41 | this._points = [];
42 | this._lerp = lerp;
43 | }
44 |
45 | AddPoint(t, d) {
46 | this._points.push([t, d]);
47 | }
48 |
49 | Get(t) {
50 | let p1 = 0;
51 |
52 | for (let i = 0; i < this._points.length; i++) {
53 | if (this._points[i][0] >= t) {
54 | break;
55 | }
56 | p1 = i;
57 | }
58 |
59 | const p2 = Math.min(this._points.length - 1, p1 + 1);
60 |
61 | if (p1 == p2) {
62 | return this._points[p1][1];
63 | }
64 |
65 | return this._lerp(
66 | (t - this._points[p1][0]) / (
67 | this._points[p2][0] - this._points[p1][0]),
68 | this._points[p1][1], this._points[p2][1]);
69 | }
70 | }
71 |
72 | return {
73 | CubicHermiteSpline: _CubicHermiteSpline,
74 | LinearSpline: _LinearSpline,
75 | };
76 | })();
77 |
--------------------------------------------------------------------------------
/src/terrain-chunk.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 |
4 | export const terrain_chunk = (function() {
5 |
6 | class TerrainChunk {
7 | constructor(params) {
8 | this._params = params;
9 | this._Init(params);
10 | }
11 |
12 | Destroy() {
13 | this._params.group.remove(this._plane);
14 | }
15 |
16 | Hide() {
17 | this._plane.visible = false;
18 | }
19 |
20 | Show() {
21 | this._plane.visible = true;
22 | }
23 |
24 | _Init(params) {
25 | this._geometry = new THREE.BufferGeometry();
26 | this._plane = new THREE.Mesh(this._geometry, params.material);
27 | this._plane.castShadow = false;
28 | this._plane.receiveShadow = true;
29 | this._params.group.add(this._plane);
30 | }
31 |
32 | _GenerateHeight(v) {
33 | return this._params.heightGenerators[0].Get(v.x, v.y, v.z)[0];
34 | }
35 |
36 | *_Rebuild() {
37 | const _D = new THREE.Vector3();
38 | const _D1 = new THREE.Vector3();
39 | const _D2 = new THREE.Vector3();
40 | const _P = new THREE.Vector3();
41 | const _H = new THREE.Vector3();
42 | const _W = new THREE.Vector3();
43 | const _C = new THREE.Vector3();
44 | const _S = new THREE.Vector3();
45 |
46 | const _N = new THREE.Vector3();
47 | const _N1 = new THREE.Vector3();
48 | const _N2 = new THREE.Vector3();
49 | const _N3 = new THREE.Vector3();
50 |
51 | const positions = [];
52 | const colors = [];
53 | const normals = [];
54 | const tangents = [];
55 | const uvs = [];
56 | const weights1 = [];
57 | const weights2 = [];
58 | const indices = [];
59 | const wsPositions = [];
60 |
61 | const localToWorld = this._params.group.matrix;
62 | const resolution = this._params.resolution;
63 | const radius = this._params.radius;
64 | const offset = this._params.offset;
65 | const width = this._params.width;
66 | const half = width / 2;
67 |
68 | for (let x = 0; x < resolution + 1; x++) {
69 | const xp = width * x / resolution;
70 | for (let y = 0; y < resolution + 1; y++) {
71 | const yp = width * y / resolution;
72 |
73 | // Compute position
74 | _P.set(xp - half, yp - half, radius);
75 | _P.add(offset);
76 | _P.normalize();
77 | _D.copy(_P);
78 | _P.multiplyScalar(radius);
79 | _P.z -= radius;
80 |
81 | // Compute a world space position to sample noise
82 | _W.copy(_P);
83 | _W.applyMatrix4(localToWorld);
84 |
85 | const height = this._GenerateHeight(_W);
86 |
87 | // Purturb height along z-vector
88 | _H.copy(_D);
89 | _H.multiplyScalar(height);
90 | _P.add(_H);
91 |
92 | positions.push(_P.x, _P.y, _P.z);
93 |
94 | _S.set(_W.x, _W.y, height);
95 |
96 | const color = this._params.colourGenerator.GetColour(_S);
97 | colors.push(color.r, color.g, color.b);
98 | normals.push(_D.x, _D.y, _D.z);
99 | tangents.push(1, 0, 0, 1);
100 | wsPositions.push(_W.x, _W.y, height);
101 | // TODO GUI
102 | uvs.push(_P.x / 200.0, _P.y / 200.0);
103 | }
104 | }
105 | yield;
106 |
107 | for (let i = 0; i < resolution; i++) {
108 | for (let j = 0; j < resolution; j++) {
109 | indices.push(
110 | i * (resolution + 1) + j,
111 | (i + 1) * (resolution + 1) + j + 1,
112 | i * (resolution + 1) + j + 1);
113 | indices.push(
114 | (i + 1) * (resolution + 1) + j,
115 | (i + 1) * (resolution + 1) + j + 1,
116 | i * (resolution + 1) + j);
117 | }
118 | }
119 | yield;
120 |
121 | const up = [...normals];
122 |
123 | for (let i = 0, n = indices.length; i < n; i+= 3) {
124 | const i1 = indices[i] * 3;
125 | const i2 = indices[i+1] * 3;
126 | const i3 = indices[i+2] * 3;
127 |
128 | _N1.fromArray(positions, i1);
129 | _N2.fromArray(positions, i2);
130 | _N3.fromArray(positions, i3);
131 |
132 | _D1.subVectors(_N3, _N2);
133 | _D2.subVectors(_N1, _N2);
134 | _D1.cross(_D2);
135 |
136 | normals[i1] += _D1.x;
137 | normals[i2] += _D1.x;
138 | normals[i3] += _D1.x;
139 |
140 | normals[i1+1] += _D1.y;
141 | normals[i2+1] += _D1.y;
142 | normals[i3+1] += _D1.y;
143 |
144 | normals[i1+2] += _D1.z;
145 | normals[i2+2] += _D1.z;
146 | normals[i3+2] += _D1.z;
147 | }
148 | yield;
149 |
150 | for (let i = 0, n = normals.length; i < n; i+=3) {
151 | _N.fromArray(normals, i);
152 | _N.normalize();
153 | normals[i] = _N.x;
154 | normals[i+1] = _N.y;
155 | normals[i+2] = _N.z;
156 | }
157 | yield;
158 |
159 | let count = 0;
160 | for (let i = 0, n = indices.length; i < n; i+=3) {
161 | const splats = [];
162 | const i1 = indices[i] * 3;
163 | const i2 = indices[i+1] * 3;
164 | const i3 = indices[i+2] * 3;
165 | const indexes = [i1, i2, i3];
166 | for (let j = 0; j < 3; j++) {
167 | const j1 = indexes[j];
168 | _P.fromArray(wsPositions, j1);
169 | _N.fromArray(normals, j1);
170 | _D.fromArray(up, j1);
171 | const s = this._params.colourGenerator.GetSplat(_P, _N, _D);
172 | splats.push(s);
173 | }
174 |
175 | const splatStrengths = {};
176 | for (let k in splats[0]) {
177 | splatStrengths[k] = {key: k, strength: 0.0};
178 | }
179 | for (let curSplat of splats) {
180 | for (let k in curSplat) {
181 | splatStrengths[k].strength += curSplat[k].strength;
182 | }
183 | }
184 |
185 | let typeValues = Object.values(splatStrengths);
186 | typeValues.sort((a, b) => {
187 | if (a.strength < b.strength) {
188 | return 1;
189 | }
190 | if (a.strength > b.strength) {
191 | return -1;
192 | }
193 | return 0;
194 | });
195 |
196 | const w1 = indices[i] * 4;
197 | const w2 = indices[i+1] * 4;
198 | const w3 = indices[i+2] * 4;
199 |
200 | for (let s = 0; s < 3; s++) {
201 | let total = (
202 | splats[s][typeValues[0].key].strength +
203 | splats[s][typeValues[1].key].strength +
204 | splats[s][typeValues[2].key].strength +
205 | splats[s][typeValues[3].key].strength);
206 | const normalization = 1.0 / total;
207 |
208 | splats[s][typeValues[0].key].strength *= normalization;
209 | splats[s][typeValues[1].key].strength *= normalization;
210 | splats[s][typeValues[2].key].strength *= normalization;
211 | splats[s][typeValues[3].key].strength *= normalization;
212 | }
213 |
214 | weights1.push(splats[0][typeValues[3].key].index);
215 | weights1.push(splats[0][typeValues[2].key].index);
216 | weights1.push(splats[0][typeValues[1].key].index);
217 | weights1.push(splats[0][typeValues[0].key].index);
218 |
219 | weights1.push(splats[1][typeValues[3].key].index);
220 | weights1.push(splats[1][typeValues[2].key].index);
221 | weights1.push(splats[1][typeValues[1].key].index);
222 | weights1.push(splats[1][typeValues[0].key].index);
223 |
224 | weights1.push(splats[2][typeValues[3].key].index);
225 | weights1.push(splats[2][typeValues[2].key].index);
226 | weights1.push(splats[2][typeValues[1].key].index);
227 | weights1.push(splats[2][typeValues[0].key].index);
228 |
229 | weights2.push(splats[0][typeValues[3].key].strength);
230 | weights2.push(splats[0][typeValues[2].key].strength);
231 | weights2.push(splats[0][typeValues[1].key].strength);
232 | weights2.push(splats[0][typeValues[0].key].strength);
233 |
234 | weights2.push(splats[1][typeValues[3].key].strength);
235 | weights2.push(splats[1][typeValues[2].key].strength);
236 | weights2.push(splats[1][typeValues[1].key].strength);
237 | weights2.push(splats[1][typeValues[0].key].strength);
238 |
239 | weights2.push(splats[2][typeValues[3].key].strength);
240 | weights2.push(splats[2][typeValues[2].key].strength);
241 | weights2.push(splats[2][typeValues[1].key].strength);
242 | weights2.push(splats[2][typeValues[0].key].strength);
243 |
244 | count++;
245 | if ((count % 10000) == 0) {
246 | yield;
247 | }
248 | }
249 | yield;
250 |
251 | function _Unindex(src, stride) {
252 | const dst = [];
253 | for (let i = 0, n = indices.length; i < n; i+= 3) {
254 | const i1 = indices[i] * stride;
255 | const i2 = indices[i+1] * stride;
256 | const i3 = indices[i+2] * stride;
257 |
258 | for (let j = 0; j < stride; j++) {
259 | dst.push(src[i1 + j]);
260 | }
261 | for (let j = 0; j < stride; j++) {
262 | dst.push(src[i2 + j]);
263 | }
264 | for (let j = 0; j < stride; j++) {
265 | dst.push(src[i3 + j]);
266 | }
267 | }
268 | return dst;
269 | }
270 |
271 | const uiPositions = _Unindex(positions, 3);
272 | yield;
273 |
274 | const uiColours = _Unindex(colors, 3);
275 | yield;
276 |
277 | const uiNormals = _Unindex(normals, 3);
278 | yield;
279 |
280 | const uiTangents = _Unindex(tangents, 4);
281 | yield;
282 |
283 | const uiUVs = _Unindex(uvs, 2);
284 | yield;
285 |
286 | const uiWeights1 = weights1;
287 | const uiWeights2 = weights2;
288 |
289 | this._geometry.setAttribute(
290 | 'position', new THREE.Float32BufferAttribute(uiPositions, 3));
291 | this._geometry.setAttribute(
292 | 'color', new THREE.Float32BufferAttribute(uiColours, 3));
293 | this._geometry.setAttribute(
294 | 'normal', new THREE.Float32BufferAttribute(uiNormals, 3));
295 | this._geometry.setAttribute(
296 | 'tangent', new THREE.Float32BufferAttribute(uiTangents, 4));
297 | this._geometry.setAttribute(
298 | 'weights1', new THREE.Float32BufferAttribute(uiWeights1, 4));
299 | this._geometry.setAttribute(
300 | 'weights2', new THREE.Float32BufferAttribute(uiWeights2, 4));
301 | this._geometry.setAttribute(
302 | 'uv', new THREE.Float32BufferAttribute(uiUVs, 2));
303 | }
304 | }
305 |
306 | return {
307 | TerrainChunk: TerrainChunk
308 | }
309 | })();
310 |
--------------------------------------------------------------------------------
/src/terrain-shader.js:
--------------------------------------------------------------------------------
1 | export const terrain_shader = (function() {
2 |
3 | const _VS = `#version 300 es
4 |
5 | precision highp float;
6 |
7 | uniform mat4 modelMatrix;
8 | uniform mat4 modelViewMatrix;
9 | uniform mat4 projectionMatrix;
10 | uniform vec3 cameraPosition;
11 | uniform float fogDensity;
12 | uniform vec3 cloudScale;
13 |
14 | // Attributes
15 | in vec3 position;
16 | in vec3 normal;
17 | in vec4 tangent;
18 | in vec3 color;
19 | in vec2 uv;
20 | in vec4 weights1;
21 | in vec4 weights2;
22 |
23 | // Outputs
24 | out vec2 vUV;
25 | out vec4 vColor;
26 | out vec3 vNormal;
27 | out vec4 vTangent;
28 | out vec3 vPosition;
29 | out vec4 vWeights1;
30 | out vec4 vWeights2;
31 |
32 | #define saturate(a) clamp( a, 0.0, 1.0 )
33 |
34 | void main(){
35 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
36 |
37 | vUV = uv;
38 | vNormal = normal;
39 | vTangent = tangent;
40 |
41 | vColor = vec4(color, 1);
42 | vPosition = position.xyz;
43 | vWeights1 = weights1;
44 | vWeights2 = weights2;
45 | }
46 | `;
47 |
48 |
49 | const _PS = `#version 300 es
50 |
51 | precision highp float;
52 | precision highp int;
53 | precision highp sampler2DArray;
54 |
55 | uniform sampler2DArray normalMap;
56 | uniform sampler2DArray diffuseMap;
57 | uniform sampler2D noiseMap;
58 |
59 | uniform mat4 modelMatrix;
60 | uniform mat4 modelViewMatrix;
61 | uniform vec3 cameraPosition;
62 |
63 | in vec2 vUV;
64 | in vec4 vColor;
65 | in vec3 vNormal;
66 | in vec4 vTangent;
67 | in vec3 vPosition;
68 | in vec4 vWeights1;
69 | in vec4 vWeights2;
70 |
71 | out vec4 out_FragColor;
72 |
73 | #define saturate(a) clamp( a, 0.0, 1.0 )
74 |
75 | const float _TRI_SCALE = 2000.0;
76 |
77 | float sum( vec3 v ) { return v.x+v.y+v.z; }
78 |
79 | vec4 hash4( vec2 p ) {
80 | return fract(
81 | sin(vec4(1.0+dot(p,vec2(37.0,17.0)),
82 | 2.0+dot(p,vec2(11.0,47.0)),
83 | 3.0+dot(p,vec2(41.0,29.0)),
84 | 4.0+dot(p,vec2(23.0,31.0))))*103.0);
85 | }
86 |
87 |
88 | vec3 _ACESFilmicToneMapping(vec3 x) {
89 | float a = 2.51;
90 | float b = 0.03;
91 | float c = 2.43;
92 | float d = 0.59;
93 | float e = 0.14;
94 | return saturate((x*(a*x+b))/(x*(c*x+d)+e));
95 | }
96 |
97 | vec4 _CalculateLighting(
98 | vec3 lightDirection, vec3 lightColour, vec3 worldSpaceNormal, vec3 viewDirection) {
99 | float diffuse = saturate(dot(worldSpaceNormal, lightDirection));
100 |
101 | vec3 H = normalize(lightDirection + viewDirection);
102 | float NdotH = dot(worldSpaceNormal, H);
103 | float specular = saturate(pow(NdotH, 8.0));
104 |
105 | return vec4(lightColour * (diffuse + diffuse * specular), 0);
106 | }
107 |
108 | vec4 _ComputeLighting(vec3 worldSpaceNormal, vec3 sunDir, vec3 viewDirection) {
109 | // Hardcoded, whee!
110 | vec4 lighting;
111 |
112 | lighting += _CalculateLighting(
113 | sunDir, vec3(1.0, 1.0, 1.0), worldSpaceNormal, viewDirection);
114 | // lighting += _CalculateLighting(
115 | // -sunDir, vec3(0.1, 0.1, 0.15), worldSpaceNormal, viewDirection);
116 | lighting += _CalculateLighting(
117 | vec3(0, 1, 0), vec3(0.25, 0.25, 0.25), worldSpaceNormal, viewDirection);
118 |
119 | lighting += vec4(0.15, 0.15, 0.15, 0.0);
120 |
121 | return lighting;
122 | }
123 |
124 | vec4 _TerrainBlend_4(vec4 samples[4]) {
125 | float depth = 0.2;
126 | float ma = max(
127 | samples[0].w,
128 | max(
129 | samples[1].w,
130 | max(samples[2].w, samples[3].w))) - depth;
131 |
132 | float b1 = max(samples[0].w - ma, 0.0);
133 | float b2 = max(samples[1].w - ma, 0.0);
134 | float b3 = max(samples[2].w - ma, 0.0);
135 | float b4 = max(samples[3].w - ma, 0.0);
136 |
137 | vec4 numer = (
138 | samples[0] * b1 + samples[1] * b2 +
139 | samples[2] * b3 + samples[3] * b4);
140 | float denom = (b1 + b2 + b3 + b4);
141 | return numer / denom;
142 | }
143 |
144 | vec4 _TerrainBlend_4_lerp(vec4 samples[4]) {
145 | return (
146 | samples[0] * samples[0].w + samples[1] * samples[1].w +
147 | samples[2] * samples[2].w + samples[3] * samples[3].w);
148 | }
149 |
150 | // Lifted from https://www.shadertoy.com/view/Xtl3zf
151 | vec4 texture_UV(in sampler2DArray srcTexture, in vec3 x) {
152 | float k = texture(noiseMap, 0.0025*x.xy).x; // cheap (cache friendly) lookup
153 | float l = k*8.0;
154 | float f = fract(l);
155 |
156 | float ia = floor(l+0.5); // suslik's method (see comments)
157 | float ib = floor(l);
158 | f = min(f, 1.0-f)*2.0;
159 |
160 | vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
161 | vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
162 |
163 | vec4 cola = texture(srcTexture, vec3(x.xy + offa, x.z));
164 | vec4 colb = texture(srcTexture, vec3(x.xy + offb, x.z));
165 |
166 | return mix(cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola.xyz-colb.xyz)));
167 | }
168 |
169 | vec4 _Triplanar_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
170 | vec4 dx = texture_UV(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
171 | vec4 dy = texture_UV(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
172 | vec4 dz = texture_UV(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
173 |
174 | vec3 weights = abs(normal.xyz);
175 | weights = weights / (weights.x + weights.y + weights.z);
176 |
177 | return dx * weights.x + dy * weights.y + dz * weights.z;
178 | }
179 |
180 | vec4 _TriplanarN_UV(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
181 | // Tangent Reconstruction
182 | // Triplanar uvs
183 | vec2 uvX = pos.zy; // x facing plane
184 | vec2 uvY = pos.xz; // y facing plane
185 | vec2 uvZ = pos.xy; // z facing plane
186 | // Tangent space normal maps
187 | vec3 tx = texture_UV(tex, vec3(uvX / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
188 | vec3 ty = texture_UV(tex, vec3(uvY / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
189 | vec3 tz = texture_UV(tex, vec3(uvZ / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
190 |
191 | vec3 weights = abs(normal.xyz);
192 | weights = weights / (weights.x + weights.y + weights.z);
193 |
194 | // Get the sign (-1 or 1) of the surface normal
195 | vec3 axis = sign(normal);
196 | // Construct tangent to world matrices for each axis
197 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
198 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
199 | mat3 tbnX = mat3(tangentX, bitangentX, normal);
200 |
201 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
202 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
203 | mat3 tbnY = mat3(tangentY, bitangentY, normal);
204 |
205 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
206 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
207 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
208 |
209 | // Apply tangent to world matrix and triblend
210 | // Using clamp() because the cross products may be NANs
211 | vec3 worldNormal = normalize(
212 | clamp(tbnX * tx, -1.0, 1.0) * weights.x +
213 | clamp(tbnY * ty, -1.0, 1.0) * weights.y +
214 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z
215 | );
216 | return vec4(worldNormal, 0.0);
217 | }
218 |
219 | vec4 _Triplanar(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
220 | vec4 dx = texture(tex, vec3(pos.zy / _TRI_SCALE, texSlice));
221 | vec4 dy = texture(tex, vec3(pos.xz / _TRI_SCALE, texSlice));
222 | vec4 dz = texture(tex, vec3(pos.xy / _TRI_SCALE, texSlice));
223 |
224 | vec3 weights = abs(normal.xyz);
225 | weights = weights / (weights.x + weights.y + weights.z);
226 |
227 | return dx * weights.x + dy * weights.y + dz * weights.z;
228 | }
229 |
230 | vec4 _TriplanarN(vec3 pos, vec3 normal, float texSlice, sampler2DArray tex) {
231 | vec2 uvx = pos.zy;
232 | vec2 uvy = pos.xz;
233 | vec2 uvz = pos.xy;
234 | vec3 tx = texture(tex, vec3(uvx / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
235 | vec3 ty = texture(tex, vec3(uvy / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
236 | vec3 tz = texture(tex, vec3(uvz / _TRI_SCALE, texSlice)).xyz * vec3(2,2,2) - vec3(1,1,1);
237 |
238 | vec3 weights = abs(normal.xyz);
239 | weights *= weights;
240 | weights = weights / (weights.x + weights.y + weights.z);
241 |
242 | vec3 axis = sign(normal);
243 | vec3 tangentX = normalize(cross(normal, vec3(0.0, axis.x, 0.0)));
244 | vec3 bitangentX = normalize(cross(tangentX, normal)) * axis.x;
245 | mat3 tbnX = mat3(tangentX, bitangentX, normal);
246 |
247 | vec3 tangentY = normalize(cross(normal, vec3(0.0, 0.0, axis.y)));
248 | vec3 bitangentY = normalize(cross(tangentY, normal)) * axis.y;
249 | mat3 tbnY = mat3(tangentY, bitangentY, normal);
250 |
251 | vec3 tangentZ = normalize(cross(normal, vec3(0.0, -axis.z, 0.0)));
252 | vec3 bitangentZ = normalize(-cross(tangentZ, normal)) * axis.z;
253 | mat3 tbnZ = mat3(tangentZ, bitangentZ, normal);
254 |
255 | vec3 worldNormal = normalize(
256 | clamp(tbnX * tx, -1.0, 1.0) * weights.x +
257 | clamp(tbnY * ty, -1.0, 1.0) * weights.y +
258 | clamp(tbnZ * tz, -1.0, 1.0) * weights.z);
259 | return vec4(worldNormal, 0.0);
260 | }
261 |
262 | void main() {
263 | vec3 worldPosition = (modelMatrix * vec4(vPosition, 1)).xyz;
264 | vec3 eyeDirection = normalize(worldPosition - cameraPosition);
265 | vec3 sunDir = normalize(vec3(1, 1, -1));
266 |
267 | float weightIndices[4] = float[4](vWeights1.x, vWeights1.y, vWeights1.z, vWeights1.w);
268 | float weightValues[4] = float[4](vWeights2.x, vWeights2.y, vWeights2.z, vWeights2.w);
269 |
270 | // TRIPLANAR SPLATTING w/ NORMALS & UVS
271 | vec3 worldSpaceNormal = (modelMatrix * vec4(vNormal, 0.0)).xyz;
272 | vec4 diffuseSamples[4];
273 | vec4 normalSamples[4];
274 |
275 | for (int i = 0; i < 4; ++i) {
276 | vec4 d = vec4(0.0);
277 | vec4 n = vec4(0.0);
278 | if (weightValues[i] > 0.0) {
279 | d = _Triplanar_UV(
280 | worldPosition, worldSpaceNormal, weightIndices[i], diffuseMap);
281 | n = _TriplanarN_UV(
282 | worldPosition, worldSpaceNormal, weightIndices[i], normalMap);
283 |
284 | d.w *= weightValues[i];
285 | n.w = d.w;
286 | }
287 |
288 | diffuseSamples[i] = d;
289 | normalSamples[i] = n;
290 | }
291 |
292 | vec4 diffuseBlended = _TerrainBlend_4(diffuseSamples);
293 | vec4 normalBlended = _TerrainBlend_4(normalSamples);
294 |
295 | vec3 diffuse = diffuseBlended.xyz;
296 | worldSpaceNormal = normalize(normalBlended.xyz);
297 |
298 | // Bit of a hack to remove lighting on dark side of planet
299 | vec3 planetNormal = normalize(worldPosition);
300 | float planetLighting = saturate(dot(planetNormal, sunDir));
301 |
302 | vec4 lighting = _ComputeLighting(worldSpaceNormal, sunDir, -eyeDirection);
303 | vec3 finalColour = mix(vec3(1.0, 1.0, 1.0), vColor.xyz, 0.25) * diffuse;
304 |
305 | finalColour *= lighting.xyz * planetLighting;
306 |
307 | out_FragColor = vec4(_ACESFilmicToneMapping(finalColour), 1);
308 | }
309 |
310 | `;
311 |
312 | return {
313 | VS: _VS,
314 | PS: _PS,
315 | };
316 | })();
317 |
--------------------------------------------------------------------------------
/src/terrain.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 | import {graphics} from './graphics.js';
4 | import {math} from './math.js';
5 | import {noise} from './noise.js';
6 | import {quadtree} from './quadtree.js';
7 | import {spline} from './spline.js';
8 | import {terrain_chunk} from './terrain-chunk.js';
9 | import {terrain_shader} from './terrain-shader.js';
10 | import {textures} from './textures.js';
11 | import {utils} from './utils.js';
12 |
13 | export const terrain = (function() {
14 |
15 | const _WHITE = new THREE.Color(0x808080);
16 |
17 | const _DEEP_OCEAN = new THREE.Color(0x20020FF);
18 | const _SHALLOW_OCEAN = new THREE.Color(0x8080FF);
19 | const _BEACH = new THREE.Color(0xd9d592);
20 | const _SNOW = new THREE.Color(0xFFFFFF);
21 | const _ApplyWeightsOREST_TROPICAL = new THREE.Color(0x4f9f0f);
22 | const _ApplyWeightsOREST_TEMPERATE = new THREE.Color(0x2b960e);
23 | const _ApplyWeightsOREST_BOREAL = new THREE.Color(0x29c100);
24 |
25 | const _GREEN = new THREE.Color(0x80FF80);
26 | const _RED = new THREE.Color(0xFF8080);
27 | const _BLACK = new THREE.Color(0x000000);
28 |
29 | const _MIN_CELL_SIZE = 500;
30 | const _MIN_CELL_RESOLUTION = 64;
31 | const _PLANET_RADIUS = 4000;
32 |
33 |
34 | class HeightGenerator {
35 | constructor(generator, position, minRadius, maxRadius) {
36 | this._position = position.clone();
37 | this._radius = [minRadius, maxRadius];
38 | this._generator = generator;
39 | }
40 |
41 | Get(x, y, z) {
42 | return [this._generator.Get(x, y, z), 1];
43 | }
44 | }
45 |
46 |
47 | class FixedHeightGenerator {
48 | constructor() {}
49 |
50 | Get() {
51 | return [50, 1];
52 | }
53 | }
54 |
55 |
56 | class TextureSplatter {
57 | constructor(params) {
58 | const _colourLerp = (t, p0, p1) => {
59 | const c = p0.clone();
60 |
61 | return c.lerp(p1, t);
62 | };
63 | this._colourSpline = [
64 | new spline.LinearSpline(_colourLerp),
65 | new spline.LinearSpline(_colourLerp)
66 | ];
67 |
68 | // Arid
69 | this._colourSpline[0].AddPoint(0.0, new THREE.Color(0xb7a67d));
70 | this._colourSpline[0].AddPoint(0.5, new THREE.Color(0xf1e1bc));
71 | this._colourSpline[0].AddPoint(1.0, _SNOW);
72 |
73 | // Humid
74 | this._colourSpline[1].AddPoint(0.0, _ApplyWeightsOREST_BOREAL);
75 | this._colourSpline[1].AddPoint(0.5, new THREE.Color(0xcee59c));
76 | this._colourSpline[1].AddPoint(1.0, _SNOW);
77 |
78 | this._oceanSpline = new spline.LinearSpline(_colourLerp);
79 | this._oceanSpline.AddPoint(0, _DEEP_OCEAN);
80 | this._oceanSpline.AddPoint(0.03, _SHALLOW_OCEAN);
81 | this._oceanSpline.AddPoint(0.05, _SHALLOW_OCEAN);
82 |
83 | this._params = params;
84 | }
85 |
86 | _BaseColour(x, y, z) {
87 | const m = this._params.biomeGenerator.Get(x, y, z);
88 | const h = math.sat(z / 100.0);
89 |
90 | const c1 = this._colourSpline[0].Get(h);
91 | const c2 = this._colourSpline[1].Get(h);
92 |
93 | let c = c1.lerp(c2, m);
94 |
95 | if (h < 0.1) {
96 | c = c.lerp(new THREE.Color(0x54380e), 1.0 - math.sat(h / 0.05));
97 | }
98 | return c;
99 | }
100 |
101 | _Colour(x, y, z) {
102 | const c = this._BaseColour(x, y, z);
103 | const r = this._params.colourNoise.Get(x, y, z) * 2.0 - 1.0;
104 |
105 | c.offsetHSL(0.0, 0.0, r * 0.01);
106 | return c;
107 | }
108 |
109 | _GetTextureWeights(p, n, up) {
110 | const m = this._params.biomeGenerator.Get(p.x, p.y, p.z);
111 | const h = p.z / 100.0;
112 |
113 | const types = {
114 | dirt: {index: 0, strength: 0.0},
115 | grass: {index: 1, strength: 0.0},
116 | gravel: {index: 2, strength: 0.0},
117 | rock: {index: 3, strength: 0.0},
118 | snow: {index: 4, strength: 0.0},
119 | snowrock: {index: 5, strength: 0.0},
120 | cobble: {index: 6, strength: 0.0},
121 | sandyrock: {index: 7, strength: 0.0},
122 | };
123 |
124 | function _ApplyWeights(dst, v, m) {
125 | for (let k in types) {
126 | types[k].strength *= m;
127 | }
128 | types[dst].strength = v;
129 | };
130 |
131 | types.grass.strength = 1.0;
132 | _ApplyWeights('gravel', 1.0 - m, m);
133 |
134 | if (h < 0.2) {
135 | const s = 1.0 - math.sat((h - 0.1) / 0.05);
136 | _ApplyWeights('cobble', s, 1.0 - s);
137 |
138 | if (h < 0.1) {
139 | const s = 1.0 - math.sat((h - 0.05) / 0.05);
140 | _ApplyWeights('sandyrock', s, 1.0 - s);
141 | }
142 | } else {
143 | if (h > 0.125) {
144 | const s = (math.sat((h - 0.125) / 1.25));
145 | _ApplyWeights('rock', s, 1.0 - s);
146 | }
147 |
148 | if (h > 1.5) {
149 | const s = math.sat((h - 0.75) / 2.0);
150 | _ApplyWeights('snow', s, 1.0 - s);
151 | }
152 | }
153 |
154 | // In case nothing gets set.
155 | types.dirt.strength = 0.01;
156 |
157 | let total = 0.0;
158 | for (let k in types) {
159 | total += types[k].strength;
160 | }
161 | if (total < 0.01) {
162 | const a = 0;
163 | }
164 | const normalization = 1.0 / total;
165 |
166 | for (let k in types) {
167 | types[k].strength / normalization;
168 | }
169 |
170 | return types;
171 | }
172 |
173 | GetColour(position) {
174 | return this._Colour(position.x, position.y, position.z);
175 | }
176 |
177 | GetSplat(position, normal, up) {
178 | return this._GetTextureWeights(position, normal, up);
179 | }
180 | }
181 |
182 |
183 | class FixedColourGenerator {
184 | constructor(params) {
185 | this._params = params;
186 | }
187 |
188 | Get() {
189 | return this._params.colour;
190 | }
191 | }
192 |
193 |
194 |
195 | class TerrainChunkRebuilder {
196 | constructor(params) {
197 | this._pool = {};
198 | this._params = params;
199 | this._Reset();
200 | }
201 |
202 | AllocateChunk(params) {
203 | const w = params.width;
204 |
205 | if (!(w in this._pool)) {
206 | this._pool[w] = [];
207 | }
208 |
209 | let c = null;
210 | if (this._pool[w].length > 0) {
211 | c = this._pool[w].pop();
212 | c._params = params;
213 | } else {
214 | c = new terrain_chunk.TerrainChunk(params);
215 | }
216 |
217 | c.Hide();
218 |
219 | this._queued.push(c);
220 |
221 | return c;
222 | }
223 |
224 | _RecycleChunks(chunks) {
225 | for (let c of chunks) {
226 | if (!(c.chunk._params.width in this._pool)) {
227 | this._pool[c.chunk._params.width] = [];
228 | }
229 |
230 | c.chunk.Destroy();
231 | }
232 | }
233 |
234 | _Reset() {
235 | this._active = null;
236 | this._queued = [];
237 | this._old = [];
238 | this._new = [];
239 | }
240 |
241 | get Busy() {
242 | return this._active || this._queued.length > 0;
243 | }
244 |
245 | Rebuild(chunks) {
246 | if (this.Busy) {
247 | return;
248 | }
249 | for (let k in chunks) {
250 | this._queued.push(chunks[k].chunk);
251 | }
252 | }
253 |
254 | Update() {
255 | if (this._active) {
256 | const r = this._active.next();
257 | if (r.done) {
258 | this._active = null;
259 | }
260 | } else {
261 | const b = this._queued.pop();
262 | if (b) {
263 | this._active = b._Rebuild();
264 | this._new.push(b);
265 | }
266 | }
267 |
268 | if (this._active) {
269 | return;
270 | }
271 |
272 | if (!this._queued.length) {
273 | this._RecycleChunks(this._old);
274 | for (let b of this._new) {
275 | b.Show();
276 | }
277 | this._Reset();
278 | }
279 | }
280 | }
281 |
282 | class TerrainChunkManager {
283 | constructor(params) {
284 | this._Init(params);
285 | }
286 |
287 | _Init(params) {
288 | this._params = params;
289 |
290 | const loader = new THREE.TextureLoader();
291 |
292 | const noiseTexture = loader.load('./resources/simplex-noise.png');
293 | noiseTexture.wrapS = THREE.RepeatWrapping;
294 | noiseTexture.wrapT = THREE.RepeatWrapping;
295 |
296 | const diffuse = new textures.TextureAtlas(params);
297 | diffuse.Load('diffuse', [
298 | './resources/dirt_01_diffuse-1024.png',
299 | './resources/grass1-albedo3-1024.png',
300 | './resources/sandyground-albedo-1024.png',
301 | './resources/worn-bumpy-rock-albedo-1024.png',
302 | './resources/rock-snow-ice-albedo-1024.png',
303 | './resources/snow-packed-albedo-1024.png',
304 | './resources/rough-wet-cobble-albedo-1024.png',
305 | './resources/sandy-rocks1-albedo-1024.png',
306 | ]);
307 | diffuse.onLoad = () => {
308 | this._material.uniforms.diffuseMap.value = diffuse.Info['diffuse'].atlas;
309 | };
310 |
311 | const normal = new textures.TextureAtlas(params);
312 | normal.Load('normal', [
313 | './resources/dirt_01_normal-1024.jpg',
314 | './resources/grass1-normal-1024.jpg',
315 | './resources/sandyground-normal-1024.jpg',
316 | './resources/worn-bumpy-rock-normal-1024.jpg',
317 | './resources/rock-snow-ice-normal-1024.jpg',
318 | './resources/snow-packed-normal-1024.jpg',
319 | './resources/rough-wet-cobble-normal-1024.jpg',
320 | './resources/sandy-rocks1-normal-1024.jpg',
321 | ]);
322 | normal.onLoad = () => {
323 | this._material.uniforms.normalMap.value = normal.Info['normal'].atlas;
324 | };
325 |
326 | this._material = new THREE.MeshStandardMaterial({
327 | wireframe: false,
328 | wireframeLinewidth: 1,
329 | color: 0xFFFFFF,
330 | side: THREE.FrontSide,
331 | vertexColors: THREE.VertexColors,
332 | // normalMap: texture,
333 | });
334 |
335 | this._material = new THREE.RawShaderMaterial({
336 | uniforms: {
337 | diffuseMap: {
338 | },
339 | normalMap: {
340 | },
341 | noiseMap: {
342 | value: noiseTexture
343 | },
344 | },
345 | vertexShader: terrain_shader.VS,
346 | fragmentShader: terrain_shader.PS,
347 | side: THREE.FrontSide
348 | });
349 |
350 | this._builder = new TerrainChunkRebuilder();
351 |
352 | this._InitNoise(params);
353 | this._InitBiomes(params);
354 | this._InitTerrain(params);
355 | }
356 |
357 | _InitNoise(params) {
358 | params.guiParams.noise = {
359 | octaves: 10,
360 | persistence: 0.5,
361 | lacunarity: 1.6,
362 | exponentiation: 7.5,
363 | height: 900.0,
364 | scale: 1800.0,
365 | seed: 1
366 | };
367 |
368 | const onNoiseChanged = () => {
369 | this._builder.Rebuild(this._chunks);
370 | };
371 |
372 | const noiseRollup = params.gui.addFolder('Terrain.Noise');
373 | noiseRollup.add(params.guiParams.noise, "scale", 32.0, 4096.0).onChange(
374 | onNoiseChanged);
375 | noiseRollup.add(params.guiParams.noise, "octaves", 1, 20, 1).onChange(
376 | onNoiseChanged);
377 | noiseRollup.add(params.guiParams.noise, "persistence", 0.25, 1.0).onChange(
378 | onNoiseChanged);
379 | noiseRollup.add(params.guiParams.noise, "lacunarity", 0.01, 4.0).onChange(
380 | onNoiseChanged);
381 | noiseRollup.add(params.guiParams.noise, "exponentiation", 0.1, 10.0).onChange(
382 | onNoiseChanged);
383 | noiseRollup.add(params.guiParams.noise, "height", 0, 20000).onChange(
384 | onNoiseChanged);
385 |
386 | this._noise = new noise.Noise(params.guiParams.noise);
387 |
388 | params.guiParams.heightmap = {
389 | height: 16,
390 | };
391 |
392 | const heightmapRollup = params.gui.addFolder('Terrain.Heightmap');
393 | heightmapRollup.add(params.guiParams.heightmap, "height", 0, 128).onChange(
394 | onNoiseChanged);
395 | }
396 |
397 | _InitBiomes(params) {
398 | params.guiParams.biomes = {
399 | octaves: 2,
400 | persistence: 0.5,
401 | lacunarity: 2.0,
402 | scale: 2048.0,
403 | noiseType: 'simplex',
404 | seed: 2,
405 | exponentiation: 1,
406 | height: 1.0
407 | };
408 |
409 | const onNoiseChanged = () => {
410 | this._builder.Rebuild(this._chunks);
411 | };
412 |
413 | const noiseRollup = params.gui.addFolder('Terrain.Biomes');
414 | noiseRollup.add(params.guiParams.biomes, "scale", 64.0, 4096.0).onChange(
415 | onNoiseChanged);
416 | noiseRollup.add(params.guiParams.biomes, "octaves", 1, 20, 1).onChange(
417 | onNoiseChanged);
418 | noiseRollup.add(params.guiParams.biomes, "persistence", 0.01, 1.0).onChange(
419 | onNoiseChanged);
420 | noiseRollup.add(params.guiParams.biomes, "lacunarity", 0.01, 4.0).onChange(
421 | onNoiseChanged);
422 | noiseRollup.add(params.guiParams.biomes, "exponentiation", 0.1, 10.0).onChange(
423 | onNoiseChanged);
424 |
425 | this._biomes = new noise.Noise(params.guiParams.biomes);
426 |
427 | const colourParams = {
428 | octaves: 1,
429 | persistence: 0.5,
430 | lacunarity: 2.0,
431 | exponentiation: 1.0,
432 | scale: 256.0,
433 | noiseType: 'simplex',
434 | seed: 2,
435 | height: 1.0,
436 | };
437 | this._colourNoise = new noise.Noise(colourParams);
438 | }
439 |
440 | _InitTerrain(params) {
441 | params.guiParams.terrain= {
442 | wireframe: false,
443 | };
444 |
445 | this._groups = [...new Array(6)].map(_ => new THREE.Group());
446 | params.scene.add(...this._groups);
447 |
448 | const terrainRollup = params.gui.addFolder('Terrain');
449 | terrainRollup.add(params.guiParams.terrain, "wireframe").onChange(() => {
450 | for (let k in this._chunks) {
451 | this._chunks[k].chunk._plane.material.wireframe = params.guiParams.terrain.wireframe;
452 | }
453 | });
454 |
455 | this._chunks = {};
456 | this._params = params;
457 | }
458 |
459 | _CellIndex(p) {
460 | const xp = p.x + _MIN_CELL_SIZE * 0.5;
461 | const yp = p.z + _MIN_CELL_SIZE * 0.5;
462 | const x = Math.floor(xp / _MIN_CELL_SIZE);
463 | const z = Math.floor(yp / _MIN_CELL_SIZE);
464 | return [x, z];
465 | }
466 |
467 | _CreateTerrainChunk(group, offset, width, resolution) {
468 | const params = {
469 | group: group,
470 | material: this._material,
471 | width: width,
472 | offset: offset,
473 | radius: _PLANET_RADIUS,
474 | resolution: resolution,
475 | biomeGenerator: this._biomes,
476 | colourGenerator: new TextureSplatter({biomeGenerator: this._biomes, colourNoise: this._colourNoise}),
477 | heightGenerators: [new HeightGenerator(this._noise, offset, 100000, 100000 + 1)],
478 | };
479 |
480 | return this._builder.AllocateChunk(params);
481 | }
482 |
483 | Update(_) {
484 | this._builder.Update();
485 | if (!this._builder.Busy) {
486 | this._UpdateVisibleChunks_Quadtree();
487 | }
488 | }
489 |
490 | _UpdateVisibleChunks_Quadtree() {
491 | function _Key(c) {
492 | return c.position[0] + '/' + c.position[1] + ' [' + c.size + ']' + ' [' + c.index + ']';
493 | }
494 |
495 | const q = new quadtree.CubeQuadTree({
496 | radius: _PLANET_RADIUS,
497 | min_node_size: _MIN_CELL_SIZE,
498 | });
499 | q.Insert(this._params.camera.position);
500 |
501 | const sides = q.GetChildren();
502 |
503 | let newTerrainChunks = {};
504 | const center = new THREE.Vector3();
505 | const dimensions = new THREE.Vector3();
506 | for (let i = 0; i < sides.length; i++) {
507 | this._groups[i].matrix = sides[i].transform;
508 | this._groups[i].matrixAutoUpdate = false;
509 | for (let c of sides[i].children) {
510 | c.bounds.getCenter(center);
511 | c.bounds.getSize(dimensions);
512 |
513 | const child = {
514 | index: i,
515 | group: this._groups[i],
516 | position: [center.x, center.y, center.z],
517 | bounds: c.bounds,
518 | size: dimensions.x,
519 | };
520 |
521 | const k = _Key(child);
522 | newTerrainChunks[k] = child;
523 | }
524 | }
525 |
526 | const intersection = utils.DictIntersection(this._chunks, newTerrainChunks);
527 | const difference = utils.DictDifference(newTerrainChunks, this._chunks);
528 | const recycle = Object.values(utils.DictDifference(this._chunks, newTerrainChunks));
529 |
530 | this._builder._old.push(...recycle);
531 |
532 | newTerrainChunks = intersection;
533 |
534 | for (let k in difference) {
535 | const [xp, yp, zp] = difference[k].position;
536 |
537 | const offset = new THREE.Vector3(xp, yp, zp);
538 | newTerrainChunks[k] = {
539 | position: [xp, zp],
540 | chunk: this._CreateTerrainChunk(
541 | difference[k].group, offset, difference[k].size, _MIN_CELL_RESOLUTION),
542 | };
543 | }
544 |
545 | this._chunks = newTerrainChunks;
546 | }
547 | }
548 |
549 | return {
550 | TerrainChunkManager: TerrainChunkManager
551 | }
552 | })();
553 |
--------------------------------------------------------------------------------
/src/textures.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 |
4 | export const textures = (function() {
5 |
6 | // Taken from https://github.com/mrdoob/three.js/issues/758
7 | function _GetImageData( image ) {
8 | var canvas = document.createElement('canvas');
9 | canvas.width = image.width;
10 | canvas.height = image.height;
11 |
12 | var context = canvas.getContext('2d');
13 | context.drawImage( image, 0, 0 );
14 |
15 | return context.getImageData( 0, 0, image.width, image.height );
16 | }
17 |
18 | return {
19 | TextureAtlas: class {
20 | constructor(params) {
21 | this._game = params.game;
22 | this._Create();
23 | this.onLoad = () => {};
24 | }
25 |
26 | Load(atlas, names) {
27 | this._LoadAtlas(atlas, names);
28 | }
29 |
30 | _Create() {
31 | this._manager = new THREE.LoadingManager();
32 | this._loader = new THREE.TextureLoader(this._manager);
33 | this._textures = {};
34 |
35 | this._manager.onLoad = () => {
36 | this._OnLoad();
37 | };
38 | }
39 |
40 | get Info() {
41 | return this._textures;
42 | }
43 |
44 | _OnLoad() {
45 | for (let k in this._textures) {
46 | const atlas = this._textures[k];
47 | const data = new Uint8Array(atlas.textures.length * 4 * 1024 * 1024);
48 |
49 | for (let t = 0; t < atlas.textures.length; t++) {
50 | const curTexture = atlas.textures[t];
51 | const curData = _GetImageData(curTexture.image);
52 | const offset = t * (4 * 1024 * 1024);
53 |
54 | data.set(curData.data, offset);
55 | }
56 |
57 | const diffuse = new THREE.DataTexture2DArray(data, 1024, 1024, atlas.textures.length);
58 | diffuse.format = THREE.RGBAFormat;
59 | diffuse.type = THREE.UnsignedByteType;
60 | diffuse.minFilter = THREE.LinearMipMapLinearFilter;
61 | diffuse.magFilter = THREE.NearestFilter;
62 | diffuse.wrapS = THREE.RepeatWrapping;
63 | diffuse.wrapT = THREE.RepeatWrapping;
64 | diffuse.generateMipmaps = true;
65 |
66 | const caps = this._game._graphics._threejs.capabilities;
67 | const aniso = caps.getMaxAnisotropy();
68 |
69 | diffuse.anisotropy = 4;
70 |
71 | atlas.atlas = diffuse;
72 | }
73 |
74 | this.onLoad();
75 | }
76 |
77 | _LoadAtlas(atlas, names) {
78 | this._textures[atlas] = {
79 | textures: names.map(n => this._loader.load(n))
80 | };
81 | }
82 | }
83 | };
84 | })();
85 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export const utils = (function() {
2 | return {
3 | DictIntersection: function(dictA, dictB) {
4 | const intersection = {};
5 | for (let k in dictB) {
6 | if (k in dictA) {
7 | intersection[k] = dictA[k];
8 | }
9 | }
10 | return intersection
11 | },
12 |
13 | DictDifference: function(dictA, dictB) {
14 | const diff = {...dictA};
15 | for (let k in dictB) {
16 | delete diff[k];
17 | }
18 | return diff;
19 | }
20 | };
21 | })();
22 |
--------------------------------------------------------------------------------