diff options
Diffstat (limited to 'js/wgpu/core')
| -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 |
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; + } +} |