@luma.gl/engine
Version:
3D Engine Components for luma.gl
176 lines • 7.08 kB
JavaScript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { Buffer, Sampler, Texture, TextureView, UniformStore } from '@luma.gl/core';
import { DynamicTexture } from "../dynamic-texture/dynamic-texture.js";
import { ShaderInputs } from "../shader-inputs.js";
import { shaderModuleHasUniforms } from "../utils/shader-module-utils.js";
import { uid } from "../utils/uid.js";
import { getModuleNameFromUniformBinding, MATERIAL_BIND_GROUP, MaterialFactory } from "./material-factory.js";
/**
* Material owns bind group `3` resources and uniforms for one material instance.
*
* `setProps()` mutates uniform values in place. Structural resource changes are
* expressed through `clone({...})`, which creates a new material identity.
*/
export class Material {
/** Application-provided identifier. */
id;
/** Device that owns the material resources. */
device;
/** Factory that defines the material schema. */
factory;
/** Shader inputs for the material-owned modules. */
shaderInputs;
/** Internal binding store including uniform buffers and resource bindings. */
bindings = {};
_uniformStore;
_bindGroupCacheToken = {};
constructor(device, props = {}) {
this.id = props.id || uid('material');
this.device = device;
this.factory =
props.factory ||
new MaterialFactory(device, {
modules: props.modules || props.shaderInputs?.getModules() || []
});
const moduleMap = Object.fromEntries((props.shaderInputs?.getModules() || this.factory.modules).map(module => [
module.name,
module
]));
this.shaderInputs = props.shaderInputs || new ShaderInputs(moduleMap);
this._uniformStore = new UniformStore(this.device, this.shaderInputs.modules);
for (const [moduleName, module] of Object.entries(this.shaderInputs.modules)) {
if (this.ownsModule(moduleName) && shaderModuleHasUniforms(module)) {
const uniformBuffer = this._uniformStore.getManagedUniformBuffer(moduleName);
this.bindings[`${moduleName}Uniforms`] = uniformBuffer;
}
}
this.updateShaderInputs();
if (props.bindings) {
this._replaceOwnedBindings(props.bindings);
}
}
/** Destroys managed uniform-buffer resources owned by this material. */
destroy() {
this._uniformStore.destroy();
}
/** Creates a new material variant with optional structural and uniform overrides. */
clone(props = {}) {
const material = this.factory.createMaterial({
id: props.id,
shaderInputs: props.shaderInputs,
bindings: {
...this.getResourceBindings(),
...props.bindings
}
});
if (!props.shaderInputs) {
material.setProps(this.shaderInputs.getUniformValues());
}
if (props.moduleProps) {
material.setProps(props.moduleProps);
}
return material;
}
/** Returns `true` if this material owns the supplied binding name. */
ownsBinding(bindingName) {
return this.factory.ownsBinding(bindingName);
}
/** Returns `true` if this material owns the supplied shader module. */
ownsModule(moduleName) {
return this.factory.ownsModule(moduleName);
}
/** Updates material uniform/module props in place without changing material identity. */
setProps(props) {
this.shaderInputs.setProps(props);
this.updateShaderInputs();
}
/** Updates managed uniform buffers and shader-input-owned bindings. */
updateShaderInputs() {
this._uniformStore.setUniforms(this.shaderInputs.getUniformValues());
const didChange = this._setOwnedBindings(this.shaderInputs.getBindingValues());
if (didChange) {
this._bindGroupCacheToken = {};
}
}
/** Returns the material-owned resource bindings without internal uniform buffers. */
getResourceBindings() {
const resourceBindings = {};
for (const [name, binding] of Object.entries(this.bindings)) {
if (!getModuleNameFromUniformBinding(name)) {
resourceBindings[name] = binding;
}
}
return resourceBindings;
}
/** Returns the resolved bindings, including internal uniform buffers and ready textures. */
getBindings() {
const validBindings = {};
const validBindingsMap = validBindings;
for (const [name, binding] of Object.entries(this.bindings)) {
if (binding instanceof DynamicTexture) {
if (binding.isReady) {
validBindingsMap[name] = binding.texture;
}
}
else {
validBindingsMap[name] = binding;
}
}
return validBindings;
}
/** Packages resolved material bindings into logical bind group `3`. */
getBindingsByGroup() {
return this.factory.getBindingsByGroup(this.getBindings());
}
/** Returns the stable bind-group cache token for the requested bind group. */
getBindGroupCacheKey(group) {
return group === MATERIAL_BIND_GROUP ? this._bindGroupCacheToken : null;
}
/** Returns the latest update timestamp across material-owned resources. */
getBindingsUpdateTimestamp() {
let timestamp = 0;
for (const binding of Object.values(this.bindings)) {
if (binding instanceof TextureView) {
timestamp = Math.max(timestamp, binding.texture.updateTimestamp);
}
else if (binding instanceof Buffer || binding instanceof Texture) {
timestamp = Math.max(timestamp, binding.updateTimestamp);
}
else if (binding instanceof DynamicTexture) {
timestamp = binding.texture
? Math.max(timestamp, binding.texture.updateTimestamp)
: Infinity;
}
else if (!(binding instanceof Sampler)) {
timestamp = Math.max(timestamp, binding.buffer.updateTimestamp);
}
}
return timestamp;
}
/** Replaces owned resource bindings and invalidates the material cache identity when needed. */
_replaceOwnedBindings(bindings) {
const didChange = this._setOwnedBindings(bindings);
if (didChange) {
this._bindGroupCacheToken = {};
}
}
_setOwnedBindings(bindings) {
let didChange = false;
for (const [name, binding] of Object.entries(bindings)) {
if (binding === undefined) {
continue;
}
if (!this.ownsBinding(name)) {
continue;
}
if (this.bindings[name] !== binding) {
this.bindings[name] = binding;
didChange = true;
}
}
return didChange;
}
}
//# sourceMappingURL=material.js.map