summaryrefslogtreecommitdiff
path: root/js/wgpu/core
diff options
context:
space:
mode:
Diffstat (limited to 'js/wgpu/core')
-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
6 files changed, 715 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;
+ }
+}