diff --git a/example/script.ts b/example/script.ts index b1fd372..3a9652b 100644 --- a/example/script.ts +++ b/example/script.ts @@ -1,4 +1,4 @@ -import { Color, DirectionalLight, Material, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index"; +import { Color, DirectionalLight, Mesh, Node, PerspectiveCamera, PointLight, Quaternion, Scene, Submesh, Vector3 } from "../src/data/index"; import { Renderer, degToRad } from "../src/oktaeder"; import "./style.css"; @@ -44,7 +44,7 @@ const submesh: Submesh = { start: 0, length: 24 }; const mesh = new Mesh({ vertexBuffer, indexBuffer, submeshes: [submesh] }); -const material = new Material({ +const material = renderer.createMaterial({ baseColor: Color.white(), roughness: 0.5, metallic: 1, diff --git a/src/data/Material.ts b/src/data/DynamicMaterial.ts similarity index 80% rename from src/data/Material.ts rename to src/data/DynamicMaterial.ts index 3a8ec5f..65e4b60 100644 --- a/src/data/Material.ts +++ b/src/data/DynamicMaterial.ts @@ -6,35 +6,11 @@ import { Color, ColorObject } from "."; import { Texture2D } from "../resources"; +import { MaterialProps } from "./MaterialProps"; -export interface MaterialProps { - name?: string; +export class DynamicMaterial { - baseColor?: ColorObject; - partialCoverage?: number; - transmission?: ColorObject; - collimation?: number; - occlusionTextureStrength?: number; - roughness?: number; - metallic?: number; - normalScale?: number; - emissive?: ColorObject; - ior?: number; - - baseColorPartialCoverageTexture?: Texture2D | null; - occlusionTexture?: Texture2D | null; - roughnessMetallicTexture?: Texture2D | null; - normalTexture?: Texture2D | null; - emissiveTexture?: Texture2D | null; - transmissionCollimationTexture?: Texture2D | null; - - transparent?: boolean; - doubleSided?: boolean; -} - -export class Material { - - declare readonly type: "Material"; + declare readonly type: "DynamicMaterial"; _name: string; @@ -107,7 +83,7 @@ export class Material { set name(value: string) { this._name = value; } get name(): string { return this._name; } - setBaseColor(value: ColorObject): Material { + setBaseColor(value: ColorObject): DynamicMaterial { this._baseColor.setObject(value); return this; } @@ -130,7 +106,7 @@ export class Material { set normalScale(value: number) { this._normalScale = value; } get normalScale(): number { return this._normalScale; } - setEmissive(value: ColorObject): Material { + setEmissive(value: ColorObject): DynamicMaterial { this._emissive.setObject(value); return this; } @@ -138,7 +114,7 @@ export class Material { return res.setObject(this._emissive); } - setTransmission(value: ColorObject): Material { + setTransmission(value: ColorObject): DynamicMaterial { this._transmission.setObject(value); return this; } @@ -152,22 +128,22 @@ export class Material { set ior(value: number) { this._ior = value; } get ior(): number { return this._ior; } - set baseColorPartialCoverageTexture(value: Texture2D | null) { this._baseColorPartialCoverageTexture = value;} + set baseColorPartialCoverageTexture(value: Texture2D | null) { this._baseColorPartialCoverageTexture = value; } get baseColorPartialCoverageTexture(): Texture2D | null { return this._baseColorPartialCoverageTexture; } - set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value;} + set occlusionTexture(value: Texture2D | null) { this._occlusionTexture = value; } get occlusionTexture(): Texture2D | null { return this._occlusionTexture; } - set roughnessMetallicTexture(value: Texture2D | null) { this._roughnessMetallicTexture = value;} + set roughnessMetallicTexture(value: Texture2D | null) { this._roughnessMetallicTexture = value; } get roughnessMetallicTexture(): Texture2D | null { return this._roughnessMetallicTexture; } - set normalTexture(value: Texture2D | null) { this._normalTexture = value;} + set normalTexture(value: Texture2D | null) { this._normalTexture = value; } get normalTexture(): Texture2D | null { return this._normalTexture; } - set emissiveTexture(value: Texture2D | null) { this._emissiveTexture = value;} + set emissiveTexture(value: Texture2D | null) { this._emissiveTexture = value; } get emissiveTexture(): Texture2D | null { return this._emissiveTexture; } - set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value;} + set transmissionCollimationTexture(value: Texture2D | null) { this._transmissionCollimationTexture = value; } get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; } set transparent(value: boolean) { this._transparent = value; } @@ -177,8 +153,8 @@ export class Material { get doubleSided(): boolean { return this._doubleSided; } } -Object.defineProperty(Material.prototype, "type", { value: "Material" }); +Object.defineProperty(DynamicMaterial.prototype, "type", { value: "DynamicMaterial" }); -export function isMaterial(value: unknown): value is Material { - return Boolean(value) && (value as Material).type === "Material"; +export function isDynamicMaterial(value: unknown): value is DynamicMaterial { + return Boolean(value) && (value as DynamicMaterial).type === "DynamicMaterial"; } diff --git a/src/data/MaterialProps.ts b/src/data/MaterialProps.ts new file mode 100644 index 0000000..e7817fe --- /dev/null +++ b/src/data/MaterialProps.ts @@ -0,0 +1,33 @@ +/*! + * 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 } from "."; +import { Texture2D } from "../resources"; + +export interface MaterialProps { + name?: string; + + baseColor?: ColorObject; + partialCoverage?: number; + transmission?: ColorObject; + collimation?: number; + occlusionTextureStrength?: number; + roughness?: number; + metallic?: number; + normalScale?: number; + emissive?: ColorObject; + ior?: number; + + baseColorPartialCoverageTexture?: Texture2D | null; + occlusionTexture?: Texture2D | null; + roughnessMetallicTexture?: Texture2D | null; + normalTexture?: Texture2D | null; + emissiveTexture?: Texture2D | null; + transmissionCollimationTexture?: Texture2D | null; + + transparent?: boolean; + doubleSided?: boolean; +} diff --git a/src/data/Node.ts b/src/data/Node.ts index 79c8546..5745bb0 100644 --- a/src/data/Node.ts +++ b/src/data/Node.ts @@ -4,7 +4,8 @@ * obtain one at http://mozilla.org/MPL/2.0/. */ -import { Camera, Light, Material, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; +import { Camera, DynamicMaterial, Light, Matrix4x4, Mesh, Quaternion, QuaternionObject, Vector3, Vector3Object } from "."; +import { Material } from "../resources"; export interface NodeProps { readonly name?: string; @@ -16,7 +17,7 @@ export interface NodeProps { readonly camera?: Camera | null; readonly light?: Light | null; readonly mesh?: Mesh | null; - readonly materials?: Material[]; + readonly materials?: (Material | DynamicMaterial)[]; readonly children?: Node[]; } @@ -38,7 +39,7 @@ export class Node { /** shared */ _mesh: Mesh | null; /** shared */ - _materials: Material[]; + _materials: (Material | DynamicMaterial)[]; /** unique */ _children: Node[]; @@ -218,13 +219,13 @@ export class Node { set mesh(value: Mesh | null) { this._mesh = value; } get mesh(): Mesh | null { return this._mesh; } - setMaterials(value: readonly Material[]): Node { + setMaterials(value: readonly (Material | DynamicMaterial)[]): Node { this._materials.length = 0; this._materials.push(...value); return this; } - getMaterials(res: Material[]): Material[] { + getMaterials(res: (Material | DynamicMaterial)[]): (Material | DynamicMaterial)[] { res.length = 0; res.push(...this._materials); return res; diff --git a/src/data/index.ts b/src/data/index.ts index dd285c4..281fcf1 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -6,8 +6,9 @@ export * from "./Camera"; export * from "./Color"; +export * from "./DynamicMaterial"; export * from "./Light"; -export * from "./Material"; +export * from "./MaterialProps"; export * from "./Matrix4x4"; export * from "./Mesh"; export * from "./Node"; diff --git a/src/oktaeder.ts b/src/oktaeder.ts index 3899453..3878bdd 100644 --- a/src/oktaeder.ts +++ b/src/oktaeder.ts @@ -10,8 +10,8 @@ export * from "./shader"; import { _BinaryWriter as BinaryWriter } from "./_BinaryWriter"; import { _Mapping as Mapping } from "./_Mapping"; -import { Camera, Material, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isPointLight, preOrder } from "./data"; -import { IndexBuffer, IndexBufferProps, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps } from "./resources"; +import { Camera, DynamicMaterial, MaterialProps, Matrix4x4, Node, Scene, Vector3, isDirectionalLight, isDynamicMaterial, isPointLight, preOrder } from "./data"; +import { IndexBuffer, IndexBufferProps, Material, Texture2D, Texture2DProps, VertexBuffer, VertexBufferProps, isMaterial } from "./resources"; import { GLOBAL_UNIFORMS_SIZE, MATERIAL_UNIFORMS_SIZE, OBJECT_UNIFORMS_SIZE, ShaderFlagKey, ShaderFlags, _createPipeline, _shaderFlagsKey } from "./shader"; const _matrixOStoWSNormal = new Matrix4x4( @@ -329,6 +329,10 @@ 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); } @@ -382,16 +386,18 @@ export class Renderer { this._uniformWriter.clear(); - // gather materials + // gather dynamic materials - const materialMapping = new Mapping(); + const dynamicMaterialMapping = new Mapping(); for (const node of preOrder(scene._nodes)) { for (const material of node._materials) { - materialMapping.add(material); + if (isDynamicMaterial(material)) { + dynamicMaterialMapping.add(material); + } } } - const materialBindGroups = materialMapping.table.map((material) => { + const dynamicMaterialBindGroups = dynamicMaterialMapping.table.map((material) => { const offset = this._uniformWriter._length; this._uniformWriter.writeColorF32(material._baseColor); this._uniformWriter.writeF32(material._partialCoverage); @@ -541,9 +547,17 @@ export class Renderer { 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]); + if (isMaterial(material)) { + pass.setBindGroup(1, material._bindGroup, [0]); + } else if (isDynamicMaterial(material)) { + const { + bindGroup: materialBindGroup, + offset: materialOffset + } = dynamicMaterialBindGroups[dynamicMaterialMapping.get(material)!]!; + pass.setBindGroup(1, materialBindGroup, [materialOffset]); + } + pass.drawIndexed(submesh.length, 1, submesh.start, 0, 0); } } diff --git a/src/resources/Material.ts b/src/resources/Material.ts new file mode 100644 index 0000000..afd49de --- /dev/null +++ b/src/resources/Material.ts @@ -0,0 +1,169 @@ +/*! + * 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 { Texture2D } from "."; +import { Color, MaterialProps } from "../data"; +import { Renderer, _BinaryWriter } from "../oktaeder"; + +export class Material { + + declare readonly type: "Material"; + _renderer: Renderer; + + _uniformBuffer: GPUBuffer; + _bindGroup: GPUBindGroup; + + _name: string; + + readonly _baseColor: Color; + readonly _partialCoverage: number; + readonly _occlusionTextureStrength: number; + readonly _metallic: number; + readonly _roughness: number; + readonly _normalScale: number; + readonly _emissive: Color; + readonly _transmission: Color; + readonly _collimation: number; + readonly _ior: number; + + readonly _baseColorPartialCoverageTexture: Texture2D | null; + readonly _occlusionTexture: Texture2D | null; + readonly _roughnessMetallicTexture: Texture2D | null; + readonly _normalTexture: Texture2D | null; + readonly _emissiveTexture: Texture2D | null; + readonly _transmissionCollimationTexture: Texture2D | null; + + readonly _transparent: boolean; + readonly _doubleSided: boolean; + + constructor(renderer: Renderer, { + name = "", + baseColor, + partialCoverage = 1, + occlusionTextureStrength = 1, + metallic = 1, + roughness = 1, + normalScale = 1, + emissive, + transmission, + collimation = 1, + ior = 1.45, + baseColorPartialCoverageTexture = null, + occlusionTexture = 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(); + this._partialCoverage = partialCoverage; + this._occlusionTextureStrength = occlusionTextureStrength; + this._metallic = metallic; + this._roughness = roughness; + this._normalScale = normalScale; + this._emissive = emissive !== undefined ? Color.fromObject(emissive) : Color.black(); + this._transmission = transmission !== undefined ? Color.fromObject(transmission) : Color.black(); + this._collimation = collimation; + this._ior = ior; + + this._baseColorPartialCoverageTexture = baseColorPartialCoverageTexture; + this._occlusionTexture = occlusionTexture; + this._roughnessMetallicTexture = roughnessMetallicTexture; + this._normalTexture = normalTexture; + this._emissiveTexture = emissiveTexture; + this._transmissionCollimationTexture = transmissionCollimationTexture; + + this._transparent = transparent; + this._doubleSided = doubleSided; + + this._uniformBuffer = renderer._device.createBuffer({ + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, + size: 64, + label: name, + }); + + const writer = new _BinaryWriter(64); + writer.writeColorF32(this._baseColor); + writer.writeF32(this._partialCoverage); + writer.writeColorF32(this._transmission); + writer.writeF32(this._collimation); + writer.writeF32(this._occlusionTextureStrength); + writer.writeF32(this._roughness); + writer.writeF32(this._metallic); + writer.writeF32(this._normalScale); + writer.writeColorF32(this._emissive); + writer.writeF32(this._ior); + + renderer._device.queue.writeBuffer(this._uniformBuffer, 0, writer.subarray); + + this._bindGroup = renderer._device.createBindGroup({ + layout: renderer._materialBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this._uniformBuffer, size: 64 } }, + { binding: 1, resource: renderer._sampler }, + { binding: 2, resource: this._baseColorPartialCoverageTexture?._textureView ?? renderer._textureWhite._textureView }, + { binding: 3, resource: this._occlusionTexture?._textureView ?? renderer._textureWhite._textureView }, + { binding: 4, resource: this._roughnessMetallicTexture?._textureView ?? renderer._textureWhite._textureView }, + { binding: 5, resource: this._normalTexture?._textureView ?? renderer._textureNormal._textureView }, + { binding: 6, resource: this._emissiveTexture?._textureView ?? renderer._textureWhite._textureView }, + { binding: 7, resource: this._transmissionCollimationTexture?._textureView ?? renderer._textureBlack._textureView }, + ], + label: name, + }); + } + + /** + * Destroys owned GPU resources. The index buffer should not be used after + * calling this method. + * @returns `this` for chaining + */ + dispose(): Material { + this._uniformBuffer.destroy(); + return this; + } + + getBaseColor(res: Color): Color { + return res.setObject(this._baseColor); + } + + get partialCoverage(): number { return this._partialCoverage; } + get occlusionTextureStrength(): number { return this._occlusionTextureStrength; } + get metallic(): number { return this._metallic; } + get roughness(): number { return this._roughness; } + get normalScale(): number { return this._normalScale; } + + getEmissive(res: Color): Color { + return res.setObject(this._emissive); + } + + getTransmission(res: Color): Color { + return res.setObject(this._transmission); + } + + get collimation(): number { return this._collimation; } + get ior(): number { return this._ior; } + get baseColorPartialCoverageTexture(): Texture2D | null { return this._baseColorPartialCoverageTexture; } + get occlusionTexture(): Texture2D | null { return this._occlusionTexture; } + get roughnessMetallicTexture(): Texture2D | null { return this._roughnessMetallicTexture; } + get normalTexture(): Texture2D | null { return this._normalTexture; } + get emissiveTexture(): Texture2D | null { return this._emissiveTexture; } + get transmissionCollimationTexture(): Texture2D | null { return this._transmissionCollimationTexture; } + + get transparent(): boolean { return this._transparent; } + get doubleSided(): boolean { return this._doubleSided; } +} + +Object.defineProperty(Material.prototype, "type", { value: "Material" }); + +export function isMaterial(value: unknown): value is Material { + return Boolean(value) && (value as Material).type === "Material"; +} diff --git a/src/resources/index.ts b/src/resources/index.ts index d1718ac..c526276 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -5,5 +5,6 @@ */ export * from "./IndexBuffer"; +export * from "./Material"; export * from "./Texture2D"; export * from "./VertexBuffer";