@luma.gl/engine
Version:
3D Engine Components for luma.gl
158 lines (136 loc) • 5.31 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {Binding, BindingsByGroup, Device} from '@luma.gl/core';
import type {ShaderModule} from '@luma.gl/shadertools';
import {ShaderInputs} from '../shader-inputs';
import type {Material, MaterialProps} from './material';
import {Material as MaterialClass} from './material';
type MaterialModuleProps = Partial<Record<string, Record<string, unknown>>>;
type MaterialBindings = Record<
string,
Binding | import('../dynamic-texture/dynamic-texture').DynamicTexture
>;
/** Logical bind-group slot reserved for material-owned bindings. */
export const MATERIAL_BIND_GROUP = 3;
/** Construction props for {@link MaterialFactory}. */
export type MaterialFactoryProps = {
/** Shader modules that define the material schema for bind group `3`. */
modules?: ShaderModule[];
};
/**
* Creates typed {@link Material} instances for a stable material binding schema.
*
* @example
* ```ts
* const pbrFactory = new MaterialFactory<
* {pbrMaterial: PBRMaterialUniforms},
* PBRMaterialBindings
* >(device, {modules: [pbrMaterial]});
* const pbr = pbrFactory.createMaterial();
* pbr.setProps({pbrMaterial: {baseColorFactor: [1, 0, 0, 1]}});
* const pbrVariant = pbr.clone({bindings: {pbr_baseColorSampler: texture}});
* ```
*
* @example
* ```ts
* const phongFactory = new MaterialFactory<
* {phongMaterial: PhongMaterialProps},
* {}
* >(device, {modules: [phongMaterial]});
* const phong = phongFactory.createMaterial();
* phong.setProps({phongMaterial: {ambient: 0.4, diffuse: 0.7}});
* const phongVariant = phong.clone({moduleProps: {phongMaterial: {shininess: 64}}});
* ```
*
* @example
* ```ts
* const gouraudFactory = new MaterialFactory<
* {gouraudMaterial: GouraudMaterialProps},
* {}
* >(device, {modules: [gouraudMaterial]});
* const gouraud = gouraudFactory.createMaterial();
* gouraud.setProps({gouraudMaterial: {ambient: 0.25}});
* const gouraudVariant = gouraud.clone({moduleProps: {gouraudMaterial: {diffuse: 0.8}}});
* ```
*/
export class MaterialFactory<
TModuleProps extends MaterialModuleProps = MaterialModuleProps,
TBindings extends MaterialBindings = MaterialBindings
> {
/** Device that creates materials for this schema. */
readonly device: Device;
/** Shader modules that define the material schema. */
readonly modules: ShaderModule[];
private _materialBindingNames: Set<string>;
private _materialModuleNames: Set<string>;
constructor(device: Device, props: MaterialFactoryProps = {}) {
this.device = device;
this.modules = props.modules || [];
const shaderInputs = new ShaderInputs(
Object.fromEntries(this.modules.map(module => [module.name, module]))
);
this._materialBindingNames = getMaterialBindingNames(shaderInputs);
this._materialModuleNames = getMaterialModuleNames(shaderInputs);
}
/** Creates one typed material instance for this factory's schema. */
createMaterial(
props: Omit<MaterialProps<TModuleProps, TBindings>, 'factory' | 'modules'> = {}
): Material<TModuleProps, TBindings> {
return new MaterialClass<TModuleProps, TBindings>(this.device, {
...props,
factory: this
});
}
/** Returns the logical material-owned resource binding names. */
getBindingNames(): string[] {
return Array.from(this._materialBindingNames);
}
/** Returns `true` when the supplied binding belongs to this material schema. */
ownsBinding(bindingName: string): boolean {
if (this._materialBindingNames.has(bindingName)) {
return true;
}
const aliasedModuleName = getModuleNameFromUniformBinding(bindingName);
return aliasedModuleName ? this._materialModuleNames.has(aliasedModuleName) : false;
}
/** Returns `true` when the supplied shader module is owned by this material schema. */
ownsModule(moduleName: string): boolean {
return this._materialModuleNames.has(moduleName);
}
/** Packages resolved material bindings into bind group `3`. */
getBindingsByGroup(
bindings: Partial<{[K in keyof TBindings]: Binding}> & Record<string, Binding>
): BindingsByGroup {
return Object.keys(bindings).length > 0 ? {[MATERIAL_BIND_GROUP]: bindings} : {};
}
}
/** Returns the module name corresponding to an auto-generated `*Uniforms` binding. */
export function getModuleNameFromUniformBinding(bindingName: string): string | null {
return bindingName.endsWith('Uniforms') ? bindingName.slice(0, -'Uniforms'.length) : null;
}
function getMaterialBindingNames(shaderInputs: ShaderInputs): Set<string> {
const bindingNames = new Set<string>();
for (const module of Object.values(shaderInputs.modules)) {
for (const binding of module.bindingLayout || []) {
if (binding.group === MATERIAL_BIND_GROUP) {
bindingNames.add(binding.name);
}
}
}
return bindingNames;
}
function getMaterialModuleNames(shaderInputs: ShaderInputs): Set<string> {
const moduleNames = new Set<string>();
for (const module of Object.values(shaderInputs.modules)) {
if (
module.name &&
module.bindingLayout?.some(
binding => binding.group === MATERIAL_BIND_GROUP && binding.name === module.name
)
) {
moduleNames.add(module.name);
}
}
return moduleNames;
}