summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/chunk.js655
-rw-r--r--js/controller.js182
-rw-r--r--js/minecraft.js55
-rw-r--r--js/noise.js164
-rw-r--r--js/wgpu/core/Buffer.js50
-rw-r--r--js/wgpu/core/Mesh.js48
-rw-r--r--js/wgpu/core/Object.js105
-rw-r--r--js/wgpu/core/Renderer.js172
-rw-r--r--js/wgpu/core/Shader.js289
-rw-r--r--js/wgpu/core/Texture.js51
-rw-r--r--js/wgpu/io/File.js11
-rw-r--r--js/wgpu/io/Input.js62
-rw-r--r--js/wgpu/math/Mat4.js126
-rw-r--r--js/wgpu/math/Math.js3
-rw-r--r--js/wgpu/math/Vec2.js103
-rw-r--r--js/wgpu/math/Vec3.js156
-rw-r--r--js/wgpu/math/Vec4.js132
-rw-r--r--js/wgpu/wgpu.js84
-rw-r--r--js/world.js116
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);
+ }
+}