Camera projection, upload lights and global uniforms, issue draw calls

This commit is contained in:
Szymon Nowakowski 2023-08-05 15:41:10 +02:00
parent bffb997d5c
commit 3ca88ca9bf
9 changed files with 284 additions and 28 deletions

View File

@ -4,7 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Node } from "."; import { Matrix4x4, Node } from ".";
export type Camera = OrthographicCamera | PerspectiveCamera; export type Camera = OrthographicCamera | PerspectiveCamera;
@ -30,7 +30,7 @@ export class OrthographicCamera {
_name: string; _name: string;
_verticalSize: number; _halfVerticalSize: number;
_nearPlane: number; _nearPlane: number;
_farPlane: number; _farPlane: number;
@ -45,7 +45,7 @@ export class OrthographicCamera {
}: OrthographicCameraProps) { }: OrthographicCameraProps) {
this._name = name; this._name = name;
this._verticalSize = verticalSize; this._halfVerticalSize = verticalSize;
this._nearPlane = nearPlane; this._nearPlane = nearPlane;
this._farPlane = farPlane; this._farPlane = farPlane;
@ -55,8 +55,8 @@ export class OrthographicCamera {
set name(value: string) { this._name = value; } set name(value: string) { this._name = value; }
get name(): string { return this._name; } get name(): string { return this._name; }
set verticalSize(value: number) { this._verticalSize = value; } set halfVerticalSize(value: number) { this._halfVerticalSize = value; }
get verticalSize(): number { return this._verticalSize; } get halfVerticalSize(): number { return this._halfVerticalSize; }
set nearPlane(value: number) { this._nearPlane = value; } set nearPlane(value: number) { this._nearPlane = value; }
get nearPlane(): number { return this._nearPlane; } get nearPlane(): number { return this._nearPlane; }
@ -87,6 +87,16 @@ export class OrthographicCamera {
this._node = null; this._node = null;
return this; 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 { export class PerspectiveCamera {
@ -149,6 +159,25 @@ export class PerspectiveCamera {
this._node = null; this._node = null;
return this; 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" }); Object.defineProperty(OrthographicCamera.prototype, "type", { value: "OrthographicCamera" });

View File

@ -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 { setObject(object: Matrix4x4Object): Matrix4x4 {
this.ix = object.ix; this.ix = object.ix;
this.iy = object.iy; this.iy = object.iy;
@ -507,6 +532,40 @@ export class Matrix4x4 {
return this; 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 { inverseTransposeAffine(): Matrix4x4 {
const ix = this.ix; const ix = this.ix;
const iy = this.iy; const iy = this.iy;

View File

@ -4,12 +4,14 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Node } from "."; import { Color, ColorObject, Node } from ".";
export interface SceneProps { export interface SceneProps {
readonly name?: string; readonly name?: string;
readonly nodes?: Node[]; readonly nodes?: Node[];
readonly ambientLight?: ColorObject;
} }
export class Scene { export class Scene {
@ -20,17 +22,31 @@ export class Scene {
_nodes: Node[]; _nodes: Node[];
_ambientLight: Color;
constructor({ constructor({
name = "", name = "",
nodes = [], nodes = [],
ambientLight,
}: SceneProps) { }: SceneProps) {
this._name = name; this._name = name;
this._nodes = nodes; this._nodes = nodes;
this._ambientLight = ambientLight !== undefined ? Color.fromObject(ambientLight) : Color.black();
} }
set name(value: string) { this._name = value; } set name(value: string) { this._name = value; }
get name(): string { return this._name; } 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" }); Object.defineProperty(Scene.prototype, "type", { value: "Scene" });

View File

@ -78,6 +78,13 @@ export class Vector2 {
this.y = y; this.y = y;
return this; 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" }); Object.defineProperty(Vector2.prototype, "type", { value: "Vector2" });

View File

@ -91,6 +91,14 @@ export class Vector3 {
this.z = z; this.z = z;
return this; 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" }); Object.defineProperty(Vector3.prototype, "type", { value: "Vector3" });

View File

@ -104,6 +104,15 @@ export class Vector4 {
this.w = w; this.w = w;
return this; 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" }); Object.defineProperty(Vector4.prototype, "type", { value: "Vector4" });

View File

@ -9,17 +9,34 @@ export * from "./shader";
import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter";
import { _Mapping as Mapping } from "./_Mapping"; 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 { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources";
import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader"; 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,
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 { export class Renderer {
_adapter: GPUAdapter; _adapter: GPUAdapter;
@ -45,11 +62,14 @@ export class Renderer {
_uniformWriter: BinaryWriter; _uniformWriter: BinaryWriter;
_uniformBuffer: GPUBuffer; _uniformBuffer: GPUBuffer;
_directionalLightBuffer: GPUBuffer;
_lightWriter: BinaryWriter;
_pointLightBuffer: GPUBuffer; _pointLightBuffer: GPUBuffer;
_directionalLightBuffer: GPUBuffer;
_sampler: GPUSampler; _sampler: GPUSampler;
_globalBindGroup: GPUBindGroup;
_objectBindGroup: GPUBindGroup; _objectBindGroup: GPUBindGroup;
/** /**
@ -113,7 +133,6 @@ export class Renderer {
binding: 1, binding: 1,
visibility: GPUShaderStage.FRAGMENT, visibility: GPUShaderStage.FRAGMENT,
buffer: { buffer: {
hasDynamicOffset: true,
type: "read-only-storage", type: "read-only-storage",
}, },
}, },
@ -121,7 +140,6 @@ export class Renderer {
binding: 2, binding: 2,
visibility: GPUShaderStage.FRAGMENT, visibility: GPUShaderStage.FRAGMENT,
buffer: { buffer: {
hasDynamicOffset: true,
type: "read-only-storage", type: "read-only-storage",
}, },
}, },
@ -224,11 +242,13 @@ export class Renderer {
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
label: "Uniform", label: "Uniform",
}); });
this._directionalLightBuffer = device.createBuffer({
this._lightWriter = new BinaryWriter();
this._pointLightBuffer = device.createBuffer({
size: 1024 * 32, size: 1024 * 32,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
}); });
this._pointLightBuffer = device.createBuffer({ this._directionalLightBuffer = device.createBuffer({
size: 1024 * 32, size: 1024 * 32,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
}); });
@ -243,6 +263,15 @@ export class Renderer {
maxAnisotropy: 16, 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({ this._objectBindGroup = device.createBindGroup({
layout: this._objectBindGroupLayout, layout: this._objectBindGroupLayout,
entries: [ entries: [
@ -320,6 +349,11 @@ export class Renderer {
} }
render(scene: Scene, camera: Camera): 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(); const { width, height } = this._context.getCurrentTexture();
if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) { if (this._depthBuffer.width !== width || this._depthBuffer.height !== height) {
this._depthBuffer.resizeDiscard({ this._depthBuffer.resizeDiscard({
@ -346,7 +380,7 @@ export class Renderer {
this._uniformWriter.clear(); this._uniformWriter.clear();
// material gather // gather materials
const materialMapping = new Mapping<Material>(); const materialMapping = new Mapping<Material>();
for (const node of preOrder(scene._nodes)) { for (const node of preOrder(scene._nodes)) {
@ -385,7 +419,7 @@ export class Renderer {
return { offset, bindGroup }; return { offset, bindGroup };
}); });
// object gather // gather objects
const objectMapping = new Mapping<Node>(); const objectMapping = new Mapping<Node>();
for (const node of preOrder(scene._nodes)) { for (const node of preOrder(scene._nodes)) {
@ -398,20 +432,112 @@ export class Renderer {
const offset = this._uniformWriter._length; const offset = this._uniformWriter._length;
object._updateWorldMatrix(); object._updateWorldMatrix();
this._uniformWriter.writeMatrix4x4(object._worldMatrix); this._uniformWriter.writeMatrix4x4(object._worldMatrix);
this._uniformWriter.writeMatrix4x4(_normalMatrix.setObject(object._worldMatrix).inverseTransposeAffine()); this._uniformWriter.writeMatrix4x4(_matrixOStoWSNormal.setObject(object._worldMatrix).inverseTransposeAffine());
return offset; 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; pointLightCount += 1;
void objectOffsets; }
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(); pass.end();

View File

@ -116,6 +116,9 @@ export class IndexBuffer {
indexCount, indexCount,
}); });
} }
get indexFormat(): "uint16" | "uint32" { return this._indexFormat; }
get indexSize(): number { return indexSize(this._indexFormat); }
} }
Object.defineProperty(IndexBuffer.prototype, "type", { value: "IndexBuffer" }); Object.defineProperty(IndexBuffer.prototype, "type", { value: "IndexBuffer" });

View File

@ -136,16 +136,15 @@ export function createShaderCode({
normal, normal,
tangent, tangent,
}: ShaderFlags): string { }: ShaderFlags): string {
let vertexLocation = 0;
let varyingLocation = 0; let varyingLocation = 0;
return ` return `
struct Vertex { struct Vertex {
@location(${vertexLocation++}) positionOS: vec3<f32>, @location(0) positionOS: vec3<f32>,
${texCoord ? `@location(${vertexLocation++}) texCoord: vec2<f32>,` : ""} ${texCoord ? `@location(1) texCoord: vec2<f32>,` : ""}
${lightTexCoord ? `@location(${vertexLocation++}) lightTexCoord: vec2<f32>,` : ""} ${lightTexCoord ? `@location(2) lightTexCoord: vec2<f32>,` : ""}
${normal ? `@location(${vertexLocation++}) normalOS: vec3<f32>,` : ""} ${normal ? `@location(3) normalOS: vec3<f32>,` : ""}
${normal && tangent ? `@location(${vertexLocation++}) tangentOS: vec4<f32>,` : ""} ${normal && tangent ? `@location(4) tangentOS: vec4<f32>,` : ""}
} }
struct Varyings { struct Varyings {