From 472e91c15389271c3040a0e3ca4614b82b60e687 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Thu, 3 Aug 2023 20:05:28 +0200 Subject: [PATCH] Light classes, gathering materials --- src/_BinaryWriter.ts | 15 ++- src/_Mapping.ts | 29 ++++++ src/data/Camera.ts | 40 ++++---- src/data/Light.ts | 151 ++++++++++++++++++++++++++++ src/{resources => data}/Material.ts | 27 ++--- src/data/Node.ts | 66 ++++++++++-- src/data/index.ts | 2 + src/oktaeder.ts | 112 +++++++++++++++++++-- 8 files changed, 385 insertions(+), 57 deletions(-) create mode 100644 src/_Mapping.ts create mode 100644 src/data/Light.ts rename src/{resources => data}/Material.ts (83%) diff --git a/src/_BinaryWriter.ts b/src/_BinaryWriter.ts index 4fc2652..f93028c 100644 --- a/src/_BinaryWriter.ts +++ b/src/_BinaryWriter.ts @@ -1,4 +1,10 @@ -import { Matrix4x4Object, Vector2Object, Vector3Object, Vector4Object } from "./data"; +/*! + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ColorObject, Matrix4x4Object, Vector2Object, Vector3Object, Vector4Object } from "./data"; export class _BinaryWriter { @@ -103,6 +109,13 @@ export class _BinaryWriter { return this; } + writeColorF32(value: ColorObject): _BinaryWriter { + this.writeF32(value.r); + this.writeF32(value.g); + this.writeF32(value.b); + return this; + } + alloc(byteLength: number): DataView { this.ensureUnusedCapacity(byteLength); const dataView = new DataView(this._buffer, this._length, byteLength); diff --git a/src/_Mapping.ts b/src/_Mapping.ts new file mode 100644 index 0000000..d4f8c8a --- /dev/null +++ b/src/_Mapping.ts @@ -0,0 +1,29 @@ +/*! + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +export class _Mapping { + table: T[]; + map: Map; + + constructor() { + this.table = []; + this.map = new Map(); + } + + add(item: T) { + if (this.map.has(item)) { + return; + } + + const id = this.table.length; + this.table.push(item); + this.map.set(item, id); + } + + get(item: T): number | undefined { + return this.map.get(item); + } +} diff --git a/src/data/Camera.ts b/src/data/Camera.ts index 5486c17..cf0b598 100644 --- a/src/data/Camera.ts +++ b/src/data/Camera.ts @@ -6,9 +6,9 @@ import { Node } from "."; -export type Camera = CameraOrthographic | CameraPerspective; +export type Camera = OrthographicCamera | PerspectiveCamera; -export interface CameraOrthographicProps { +export interface OrthographicCameraProps { readonly name?: string; readonly verticalSize: number; @@ -16,7 +16,7 @@ export interface CameraOrthographicProps { readonly farPlane: number; } -export interface CameraPerspectiveProps { +export interface PerspectiveCameraProps { readonly name?: string; readonly verticalFovRad: number; @@ -24,9 +24,9 @@ export interface CameraPerspectiveProps { readonly farPlane: number; } -export class CameraOrthographic { +export class OrthographicCamera { - readonly type!: "CameraOrthographic"; + readonly type!: "OrthographicCamera"; _name: string; @@ -42,7 +42,7 @@ export class CameraOrthographic { verticalSize, nearPlane, farPlane, - }: CameraOrthographicProps) { + }: OrthographicCameraProps) { this._name = name; this._verticalSize = verticalSize; @@ -64,7 +64,7 @@ export class CameraOrthographic { set farPlane(value: number) { this._farPlane = value; } get farPlane(): number { return this._farPlane; } - attach(node: Node): CameraOrthographic { + attach(node: Node): OrthographicCamera { if (this._node !== null) { this._node._camera = null; } @@ -78,7 +78,7 @@ export class CameraOrthographic { return this; } - detach(): CameraOrthographic { + detach(): OrthographicCamera { if (this._node === null) { return this; } @@ -89,11 +89,9 @@ export class CameraOrthographic { } } -Object.defineProperty(CameraOrthographic.prototype, "type", { value: "CameraOrthographic" }); +export class PerspectiveCamera { -export class CameraPerspective { - - readonly type!: "CameraPerspective"; + readonly type!: "PerspectiveCamera"; _name: string; @@ -109,7 +107,7 @@ export class CameraPerspective { verticalFovRad, nearPlane, farPlane, - }: CameraPerspectiveProps) { + }: PerspectiveCameraProps) { this._name = name; this._verticalFovRad = verticalFovRad; @@ -128,7 +126,7 @@ export class CameraPerspective { set farPlane(value: number) { this._farPlane = value; } get farPlane(): number { return this._farPlane; } - attach(node: Node): CameraPerspective { + attach(node: Node): PerspectiveCamera { if (this._node !== null) { this._node._camera = null; } @@ -142,7 +140,7 @@ export class CameraPerspective { return this; } - detach(): CameraPerspective { + detach(): PerspectiveCamera { if (this._node === null) { return this; } @@ -153,12 +151,14 @@ export class CameraPerspective { } } -Object.defineProperty(CameraPerspective.prototype, "type", { value: "CameraPerspective" }); +Object.defineProperty(OrthographicCamera.prototype, "type", { value: "OrthographicCamera" }); -export function isCameraOrthographic(value: unknown): value is CameraOrthographic { - return Boolean(value) && (value as CameraOrthographic).type === "CameraOrthographic"; +Object.defineProperty(PerspectiveCamera.prototype, "type", { value: "PerspectiveCamera" }); + +export function isOrthographicCamera(value: unknown): value is OrthographicCamera { + return Boolean(value) && (value as OrthographicCamera).type === "OrthographicCamera"; } -export function isCameraPerspective(value: unknown): value is CameraPerspective { - return Boolean(value) && (value as CameraPerspective).type === "CameraPerspective"; +export function isPerspectiveCamera(value: unknown): value is PerspectiveCamera { + return Boolean(value) && (value as PerspectiveCamera).type === "PerspectiveCamera"; } diff --git a/src/data/Light.ts b/src/data/Light.ts new file mode 100644 index 0000000..4a564a0 --- /dev/null +++ b/src/data/Light.ts @@ -0,0 +1,151 @@ +/*! + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Color, ColorObject, Node } from "."; + +export type Light = DirectionalLight | PointLight; + +export interface DirectionalLightProps { + readonly name?: string; + + readonly color: ColorObject; +} + +export interface PointLightProps { + readonly name?: string; + + readonly color: ColorObject; +} + +export class DirectionalLight { + + readonly type!: "DirectionalLight"; + + _name: string; + + _color: Color; + + /** backreference */ + _node: Node | null; + + constructor({ + name = "", + color, + }: DirectionalLightProps) { + this._name = name; + + this._color = Color.fromObject(color); + + this._node = null; + } + + set name(value: string) { this._name = value; } + get name(): string { return this._name; } + + setColor(value: ColorObject): DirectionalLight { + this._color.setObject(value); + return this; + } + + getColor(res: Color): Color { + return res.setObject(this._color); + } + + attach(node: Node): DirectionalLight { + if (this._node !== null) { + this._node._light = null; + } + + if (node._light !== null) { + node._light._node = null; + } + + node._light = this; + this._node = node; + return this; + } + + detach(): DirectionalLight { + if (this._node === null) { + return this; + } + + this._node._light = null; + this._node = null; + return this; + } +} + +export class PointLight { + + readonly type!: "PointLight"; + + _name: string; + + _color: Color; + + /** backreference */ + _node: Node | null; + + constructor({ + name = "", + color, + }: PointLightProps) { + this._name = name; + + this._color = Color.fromObject(color); + + this._node = null; + } + + set name(value: string) { this._name = value; } + get name(): string { return this._name; } + + setColor(value: ColorObject): PointLight { + this._color.setObject(value); + return this; + } + + getColor(res: Color): Color { + return res.setObject(this._color); + } + + attach(node: Node): PointLight { + if (this._node !== null) { + this._node._light = null; + } + + if (node._light !== null) { + node._light._node = null; + } + + node._light = this; + this._node = node; + return this; + } + + detach(): PointLight { + if (this._node === null) { + return this; + } + + this._node._light = null; + this._node = null; + return this; + } +} + +Object.defineProperty(DirectionalLight.prototype, "type", { value: "DirectionalLight" }); + +Object.defineProperty(PointLight.prototype, "type", { value: "PointLight" }); + +export function isDirectionalLight(value: unknown): value is DirectionalLight { + return Boolean(value) && (value as DirectionalLight).type === "DirectionalLight"; +} + +export function isPointLight(value: unknown): value is PointLight { + return Boolean(value) && (value as PointLight).type === "PointLight"; +} diff --git a/src/resources/Material.ts b/src/data/Material.ts similarity index 83% rename from src/resources/Material.ts rename to src/data/Material.ts index cf1c6b0..234aed4 100644 --- a/src/resources/Material.ts +++ b/src/data/Material.ts @@ -4,9 +4,8 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Texture2D } from "."; -import { Color, ColorObject } from "../data"; -import { Renderer } from "../oktaeder"; +import { Color, ColorObject } from "."; +import { Texture2D } from "../resources"; export const UNIFORM_BUFFER_SIZE = 64; @@ -26,7 +25,7 @@ export interface MaterialProps { baseColorPartialCoverageTexture?: Texture2D | null; occlusionTexture?: Texture2D | null; - metallicRoughnessTexture?: Texture2D | null; + roughnessMetallicTexture?: Texture2D | null; normalTexture?: Texture2D | null; emissiveTexture?: Texture2D | null; transmissionCollimationTexture?: Texture2D | null; @@ -38,7 +37,6 @@ export interface MaterialProps { export class Material { readonly type!: "Material"; - _renderer: Renderer; _name: string; @@ -55,7 +53,7 @@ export class Material { _baseColorPartialCoverageTexture: Texture2D | null; _occlusionTexture: Texture2D | null; - _metallicRoughnessTexture: Texture2D | null; + _roughnessMetallicTexture: Texture2D | null; _normalTexture: Texture2D | null; _emissiveTexture: Texture2D | null; _transmissionCollimationTexture: Texture2D | null; @@ -63,7 +61,7 @@ export class Material { _transparent: boolean; _doubleSided: boolean; - constructor(renderer: Renderer, { + constructor({ name = "", baseColor, partialCoverage = 1, @@ -77,15 +75,13 @@ export class Material { ior = 1.45, baseColorPartialCoverageTexture = null, occlusionTexture = null, - metallicRoughnessTexture = null, + roughnessMetallicTexture = null, normalTexture = null, emissiveTexture = null, transmissionCollimationTexture = null, transparent = false, doubleSided = false, }: MaterialProps) { - this._renderer = renderer; - this._name = name; this._baseColor = baseColor !== undefined ? Color.fromObject(baseColor) : Color.white(); @@ -101,7 +97,7 @@ export class Material { this._baseColorPartialCoverageTexture = baseColorPartialCoverageTexture; this._occlusionTexture = occlusionTexture; - this._metallicRoughnessTexture = metallicRoughnessTexture; + this._roughnessMetallicTexture = roughnessMetallicTexture; this._normalTexture = normalTexture; this._emissiveTexture = emissiveTexture; this._transmissionCollimationTexture = transmissionCollimationTexture; @@ -109,15 +105,6 @@ export class Material { this._transparent = transparent; this._doubleSided = doubleSided; } - - /** - * Destroys owned GPU resources. The material should not be used after - * calling this method. - * @returns `this` for chaining - */ - dispose(): Material { - return this; - } } Object.defineProperty(Material.prototype, "type", { value: "Material" }); diff --git a/src/data/Node.ts b/src/data/Node.ts index 4d5ed84..8b9da74 100644 --- a/src/data/Node.ts +++ b/src/data/Node.ts @@ -4,8 +4,7 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Camera, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; -import { Material } from "../resources"; +import { Camera, Light, Material, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; export interface NodeProps { readonly name?: string; @@ -15,6 +14,7 @@ export interface NodeProps { readonly scale?: Vector3Object; readonly camera?: Camera | null; + readonly light?: Light | null; readonly mesh?: Mesh | null; readonly materials?: Material[]; @@ -33,6 +33,8 @@ export class Node { /** unique */ _camera: Camera | null; + /** unique */ + _light: Light | null; /** shared */ _mesh: Mesh | null; /** shared */ @@ -56,6 +58,7 @@ export class Node { rotation, scale, camera = null, + light = null, mesh = null, materials = [], children = [], @@ -67,6 +70,7 @@ export class Node { this._scale = scale !== undefined ? Vector3.fromObject(scale) : Vector3.one(); this._camera = camera; + this._light = light; this._mesh = mesh; this._materials = materials; @@ -94,6 +98,10 @@ export class Node { this._camera._node = this; } + if (this._light !== null) { + this._light._node = this; + } + if (this._children !== null) { for (const child of this._children) { child._parent = this; @@ -101,6 +109,9 @@ export class Node { } } + set name(value: string) { this._name = value; } + get name(): string { return this._name; } + setTranslation(value: Vector3Object): Node { this._translation.setObject(value); this._localMatrixNeedsUpdate = true; @@ -109,8 +120,7 @@ export class Node { } getTranslation(res: Vector3): Vector3 { - res.setObject(this._translation); - return res; + return res.setObject(this._translation); } setRotation(value: QuaternionObject): Node { @@ -121,8 +131,7 @@ export class Node { } getRotation(res: Quaternion): Quaternion { - res.setObject(this._rotation); - return res; + return res.setObject(this._rotation); } setScale(value: Vector3Object): Node { @@ -133,8 +142,7 @@ export class Node { } getScale(res: Vector3): Vector3 { - res.setObject(this._scale); - return res; + return res.setObject(this._scale); } set camera(value: Camera | null) { @@ -172,6 +180,41 @@ export class Node { return this; } + set light(value: Light | null) { + if (value !== null) { + this.attachLight(value); + } else { + this.detachLight(); + } + } + get light(): Light | null { return this._light; } + + attachLight(light: Light): Node { + if (this._light !== null) { + this._light._node = null; + } + + this._light = light; + + if (light._node !== null) { + light._node._light = null; + } + + light._node = this; + this._light = light; + return this; + } + + detachLight(): Node { + if (this._light === null) { + return this; + } + + this._light._node = null; + this._light = null; + return this; + } + set mesh(value: Mesh | null) { this._mesh = value; } get mesh(): Mesh | null { return this._mesh; } @@ -269,3 +312,10 @@ Object.defineProperty(Node.prototype, "type", { value: "Node" }); export function isNode(value: unknown): value is Node { return Boolean(value) && (value as Node).type === "Node"; } + +export function* preOrder(nodes: Iterable): Generator { + for (const node of nodes) { + yield node; + yield* node._children; + } +} diff --git a/src/data/index.ts b/src/data/index.ts index 0f41f51..dd285c4 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -6,6 +6,8 @@ export * from "./Camera"; export * from "./Color"; +export * from "./Light"; +export * from "./Material"; export * from "./Matrix4x4"; export * from "./Mesh"; export * from "./Node"; diff --git a/src/oktaeder.ts b/src/oktaeder.ts index 2f75a91..1a9d0c9 100644 --- a/src/oktaeder.ts +++ b/src/oktaeder.ts @@ -8,8 +8,9 @@ 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 { _Mapping as Mapping } from "./_Mapping"; +import { Camera, Material, Node, Scene, preOrder } from "./data"; +import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader"; export class Renderer { @@ -36,6 +37,13 @@ export class Renderer { _pipelineCache: Map; _uniformWriter: BinaryWriter; + _uniformBuffer: GPUBuffer; + _directionalLightBuffer: GPUBuffer; + _pointLightBuffer: GPUBuffer; + + _sampler: GPUSampler; + + _objectBindGroup: GPUBindGroup; /** * This constructor is intended primarily for internal use. Consider using @@ -204,6 +212,37 @@ export class Renderer { this._pipelineCache = new Map(); this._uniformWriter = new BinaryWriter(); + this._uniformBuffer = device.createBuffer({ + size: 4 * 1024 * 1024, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + label: "Uniform", + }); + this._directionalLightBuffer = device.createBuffer({ + size: 1024 * 32, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + this._pointLightBuffer = device.createBuffer({ + size: 1024 * 32, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE, + }); + + this._sampler = device.createSampler({ + addressModeU: "repeat", + addressModeV: "repeat", + addressModeW: "repeat", + magFilter: "linear", + minFilter: "linear", + mipmapFilter: "linear", + maxAnisotropy: 16, + }); + + this._objectBindGroup = device.createBindGroup({ + layout: this._objectBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this._uniformBuffer } }, + ], + label: "Object", + }); } static async init(canvas: HTMLCanvasElement): Promise { @@ -242,6 +281,9 @@ export class Renderer { this._textureBlack.dispose(); this._textureNormal.dispose(); this._depthBuffer.dispose(); + this._uniformBuffer.destroy(); + this._directionalLightBuffer.destroy(); + this._pointLightBuffer.destroy(); return this; } @@ -249,10 +291,6 @@ export class Renderer { return new IndexBuffer(this, props); } - createMaterial(props: MaterialProps): Material { - return new Material(this, props); - } - createTexture(props: Texture2DProps): Texture2D { return new Texture2D(this, props); } @@ -299,8 +337,66 @@ export class Renderer { }, }); - void scene; - void camera; + this._uniformWriter.clear(); + + // material gather + + const materialMapping = new Mapping(); + for (const node of preOrder(scene._nodes)) { + for (const material of node._materials) { + materialMapping.add(material); + } + } + + const materialBindGroups = materialMapping.table.map((material) => { + const offset = this._uniformWriter._length; + this._uniformWriter.writeColorF32(material._baseColor); + this._uniformWriter.writeF32(material._partialCoverage); + this._uniformWriter.writeColorF32(material._transmission); + this._uniformWriter.writeF32(material._collimation); + this._uniformWriter.writeF32(material._occlusionTextureStrength); + this._uniformWriter.writeF32(material._roughness); + this._uniformWriter.writeF32(material._metallic); + this._uniformWriter.writeF32(material._normalScale); + this._uniformWriter.writeColorF32(material._emissive); + this._uniformWriter.writeF32(material._ior); + + const bindGroup = this._device.createBindGroup({ + layout: this._materialBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this._uniformBuffer } }, + { binding: 1, resource: this._sampler }, + { binding: 2, resource: material._baseColorPartialCoverageTexture?._textureView ?? this._textureWhite._textureView }, + { binding: 3, resource: material._occlusionTexture?._textureView ?? this._textureWhite._textureView }, + { binding: 4, resource: material._roughnessMetallicTexture?._textureView ?? this._textureWhite._textureView }, + { binding: 5, resource: material._normalTexture?._textureView ?? this._textureNormal._textureView }, + { binding: 6, resource: material._emissiveTexture?._textureView ?? this._textureWhite._textureView }, + { binding: 7, resource: material._transmissionCollimationTexture?._textureView ?? this._textureBlack._textureView }, + ], + label: material._name, + }); + return { offset, bindGroup }; + }); + + // object gather + + const objectMapping = new Mapping(); + for (const node of preOrder(scene._nodes)) { + if (node._mesh !== null) { + objectMapping.add(node); + } + } + + const objectOffsets = objectMapping.table.map((object) => { + const offset = this._uniformWriter._length; + object._updateWorldMatrix(); + this._uniformWriter.writeMatrix4x4(object._worldMatrix); + + return offset; + }); + + void materialBindGroups; + void objectOffsets; pass.end();