// 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>>; 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; private _materialModuleNames: Set; 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, 'factory' | 'modules'> = {} ): Material { return new MaterialClass(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 ): 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 { const bindingNames = new Set(); 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 { const moduleNames = new Set(); 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; }