From 7fef3c90d868ae74d26fa84e1225dd851e3b5516 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Wed, 2 Aug 2023 20:00:15 +0200 Subject: [PATCH] Pipeline creation, work on shader code --- src/_BinaryWriter.ts | 112 +++++++++++++++++++++++++++++ src/oktaeder.ts | 154 +++++++++++++++++++++++++++++++++++++++- src/shader.ts | 164 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 src/_BinaryWriter.ts diff --git a/src/_BinaryWriter.ts b/src/_BinaryWriter.ts new file mode 100644 index 0000000..4fc2652 --- /dev/null +++ b/src/_BinaryWriter.ts @@ -0,0 +1,112 @@ +import { Matrix4x4Object, Vector2Object, Vector3Object, Vector4Object } from "./data"; + +export class _BinaryWriter { + + static readonly DEFAULT_CAPACITY = 16; + + _buffer: ArrayBuffer; + _dataView: DataView; + _typedArray: Uint8Array; + _length: number; + + get subarray(): Uint8Array { return new Uint8Array(this._buffer, 0, this._length); } + + constructor(capacity = _BinaryWriter.DEFAULT_CAPACITY) { + capacity = Math.max(capacity, 1); + this._buffer = new ArrayBuffer(capacity); + this._dataView = new DataView(this._buffer); + this._typedArray = new Uint8Array(this._buffer); + this._length = 0; + } + + clear(): _BinaryWriter { + this._length = 0; + return this; + } + + ensureCapacity(desiredCapacity: number): _BinaryWriter { + if (this._buffer.byteLength >= desiredCapacity) { + return this; + } + + let newCapacity = this._buffer.byteLength * 2; + while (newCapacity < desiredCapacity) { + newCapacity *= 2; + } + + const newBuffer = new ArrayBuffer(newCapacity); + const newDataView = new DataView(newBuffer); + const newTypedArray = new Uint8Array(newBuffer); + + newTypedArray.set(this.subarray); + + this._buffer = newBuffer; + this._dataView = newDataView; + this._typedArray = newTypedArray; + return this; + } + + ensureUnusedCapacity(desiredUnusedCapacity: number): _BinaryWriter { + return this.ensureCapacity(this._buffer.byteLength + desiredUnusedCapacity); + } + + writeU32(value: number): _BinaryWriter { + this.ensureUnusedCapacity(4); + this._dataView.setUint32(this._length, value, true); + return this; + } + + writeF32(value: number): _BinaryWriter { + this.ensureUnusedCapacity(4); + this._dataView.setFloat32(this._length, value, true); + return this; + } + + writeVector2(value: Vector2Object): _BinaryWriter { + this.writeF32(value.x); + this.writeF32(value.y); + return this; + } + + writeVector3(value: Vector3Object): _BinaryWriter { + this.writeF32(value.x); + this.writeF32(value.y); + this.writeF32(value.z); + return this; + } + + writeVector4(value: Vector4Object): _BinaryWriter { + this.writeF32(value.x); + this.writeF32(value.y); + this.writeF32(value.z); + this.writeF32(value.w); + return this; + } + + writeMatrix4x4(value: Matrix4x4Object): _BinaryWriter { + this.writeF32(value.ix); + this.writeF32(value.iy); + this.writeF32(value.iz); + this.writeF32(value.iw); + this.writeF32(value.jx); + this.writeF32(value.jy); + this.writeF32(value.jz); + this.writeF32(value.jw); + this.writeF32(value.kx); + this.writeF32(value.ky); + this.writeF32(value.kz); + this.writeF32(value.kw); + this.writeF32(value.tx); + this.writeF32(value.ty); + this.writeF32(value.tz); + this.writeF32(value.tw); + return this; + } + + alloc(byteLength: number): DataView { + this.ensureUnusedCapacity(byteLength); + const dataView = new DataView(this._buffer, this._length, byteLength); + this._length += byteLength; + return dataView; + } +} diff --git a/src/oktaeder.ts b/src/oktaeder.ts index caf9fa4..2f75a91 100644 --- a/src/oktaeder.ts +++ b/src/oktaeder.ts @@ -4,8 +4,13 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ +export * from "./_BinaryWriter"; +export * from "./shader"; + +import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { Camera, Scene } from "./data"; import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; +import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader"; export class Renderer { @@ -23,11 +28,20 @@ export class Renderer { _depthBuffer: Texture2D; + _globalBindGroupLayout: GPUBindGroupLayout; + _materialBindGroupLayout: GPUBindGroupLayout; + _objectBindGroupLayout: GPUBindGroupLayout; + _pipelineLayout: GPUPipelineLayout; + + _pipelineCache: Map; + + _uniformWriter: BinaryWriter; + /** * This constructor is intended primarily for internal use. Consider using * `Renderer.createIndexBuffer` instead. */ - private constructor ( + private constructor( adapter: GPUAdapter, device: GPUDevice, context: GPUCanvasContext, @@ -69,9 +83,130 @@ export class Renderer { height: framebufferTexture.height, format: "depth", }); + + this._globalBindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { + hasDynamicOffset: true, + type: "uniform", + }, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + hasDynamicOffset: true, + type: "read-only-storage", + }, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + hasDynamicOffset: true, + type: "read-only-storage", + }, + }, + ], + label: "Global", + }); + this._materialBindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { + hasDynamicOffset: true, + type: "uniform", + }, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: { type: "filtering" }, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + { + binding: 3, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + { + binding: 4, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + { + binding: 5, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + { + binding: 6, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + { + binding: 7, + visibility: GPUShaderStage.FRAGMENT, + texture: { + sampleType: "float", + viewDimension: "2d", + }, + }, + ], + label: "Material", + }); + this._objectBindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { + hasDynamicOffset: true, + type: "uniform", + }, + }, + ], + label: "Object", + }); + + this._pipelineLayout = device.createPipelineLayout({ + bindGroupLayouts: [ + this._globalBindGroupLayout, + this._materialBindGroupLayout, + this._objectBindGroupLayout, + ], + }); + + this._pipelineCache = new Map(); + + this._uniformWriter = new BinaryWriter(); } - static async init(canvas: HTMLCanvasElement) { + static async init(canvas: HTMLCanvasElement): Promise { if (!navigator.gpu) { throw new Error("WebGPU is not supported"); } @@ -92,6 +227,8 @@ export class Renderer { const format = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format }); + + return new Renderer(adapter, device, context, format); } /** @@ -124,6 +261,19 @@ export class Renderer { return new VertexBuffer(this, props); } + _getOrCreatePipeline(flags: ShaderFlags): GPURenderPipeline { + const key = shaderFlagsKey(flags); + + let pipeline = this._pipelineCache.get(key); + if (pipeline !== undefined) { + return pipeline; + } + + pipeline = createPipeline(this, flags); + this._pipelineCache.set(key, pipeline); + return pipeline; + } + render(scene: Scene, camera: Camera): Renderer { const { width, height } = this._context.getCurrentTexture(); if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) { diff --git a/src/shader.ts b/src/shader.ts index e5aa6ae..05919b7 100644 --- a/src/shader.ts +++ b/src/shader.ts @@ -1,8 +1,127 @@ +import { Renderer } from "./oktaeder"; + +export type ShaderFlagKey = number; + export interface ShaderFlags { - texCoord: boolean; - lightTexCoord: boolean; - normal: boolean; - tangent: boolean; + readonly texCoord: boolean; + readonly lightTexCoord: boolean; + readonly normal: boolean; + readonly tangent: boolean; +} + +export function shaderFlagsKey({ + texCoord, + lightTexCoord, + normal, + tangent, +}: ShaderFlags): ShaderFlagKey { + let key = 0; + key |= Number(texCoord) << 0; + key |= Number(lightTexCoord) << 1; + key |= Number(normal) << 2; + key |= Number(tangent) << 3; + return key; +} + +export function createPipeline(renderer: Renderer, { + texCoord, + lightTexCoord, + normal, + tangent, +}: ShaderFlags): GPURenderPipeline { + const shaderCode = createShaderCode({ texCoord, lightTexCoord, normal, tangent }); + + const shaderModule = renderer._device.createShaderModule({ + code: shaderCode, + hints: { + "vert": { layout: renderer._pipelineLayout }, + "frag": { layout: renderer._pipelineLayout }, + }, + }); + + let vertexLocation = 0; + + const pipeline = renderer._device.createRenderPipeline({ + layout: renderer._pipelineLayout, + vertex: { + entryPoint: "vert", + module: shaderModule, + buffers: [ + { + arrayStride: 12, + attributes: [{ + shaderLocation: vertexLocation++, + format: "float32x3", + offset: 0, + }], + }, + ...(texCoord ? [{ + arrayStride: 8, + attributes: [{ + shaderLocation: vertexLocation++, + format: "float32x2", + offset: 0, + }], + } satisfies GPUVertexBufferLayout] : []), + ...(lightTexCoord ? [{ + arrayStride: 8, + attributes: [{ + shaderLocation: vertexLocation++, + format: "float32x2", + offset: 0, + }], + } satisfies GPUVertexBufferLayout] : []), + ...(normal ? [{ + arrayStride: 12, + attributes: [{ + shaderLocation: vertexLocation++, + format: "float32x3", + offset: 0, + }], + } satisfies GPUVertexBufferLayout] : []), + ...(tangent ? [{ + arrayStride: 16, + attributes: [{ + shaderLocation: vertexLocation++, + format: "float32x4", + offset: 0, + }], + } satisfies GPUVertexBufferLayout] : []), + ], + }, + fragment: { + entryPoint: "frag", + module: shaderModule, + targets: [{ + format: renderer._format, + blend: { + color: { + operation: "add", + srcFactor: "one", + dstFactor: "one-minus-src-alpha", + }, + alpha: { + operation: "add", + srcFactor: "one", + dstFactor: "one-minus-src-alpha", + }, + }, + writeMask: GPUColorWrite.ALL, + }], + }, + depthStencil: { + depthCompare: "greater", + depthWriteEnabled: true, + format: "depth32float", + }, + primitive: { + cullMode: "back", + frontFace: "ccw", + topology: "triangle-list", + }, + }); + + return pipeline; } export function createShaderCode({ @@ -84,6 +203,24 @@ struct ObjectUniforms { @group(1) @binding(6) var _EmissiveTexture: texture_2d; @group(1) @binding(7) var _TransmissionCollimationTexture: texture_2d; +fn screenSpaceMatrixTStoVS(positionVS: vec3, normalVS: vec3, texCoord: vec2) -> mat3x3 { + let q0 = dpdx(positionVS); + let q1 = dpdy(positionVS); + let uv0 = dpdx(texCoord); + let uv1 = dpdy(texCoord); + + let q1perp = cross(q1, normalVS); + let q0perp = cross(normalVS, q0); + + let tangentVS = q1perp * uv0.x + q0perp * uv1.x; + let bitangentVS = q1perp * uv0.y + q0perp * uv1.y; + + let det = max(dot(tangentVS, tangentVS), dot(bitangentVS, bitangentVS)); + let scale = (det == 0.0) ? 0.0 : inserseSqrt(det); + + return mat3x3(tangentVS * scale, bitangentVS * scale, normalVS); +} + @vertex fn vert(vertex: Vertex) -> Varyings { var output: Varyings; @@ -122,7 +259,7 @@ fn frag(fragment: Varyings) -> @location(0) vec2 { let baseColorPartialCoverageTexel = texture(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord); baseColor *= baseColorPartialCoverageTexel.rgb; partialCoverage *= baseColorPartialCoverageTexel.a; - let roughnessMetallicTexel = texture(_RoughnessMetallic, _Sampler, fragment.texCoord); + let roughnessMetallicTexel = texture(_RoughnessMetallicTexture, _Sampler, fragment.texCoord); roughness *= roughnessMetallicTexel.g; metallic *= roughnessMetallicTexel.b; let emissiveTexel = texture(_EmissiveTexture, _Sampler, fragment.texCoord); @@ -140,14 +277,19 @@ fn frag(fragment: Varyings) -> @location(0) vec2 { let dPositionVSdx = dpdx(positionVS); let dPositionVSdy = dpdy(positionVS); let geometricNormalVS = normalize(cross(dPositionVSdx, dPositionVSdy)); - let actualNormalVS = geometricNormalVS; `} ${texCoord ? ` - ` : ` - let actualNormalVS = geometricNormalVS; - `} - ${tangent ? ` - let tangentVS = + ${tangent ? ` + let tangentVS = normalize(fragment.tangentVS); + let bitangentVS = normalize(fragment.bitangentVS); + let matrixTStoVS = mat3x3(tangentVS, bitangentVS, geometricNormalVS); + ` : ` + let matrixTStoVS = screenSpaceMatrixTStoVS(positionVS, geometricNormalVS, fragment.texCoord); + `} + let normalTextureTexel = texture(_NormalTexture, _Sampler, fragment.texCoord); + var normalTS = normalTextureTexel.xyz * 2.0 - 1.0; + normalTS.xy *= _Material.normalScale; + let actualNormalVS = normalize(matrixTStoVS * geometricNormalVS); ` : ` let actualNormalVS = geometricNormalVS; `}