Light classes, gathering materials

This commit is contained in:
Szymon Nowakowski 2023-08-03 20:05:28 +02:00
parent 3824c8f0c5
commit 537f70ef88
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 {
@ -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);

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

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/.
*/
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" });

View File

@ -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<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 "./Color";
export * from "./Light";
export * from "./Material";
export * from "./Matrix4x4";
export * from "./Mesh";
export * from "./Node";

View File

@ -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<ShaderFlagKey, GPURenderPipeline>;
_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<Renderer> {
@ -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<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();