├── LICENSE
├── base.css
├── index.html
├── resources
├── minecraft
│ └── textures
│ │ └── blocks
│ │ ├── dirt.png
│ │ ├── grass_combined.png
│ │ ├── leaves_spruce_opaque.png
│ │ ├── log_spruce_combined.png
│ │ ├── sand.png
│ │ ├── snow.png
│ │ ├── stone.png
│ │ └── water_single.png
├── negx.jpg
├── negy.jpg
├── negz.jpg
├── posx.jpg
├── posy.jpg
└── posz.jpg
└── src
├── clouds.js
├── controls.js
├── game.js
├── graphics.js
├── main.js
├── math.js
├── textures.js
├── utils.js
├── voxels.js
├── voxels_shader.js
└── voxels_tool.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 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | flex-direction: column;
27 | }
28 |
29 | .visible {
30 | display: block;
31 | }
32 |
33 | #target {
34 | width: 100% !important;
35 | height: 100% !important;
36 | position: absolute;
37 | }
38 |
39 | body {
40 | background: #000000;
41 | margin: 0;
42 | padding: 0;
43 | overscroll-behavior: none;
44 | }
45 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SimonDevCraft
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/dirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/dirt.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/grass_combined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/grass_combined.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/leaves_spruce_opaque.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/leaves_spruce_opaque.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/log_spruce_combined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/log_spruce_combined.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/sand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/sand.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/snow.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/stone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/stone.png
--------------------------------------------------------------------------------
/resources/minecraft/textures/blocks/water_single.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/minecraft/textures/blocks/water_single.png
--------------------------------------------------------------------------------
/resources/negx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/negx.jpg
--------------------------------------------------------------------------------
/resources/negy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/negy.jpg
--------------------------------------------------------------------------------
/resources/negz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/negz.jpg
--------------------------------------------------------------------------------
/resources/posx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/posx.jpg
--------------------------------------------------------------------------------
/resources/posy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/posy.jpg
--------------------------------------------------------------------------------
/resources/posz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simondevyoutube/MinecraftClone/92f5d411b8cc8a700a0b6c0341192812c606e3b6/resources/posz.jpg
--------------------------------------------------------------------------------
/src/clouds.js:
--------------------------------------------------------------------------------
1 | import {math} from './math.js';
2 | import {voxels} from './voxels.js';
3 |
4 |
5 | export const clouds = (function() {
6 |
7 | class CloudBlock {
8 | constructor(game) {
9 | this._game = game;
10 | this._mgr = new voxels.InstancedBlocksManager(this._game);
11 | this._CreateClouds();
12 | }
13 |
14 | _CreateClouds() {
15 | this._cells = {};
16 |
17 | for (let i = 0; i < 25; i++) {
18 | const x = Math.floor(math.rand_range(-1000, 1000));
19 | const z = Math.floor(math.rand_range(-1000, 1000));
20 |
21 | const num = math.rand_int(2, 5);
22 | for (let j = 0; j < num; j++) {
23 | const w = 128;
24 | const h = 128;
25 | const xi = Math.floor(math.rand_range(-w * 0.75, w * 0.75));
26 | const zi = Math.floor(math.rand_range(-h * 0.75, h * 0.75));
27 |
28 | const xPos = x + xi;
29 | const zPos = z + zi;
30 |
31 | const k = xPos + '.' + zPos;
32 | this._cells[k] = {
33 | position: [xPos, 200, zPos],
34 | type: 'cloud',
35 | visible: true
36 | }
37 | }
38 | }
39 |
40 | this._mgr.RebuildFromCellBlock(this._cells);
41 | }
42 | }
43 |
44 | class CloudManager {
45 | constructor(game) {
46 | this._game = game;
47 | this._Init();
48 | }
49 |
50 | _Init() {
51 | this._clouds = new CloudBlock(this._game);
52 | }
53 |
54 | Update(_) {
55 | const cameraPosition = this._game._graphics._camera.position;
56 |
57 | this._clouds._mgr._meshes['cloud'].position.x = cameraPosition.x;
58 | this._clouds._mgr._meshes['cloud'].position.z = cameraPosition.z;
59 | }
60 | }
61 |
62 | return {
63 | CloudManager: CloudManager
64 | };
65 | })();
66 |
--------------------------------------------------------------------------------
/src/controls.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import {PointerLockControls} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/controls/PointerLockControls.js';
3 |
4 |
5 | export const controls = (function() {
6 | return {
7 | // FPSControls was adapted heavily from a threejs example. Movement control
8 | // and collision detection was completely rewritten, but credit to original
9 | // class for the setup code.
10 | FPSControls: class {
11 | constructor(params) {
12 | this._cells = params.cells;
13 | this._Init(params);
14 | }
15 |
16 | _Init(params) {
17 | this._radius = 2;
18 | this._enabled = false;
19 | this._move = {
20 | forward: false,
21 | backward: false,
22 | left: false,
23 | right: false,
24 | };
25 | this._standing = true;
26 | this._velocity = new THREE.Vector3(0, 0, 0);
27 | this._decceleration = new THREE.Vector3(-10, -9.8, -10);
28 | this._acceleration = new THREE.Vector3(30, 7, 80);
29 |
30 | this._SetupPointerLock();
31 |
32 | this._controls = new PointerLockControls(
33 | params.camera, document.body);
34 | this._controls.getObject().position.set(38, 50, 354);
35 | params.scene.add(this._controls.getObject());
36 |
37 | document.addEventListener('keydown', (e) => this._onKeyDown(e), false);
38 | document.addEventListener('keyup', (e) => this._onKeyUp(e), false);
39 | }
40 |
41 | _onKeyDown(event) {
42 | switch (event.keyCode) {
43 | case 38: // up
44 | case 87: // w
45 | this._move.forward = true;
46 | break;
47 | case 37: // left
48 | case 65: // a
49 | this._move.left = true; break;
50 | case 40: // down
51 | case 83: // s
52 | this._move.backward = true;
53 | break;
54 | case 39: // right
55 | case 68: // d
56 | this._move.right = true;
57 | break;
58 | case 32: // space
59 | if (this._standing) this._velocity.y += this._acceleration.y;
60 | this._standing = false;
61 | break;
62 | }
63 | }
64 |
65 | _onKeyUp(event) {
66 | switch(event.keyCode) {
67 | case 38: // up
68 | case 87: // w
69 | this._move.forward = false;
70 | break;
71 | case 37: // left
72 | case 65: // a
73 | this._move.left = false;
74 | break;
75 | case 40: // down
76 | case 83: // s
77 | this._move.backward = false;
78 | break;
79 | case 39: // right
80 | case 68: // d
81 | this._move.right = false;
82 | break;
83 | case 33: // PG_UP
84 | this._cells.ChangeActiveTool(1);
85 | break;
86 | case 34: // PG_DOWN
87 | this._cells.ChangeActiveTool(-1);
88 | break;
89 | case 13: // enter
90 | this._cells.PerformAction()
91 | break;
92 | }
93 | }
94 |
95 | _SetupPointerLock() {
96 | const hasPointerLock = (
97 | 'pointerLockElement' in document ||
98 | 'mozPointerLockElement' in document ||
99 | 'webkitPointerLockElement' in document);
100 | if (hasPointerLock) {
101 | const lockChange = (event) => {
102 | if (document.pointerLockElement === document.body ||
103 | document.mozPointerLockElement === document.body ||
104 | document.webkitPointerLockElement === document.body ) {
105 | this._enabled = true;
106 | this._controls.enabled = true;
107 | } else {
108 | this._controls.enabled = false;
109 | }
110 | };
111 | const lockError = (event) => {
112 | console.log(event);
113 | };
114 |
115 | document.addEventListener('pointerlockchange', lockChange, false);
116 | document.addEventListener('webkitpointerlockchange', lockChange, false);
117 | document.addEventListener('mozpointerlockchange', lockChange, false);
118 | document.addEventListener('pointerlockerror', lockError, false);
119 | document.addEventListener('mozpointerlockerror', lockError, false);
120 | document.addEventListener('webkitpointerlockerror', lockError, false);
121 |
122 | document.getElementById('target').addEventListener('click', (event) => {
123 | document.body.requestPointerLock = (
124 | document.body.requestPointerLock ||
125 | document.body.mozRequestPointerLock ||
126 | document.body.webkitRequestPointerLock);
127 |
128 | if (/Firefox/i.test(navigator.userAgent)) {
129 | const fullScreenChange = (event) => {
130 | if (document.fullscreenElement === document.body ||
131 | document.mozFullscreenElement === document.body ||
132 | document.mozFullScreenElement === document.body) {
133 | document.removeEventListener('fullscreenchange', fullScreenChange);
134 | document.removeEventListener('mozfullscreenchange', fullScreenChange);
135 | document.body.requestPointerLock();
136 | }
137 | };
138 | document.addEventListener(
139 | 'fullscreenchange', fullScreenChange, false);
140 | document.addEventListener(
141 | 'mozfullscreenchange', fullScreenChange, false);
142 | document.body.requestFullscreen = (
143 | document.body.requestFullscreen ||
144 | document.body.mozRequestFullscreen ||
145 | document.body.mozRequestFullScreen ||
146 | document.body.webkitRequestFullscreen);
147 | document.body.requestFullscreen();
148 | } else {
149 | document.body.requestPointerLock();
150 | }
151 | }, false);
152 | }
153 | }
154 |
155 | _FindIntersections(boxes, position) {
156 | const sphere = new THREE.Sphere(position, this._radius);
157 |
158 | const intersections = boxes.filter(b => {
159 | return sphere.intersectsBox(b);
160 | });
161 |
162 | return intersections;
163 | }
164 |
165 | Update(timeInSeconds) {
166 | if (!this._enabled) {
167 | return;
168 | }
169 |
170 | const demo = false;
171 | if (demo) {
172 | this._controls.getObject().position.x += timeInSeconds * 10;
173 | return;
174 | }
175 |
176 | const frameDecceleration = new THREE.Vector3(
177 | this._velocity.x * this._decceleration.x,
178 | this._decceleration.y,
179 | this._velocity.z * this._decceleration.z
180 | );
181 | frameDecceleration.multiplyScalar(timeInSeconds);
182 |
183 | this._velocity.add(frameDecceleration);
184 |
185 | if (this._move.forward) {
186 | this._velocity.z -= this._acceleration.z * timeInSeconds;
187 | }
188 | if (this._move.backward) {
189 | this._velocity.z += this._acceleration.z * timeInSeconds;
190 | }
191 | if (this._move.left) {
192 | this._velocity.x -= this._acceleration.x * timeInSeconds;
193 | }
194 | if (this._move.right) {
195 | this._velocity.x += this._acceleration.x * timeInSeconds;
196 | }
197 |
198 | const controlObject = this._controls.getObject();
199 | const cells = this._cells.LookupCells(
200 | this._controls.getObject().position, 3);
201 | const boxes = [];
202 | for (let c of cells) {
203 | boxes.push(...c.AsBox3Array(this._controls.getObject().position, 3));
204 | }
205 |
206 | const oldPosition = new THREE.Vector3();
207 | oldPosition.copy(controlObject.position);
208 |
209 | const forward = new THREE.Vector3(0, 0, 1);
210 | forward.applyQuaternion(controlObject.quaternion);
211 | forward.y = 0;
212 | forward.normalize();
213 |
214 | const sideways = new THREE.Vector3(1, 0, 0);
215 | sideways.applyQuaternion(controlObject.quaternion);
216 | sideways.normalize();
217 |
218 | sideways.multiplyScalar(this._velocity.x * timeInSeconds);
219 | forward.multiplyScalar(this._velocity.z * timeInSeconds);
220 |
221 | controlObject.position.add(forward);
222 | controlObject.position.add(sideways);
223 |
224 | let intersections = this._FindIntersections(
225 | boxes, controlObject.position);
226 | if (intersections.length > 0) {
227 | controlObject.position.copy(oldPosition);
228 | }
229 |
230 | oldPosition.copy(controlObject.position);
231 | controlObject.position.y += this._velocity.y * timeInSeconds;
232 | intersections = this._FindIntersections(boxes, controlObject.position);
233 | if (intersections.length > 0) {
234 | controlObject.position.copy(oldPosition);
235 |
236 | this._velocity.y = Math.max(0, this._velocity.y);
237 | this._standing = true;
238 | }
239 |
240 | if (controlObject.position.y < -100) {
241 | this._velocity.y = 0;
242 | controlObject.position.y = 150;
243 | this._standing = true;
244 | }
245 | }
246 | }
247 | };
248 | })();
249 |
--------------------------------------------------------------------------------
/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 | export const game = (function() {
6 | return {
7 | Game: class {
8 | constructor() {
9 | this._Initialize();
10 | }
11 |
12 | _Initialize() {
13 | this._graphics = new graphics.Graphics(this);
14 | if (!this._graphics.Initialize()) {
15 | this._DisplayError('WebGL2 is not available.');
16 | return;
17 | }
18 |
19 | this._previousRAF = null;
20 |
21 | this._OnInitialize();
22 | this._RAF();
23 | }
24 |
25 | _DisplayError(errorText) {
26 | const error = document.getElementById('error');
27 | error.innerText = errorText;
28 | }
29 |
30 | _RAF() {
31 | requestAnimationFrame((t) => {
32 | if (this._previousRAF === null) {
33 | this._previousRAF = t;
34 | }
35 | this._Render(t - this._previousRAF);
36 | this._previousRAF = t;
37 | });
38 | }
39 |
40 | _Render(timeInMS) {
41 | const timeInSeconds = timeInMS * 0.001;
42 | this._OnStep(timeInSeconds);
43 | this._graphics.Render(timeInSeconds);
44 |
45 | this._RAF();
46 | }
47 | }
48 | };
49 | })();
50 |
--------------------------------------------------------------------------------
/src/graphics.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import Stats from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/libs/stats.module.js';
3 | import {WEBGL} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/WebGL.js';
4 |
5 |
6 | export const graphics = (function() {
7 | return {
8 | Graphics: class {
9 | constructor(game) {
10 | }
11 |
12 | Initialize() {
13 | if (!WEBGL.isWebGL2Available()) {
14 | return false;
15 | }
16 |
17 | this._threejs = new THREE.WebGLRenderer({
18 | antialias: true,
19 | });
20 | this._threejs.setPixelRatio(window.devicePixelRatio);
21 | this._threejs.setSize(window.innerWidth, window.innerHeight);
22 |
23 | const target = document.getElementById('target');
24 | target.appendChild(this._threejs.domElement);
25 |
26 | this._stats = new Stats();
27 | target.appendChild(this._stats.dom);
28 |
29 | window.addEventListener('resize', () => {
30 | this._OnWindowResize();
31 | }, false);
32 |
33 | const fov = 60;
34 | const aspect = 1920 / 1080;
35 | const near = 0.1;
36 | const far = 10000.0;
37 | this._camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
38 |
39 | this._scene = new THREE.Scene();
40 | this._scene.background = new THREE.Color(0xaaaaaa);
41 |
42 | return true;
43 | }
44 |
45 | _OnWindowResize() {
46 | this._camera.aspect = window.innerWidth / window.innerHeight;
47 | this._camera.updateProjectionMatrix();
48 | this._threejs.setSize(window.innerWidth, window.innerHeight);
49 | }
50 |
51 | get Scene() {
52 | return this._scene;
53 | }
54 |
55 | get Camera() {
56 | return this._camera;
57 | }
58 |
59 | Render(timeInSeconds) {
60 | this._threejs.render(this._scene, this._camera);
61 | this._stats.update();
62 | }
63 | }
64 | };
65 | })();
66 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import 'https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.js';
3 | import {clouds} from './clouds.js';
4 | import {controls} from './controls.js';
5 | import {game} from './game.js';
6 | import {graphics} from './graphics.js';
7 | import {math} from './math.js';
8 | import {textures} from './textures.js';
9 | import {voxels} from './voxels.js';
10 |
11 |
12 | let _APP = null;
13 |
14 |
15 | class SimonDevCraft extends game.Game {
16 | constructor() {
17 | super();
18 | }
19 |
20 | _OnInitialize() {
21 | this._entities = {};
22 |
23 | this._LoadBackground();
24 |
25 | this._atlas = new textures.TextureAtlas(this);
26 | this._atlas.onLoad = () => {
27 | this._entities['_voxels'] = new voxels.SparseVoxelCellManager(this);
28 | this._entities['_clouds'] = new clouds.CloudManager(this);
29 | this._entities['_controls'] = new controls.FPSControls(
30 | {
31 | cells: this._entities['_voxels'],
32 | scene: this._graphics.Scene,
33 | camera: this._graphics.Camera
34 | });
35 | };
36 | }
37 |
38 | _LoadBackground() {
39 | const loader = new THREE.CubeTextureLoader();
40 | const texture = loader.load([
41 | './resources/posx.jpg',
42 | './resources/posx.jpg',
43 | './resources/posy.jpg',
44 | './resources/negy.jpg',
45 | './resources/posx.jpg',
46 | './resources/posx.jpg',
47 | ]);
48 | this._graphics.Scene.background = texture;
49 | }
50 |
51 | _OnStep(timeInSeconds) {
52 | timeInSeconds = Math.min(timeInSeconds, 1 / 10.0);
53 |
54 | this._StepEntities(timeInSeconds);
55 | }
56 |
57 | _StepEntities(timeInSeconds) {
58 | for (let k in this._entities) {
59 | this._entities[k].Update(timeInSeconds);
60 | }
61 | }
62 | }
63 |
64 |
65 | function _Main() {
66 | _APP = new SimonDevCraft();
67 | }
68 |
69 | _Main();
70 |
--------------------------------------------------------------------------------
/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 | clamp: function (x, a, b) {
21 | return Math.min(Math.max(x, a), b);
22 | },
23 |
24 | sat: function (x) {
25 | return Math.min(Math.max(x, 0.0), 1.0);
26 | },
27 | };
28 | })();
29 |
--------------------------------------------------------------------------------
/src/textures.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 |
4 | export const textures = (function() {
5 | return {
6 | // Originally I planned to do texture atlasing, then got lazy.
7 | TextureAtlas: class {
8 | constructor(game) {
9 | this._game = game;
10 | this._Create(game);
11 | this.onLoad = () => {};
12 | }
13 |
14 | _Create(game) {
15 | this._manager = new THREE.LoadingManager();
16 | this._loader = new THREE.TextureLoader(this._manager);
17 | this._textures = {};
18 |
19 | this._LoadType(
20 | 'grass',
21 | ['resources/minecraft/textures/blocks/grass_combined.png'],
22 | new THREE.Vector2(1.0, 1.0),
23 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
24 | );
25 |
26 | this._LoadType(
27 | 'desert',
28 | ['resources/minecraft/textures/blocks/grass_combined.png'],
29 | new THREE.Vector2(1.0, 1.0),
30 | [new THREE.Color(0xbfb755), new THREE.Color(0xbfb755)]
31 | );
32 |
33 | this._LoadType(
34 | 'dirt',
35 | ['resources/minecraft/textures/blocks/dirt.png'],
36 | new THREE.Vector2(1.0, 4.0),
37 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
38 | );
39 |
40 | this._LoadType(
41 | 'sand',
42 | ['resources/minecraft/textures/blocks/sand.png'],
43 | new THREE.Vector2(1.0, 4.0),
44 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
45 | );
46 |
47 | this._LoadType(
48 | 'ocean',
49 | ['resources/minecraft/textures/blocks/sand.png'],
50 | new THREE.Vector2(1.0, 4.0),
51 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
52 | );
53 |
54 | this._LoadType(
55 | 'water',
56 | ['resources/minecraft/textures/blocks/water_single.png'],
57 | new THREE.Vector2(1.0, 2.0),
58 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
59 | );
60 |
61 | this._LoadType(
62 | 'stone',
63 | ['resources/minecraft/textures/blocks/stone.png'],
64 | new THREE.Vector2(1.0, 4.0),
65 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
66 | );
67 |
68 | this._LoadType(
69 | 'snow',
70 | ['resources/minecraft/textures/blocks/snow.png'],
71 | new THREE.Vector2(1.0, 4.0),
72 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
73 | );
74 |
75 | this._LoadType(
76 | 'log_spruce',
77 | ['resources/minecraft/textures/blocks/log_spruce_combined.png'],
78 | new THREE.Vector2(1.0, 2.0),
79 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
80 | );
81 |
82 | this._LoadType(
83 | 'leaves_spruce',
84 | ['resources/minecraft/textures/blocks/leaves_spruce_opaque.png'],
85 | new THREE.Vector2(1.0, 4.0),
86 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
87 | );
88 |
89 | // Whatever, don't judge me.
90 | this._LoadType(
91 | 'cloud',
92 | ['resources/minecraft/textures/blocks/snow.png'],
93 | new THREE.Vector2(1.0, 4.0),
94 | [new THREE.Color(1.0, 1.0, 1.0), new THREE.Color(1.0, 1.0, 1.0)]
95 | );
96 |
97 | this._manager.onLoad = () => {
98 | this._OnLoad();
99 | };
100 |
101 | this._game = game;
102 | }
103 |
104 | get Info() {
105 | return this._textures;
106 | }
107 |
108 | _OnLoad() {
109 | this.onLoad();
110 | }
111 |
112 | _LoadType(name, textureNames, offset, colourRange) {
113 | this._textures[name] = {
114 | colourRange: colourRange,
115 | uvOffset: [
116 | offset.x,
117 | offset.y,
118 | ],
119 | textures: textureNames.map(n => this._loader.load(n))
120 | };
121 | if (this._textures[name].textures.length > 1) {
122 | } else {
123 | const caps = this._game._graphics._threejs.capabilities;
124 | const aniso = caps.getMaxAnisotropy();
125 |
126 | this._textures[name].texture = this._textures[name].textures[0];
127 | this._textures[name].texture.minFilter = THREE.LinearMipMapLinearFilter;
128 | this._textures[name].texture.magFilter = THREE.NearestFilter;
129 | this._textures[name].texture.wrapS = THREE.RepeatWrapping;
130 | this._textures[name].texture.wrapT = THREE.RepeatWrapping;
131 | this._textures[name].texture.anisotropy = aniso;
132 | }
133 | }
134 | }
135 | };
136 | })();
137 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/voxels.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 | import 'https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.js';
3 | import {BufferGeometryUtils} from 'https://cdn.jsdelivr.net/npm/three@0.112.1/examples/jsm/utils/BufferGeometryUtils.js';
4 | import {math} from './math.js';
5 | import {utils} from './utils.js';
6 | import {voxels_shader} from './voxels_shader.js';
7 | import {voxels_tool} from './voxels_tool.js';
8 |
9 |
10 | export const voxels = (function() {
11 |
12 | const _VOXEL_HEIGHT = 128;
13 | const _OCEAN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.12);
14 | const _BEACH_LEVEL = _OCEAN_LEVEL + 2;
15 | const _SNOW_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.8);
16 | const _MOUNTAIN_LEVEL = Math.floor(_VOXEL_HEIGHT * 0.5);
17 |
18 | // HACKY TODO: Pass a terrain generation object through instead of these
19 | // loose functions.
20 | const _N1 = new SimplexNoise(2);
21 | const _N2 = new SimplexNoise(3);
22 | const _N3 = new SimplexNoise(4);
23 | function _SimplexNoise(gen, nx, ny){
24 | return gen.noise2D(nx, ny) * 0.5 + 0.5;
25 | }
26 |
27 | function Noise(gen, x, y, sc, octaves, persistence, exponentiation) {
28 | const xs = x / sc;
29 | const ys = y / sc;
30 | let amplitude = 1.0;
31 | let frequency = 1.0;
32 | let normalization = 0;
33 | let total = 0;
34 | for (let o = 0; o < octaves; o++) {
35 | total += _SimplexNoise(gen, xs * frequency, ys * frequency) * amplitude;
36 | normalization += amplitude;
37 | amplitude *= persistence;
38 | frequency *= 2.0;
39 | }
40 | total /= normalization;
41 | return Math.pow(total, exponentiation);
42 | }
43 |
44 | function Biome(e, m) {
45 | if (e < _OCEAN_LEVEL) return 'ocean';
46 | if (e < _BEACH_LEVEL) return 'sand';
47 |
48 | if (e > _SNOW_LEVEL) {
49 | return 'snow';
50 | }
51 |
52 | if (e > _MOUNTAIN_LEVEL) {
53 | if (m < 0.1) {
54 | return 'stone';
55 | } else if (m < 0.25) {
56 | return 'hills';
57 | }
58 | }
59 |
60 | // if (m < 0.1) {
61 | // return 'desert';
62 | // }
63 |
64 | return 'grass';
65 | }
66 |
67 | class InstancedBlocksManager {
68 | constructor(game, cell) {
69 | this._game = game;
70 | this._geometryBuffers = {};
71 | this._meshes = {};
72 | this._materials = {};
73 | this._Create(game);
74 | }
75 |
76 | _Create(game) {
77 | const pxGeometry = new THREE.PlaneBufferGeometry(1, 1);
78 | pxGeometry.rotateY(Math.PI / 2);
79 | pxGeometry.translate(0.5, 0, 0);
80 |
81 | const nxGeometry = new THREE.PlaneBufferGeometry(1, 1);
82 | nxGeometry.rotateY(-Math.PI / 2);
83 | nxGeometry.translate(-0.5, 0, 0);
84 |
85 | const pyGeometry = new THREE.PlaneBufferGeometry(1, 1);
86 | pyGeometry.attributes.uv.array[5] = 3.0 / 4.0;
87 | pyGeometry.attributes.uv.array[7] = 3.0 / 4.0;
88 | pyGeometry.attributes.uv.array[1] = 4.0 / 4.0;
89 | pyGeometry.attributes.uv.array[3] = 4.0 / 4.0;
90 | pyGeometry.rotateX(-Math.PI / 2);
91 | pyGeometry.translate(0, 0.5, 0);
92 |
93 | const nyGeometry = new THREE.PlaneBufferGeometry(1, 1);
94 | nyGeometry.attributes.uv.array[5] = 1.0 / 4.0;
95 | nyGeometry.attributes.uv.array[7] = 1.0 / 4.0;
96 | nyGeometry.attributes.uv.array[1] = 2.0 / 4.0;
97 | nyGeometry.attributes.uv.array[3] = 2.0 / 4.0;
98 | nyGeometry.rotateX(Math.PI / 2);
99 | nyGeometry.translate(0, -0.5, 0);
100 |
101 | const pzGeometry = new THREE.PlaneBufferGeometry(1, 1);
102 | pzGeometry.translate(0, 0, 0.5);
103 |
104 | const nzGeometry = new THREE.PlaneBufferGeometry(1, 1);
105 | nzGeometry.rotateY( Math.PI );
106 | nzGeometry.translate(0, 0, -0.5);
107 |
108 | const flipGeometries = [
109 | pxGeometry, nxGeometry, pzGeometry, nzGeometry
110 | ];
111 |
112 | for (let g of flipGeometries) {
113 | g.attributes.uv.array[5] = 2.0 / 4.0;
114 | g.attributes.uv.array[7] = 2.0 / 4.0;
115 | g.attributes.uv.array[1] = 3.0 / 4.0;
116 | g.attributes.uv.array[3] = 3.0 / 4.0;
117 | }
118 |
119 | this._geometries = [
120 | pxGeometry, nxGeometry,
121 | pyGeometry, nyGeometry,
122 | pzGeometry, nzGeometry
123 | ];
124 |
125 | this._geometries = {
126 | cube: BufferGeometryUtils.mergeBufferGeometries(this._geometries),
127 | plane: pyGeometry,
128 | };
129 | }
130 |
131 | RebuildFromCellBlock(cells) {
132 | const cellsOfType = {};
133 |
134 | for (let k in cells) {
135 | const c = cells[k];
136 | if (!(c.type in cellsOfType)) {
137 | cellsOfType[c.type] = [];
138 | }
139 | if (c.visible) {
140 | cellsOfType[c.type].push(c);
141 | }
142 | }
143 |
144 | for (let k in cellsOfType) {
145 | this._RebuildFromCellType(cellsOfType[k], k);
146 | }
147 |
148 | for (let k in this._geometryBuffers) {
149 | if (!(k in cellsOfType)) {
150 | this._RebuildFromCellType([], k);
151 | }
152 | }
153 | }
154 |
155 | _GetBaseGeometryForCellType(cellType) {
156 | if (cellType == 'water') {
157 | return this._geometries.plane;
158 | }
159 | return this._geometries.cube;
160 | }
161 |
162 | _RebuildFromCellType(cells, cellType) {
163 | const textureInfo = this._game._atlas.Info[cellType];
164 |
165 | if (!(cellType in this._geometryBuffers)) {
166 | this._geometryBuffers[cellType] = new THREE.InstancedBufferGeometry();
167 |
168 | this._materials[cellType] = new THREE.RawShaderMaterial({
169 | uniforms: {
170 | diffuseTexture: {
171 | value: textureInfo.texture
172 | },
173 | skybox: {
174 | value: this._game._graphics._scene.background
175 | },
176 | fogDensity: {
177 | value: 0.005
178 | },
179 | cloudScale: {
180 | value: [1, 1, 1]
181 | }
182 | },
183 | vertexShader: voxels_shader.VS,
184 | fragmentShader: voxels_shader.PS,
185 | side: THREE.FrontSide
186 | });
187 |
188 | // HACKY: Need to have some sort of material manager and pass
189 | // these params.
190 | if (cellType == 'water') {
191 | this._materials[cellType].blending = THREE.NormalBlending;
192 | this._materials[cellType].depthWrite = false;
193 | this._materials[cellType].depthTest = true;
194 | this._materials[cellType].transparent = true;
195 | }
196 |
197 | if (cellType == 'cloud') {
198 | this._materials[cellType].uniforms.fogDensity.value = 0.001;
199 | this._materials[cellType].uniforms.cloudScale.value = [64, 10, 64];
200 | }
201 |
202 | this._meshes[cellType] = new THREE.Mesh(
203 | this._geometryBuffers[cellType], this._materials[cellType]);
204 | this._game._graphics._scene.add(this._meshes[cellType]);
205 | }
206 |
207 | this._geometryBuffers[cellType].maxInstancedCount = cells.length;
208 |
209 | const baseGeometry = this._GetBaseGeometryForCellType(cellType);
210 |
211 | this._geometryBuffers[cellType].setAttribute(
212 | 'position', new THREE.Float32BufferAttribute(
213 | [...baseGeometry.attributes.position.array], 3));
214 | this._geometryBuffers[cellType].setAttribute(
215 | 'uv', new THREE.Float32BufferAttribute(
216 | [...baseGeometry.attributes.uv.array], 2));
217 | this._geometryBuffers[cellType].setAttribute(
218 | 'normal', new THREE.Float32BufferAttribute(
219 | [...baseGeometry.attributes.normal.array], 3));
220 | this._geometryBuffers[cellType].setIndex(
221 | new THREE.BufferAttribute(
222 | new Uint32Array([...baseGeometry.index.array]), 1));
223 |
224 | const offsets = [];
225 | const uvOffsets = [];
226 | const colors = [];
227 |
228 | const box = new THREE.Box3();
229 |
230 | for (let c in cells) {
231 | const curCell = cells[c];
232 |
233 | let randomLuminance = Noise(
234 | _N2, curCell.position[0], curCell.position[2], 16, 8, 0.6, 2) * 0.2 + 0.8;
235 | if (curCell.luminance !== undefined) {
236 | randomLuminance = curCell.luminance;
237 | } else if (cellType == 'cloud') {
238 | randomLuminance = 1;
239 | }
240 |
241 | const colour = textureInfo.colourRange[0].clone();
242 | colour.r *= randomLuminance;
243 | colour.g *= randomLuminance;
244 | colour.b *= randomLuminance;
245 |
246 | colors.push(colour.r, colour.g, colour.b);
247 | offsets.push(...curCell.position);
248 | uvOffsets.push(...textureInfo.uvOffset);
249 | box.expandByPoint(new THREE.Vector3(
250 | curCell.position[0],
251 | curCell.position[1],
252 | curCell.position[2]));
253 | }
254 |
255 | this._geometryBuffers[cellType].setAttribute(
256 | 'color', new THREE.InstancedBufferAttribute(
257 | new Float32Array(colors), 3));
258 | this._geometryBuffers[cellType].setAttribute(
259 | 'offset', new THREE.InstancedBufferAttribute(
260 | new Float32Array(offsets), 3));
261 | this._geometryBuffers[cellType].setAttribute(
262 | 'uvOffset', new THREE.InstancedBufferAttribute(
263 | new Float32Array(uvOffsets), 2));
264 | this._geometryBuffers[cellType].attributes.offset.needsUpdate = true;
265 | this._geometryBuffers[cellType].attributes.uvOffset.uvOffset = true;
266 | this._geometryBuffers[cellType].attributes.color.uvOffset = true;
267 |
268 | this._geometryBuffers[cellType].boundingBox = box;
269 | this._geometryBuffers[cellType].boundingSphere = new THREE.Sphere();
270 | box.getBoundingSphere(this._geometryBuffers[cellType].boundingSphere);
271 | }
272 |
273 | Update() {
274 | }
275 | };
276 |
277 | const _RAND_VALS = {};
278 |
279 | class SparseVoxelCellBlock {
280 | constructor(game, parent, offset, dimensions, id) {
281 | this._game = game;
282 | this._parent = parent;
283 | this._atlas = game._atlas;
284 | this._blockOffset = offset;
285 | this._blockDimensions = dimensions;
286 | this._mgr = new InstancedBlocksManager(this._game, this);
287 | this._id = id;
288 |
289 | this._Init();
290 | }
291 |
292 | get ID() {
293 | return this._id;
294 | }
295 |
296 | _GenerateNoise(x, y) {
297 | const elevation = Math.floor(Noise(_N1, x, y, 1024, 6, 0.4, 5.65) * 128);
298 | const moisture = Noise(_N2, x, y, 512, 6, 0.5, 4);
299 |
300 | return [Biome(elevation, moisture), elevation];
301 | }
302 |
303 | _Init() {
304 | this._cells = {};
305 |
306 | for (let x = 0; x < this._blockDimensions.x; x++) {
307 | for (let z = 0; z < this._blockDimensions.z; z++) {
308 | const xPos = x + this._blockOffset.x;
309 | const zPos = z + this._blockOffset.z;
310 |
311 | const [atlasType, yOffset] = this._GenerateNoise(xPos, zPos);
312 |
313 | this._cells[xPos + '.' + yOffset + '.' + zPos] = {
314 | position: [xPos, yOffset, zPos],
315 | type: atlasType,
316 | visible: true
317 | };
318 |
319 | if (atlasType == 'ocean') {
320 | this._cells[xPos + '.' + _OCEAN_LEVEL + '.' + zPos] = {
321 | position: [xPos, _OCEAN_LEVEL, zPos],
322 | type: 'water',
323 | visible: true
324 | };
325 | } else {
326 | // Possibly have to generate cliffs
327 | let lowestAdjacent = yOffset;
328 | for (let xi = -1; xi <= 1; xi++) {
329 | for (let zi = -1; zi <= 1; zi++) {
330 | const [_, otherOffset] = this._GenerateNoise(xPos + xi, zPos + zi);
331 | lowestAdjacent = Math.min(otherOffset, lowestAdjacent);
332 | }
333 | }
334 |
335 | if (lowestAdjacent < yOffset) {
336 | const heightDifference = yOffset - lowestAdjacent;
337 | for (let yi = lowestAdjacent + 1; yi < yOffset; yi++) {
338 | this._cells[xPos + '.' + yi + '.' + zPos] = {
339 | position: [xPos, yi, zPos],
340 | type: 'dirt',
341 | visible: true
342 | };
343 | }
344 | }
345 | }
346 | }
347 | }
348 |
349 | this._GenerateTrees();
350 | }
351 |
352 | _GenerateTrees() {
353 | // This is terrible, but works fine for demo purposes. Just a straight up
354 | // grid of trees, with random removal/jittering.
355 | for (let x = 0; x < this._blockDimensions.x; x++) {
356 | for (let z = 0; z < this._blockDimensions.z; z++) {
357 | const xPos = this._blockOffset.x + x;
358 | const zPos = this._blockOffset.z + z;
359 | if (xPos % 11 != 0 || zPos % 11 != 0) {
360 | continue;
361 | }
362 |
363 | const roll = Math.random();
364 | if (roll < 0.35) {
365 | const xTreePos = xPos + math.rand_int(-3, 3);
366 | const zTreePos = zPos + math.rand_int(-3, 3);
367 |
368 | const [terrainType, _] = this._GenerateNoise(xTreePos, zTreePos);
369 | if (terrainType != 'grass') {
370 | continue;
371 | }
372 |
373 | this._MakeSpruceTree(xTreePos, zTreePos);
374 | }
375 | }
376 | }
377 | }
378 |
379 | HasVoxelAt(x, y, z) {
380 | const k = this._Key(x, y, z);
381 | if (!(k in this._cells)) {
382 | return false;
383 | }
384 |
385 | return this._cells[k].visible;
386 | }
387 |
388 | InsertVoxel(cellData, overwrite=true) {
389 | const k = this._Key(
390 | cellData.position[0],
391 | cellData.position[1],
392 | cellData.position[2]);
393 | if (!overwrite && k in this._cells) {
394 | return;
395 | }
396 | this._cells[k] = cellData;
397 | this._parent.MarkDirty(this);
398 | }
399 |
400 | RemoveVoxel(key) {
401 | const v = this._cells[key];
402 | this._cells[key].visible = false;
403 |
404 | this._parent.MarkDirty(this);
405 |
406 | // Probably better to just pregenerate these voxels, version 2 maybe.
407 | const [atlasType, groundLevel] = this._GenerateNoise(
408 | v.position[0], v.position[2]);
409 |
410 | if (v.position[1] <= groundLevel) {
411 | for (let xi = -1; xi <= 1; xi++) {
412 | for (let yi = -1; yi <= 1; yi++) {
413 | for (let zi = -1; zi <= 1; zi++) {
414 | const xPos = v.position[0] + xi;
415 | const zPos = v.position[2] + zi;
416 | const yPos = v.position[1] + yi;
417 |
418 | const [adjacentType, groundLevelAdjacent] = this._GenerateNoise(xPos, zPos);
419 | const k = this._Key(xPos, yPos, zPos);
420 |
421 | if (!(k in this._cells) && yPos < groundLevelAdjacent) {
422 | let type = 'dirt';
423 |
424 | if (adjacentType == 'sand') {
425 | type = 'sand';
426 | }
427 |
428 | if (yPos < groundLevelAdjacent - 2) {
429 | type = 'stone';
430 | }
431 |
432 | // This is potentially out of bounds of the cell, so route the
433 | // voxel insertion via parent.
434 | this._parent.InsertVoxel({
435 | position: [xPos, yPos, zPos],
436 | type: type,
437 | visible: true
438 | }, false);
439 | }
440 | }
441 | }
442 | }
443 | }
444 | }
445 |
446 | Build() {
447 | this._mgr.RebuildFromCellBlock(this._cells);
448 | }
449 |
450 | _Key(x, y, z) {
451 | return x + '.' + y + '.' + z;
452 | }
453 |
454 | _MakeSpruceTree(x, z) {
455 | const [_, yOffset] = this._GenerateNoise(x, z);
456 |
457 | // TODO: Technically, inserting into cells can go outside the bounds
458 | // of an individual SparseVoxelCellBlock. These calls should be routed
459 | // to the parent.
460 | const treeHeight = math.rand_int(3, 5);
461 | for (let y = 1; y < treeHeight; y++) {
462 | const yPos = y + yOffset;
463 | const k = this._Key(x, yPos, z);
464 | this._cells[k] = {
465 | position: [x, yPos, z],
466 | type: 'log_spruce',
467 | visible: true
468 | };
469 | }
470 |
471 | for (let h = 0; h < 2; h++) {
472 | for (let xi = -2; xi <= 2; xi++) {
473 | for (let zi = -2; zi <= 2; zi++) {
474 | if (Math.abs(xi) == 2 && Math.abs(zi) == 2) {
475 | continue;
476 | }
477 |
478 | const yPos = yOffset + h + treeHeight;
479 | const xPos = x + xi;
480 | const zPos = z + zi;
481 | const k = xPos + '.' + yPos + '.' + zPos;
482 | this._cells[k] = {
483 | position: [xPos, yPos, zPos],
484 | type: 'leaves_spruce',
485 | visible: true
486 | };
487 | }
488 | }
489 | }
490 |
491 | for (let h = 0; h < 2; h++) {
492 | for (let xi = -1; xi <= 1; xi++) {
493 | for (let zi = -1; zi <= 1; zi++) {
494 | if (Math.abs(xi) == 1 && Math.abs(zi) == 1) {
495 | continue;
496 | }
497 |
498 | const yPos = yOffset + h + treeHeight + 2;
499 | const xPos = x + xi;
500 | const zPos = z + zi;
501 | const k = xPos + '.' + yPos + '.' + zPos;
502 | this._cells[k] = {
503 | position: [xPos, yPos, zPos],
504 | type: 'leaves_spruce',
505 | visible: true
506 | };
507 | }
508 | }
509 | }
510 | }
511 |
512 | AsVoxelArray(pos, radius) {
513 | const x = Math.floor(pos.x);
514 | const y = Math.floor(pos.y);
515 | const z = Math.floor(pos.z);
516 |
517 | const voxels = [];
518 | for (let xi = -radius; xi <= radius; xi++) {
519 | for (let yi = -radius; yi <= radius; yi++) {
520 | for (let zi = -radius; zi <= radius; zi++) {
521 | const xPos = xi + x;
522 | const yPos = yi + y;
523 | const zPos = zi + z;
524 | const k = xPos + '.' + yPos + '.' + zPos;
525 | if (k in this._cells) {
526 | const cell = this._cells[k];
527 | if (!cell.visible) {
528 | continue;
529 | }
530 |
531 | if (cell.blinker !== undefined) {
532 | continue;
533 | }
534 |
535 | const position = new THREE.Vector3(
536 | cell.position[0], cell.position[1], cell.position[2]);
537 | const half = new THREE.Vector3(0.5, 0.5, 0.5);
538 |
539 | const m1 = new THREE.Vector3();
540 | m1.copy(position);
541 | m1.sub(half);
542 |
543 | const m2 = new THREE.Vector3();
544 | m2.copy(position);
545 | m2.add(half);
546 |
547 | const box = new THREE.Box3(m1, m2);
548 | const voxelData = {...cell};
549 | voxelData.aabb = box;
550 | voxelData.key = k;
551 | voxels.push(voxelData);
552 | }
553 | }
554 | }
555 | }
556 |
557 | return voxels;
558 | }
559 |
560 | AsBox3Array(pos, radius) {
561 | const x = Math.floor(pos.x);
562 | const y = Math.floor(pos.y);
563 | const z = Math.floor(pos.z);
564 |
565 | const boxes = [];
566 | for (let xi = -radius; xi <= radius; xi++) {
567 | for (let yi = -radius; yi <= radius; yi++) {
568 | for (let zi = -radius; zi <= radius; zi++) {
569 | const xPos = xi + x;
570 | const yPos = yi + y;
571 | const zPos = zi + z;
572 | const k = xPos + '.' + yPos + '.' + zPos;
573 | if (k in this._cells) {
574 | const cell = this._cells[k];
575 | if (!cell.visible) {
576 | continue;
577 | }
578 |
579 | const position = new THREE.Vector3(
580 | cell.position[0], cell.position[1], cell.position[2]);
581 | const half = new THREE.Vector3(0.5, 0.5, 0.5);
582 |
583 | const m1 = new THREE.Vector3();
584 | m1.copy(position);
585 | m1.sub(half);
586 |
587 | const m2 = new THREE.Vector3();
588 | m2.copy(position);
589 | m2.add(half);
590 |
591 | const box = new THREE.Box3(m1, m2);
592 | boxes.push(box);
593 | }
594 | }
595 | }
596 | }
597 |
598 | return boxes;
599 | }
600 | };
601 |
602 | class SparseVoxelCellManager {
603 | constructor(game) {
604 | this._game = game;
605 | this._cells = {};
606 | this._cellDimensions = new THREE.Vector3(32, 32, 32);
607 | this._visibleDimensions = [32, 32];
608 | this._dirtyBlocks = {};
609 | this._ids = 0;
610 |
611 | this._tools = [
612 | null,
613 | new voxels_tool.InsertTool(this),
614 | new voxels_tool.DeleteTool(this),
615 | ];
616 | this._activeTool = 0;
617 | }
618 |
619 | _Key(x, y, z) {
620 | return x + '.' + y + '.' + z;
621 | }
622 |
623 | _CellIndex(xp, yp) {
624 | const x = Math.floor(xp / this._cellDimensions.x);
625 | const z = Math.floor(yp / this._cellDimensions.z);
626 | return [x, z];
627 | }
628 |
629 | MarkDirty(block) {
630 | this._dirtyBlocks[block.ID] = block;
631 | }
632 |
633 | InsertVoxel(cellData, overwrite=true) {
634 | const [x, z] = this._CellIndex(cellData.position[0], cellData.position[2]);
635 | const key = this._Key(x, 0, z);
636 |
637 | if (key in this._cells) {
638 | this._cells[key].InsertVoxel(cellData, overwrite);
639 | }
640 | }
641 |
642 | _FindIntersections(ray, maxDistance) {
643 | const camera = this._game._graphics._camera;
644 | const cells = this.LookupCells(camera.position, maxDistance);
645 | const intersections = [];
646 |
647 | for (let c of cells) {
648 | const voxels = c.AsVoxelArray(camera.position, maxDistance);
649 |
650 | for (let v of voxels) {
651 | const intersectionPoint = new THREE.Vector3();
652 |
653 | if (ray.intersectBox(v.aabb, intersectionPoint)) {
654 | intersections.push({
655 | cell: c,
656 | voxel: v,
657 | intersectionPoint: intersectionPoint,
658 | distance: intersectionPoint.distanceTo(camera.position)
659 | });
660 | }
661 | }
662 | }
663 |
664 | intersections.sort((a, b) => {
665 | const d1 = a.intersectionPoint.distanceTo(camera.position);
666 | const d2 = b.intersectionPoint.distanceTo(camera.position);
667 | if (d1 < d2) {
668 | return -1;
669 | } else if (d2 < d1) {
670 | return 1;
671 | } else {
672 | return 0;
673 | }
674 | });
675 |
676 | return intersections;
677 | }
678 |
679 | ChangeActiveTool(dir) {
680 | if (this._tools[this._activeTool]) {
681 | this._tools[this._activeTool].LoseFocus();
682 | }
683 |
684 | this._activeTool += dir + this._tools.length;
685 | this._activeTool %= this._tools.length;
686 | }
687 |
688 | PerformAction() {
689 | if (this._tools[this._activeTool]) {
690 | this._tools[this._activeTool].PerformAction();
691 | }
692 | }
693 |
694 | LookupCells(pos, radius) {
695 | // TODO only lookup really close by
696 | const [x, z] = this._CellIndex(pos.x, pos.z);
697 |
698 | const cells = [];
699 | for (let xi = -1; xi <= 1; xi++) {
700 | for (let zi = -1; zi <= 1; zi++) {
701 | const key = this._Key(x + xi, 0, z + zi);
702 | if (key in this._cells) {
703 | cells.push(this._cells[key]);
704 | }
705 | }
706 | }
707 |
708 | return cells;
709 | }
710 |
711 | Update(timeInSeconds) {
712 | if (this._tools[this._activeTool]) {
713 | this._tools[this._activeTool].Update(timeInSeconds);
714 | }
715 |
716 | this._UpdateDirtyBlocks();
717 | this._UpdateTerrain();
718 | }
719 |
720 | _UpdateDirtyBlocks() {
721 | for (let k in this._dirtyBlocks) {
722 | const b = this._dirtyBlocks[k];
723 | b.Build();
724 | delete this._dirtyBlocks[k];
725 | break;
726 | }
727 | }
728 |
729 | _UpdateTerrain() {
730 | const cameraPosition = this._game._graphics._camera.position;
731 | const cellIndex = this._CellIndex(cameraPosition.x, cameraPosition.z);
732 |
733 | const xs = Math.floor((this._visibleDimensions[0] - 1 ) / 2);
734 | const zs = Math.floor((this._visibleDimensions[1] - 1) / 2);
735 | let cells = {};
736 |
737 | for (let x = -xs; x <= xs; x++) {
738 | for (let z = -zs; z <= zs; z++) {
739 | const xi = x + cellIndex[0];
740 | const zi = z + cellIndex[1];
741 |
742 | const key = this._Key(xi, 0, zi);
743 | cells[key] = [xi, zi];
744 | }
745 | }
746 |
747 | const intersection = utils.DictIntersection(this._cells, cells);
748 | const difference = utils.DictDifference(cells, this._cells);
749 | const recycle = Object.values(utils.DictDifference(this._cells, cells));
750 |
751 | cells = intersection;
752 |
753 | for (let k in difference) {
754 | const [xi, zi] = difference[k];
755 | const offset = new THREE.Vector3(
756 | xi * this._cellDimensions.x, 0, zi * this._cellDimensions.z);
757 |
758 | let block = recycle.pop();
759 | if (block) {
760 | // TODO MAKE PUBLIC API
761 | block._blockOffset = offset;
762 | block._Init();
763 | } else {
764 | block = new voxels.SparseVoxelCellBlock(
765 | this._game, this, offset, this._cellDimensions, this._ids++);
766 | }
767 |
768 | this.MarkDirty(block);
769 |
770 | cells[k] = block;
771 | }
772 |
773 | this._cells = cells;
774 | }
775 | }
776 |
777 | return {
778 | InstancedBlocksManager: InstancedBlocksManager,
779 | SparseVoxelCellBlock: SparseVoxelCellBlock,
780 | SparseVoxelCellManager: SparseVoxelCellManager,
781 | };
782 | })();
783 |
--------------------------------------------------------------------------------
/src/voxels_shader.js:
--------------------------------------------------------------------------------
1 |
2 | export const voxels_shader = (function() {
3 |
4 | const _VS = `
5 | precision highp float;
6 |
7 | uniform mat4 modelViewMatrix;
8 | uniform mat4 projectionMatrix;
9 | uniform vec3 cameraPosition;
10 | uniform float fogDensity;
11 | uniform vec3 cloudScale;
12 |
13 | // Attributes
14 | attribute vec3 position;
15 | attribute vec3 normal;
16 | attribute vec3 color;
17 | attribute vec2 uv;
18 |
19 | // Instance attributes
20 | attribute vec3 offset;
21 | attribute vec2 uvOffset;
22 |
23 | // Outputs
24 | varying vec2 vUV;
25 | varying vec4 vColor;
26 | varying vec4 vLight;
27 | varying vec3 vNormal;
28 | varying float vFog;
29 |
30 | #define saturate(a) clamp( a, 0.0, 1.0 )
31 |
32 |
33 | float _Fog2(const vec3 worldPosition, const float density) {
34 | vec4 viewPosition = modelViewMatrix * vec4(worldPosition, 1.0);
35 |
36 | float att = density * viewPosition.z;
37 | att = att * att * -1.442695;
38 | return 1.0 - clamp(exp2(att), 0.0, 1.0);
39 | }
40 |
41 | vec4 _ComputeLighting() {
42 | // Hardcoded vertex lighting is the best lighting.
43 | float lighting = clamp(dot(normal, normalize(vec3(1, 1, 0.5))), 0.0, 1.0);
44 | vec3 diffuseColour = vec3(1, 1, 1);
45 | vec4 diffuseLighting = vec4(diffuseColour * lighting, 1);
46 |
47 | lighting = clamp(dot(normal, normalize(vec3(-1, 1, -1))), 0.0, 1.0);
48 | diffuseColour = vec3(0.25, 0.25, 0.25);
49 | diffuseLighting += vec4(diffuseColour * lighting, 1);
50 |
51 | lighting = clamp(dot(normal, normalize(vec3(1, 1, 1))), 0.0, 1.0);
52 | diffuseColour = vec3(0.5, 0.5, 0.5);
53 | diffuseLighting += vec4(diffuseColour * lighting, 1);
54 |
55 | vec4 ambientLighting = vec4(1, 1, 1, 1);
56 |
57 | return diffuseLighting + ambientLighting;
58 | }
59 |
60 | void main(){
61 | vec3 worldPosition = offset + position * cloudScale;
62 |
63 | gl_Position = projectionMatrix * modelViewMatrix * vec4(worldPosition, 1.0);
64 |
65 | vUV = uv * uvOffset;
66 | vNormal = normalize(worldPosition - cameraPosition);
67 | vFog = _Fog2(worldPosition, fogDensity);
68 |
69 | vLight = _ComputeLighting();
70 | vColor = vec4(color, 1);
71 | }
72 | `;
73 |
74 | const _PS = `
75 | precision highp float;
76 |
77 | uniform sampler2D diffuseTexture;
78 | uniform samplerCube skybox;
79 |
80 | varying vec2 vUV;
81 | varying vec4 vColor;
82 | varying vec4 vLight;
83 | varying vec3 vNormal;
84 | varying float vFog;
85 |
86 | #define saturate(a) clamp( a, 0.0, 1.0 )
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 | void main() {
98 | vec4 fragmentColor = texture2D(diffuseTexture, vUV);
99 | fragmentColor *= vColor;
100 | fragmentColor *= vLight;
101 |
102 | vec4 outColor = vec4(
103 | _ACESFilmicToneMapping(fragmentColor.xyz), fragmentColor.a);
104 | vec4 fogColor = textureCube(skybox, vNormal);
105 |
106 | gl_FragColor = mix(outColor, fogColor, vFog);
107 | }
108 | `;
109 |
110 | return {
111 | VS: _VS,
112 | PS: _PS,
113 | };
114 | })();
115 |
--------------------------------------------------------------------------------
/src/voxels_tool.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.112.1/build/three.module.js';
2 |
3 |
4 | export const voxels_tool = (function() {
5 |
6 | // HACKY TODO: Separate luminance and highlight, right now one overwrites the
7 | // other.
8 | class InsertTool {
9 | constructor(parent) {
10 | this._parent = parent;
11 | this._cell = null;
12 | this._prev = null;
13 | this._blinkTimer = 0;
14 | this._luminance = 1;
15 | }
16 |
17 | LoseFocus() {
18 | if (this._prev) {
19 | this._parent.MarkDirty(this._prev.cell);
20 | this._prev.cell.RemoveVoxel(
21 | this._prev.cell._Key(
22 | this._prevVoxel.position[0],
23 | this._prevVoxel.position[1],
24 | this._prevVoxel.position[2]));
25 | this._prev = null;
26 | this._prevVoxel = null;
27 | }
28 | }
29 |
30 | PerformAction() {
31 | this.LoseFocus();
32 |
33 | const camera = this._parent._game._graphics._camera;
34 | const forward = new THREE.Vector3(0, 0, -1);
35 | forward.applyQuaternion(camera.quaternion);
36 |
37 | const ray = new THREE.Ray(camera.position, forward);
38 | const intersections = this._parent._FindIntersections(ray, 5);
39 | if (!intersections.length) {
40 | return;
41 | }
42 |
43 | const possibleCoords = [...intersections[0].voxel.position];
44 | possibleCoords[1] += 1;
45 |
46 | if (!intersections[0].cell.HasVoxelAt(
47 | possibleCoords[0], possibleCoords[1], possibleCoords[2])) {
48 | intersections[0].cell.InsertVoxel({
49 | position: [...possibleCoords],
50 | type: 'stone',
51 | visible: true
52 | }, true);
53 | }
54 | }
55 |
56 | Update(timeInSeconds) {
57 | const camera = this._parent._game._graphics._camera;
58 | const forward = new THREE.Vector3(0, 0, -1);
59 | forward.applyQuaternion(camera.quaternion);
60 |
61 | const ray = new THREE.Ray(camera.position, forward);
62 | const intersections = this._parent._FindIntersections(ray, 5);
63 | if (intersections.length) {
64 | if (this._prev) {
65 | this._parent.MarkDirty(this._prev.cell);
66 | this._prev.cell.RemoveVoxel(
67 | this._prev.cell._Key(
68 | this._prevVoxel.position[0],
69 | this._prevVoxel.position[1],
70 | this._prevVoxel.position[2]));
71 | }
72 | const cur = intersections[0];
73 | const newVoxel = {
74 | position: [...cur.voxel.position],
75 | visible: true,
76 | type: 'stone',
77 | blinker: true
78 | };
79 | newVoxel.position[1] += 1;
80 |
81 | if (cur.cell.HasVoxelAt(newVoxel.position[0],
82 | newVoxel.position[1],
83 | newVoxel.position[2])) {
84 | return;
85 | }
86 |
87 | this._prev = cur;
88 | this._prevVoxel = newVoxel;
89 | this._blinkTimer -= timeInSeconds;
90 | if (this._blinkTimer < 0) {
91 | this._blinkTimer = 0.25;
92 | if (this._luminance == 1) {
93 | this._luminance = 2;
94 | } else {
95 | this._luminance = 1;
96 | }
97 | }
98 | const k = cur.cell._Key(newVoxel.position[0],
99 | newVoxel.position[1],
100 | newVoxel.position[2]);
101 | intersections[0].cell.InsertVoxel(newVoxel);
102 | intersections[0].cell._cells[k].luminance = this._luminance;
103 | }
104 | }
105 | };
106 |
107 | class DeleteTool {
108 | constructor(parent) {
109 | this._parent = parent;
110 | this._cell = null;
111 | this._blinkTimer = 0;
112 | this._luminance = 1;
113 | }
114 |
115 | LoseFocus() {
116 | if (this._prev) {
117 | this._prev.cell._cells[this._prev.voxel.key].luminance = 1;
118 | this._parent.MarkDirty(this._prev.cell);
119 | }
120 | }
121 |
122 | PerformAction() {
123 | const camera = this._parent._game._graphics._camera;
124 | const forward = new THREE.Vector3(0, 0, -1);
125 | forward.applyQuaternion(camera.quaternion);
126 |
127 | const ray = new THREE.Ray(camera.position, forward);
128 | const intersections = this._parent._FindIntersections(ray, 5);
129 | if (!intersections.length) {
130 | return;
131 | }
132 |
133 | intersections[0].cell.RemoveVoxel(intersections[0].voxel.key);
134 | }
135 |
136 | Update(timeInSeconds) {
137 | this.LoseFocus();
138 |
139 | const camera = this._parent._game._graphics._camera;
140 | const forward = new THREE.Vector3(0, 0, -1);
141 | forward.applyQuaternion(camera.quaternion);
142 |
143 | const ray = new THREE.Ray(camera.position, forward);
144 | const intersections = this._parent._FindIntersections(ray, 5);
145 | if (intersections.length) {
146 | this._prev = intersections[0];
147 | this._blinkTimer -= timeInSeconds;
148 | if (this._blinkTimer < 0) {
149 | this._blinkTimer = 0.25;
150 | if (this._luminance == 1) {
151 | this._luminance = 2;
152 | } else {
153 | this._luminance = 1;
154 | }
155 | }
156 | intersections[0].cell._cells[intersections[0].voxel.key].luminance = this._luminance;
157 | this._parent.MarkDirty(intersections[0].cell);
158 | }
159 | }
160 | };
161 |
162 | return {
163 | InsertTool: InsertTool,
164 | DeleteTool: DeleteTool,
165 | };
166 | })();
167 |
--------------------------------------------------------------------------------