diff options
| -rw-r--r-- | CS510_Graphics_Final_Presentation.pdf | bin | 0 -> 2274795 bytes | |||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | atlas.png | bin | 0 -> 3717 bytes | |||
| -rw-r--r-- | atlas.raw | 1 | ||||
| -rw-r--r-- | index.html | 29 | ||||
| -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 | ||||
| -rw-r--r-- | proposal.html | 145 | ||||
| -rw-r--r-- | shader.wgsl | 61 | ||||
| -rw-r--r-- | writeup.html | 167 |
27 files changed, 2973 insertions, 0 deletions
diff --git a/CS510_Graphics_Final_Presentation.pdf b/CS510_Graphics_Final_Presentation.pdf Binary files differnew file mode 100644 index 0000000..4d67f63 --- /dev/null +++ b/CS510_Graphics_Final_Presentation.pdf diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcb3c60 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ + +requires web server to run + +takes a moment to load on startup +* has to generate and mesh initial chunks +* js does not support multithreading :( diff --git a/atlas.png b/atlas.png Binary files differnew file mode 100644 index 0000000..6c11e0f --- /dev/null +++ b/atlas.png diff --git a/atlas.raw b/atlas.raw new file mode 100644 index 0000000..e1ab029 --- /dev/null +++ b/atlas.raw @@ -0,0 +1 @@ +]lJlJyV:yV:]lJlJyV:yV:Y=(yV:yV:]yV:]yV:lJY=(yV:yV:Y=(Y=(Y=(yV:Y=(lJyV:]lJY=(Y=(]yV:yV:Y=(]lJlJyV:]Y=(yV:yV:Y=(]yV:lJlll]yV:Y=(lJyV:]lJlJyV:Y=(yV:]lJyV:lJyV:lJ]Y=(lJyV:yV:lJY=(yV:]yV:lJY=(yV:]Y=(lJlJyV:lJY=(Y=(Y=(yV:yV:Y=(yV:yV:yV:]]yV:yV:yV:lllyV:yV:]]yV:]]yV:lJyV:lJyV:yV:]]lJlJY=(yV:lJY=(lJyV:yV:lJlJlJyV:yV:lJyV:lJY=(Y=(yV:lJyV:yV:lJlJY=(yV:yV:lJY=(yV:yV:Y=(Y=(yV:yV:lJyV:Y=(]]yV:lJyV:lJyV:]yV:lJ]lJY=(]]Y=(lJlJyV:lJyV:yV:lJyV:lJyV:lJllllJY=(Y=(lJlJyV:Y=(Y=(Y=(yV:lJyV:yV:]yV:yV:yV:yV:lJyV:yV:]]yV:lJyV:yV:tXCyV:lJlJyV:Y=(Y=(Y=(yV:]lJlJlJyV:yV:]yV:Y=(yV:Y=(]yV:yV:lJyV:yV:lJlJlJyV:]lJlJyV:yV:lJlJyV:yV:lJyV:Y=(tJvLsIf<f<oE_5lB~TvLj@g=i?a7P&mCuKlBZQSY=)h>b8_5c`sIa7lBg=kA]Y=)ld:i?Y=)pFY=)tJUbgY=)W-`6Y=)Y=)lllY=)Y=)qGY=)Y=)Y=)_5Y=)mCY=)yU:Y=)Y=)yU:lJyU:lJ\Y=)lJyU:yU:Y=)Y=)Y=)lllyU:lJY=)yU:yU:Y=)lJlJyU:lJY=)Y=)Y=)yU:yU:Y=)yU:yU:yU:\\yU:yU:yU:yU:yU:\\yU:\\yU:lJyU:lJyU:yU:\\lJlJyU:yU:lJY=)lJlJyU:yU:lJlJlJyU:yU:lJyU:lJyU:Y=)yU:lJlJyU:yU:yU:Y=)yU:yU:lJY=)yU:yU:Y=)Y=)yU:yU:yU:yU:yU:\\yU:lJyU:lJyU:\\yU:\lJY=)\\Y=)lJlJyU:lJyU:yU:lJlJ\yU:lJllllJlJyU:Y=)lJyU:Y=)yU:Y=)lJyU:lJlJ\yU:yU:yU:yU:yU:yU:yU:\\yU:lJyU:yU:tXDyU:lJlJyU:Y=)\Y=)yU:\lJlJlJyU:Y=)\yU:Y=)yU:Y=)\\yU:lJyU:yU:lJlJlJyU:\lJlJyU:yU:lJlJyU:yU:lJlJyU:Y=)h;^h;Y.Y.`3O*`3Y.h;a1Y.a1Y.H)`3h;`3|Bh;`H)`H)O*d8|Bh;``3Y.`3|B`3O*Y.a1a1^1`3h;|BY.n9Y.H)H)Y.H)Y.a1h;d8_O*d8O*Y._2h;`3`3Y.O*|BY.n9Y.n9n9a1n9|BH)H)Y.d8O*a1_2`O*H)|BH)h;h;_Y.h;a1Y.H)a1_2h;O*Y.|B`3d8Y.a1O*Y.Y.d8n9d8Y.h;`3`3Y.Y.`3`3O*|BY.h;n9Y.H)`3|BO*d8Y.`Y.|Bn9H)n9Y.|B|BH)d8Y.h;Y.O*h;H)|B`3_2O*|BH)Y.d8d8d8`3|B`3H)_Y.h;|BH)Y.`3h;`H)Y.Y.`3a1h;`3Y.m8Y.|Bh;Y.d8a1h;d8`3O*|BO*O*a1|Bh;G(a1`d8Y.H)a1d8a1O*|B|BH)h;H)Y.H)d8O*^H)`|Bh;Y.`|Ba1Y.n9|B|B^O*|Bd8h;h;O*h;h;Y.d8Y.`3H)Y.h;a1a1|BO*h;h;`3d8O*d8Y.`3a1Y.tttttttttttthhhttttttttttttttttttttthhhttthhhtttttthhhtttttttttttthhhtttttttttttttttttttttttttttttttttttthhhttttttttttttttthhhtttttthhhttthhhtttttttttttttttttthhhtttttttttttthhhttttttttttttttttttttthhhtttttttttttttttttthhhttthhhttthhhttttttttttttttttttttttttttttttttttttttttttttthhhtttttttttttttttttttttttttttttttttttthhhttttttttt͜զרեΞИʒљةߵצԠרӞצ٪ݲң͖̕Ϙ۰ۭۯԡЛЛެեܭњ͗ҜϙҜďԝקۭبӠϚդϗњԠԝМ٧͚ӠȒϙڦڪ֥ץȓןӞحڭ٬ݱ٫گȗΘΗӥÐӣΜ̕ԠѦɔ՟ѥϙڤΙ˕̗ќȒƒϤțϗءӜԞњΘ֫߫تڬըҜѝ٫њԞʔިנȝ̙͗ԝէؤ֬ٮӧ̖ܱҧ̜ʘњњϡѥԠčҟثќЛҦƖ˖ӝ՞֟ۥԤТ٦إ˞թϘϦӜқћϝ٭ա×ԞϛԞЛآסҜܦ͙̗ƾڪ֡ӝުס̖٣ŽŏӜњԞԣէϜɓ٫֩ŏž͗˕ϙߴХۮ֣٤ՠեУԧ̔ǑҢʛӜܪŒףÐ͗ا͞ȖϡӢԣ̕˗Ҡȑޱ͗ݬɕЛ֥џ̗ʖ4M-D-?+=-?+=.@4L1J1J/F/B1D3G:Z<]+>+>-?.@5O4K.D+>+=+=+=+=*<*<+;,;3G5N1I.@-?.@+=+=+=.@2J0I;V1G-C3G3G,?+=+=*<*<+=+=+=+=+=,?,?,A+@-B-?+>+=-?+=+=+=+=*<+=-?-?-?/B2H.D+=+=+=+=+=/B2J/B-?+=+=+=+=+=*<+=/B-?-?1D-?-?-?-?/A1D/B.@4L8R:W/G/B-?-?-?.@1D3G1E-?-?+=+=+=,A3L0I2E/B/B0C2F0C/B.A/B/B0C6O8R1H0I1J0C.A+=*<*<*<*<*<+=-?-?,A-B2H0F5O+=+=+=*<*<*<*<*<*<*<*<+=-?/B0C,?*<*<*<*<*<+=*<+=+=-?+=*<*<*<*<*<.F0G-?+=+=-?/B/B-?-?+=+=*<*<*<+=-D.B+>*<*<+=-?-?/A0B/A.@.@/B-?.@*<*<+=+=+=+=+=+=/A2F0C2H<[0E-?+>-?-?-?-?.@1D1D3G6P;Z=\2H2H2H/A.@
\ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..91de0e3 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="author" content="freya"> + <title>minecraft</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + background: black; + } + + canvas { + width: 100%; + height: 100%; + } + </style> + <script type="module" src="js/wgpu/wgpu.js"></script> + </head> + <body> + <script type="module" src="js/minecraft.js"></script> + </body> +</html> 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); + } +} diff --git a/proposal.html b/proposal.html new file mode 100644 index 0000000..c69114a --- /dev/null +++ b/proposal.html @@ -0,0 +1,145 @@ +<html> + <head> + <title>my very super duper cool proposal</title> + <style> +html { + background-color: black; + color:yellow; + font-family: sans-serif; + padding: 1rem; +} +a, button { + all: unset; + display: inline-block; + color:yellow; + border: 1px solid; + border-radius: 10px; + margin: .5rem 0; + margin-right: 1rem; + padding: .5rem; +} + +a:hover, button:hover { + background-color: #313244; + color: #a6adc8; + cursor: pointer; +} + + +.trail { + position: absolute; + z-index: 9999; + pointer-events: none; +} + + </style> + </head> + <body> + <h1>the proposal</h1> + <marquee>itz super kewl</marquee> + + <h2>group</h2> + me (freya) + + <h2>description</h2> + I, as I have done a few times before now, and I enjoy doing, would like to make a minecraft (legaly distinct) clone! (ik, so very unique, no one has done that before) + <br><br> + The component of this "clone" are described below + + <h3>part one (first milestone)</h3> + basic chunk rendering & textures + <ul> + <li>Chunks are generated (simple noise/height map), no greedy meshing yet, possibly fixed map + <li>Textures are loading, but no simple lighting, just ambient. Textures may be stolen from mojang. + <li>Player can move around + </ul> + + <h3>part twooooo (the second stone of mile)</h3> + <ul> + <li>Greedy meshing for chunks + <li>Simple lighting for chunks + <li>Player collision + <li>Procedural generation is fully working + <li>Textures are no longer stolen from mojang + </ul> + + <h3>other possible additions</h3> + These can be thought as "stretch goals" + <ul> + <li>Music (likely using browser audio system) + <li>Place and break blocks + <li>Game UI + </ul> + + Please let me know if you would like any of these to be a requirement :) + + <h2>johnvertize</h2> + <marquee>scream into the void</marquee> + <iframe src="https://john.citrons.xyz/embed?ref=freya.cat" style="margin-left:auto;display:block;margin-right:auto;max-width:732px;width:100%;height:94px;border:none;"></iframe> + + <div class="buttons"> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/eyes.webp?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/png" srcset="https://freya.cat/public/buttons/eyes.png?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/gif" srcset="https://freya.cat/public/buttons/eyes.gif?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/eyes.png?timestamp=1716556123" alt="Best viewed with eyes" title="Best viewed with eyes" width="88" height="30"> + </picture> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/vim.webp?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/png" srcset="https://freya.cat/public/buttons/vim.png?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/gif" srcset="https://freya.cat/public/buttons/vim.gif?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/vim.png?timestamp=1716556123" alt="Edited with VIM" title="Edited with VIM" width="88" height="30"> + </picture> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/gnu-linux.webp?timestamp=1716556123"> + <source type="image/png" srcset="https://freya.cat/public/buttons/gnu-linux.png?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/gnu-linux.png?timestamp=1716556123" alt="Made with GNU/Linux" title="Made with GNU/Linux" width="88" height="30"> + </picture> + <script> + alert('welcome') + + // thank u ryan, very cool + + var trailLength = 12; + var path = []; + var delay = 100; + var lastX = 0; + var lastY = 0; + + function createMouseTrail() { + for (var i = 0; i < trailLength; i++) { + var div = document.createElement('div'); + div.setAttribute('class', 'trail'); + div.style.top = '-200px'; + div.style.left = '-200px'; + div.style.backgroundImage = 'url(https://www.rochesterapex.com/css/cursor.gif)'; + div.style.backgroundSize = 'cover'; + div.style.width = '11px'; + div.style.height = '19px'; + document.body.appendChild(div); + path.push(div); + } + } + + var lastX = 0; + var lastY = 0; + + function moveTrail(e) { + if (lastX !== e.pageX || lastY !== e.pageY) { + for (let i = 0; i < path.length; i++) { + setTimeout(function() { + path[i].style.top = (e.pageY) + 'px'; + path[i].style.left = (e.pageX) + 'px'; + }, i * delay); + } + } + + lastX = e.pageX; + lastY = e.pageY; + } + + document.addEventListener('mousemove', moveTrail); + createMouseTrail(); + </script> + </body> +</html> diff --git a/shader.wgsl b/shader.wgsl new file mode 100644 index 0000000..ac43a44 --- /dev/null +++ b/shader.wgsl @@ -0,0 +1,61 @@ +struct VertexInput { + @location(0) position: vec3<f32>, + @location(1) normal: vec3<f32>, + @location(2) uv: vec2<f32>, + @location(3) block: vec3<f32>, + @location(4) material: f32, +}; + +struct VertexOutput { + @builtin(position) position: vec4<f32>, + @location(0) tint: vec3<f32>, + @location(1) uv: vec2<f32>, + @location(2) visibility: f32, + @location(3) material: f32, +}; + +struct Uniforms { + proj: mat4x4<f32>, + view: mat4x4<f32>, + skyColor: vec3<f32>, + cameraPos: vec3<f32>, + raycast: vec3<f32>, + renderDistance: f32, +}; + +@group(0) @binding(0) var<uniform> uniforms: Uniforms; +@group(1) @binding(0) var<uniform> tran: mat4x4<f32>; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var worldPosition = tran * vec4<f32>(input.position, 1.0); + output.position = uniforms.proj * uniforms.view * worldPosition; + output.tint = vec3(dot(abs(input.normal), vec3(0.8, 0.9, 0.75))); + output.uv = input.uv; + output.material = input.material; + + var dist = distance(uniforms.cameraPos.xz, worldPosition.xz); + var visibility = 1.0; + var fogDepth = 16.0; + if (dist / fogDepth > uniforms.renderDistance - 2) { + visibility = 1 - (dist - (fogDepth * (uniforms.renderDistance - 2))) / fogDepth; + } + output.visibility = clamp(visibility, 0.0, 1.0); + + if (all(uniforms.raycast == input.block)) { + output.tint = vec3(1.2); + } + + return output; +} + +@group(0) @binding(1) var texSampler: sampler; +@group(0) @binding(2) var texture: texture_2d_array<f32>; + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { + var color = vec4(input.tint, 1.0) * textureSample(texture, texSampler, input.uv, i32(input.material)); + var skyColor = vec4(uniforms.skyColor, 1.0); + return mix(skyColor, color, input.visibility); +} diff --git a/writeup.html b/writeup.html new file mode 100644 index 0000000..aa1da7e --- /dev/null +++ b/writeup.html @@ -0,0 +1,167 @@ +<html> + <head> + <title>my very super duper cool writeup</title> + <style> +html { + background-color: black; + color:yellow; + font-family: sans-serif; + padding: 1rem; +} +a, button { + all: unset; + display: inline-block; + color:yellow; + border: 1px solid; + border-radius: 10px; + margin: .5rem 0; + margin-right: 1rem; + padding: .5rem; +} + +a:hover, button:hover { + background-color: #313244; + color: #a6adc8; + cursor: pointer; +} + + +.trail { + position: absolute; + z-index: 9999; + pointer-events: none; +} + + </style> + </head> + <body> + <h1>the writeup</h1> + <marquee>woah its a funny html file again</marquee> + + <h2>overview</h2> + The project itself I would say was built quite well. It is reasonably able to render + chunks at a good enough framerate that im happy with it. I personally would of not picked + java script, mostly because of the multithreading support. + + <h3>js slow</h3> + + Being forced to halt rendering + so that the game can generate AND mesh chunks does cause quite noticeable lag spikes. + The best thing to do would create a render thread, and only have generation and meshing done + on its own thread, but I cannot do this in JS. + + <h3>vertex data</h3> + + As also described in the presentation, I do thing the vertex mesh for the chunk is not well + made. 192 bytes per quad (vec3 + vec3 + vec2 + vec3 + float) * 4, is really terrible for + gpu performance. The smaller the mesh, the quicker the gpu can process it each frame and the + high the fps. I have decided to continue this project but not in javascript, but in c, and I + have gotten it down to 4 bytes, horray! + + <pre> + <code> +typedef union { + struct { + u32 x : 5; + u32 y : 5; + u32 z : 5; + u32 width : 5; + u32 height : 5; + u32 face : 3; + u32 block : 4; + }; + u32 raw; +} Quad; + </code> + </pre> + + Above is the struct I am not using in C for per quad data. Each quad is then instanced on the gpu, stored in a uniform buffer. Then I use the instance index to index into the uniform of "Quad"s, to render each face. It is quite a lot faster than the approach I did in JS. + + You may also notice that this contains a width and height field, which allows us to use this + with greedy meshing! Horray!!! + + If you would like to take a look at my C code, you can find it at <a href="https://g.freya.cat/voxel">https://g.freya.cat/voxel</a>. Yes its OpenGL, but I've done Vulkan once, and never again. + + <h3>texture array</h3> + + The first time i made minecraft I ran into the issue where texture atlases would get blurd and wonky when msaa or mipmapping were turned on. This ended up being because the edges of each texture in atlas would get blured (msaa'd) togeather, and the edges of blocks would look terrible. The solution to this was to use a texture array (or a 3d texture), since each layer gets sampeled differently, and such there would be no bluring between the layers. + + <h3>what would i do differently</h3> + + So though all the issues I ran into, what would i do differently? Well thats easy, the C code I just linked to in the previous section. It fixes alot of the issues I ran into in the + JS voxel "engine", and also adds a whole new slew of features such as frustum culling and soon will have ambient occlusion. + + Frustum culling turns out is alot easier than I thought, because once can get each plane from the proj-view matrix. + + I rally had a fun time writing this, and I hope you think It was a cool project :) + + <h2>johnvertize</h2> + <marquee>scream into the void</marquee> + <iframe src="https://john.citrons.xyz/embed?ref=freya.cat" style="margin-left:auto;display:block;margin-right:auto;max-width:732px;width:100%;height:94px;border:none;"></iframe> + + <div class="buttons"> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/eyes.webp?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/png" srcset="https://freya.cat/public/buttons/eyes.png?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/gif" srcset="https://freya.cat/public/buttons/eyes.gif?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/eyes.png?timestamp=1716556123" alt="Best viewed with eyes" title="Best viewed with eyes" width="88" height="30"> + </picture> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/vim.webp?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/png" srcset="https://freya.cat/public/buttons/vim.png?timestamp=1716556123" media="(prefers-reduced-motion: reduce)"> + <source type="image/gif" srcset="https://freya.cat/public/buttons/vim.gif?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/vim.png?timestamp=1716556123" alt="Edited with VIM" title="Edited with VIM" width="88" height="30"> + </picture> + <picture> + <source type="image/webp" srcset="https://freya.cat/public/buttons/gnu-linux.webp?timestamp=1716556123"> + <source type="image/png" srcset="https://freya.cat/public/buttons/gnu-linux.png?timestamp=1716556123"> + <img src="https://freya.cat/public/buttons/gnu-linux.png?timestamp=1716556123" alt="Made with GNU/Linux" title="Made with GNU/Linux" width="88" height="30"> + </picture> + <script> + alert('welcome back') + + // thank u ryan, very cool + + var trailLength = 12; + var path = []; + var delay = 100; + var lastX = 0; + var lastY = 0; + + function createMouseTrail() { + for (var i = 0; i < trailLength; i++) { + var div = document.createElement('div'); + div.setAttribute('class', 'trail'); + div.style.top = '-200px'; + div.style.left = '-200px'; + div.style.backgroundImage = 'url(https://www.rochesterapex.com/css/cursor.gif)'; + div.style.backgroundSize = 'cover'; + div.style.width = '11px'; + div.style.height = '19px'; + document.body.appendChild(div); + path.push(div); + } + } + + var lastX = 0; + var lastY = 0; + + function moveTrail(e) { + if (lastX !== e.pageX || lastY !== e.pageY) { + for (let i = 0; i < path.length; i++) { + setTimeout(function() { + path[i].style.top = (e.pageY) + 'px'; + path[i].style.left = (e.pageX) + 'px'; + }, i * delay); + } + } + + lastX = e.pageX; + lastY = e.pageY; + } + + document.addEventListener('mousemove', moveTrail); + createMouseTrail(); + </script> + </body> +</html> |