diff options
Diffstat (limited to '')
| -rw-r--r-- | js/chunk.js | 655 | ||||
| -rw-r--r-- | js/controller.js | 182 | ||||
| -rw-r--r-- | js/minecraft.js | 55 | ||||
| -rw-r--r-- | js/noise.js | 164 | ||||
| -rw-r--r-- | js/wgpu/core/Buffer.js | 50 | ||||
| -rw-r--r-- | js/wgpu/core/Mesh.js | 48 | ||||
| -rw-r--r-- | js/wgpu/core/Object.js | 105 | ||||
| -rw-r--r-- | js/wgpu/core/Renderer.js | 172 | ||||
| -rw-r--r-- | js/wgpu/core/Shader.js | 289 | ||||
| -rw-r--r-- | js/wgpu/core/Texture.js | 51 | ||||
| -rw-r--r-- | js/wgpu/io/File.js | 11 | ||||
| -rw-r--r-- | js/wgpu/io/Input.js | 62 | ||||
| -rw-r--r-- | js/wgpu/math/Mat4.js | 126 | ||||
| -rw-r--r-- | js/wgpu/math/Math.js | 3 | ||||
| -rw-r--r-- | js/wgpu/math/Vec2.js | 103 | ||||
| -rw-r--r-- | js/wgpu/math/Vec3.js | 156 | ||||
| -rw-r--r-- | js/wgpu/math/Vec4.js | 132 | ||||
| -rw-r--r-- | js/wgpu/wgpu.js | 84 | ||||
| -rw-r--r-- | js/world.js | 116 |
19 files changed, 2564 insertions, 0 deletions
diff --git a/js/chunk.js b/js/chunk.js new file mode 100644 index 0000000..a30360f --- /dev/null +++ b/js/chunk.js @@ -0,0 +1,655 @@ +import { height2d } from "./noise.js"; +import { Mesh, Vec2, Vec3 } from "./wgpu/wgpu.js"; + +// BLOCK TYPE +export const INVALID = -1; +export const AIR = 0; +export const DIRT = 1; +export const GRASS = 2; +export const FULL_GRASS = 3; +export const STONE = 4; +export const SNOW = 5; +export const SAND = 6; +export const WATER = 7; + +// CHUNK STATE +export const CHUNK_NEW = 0; +export const CHUNK_GENERATING = 1; +export const CHUNK_GENERATED = 2; +export const CHUNK_MESHING = 3; +export const CHUNK_DONE = 4; + +export const WATER_LEVEL = 20; + +export class Chunk { + + static chunks = {} + static size = new Vec3(16,64,16); + + constructor(gridX, gridZ, seed) { + this.gridX = gridX; + this.gridZ = gridZ; + this.seed = seed; + + this.id = Chunk.id(gridX, gridZ); + Chunk.chunks[this.id] = this; + + this.cubes = []; + this.state = CHUNK_NEW; + } + + static id(gridX, gridZ) { + return `${gridX},${gridZ}` + } + + static get(gridX, gridZ) { + let id = Chunk.id(gridX, gridZ); + return Chunk.chunks[id] ?? null + } + + static delete(gridX, gridZ) { + let id = Chunk.id(gridX, gridZ); + let chunk = Chunk.chunks[id]; + if (chunk) { + chunk.delete(); + } + } + + delete() { + if (this.mesh) { + this.mesh.delete(); + } + delete Chunk.chunks[this.id]; + } + + static isGenerated(gridX, gridZ) { + let chunk = Chunk.get(gridX, gridZ); + if (!chunk) { + return false; + } + return chunk.state >= CHUNK_GENERATED; + } + + static isMeshed(gridX, gridZ) { + let chunk = Chunk.get(gridX, gridZ); + if (!chunk) { + return false; + } + return !!chunk.mesh; + } + + static forEach(cb) { + Object.values(Chunk.chunks).forEach(cb); + } + + getBlock(x, y, z) { + if (y >= Chunk.size.y) + return AIR; + if (y < 0) + return INVALID; + let chunkX = this.gridX; + let chunkZ = this.gridZ; + if (x < 0) { + chunkX--; + } else if (x > Chunk.size.x - 1) { + chunkX++; + } + if (z < 0) { + chunkZ--; + } else if (z > Chunk.size.z - 1) { + chunkZ++; + } + x = (x + Chunk.size.x) % Chunk.size.x; + z = (z + Chunk.size.z) % Chunk.size.z; + let index = cubes_index(x, y, z); + if (chunkX == this.gridX && chunkZ == this.gridZ) { + return this.cubes[index]; + } else { + let chunk = Chunk.get(chunkX, chunkZ); + if (!chunk) { + return INVALID; + } + return chunk.cubes[index]; + } + } + + setBlock(x, y, z, block, reMesh = true) { + if (x < 0 || x >= Chunk.size.x) return; + if (y < 0 || y >= Chunk.size.y) return; + if (z < 0 || z >= Chunk.size.z) return; + let index = cubes_index(x, y, z); + this.cubes[index] = block; + + // now update chunks to remesh + if (!reMesh) + return; + + Chunk.markChunk(this.gridX, this.gridZ); + if (x == 0) + Chunk.markChunk(this.gridX - 1, this.gridZ); + if (x == Chunk.size.x - 1) + Chunk.markChunk(this.gridX + 1, this.gridZ); + if (z == 0) + Chunk.markChunk(this.gridX, this.gridZ - 1); + if (z == Chunk.size.z - 1) + Chunk.markChunk(this.gridX, this.gridZ + 1); + } + + static markChunk(gridX, gridZ) { + let chunk = Chunk.get(gridX, gridZ); + if (!chunk) + return; + if (chunk.state < CHUNK_GENERATED) + return; + chunk.state = CHUNK_GENERATED; + } + + static getGlobalBlock(x, y, z) { + if (y >= Chunk.size.y) + return AIR; + if (y < 0) + return INVALID; + let gridX = Math.floor(x / Chunk.size.x); + let gridZ = Math.floor(z / Chunk.size.z); + let chunk = Chunk.get(gridX, gridZ); + if (!chunk) + return INVALID; + let localX = x - (gridX * Chunk.size.x); + let localZ = z - (gridZ * Chunk.size.z); + return chunk.getBlock(localX, y, localZ); + } + + static setGlobalBlock(x, y, z, block) { + if (y < 0 || y >= Chunk.size.y) + return; + let gridX = Math.floor(x / Chunk.size.x); + let gridZ = Math.floor(z / Chunk.size.z); + let chunk = Chunk.get(gridX, gridZ); + if (!chunk) + return; + let localX = x - (gridX * Chunk.size.x); + let localZ = z - (gridZ * Chunk.size.z); + chunk.setBlock(localX, y, localZ, block); + } + + generate() { + // only create a mesh if this chunk has just + // been freshly created + if (this.state != CHUNK_NEW) + return; + + this.state = CHUNK_GENERATING; + + let me = this; + let size = Chunk.size.x * Chunk.size.y * Chunk.size.z; + this.cubes = new Uint8Array(size).fill(AIR); + + function getBlock(y, height) { + // fill in water table + if (y > height) { + if (height > WATER_LEVEL) + return AIR; + else + return WATER; + } + + let layer = height - WATER_LEVEL; + + // beach + if (layer <= 3) { + return SAND; + } + + // plains + else if (layer <= 8) { + let difference = height - y; + if (difference == 0) + return GRASS; + else if (difference <= 3) + return DIRT; + else + return STONE; + } + + // mountains + else { + let difference = height - y; + if (difference == 0) + return SNOW; + else + return STONE; + } + } + + function generateAt(x, z) { + let globalX = x + me.gridX * Chunk.size.x; + let globalZ = z + me.gridZ * Chunk.size.z; + + // get perlin height + let opts = { + frequency: 1.0, + amplitude: Chunk.size.y, + octaves: 1, + scale: 0.05, + }; + let height = height2d(globalX, globalZ, me.seed, opts); + + let maxY = Math.max(height, WATER_LEVEL); + for (let y = 0; y <= maxY; y++) { + let block = getBlock(y, height); + me.setBlock(x, y, z, block, false); + } + } + + for (let x = 0; x < Chunk.size.x; x++) { + for (let z = 0; z < Chunk.size.z; z++) { + generateAt(x, z); + } + } + + this.state = CHUNK_GENERATED; + } + + async generateAsync() { + this.generate() + } + + createMesh() { + // we do not support updating chunks on changes yet, + // so wait till the neighbors are all generated + if (!Chunk.isGenerated(this.gridX - 1, this.gridZ) || + !Chunk.isGenerated(this.gridX + 1, this.gridZ) || + !Chunk.isGenerated(this.gridX, this.gridZ - 1) || + !Chunk.isGenerated(this.gridX, this.gridZ + 1)) + return; + + // only create a mesh if this chunk has just + // been freshly generated + if (this.state != CHUNK_GENERATED) + return; + + this.state = CHUNK_MESHING; + + let mesher = new SimpleMesher(this); + mesher.createMesh(); + let mesh = mesher.finish(); + + this.mesh = mesh; + this.state = CHUNK_MESHING; + } + + async createMeshAsync() { + this.createMesh() + } + +} + +// stores a block id and direction for a quad face +class Mask { + constructor(block, normal) { + this.block = block; + this.normal = normal; + } + + compare(other) { + if (this.block == INVALID || other.block == INVALID) { + return false; + } + return this.block == other.block && this.normal == other.normal; + } +} + +// cube faces used for simple mesher +const CUBE = { + PX: [ + new Vec3(1,1,1), + new Vec3(1,0,1), + new Vec3(1,0,0), + new Vec3(1,0,0), + new Vec3(1,1,0), + new Vec3(1,1,1), + ], + + NX: [ + new Vec3(0,1,0), + new Vec3(0,0,0), + new Vec3(0,0,1), + new Vec3(0,0,1), + new Vec3(0,1,1), + new Vec3(0,1,0), + ], + + PY: [ + new Vec3(1,1,0), + new Vec3(0,1,0), + new Vec3(0,1,1), + new Vec3(0,1,1), + new Vec3(1,1,1), + new Vec3(1,1,0), + ], + + NY: [ + new Vec3(0,0,1), + new Vec3(0,0,0), + new Vec3(1,0,0), + new Vec3(1,0,0), + new Vec3(1,0,1), + new Vec3(0,0,1), + ], + + PZ: [ + new Vec3(0,1,1), + new Vec3(0,0,1), + new Vec3(1,0,1), + new Vec3(1,0,1), + new Vec3(1,1,1), + new Vec3(0,1,1), + ], + + NZ: [ + new Vec3(1,1,0), + new Vec3(1,0,0), + new Vec3(0,0,0), + new Vec3(0,0,0), + new Vec3(0,1,0), + new Vec3(1,1,0), + ], +}; + +const MATS = { + [AIR]: null, + [DIRT]: [0, 0, 0, 0, 0, 0], + [GRASS]: [1, 1, 0, 2, 1, 1], + [FULL_GRASS]: [2, 2, 2, 2, 2, 2], + [STONE]: [3, 3, 3, 3, 3, 3], + [SNOW]: [4, 4, 4, 4, 4, 4], + [SAND]: [5, 5, 5, 5, 5, 5], + [WATER]: [6, 6, 6, 6, 6, 6], +}; + +class SimpleMesher { + constructor(chunk) { + this.chunk = chunk; + this.positions = []; + this.normals = []; + this.uvs = []; + this.blocks = []; + this.materials = []; + } + + /** + * @param {Vec3} pos + * @param {Vec3} normal + * @param {Vec2} uv + * @param {Vec2} block + */ + addVertex(pos, normal, uv, block, material) { + this.positions.push(pos.x); + this.positions.push(pos.y); + this.positions.push(pos.z); + this.normals.push(normal.x); + this.normals.push(normal.y); + this.normals.push(normal.z); + this.uvs.push(uv.x); + this.uvs.push(uv.y); + this.blocks.push(block.x); + this.blocks.push(block.y); + this.blocks.push(block.z); + this.materials.push(material); + } + + /** + * @param {Array} face + * @param {Vec3} normal + * @param {Vec3} x + * @param {Vec3} y + * @param {Vec3} z + * @param {Number} uvIdx + */ + addQuad(face, normal, x, y, z, block, material) { + + let uv = []; + uv[0] = new Vec2(1, 1); + uv[1] = new Vec2(1, 0); + uv[2] = new Vec2(0, 1); + uv[3] = new Vec2(0, 0); + + let pos = []; + pos[0] = face[0].addV(new Vec3(x, y, z)); + pos[1] = face[1].addV(new Vec3(x, y, z)); + pos[2] = face[2].addV(new Vec3(x, y, z)); + pos[3] = face[3].addV(new Vec3(x, y, z)); + pos[4] = face[4].addV(new Vec3(x, y, z)); + pos[5] = face[5].addV(new Vec3(x, y, z)); + + this.addVertex(pos[0], normal, uv[3], block, material); + this.addVertex(pos[1], normal, uv[2], block, material); + this.addVertex(pos[2], normal, uv[0], block, material); + this.addVertex(pos[3], normal, uv[0], block, material); + this.addVertex(pos[4], normal, uv[1], block, material); + this.addVertex(pos[5], normal, uv[3], block, material); + } + + createMesh() { + let size = Chunk.size.x * Chunk.size.y * Chunk.size.z; + for (let i = 0; i < size; i++) { + let x = i % Chunk.size.x; + let z = Math.floor(i / Chunk.size.x) % Chunk.size.z; + let y = Math.floor(i / (Chunk.size.x * Chunk.size.z)); + + let self = this.chunk.getBlock(x, y, z); + if (self == INVALID || self == AIR) + continue; + + function check(b) { + return b == INVALID || b == AIR + } + + let xOff = this.chunk.gridX * Chunk.size.x; + let zOff = this.chunk.gridZ * Chunk.size.z; + let block = new Vec3(x + xOff, y, z + zOff); + let material = MATS[self]; + + let nx = this.chunk.getBlock(x - 1, y, z); + if (check(nx)) + this.addQuad(CUBE.NX, new Vec3(-1, 0, 0), x, y, z, block, material[0]); + + let px = this.chunk.getBlock(x + 1, y, z); + if (check(px)) + this.addQuad(CUBE.PX, new Vec3(1, 0, 0), x, y, z, block, material[1]); + + let ny = this.chunk.getBlock(x, y - 1, z); + if (check(ny)) + this.addQuad(CUBE.NY, new Vec3(0, -1, 0), x, y, z, block, material[2]); + + let py = this.chunk.getBlock(x, y + 1, z); + if (check(py)) + this.addQuad(CUBE.PY, new Vec3(0, 1, 0), x, y, z, block, material[3]); + + let nz = this.chunk.getBlock(x, y, z - 1); + if (check(nz)) + this.addQuad(CUBE.NZ, new Vec3(0, 0, -1), x, y, z, block, material[4]); + + let pz = this.chunk.getBlock(x, y, z + 1); + if (check(pz)) + this.addQuad(CUBE.PZ, new Vec3(0, 0, 1), x, y, z, block, material[5]); + } + } + + finish() { + return new Mesh(this.positions.length / 3) + .store(this.positions) + .store(this.normals) + .store(this.uvs) + .store(this.blocks) + .store(this.materials); + } +} + +// meshes a chunk! +class GreedyMesher extends SimpleMesher { + + /** + * @param {Mask} mask + * @param {Vec3} axisMask + * @param {Vec3} v1 + * @param {Vec3} v2 + * @param {Vec3} v3 + * @param {Vec3} v4 + * @param {Number} width + * @param {Number} height + */ + addQuad(mask, axisMask, v1, v2, v3, v4, width, height) { + if (mask.block == AIR || mask.block == INVALID) { + return + } + + const normal = axisMask.mul(mask.normal); + let verticies = [v1, v2, v3, v4]; + let uv = []; + + if (axisMask.x == 1) { + uv[0] = new Vec2(height, width); + uv[1] = new Vec2(height, 0); + uv[2] = new Vec2(0, width); + uv[3] = new Vec2(0, 0); + } else { + uv[0] = new Vec2(width, height); + uv[1] = new Vec2(0, height); + uv[2] = new Vec2(width, 0); + uv[3] = new Vec2(0, 0); + } + + this.addVertex(verticies[0], normal, uv[0]); + this.addVertex(verticies[2 - mask.normal], normal, uv[2 - mask.normal]); + this.addVertex(verticies[2 + mask.normal], normal, uv[2 + mask.normal]); + this.addVertex(verticies[3], normal, uv[3]); + this.addVertex(verticies[1 + mask.normal], normal, uv[1 + mask.normal]); + this.addVertex(verticies[1 - mask.normal], normal, uv[1 - mask.normal]); + } + + // i wrote this code 3 years ago + // i legit have no idea what its doing + // i am sorry for the nesting + // i know it works + // i refuse to touch it + createMesh() { + for (let axis = 0; axis < 3; axis++) { + const axis1 = (axis + 1) % 3; + const axis2 = (axis + 2) % 3; + + const mainAxisLimit = Chunk.size.getAxis(axis); + const axis1Limit = Chunk.size.getAxis(axis1); + const axis2Limit = Chunk.size.getAxis(axis2); + + let deltaAxis1 = new Vec3(0, 0, 0); + let deltaAxis2 = new Vec3(0, 0, 0); + + let axisMask = new Vec3(0, 0, 0); + axisMask.setAxis(axis, 1); + + let mask = new Array(axis1Limit * axis2Limit); + + for (let iterAxis = -1; iterAxis < mainAxisLimit;) { + let n = 0; + + for (let iterAxis2 = 0; iterAxis2 < axis2Limit; iterAxis2++) { + for (let iterAxis1 = 0; iterAxis1 < axis1Limit; iterAxis1++) { + let pos = new Vec3(0, 0, 0); + pos.setAxis(axis, iterAxis); + pos.setAxis(axis1, iterAxis1); + pos.setAxis(axis2, iterAxis2); + + let currentBlock = this.chunk.getBlock(pos.x, pos.y, pos.z); + let compareBlock = this.chunk.getBlock( + pos.x + axisMask.x, + pos.y + axisMask.y, + pos.z + axisMask.z, + ); + + let currentBlockOpaque = currentBlock != AIR && currentBlock != INVALID; + let compareBlockOpaque = compareBlock != AIR && compareBlock != INVALID; + + if (currentBlockOpaque == compareBlockOpaque) { + mask[n++] = new Mask(INVALID, 0); + } else if (currentBlockOpaque) { + mask[n++] = new Mask(currentBlock, 1); + } else { + mask[n++] = new Mask(compareBlock, -1); + } + } + } + + iterAxis++; + n = 0; + + for (let iterAxis2 = 0; iterAxis2 < axis2Limit; iterAxis2++) { + for (let iterAxis1 = 0; iterAxis1 < axis1Limit;) { + if (mask[n].normal != 0) { + let currentMask = mask[n]; + let pos = new Vec3(0, 0, 0); + pos.setAxis(axis, iterAxis); + pos.setAxis(axis1, iterAxis1); + pos.setAxis(axis2, iterAxis2); + + let width; + for ( + width = 1; + iterAxis1 + width < axis1Limit && mask[n + width].compare(currentMask); + width++ + ); + + let height; + let done = false; + for (height = 1; iterAxis2 + height < axis2Limit; height++) { + for (let k = 0; k < width; k++) { + if (mask[n + k + height * axis1Limit].compare(currentMask)) + continue; + + done = true; + break; + } + + if (done) + break; + } + + deltaAxis1.setAxis(axis1, width); + deltaAxis2.setAxis(axis2, height); + + this.addQuad( + currentMask, + axisMask, + pos, + pos.addV(deltaAxis1), + pos.addV(deltaAxis2), + pos.addV(deltaAxis1).addV(deltaAxis2), + width, + height, + ); + + deltaAxis1.setAxis(axis1, 0); + deltaAxis2.setAxis(axis2, 0); + + for (let l = 0; l < height; l++) { + for (let k = 0; k < width; k++) { + mask[n + k + l * axis1Limit] = new Mask(INVALID, 0); + } + } + + iterAxis1 += width; + n += width; + } else { + iterAxis1++; + n++; + } + } + } + } + } + } +} + +function cubes_index(x, y, z) { + return x + (y * Chunk.size.x) + (z * Chunk.size.x * Chunk.size.y); +} diff --git a/js/controller.js b/js/controller.js new file mode 100644 index 0000000..0f9a73d --- /dev/null +++ b/js/controller.js @@ -0,0 +1,182 @@ +import { AIR, Chunk, DIRT } from "./chunk.js"; +import { Input, M, Vec3 } from "./wgpu/wgpu.js"; + +const press = Input.isKeyDown; + +export class Controller { + + constructor(world) { + this.world = world; + this.camera = world.camera; + this.lookSpeed = 100; + this.moveSpeed = 4.317; + this.jumpPower = 8.4; + + this.upSpeed = this.jumpPower; + this.inAir = true; + } + + update(dt) { + // movement + this.updateRotation(dt); + this.updateMovement(dt); + this.updateJumpFall(dt); + this.updateInteract(); + } + + updateRotation(dt) { + let rotate = new Vec3(); + if (press('ArrowRight')) rotate.y += 1; + if (press('ArrowLeft')) rotate.y -= 1; + if (press('ArrowUp')) rotate.x -= 1; + if (press('ArrowDown')) rotate.x += 1; + + // normalize arrow key movement + rotate = rotate.normalize(); + + let mouse = Input.getMouseMovement(); + rotate.y += mouse.dx * 200; + rotate.x += mouse.dy * 200; + + if(rotate.dot(rotate) <= Number.EPSILON) + return; + + this.camera.rotation.x += this.lookSpeed * dt * rotate.x; + this.camera.rotation.y += this.lookSpeed * dt * rotate.y; + this.camera.rotation.z += this.lookSpeed * dt * rotate.z; + + this.camera.rotation.x = M.clamp(this.camera.rotation.x, -90, 90) + this.camera.rotation.y = this.camera.rotation.y % 360 + } + + updateMovement(dt) { + const yaw = this.camera.rotation.y * (Math.PI/180); + let forward = new Vec3(Math.sin(yaw), 0, Math.cos(yaw)); + let left = new Vec3(-forward.z, 0, forward.x); + + let move = new Vec3(); + if (press('w')) move = move.addV(forward); + if (press('s')) move = move.subV(forward); + if (press('a')) move = move.addV(left); + if (press('d')) move = move.subV(left); + + if(move.dot(move) <= Number.EPSILON) + return; + + let speed = this.moveSpeed; + if (press('shift')) + speed *= 1.3; + + let normal = move.normalize(); + let dx = speed * dt * normal.x; + let dz = speed * dt * normal.z; + + if (!this.willCollide(dx, 0, 0)) + this.camera.position.x += dx; + + if (!this.willCollide(0, 0, dz)) + this.camera.position.z += dz; + } + + updateJumpFall(dt) { + const GRAVITY = 32; + + if (this.inAir) { + let dy = this.upSpeed * dt; + let pos = this.willCollideAt(0, dy, 0); + + if (pos == null) { + this.camera.position.y += dy; + this.upSpeed -= GRAVITY * dt; + } else { + if (dy < 0) + this.inAir = false; + this.upSpeed = 0; + //this.camera.position.y = pos.y; + } + } else { + // check if we need to fall + if (!this.willCollide(0, -0.1, 0)) { + this.inAir = true; + this.upSpeed = 0; + } + + if (press(' ')) { + this.inAir = true; + this.upSpeed = this.jumpPower; + } + } + + } + + willCollideAt(dx, dy, dz, extraPos = null) { + let px = this.camera.position.x + dx; + let py = this.camera.position.y + dy; + let pz = this.camera.position.z + dz; + + // the size of our player + let width = 0.5; + let height = 2; + + let minX = Math.floor(px - width/2); + let maxX = Math.ceil(px + width/2); + let minY = Math.floor(py - height * .75); + let maxY = Math.ceil(py + height * .10); + let minZ = Math.floor(pz - width/2); + let maxZ = Math.ceil(pz + width/2); + + for (let x = minX; x < maxX; x++) { + for (let y = minY; y < maxY; y++) { + for (let z = minZ; z < maxZ; z++) { + let pos = new Vec3(x, y, z); + if (pos.eq(extraPos)) + return extraPos; + + let block = Chunk.getGlobalBlock(x, y, z); + + // TODO: swimming + if (block != AIR) + return pos; + } + } + } + + return null; + } + + willCollide(dx, dy, dz, extraPos = null) { + return this.willCollideAt(dx, dy, dz, extraPos) != null + } + + updateInteract() { + // TODO: change + let block = DIRT; + + let left = Input.getLeftClick(); + let right = Input.getRightClick(); + + if (left && !right) { + // break block + let raycast = this.world.raycastCamera(); + if (!raycast) + return; + this.world.breakBlock(raycast); + return; + } + + if (!right) + return; + + let early = !left; + let raycast = this.world.raycastCamera(5, early); + if (!raycast) + return; + + // place block + if (this.willCollide(0, 0, 0, raycast)) + return; + this.world.placeBlock(raycast, block); + + + } +} diff --git a/js/minecraft.js b/js/minecraft.js new file mode 100644 index 0000000..aeb4c30 --- /dev/null +++ b/js/minecraft.js @@ -0,0 +1,55 @@ +import { Init, Run, Renderer, Shader, File, Texture, Vec3 } from './wgpu/wgpu.js' + +import { Controller } from './controller.js' +import { World } from './world.js'; + +async function main() { + let renderer = new Renderer(); + renderer.FOV = 70; + + let atlasData = await File.readFileAsync("atlas.raw", "bytes"); + let atlas = new Texture(16, 16, 7); + atlas.store(atlasData, 0, 7); + + let shaderCode = await File.readFileAsync("shader.wgsl", "text"); + let shader = new Shader(shaderCode, { + vertex: [ + "vec3", // position + "vec3", // normals + "vec2", // uvs + "vec3", // block + "float", // material + ], + texture: [ + { sampler: { bind: 1 } }, + { texture: { resource: atlas, bind: 2 }}, + ], + uniforms: [ + { name: "skyColor", type: "vec3" }, + { name: "cameraPos", type: "vec3" }, + { name: "raycast", type: "vec3" }, + { name: "renderDistance", type: "float" }, + ] + }); + + let seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + let world = new World(seed, shader); + let controller = new Controller(world); + + let skyColor = new Vec3(0.69, 0.89, 0.89); + + Run((dt) => { + let raycast = world.raycastCamera(); + world.update(); + shader.load("skyColor", skyColor); + shader.load("cameraPos", world.camera.position); + shader.load("renderDistance", world.renderDistance); + shader.load("raycast", raycast ?? new Vec3(NaN, NaN, NaN)); + renderer.beginFrame(skyColor); + world.draw(renderer); + renderer.endFrame(); + controller.update(dt); + }) +} + +Init(main) diff --git a/js/noise.js b/js/noise.js new file mode 100644 index 0000000..50cf8b7 --- /dev/null +++ b/js/noise.js @@ -0,0 +1,164 @@ +import { Vec3 } from "./wgpu/wgpu.js"; + +const PERM_SIZE = 256; +const PERMUTATIONS = {} +const RANDS = {} + +function rng(seed) { + // mulberry32 + return function() { + let t = seed += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + return ((t ^ t >>> 14) >>> 0) / 4294967296; + } +} + +function shuffle(permutation, seed) { + let rand = rng(seed); + for(let e = permutation.length - 1; e > 0; e--) { + const index = Math.round(rand() * (e - 1)); + const temp = permutation[e]; + + permutation[e] = permutation[index]; + permutation[index] = temp; + } +} + +function makePermutation(seed) { + if (PERMUTATIONS[seed]) + return PERMUTATIONS[seed]; + + const permutation = []; + for (let i = 0; i < PERM_SIZE; i++) { + permutation.push(i); + } + + shuffle(permutation, seed); + + for(let i = 0; i < PERM_SIZE; i++) { + permutation.push(permutation[i]); + } + + PERMUTATIONS[seed] = permutation; + return permutation; +} + +function makeRandomArray(seed) { + if (RANDS[seed]) + return RANDS[seed]; + + const randarr = []; + + let rand = rng(seed); + + for (let i = 0; i < PERM_SIZE; i++) { + let s, v; + do { + let x = rand() * 2 - 1; + let y = rand() * 2 - 1; + let z = rand() * 2 - 1; + v = new Vec3(x, y, z); + s = v.dot(v); + } while (s > 1.0); + + s = Math.sqrt(s); + randarr[i] = v.div(s); + } + + for(let i = 0; i < PERM_SIZE; i++) { + randarr.push(randarr[i]); + } + + RANDS[seed] = randarr; + return randarr; +} + +function lerp(t, a, b) { + return a + t * (b - a); +} + +function sCurve(t) { + let inner = new Vec3(3.0, 3.0, 3.0) + .subV(t.mul(2)); + return t.mulV(t).mulV(inner); +} + +/** + * @param {Vec3} vec + */ +function pnoise(vec, seed) { + const permutation = makePermutation(seed); + const randArr = makeRandomArray(seed); + + let t = vec.add(10000.0); + let b0 = t.and(PERM_SIZE - 1); + let b1 = b0.add(1).and(PERM_SIZE - 1); + let r0 = t.subV(t.trunc()); + let r1 = r0.sub(1); + + let i = permutation[b0.x]; + let j = permutation[b1.x]; + + let b00 = permutation[i + b0.y]; + let b10 = permutation[j + b0.y]; + let b01 = permutation[i + b1.y]; + let b11 = permutation[j + b1.y]; + + let s = sCurve(r0); + + let u, v, a, b, c, d; + + u = randArr[b00 + b0.z].dot(r0); + v = randArr[b10 + b0.z].dot(new Vec3(r1.x, r0.y, r0.z)); + a = lerp(s.x, u, v); + + u = randArr[b01 + b0.z].dot(new Vec3(r0.x, r1.y, r0.z)); + v = randArr[b11 + b0.z].dot(new Vec3(r1.x, r1.y, r0.z)); + b = lerp(s.x, u, v); + + c = lerp(s.y, a, b); + + u = randArr[b00 + b1.z].dot(new Vec3(r0.x, r0.y, r1.z)); + v = randArr[b10 + b1.z].dot(new Vec3(r1.x, r0.y, r1.z)); + a = lerp(s.x, u, v); + + u = randArr[b01 + b1.z].dot(new Vec3(r0.x, r1.y, r1.z)); + v = randArr[b11 + b1.z].dot(r1); + b = lerp(s.x, u, v); + + d = lerp(s.y, a, b); + + return 1.5 * lerp(s.z, c, d); +} + +export function height2d(x, y, seed, opts = {}) { + // read perlin noise opts + const frequency = opts.frequency ?? 1.0; + const amplitude = opts.amplitude ?? 0.5; + const octaves = opts.octaves ?? 5; + const lacunarity = opts.lacunarity ?? 2.0; + const gain = opts.gain ?? 0.5; + const scale = opts.scale ?? 0.01; + + let rand = rng(seed); + const xOffset = rand(); + const yOffset = rand(); + + let freq = frequency; + let amp = amplitude; + let sum = 0.0; + let maxSum = 0.0; + + for (let i = 0; i < octaves; i++) { + let vec = new Vec3(x + xOffset, y + yOffset, 0).mul(freq * scale); + sum += pnoise(vec, seed) * amp; + maxSum += amp; + freq *= lacunarity; + amp *= gain; + } + + let normalized = (sum + maxSum) / (2 * maxSum) * amplitude; + + return Math.round(normalized); +} diff --git a/js/wgpu/core/Buffer.js b/js/wgpu/core/Buffer.js new file mode 100644 index 0000000..190852a --- /dev/null +++ b/js/wgpu/core/Buffer.js @@ -0,0 +1,50 @@ +import { device } from '../wgpu.js' + +class Buffer { + #buffer + #type + + constructor(usage, type, length) { + let desc = { + size: length * (type).BYTES_PER_ELEMENT, + usage: usage | GPUBufferUsage.COPY_DST, + mappedAtCreation: false, + + }; + this.#buffer = device.createBuffer(desc); + this.#type = type + } + + write(data) { + if (Array.isArray(data)) { + data = new (this.#type)(data); + } + device.queue.writeBuffer(this.#buffer, 0, data); + } + + get() { + return this.#buffer + } + + delete() { + this.#buffer.destroy() + } +} + +export class VertexBuffer extends Buffer { + constructor(length) { + super(GPUBufferUsage.VERTEX, Float32Array, length) + } +} + +export class IndexBuffer extends Buffer { + constructor(length) { + super(GPUBufferUsage.INDEX, Uint16Array, length) + } +} + +export class UniformBuffer extends Buffer { + constructor(length) { + super(GPUBufferUsage.UNIFORM, Float32Array, length) + } +} diff --git a/js/wgpu/core/Mesh.js b/js/wgpu/core/Mesh.js new file mode 100644 index 0000000..00cbcde --- /dev/null +++ b/js/wgpu/core/Mesh.js @@ -0,0 +1,48 @@ +import { VertexBuffer, IndexBuffer } from './Buffer.js' + +export class Mesh { + + #vertexCount + #vertexBuffers + #indexBuffer + + constructor(vertexCount) { + this.#vertexCount = vertexCount + this.#vertexBuffers = [] + this.#indexBuffer = null + } + + store(data) { + let buffer = new VertexBuffer(data.length); + buffer.write(data); + this.#vertexBuffers.push(buffer); + return this; + } + + indicies(data) { + this.#indexBuffer = new IndexBuffer(data.length); + this.#indexBuffer.write(data); + return this; + } + + draw(pass) { + this.#vertexBuffers.forEach((buffer, idx) => { + pass.setVertexBuffer(idx, buffer.get()); + }); + if (this.#indexBuffer) { + pass.setIndexBuffer(this.#indexBuffer.get(), "uint16"); + pass.drawIndexed(this.#vertexCount, 1); + } else { + pass.draw(this.#vertexCount, 1); + } + } + + delete() { + for (let buffer of this.#vertexBuffers) { + buffer.delete(); + } + if (this.#indexBuffer) { + this.#indexBuffer.delete(); + } + } +} diff --git a/js/wgpu/core/Object.js b/js/wgpu/core/Object.js new file mode 100644 index 0000000..b759bea --- /dev/null +++ b/js/wgpu/core/Object.js @@ -0,0 +1,105 @@ +import { Vec3 } from '../math/Vec3.js' +import { Mat4 } from '../math/Mat4.js' + +export class Object { + + static #id = 0 + + constructor(mesh = null) { + this.mesh = mesh; + this.position = new Vec3(); + this.rotation = new Vec3(); + this.scale = new Vec3(1, 1, 1); + this.id = Object.#id; + Object.#id++; + } + + view() { + const view = new Mat4(); + const d = view.data; + + const c3 = Math.cos(this.rotation.z * (Math.PI / 180)); + const s3 = Math.sin(this.rotation.z * (Math.PI / 180)); + const c2 = Math.cos(this.rotation.x * (Math.PI / 180)); + const s2 = Math.sin(this.rotation.x * (Math.PI / 180)); + const c1 = Math.cos(this.rotation.y * (Math.PI / 180)); + const s1 = Math.sin(this.rotation.y * (Math.PI / 180)); + + const u = new Vec3((c1 * c3 + s1 * s2 * s3), (c2 * s3), (c1 * s2 * s3 - c3 * s1)); + const v = new Vec3((c3 * s1 * s2 - c1 * s3), (c2 * c3), (c1 * c3 * s2 + s1 * s3)); + const w = new Vec3((c2 * s1), (-s2), (c1 * c2)); + + d[0] = u.x; + d[1] = v.x; + d[2] = w.x; + d[3] = 0; + d[4] = u.y; + d[5] = v.y; + d[6] = w.y; + d[7] = 0; + d[8] = u.z; + d[9] = v.z; + d[10] = w.z; + d[11] = 0; + d[12] = -u.dot(this.position); + d[13] = -v.dot(this.position); + d[14] = -w.dot(this.position); + d[15] = 1; + + return view + } + + axis() { + const view = this.view().invert().data; + const forward = new Vec3(0, 0, 1); + + const u = new Vec3(view[0], view[4], view[8]); + const v = new Vec3(view[1], view[5], view[9]); + const w = new Vec3(view[2], view[6], view[10]); + + return new Vec3( + forward.dot(u), + forward.dot(v), + forward.dot(w), + ); + } + + tran() { + this.rotation.x %= 360; + this.rotation.y %= 360; + this.rotation.z %= 360; + + const tran = Mat4.identity(); + const d = tran.data; + + const c3 = Math.cos(this.rotation.z * (Math.PI / 180)); + const s3 = Math.sin(this.rotation.z * (Math.PI / 180)); + const c2 = Math.cos(this.rotation.x * (Math.PI / 180)); + const s2 = Math.sin(this.rotation.x * (Math.PI / 180)); + const c1 = Math.cos(this.rotation.y * (Math.PI / 180)); + const s1 = Math.sin(this.rotation.y * (Math.PI / 180)); + + d[0] = this.scale.x * (c1 * c3 + s1 * s2 * s3); + d[1] = this.scale.x * (c2 * s3); + d[2] = this.scale.x * (c1 * s2 * s3 - c3 * s1); + d[3] = 0; + + d[4] = this.scale.y * (c3 * s1 * s2 - c1 * s3); + d[5] = this.scale.y * (c2 * c3); + d[6] = this.scale.y * (c1 * c3 * s2 + s1 * s3); + d[7] = 0; + + d[8] = this.scale.z * (c2 * s1); + d[9] = this.scale.z * (-s2); + d[10] = this.scale.z * (c1 * c2); + d[11] = 0; + + d[12] = this.position.x; + d[13] = this.position.y; + d[14] = this.position.z; + d[15] = 1; + + return tran; + } + +} diff --git a/js/wgpu/core/Renderer.js b/js/wgpu/core/Renderer.js new file mode 100644 index 0000000..708b9d0 --- /dev/null +++ b/js/wgpu/core/Renderer.js @@ -0,0 +1,172 @@ +import { device, canvas, Vec3 } from '../wgpu.js' +import { Mat4 } from '../math/Mat4.js'; +import { UniformBuffer } from './Buffer.js'; + +export class Renderer { + + static sampleCount = 4; + + #context + #colorTexture + #depthTexture + + #objectBindGroups + #objectBindGroupsUsage + + #commandEncoder + #passEncoder + + constructor() { + let context = canvas.getContext('webgpu'); + const canvasConfig = { + device, + format: navigator.gpu.getPreferredCanvasFormat(), + usage: GPUTextureUsage.RENDER_ATTACHMENT, + alphaMode: 'opaque' + }; + context.configure(canvasConfig); + + let colorTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'bgra8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: Renderer.sampleCount, + }); + + let depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount: Renderer.sampleCount, + }); + + this.#context = context; + this.#colorTexture = colorTexture; + this.#depthTexture = depthTexture; + this.#objectBindGroups = {}; + + this.FOV = 90 + this.FAR = 1000 + this.NEAR = .1 + } + + #allocateObjectBindGroup(shader) { + let perShaderBindGroups = this.#objectBindGroups[shader.id]; + if (!perShaderBindGroups) + perShaderBindGroups = []; + + let pipeline = shader.pipeline(); + + // holds a single 4x4 matrix + let uniformBuffer = new UniformBuffer(16); + let bindGroupDesc = { + label: shader.label, + layout: pipeline.getBindGroupLayout(1), + entries: [ + { + binding: 0, + resource: { + buffer: uniformBuffer.get(), + }, + } + ], + }; + let bindGroup = device.createBindGroup(bindGroupDesc); + + perShaderBindGroups.push([bindGroup, uniformBuffer]); + this.#objectBindGroups[shader.id] = perShaderBindGroups; + + return [bindGroup, uniformBuffer]; + } + + #getObjectBindGroup(shader) { + let usage = this.#objectBindGroupsUsage; + let perShaderUsage = usage[shader.id]; + + if (!perShaderUsage) + perShaderUsage = 0; + + let bindGroup; + let perShaderBindGroups = this.#objectBindGroups[shader.id]; + if (perShaderBindGroups && perShaderUsage < perShaderBindGroups.length) { + bindGroup = perShaderBindGroups[perShaderUsage]; + } else { + bindGroup = this.#allocateObjectBindGroup(shader); + } + + this.#objectBindGroupsUsage[shader.id] = perShaderUsage + 1; + + return bindGroup; + } + + beginFrame(clearColor) { + // get valid "sky" clear color + clearColor = clearColor ?? new Vec3(0.0, 0.0, 0.0); + + let colorAttachment = { + view: this.#colorTexture.createView(), + resolveTarget: this.#context.getCurrentTexture().createView(), + clearValue: { r: clearColor.x, g: clearColor.y, b: clearColor.z, a: 1 }, + loadOp: 'clear', + storeOp: 'store', + }; + + let depthAttachment = { + view: this.#depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }; + + let renderPassDesc = { + colorAttachments: [colorAttachment], + depthStencilAttachment: depthAttachment, + }; + + this.#commandEncoder = device.createCommandEncoder(); + this.#passEncoder = this.#commandEncoder.beginRenderPass(renderPassDesc); + this.#passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1); + this.#objectBindGroupsUsage = {}; + } + + draw(camera, shader, objects = []) { + shader.bind(this.#passEncoder); + shader.load("proj", this.proj()); + shader.load("view", camera.view()); + shader.flush(); + for (let object of objects) { + if (!object.mesh) + continue; + + // write object transformation matrix + // to per object uniform + let [bindGroup, uniformBuffer] = this.#getObjectBindGroup(shader); + uniformBuffer.write(new Float32Array(object.tran().data)); + + this.#passEncoder.setBindGroup(1, bindGroup); + object.mesh.draw(this.#passEncoder); + + } + } + + endFrame() { + this.#passEncoder.end(); + device.queue.submit([this.#commandEncoder.finish()]); + } + + proj() { + const proj = new Mat4() + const d = proj.data + + const tanHalfFovy = Math.tan((this.FOV * (Math.PI/180)) / 2.0); + const aspect = canvas.width / canvas.height + + d[0] = 1.0 / (aspect * tanHalfFovy) + d[5] = 1.0 / tanHalfFovy + d[10] = this.FAR / (this.FAR - this.NEAR) + d[11] = 1.0 + d[14] = -(this.FAR * this.NEAR) / (this.FAR - this.NEAR) + + return proj + } +} diff --git a/js/wgpu/core/Shader.js b/js/wgpu/core/Shader.js new file mode 100644 index 0000000..dd87b17 --- /dev/null +++ b/js/wgpu/core/Shader.js @@ -0,0 +1,289 @@ +import { device, Renderer } from '../wgpu.js' +import { UniformBuffer } from './Buffer.js'; + +export class Shader { + + static #id = 0 + + #pipeline + #bindGroup + + #attributes + #uniformBuffer + #uniforms + #dirty + + constructor(code, opts = {}) { + // set the shader id + this.id = Shader.#id; + Shader.#id++; + + // read opts + let label = opts.label ?? ''; + let vertex_args = opts.vertex ?? ["vec3"]; + let texture_args = opts.texture ?? []; + let custom_uniform_args = opts.uniforms ?? []; + + // set label + this.label = label; + + // parse uniforms + let attributes = {} + let uniformSize = 0; + let forced_uniform_args = [ + { name: "proj", type: "mat4" }, + { name: "view", type: "mat4" }, + ]; + let uniform_args = forced_uniform_args.concat(custom_uniform_args); + uniform_args.forEach((arg) => { + let name = arg.name; + let type = arg.type; + let size = 0; + let alignment = 0; + switch (type) { + case "mat4": + size = 16; + alignment = 4; + break; + case "vec3": + size = 3; + alignment = 4; + break; + case "float": + size = 1; + alignment = 1; + break; + case "bool": + size = 1; + alignment = 1; + break; + }; + + if (size > 0) { + let idx = Math.floor((uniformSize + alignment - 1) / alignment) * alignment; + attributes[name] = { idx, type }; + uniformSize = idx + size; + } + }); + + // parse and load shader code + let shaderDesc = { code }; + let shaderModule = device.createShaderModule(shaderDesc); + + // create bind group layout + let bindGroupLayoutAttribs = [ + // default bind for uniforms + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: "uniform" }, + }, + // default bind for trans/model matrix + ]; + // add texture binds + for (let arg of texture_args) { + if (arg.sampler) { + bindGroupLayoutAttribs.push({ + binding: arg.sampler.bind, + visibility: GPUShaderStage.FRAGMENT, + sampler: { + type: "filtering", + }, + }); + } else if (arg.texture) { + bindGroupLayoutAttribs.push({ + binding: arg.texture.bind, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d-array", + multisampled: false, + }, + }); + + } + } + let bindGroupLayoutDesc = { + entries: bindGroupLayoutAttribs, + }; + let bindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDesc); + + let bindGroupLayoutDesc2 = { + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { type: "uniform" }, + } + ] + }; + let bindGroupLayout2 = device.createBindGroupLayout(bindGroupLayoutDesc2); + + // create pipeline layout + let pipelineLayoutDesc = { + bindGroupLayouts: [bindGroupLayout, bindGroupLayout2], + }; + let layout = device.createPipelineLayout(pipelineLayoutDesc); + + // create vertex attributes + let vertexAttributes = []; + for (let idx in vertex_args) { + let arg = vertex_args[idx]; + let format; + let stride; + switch (arg) { + case "vec3": + stride = 3; + format = "float32x3"; + break; + case "vec2": + stride = 2; + format = "float32x2"; + break; + case "float": + stride = 1; + format = "float32"; + break; + } + vertexAttributes.push({ + arrayStride: Float32Array.BYTES_PER_ELEMENT * stride, + attributes: [ + { shaderLocation: idx, offset: 0, format }, + ], + }); + } + + // create pipeline + let colorState = { + format: 'bgra8unorm' + }; + let pipelineDesc = { + label, + layout, + vertex: { + module: shaderModule, + entryPoint: 'vs_main', + buffers: vertexAttributes, + }, + fragment: { + module: shaderModule, + entryPoint: 'fs_main', + targets: [colorState] + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, + primitive: { + topology: 'triangle-list', + frontFace: 'cw', + cullMode: 'back' + }, + multisample: { + count: Renderer.sampleCount, + }, + }; + let pipeline = device.createRenderPipeline(pipelineDesc); + + // create uniform buffer + let uniformBuffer = new UniformBuffer(uniformSize); + + // create bind group + let bindGroupAttribs = [ + // default bind for uniforms + { + binding: 0, + resource: { + buffer: uniformBuffer.get(), + }, + } + ]; + for (let arg of texture_args) { + if (arg.sampler) { + bindGroupAttribs.push({ + binding: arg.sampler.bind, + resource: device.createSampler(), + }); + } else if (arg.texture) { + bindGroupAttribs.push({ + binding: arg.texture.bind, + resource: arg.texture.resource.view(), + }); + } + } + let uniformBindGroupDesc = { + label, + layout: pipeline.getBindGroupLayout(0), + entries: bindGroupAttribs, + }; + let uniformBindGroup = device.createBindGroup(uniformBindGroupDesc); + + this.#pipeline = pipeline; + this.#bindGroup = uniformBindGroup; + + this.#attributes = attributes; + this.#uniformBuffer = uniformBuffer; + this.#uniforms = new Float32Array(uniformSize); + this.#dirty = false; + } + + load(name, value) { + let attribute = this.#attributes[name]; + if (!attribute) { + return; + } + let idx = attribute.idx; + switch (attribute.type) { + case "float": + this.#loadFloat(idx, value); + break; + case "bool": + this.#loadBool(idx, value); + break; + case "vec3": + this.#loadVec3(idx, value); + break; + case "mat4": + this.#loadMat4(idx, value); + break; + } + this.#dirty = true; + } + + #loadFloat(idx, f) { + this.#uniforms[idx] = f; + } + + #loadBool(idx, b) { + this.#uniforms[idx] = b ? 1 : 0; + } + + #loadVec3(idx, v) { + this.#uniforms[idx + 0] = v.x; + this.#uniforms[idx + 1] = v.y; + this.#uniforms[idx + 2] = v.z; + } + + #loadMat4(idx, m) { + for (let i = 0; i < m.data.length; i++) { + this.#uniforms[idx + i] = m.data[i]; + } + } + + bind(pass) { + pass.setPipeline(this.#pipeline); + pass.setBindGroup(0, this.#bindGroup); + } + + flush() { + if (this.#dirty) { + this.#uniformBuffer.write(this.#uniforms); + this.#dirty = false; + } + } + + pipeline() { + return this.#pipeline; + } +} diff --git a/js/wgpu/core/Texture.js b/js/wgpu/core/Texture.js new file mode 100644 index 0000000..311aead --- /dev/null +++ b/js/wgpu/core/Texture.js @@ -0,0 +1,51 @@ +import { device } from "../wgpu.js" + +export class Texture { + + #texture + + constructor(width, height, depth = 1) { + this.width = width; + this.height = height; + + let mips = 1; + + let texture = device.createTexture({ + size: [width, height, depth], + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, + mipLevelCount: mips, + dimension: '2d', + }); + + this.#texture = texture; + this.depth = depth; + this.mips = mips; + } + + store(data, layer = 0, depth = 1) { + let texture = this.#texture; + let width = this.width; + let height = this.height; + + device.queue.writeTexture( + { texture, origin: { x: 0, y: 0, z: layer } }, + data, + { bytesPerRow: width * 4, rowsPerImage: height }, + { width, height, depthOrArrayLayers: depth }, + ); + } + + view() { + return this.#texture.createView({ + dimension: '2d-array', + baseArrayLayer: 0, + arrayLayerCount: this.depth, + mipLevelCount: this.mips, + }); + } + + get() { + return this.#texture; + } +} diff --git a/js/wgpu/io/File.js b/js/wgpu/io/File.js new file mode 100644 index 0000000..44338a3 --- /dev/null +++ b/js/wgpu/io/File.js @@ -0,0 +1,11 @@ +export const File = {} + +File.readFileAsync = async (path, type = "text") => { + try { + let data = await fetch(path, { cache: 'no-store' }); + let res = await data[type](); + return res; + } catch (err) { + return undefined + } +} diff --git a/js/wgpu/io/Input.js b/js/wgpu/io/Input.js new file mode 100644 index 0000000..0f177e8 --- /dev/null +++ b/js/wgpu/io/Input.js @@ -0,0 +1,62 @@ +import { canvas } from "../wgpu.js"; + +export const Input = {} + +const keys = {}; +let dx = 0; +let dy = 0; + +let leftClick = false; +let rightClick = false; + +Input.setup = () => { + document.onkeydown = function(e) { + keys[e.key.toLowerCase()] = true + + }; + document.onkeyup = function(e) { + keys[e.key.toLowerCase()] = false + }; + document.onmousemove = function(e) { + if (document.pointerLockElement !== canvas) + return; + dx += e.movementX / canvas.width; + dy += e.movementY / canvas.height; + }; + document.onmousedown = function(e) { + if (document.pointerLockElement !== canvas) + return; + if (e.buttons == 1) + leftClick = true; + if (e.buttons == 2) + rightClick = true; + } + canvas.onclick = () => { + canvas.requestPointerLock(); + }; +} + +Input.isKeyDown = (key) => { + return keys[key.toLowerCase()] +} + +Input.getMouseMovement = () => { + let res = { dx, dy }; + dx = 0; + dy = 0; + return res; +} + +Input.getLeftClick = () => { + if (!leftClick) + return false; + leftClick = false; + return true; +}; + +Input.getRightClick = () => { + if (!rightClick) + return false; + rightClick = false; + return true; +}; diff --git a/js/wgpu/math/Mat4.js b/js/wgpu/math/Mat4.js new file mode 100644 index 0000000..d4846f1 --- /dev/null +++ b/js/wgpu/math/Mat4.js @@ -0,0 +1,126 @@ +export class Mat4 { + + constructor(data) { + this.data = data ?? [ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ] + } + + mulV(v) { + let d = this.data; + return new Vec4( + new Vec4(d[0], d[1], d[2], d[3]).dot(v), + new Vec4(d[4], d[5], d[6], d[7]).dot(v), + new Vec4(d[8], d[9], d[10], d[11]).dot(v), + new Vec4(d[12], d[13], d[14], d[15]).dot(v), + ) + } + + determinant() { + // taken from gl-matrix + + let a = this.data; + let a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3]; + let a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7]; + let a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + let a30 = a[12], + a31 = a[13], + a32 = a[14], + a33 = a[15]; + let b0 = a00 * a11 - a01 * a10; + let b1 = a00 * a12 - a02 * a10; + let b2 = a01 * a12 - a02 * a11; + let b3 = a20 * a31 - a21 * a30; + let b4 = a20 * a32 - a22 * a30; + let b5 = a21 * a32 - a22 * a31; + let b6 = a00 * b5 - a01 * b4 + a02 * b3; + let b7 = a10 * b5 - a11 * b4 + a12 * b3; + let b8 = a20 * b2 - a21 * b1 + a22 * b0; + let b9 = a30 * b2 - a31 * b1 + a32 * b0; + + // Calculate the determinant + return a13 * b6 - a03 * b7 + a33 * b8 - a23 * b9; + } + + invert() { + // taken from gl-matrix + + let a = this.data; + let a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3]; + let a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7]; + let a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + let a30 = a[12], + a31 = a[13], + a32 = a[14], + a33 = a[15]; + let b00 = a00 * a11 - a01 * a10; + let b01 = a00 * a12 - a02 * a10; + let b02 = a00 * a13 - a03 * a10; + let b03 = a01 * a12 - a02 * a11; + let b04 = a01 * a13 - a03 * a11; + let b05 = a02 * a13 - a03 * a12; + let b06 = a20 * a31 - a21 * a30; + let b07 = a20 * a32 - a22 * a30; + let b08 = a20 * a33 - a23 * a30; + let b09 = a21 * a32 - a22 * a31; + let b10 = a21 * a33 - a23 * a31; + let b11 = a22 * a33 - a23 * a32; + + let det = this.determinant(); + if (!det) + return null; + + det = 1.0 / det; + + let out = []; + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; + out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; + out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; + out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; + out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; + out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; + out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + + return new Mat4(out); + } + + static identity() { + return new Mat4([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]); + } + +} diff --git a/js/wgpu/math/Math.js b/js/wgpu/math/Math.js new file mode 100644 index 0000000..45e5bf8 --- /dev/null +++ b/js/wgpu/math/Math.js @@ -0,0 +1,3 @@ +export const M = {} + +M.clamp = (num, min, max) => Math.min(Math.max(num, min), max) diff --git a/js/wgpu/math/Vec2.js b/js/wgpu/math/Vec2.js new file mode 100644 index 0000000..9c7f68b --- /dev/null +++ b/js/wgpu/math/Vec2.js @@ -0,0 +1,103 @@ +export class Vec2 { + + constructor(x = 0, y = 0) { + this.x = x + this.y = y + } + + setAxis(axis, value) { + switch (axis) { + case 0: + this.x = value; + break; + case 1: + this.y = value; + break; + }; + } + + getAxis(axis) { + switch (axis) { + case 0: + return this.x; + case 1: + return this.y; + }; + } + + add(n) { + return new Vec2( + this.x + n, + this.y + n, + ); + } + + addV(v) { + return new Vec2( + this.x + v.x, + this.y + v.y, + ); + } + + sub(n) { + return new Vec2( + this.x - n, + this.y - n, + ); + } + + subV(v) { + return new Vec2( + this.x - v.x, + this.y - v.y, + ); + } + + mul(s) { + return new Vec2( + this.x * s, + this.y * s, + ); + } + + mulV(v) { + return new Vec2( + this.x * v.x, + this.y * v.y, + ); + } + + div(s) { + return new Vec2( + this.x / s, + this.y / s, + ); + } + + divV(v) { + return new Vec2( + this.x / v.x, + this.y / v.y, + ); + } + + invert() { + return new Vec2( + 1 / this.x, + 1 / this.y, + ); + } + + normalize() { + return this.div(this.length() || 1); + } + + dot(v) { + return this.x * v.x + this.y * v.y; + } + + length() { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + +} diff --git a/js/wgpu/math/Vec3.js b/js/wgpu/math/Vec3.js new file mode 100644 index 0000000..f084fee --- /dev/null +++ b/js/wgpu/math/Vec3.js @@ -0,0 +1,156 @@ +export class Vec3 { + + constructor(x = 0, y = 0, z = 0) { + this.x = x + this.y = y + this.z = z + } + + setAxis(axis, value) { + switch (axis) { + case 0: + this.x = value; + break; + case 1: + this.y = value; + break; + case 2: + this.z = value; + break; + }; + } + + getAxis(axis) { + switch (axis) { + case 0: + return this.x; + case 1: + return this.y; + case 2: + return this.z; + }; + } + + add(n) { + return new Vec3( + this.x + n, + this.y + n, + this.z + n, + ); + } + + addV(v) { + return new Vec3( + this.x + v.x, + this.y + v.y, + this.z + v.z, + ); + } + + sub(n) { + return new Vec3( + this.x - n, + this.y - n, + this.z - n, + ); + } + + subV(v) { + return new Vec3( + this.x - v.x, + this.y - v.y, + this.z - v.z, + ); + } + + mul(s) { + return new Vec3( + this.x * s, + this.y * s, + this.z * s, + ); + } + + mulV(v) { + return new Vec3( + this.x * v.x, + this.y * v.y, + this.z * v.z, + ); + } + + div(s) { + return new Vec3( + this.x / s, + this.y / s, + this.z / s, + ); + } + + divV(v) { + return new Vec3( + this.x / v.x, + this.y / v.y, + this.z / v.z, + ); + } + + invert() { + return new Vec3( + 1 / this.x, + 1 / this.y, + 1 / this.z, + ); + } + + normalize() { + return this.div(this.length() || 1); + } + + dot(v) { + return this.x * v.x + this.y * v.y + this.z * v.z; + } + + length() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + } + + floor() { + return new Vec3( + Math.floor(this.x), + Math.floor(this.y), + Math.floor(this.z), + ) + } + + ciel() { + return new Vec3( + Math.ceil(this.x), + Math.ceil(this.y), + Math.ceil(this.z), + ) + } + + trunc() { + return new Vec3( + Math.trunc(this.x), + Math.trunc(this.y), + Math.trunc(this.z), + ) + } + + and(n) { + return new Vec3( + this.x & n, + this.y & n, + this.z & n, + ); + } + + eq(v) { + return v && + this.x == v.x && + this.y == v.y && + this.z == v.z + } +} diff --git a/js/wgpu/math/Vec4.js b/js/wgpu/math/Vec4.js new file mode 100644 index 0000000..ea3ded2 --- /dev/null +++ b/js/wgpu/math/Vec4.js @@ -0,0 +1,132 @@ +export class Vec4 { + + constructor(x = 0, y = 0, z = 0, w = 0) { + this.x = x + this.y = y + this.z = z + this.w = w + } + + setAxis(axis, value) { + switch (axis) { + case 0: + this.x = value; + break; + case 1: + this.y = value; + break; + case 2: + this.z = value; + break; + case 3: + this.w = value; + break; + }; + } + + getAxis(axis) { + switch (axis) { + case 0: + return this.x; + case 1: + return this.y; + case 2: + return this.z; + case 3: + return this.w; + }; + } + + add(n) { + return new Vec4( + this.x + n, + this.y + n, + this.z + n, + this.w + n, + ); + } + + addV(v) { + return new Vec4( + this.x + v.x, + this.y + v.y, + this.z + v.z, + this.w + v.w, + ); + } + + sub(n) { + return new Vec4( + this.x - n, + this.y - n, + this.z - n, + this.w - n, + ); + } + + subV(v) { + return new Vec4( + this.x - v.x, + this.y - v.y, + this.z - v.z, + this.w - v.w, + ); + } + + mul(s) { + return new Vec4( + this.x * s, + this.y * s, + this.z * s, + this.w * s, + ); + } + + mulV(v) { + return new Vec4( + this.x * v.x, + this.y * v.y, + this.z * v.z, + this.w * v.w, + ); + } + + div(s) { + return new Vec4( + this.x / s, + this.y / s, + this.z / s, + this.w / s, + ); + } + + divV(v) { + return new Vec4( + this.x / v.x, + this.y / v.y, + this.z / v.z, + this.w / v.w, + ); + } + + invert() { + return new Vec4( + 1 / this.x, + 1 / this.y, + 1 / this.z, + 1 / this.w, + ); + } + + normalize() { + return this.div(this.length() || 1); + } + + dot(v) { + return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w; + } + + length() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); + } +} diff --git a/js/wgpu/wgpu.js b/js/wgpu/wgpu.js new file mode 100644 index 0000000..85dcf41 --- /dev/null +++ b/js/wgpu/wgpu.js @@ -0,0 +1,84 @@ +export { Mesh } from './core/Mesh.js' +export { Object } from './core/Object.js' +export { Renderer } from './core/Renderer.js' +export { Shader } from './core/Shader.js' +export { Texture } from './core/Texture.js' +export { Mat4 } from './math/Mat4.js' +export { Vec2 } from './math/Vec2.js' +export { Vec3 } from './math/Vec3.js' +export { Vec4 } from './math/Vec4.js' +export { M } from './math/Math.js' +export { File } from './io/File.js' +export { Input } from './io/Input.js' + +export { Run, Init } + +import { Input } from './io/Input.js' + +export let adapter = null; +export let device = null; +export let canvas = null; + +async function Init(callback) { + // Wgpu is already loaded + if (adapter && device && canvas) { + callback(); + return; + } + + // Check to see if WebGPU can run + if (!navigator.gpu) { + console.error("WebGPU not supported on this browser."); + return; + } + + // get webgpu browser software layer for graphics device + adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + console.error("No appropriate GPUAdapter found."); + return; + } + + // get the instantiation of webgpu on this device + device = await adapter.requestDevice(); + if (!device) { + console.error("Failed to request Device."); + return; + } + + canvas = document.createElement("canvas"); + canvas.id = "canvas"; + canvas.width = window.screen.width; + canvas.height = window.screen.height; + canvas.style = "display: block;"; + document.body.appendChild(canvas); + document.body.style.margin = 0; + + let context = canvas.getContext('webgpu'); + const canvasConfig = { + device, + format: navigator.gpu.getPreferredCanvasFormat(), + usage: GPUTextureUsage.RENDER_ATTACHMENT, + alphaMode: 'opaque' + }; + context.configure(canvasConfig); + + callback(); +} + +var DT = 0; +var last = Date.now() +async function Run(fn) { + // setup keyboard input + Input.setup() + + // create and run main game loop + const callback = () => { + var now = Date.now(); + DT = ( now - last) / 1000 + last = now + fn(DT) + window.requestAnimationFrame(callback) + }; + callback() +} diff --git a/js/world.js b/js/world.js new file mode 100644 index 0000000..a7de901 --- /dev/null +++ b/js/world.js @@ -0,0 +1,116 @@ +import { Chunk, AIR, INVALID, CHUNK_GENERATED, CHUNK_NEW } from "./chunk.js"; +import { Object, Vec3 } from "./wgpu/wgpu.js"; + +export class World { + + constructor(seed, shader) { + this.seed = seed; + this.shader = shader; + + this.camera = new Object(); + this.camera.position.y = 60; + this.camera.rotation.x = 30; + + this.viewX = null; + this.viewZ = null; + this.renderDistance = 10; + } + + update() { + // update camera and reload chunks if needed + let currentViewX = Math.floor(this.camera.position.x / Chunk.size.x); + let currentViewZ = Math.floor(this.camera.position.z / Chunk.size.z); + if (currentViewX != this.viewX || currentViewZ != this.viewZ) { + this.viewX = currentViewX; + this.viewZ = currentViewZ; + this.#updateChunks(); + } + + // update meshes + Chunk.forEach((chunk) => { + switch (chunk.state) { + case CHUNK_NEW: + chunk.generateAsync() + break; + case CHUNK_GENERATED: + chunk.createMeshAsync() + } + }); + } + + #updateChunks() { + let minX = this.viewX - this.renderDistance; + let minZ = this.viewZ - this.renderDistance; + let maxX = this.viewX + this.renderDistance; + let maxZ = this.viewZ + this.renderDistance; + + // remove old chunks + Chunk.forEach((chunk) => { + if (chunk.gridX < minX || chunk.gridZ < minZ || chunk.gridX > maxX || chunk.gridZ > maxZ) { + // chunk is out of bounds :( + chunk.delete(); + } + }); + + // create new chunks + for (let gridX = minX; gridX <= maxX; gridX++) { + for (let gridZ = minZ; gridZ <= maxZ; gridZ++) { + let id = Chunk.id(gridX, gridZ); + if (!Chunk.chunks[id]) { + new Chunk(gridX, gridZ, this.seed); + } + } + } + } + + /** + * @param {Renderer} source + */ + draw(renderer) { + let objects = []; + Chunk.forEach((chunk) => { + if (chunk.mesh) { + let object = new Object(chunk.mesh); + object.position.x = chunk.gridX * Chunk.size.x; + object.position.z = chunk.gridZ * Chunk.size.z; + objects.push(object); + } + }); + renderer.draw(this.camera, this.shader, objects); + } + + /** + * @param {Vec3} source + * @param {Vec3} normal + * @param {Number} reach + */ + raycast(source, normal, reach = 5, early = false) { + const STEP = 0.05; + let lastPos = null; + for (let dist = 0; dist <= reach; dist += STEP) { + let pos = source.addV(normal.mul(dist)).floor(); + let block = Chunk.getGlobalBlock(pos.x, pos.y, pos.z); + if (block != INVALID && block != AIR) { + return early ? lastPos : pos; + } + lastPos = pos; + } + return null; + } + + /** + * @param {Number} reach + */ + raycastCamera(reach = 5, early = false) { + let axis = this.camera.axis().normalize(); + return this.raycast(this.camera.position, axis, reach, early); + } + + placeBlock(pos, block) { + Chunk.setGlobalBlock(pos.x, pos.y, pos.z, block); + } + + breakBlock(pos) { + this.placeBlock(pos, AIR); + } +} |