├── .gitignore ├── README.md ├── gfx ├── blocks.png ├── crosshairs.png └── guy.png ├── gimp ├── blocks.xcf ├── crosshairs.xcf └── guy.xcf ├── index.html ├── save └── .gitkeep ├── screenshots └── 2020-04-07.jpg ├── server.sh ├── server ├── Client.js └── main.js ├── sfx └── jump.ogg └── src ├── blocks.js ├── body.js ├── bone.js ├── buffer.js ├── camera.js ├── chunk.js ├── controller.js ├── crosshairs.js ├── debugger.js ├── display.js ├── generator.js ├── main.js ├── map.js ├── math.js ├── matrix.js ├── mesher.js ├── mob.js ├── model.js ├── picker.js ├── server.js ├── shader.js ├── sky.js ├── speaker.js ├── texture.js └── vector.js /.gitignore: -------------------------------------------------------------------------------- 1 | save/chunk_* 2 | deno 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voxel-game 2 | 3 | This ist just another Minecraft clone for the browser. It ist made with JavaScript and WebGL. 4 | No third party libraries are used. Everything ist written entirely from scratch. 5 | 6 | The server ist build on [Deno](https://deno.land/). 7 | 8 | ![Latest screenshot](./screenshots/2020-04-07.jpg) 9 | -------------------------------------------------------------------------------- /gfx/blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gfx/blocks.png -------------------------------------------------------------------------------- /gfx/crosshairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gfx/crosshairs.png -------------------------------------------------------------------------------- /gfx/guy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gfx/guy.png -------------------------------------------------------------------------------- /gimp/blocks.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gimp/blocks.xcf -------------------------------------------------------------------------------- /gimp/crosshairs.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gimp/crosshairs.xcf -------------------------------------------------------------------------------- /gimp/guy.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/gimp/guy.xcf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Voxel Game 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /save/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/save/.gitkeep -------------------------------------------------------------------------------- /screenshots/2020-04-07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/screenshots/2020-04-07.jpg -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./deno run --allow-net --allow-read=. --allow-write=. server/main.js -------------------------------------------------------------------------------- /server/Client.js: -------------------------------------------------------------------------------- 1 | let next_id = 0; 2 | 3 | export class Client extends EventTarget 4 | { 5 | constructor(socket, map) 6 | { 7 | super(); 8 | 9 | this.socket = socket; 10 | this.map = map; 11 | this.id = create_id(); 12 | 13 | socket.addEventListener("message", e => { 14 | let message = JSON.parse(e.data); 15 | this.handle_message(message); 16 | }); 17 | 18 | socket.addEventListener("close", e => { 19 | this.dispatchEvent(custom_event("leave")); 20 | }); 21 | } 22 | 23 | async handle_message(message) 24 | { 25 | let msg = message.msg; 26 | 27 | if(msg === 0) { 28 | let chunk = await this.get_chunk(message.x, message.y); 29 | let buf = new Uint8Array(3 * 8 + chunk.data.byteLength); 30 | let f64 = new Float64Array(buf.buffer); 31 | 32 | f64[0] = 1; 33 | f64[1] = message.x; 34 | f64[2] = message.y; 35 | buf.set(chunk.data, 3 * 8); 36 | this.socket.send(buf); 37 | } 38 | else if(msg === 2) { 39 | let x = message.x; 40 | let y = message.y; 41 | let z = message.z; 42 | let block = message.block; 43 | let chunk = this.map.setBlock(x, y, z, block); 44 | this.dispatchEvent(custom_event("setblock", {x,y,z,block})); 45 | 46 | Deno.writeFile( 47 | "./save/chunk_" + chunk.cx + "_" + chunk.cy, 48 | chunk.data 49 | ); 50 | } 51 | else if(msg === 5) { 52 | let x = message.x; 53 | let y = message.y; 54 | let z = message.z; 55 | let rx = message.rx; 56 | let rz = message.rz; 57 | this.dispatchEvent(custom_event("move", {x,y,z,rx,rz})); 58 | } 59 | } 60 | 61 | async get_chunk(cx, cy) 62 | { 63 | let chunk = this.map.getChunk(cx, cy); 64 | 65 | if(chunk) { 66 | return chunk; 67 | } 68 | 69 | try { 70 | let data = await Deno.readFile("./save/chunk_" + cx + "_" + cy); 71 | 72 | return this.map.loadChunk(cx, cy, data); 73 | } 74 | catch(e) { 75 | return this.map.loadChunk(cx, cy); 76 | } 77 | } 78 | 79 | send_json(msg) 80 | { 81 | this.socket.send(JSON.stringify(msg)); 82 | } 83 | } 84 | 85 | function create_id() 86 | { 87 | return next_id ++; 88 | } 89 | 90 | function custom_event(name, data) 91 | { 92 | let e = new CustomEvent(name); 93 | Object.assign(e, data); 94 | return e; 95 | } -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import Map from "../src/map.js"; 2 | import {contentType} from "https://deno.land/std@0.177.0/media_types/mod.ts"; 3 | import {extname} from "https://deno.land/std@0.177.0/path/mod.ts"; 4 | import {Client} from "./Client.js"; 5 | 6 | let map = new Map(); 7 | let clients = new Set(); 8 | let http_listener = Deno.listen({port: 12345}); 9 | let ws_listener = Deno.listen({port: 54321}); 10 | 11 | handle_http(); 12 | handle_ws(); 13 | 14 | async function handle_http() 15 | { 16 | console.log("Game Link http://localhost:12345/"); 17 | 18 | for await(let conn of http_listener) { 19 | handle_http_conn(conn); 20 | } 21 | } 22 | 23 | async function handle_http_conn(conn) 24 | { 25 | let http_conn = Deno.serveHttp(conn); 26 | 27 | for await(let event of http_conn) { 28 | let request = event.request; 29 | let url = new URL(request.url); 30 | let path = "." + decodeURIComponent(url.pathname); 31 | 32 | if(path === "./") { 33 | path = "./index.html"; 34 | } 35 | 36 | try { 37 | let file = await Deno.open(path); 38 | let stream = file.readable; 39 | let extension = extname(path); 40 | let content_type = contentType(extension); 41 | let headers = new Headers({"Content-Type": content_type}); 42 | let response = new Response(stream, {headers}); 43 | event.respondWith(response); 44 | } 45 | catch(error) { 46 | if(error instanceof Deno.errors.NotFound) { 47 | let response = new Response("Not Found", {status: 404}); 48 | event.respondWith(response); 49 | } 50 | } 51 | } 52 | } 53 | 54 | async function handle_ws() 55 | { 56 | for await(let conn of ws_listener) { 57 | handle_ws_conn(conn); 58 | } 59 | } 60 | 61 | async function handle_ws_conn(conn) 62 | { 63 | let http_conn = Deno.serveHttp(conn); 64 | 65 | for await(let event of http_conn) { 66 | let request = event.request; 67 | try { 68 | let upgrade = Deno.upgradeWebSocket(request); 69 | let socket = upgrade.socket; 70 | let response = upgrade.response; 71 | await event.respondWith(response); 72 | return await handle_socket(socket); 73 | } 74 | catch(e) {} 75 | } 76 | } 77 | 78 | async function handle_socket(socket) 79 | { 80 | let client = new Client(socket, map); 81 | 82 | clients.add(client); 83 | 84 | clients.forEach(other => { 85 | if(other !== client) { 86 | client.send_json({msg: 3, id: other.id}); 87 | other.send_json({msg: 3, id: client.id}); 88 | } 89 | }); 90 | 91 | client.addEventListener("leave", e => { 92 | clients.delete(client); 93 | 94 | clients.forEach(other => { 95 | other.send_json({msg: 4, id: client.id}); 96 | }); 97 | }); 98 | 99 | client.addEventListener("setblock", e => { 100 | clients.forEach(other => { 101 | if(other !== client) { 102 | other.send_json({msg: 2, ...e}); 103 | } 104 | }); 105 | }); 106 | 107 | client.addEventListener("move", e => { 108 | clients.forEach(other => { 109 | if(other !== client) { 110 | other.send_json({msg: 6, id: client.id, ...e}); 111 | } 112 | }); 113 | }); 114 | 115 | return await new Promise(res => { 116 | socket.addEventListener("close", res, true); 117 | }); 118 | } -------------------------------------------------------------------------------- /sfx/jump.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guckstift/voxel-game-js/6d53fe50dee893140b5f529320418db4ce535dd1/sfx/jump.ogg -------------------------------------------------------------------------------- /src/blocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | name: "blockname", 4 | faces: [left, front, bottom, right, back, top] 5 | }, 6 | */ 7 | 8 | export default [ 9 | { 10 | name: "air", 11 | transparent: true, 12 | }, 13 | { 14 | name: "grass", 15 | faces: [2, 2, 1, 2, 2, 0], 16 | }, 17 | { 18 | name: "dirt", 19 | faces: [1, 1, 1, 1, 1, 1], 20 | }, 21 | { 22 | name: "stone", 23 | faces: [3, 3, 3, 3, 3, 3], 24 | }, 25 | { 26 | name: "water", 27 | faces: [16,16,16,16,16,16], 28 | transparent: true, 29 | }, 30 | { 31 | name: "sand", 32 | faces: [4, 4, 4, 4, 4, 4], 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/body.js: -------------------------------------------------------------------------------- 1 | import Vector from "./vector.js"; 2 | import Matrix from "./matrix.js"; 3 | import {radians} from "./math.js"; 4 | 5 | export default class Body 6 | { 7 | constructor(map, x, y, z, rx, rz, boxmin, boxmax) 8 | { 9 | this.map = map; 10 | this.pos = new Vector(x, y, z); 11 | this.vel = new Vector(); 12 | this.acc = new Vector(); 13 | this.rest = new Vector(); 14 | this.rx = rx; 15 | this.rz = rz; 16 | this.boxmin = new Vector(...boxmin); 17 | this.boxmax = new Vector(...boxmax); 18 | this.mat = new Matrix(); 19 | } 20 | 21 | move(vec, delta) 22 | { 23 | let deltavec = vec.clone(); 24 | 25 | deltavec.scale(delta); 26 | 27 | let boxmin = this.pos.clone(); 28 | 29 | boxmin.add(this.boxmin); 30 | 31 | let boxmax = this.pos.clone(); 32 | 33 | boxmax.add(this.boxmax); 34 | 35 | for(let i=0; i<3; i++) { 36 | let hit = this.map.boxmarch(boxmin.data, boxmax.data, deltavec.data); 37 | 38 | if(!hit) { 39 | break; 40 | } 41 | 42 | deltavec.data[hit.axis] = hit.offs; 43 | this.rest.data[hit.axis] = hit.step; 44 | } 45 | 46 | this.pos.add(deltavec); 47 | 48 | if(deltavec.x !== 0) { 49 | this.rest.data[0] = 0; 50 | } 51 | 52 | if(deltavec.y !== 0) { 53 | this.rest.data[1] = 0; 54 | } 55 | 56 | if(deltavec.z !== 0) { 57 | this.rest.data[2] = 0; 58 | } 59 | } 60 | 61 | accel(acc, delta) 62 | { 63 | this.vel.addScaled(acc, delta); 64 | } 65 | 66 | update(delta) 67 | { 68 | this.accel(this.acc, delta); 69 | this.move(this.vel, delta); 70 | 71 | if(this.rest.x !== 0) { 72 | this.vel.data[0] = 0; 73 | } 74 | 75 | if(this.rest.y !== 0) { 76 | this.vel.data[1] = 0; 77 | } 78 | 79 | if(this.rest.z !== 0) { 80 | this.vel.data[2] = 0; 81 | } 82 | 83 | this.mat.set(); 84 | this.mat.translate(this.pos.x, this.pos.y, this.pos.z); 85 | this.mat.rotateZ(radians(this.rz)); 86 | this.mat.rotateX(radians(this.rx)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/bone.js: -------------------------------------------------------------------------------- 1 | import Vector from "./vector.js"; 2 | import Matrix from "./matrix.js"; 3 | import {radians} from "./math.js"; 4 | 5 | export default class Bone 6 | { 7 | constructor(rootX, rootY, rootZ) 8 | { 9 | this.root = new Vector(rootX, rootY, rootZ); 10 | this.rx = 0; 11 | this.ry = 0; 12 | this.rz = 0; 13 | this.mat = new Matrix(); 14 | } 15 | 16 | update() 17 | { 18 | this.mat.set(); 19 | this.mat.translate(this.root.x, this.root.y, this.root.z); 20 | this.mat.rotateX(radians(this.rx)); 21 | this.mat.rotateX(radians(this.ry)); 22 | this.mat.rotateX(radians(this.rz)); 23 | this.mat.translate(-this.root.x, -this.root.y, -this.root.z); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/buffer.js: -------------------------------------------------------------------------------- 1 | export default class Buffer 2 | { 3 | constructor(display, data = null) 4 | { 5 | let gl = display.gl; 6 | let buf = gl.createBuffer(); 7 | 8 | if(data) { 9 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 10 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); 11 | } 12 | 13 | this.buf = buf; 14 | this.gl = gl; 15 | } 16 | 17 | update(data) 18 | { 19 | let gl = this.gl; 20 | 21 | gl.bindBuffer(gl.ARRAY_BUFFER, this.buf); 22 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/camera.js: -------------------------------------------------------------------------------- 1 | import Vector from "./vector.js"; 2 | import Matrix from "./matrix.js"; 3 | import {radians} from "./math.js"; 4 | import Body from "./body.js"; 5 | 6 | export default class Camera extends Body 7 | { 8 | constructor(map, fovy, aspect, near, far, x, y, z, rx, rz) 9 | { 10 | super(map, x, y, z, rx, rz, [-0.25, -0.25, -1.75], [+0.25, +0.25, +0.25]); 11 | 12 | this.fovy = fovy; 13 | this.aspect = aspect; 14 | this.near = near; 15 | this.far = far; 16 | this.proj = new Matrix(); 17 | this.view = new Matrix(); 18 | this.model = new Matrix(); 19 | this.rightward = new Vector(1,0,0); 20 | this.forward = new Vector(0,1,0); 21 | this.upward = new Vector(0,0,1); 22 | this.lookat = new Vector(0,1,0); 23 | } 24 | 25 | update(delta) 26 | { 27 | super.update(delta); 28 | 29 | this.proj.perspective(radians(this.fovy), this.aspect, this.near, this.far) 30 | this.view.set(); 31 | this.view.rotateX(-radians(this.rx + 90)); 32 | this.view.rotateZ(-radians(this.rz)); 33 | this.view.translate(-this.pos.x, -this.pos.y, -this.pos.z); 34 | this.model.set(); 35 | this.model.translate(this.pos.x, this.pos.y, this.pos.z); 36 | this.rightward.set(1,0,0); 37 | this.rightward.rotateZ(radians(this.rz)); 38 | this.forward.set(0,1,0); 39 | this.forward.rotateZ(radians(this.rz)); 40 | this.lookat.set(0,1,0); 41 | this.lookat.rotateX(radians(this.rx)); 42 | this.lookat.rotateZ(radians(this.rz)); 43 | } 44 | 45 | moveForward(delta) 46 | { 47 | this.move(this.forward, +delta); 48 | } 49 | 50 | moveBackward(delta) 51 | { 52 | this.move(this.forward, -delta); 53 | } 54 | 55 | moveRightward(delta) 56 | { 57 | this.move(this.rightward, +delta); 58 | } 59 | 60 | moveLeftward(delta) 61 | { 62 | this.move(this.rightward, -delta); 63 | } 64 | 65 | moveUpward(delta) 66 | { 67 | this.move(this.upward, +delta); 68 | } 69 | 70 | moveDownward(delta) 71 | { 72 | this.move(this.upward, -delta); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/chunk.js: -------------------------------------------------------------------------------- 1 | import Shader from "./shader.js"; 2 | import Texture from "./texture.js"; 3 | import Buffer from "./buffer.js"; 4 | import Matrix from "./matrix.js"; 5 | import Generator from "./generator.js"; 6 | 7 | let vert = ` 8 | uniform mat4 proj; 9 | uniform mat4 view; 10 | uniform mat4 model; 11 | uniform vec3 sun; 12 | attribute vec3 pos; 13 | attribute vec3 norm; 14 | attribute vec2 uv; 15 | attribute float face; 16 | attribute float ao; 17 | varying mediump vec2 vUv; 18 | varying mediump vec2 texOffs; 19 | varying mediump float factor; 20 | 21 | void main() 22 | { 23 | gl_Position = proj * view * model * vec4(pos, 1); 24 | vUv = uv; 25 | 26 | float texX = mod(face, 16.0); 27 | float texY = floor(face / 16.0); 28 | 29 | texOffs = vec2(texX, texY) / 16.0; 30 | 31 | float diffuse = max(0.0, dot(sun, norm)); 32 | float ambient = (4.0 - ao) * 0.25; 33 | 34 | factor = mix(diffuse, ambient, 0.5); 35 | } 36 | `; 37 | 38 | let frag = ` 39 | uniform sampler2D tex; 40 | varying mediump vec2 vUv; 41 | varying mediump vec2 texOffs; 42 | varying mediump float factor; 43 | 44 | void main() 45 | { 46 | mediump vec2 texCoord = texOffs + fract(vUv) / 16.0; 47 | 48 | gl_FragColor = texture2D(tex, texCoord); 49 | gl_FragColor.rgb *= factor; 50 | } 51 | `; 52 | 53 | let zeroChunk = new Uint8Array(16 * 16 * 256); 54 | 55 | export default class Chunk 56 | { 57 | constructor(display, map, cx, cy) 58 | { 59 | if(display) { 60 | this.shader = display.getCached("Chunk.shader", () => new Shader(display, vert, frag)); 61 | this.texture = display.getCached("Chunk.texture", () => new Texture(display, "gfx/blocks.png")); 62 | this.buffer = new Buffer(display); 63 | this.transbuf = new Buffer(display); 64 | this.gl = display.gl; 65 | } 66 | 67 | this.data = new Uint8Array(16 * 16 * 256); 68 | this.generator = new Generator(); 69 | this.count = 0; 70 | this.transcount = 0; 71 | this.display = display; 72 | this.map = map; 73 | this.cx = cx; 74 | this.cy = cy; 75 | this.invalid = false; 76 | this.meshingStartTime = 0; 77 | this.model = new Matrix(); 78 | this.model.translate(cx * 16, cy * 16, 0); 79 | } 80 | 81 | getBlock(x, y, z) 82 | { 83 | if(x >= 0 && y >= 0 && z >= 0 && x < 16 && y < 16 && z < 256) { 84 | return this.data[x + y * 16 + z * 16 * 16]; 85 | } 86 | 87 | if(z >= 0 && z < 256) { 88 | return this.map.getBlock(this.cx * 16 + x, this.cy * 16 + y, z); 89 | } 90 | 91 | return 0; 92 | } 93 | 94 | setBlock(x, y, z, b) 95 | { 96 | if(x >= 0 && y >= 0 && z >= 0 && x < 16 && y < 16 && z < 256) { 97 | this.data[x + y * 16 + z * 16 * 16] = b; 98 | this.invalid = true; 99 | 100 | let adjacentList = []; 101 | 102 | if(x === 0) { 103 | adjacentList.push(this.map.getChunk(this.cx - 1, this.cy)); 104 | 105 | if(y === 0) { 106 | adjacentList.push(this.map.getChunk(this.cx - 1, this.cy - 1)); 107 | } 108 | else if(y === 15) { 109 | adjacentList.push(this.map.getChunk(this.cx - 1, this.cy + 1)); 110 | } 111 | } 112 | else if(x === 15) { 113 | adjacentList.push(this.map.getChunk(this.cx + 1, this.cy)); 114 | 115 | if(y === 0) { 116 | adjacentList.push(this.map.getChunk(this.cx + 1, this.cy - 1)); 117 | } 118 | else if(y === 15) { 119 | adjacentList.push(this.map.getChunk(this.cx + 1, this.cy + 1)); 120 | } 121 | } 122 | 123 | if(y === 0) { 124 | adjacentList.push(this.map.getChunk(this.cx, this.cy - 1)); 125 | } 126 | else if(y === 15) { 127 | adjacentList.push(this.map.getChunk(this.cx, this.cy + 1)); 128 | } 129 | 130 | adjacentList.forEach(chunk => { 131 | if(chunk) { 132 | chunk.invalid = true; 133 | } 134 | }); 135 | } 136 | } 137 | 138 | generate() 139 | { 140 | for(let z=0, i=0; z<256; z++) { 141 | for(let y=0; y<16; y++) { 142 | for(let x=0; x<16; x++, i++) { 143 | this.data[i] = this.generator.getBlock( 144 | x + this.cx * 16, 145 | y + this.cy * 16, 146 | z, 147 | ); 148 | } 149 | } 150 | } 151 | 152 | this.invalidateVicinity(); 153 | } 154 | 155 | setData(data) 156 | { 157 | this.data = data; 158 | this.invalidateVicinity(); 159 | } 160 | 161 | invalidateVicinity() 162 | { 163 | this.invalid = true; 164 | 165 | for(let y = -1; y <= +1; y++) { 166 | for(let x = -1; x <= +1; x++) { 167 | let chunk = this.map.getChunk(this.cx + x, this.cy + y); 168 | 169 | if(chunk) { 170 | chunk.invalid = true; 171 | } 172 | } 173 | } 174 | } 175 | 176 | getVicinity() 177 | { 178 | let chunks = []; 179 | 180 | for(let y = -1; y <= +1; y++) { 181 | for(let x = -1; x <= +1; x++) { 182 | let chunk = this.map.getChunk(this.cx + x, this.cy + y); 183 | 184 | if(chunk) { 185 | chunks.push(chunk.data); 186 | } 187 | else { 188 | chunks.push(zeroChunk); 189 | } 190 | } 191 | } 192 | 193 | return chunks; 194 | } 195 | 196 | update() 197 | { 198 | if(this.invalid) { 199 | this.meshingStartTime = performance.now(); 200 | this.map.remeshChunk(this); 201 | this.invalid = false; 202 | } 203 | } 204 | 205 | applyMesh(mesh, transmesh) 206 | { 207 | this.buffer.update(new Float32Array(mesh)); 208 | this.count = mesh.length / 10; 209 | this.transbuf.update(new Float32Array(transmesh)); 210 | this.transcount = transmesh.length / 10; 211 | 212 | console.log("chunk mesh updated", this.cx, this.cy, "time", performance.now() - this.meshingStartTime); 213 | } 214 | 215 | draw(camera, sun, drawTrans) 216 | { 217 | let shader = this.shader; 218 | let buffer = null; 219 | let count = 0; 220 | 221 | if(drawTrans) { 222 | buffer = this.transbuf; 223 | count = this.transcount; 224 | } 225 | else { 226 | buffer = this.buffer; 227 | count = this.count; 228 | } 229 | 230 | if(count === 0) { 231 | return; 232 | } 233 | 234 | shader.assignFloatAttrib("pos", buffer, 3, 10, 0); 235 | shader.assignFloatAttrib("norm", buffer, 3, 10, 3); 236 | shader.assignFloatAttrib("uv", buffer, 2, 10, 6); 237 | shader.assignFloatAttrib("face", buffer, 1, 10, 8); 238 | shader.assignFloatAttrib("ao", buffer, 1, 10, 9); 239 | shader.use(); 240 | shader.assignMatrix("proj", camera.proj); 241 | shader.assignMatrix("view", camera.view); 242 | shader.assignMatrix("model", this.model); 243 | shader.assignVector("sun", sun); 244 | shader.assignTexture("tex", this.texture, 0); 245 | 246 | this.display.drawTriangles(count); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | import Vector from "./vector.js"; 2 | 3 | export default class Controller 4 | { 5 | constructor(camera, display, picker, map, speaker) 6 | { 7 | let canvas = display.canvas; 8 | 9 | this.canvas = canvas; 10 | this.camera = camera; 11 | this.picker = picker; 12 | this.speaker = speaker; 13 | this.map = map; 14 | this.keymap = {}; 15 | this.locked = false; 16 | this.movespeed = 4; 17 | this.flyspeed = 8; 18 | this.jumpspeed = 8; 19 | this.flymode = false; 20 | this.jumpsound = null; 21 | this.soundcooldown = true; 22 | 23 | speaker.loadSound("sfx/jump.ogg").then(sound => this.jumpsound = sound); 24 | 25 | window.addEventListener("keydown", e => this.keydown(e)); 26 | window.addEventListener("keyup", e => this.keyup(e)); 27 | canvas.addEventListener("mousedown", e => this.mousedown(e)); 28 | canvas.addEventListener("mousemove", e => this.mousemove(e)); 29 | document.addEventListener("pointerlockchange", e => this.lockchange(e)); 30 | } 31 | 32 | getKey(e) 33 | { 34 | let key = e.key.toLowerCase(); 35 | 36 | if(key === " ") { 37 | key = "space"; 38 | } 39 | 40 | return key; 41 | } 42 | 43 | keydown(e) 44 | { 45 | this.speaker.activate(); 46 | 47 | let key = this.getKey(e); 48 | 49 | this.keymap[key] = true; 50 | } 51 | 52 | keyup(e) 53 | { 54 | let key = this.getKey(e); 55 | 56 | this.keymap[key] = false; 57 | } 58 | 59 | mousedown(e) 60 | { 61 | this.speaker.activate(); 62 | 63 | if(this.locked) { 64 | if(e.button === 0 && this.picker.hasHit) { 65 | this.map.setBlock(...this.picker.hitVox, 0); 66 | } 67 | } 68 | else { 69 | this.canvas.requestPointerLock(); 70 | this.locked = true; 71 | } 72 | } 73 | 74 | mousemove(e) 75 | { 76 | if(this.locked) { 77 | this.camera.rx -= e.movementY; 78 | this.camera.rz -= e.movementX; 79 | } 80 | } 81 | 82 | lockchange(e) 83 | { 84 | if(document.pointerLockElement !== this.canvas) { 85 | this.locked = false; 86 | } 87 | } 88 | 89 | enableFly() 90 | { 91 | this.flymode = true; 92 | this.camera.acc.set(0,0,0); 93 | this.camera.vel.set(0,0,0); 94 | } 95 | 96 | disableFly() 97 | { 98 | this.flymode = false; 99 | this.camera.acc.set(0,0,-20); 100 | } 101 | 102 | update(delta) 103 | { 104 | let movespeed = this.flymode ? this.flyspeed : this.movespeed; 105 | 106 | if(this.flymode) { 107 | if(this.keymap.space) { 108 | this.camera.moveUpward(delta * movespeed); 109 | } 110 | if(this.keymap.shift) { 111 | this.camera.moveDownward(delta * movespeed); 112 | } 113 | } 114 | else { 115 | if(this.keymap.space && this.camera.rest.z < 0) { 116 | this.camera.accel(new Vector(0, 0, this.jumpspeed), 1); 117 | 118 | if(this.jumpsound && this.soundcooldown) { 119 | this.speaker.playSound(this.jumpsound); 120 | this.soundcooldown = false; 121 | setTimeout(() => this.soundcooldown = true, 500); 122 | } 123 | } 124 | } 125 | 126 | if(this.keymap.w) { 127 | this.camera.moveForward(delta * movespeed); 128 | } 129 | if(this.keymap.s) { 130 | this.camera.moveBackward(delta * movespeed); 131 | } 132 | if(this.keymap.d) { 133 | this.camera.moveRightward(delta * movespeed); 134 | } 135 | if(this.keymap.a) { 136 | this.camera.moveLeftward(delta * movespeed); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/crosshairs.js: -------------------------------------------------------------------------------- 1 | export default class Crosshairs 2 | { 3 | constructor() 4 | { 5 | let dom = document.createElement("img"); 6 | 7 | dom.src = "gfx/crosshairs.png"; 8 | dom.style.display = "block"; 9 | dom.style.position = "absolute"; 10 | dom.style.left = "50%"; 11 | dom.style.top = "50%"; 12 | dom.style.transform = "translate(-50%,-50%)"; 13 | 14 | this.dom = dom; 15 | } 16 | 17 | appendToBody() 18 | { 19 | document.body.appendChild(this.dom); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/debugger.js: -------------------------------------------------------------------------------- 1 | export default class Debugger 2 | { 3 | constructor(camera, map, controller, server) 4 | { 5 | let dom = document.createElement("div"); 6 | 7 | dom.style.display = "none"; 8 | dom.style.position = "absolute"; 9 | dom.style.left = "16px"; 10 | dom.style.top = "16px"; 11 | dom.style.padding = "16px"; 12 | dom.style.boxSizing = "border-box"; 13 | dom.style.backgroundColor = "#0000ff80"; 14 | dom.style.borderRadius = "8px"; 15 | dom.style.color = "#fff"; 16 | dom.style.fontFamily = "monospace"; 17 | 18 | this.dom = dom; 19 | this.frames = 0; 20 | this.timeAccu = 0; 21 | this.last = 0; 22 | this.camera = camera; 23 | this.map = map; 24 | this.controller = controller; 25 | this.server = server; 26 | this.enabled = false; 27 | 28 | this.fpsMonitor = this.addMonitor("FPS"); 29 | this.posMonitor = this.addMonitor("Position"); 30 | this.chunksMonitor = this.addMonitor("Loaded chunks"); 31 | this.chunkPosMonitor = this.addMonitor("Chunk position"); 32 | this.chunkQuadsMonitor = this.addMonitor("Chunk quad count"); 33 | this.othersMonitor = this.addMonitor("Other player IDs"); 34 | 35 | window.addEventListener("keydown", e => { 36 | if(e.key === "F3") { 37 | e.preventDefault(); 38 | this.toggle(); 39 | } 40 | }); 41 | } 42 | 43 | appendToBody() 44 | { 45 | document.body.appendChild(this.dom); 46 | } 47 | 48 | addMonitor(labelText) 49 | { 50 | let monitor = document.createElement("div"); 51 | let label = document.createElement("span"); 52 | let value = document.createElement("span"); 53 | 54 | label.textContent = labelText + ": "; 55 | monitor.appendChild(label); 56 | monitor.appendChild(value); 57 | this.dom.appendChild(monitor); 58 | 59 | return value; 60 | } 61 | 62 | frame() 63 | { 64 | if(!this.enabled) { 65 | return; 66 | } 67 | 68 | let now = performance.now(); 69 | 70 | this.last = this.last || now; 71 | this.frames ++; 72 | 73 | let delta = now - this.last; 74 | 75 | if(delta >= 1000) { 76 | let fps = this.frames * 1000 / delta; 77 | 78 | this.fpsMonitor.textContent = fps.toFixed(0); 79 | this.last = now; 80 | this.frames = 0; 81 | } 82 | 83 | this.posMonitor.textContent = ` 84 | x = ${this.camera.pos.x.toFixed(2)} 85 | y = ${this.camera.pos.y.toFixed(2)} 86 | z = ${this.camera.pos.z.toFixed(2)} 87 | `; 88 | 89 | this.chunksMonitor.textContent = this.map.loadedChunks; 90 | 91 | let cx = Math.floor(this.camera.pos.x / 16); 92 | let cy = Math.floor(this.camera.pos.y / 16); 93 | 94 | this.chunkPosMonitor.textContent = ` 95 | x = ${cx.toFixed(0)} 96 | y = ${cy.toFixed(0)} 97 | `; 98 | 99 | let chunk = this.map.getChunk(cx, cy); 100 | 101 | this.chunkQuadsMonitor.textContent = chunk ? chunk.count / 6 : "no chunk loaded"; 102 | 103 | this.othersMonitor.textContent = Array.from(this.server.others).join(","); 104 | } 105 | 106 | enable() 107 | { 108 | this.enabled = true; 109 | this.dom.style.display = "block"; 110 | this.controller.enableFly(); 111 | } 112 | 113 | disable() 114 | { 115 | this.enabled = false; 116 | this.dom.style.display = "none"; 117 | this.controller.disableFly(); 118 | } 119 | 120 | toggle() 121 | { 122 | if(this.enabled) { 123 | this.disable(); 124 | } 125 | else { 126 | this.enable(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/display.js: -------------------------------------------------------------------------------- 1 | export default class Display 2 | { 3 | constructor() 4 | { 5 | let canvas = document.createElement("canvas"); 6 | 7 | canvas.style.position = "absolute"; 8 | canvas.style.left = "0"; 9 | canvas.style.top = "0"; 10 | canvas.style.width = "100%"; 11 | canvas.style.height = "100%"; 12 | 13 | let gl = canvas.getContext("webgl", {alpha: false, antialias: false}); 14 | 15 | gl.enable(gl.DEPTH_TEST); 16 | gl.enable(gl.CULL_FACE); 17 | gl.enable(gl.BLEND); 18 | 19 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 20 | 21 | let self = this; 22 | 23 | requestAnimationFrame(function frame() { 24 | requestAnimationFrame(frame); 25 | self.adjustCanvasSize(); 26 | self.onframe(); 27 | }); 28 | 29 | this.canvas = canvas; 30 | this.gl = gl; 31 | this.onframe = () => {}; 32 | this.cache = {}; 33 | } 34 | 35 | appendToBody() 36 | { 37 | document.body.appendChild(this.canvas); 38 | } 39 | 40 | drawTriangles(count) 41 | { 42 | let gl = this.gl; 43 | 44 | gl.drawArrays(gl.TRIANGLES, 0, count); 45 | } 46 | 47 | adjustCanvasSize() 48 | { 49 | let gl = this.gl; 50 | let canvas = this.canvas; 51 | let w = canvas.clientWidth; 52 | let h = canvas.clientHeight; 53 | 54 | if(w !== canvas.width || h !== canvas.height) { 55 | canvas.width = w; 56 | canvas.height = h; 57 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 58 | } 59 | } 60 | 61 | getAspect() 62 | { 63 | let canvas = this.canvas; 64 | 65 | return canvas.clientWidth / canvas.clientHeight; 66 | } 67 | 68 | getCached(id, factory) 69 | { 70 | if(!this.cache[id]) { 71 | this.cache[id] = factory(); 72 | } 73 | 74 | return this.cache[id]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/generator.js: -------------------------------------------------------------------------------- 1 | export default class Generator 2 | { 3 | rand2d(x, y) 4 | { 5 | x *= 15485863; // mult with 1000000. prime 6 | y *= 285058399; // mult with 15485863. prime 7 | x += y; // add x and y together 8 | x *= 3141592653; // scramble with 10 first digits of PI 9 | x ^= x >>> 2; // xor with r-shift with 1. prime 10 | x ^= x << 5; // xor with l-shift with 3. prime 11 | x ^= x >>> 11; // xor with r-shift with 5. prime 12 | x ^= x << 17; // xor with l-shift with 7. prime 13 | x ^= x >>> 23; // xor with r-shift with 9. prime 14 | x ^= x << 31; // xor with l-shift with 11. prime 15 | 16 | return (x >>> 0) / 0xFFffFFff; 17 | } 18 | 19 | smooth(x, a, b) 20 | { 21 | return a + (3 * x ** 2 - 2 * x ** 3) * (b - a); 22 | } 23 | 24 | sample2d(x, y) 25 | { 26 | let ax = Math.floor(x); 27 | let ay = Math.floor(y); 28 | let bx = Math.ceil(x); 29 | let by = Math.ceil(y); 30 | let aa = this.rand2d(ax, ay); 31 | let ab = this.rand2d(ax, by); 32 | let ba = this.rand2d(bx, ay); 33 | let bb = this.rand2d(bx, by); 34 | let fx = x - ax; 35 | let fy = y - ay; 36 | 37 | return this.smooth(fy, 38 | this.smooth(fx, aa, ba), 39 | this.smooth(fx, ab, bb), 40 | ); 41 | } 42 | 43 | getBlock(x, y, z) 44 | { 45 | let height = Math.floor(16 * this.sample2d(x / 16, y / 16)); 46 | 47 | if(z < height / 3 * 2) { 48 | return 3; 49 | } 50 | else if(height <= 8 && z <= height) { 51 | return 5; 52 | } 53 | else if(z < height) { 54 | return 2; 55 | } 56 | else if(z === height) { 57 | return 1; 58 | } 59 | else if(z < 8) { 60 | return 4; 61 | } 62 | else { 63 | return 0; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Display from "./display.js"; 2 | import Camera from "./camera.js"; 3 | import Controller from "./controller.js"; 4 | import Map from "./map.js"; 5 | import Vector from "./vector.js"; 6 | import {radians} from "./math.js"; 7 | import Picker from "./picker.js"; 8 | import Crosshairs from "./crosshairs.js"; 9 | import Debugger from "./debugger.js"; 10 | import Model from "./model.js"; 11 | import Texture from "./texture.js"; 12 | import Server from "./server.js"; 13 | import Sky from "./sky.js"; 14 | import Speaker from "./speaker.js"; 15 | import Mob from "./mob.js"; 16 | 17 | let display = new Display(); 18 | 19 | display.appendToBody(); 20 | 21 | let crosshairs = new Crosshairs(); 22 | 23 | crosshairs.appendToBody(); 24 | 25 | let server = new Server(); 26 | let map = new Map(display, server); 27 | 28 | let camera = new Camera(map, 90, 800/600, 0.1, 1000, 8,8,16, -30,0); 29 | 30 | let picker = new Picker(display, map); 31 | let speaker = new Speaker(); 32 | let controller = new Controller(camera, display, picker, map, speaker); 33 | 34 | let dbg = new Debugger(camera, map, controller, server); 35 | 36 | dbg.enable(); 37 | dbg.appendToBody(); 38 | 39 | let sun = new Vector(0,0,1); 40 | 41 | sun.rotateX(radians(30)); 42 | 43 | let model = new Model( 44 | display, 45 | new Texture(display, "gfx/guy.png"), 46 | [[-0.375, 0, -0.375], [+0.375, 0, -0.375], [0, 0, -0.25]] 47 | ); 48 | 49 | model.addCube([-0.25, -0.25,-0.25], [ 0.5, 0.5, 0.5], [ 0, 0], [8,8, 8], 64, 3); // head 50 | model.addCube([-0.25,-0.125, -1.0], [ 0.5,0.25,0.75], [ 0, 8], [8,4,12], 64, 0); // upper body 51 | model.addCube([ -0.5,-0.125, -1.0], [0.25,0.25,0.75], [40, 0], [4,4,12], 64, 1); // left arm 52 | model.addCube([ 0.25,-0.125, -1.0], [0.25,0.25,0.75], [40,12], [4,4,12], 64, 2); // right arm 53 | model.addCube([-0.25,-0.125,-1.75], [0.25,0.25,0.75], [ 0,20], [4,4,12], 64, 0); // left leg 54 | model.addCube([ 0,-0.125,-1.75], [0.25,0.25,0.75], [20,20], [4,4,12], 64, 0); // right leg 55 | 56 | let players = {}; 57 | 58 | server.onAddPlayer = id => { 59 | players[id] = new Mob(map, 6,15,16, 0,0, [-0.25, -0.25, -1.75], [+0.25, +0.25, +0.25], model); 60 | }; 61 | 62 | server.onRemovePlayer = id => { 63 | delete players[id]; 64 | }; 65 | 66 | server.onSetPlayerPos = (id, x, y, z, rx, rz) => { 67 | let player = players[id]; 68 | 69 | if(player) { 70 | player.pos.data[0] = x; 71 | player.pos.data[1] = y; 72 | player.pos.data[2] = z; 73 | player.bones[2].rx = rx; 74 | player.rz = rz; 75 | } 76 | }; 77 | 78 | let sky = new Sky(display); 79 | 80 | display.onframe = () => 81 | { 82 | dbg.frame(); 83 | 84 | let cx = Math.floor(camera.pos.x / 16); 85 | let cy = Math.floor(camera.pos.y / 16); 86 | 87 | for(let y = cy - 1; y <= cy + 1; y++) { 88 | for(let x = cx - 1; x <= cx + 1; x++) { 89 | map.loadChunk(x, y); 90 | } 91 | } 92 | 93 | controller.update(1/60); 94 | 95 | server.setMyPos(camera.pos.x, camera.pos.y, camera.pos.z, camera.rx, camera.rz); 96 | 97 | camera.aspect = display.getAspect(); 98 | camera.update(1/60); 99 | 100 | picker.pick(camera.pos, camera.lookat, 16); 101 | 102 | map.update(); 103 | 104 | for(let id in players) { 105 | players[id].update(1/60); 106 | } 107 | 108 | sky.draw(camera); 109 | map.draw(camera, sun); 110 | 111 | for(let id in players) { 112 | players[id].draw(camera, sun); 113 | } 114 | 115 | picker.draw(camera); 116 | }; 117 | -------------------------------------------------------------------------------- /src/map.js: -------------------------------------------------------------------------------- 1 | import Chunk from "./chunk.js"; 2 | 3 | export default class Map 4 | { 5 | constructor(display, server) 6 | { 7 | if(display) { 8 | this.mesher = new Worker("src/mesher.js", {type: "module"}); 9 | 10 | this.mesher.onmessage = e => { 11 | let cx = e.data.cx; 12 | let cy = e.data.cy; 13 | let chunk = this.getChunk(cx, cy); 14 | 15 | if(chunk) { 16 | chunk.applyMesh(e.data.mesh, e.data.transmesh); 17 | } 18 | }; 19 | } 20 | 21 | if(server) { 22 | server.onSetChunk = (cx, cy, data) => { 23 | let chunk = this.getChunk(cx, cy); 24 | chunk.setData(data); 25 | }; 26 | 27 | server.onSetBlock = (x, y, z, block) => { 28 | this.setBlock(x, y, z, block, false) 29 | }; 30 | } 31 | 32 | this.chunks = {}; 33 | this.display = display; 34 | this.server = server; 35 | this.loadedChunks = 0; 36 | } 37 | 38 | getChunk(cx, cy) 39 | { 40 | if(this.chunks[cy] && this.chunks[cy][cx]) { 41 | return this.chunks[cy][cx]; 42 | } 43 | } 44 | 45 | loadChunk(cx, cy, data = null) 46 | { 47 | let chunk = this.getChunk(cx, cy); 48 | 49 | if(!chunk) { 50 | if(!this.chunks[cy]) { 51 | this.chunks[cy] = {}; 52 | } 53 | 54 | chunk = new Chunk(this.display, this, cx, cy); 55 | 56 | if(this.server) { 57 | this.server.getChunk(cx, cy); 58 | } 59 | else if(data) { 60 | chunk.setData(data); 61 | } 62 | else { 63 | chunk.generate(); 64 | } 65 | 66 | this.chunks[cy][cx] = chunk; 67 | this.loadedChunks ++; 68 | } 69 | 70 | return chunk; 71 | } 72 | 73 | remeshChunk(chunk) 74 | { 75 | let chunks = chunk.getVicinity(); 76 | let cx = chunk.cx; 77 | let cy = chunk.cy; 78 | 79 | this.mesher.postMessage({chunks, cx, cy}); 80 | } 81 | 82 | getBlock(x, y, z) 83 | { 84 | let cx = Math.floor(x / 16); 85 | let cy = Math.floor(y / 16); 86 | let chunk = this.getChunk(cx, cy); 87 | 88 | return chunk ? chunk.getBlock(x - cx * 16, y - cy * 16, z) : 0; 89 | } 90 | 91 | setBlock(x, y, z, b, pushToServer = true) 92 | { 93 | let cx = Math.floor(x / 16); 94 | let cy = Math.floor(y / 16); 95 | let chunk = this.getChunk(cx, cy); 96 | 97 | if(chunk) { 98 | chunk.setBlock(x - cx * 16, y - cy * 16, z, b); 99 | } 100 | 101 | if(this.server && pushToServer) { 102 | this.server.setBlock(x, y, z, b); 103 | } 104 | 105 | return chunk; 106 | } 107 | 108 | forEachChunk(fn) 109 | { 110 | for(let y in this.chunks) { 111 | if(this.chunks[y]) { 112 | for(let x in this.chunks[y]) { 113 | let chunk = this.chunks[y][x]; 114 | 115 | fn(chunk, x, y); 116 | } 117 | } 118 | } 119 | } 120 | 121 | update() 122 | { 123 | this.forEachChunk(chunk => chunk.update()); 124 | } 125 | 126 | draw(camera, sun) 127 | { 128 | this.forEachChunk(chunk => chunk.draw(camera, sun, false)); 129 | this.forEachChunk(chunk => chunk.draw(camera, sun, true)); 130 | } 131 | 132 | raymarch(start, vec) 133 | { 134 | let len = Math.sqrt(vec[0] ** 2 + vec[1] ** 2 + vec[2] ** 2); 135 | let way = 0; 136 | let axis = 0; 137 | let voxpos = [0,0,0]; 138 | let step = [0,0,0]; 139 | let waydelta = [0,0,0]; 140 | let waynext = [0,0,0]; 141 | 142 | for(let i=0; i<3; i++) { 143 | voxpos[i] = Math.floor(start[i]); 144 | 145 | if(vec[i] > 0) { 146 | waydelta[i] = +len / vec[i]; 147 | waynext[i] = waydelta[i] * (voxpos[i] + 1 - start[i]); 148 | step[i] = +1; 149 | } 150 | else if(vec[i] < 0) { 151 | waydelta[i] = -len / vec[i]; 152 | waynext[i] = waydelta[i] * (start[i] - voxpos[i]); 153 | step[i] = -1; 154 | } 155 | else { 156 | waynext[i] = Infinity; 157 | } 158 | } 159 | 160 | while(true) { 161 | if(waynext[0] < waynext[1] && waynext[0] < waynext[2]) { 162 | axis = 0; 163 | } 164 | else if(waynext[1] < waynext[2]) { 165 | axis = 1; 166 | } 167 | else { 168 | axis = 2; 169 | } 170 | 171 | way = waynext[axis]; 172 | waynext[axis] += waydelta[axis]; 173 | voxpos[axis] += step[axis]; 174 | 175 | if(way >= len) { 176 | break; 177 | } 178 | 179 | if(this.getBlock(...voxpos) > 0) { 180 | return {axis, voxpos, step}; 181 | } 182 | } 183 | } 184 | 185 | boxmarch(boxmin, boxmax, vec) 186 | { 187 | let len = Math.sqrt(vec[0] ** 2 + vec[1] ** 2 + vec[2] ** 2); 188 | let way = 0; 189 | let axis = 0; 190 | let slope = 0; 191 | let voxmin = [0,0,0]; 192 | let voxmax = [0,0,0]; 193 | let leadvox = [0,0,0]; 194 | let trailvox = [0,0,0]; 195 | let step = [0,0,0]; 196 | let waydelta = [0,0,0]; 197 | let waynext = [0,0,0]; 198 | 199 | for(let i=0; i<3; i++) { 200 | voxmin[i] = Math.floor(boxmin[i]); 201 | voxmax[i] = Math.ceil(boxmax[i]) - 1; 202 | 203 | if(vec[i] > 0) { 204 | leadvox[i] = voxmax[i]; 205 | trailvox[i] = voxmin[i]; 206 | waydelta[i] = +len / vec[i]; 207 | waynext[i] = waydelta[i] * (voxmax[i] + 1 - boxmax[i]); 208 | step[i] = +1; 209 | } 210 | else if(vec[i] < 0) { 211 | leadvox[i] = voxmin[i]; 212 | trailvox[i] = voxmax[i]; 213 | waydelta[i] = -len / vec[i]; 214 | waynext[i] = waydelta[i] * (boxmin[i] - voxmin[i]); 215 | step[i] = -1; 216 | } 217 | else { 218 | leadvox[i] = voxmax[i]; 219 | trailvox[i] = voxmin[i]; 220 | waynext[i] = Infinity; 221 | step[i] = +1; 222 | } 223 | } 224 | 225 | while(true) { 226 | if(waynext[0] < waynext[1] && waynext[0] < waynext[2]) { 227 | axis = 0; 228 | } 229 | else if(waynext[1] < waynext[2]) { 230 | axis = 1; 231 | } 232 | else { 233 | axis = 2; 234 | } 235 | 236 | way = waynext[axis]; 237 | waynext[axis] += waydelta[axis]; 238 | leadvox[axis] += step[axis]; 239 | trailvox[axis] += step[axis]; 240 | 241 | if(way >= len) { 242 | break; 243 | } 244 | 245 | let xs = axis === 0 ? leadvox[0] : trailvox[0]; 246 | let ys = axis === 1 ? leadvox[1] : trailvox[1]; 247 | let zs = axis === 2 ? leadvox[2] : trailvox[2]; 248 | let xe = leadvox[0] + step[0]; 249 | let ye = leadvox[1] + step[1]; 250 | let ze = leadvox[2] + step[2]; 251 | 252 | for(let x = xs; x !== xe; x += step[0]) { 253 | for(let y = ys; y !== ye; y += step[1]) { 254 | for(let z = zs; z !== ze; z += step[2]) { 255 | if(this.getBlock(x, y, z) > 0) { 256 | let offs = way / len * vec[axis]; 257 | 258 | return {axis, offs, step: step[axis]}; 259 | } 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | export function radians(d) 2 | { 3 | return d * Math.PI / 180; 4 | } 5 | -------------------------------------------------------------------------------- /src/matrix.js: -------------------------------------------------------------------------------- 1 | export default class Matrix 2 | { 3 | constructor() 4 | { 5 | this.data = [ 6 | 1,0,0,0, 7 | 0,1,0,0, 8 | 0,0,1,0, 9 | 0,0,0,1, 10 | ]; 11 | } 12 | 13 | set( 14 | m00 = 1, m01 = 0, m02 = 0, m03 = 0, 15 | m10 = 0, m11 = 1, m12 = 0, m13 = 0, 16 | m20 = 0, m21 = 0, m22 = 1, m23 = 0, 17 | m30 = 0, m31 = 0, m32 = 0, m33 = 1, 18 | ) { 19 | let m = this.data; 20 | 21 | m[0] = m00; 22 | m[1] = m01; 23 | m[2] = m02; 24 | m[3] = m03; 25 | m[4] = m10; 26 | m[5] = m11; 27 | m[6] = m12; 28 | m[7] = m13; 29 | m[8] = m20; 30 | m[9] = m21; 31 | m[10] = m22; 32 | m[11] = m23; 33 | m[12] = m30; 34 | m[13] = m31; 35 | m[14] = m32; 36 | m[15] = m33; 37 | } 38 | 39 | perspective(fovy, aspect, near, far) 40 | { 41 | let fy = 1 / Math.tan(fovy / 2); 42 | let fx = fy / aspect; 43 | let nf = 1 / (near - far); 44 | let a = (near + far) * nf; 45 | let b = 2 * near * far * nf; 46 | 47 | this.set( 48 | fx, 0, 0, 0, 49 | 0,fy, 0, 0, 50 | 0, 0, a,-1, 51 | 0, 0, b, 0, 52 | ); 53 | } 54 | 55 | translate(x, y, z) 56 | { 57 | let m = this.data; 58 | 59 | m[12] += x * m[0] + y * m[4] + z * m[8]; 60 | m[13] += x * m[1] + y * m[5] + z * m[9]; 61 | m[14] += x * m[2] + y * m[6] + z * m[10]; 62 | m[15] += x * m[3] + y * m[7] + z * m[11]; 63 | } 64 | 65 | rotateX(a) 66 | { 67 | let m = this.data; 68 | let s = Math.sin(a); 69 | let c = Math.cos(a); 70 | let a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7]; 71 | let a20 = m[8], a21 = m[9], a22 = m[10], a23 = m[11]; 72 | 73 | m[4] = c * a10 + s * a20; 74 | m[5] = c * a11 + s * a21; 75 | m[6] = c * a12 + s * a22; 76 | m[7] = c * a13 + s * a23; 77 | m[8] = -s * a10 + c * a20; 78 | m[9] = -s * a11 + c * a21; 79 | m[10] = -s * a12 + c * a22; 80 | m[11] = -s * a13 + c * a23; 81 | } 82 | 83 | rotateY(a) 84 | { 85 | let m = this.data; 86 | let s = Math.sin(a); 87 | let c = Math.cos(a); 88 | let a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3]; 89 | let a20 = m[8], a21 = m[9], a22 = m[10], a23 = m[11]; 90 | 91 | m[0] = c * a00 + s * a20; 92 | m[1] = c * a01 + s * a21; 93 | m[2] = c * a02 + s * a22; 94 | m[3] = c * a03 + s * a23; 95 | m[8] = -s * a00 + c * a20; 96 | m[9] = -s * a01 + c * a21; 97 | m[10] = -s * a02 + c * a22; 98 | m[11] = -s * a03 + c * a23; 99 | } 100 | 101 | rotateZ(a) 102 | { 103 | let m = this.data; 104 | let s = Math.sin(a); 105 | let c = Math.cos(a); 106 | let a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3]; 107 | let a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7]; 108 | 109 | m[0] = c * a00 + s * a10; 110 | m[1] = c * a01 + s * a11; 111 | m[2] = c * a02 + s * a12; 112 | m[3] = c * a03 + s * a13; 113 | m[4] = -s * a00 + c * a10; 114 | m[5] = -s * a01 + c * a11; 115 | m[6] = -s * a02 + c * a12; 116 | m[7] = -s * a03 + c * a13; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/mesher.js: -------------------------------------------------------------------------------- 1 | import blocks from "./blocks.js"; 2 | 3 | let chunks = null; 4 | 5 | onmessage = e => { 6 | chunks = e.data.chunks; 7 | 8 | let cx = e.data.cx; 9 | let cy = e.data.cy; 10 | let mesh = remesh(false); 11 | let transmesh = remesh(true); 12 | 13 | postMessage({mesh, transmesh, cx, cy}); 14 | }; 15 | 16 | function getBlock(x, y, z) 17 | { 18 | if(z < 0 || z > 255) { 19 | return 0; 20 | } 21 | 22 | let cx = Math.floor(x / 16); 23 | let cy = Math.floor(y / 16); 24 | let ci = (cx + 1) + (cy + 1) * 3; 25 | let ch = chunks[ci]; 26 | 27 | x -= cx * 16; 28 | y -= cy * 16; 29 | 30 | return ch[x + y * 16 + z * 16 * 16]; 31 | } 32 | 33 | function remesh(meshTrans) 34 | { 35 | let mesh = []; 36 | 37 | remeshSide(mesh, -1, 0, 0, 0, meshTrans, // left 38 | 1, 2, 0, false, true, false, 39 | [-1,+1,+1], [-1, 0,+1], [-1,-1,+1], 40 | [-1,+1, 0], [-1,-1, 0], 41 | [-1,+1,-1], [-1, 0,-1], [-1,-1,-1]); 42 | remeshSide(mesh, 0,-1, 0, 1, meshTrans, // front 43 | 0, 2, 1, false, false, false, 44 | [-1,-1,+1], [ 0,-1,+1], [+1,-1,+1], 45 | [-1,-1, 0], [+1,-1, 0], 46 | [-1,-1,-1], [ 0,-1,-1], [+1,-1,-1]); 47 | remeshSide(mesh, 0, 0,-1, 2, meshTrans, // bottom 48 | 0, 1, 2, false, true, false, 49 | [-1,-1,-1], [ 0,-1,-1], [+1,-1,-1], 50 | [-1, 0,-1], [+1, 0,-1], 51 | [-1,+1,-1], [ 0,+1,-1], [+1,+1,-1]); 52 | remeshSide(mesh, +1, 0, 0, 3, meshTrans, // right 53 | 1, 2, 0, false, false, false, 54 | [+1,-1,+1], [+1, 0,+1], [+1,+1,+1], 55 | [+1,-1, 0], [+1,+1, 0], 56 | [+1,-1,-1], [+1, 0,-1], [+1,+1,-1]); 57 | remeshSide(mesh, 0,+1, 0, 4, meshTrans, // back 58 | 0, 2, 1, true, false, false, 59 | [+1,+1,+1], [ 0,+1,+1], [-1,+1,+1], 60 | [+1,+1, 0], [-1,+1, 0], 61 | [+1,+1,-1], [ 0,+1,-1], [-1,+1,-1]); 62 | remeshSide(mesh, 0, 0,+1, 5, meshTrans, // top 63 | 0, 1, 2, false, false, false, 64 | [-1,+1,+1], [ 0,+1,+1], [+1,+1,+1], 65 | [-1, 0,+1], [+1, 0,+1], 66 | [-1,-1,+1], [ 0,-1,+1], [+1,-1,+1]); 67 | 68 | return mesh; 69 | } 70 | 71 | function remeshSide( 72 | mesh, nx, ny, nz, fid, meshTrans, 73 | ax0, ax1, ax2, fx, fy, fz, aov0, aov1, aov2, aov3, aov4, aov5, aov6, aov7 74 | ) { 75 | let map = []; 76 | 77 | for(let z=0, i=0; z<256; z++) { 78 | for(let y=0; y<16; y++) { 79 | for(let x=0; x<16; x++, i++) { 80 | let block = getBlock(x, y, z); 81 | let adjacent = getBlock(x + nx, y + ny, z + nz); 82 | let transparent = blocks[block].transparent || false; 83 | let adjTrans = blocks[adjacent].transparent || false; 84 | 85 | if(meshTrans) { 86 | map[i] = ( 87 | block > 0 && transparent === true && adjacent !== block && adjTrans === true 88 | ? block 89 | : 0 90 | ); 91 | } 92 | else { 93 | map[i] = ( 94 | block > 0 && transparent === false && adjTrans === true 95 | ? block 96 | : 0 97 | ); 98 | } 99 | 100 | if(map[i] > 0) { 101 | let ao0 = getOcclusion( 102 | [x + aov3[0], y + aov3[1], z + aov3[2]], 103 | [x + aov5[0], y + aov5[1], z + aov5[2]], 104 | [x + aov6[0], y + aov6[1], z + aov6[2]], 105 | ); 106 | 107 | let ao1 = getOcclusion( 108 | [x + aov4[0], y + aov4[1], z + aov4[2]], 109 | [x + aov7[0], y + aov7[1], z + aov7[2]], 110 | [x + aov6[0], y + aov6[1], z + aov6[2]], 111 | ); 112 | 113 | let ao2 = getOcclusion( 114 | [x + aov3[0], y + aov3[1], z + aov3[2]], 115 | [x + aov0[0], y + aov0[1], z + aov0[2]], 116 | [x + aov1[0], y + aov1[1], z + aov1[2]], 117 | ); 118 | 119 | let ao3 = getOcclusion( 120 | [x + aov4[0], y + aov4[1], z + aov4[2]], 121 | [x + aov2[0], y + aov2[1], z + aov2[2]], 122 | [x + aov1[0], y + aov1[1], z + aov1[2]], 123 | ); 124 | 125 | map[i] |= ao0 << 8 | ao1 << 10 | ao2 << 12 | ao3 << 14; 126 | } 127 | } 128 | } 129 | } 130 | 131 | let a = [fx ? 15 : 0, fy ? 15 : 0, fz ? 255 : 0]; 132 | let b = [fx ? -1 : 16, fy ? -1 : 16, fz ? -1 : 256]; 133 | let s = [fx ? -1 : +1, fy ? -1 : +1, fz ? -1 : +1]; 134 | let i = [0,0,0]; 135 | let j = [0,0,0]; 136 | let k = [0,0,0]; 137 | 138 | for(i[ax2] = a[ax2]; i[ax2] !== b[ax2]; i[ax2] += s[ax2]) { 139 | for(i[ax1] = a[ax1]; i[ax1] !== b[ax1]; i[ax1] += s[ax1]) { 140 | for(i[ax0] = a[ax0]; i[ax0] !== b[ax0]; i[ax0] += s[ax0]) { 141 | let I = i[0] + i[1] * 16 + i[2] * 16 * 16; 142 | let val = map[I]; 143 | 144 | if(val > 0) { 145 | j[0] = i[0]; 146 | j[1] = i[1]; 147 | j[2] = i[2]; 148 | k[0] = i[0]; 149 | k[1] = i[1]; 150 | k[2] = i[2]; 151 | 152 | for(j[ax0] = i[ax0] + s[ax0]; j[ax0] !== b[ax0]; j[ax0] += s[ax0]) { 153 | let J = j[0] + j[1] * 16 + j[2] * 16 * 16; 154 | 155 | if(map[J] !== val) { 156 | break; 157 | } 158 | 159 | map[J] = 0; 160 | } 161 | 162 | outer: 163 | for(k[ax1] = i[ax1] + s[ax1]; k[ax1] !== b[ax1]; k[ax1] += s[ax1]) { 164 | for(k[ax0] = i[ax0]; k[ax0] !== j[ax0]; k[ax0] += s[ax0]) { 165 | let K = k[0] + k[1] * 16 + k[2] * 16 * 16; 166 | 167 | if(map[K] !== val) { 168 | break outer; 169 | } 170 | } 171 | 172 | for(k[ax0] = i[ax0]; k[ax0] !== j[ax0]; k[ax0] += s[ax0]) { 173 | let K = k[0] + k[1] * 16 + k[2] * 16 * 16; 174 | 175 | map[K] = 0; 176 | } 177 | } 178 | 179 | let block = val & 0xff; 180 | let face = blocks[block].faces[fid]; 181 | let ao0 = val >> 8 & 3; 182 | let ao1 = val >> 10 & 3; 183 | let ao2 = val >> 12 & 3; 184 | let ao3 = val >> 14 & 3; 185 | let quadw = (j[ax0] - i[ax0]) * s[ax0]; 186 | let quadh = (k[ax1] - i[ax1]) * s[ax1]; 187 | let v0 = [...i, nx, ny, nz, 0, quadh, face, ao0]; 188 | let v1 = [...i, nx, ny, nz, quadw, quadh, face, ao1]; 189 | let v2 = [...i, nx, ny, nz, 0, 0, face, ao2]; 190 | let v3 = [...i, nx, ny, nz, quadw, 0, face, ao3]; 191 | 192 | v1[ax0] = j[ax0]; 193 | v3[ax0] = j[ax0]; 194 | v2[ax1] = k[ax1]; 195 | v3[ax1] = k[ax1]; 196 | 197 | if(s[ax0] < 0) { 198 | v0[ax0] ++; 199 | v1[ax0] ++; 200 | v2[ax0] ++; 201 | v3[ax0] ++; 202 | } 203 | 204 | if(s[ax1] < 0) { 205 | v0[ax1] ++; 206 | v1[ax1] ++; 207 | v2[ax1] ++; 208 | v3[ax1] ++; 209 | } 210 | 211 | if(nx > 0 || ny > 0 || nz > 0) { 212 | v0[ax2] ++; 213 | v1[ax2] ++; 214 | v2[ax2] ++; 215 | v3[ax2] ++; 216 | } 217 | 218 | if(ao0 + ao3 < ao1 + ao2) { 219 | mesh.push(...v1, ...v3, ...v0, ...v0, ...v3, ...v2); 220 | } 221 | else { 222 | mesh.push(...v0, ...v1, ...v2, ...v2, ...v1, ...v3); 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | function getOcclusion(p0, p1, p2) 231 | { 232 | let ao0 = getBlock(...p0) > 0 ? 1 : 0; 233 | let ao1 = getBlock(...p1) > 0 ? 1 : 0; 234 | let ao2 = getBlock(...p2) > 0 ? 1 : 0; 235 | 236 | return ao0 > 0 && ao2 > 0 ? 3 : ao0 + ao1 + ao2; 237 | } 238 | -------------------------------------------------------------------------------- /src/mob.js: -------------------------------------------------------------------------------- 1 | import Body from "./body.js"; 2 | import Bone from "./bone.js"; 3 | 4 | export default class Mob extends Body 5 | { 6 | constructor(map, x, y, z, rx, rz, boxmin, boxmax, model) 7 | { 8 | super(map, x, y, z, rx, rz, boxmin, boxmax); 9 | 10 | this.model = model; 11 | this.bones = model.roots.map(root => new Bone(...root)); 12 | } 13 | 14 | update(delta) 15 | { 16 | super.update(delta); 17 | 18 | this.model.update(); 19 | this.bones.forEach(b => b.update()); 20 | } 21 | 22 | draw(camera, sun) 23 | { 24 | this.model.draw(camera, sun, this.mat, this.bones.map(b => b.mat)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | import Shader from "./shader.js"; 2 | import Buffer from "./buffer.js"; 3 | 4 | let vert = ` 5 | uniform mat4 proj; 6 | uniform mat4 view; 7 | uniform mat4 model; 8 | uniform vec3 sun; 9 | uniform mat4 bones[16]; 10 | attribute vec3 pos; 11 | attribute vec3 norm; 12 | attribute vec2 uv; 13 | attribute float bone; 14 | varying mediump vec2 vUv; 15 | varying mediump float factor; 16 | 17 | void main() 18 | { 19 | gl_Position = vec4(pos, 1); 20 | 21 | vec4 normal = vec4(norm, 0); 22 | 23 | for(int i=0; i<16; i++) { 24 | if(i + 1 == int(bone)) { 25 | gl_Position = bones[i] * gl_Position; 26 | normal = bones[i] * normal; 27 | } 28 | } 29 | 30 | normal = model * normal; 31 | 32 | gl_Position = proj * view * model * gl_Position; 33 | vUv = uv; 34 | 35 | float diffuse = max(0.0, dot(sun, normal.xyz)); 36 | float ambient = 1.0; 37 | 38 | factor = mix(diffuse, ambient, 0.5); 39 | } 40 | `; 41 | 42 | let frag = ` 43 | uniform sampler2D tex; 44 | varying mediump vec2 vUv; 45 | varying mediump float factor; 46 | 47 | void main() 48 | { 49 | gl_FragColor = texture2D(tex, vUv); 50 | gl_FragColor.rgb *= factor; 51 | } 52 | `; 53 | 54 | export default class Model 55 | { 56 | constructor(display, texture, roots) 57 | { 58 | this.gl = display.gl; 59 | this.display = display; 60 | this.shader = display.getCached("Model.shader", () => new Shader(display, vert, frag)); 61 | this.texture = texture; 62 | this.buffer = new Buffer(display); 63 | this.mesh = []; 64 | this.invalid = false; 65 | this.boneCount = 0; 66 | this.roots = roots; 67 | } 68 | 69 | addQuad(v0, v1, v2, v3, n, uvStart, uvSize, bone) 70 | { 71 | let uv00 = [uvStart[0], uvStart[1] + uvSize[1]]; 72 | let uv01 = [uvStart[0], uvStart[1]]; 73 | let uv10 = [uvStart[0] + uvSize[0], uvStart[1] + uvSize[1]]; 74 | let uv11 = [uvStart[0] + uvSize[0], uvStart[1]]; 75 | 76 | this.mesh.push(...v0, ...n, ...uv00, bone); 77 | this.mesh.push(...v1, ...n, ...uv10, bone); 78 | this.mesh.push(...v2, ...n, ...uv01, bone); 79 | this.mesh.push(...v2, ...n, ...uv01, bone); 80 | this.mesh.push(...v1, ...n, ...uv10, bone); 81 | this.mesh.push(...v3, ...n, ...uv11, bone); 82 | 83 | this.invalid = true; 84 | this.boneCount = Math.max(this.boneCount, bone); 85 | } 86 | 87 | addCube(start, size, texpos, texbox, div, bone) 88 | { 89 | let end = [start[0] + size[0], start[1] + size[1], start[2] + size[2]]; 90 | let v000 = start; 91 | let v001 = [start[0], start[1], end[2]]; 92 | let v010 = [start[0], end[1], start[2]]; 93 | let v011 = [start[0], end[1], end[2]]; 94 | let v100 = [end[0], start[1], start[2]]; 95 | let v101 = [end[0], start[1], end[2]]; 96 | let v110 = [end[0], end[1], start[2]]; 97 | let v111 = end; 98 | let u = texpos[0]; 99 | let v = texpos[1]; 100 | let sx = texbox[0]; 101 | let sy = texbox[1]; 102 | let sz = texbox[2]; 103 | 104 | // left 105 | this.addQuad(v010, v000, v011, v001, [-1, 0, 0], [ (2*sx+sy+u)/div, v/div], [sy/div, sz/div], bone); 106 | 107 | // front 108 | this.addQuad(v000, v100, v001, v101, [ 0,-1, 0], [ u/div, v/div], [sx/div, sz/div], bone); 109 | 110 | // bottom 111 | this.addQuad(v010, v110, v000, v100, [ 0, 0,-1], [(2*sx+2*sy+u)/div, (sy+v)/div], [sx/div, sy/div], bone); 112 | 113 | // right 114 | this.addQuad(v100, v110, v101, v111, [+1, 0, 0], [ (sx+u)/div, v/div], [sy/div, sz/div], bone); 115 | 116 | // back 117 | this.addQuad(v110, v010, v111, v011, [ 0,+1, 0], [ (sx+sy+u)/div, v/div], [sx/div, sz/div], bone); 118 | 119 | // top 120 | this.addQuad(v001, v101, v011, v111, [ 0, 0,+1], [(2*sx+2*sy+u)/div, v/div], [sx/div, sy/div], bone); 121 | } 122 | 123 | update() 124 | { 125 | if(this.invalid) { 126 | this.buffer.update(new Float32Array(this.mesh)); 127 | this.invalid = false; 128 | } 129 | } 130 | 131 | draw(camera, sun, modelMat, bones) 132 | { 133 | let shader = this.shader; 134 | let buffer = this.buffer; 135 | 136 | shader.assignFloatAttrib("pos", buffer, 3, 9, 0); 137 | shader.assignFloatAttrib("norm", buffer, 3, 9, 3); 138 | shader.assignFloatAttrib("uv", buffer, 2, 9, 6); 139 | shader.assignFloatAttrib("bone", buffer, 1, 9, 8); 140 | shader.use(); 141 | shader.assignMatrix("proj", camera.proj); 142 | shader.assignMatrix("view", camera.view); 143 | shader.assignMatrix("model", modelMat); 144 | shader.assignMatrices("bones", bones); 145 | shader.assignVector("sun", sun); 146 | shader.assignTexture("tex", this.texture, 0); 147 | 148 | this.display.drawTriangles(this.mesh.length / 9); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/picker.js: -------------------------------------------------------------------------------- 1 | import Shader from "./shader.js"; 2 | import Buffer from "./buffer.js"; 3 | import Matrix from "./matrix.js"; 4 | import {radians} from "./math.js"; 5 | 6 | let vert = ` 7 | uniform mat4 proj; 8 | uniform mat4 view; 9 | uniform mat4 model; 10 | attribute vec3 pos; 11 | varying mediump vec3 vPos; 12 | 13 | void main() 14 | { 15 | gl_Position = proj * view * model * vec4(pos, 1); 16 | vPos = pos; 17 | } 18 | `; 19 | 20 | let frag = ` 21 | varying mediump vec3 vPos; 22 | 23 | void main() 24 | { 25 | if(vPos.x < 0.0625 || vPos.x > 0.9375 || vPos.y < 0.0625 || vPos.y > 0.9375) { 26 | gl_FragColor = vec4(1, 1, 1, 0.5); 27 | } 28 | else { 29 | gl_FragColor = vec4(0); 30 | } 31 | } 32 | `; 33 | 34 | export default class Picker 35 | { 36 | constructor(display, map) 37 | { 38 | this.display = display; 39 | this.map = map; 40 | this.shader = new Shader(display, vert, frag); 41 | this.buffer = new Buffer(display, new Float32Array([0,0,0, 1,0,0, 0,1,0, 0,1,0, 1,0,0, 1,1,0])); 42 | this.model = new Matrix(); 43 | this.hasHit = false; 44 | this.hitVox = null; 45 | } 46 | 47 | pick(pos, vec, len) 48 | { 49 | let scaledVec = vec.clone(); 50 | 51 | scaledVec.scale(len); 52 | 53 | let hit = this.map.raymarch(pos.data, scaledVec.data); 54 | 55 | if(hit) { 56 | this.hasHit = true; 57 | this.hitVox = hit.voxpos; 58 | this.model.set(); 59 | this.model.translate(...hit.voxpos); 60 | 61 | if(hit.axis === 0) { 62 | if(hit.step[0] < 0) { 63 | this.model.translate(1,0,0); 64 | } 65 | 66 | this.model.rotateY(radians(90)); 67 | } 68 | else if(hit.axis === 1) { 69 | if(hit.step[1] < 0) { 70 | this.model.translate(0,1,0); 71 | } 72 | 73 | this.model.rotateX(radians(90)); 74 | } 75 | else { 76 | if(hit.step[2] < 0) { 77 | this.model.translate(0,0,1); 78 | } 79 | } 80 | } 81 | else { 82 | this.hasHit = false; 83 | } 84 | } 85 | 86 | draw(camera) 87 | { 88 | if(this.hasHit) { 89 | let shader = this.shader; 90 | let gl = this.display.gl; 91 | 92 | shader.assignFloatAttrib("pos", this.buffer, 3, 3, 0); 93 | shader.use(); 94 | shader.assignMatrix("proj", camera.proj); 95 | shader.assignMatrix("view", camera.view); 96 | shader.assignMatrix("model", this.model); 97 | 98 | gl.disable(gl.DEPTH_TEST); 99 | gl.disable(gl.CULL_FACE); 100 | 101 | this.display.drawTriangles(6); 102 | 103 | gl.enable(gl.DEPTH_TEST); 104 | gl.enable(gl.CULL_FACE); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | export default class Server 2 | { 3 | constructor() 4 | { 5 | this.others = new Set(); 6 | 7 | this.socket = new WebSocket( 8 | "ws://" + window.location.hostname + ":54321" 9 | ); 10 | 11 | this.socket.onopen = e => { 12 | this.isopen = true; 13 | this.queue.forEach(fn => fn()); 14 | this.queue = []; 15 | }; 16 | 17 | this.socket.onmessage = e => { 18 | if(typeof e.data === "string") { 19 | let msg = JSON.parse(e.data); 20 | 21 | if(msg.msg === 2) { 22 | this.onSetBlock(msg.x, msg.y, msg.z, msg.block); 23 | } 24 | else if(msg.msg === 3) { 25 | this.others.add(msg.id); 26 | this.onAddPlayer(msg.id); 27 | } 28 | else if(msg.msg === 4) { 29 | this.others.delete(msg.id); 30 | this.onRemovePlayer(msg.id); 31 | } 32 | else if(msg.msg === 6) { 33 | this.onSetPlayerPos(msg.id, msg.x, msg.y, msg.z, msg.rx, msg.rz); 34 | } 35 | } 36 | else { 37 | e.data.arrayBuffer().then(buf => { 38 | let f64 = new Float64Array(buf); 39 | let msg = f64[0]; 40 | 41 | if(msg === 1) { 42 | this.onSetChunk(f64[1], f64[2], new Uint8Array(buf, 3 * 8)); 43 | } 44 | }); 45 | } 46 | }; 47 | 48 | this.isopen = false; 49 | this.queue = []; 50 | this.onSetChunk = () => {}; 51 | this.onSetBlock = () => {}; 52 | this.onAddPlayer = () => {}; 53 | this.onRemovePlayer = () => {}; 54 | this.onSetPlayerPos = () => {}; 55 | } 56 | 57 | send(msg) 58 | { 59 | if(this.isopen) { 60 | this.socket.send(JSON.stringify(msg)); 61 | } 62 | else { 63 | this.queue.push(() => this.send(msg)); 64 | } 65 | } 66 | 67 | getChunk(x, y) 68 | { 69 | this.send({msg: 0, x, y}); 70 | } 71 | 72 | setBlock(x, y, z, block) 73 | { 74 | this.send({msg: 2, x, y, z, block}); 75 | } 76 | 77 | setMyPos(x, y, z, rx, rz) 78 | { 79 | this.send({msg: 5, x, y, z, rx, rz}); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/shader.js: -------------------------------------------------------------------------------- 1 | export default class Shader 2 | { 3 | constructor(display, vertSrc, fragSrc) 4 | { 5 | let gl = display.gl; 6 | let vert = gl.createShader(gl.VERTEX_SHADER); 7 | 8 | gl.shaderSource(vert, vertSrc); 9 | gl.compileShader(vert); 10 | 11 | console.log("compile vertex shader:", gl.getShaderInfoLog(vert)); 12 | 13 | let frag = gl.createShader(gl.FRAGMENT_SHADER); 14 | 15 | gl.shaderSource(frag, fragSrc); 16 | gl.compileShader(frag); 17 | 18 | console.log("compile fragment shader:", gl.getShaderInfoLog(frag)); 19 | 20 | let prog = gl.createProgram(); 21 | 22 | gl.attachShader(prog, vert); 23 | gl.attachShader(prog, frag); 24 | gl.linkProgram(prog); 25 | 26 | console.log("link shader program:", gl.getProgramInfoLog(prog)); 27 | 28 | this.gl = gl; 29 | this.prog = prog; 30 | } 31 | 32 | assignFloatAttrib(name, buf, size, stride, offset) 33 | { 34 | let gl = this.gl; 35 | let loca = gl.getAttribLocation(this.prog, name); 36 | 37 | gl.bindBuffer(gl.ARRAY_BUFFER, buf.buf); 38 | gl.enableVertexAttribArray(loca); 39 | gl.vertexAttribPointer(loca, size, gl.FLOAT, false, stride * 4, offset * 4); 40 | } 41 | 42 | assignVector(name, vec) 43 | { 44 | let gl = this.gl; 45 | let loca = gl.getUniformLocation(this.prog, name); 46 | 47 | gl.uniform3fv(loca, vec.data); 48 | } 49 | 50 | assignMatrix(name, mat) 51 | { 52 | let gl = this.gl; 53 | let loca = gl.getUniformLocation(this.prog, name); 54 | 55 | gl.uniformMatrix4fv(loca, false, mat.data); 56 | } 57 | 58 | assignMatrices(name, mats) 59 | { 60 | let gl = this.gl; 61 | 62 | mats.forEach((mat, i) => { 63 | let loca = gl.getUniformLocation(this.prog, name + "[" + i + "]"); 64 | 65 | gl.uniformMatrix4fv(loca, false, mat.data); 66 | }); 67 | } 68 | 69 | assignTexture(name, tex, unit) 70 | { 71 | let gl = this.gl; 72 | let loca = gl.getUniformLocation(this.prog, name); 73 | 74 | gl.activeTexture(gl.TEXTURE0 + unit); 75 | gl.bindTexture(gl.TEXTURE_2D, tex.tex); 76 | gl.uniform1i(loca, unit); 77 | } 78 | 79 | use() 80 | { 81 | this.gl.useProgram(this.prog); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/sky.js: -------------------------------------------------------------------------------- 1 | import Buffer from "./buffer.js"; 2 | import Shader from "./shader.js"; 3 | 4 | const verts = new Float32Array([ 5 | -1,+1,-1, +1,+1,-1, -1,+1,+1, -1,+1,+1, +1,+1,-1, +1,+1,+1, // back 6 | +1,-1,-1, -1,-1,-1, +1,-1,+1, +1,-1,+1, -1,-1,-1, -1,-1,+1, // front 7 | -1,-1,-1, -1,+1,-1, -1,-1,+1, -1,-1,+1, -1,+1,-1, -1,+1,+1, // left 8 | +1,+1,-1, +1,-1,-1, +1,+1,+1, +1,+1,+1, +1,-1,-1, +1,-1,+1, // right 9 | -1,-1,-1, +1,-1,-1, -1,+1,-1, -1,+1,-1, +1,-1,-1, +1,+1,-1, // bottom 10 | -1,+1,+1, +1,+1,+1, -1,-1,+1, -1,-1,+1, +1,+1,+1, +1,-1,+1, // top 11 | ]); 12 | 13 | const vert = ` 14 | uniform mat4 proj; 15 | uniform mat4 view; 16 | uniform mat4 model; 17 | attribute vec3 pos; 18 | varying mediump vec3 vPos; 19 | 20 | void main() 21 | { 22 | gl_Position = proj * view * model * vec4(pos, 1); 23 | vPos = pos; 24 | } 25 | `; 26 | 27 | const frag = ` 28 | varying mediump vec3 vPos; 29 | 30 | void main() 31 | { 32 | mediump vec3 norm = normalize(vPos); 33 | 34 | gl_FragColor = mix( 35 | vec4(0.5, 0.75, 1.0, 1), 36 | vec4(0.125, 0.25, 0.5, 1), 37 | clamp(norm.z, 0.0, 1.0) 38 | ); 39 | } 40 | `; 41 | 42 | export default class Sky 43 | { 44 | constructor(display) 45 | { 46 | this.buffer = new Buffer(display, verts); 47 | this.shader = new Shader(display, vert, frag); 48 | this.display = display; 49 | } 50 | 51 | draw(camera) 52 | { 53 | let shader = this.shader; 54 | let buffer = this.buffer; 55 | let gl = this.display.gl; 56 | 57 | shader.assignFloatAttrib("pos", buffer, 3, 3, 0); 58 | shader.use(); 59 | shader.assignMatrix("proj", camera.proj); 60 | shader.assignMatrix("view", camera.view); 61 | shader.assignMatrix("model", camera.model); 62 | 63 | gl.disable(gl.DEPTH_TEST); 64 | 65 | this.display.drawTriangles(verts.length / 3); 66 | 67 | gl.enable(gl.DEPTH_TEST); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/speaker.js: -------------------------------------------------------------------------------- 1 | export default class Speaker 2 | { 3 | constructor() 4 | { 5 | this.ctx = new AudioContext(); 6 | } 7 | 8 | activate() 9 | { 10 | this.ctx.resume(); 11 | } 12 | 13 | async loadSound(url) 14 | { 15 | let response = await fetch(url); 16 | let buffer = await response.arrayBuffer(); 17 | 18 | return await this.ctx.decodeAudioData(buffer); 19 | } 20 | 21 | playSound(sound) 22 | { 23 | let source = this.ctx.createBufferSource(); 24 | 25 | source.buffer = sound; 26 | source.connect(this.ctx.destination); 27 | source.start(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/texture.js: -------------------------------------------------------------------------------- 1 | export default class Texture 2 | { 3 | constructor(display, src) 4 | { 5 | let gl = display.gl; 6 | let tex = gl.createTexture(); 7 | 8 | gl.bindTexture(gl.TEXTURE_2D, tex); 9 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 10 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 11 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 12 | 13 | let img = document.createElement("img"); 14 | 15 | img.src = src; 16 | 17 | img.onload = () => { 18 | gl.bindTexture(gl.TEXTURE_2D, tex); 19 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 20 | }; 21 | 22 | this.tex = tex; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/vector.js: -------------------------------------------------------------------------------- 1 | export default class Vector 2 | { 3 | constructor(x = 0, y = 0, z = 0) 4 | { 5 | this.data = [x, y, z]; 6 | } 7 | 8 | get x() 9 | { 10 | return this.data[0]; 11 | } 12 | 13 | get y() 14 | { 15 | return this.data[1]; 16 | } 17 | 18 | get z() 19 | { 20 | return this.data[2]; 21 | } 22 | 23 | set(x = 0, y = 0, z = 0) 24 | { 25 | this.data[0] = x; 26 | this.data[1] = y; 27 | this.data[2] = z; 28 | } 29 | 30 | add(other) 31 | { 32 | this.data[0] += other.data[0]; 33 | this.data[1] += other.data[1]; 34 | this.data[2] += other.data[2]; 35 | } 36 | 37 | addScaled(other, scale) 38 | { 39 | this.data[0] += other.data[0] * scale; 40 | this.data[1] += other.data[1] * scale; 41 | this.data[2] += other.data[2] * scale; 42 | } 43 | 44 | scale(s) 45 | { 46 | this.data[0] *= s; 47 | this.data[1] *= s; 48 | this.data[2] *= s; 49 | } 50 | 51 | rotateX(a) 52 | { 53 | let v = this.data; 54 | let s = Math.sin(a); 55 | let c = Math.cos(a); 56 | let y = v[1]; 57 | let z = v[2]; 58 | v[1] = y * c - z * s; 59 | v[2] = y * s + z * c; 60 | } 61 | 62 | rotateZ(a) 63 | { 64 | let v = this.data; 65 | let s = Math.sin(a); 66 | let c = Math.cos(a); 67 | let x = v[0]; 68 | let y = v[1]; 69 | v[0] = x * c - y * s; 70 | v[1] = x * s + y * c; 71 | } 72 | 73 | clone() 74 | { 75 | return new Vector(this.data[0], this.data[1], this.data[2]); 76 | } 77 | } 78 | --------------------------------------------------------------------------------