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