From 3ca88ca9bf1ffb735d9e1415c9afd49fdb2b9d7e Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sat, 5 Aug 2023 15:41:10 +0200 Subject: [PATCH] Camera projection, upload lights and global uniforms, issue draw calls --- src/data/Camera.ts | 39 +++++++-- src/data/Matrix4x4.ts | 59 +++++++++++++ src/data/Scene.ts | 18 +++- src/data/Vector2.ts | 7 ++ src/data/Vector3.ts | 8 ++ src/data/Vector4.ts | 9 ++ src/oktaeder.ts | 158 +++++++++++++++++++++++++++++++---- src/resources/IndexBuffer.ts | 3 + src/shader.ts | 11 ++- 9 files changed, 284 insertions(+), 28 deletions(-) diff --git a/src/data/Camera.ts b/src/data/Camera.ts index cf0b598..e656542 100644 --- a/src/data/Camera.ts +++ b/src/data/Camera.ts @@ -4,7 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Node } from "."; +import { Matrix4x4, Node } from "."; export type Camera = OrthographicCamera | PerspectiveCamera; @@ -30,7 +30,7 @@ export class OrthographicCamera { _name: string; - _verticalSize: number; + _halfVerticalSize: number; _nearPlane: number; _farPlane: number; @@ -45,7 +45,7 @@ export class OrthographicCamera { }: OrthographicCameraProps) { this._name = name; - this._verticalSize = verticalSize; + this._halfVerticalSize = verticalSize; this._nearPlane = nearPlane; this._farPlane = farPlane; @@ -55,8 +55,8 @@ export class OrthographicCamera { set name(value: string) { this._name = value; } get name(): string { return this._name; } - set verticalSize(value: number) { this._verticalSize = value; } - get verticalSize(): number { return this._verticalSize; } + set halfVerticalSize(value: number) { this._halfVerticalSize = value; } + get halfVerticalSize(): number { return this._halfVerticalSize; } set nearPlane(value: number) { this._nearPlane = value; } get nearPlane(): number { return this._nearPlane; } @@ -87,6 +87,16 @@ export class OrthographicCamera { this._node = null; return this; } + + computeProjectionMatrix(aspectRatio: number, res: Matrix4x4): Matrix4x4 { + const halfHorizontalSize = this._halfVerticalSize / aspectRatio; + return res.set( + 1 / halfHorizontalSize, 0, 0, 0, + 0, 1 / this._halfVerticalSize, 0, 0, + 0, 0, 1 / (this._nearPlane - this._farPlane), 0, + 0, 0, this._farPlane / (this._farPlane - this._nearPlane), 1, + ); + } } export class PerspectiveCamera { @@ -149,6 +159,25 @@ export class PerspectiveCamera { this._node = null; return this; } + + computeProjectionMatrix(aspectRatio: number, res: Matrix4x4): Matrix4x4 { + const halfVerticalCotangent = 1 / Math.tan(0.5 * this._verticalFovRad); + if (this._farPlane === Infinity) { + return res.set( + halfVerticalCotangent / aspectRatio, 0, 0, 0, + 0, halfVerticalCotangent, 0, 0, + 0, 0, 0, 1, + 0, 0, this._nearPlane, 0, + ); + } else { + return res.set( + halfVerticalCotangent / aspectRatio, 0, 0, 0, + 0, halfVerticalCotangent, 0, 0, + 0, 0, this._nearPlane / (this._nearPlane - this._farPlane), 1, + 0, 0, this._nearPlane * this._farPlane / (this._farPlane - this._nearPlane), 0, + ); + } + } } Object.defineProperty(OrthographicCamera.prototype, "type", { value: "OrthographicCamera" }); diff --git a/src/data/Matrix4x4.ts b/src/data/Matrix4x4.ts index 546e11a..4e7790b 100644 --- a/src/data/Matrix4x4.ts +++ b/src/data/Matrix4x4.ts @@ -155,6 +155,31 @@ export class Matrix4x4 { ); } + set( + ix: number, iy: number, iz: number, iw: number, + jx: number, jy: number, jz: number, jw: number, + kx: number, ky: number, kz: number, kw: number, + tx: number, ty: number, tz: number, tw: number, + ): Matrix4x4 { + this.ix = ix; + this.iy = iy; + this.iz = iz; + this.iw = iw; + this.jx = jx; + this.jy = jy; + this.jz = jz; + this.jw = jw; + this.kx = kx; + this.ky = ky; + this.kz = kz; + this.kw = kw; + this.tx = tx; + this.ty = ty; + this.tz = tz; + this.tw = tw; + return this; + } + setObject(object: Matrix4x4Object): Matrix4x4 { this.ix = object.ix; this.iy = object.iy; @@ -507,6 +532,40 @@ export class Matrix4x4 { return this; } + inverseAffine(): Matrix4x4 { + const ix = this.ix; + const iy = this.iy; + const iz = this.iz; + const jx = this.jx; + const jy = this.jy; + const jz = this.jz; + const kx = this.kx; + const ky = this.ky; + const kz = this.kz; + const tx = this.tx; + const ty = this.ty; + const tz = this.tz; + + const det = ix * jy * kz + iy * jz * kx + iz * jx * ky + - ix * jz * ky - iy * jx * kz - iz * jy * kx; + const invDet = 1 / det; + + this.ix = invDet * (jy * kz - jz * ky); + this.iy = invDet * (iz * ky - iy * kz); + this.iz = invDet * (iy * jz - iz * jy); + this.jx = invDet * (jz * kx - jx * kz); + this.jy = invDet * (ix * kz - iz * kx); + this.jz = invDet * (iz * jx - ix * jz); + this.kx = invDet * (jx * ky - jy * kx); + this.ky = invDet * (iy * kx - ix * ky); + this.kz = invDet * (ix * jy - iy * jx); + this.tx = -(this.ix * tx + this.jx * ty + this.kx * tz); + this.ty = -(this.iy * tx + this.jy * ty + this.ky * tz); + this.tz = -(this.iz * tx + this.jz * ty + this.kz * tz); + + return this; + } + inverseTransposeAffine(): Matrix4x4 { const ix = this.ix; const iy = this.iy; diff --git a/src/data/Scene.ts b/src/data/Scene.ts index d35499f..f39ed5d 100644 --- a/src/data/Scene.ts +++ b/src/data/Scene.ts @@ -4,12 +4,14 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Node } from "."; +import { Color, ColorObject, Node } from "."; export interface SceneProps { readonly name?: string; readonly nodes?: Node[]; + + readonly ambientLight?: ColorObject; } export class Scene { @@ -20,17 +22,31 @@ export class Scene { _nodes: Node[]; + _ambientLight: Color; + constructor({ name = "", nodes = [], + ambientLight, }: SceneProps) { this._name = name; this._nodes = nodes; + + this._ambientLight = ambientLight !== undefined ? Color.fromObject(ambientLight) : Color.black(); } set name(value: string) { this._name = value; } get name(): string { return this._name; } + + setAmbientLight(value: ColorObject): Scene { + this._ambientLight.setObject(value); + return this; + } + + getAmbientLight(res: Color): Color { + return res.setObject(this._ambientLight); + } } Object.defineProperty(Scene.prototype, "type", { value: "Scene" }); diff --git a/src/data/Vector2.ts b/src/data/Vector2.ts index 33d7f75..335274a 100644 --- a/src/data/Vector2.ts +++ b/src/data/Vector2.ts @@ -78,6 +78,13 @@ export class Vector2 { this.y = y; return this; } + + normalize(): Vector2 { + const l = Math.sqrt(this.x * this.x + this.y * this.y); + this.x /= l; + this.y /= l; + return this; + } } Object.defineProperty(Vector2.prototype, "type", { value: "Vector2" }); diff --git a/src/data/Vector3.ts b/src/data/Vector3.ts index 4cbb6dc..f5d6aac 100644 --- a/src/data/Vector3.ts +++ b/src/data/Vector3.ts @@ -91,6 +91,14 @@ export class Vector3 { this.z = z; return this; } + + normalize(): Vector3 { + const l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + this.x /= l; + this.y /= l; + this.z /= l; + return this; + } } Object.defineProperty(Vector3.prototype, "type", { value: "Vector3" }); diff --git a/src/data/Vector4.ts b/src/data/Vector4.ts index 339b097..a556829 100644 --- a/src/data/Vector4.ts +++ b/src/data/Vector4.ts @@ -104,6 +104,15 @@ export class Vector4 { this.w = w; return this; } + + normalize(): Vector4 { + const l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); + this.x /= l; + this.y /= l; + this.z /= l; + this.w /= l; + return this; + } } Object.defineProperty(Vector4.prototype, "type", { value: "Vector4" }); diff --git a/src/oktaeder.ts b/src/oktaeder.ts index ad7f2ec..63de207 100644 --- a/src/oktaeder.ts +++ b/src/oktaeder.ts @@ -9,17 +9,34 @@ export * from "./shader"; import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { _Mapping as Mapping } from "./_Mapping"; -import { Camera, Material, Matrix4x4, Node, Scene, preOrder } from "./data"; +import { Camera, Material, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isPointLight, preOrder } from "./data"; import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader"; -const _normalMatrix = new Matrix4x4( +const _matrixOStoWSNormal = new Matrix4x4( NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, ); +const _matrixWStoVS = new Matrix4x4( + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, +); + +const _matrixVStoCS = new Matrix4x4( + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, +); + +const _directionWS = new Vector3(NaN, NaN, NaN); +const _positionWS = new Vector3(NaN, NaN, NaN); + export class Renderer { _adapter: GPUAdapter; @@ -45,11 +62,14 @@ export class Renderer { _uniformWriter: BinaryWriter; _uniformBuffer: GPUBuffer; - _directionalLightBuffer: GPUBuffer; + + _lightWriter: BinaryWriter; _pointLightBuffer: GPUBuffer; + _directionalLightBuffer: GPUBuffer; _sampler: GPUSampler; + _globalBindGroup: GPUBindGroup; _objectBindGroup: GPUBindGroup; /** @@ -113,7 +133,6 @@ export class Renderer { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { - hasDynamicOffset: true, type: "read-only-storage", }, }, @@ -121,7 +140,6 @@ export class Renderer { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { - hasDynamicOffset: true, type: "read-only-storage", }, }, @@ -224,11 +242,13 @@ export class Renderer { usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, label: "Uniform", }); - this._directionalLightBuffer = device.createBuffer({ + + this._lightWriter = new BinaryWriter(); + this._pointLightBuffer = device.createBuffer({ size: 1024 * 32, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }); - this._pointLightBuffer = device.createBuffer({ + this._directionalLightBuffer = device.createBuffer({ size: 1024 * 32, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, }); @@ -243,6 +263,15 @@ export class Renderer { maxAnisotropy: 16, }); + this._globalBindGroup = device.createBindGroup({ + layout: this._globalBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this._uniformBuffer } }, + { binding: 1, resource: { buffer: this._pointLightBuffer } }, + { binding: 2, resource: { buffer: this._directionalLightBuffer } }, + ], + label: "Global", + }); this._objectBindGroup = device.createBindGroup({ layout: this._objectBindGroupLayout, entries: [ @@ -320,6 +349,11 @@ export class Renderer { } render(scene: Scene, camera: Camera): Renderer { + const cameraNode = camera._node; + if (cameraNode === null) { + throw new Error(`Cannot render with a detached camera. Camera [${camera._name}] is not attached to a node.`); + } + const { width, height } = this._context.getCurrentTexture(); if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) { this._depthBuffer.resizeDiscard({ @@ -346,7 +380,7 @@ export class Renderer { this._uniformWriter.clear(); - // material gather + // gather materials const materialMapping = new Mapping(); for (const node of preOrder(scene._nodes)) { @@ -385,7 +419,7 @@ export class Renderer { return { offset, bindGroup }; }); - // object gather + // gather objects const objectMapping = new Mapping(); for (const node of preOrder(scene._nodes)) { @@ -398,20 +432,112 @@ export class Renderer { const offset = this._uniformWriter._length; object._updateWorldMatrix(); this._uniformWriter.writeMatrix4x4(object._worldMatrix); - this._uniformWriter.writeMatrix4x4(_normalMatrix.setObject(object._worldMatrix).inverseTransposeAffine()); + this._uniformWriter.writeMatrix4x4(_matrixOStoWSNormal.setObject(object._worldMatrix).inverseTransposeAffine()); return offset; }); - // directional lights gather + // gather point lights - // TODO + this._lightWriter.clear(); + let pointLightCount = 0; + for (const node of preOrder(scene._nodes)) { + const light = node._light; + if (!isPointLight(light)) continue; - // point lights gather + node._updateWorldMatrix(); + _positionWS.set(node._worldMatrix.tx, node._worldMatrix.ty, node._worldMatrix.tz); - // TODO + this._lightWriter.writeVector3(_positionWS); + this._lightWriter.writeU32(0); + this._lightWriter.writeColorF32(light._color); + this._lightWriter.writeU32(0); - void materialBindGroups; - void objectOffsets; + pointLightCount += 1; + } + + this._device.queue.writeBuffer(this._pointLightBuffer, 0, this._lightWriter.subarray); + + // gather directional lights + + this._lightWriter.clear(); + let directionalLightCount = 0; + for (const node of preOrder(scene._nodes)) { + const light = node._light; + if (!isDirectionalLight(light)) continue; + + node._updateWorldMatrix(); + _directionWS.set(-node._worldMatrix.kx, -node._worldMatrix.ky, -node._worldMatrix.kz); + _directionWS.normalize(); + + this._lightWriter.writeVector3(_directionWS); + this._lightWriter.writeU32(0); + this._lightWriter.writeColorF32(light._color); + this._lightWriter.writeU32(0); + + directionalLightCount += 1; + } + + this._device.queue.writeBuffer(this._directionalLightBuffer, 0, this._lightWriter.subarray); + + // global uniforms + + const globalUniformsOffset = this._uniformWriter._length; + cameraNode._updateWorldMatrix(); + _matrixWStoVS.setObject(cameraNode._worldMatrix).inverseAffine(); + camera.computeProjectionMatrix(width / height, _matrixVStoCS); + + this._uniformWriter.writeMatrix4x4(_matrixWStoVS); + this._uniformWriter.writeMatrix4x4(_matrixVStoCS); + this._uniformWriter.writeColorF32(scene._ambientLight); + this._uniformWriter.writeU32(pointLightCount); + this._uniformWriter.writeU32(directionalLightCount); + this._uniformWriter.writeU32(0); + this._uniformWriter.writeU32(0); + this._uniformWriter.writeU32(0); + + // upload uniforms + + this._device.queue.writeBuffer(this._uniformBuffer, 0, this._uniformWriter.subarray); + + // render + + pass.setBindGroup(0, this._globalBindGroup, [globalUniformsOffset]); + + for (let oi = 0; oi < objectMapping.table.length; ++oi) { + const object = objectMapping.table[oi]!; + const objectOffset = objectOffsets[oi]!; + const mesh = object.mesh!; + const { _vertexBuffer: vertexBuffer, _indexBuffer: indexBuffer } = mesh; + + const flags: ShaderFlags = { + texCoord: vertexBuffer._texCoordBuffer !== null, + lightTexCoord: vertexBuffer._lightTexCoordBuffer !== null, + normal: vertexBuffer._normalBuffer !== null, + tangent: vertexBuffer._tangentBuffer !== null, + }; + + const renderPipeline = this._getOrCreatePipeline(flags); + + pass.setPipeline(renderPipeline); + + pass.setVertexBuffer(0, vertexBuffer._positionBuffer); + pass.setVertexBuffer(1, vertexBuffer._texCoordBuffer); + pass.setVertexBuffer(2, vertexBuffer._lightTexCoordBuffer); + pass.setVertexBuffer(3, vertexBuffer._normalBuffer); + pass.setVertexBuffer(4, vertexBuffer._tangentBuffer); + pass.setIndexBuffer(indexBuffer._buffer, indexBuffer._indexFormat); + + pass.setBindGroup(2, this._objectBindGroup, [objectOffset]); + + for (let si = 0; si < mesh._submeshes.length; ++si) { + const submesh = mesh._submeshes[si]!; + const material = object._materials[si]!; + const { bindGroup: materialBindGroup, offset: materialOffset } = materialBindGroups[materialMapping.get(material)!]!; + + pass.setBindGroup(1, materialBindGroup, [materialOffset]); + pass.drawIndexed(submesh.length, 1, submesh.start, 0, 0); + } + } pass.end(); diff --git a/src/resources/IndexBuffer.ts b/src/resources/IndexBuffer.ts index 42765b4..d4b897a 100644 --- a/src/resources/IndexBuffer.ts +++ b/src/resources/IndexBuffer.ts @@ -116,6 +116,9 @@ export class IndexBuffer { indexCount, }); } + + get indexFormat(): "uint16" | "uint32" { return this._indexFormat; } + get indexSize(): number { return indexSize(this._indexFormat); } } Object.defineProperty(IndexBuffer.prototype, "type", { value: "IndexBuffer" }); diff --git a/src/shader.ts b/src/shader.ts index 434ec43..1da2b99 100644 --- a/src/shader.ts +++ b/src/shader.ts @@ -136,16 +136,15 @@ export function createShaderCode({ normal, tangent, }: ShaderFlags): string { - let vertexLocation = 0; let varyingLocation = 0; return ` struct Vertex { - @location(${vertexLocation++}) positionOS: vec3, - ${texCoord ? `@location(${vertexLocation++}) texCoord: vec2,` : ""} - ${lightTexCoord ? `@location(${vertexLocation++}) lightTexCoord: vec2,` : ""} - ${normal ? `@location(${vertexLocation++}) normalOS: vec3,` : ""} - ${normal && tangent ? `@location(${vertexLocation++}) tangentOS: vec4,` : ""} + @location(0) positionOS: vec3, + ${texCoord ? `@location(1) texCoord: vec2,` : ""} + ${lightTexCoord ? `@location(2) lightTexCoord: vec2,` : ""} + ${normal ? `@location(3) normalOS: vec3,` : ""} + ${normal && tangent ? `@location(4) tangentOS: vec4,` : ""} } struct Varyings {