Pipeline creation, work on shader code

This commit is contained in:
Szymon Nowakowski 2023-08-02 20:00:15 +02:00
parent b2a59a0d6a
commit 3824c8f0c5
3 changed files with 417 additions and 13 deletions

112
src/_BinaryWriter.ts Normal file
View File

@ -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;
}
}

View File

@ -4,8 +4,13 @@
* obtain one at http://mozilla.org/MPL/2.0/. * 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 { Camera, Scene } from "./data";
import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources";
import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader";
export class Renderer { export class Renderer {
@ -23,11 +28,20 @@ export class Renderer {
_depthBuffer: Texture2D; _depthBuffer: Texture2D;
_globalBindGroupLayout: GPUBindGroupLayout;
_materialBindGroupLayout: GPUBindGroupLayout;
_objectBindGroupLayout: GPUBindGroupLayout;
_pipelineLayout: GPUPipelineLayout;
_pipelineCache: Map<ShaderFlagKey, GPURenderPipeline>;
_uniformWriter: BinaryWriter;
/** /**
* This constructor is intended primarily for internal use. Consider using * This constructor is intended primarily for internal use. Consider using
* `Renderer.createIndexBuffer` instead. * `Renderer.createIndexBuffer` instead.
*/ */
private constructor ( private constructor(
adapter: GPUAdapter, adapter: GPUAdapter,
device: GPUDevice, device: GPUDevice,
context: GPUCanvasContext, context: GPUCanvasContext,
@ -69,9 +83,130 @@ export class Renderer {
height: framebufferTexture.height, height: framebufferTexture.height,
format: "depth", 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<Renderer> {
if (!navigator.gpu) { if (!navigator.gpu) {
throw new Error("WebGPU is not supported"); throw new Error("WebGPU is not supported");
} }
@ -92,6 +227,8 @@ export class Renderer {
const format = navigator.gpu.getPreferredCanvasFormat(); const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format }); context.configure({ device, format });
return new Renderer(adapter, device, context, format);
} }
/** /**
@ -124,6 +261,19 @@ export class Renderer {
return new VertexBuffer(this, props); 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 { render(scene: Scene, camera: Camera): Renderer {
const { width, height } = this._context.getCurrentTexture(); const { width, height } = this._context.getCurrentTexture();
if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) { if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) {

View File

@ -1,8 +1,127 @@
import { Renderer } from "./oktaeder";
export type ShaderFlagKey = number;
export interface ShaderFlags { export interface ShaderFlags {
texCoord: boolean; readonly texCoord: boolean;
lightTexCoord: boolean; readonly lightTexCoord: boolean;
normal: boolean; readonly normal: boolean;
tangent: 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({ export function createShaderCode({
@ -84,6 +203,24 @@ struct ObjectUniforms {
@group(1) @binding(6) var _EmissiveTexture: texture_2d<f32>; @group(1) @binding(6) var _EmissiveTexture: texture_2d<f32>;
@group(1) @binding(7) var _TransmissionCollimationTexture: texture_2d<f32>; @group(1) @binding(7) var _TransmissionCollimationTexture: texture_2d<f32>;
fn screenSpaceMatrixTStoVS(positionVS: vec3<f32>, normalVS: vec3<f32>, texCoord: vec2<f32>) -> mat3x3<f32> {
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 @vertex
fn vert(vertex: Vertex) -> Varyings { fn vert(vertex: Vertex) -> Varyings {
var output: Varyings; var output: Varyings;
@ -122,7 +259,7 @@ fn frag(fragment: Varyings) -> @location(0) vec2<f32> {
let baseColorPartialCoverageTexel = texture(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord); let baseColorPartialCoverageTexel = texture(_BaseColorPartialCoverageTexture, _Sampler, fragment.texCoord);
baseColor *= baseColorPartialCoverageTexel.rgb; baseColor *= baseColorPartialCoverageTexel.rgb;
partialCoverage *= baseColorPartialCoverageTexel.a; partialCoverage *= baseColorPartialCoverageTexel.a;
let roughnessMetallicTexel = texture(_RoughnessMetallic, _Sampler, fragment.texCoord); let roughnessMetallicTexel = texture(_RoughnessMetallicTexture, _Sampler, fragment.texCoord);
roughness *= roughnessMetallicTexel.g; roughness *= roughnessMetallicTexel.g;
metallic *= roughnessMetallicTexel.b; metallic *= roughnessMetallicTexel.b;
let emissiveTexel = texture(_EmissiveTexture, _Sampler, fragment.texCoord); let emissiveTexel = texture(_EmissiveTexture, _Sampler, fragment.texCoord);
@ -140,14 +277,19 @@ fn frag(fragment: Varyings) -> @location(0) vec2<f32> {
let dPositionVSdx = dpdx(positionVS); let dPositionVSdx = dpdx(positionVS);
let dPositionVSdy = dpdy(positionVS); let dPositionVSdy = dpdy(positionVS);
let geometricNormalVS = normalize(cross(dPositionVSdx, dPositionVSdy)); let geometricNormalVS = normalize(cross(dPositionVSdx, dPositionVSdy));
let actualNormalVS = geometricNormalVS;
`} `}
${texCoord ? ` ${texCoord ? `
` : ` ${tangent ? `
let actualNormalVS = geometricNormalVS; let tangentVS = normalize(fragment.tangentVS);
`} let bitangentVS = normalize(fragment.bitangentVS);
${tangent ? ` let matrixTStoVS = mat3x3(tangentVS, bitangentVS, geometricNormalVS);
let tangentVS = ` : `
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; let actualNormalVS = geometricNormalVS;
`} `}