summaryrefslogtreecommitdiff
path: root/js/wgpu
diff options
context:
space:
mode:
Diffstat (limited to 'js/wgpu')
-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
14 files changed, 1392 insertions, 0 deletions
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()
+}