Light classes, gathering materials

This commit is contained in:
Szymon Nowakowski 2023-08-03 20:05:28 +02:00 committed by Szymon Nowakowski
parent da86167819
commit 472e91c153
8 changed files with 385 additions and 57 deletions

View File

@ -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 { export class _BinaryWriter {
@ -103,6 +109,13 @@ export class _BinaryWriter {
return this; return this;
} }
writeColorF32(value: ColorObject): _BinaryWriter {
this.writeF32(value.r);
this.writeF32(value.g);
this.writeF32(value.b);
return this;
}
alloc(byteLength: number): DataView { alloc(byteLength: number): DataView {
this.ensureUnusedCapacity(byteLength); this.ensureUnusedCapacity(byteLength);
const dataView = new DataView(this._buffer, this._length, byteLength); const dataView = new DataView(this._buffer, this._length, byteLength);

29
src/_Mapping.ts Normal file
View File

@ -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<T> {
table: T[];
map: Map<T, number>;
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);
}
}

View File

@ -6,9 +6,9 @@
import { Node } from "."; import { Node } from ".";
export type Camera = CameraOrthographic | CameraPerspective; export type Camera = OrthographicCamera | PerspectiveCamera;
export interface CameraOrthographicProps { export interface OrthographicCameraProps {
readonly name?: string; readonly name?: string;
readonly verticalSize: number; readonly verticalSize: number;
@ -16,7 +16,7 @@ export interface CameraOrthographicProps {
readonly farPlane: number; readonly farPlane: number;
} }
export interface CameraPerspectiveProps { export interface PerspectiveCameraProps {
readonly name?: string; readonly name?: string;
readonly verticalFovRad: number; readonly verticalFovRad: number;
@ -24,9 +24,9 @@ export interface CameraPerspectiveProps {
readonly farPlane: number; readonly farPlane: number;
} }
export class CameraOrthographic { export class OrthographicCamera {
readonly type!: "CameraOrthographic"; readonly type!: "OrthographicCamera";
_name: string; _name: string;
@ -42,7 +42,7 @@ export class CameraOrthographic {
verticalSize, verticalSize,
nearPlane, nearPlane,
farPlane, farPlane,
}: CameraOrthographicProps) { }: OrthographicCameraProps) {
this._name = name; this._name = name;
this._verticalSize = verticalSize; this._verticalSize = verticalSize;
@ -64,7 +64,7 @@ export class CameraOrthographic {
set farPlane(value: number) { this._farPlane = value; } set farPlane(value: number) { this._farPlane = value; }
get farPlane(): number { return this._farPlane; } get farPlane(): number { return this._farPlane; }
attach(node: Node): CameraOrthographic { attach(node: Node): OrthographicCamera {
if (this._node !== null) { if (this._node !== null) {
this._node._camera = null; this._node._camera = null;
} }
@ -78,7 +78,7 @@ export class CameraOrthographic {
return this; return this;
} }
detach(): CameraOrthographic { detach(): OrthographicCamera {
if (this._node === null) { if (this._node === null) {
return this; return this;
} }
@ -89,11 +89,9 @@ export class CameraOrthographic {
} }
} }
Object.defineProperty(CameraOrthographic.prototype, "type", { value: "CameraOrthographic" }); export class PerspectiveCamera {
export class CameraPerspective { readonly type!: "PerspectiveCamera";
readonly type!: "CameraPerspective";
_name: string; _name: string;
@ -109,7 +107,7 @@ export class CameraPerspective {
verticalFovRad, verticalFovRad,
nearPlane, nearPlane,
farPlane, farPlane,
}: CameraPerspectiveProps) { }: PerspectiveCameraProps) {
this._name = name; this._name = name;
this._verticalFovRad = verticalFovRad; this._verticalFovRad = verticalFovRad;
@ -128,7 +126,7 @@ export class CameraPerspective {
set farPlane(value: number) { this._farPlane = value; } set farPlane(value: number) { this._farPlane = value; }
get farPlane(): number { return this._farPlane; } get farPlane(): number { return this._farPlane; }
attach(node: Node): CameraPerspective { attach(node: Node): PerspectiveCamera {
if (this._node !== null) { if (this._node !== null) {
this._node._camera = null; this._node._camera = null;
} }
@ -142,7 +140,7 @@ export class CameraPerspective {
return this; return this;
} }
detach(): CameraPerspective { detach(): PerspectiveCamera {
if (this._node === null) { if (this._node === null) {
return this; 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 { Object.defineProperty(PerspectiveCamera.prototype, "type", { value: "PerspectiveCamera" });
return Boolean(value) && (value as CameraOrthographic).type === "CameraOrthographic";
export function isOrthographicCamera(value: unknown): value is OrthographicCamera {
return Boolean(value) && (value as OrthographicCamera).type === "OrthographicCamera";
} }
export function isCameraPerspective(value: unknown): value is CameraPerspective { export function isPerspectiveCamera(value: unknown): value is PerspectiveCamera {
return Boolean(value) && (value as CameraPerspective).type === "CameraPerspective"; return Boolean(value) && (value as PerspectiveCamera).type === "PerspectiveCamera";
} }

151
src/data/Light.ts Normal file
View File

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

View File

@ -4,9 +4,8 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Texture2D } from "."; import { Color, ColorObject } from ".";
import { Color, ColorObject } from "../data"; import { Texture2D } from "../resources";
import { Renderer } from "../oktaeder";
export const UNIFORM_BUFFER_SIZE = 64; export const UNIFORM_BUFFER_SIZE = 64;
@ -26,7 +25,7 @@ export interface MaterialProps {
baseColorPartialCoverageTexture?: Texture2D | null; baseColorPartialCoverageTexture?: Texture2D | null;
occlusionTexture?: Texture2D | null; occlusionTexture?: Texture2D | null;
metallicRoughnessTexture?: Texture2D | null; roughnessMetallicTexture?: Texture2D | null;
normalTexture?: Texture2D | null; normalTexture?: Texture2D | null;
emissiveTexture?: Texture2D | null; emissiveTexture?: Texture2D | null;
transmissionCollimationTexture?: Texture2D | null; transmissionCollimationTexture?: Texture2D | null;
@ -38,7 +37,6 @@ export interface MaterialProps {
export class Material { export class Material {
readonly type!: "Material"; readonly type!: "Material";
_renderer: Renderer;
_name: string; _name: string;
@ -55,7 +53,7 @@ export class Material {
_baseColorPartialCoverageTexture: Texture2D | null; _baseColorPartialCoverageTexture: Texture2D | null;
_occlusionTexture: Texture2D | null; _occlusionTexture: Texture2D | null;
_metallicRoughnessTexture: Texture2D | null; _roughnessMetallicTexture: Texture2D | null;
_normalTexture: Texture2D | null; _normalTexture: Texture2D | null;
_emissiveTexture: Texture2D | null; _emissiveTexture: Texture2D | null;
_transmissionCollimationTexture: Texture2D | null; _transmissionCollimationTexture: Texture2D | null;
@ -63,7 +61,7 @@ export class Material {
_transparent: boolean; _transparent: boolean;
_doubleSided: boolean; _doubleSided: boolean;
constructor(renderer: Renderer, { constructor({
name = "", name = "",
baseColor, baseColor,
partialCoverage = 1, partialCoverage = 1,
@ -77,15 +75,13 @@ export class Material {
ior = 1.45, ior = 1.45,
baseColorPartialCoverageTexture = null, baseColorPartialCoverageTexture = null,
occlusionTexture = null, occlusionTexture = null,
metallicRoughnessTexture = null, roughnessMetallicTexture = null,
normalTexture = null, normalTexture = null,
emissiveTexture = null, emissiveTexture = null,
transmissionCollimationTexture = null, transmissionCollimationTexture = null,
transparent = false, transparent = false,
doubleSided = false, doubleSided = false,
}: MaterialProps) { }: MaterialProps) {
this._renderer = renderer;
this._name = name; this._name = name;
this._baseColor = baseColor !== undefined ? Color.fromObject(baseColor) : Color.white(); this._baseColor = baseColor !== undefined ? Color.fromObject(baseColor) : Color.white();
@ -101,7 +97,7 @@ export class Material {
this._baseColorPartialCoverageTexture = baseColorPartialCoverageTexture; this._baseColorPartialCoverageTexture = baseColorPartialCoverageTexture;
this._occlusionTexture = occlusionTexture; this._occlusionTexture = occlusionTexture;
this._metallicRoughnessTexture = metallicRoughnessTexture; this._roughnessMetallicTexture = roughnessMetallicTexture;
this._normalTexture = normalTexture; this._normalTexture = normalTexture;
this._emissiveTexture = emissiveTexture; this._emissiveTexture = emissiveTexture;
this._transmissionCollimationTexture = transmissionCollimationTexture; this._transmissionCollimationTexture = transmissionCollimationTexture;
@ -109,15 +105,6 @@ export class Material {
this._transparent = transparent; this._transparent = transparent;
this._doubleSided = doubleSided; 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" }); Object.defineProperty(Material.prototype, "type", { value: "Material" });

View File

@ -4,8 +4,7 @@
* obtain one at http://mozilla.org/MPL/2.0/. * obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Camera, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; import { Camera, Light, Material, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from ".";
import { Material } from "../resources";
export interface NodeProps { export interface NodeProps {
readonly name?: string; readonly name?: string;
@ -15,6 +14,7 @@ export interface NodeProps {
readonly scale?: Vector3Object; readonly scale?: Vector3Object;
readonly camera?: Camera | null; readonly camera?: Camera | null;
readonly light?: Light | null;
readonly mesh?: Mesh | null; readonly mesh?: Mesh | null;
readonly materials?: Material[]; readonly materials?: Material[];
@ -33,6 +33,8 @@ export class Node {
/** unique */ /** unique */
_camera: Camera | null; _camera: Camera | null;
/** unique */
_light: Light | null;
/** shared */ /** shared */
_mesh: Mesh | null; _mesh: Mesh | null;
/** shared */ /** shared */
@ -56,6 +58,7 @@ export class Node {
rotation, rotation,
scale, scale,
camera = null, camera = null,
light = null,
mesh = null, mesh = null,
materials = [], materials = [],
children = [], children = [],
@ -67,6 +70,7 @@ export class Node {
this._scale = scale !== undefined ? Vector3.fromObject(scale) : Vector3.one(); this._scale = scale !== undefined ? Vector3.fromObject(scale) : Vector3.one();
this._camera = camera; this._camera = camera;
this._light = light;
this._mesh = mesh; this._mesh = mesh;
this._materials = materials; this._materials = materials;
@ -94,6 +98,10 @@ export class Node {
this._camera._node = this; this._camera._node = this;
} }
if (this._light !== null) {
this._light._node = this;
}
if (this._children !== null) { if (this._children !== null) {
for (const child of this._children) { for (const child of this._children) {
child._parent = this; 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 { setTranslation(value: Vector3Object): Node {
this._translation.setObject(value); this._translation.setObject(value);
this._localMatrixNeedsUpdate = true; this._localMatrixNeedsUpdate = true;
@ -109,8 +120,7 @@ export class Node {
} }
getTranslation(res: Vector3): Vector3 { getTranslation(res: Vector3): Vector3 {
res.setObject(this._translation); return res.setObject(this._translation);
return res;
} }
setRotation(value: QuaternionObject): Node { setRotation(value: QuaternionObject): Node {
@ -121,8 +131,7 @@ export class Node {
} }
getRotation(res: Quaternion): Quaternion { getRotation(res: Quaternion): Quaternion {
res.setObject(this._rotation); return res.setObject(this._rotation);
return res;
} }
setScale(value: Vector3Object): Node { setScale(value: Vector3Object): Node {
@ -133,8 +142,7 @@ export class Node {
} }
getScale(res: Vector3): Vector3 { getScale(res: Vector3): Vector3 {
res.setObject(this._scale); return res.setObject(this._scale);
return res;
} }
set camera(value: Camera | null) { set camera(value: Camera | null) {
@ -172,6 +180,41 @@ export class Node {
return this; 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; } set mesh(value: Mesh | null) { this._mesh = value; }
get mesh(): Mesh | null { return this._mesh; } 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 { export function isNode(value: unknown): value is Node {
return Boolean(value) && (value as Node).type === "Node"; return Boolean(value) && (value as Node).type === "Node";
} }
export function* preOrder(nodes: Iterable<Node>): Generator<Node, void, undefined> {
for (const node of nodes) {
yield node;
yield* node._children;
}
}

View File

@ -6,6 +6,8 @@
export * from "./Camera"; export * from "./Camera";
export * from "./Color"; export * from "./Color";
export * from "./Light";
export * from "./Material";
export * from "./Matrix4x4"; export * from "./Matrix4x4";
export * from "./Mesh"; export * from "./Mesh";
export * from "./Node"; export * from "./Node";

View File

@ -8,8 +8,9 @@ export * from "./_BinaryWriter";
export * from "./shader"; export * from "./shader";
import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter";
import { Camera, Scene } from "./data"; import { _Mapping as Mapping } from "./_Mapping";
import { IndexBuffer, IndexBufferProps, Material, MaterialProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; import { Camera, Material, Node, Scene, preOrder } from "./data";
import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources";
import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader"; import { ShaderFlagKey, ShaderFlags, createPipeline, shaderFlagsKey } from "./shader";
export class Renderer { export class Renderer {
@ -36,6 +37,13 @@ export class Renderer {
_pipelineCache: Map<ShaderFlagKey, GPURenderPipeline>; _pipelineCache: Map<ShaderFlagKey, GPURenderPipeline>;
_uniformWriter: BinaryWriter; _uniformWriter: BinaryWriter;
_uniformBuffer: GPUBuffer;
_directionalLightBuffer: GPUBuffer;
_pointLightBuffer: GPUBuffer;
_sampler: GPUSampler;
_objectBindGroup: GPUBindGroup;
/** /**
* This constructor is intended primarily for internal use. Consider using * This constructor is intended primarily for internal use. Consider using
@ -204,6 +212,37 @@ export class Renderer {
this._pipelineCache = new Map(); this._pipelineCache = new Map();
this._uniformWriter = new BinaryWriter(); 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<Renderer> { static async init(canvas: HTMLCanvasElement): Promise<Renderer> {
@ -242,6 +281,9 @@ export class Renderer {
this._textureBlack.dispose(); this._textureBlack.dispose();
this._textureNormal.dispose(); this._textureNormal.dispose();
this._depthBuffer.dispose(); this._depthBuffer.dispose();
this._uniformBuffer.destroy();
this._directionalLightBuffer.destroy();
this._pointLightBuffer.destroy();
return this; return this;
} }
@ -249,10 +291,6 @@ export class Renderer {
return new IndexBuffer(this, props); return new IndexBuffer(this, props);
} }
createMaterial(props: MaterialProps): Material {
return new Material(this, props);
}
createTexture(props: Texture2DProps): Texture2D { createTexture(props: Texture2DProps): Texture2D {
return new Texture2D(this, props); return new Texture2D(this, props);
} }
@ -299,8 +337,66 @@ export class Renderer {
}, },
}); });
void scene; this._uniformWriter.clear();
void camera;
// material gather
const materialMapping = new Mapping<Material>();
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<Node>();
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(); pass.end();