import * as SharedMaterialConstants from '../constants/material.js';
import * as DirectionalMaterialConstants from '../constants/directional-light-material.js';
import * as MaterialConstants from '../constants/mtl-standard-material.js';
import { ECMASCRIPT_TYPEOF_RESULTS } from '../constants/ecmascript-types.js';
import { MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES } from '../exception-messages/mtl-standard-material.js';
import { ShaderProgram } from '../shader/shader-program.js';
import { Texture2D } from '../texture/texture2d.js';
import { DirectionalLightMaterial } from './directional-light-material.js';
/**
* Options used by `MtlStandardMaterial`.
*
* @typedef {Object} MtlStandardMaterialOptions
* @property {Float32Array | number[]} [diffuseColor] - Diffuse RGB color in [0..1] range.
* @property {Float32Array | number[]} [ambientColor] - Ambient RGB color in [0..1] range.
* @property {Float32Array | number[]} [specularColor] - Specular RGB color in [0..1] range.
* @property {Float32Array | number[]} [emissiveColor] - Emissive RGB color in [0..1] range.
* @property {Float32Array | number[]} [lightDirection] - Directional light direction (world space).
* @property {number} [ambientStrength] - Ambient strength multiplier.
* @property {number} [shininess] - Specular exponent.
* @property {number} [specularStrength] - Specular strength multiplier.
* @property {number} [opticalDensity] - Optical density used for reflection blending.
*/
/**
* Texture map setter options.
*
* @typedef {Object} MtlStandardMaterialMapOptions
* @property {number} [textureUnitIndex] - Texture unit index.
* @property {Float32Array | number[]} [uvOffset] - UV-offset vector.
* @property {Float32Array | number[]} [uvScale] - UV-scale vector.
*/
/**
* Material for MTL assets with multiple texture maps and Phong lighting.
*/
export class MtlStandardMaterial extends DirectionalLightMaterial {
/**
* Placeholder texture shared by map slots until a texture is assigned.
* External map textures remain owned by the caller.
*
* @type {Texture2D}
* @private
*/
#fallbackTexture;
/**
* Per-instance texture references, UV buffers and map usage flags.
* The descriptors are shared, but mutable map state is never shared.
*
* @type {Object[]}
* @private
*/
#maps;
/**
* Ambient color.
*
* @type {Float32Array}
* @private
*/
#ambientColor = new Float32Array(SharedMaterialConstants.MATERIAL_DEFAULT_COLOR);
/**
* Specular color.
*
* @type {Float32Array}
* @private
*/
#specularColor = new Float32Array(SharedMaterialConstants.MATERIAL_DEFAULT_COLOR);
/**
* Emissive color.
*
* @type {Float32Array}
* @private
*/
#emissiveColor = new Float32Array(MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.EMISSIVE_COLOR);
/**
* Shininess exponent.
*
* @type {number}
* @private
*/
#shininess = SharedMaterialConstants.MATERIAL_SPECULAR_DEFAULTS.SHININESS;
/**
* Specular strength multiplier.
*
* @type {number}
* @private
*/
#specularStrength = SharedMaterialConstants.MATERIAL_SPECULAR_DEFAULTS.STRENGTH;
/**
* Flag, controlling the specular lighting.
*
* @type {boolean}
* @private
*/
#specularEnabled = true;
/**
* Optical density value.
*
* @type {number}
* @private
*/
#opticalDensity = MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.OPTICAL_DENSITY;
/**
* Bump multiplier.
*
* @type {number}
* @private
*/
#bumpMultiplier = MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.BUMP_MULTIPLIER;
/**
* Displacement scale factor.
*
* @type {number}
* @private
*/
#displacementScale = MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.DISPLACEMENT_SCALE;
/**
* Creates a material with its own program and fallback texture.
*
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context.
* @param {MtlStandardMaterialOptions} [options] - Material settings.
*/
constructor(webglContext, options = {}) {
if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT || Array.isArray(options)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.OPTIONS_OBJECT);
}
const { diffuseColor, lightDirection, ambientStrength } = options;
const sources = MaterialConstants.MTL_STANDARD_MATERIAL_SHADERS;
const shaderProgram = new ShaderProgram(webglContext, sources.VERTEX, sources.FRAGMENT);
const lightOptions = {
color : diffuseColor || SharedMaterialConstants.MATERIAL_DEFAULT_COLOR,
lightDirection,
ambientStrength
};
super(webglContext, shaderProgram, lightOptions, {
ownsShaderProgram: SharedMaterialConstants.MATERIAL_DEFAULTS.OWNS_GENERATED_SHADER_PROGRAM
});
try {
this.#initialize(webglContext, options);
} catch (error) {
this.#fallbackTexture?.dispose();
super.dispose();
throw error;
}
}
/**
* Uploads the material-specific uniforms in the existing order.
*
* @param {Float32Array} worldMatrix - World matrix.
* @param {Float32Array} cameraPosition - Camera position.
* @protected
*/
applyAdditionalUniforms(worldMatrix, cameraPosition) {
const shaderProgram = this.shaderProgram;
const uniforms = DirectionalMaterialConstants.DIRECTIONAL_LIGHT_MATERIAL_UNIFORMS;
shaderProgram.setMatrix4(uniforms.WORLD_MATRIX, worldMatrix);
shaderProgram.setVector3(uniforms.CAMERA_POSITION, cameraPosition);
this.#applyShadingUniforms(shaderProgram);
this.#applySurfaceUniforms(shaderProgram);
this.#applyUvTransforms(shaderProgram);
this.#applyMapFlags(shaderProgram);
this.#applyTextures(shaderProgram);
}
/**
* Sets the ambient color.
*
* @param {Float32Array | number[]} color - RGB color in [0..1] range.
*/
setAmbientColor(color) {
MtlStandardMaterial.#assertVector3(MaterialConstants.MTL_STANDARD_MATERIAL_CONTEXTS.AMBIENT_COLOR, color);
this.#ambientColor.set(color);
}
/**
* Sets the specular color.
*
* @param {Float32Array | number[]} color - RGB color in [0..1] range.
*/
setSpecularColor(color) {
MtlStandardMaterial.#assertVector3(MaterialConstants.MTL_STANDARD_MATERIAL_CONTEXTS.SPECULAR_COLOR, color);
this.#specularColor.set(color);
}
/**
* Sets the emissive color.
*
* @param {Float32Array | number[]} color - RGB color in [0..1] range.
*/
setEmissiveColor(color) {
MtlStandardMaterial.#assertVector3(MaterialConstants.MTL_STANDARD_MATERIAL_CONTEXTS.EMISSIVE_COLOR, color);
this.#emissiveColor.set(color);
}
/**
* Sets the shininess exponent.
*
* @param {number} value - Shininess exponent.
*/
setShininess(value) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.SHININESS_TYPE);
}
this.#shininess = value;
}
/**
* Sets the specular strength multiplier.
*
* @param {number} value - Specular strength.
*/
setSpecularStrength(value) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.SPECULAR_STRENGTH_TYPE);
}
this.#specularStrength = value;
}
/**
* Enables or disables specular term.
*
* @param {boolean} enabled - Specular usage flag.
*/
setSpecularEnabled(enabled) {
if (typeof enabled !== ECMASCRIPT_TYPEOF_RESULTS.BOOLEAN) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.SPECULAR_ENABLED_TYPE);
}
this.#specularEnabled = enabled;
}
/**
* Sets optical density value.
*
* @param {number} value - Optical density.
*/
setOpticalDensity(value) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.OPTICAL_DENSITY_TYPE);
}
this.#opticalDensity = value;
}
/**
* Sets bump multiplier value.
*
* @param {number} value - Bump multiplier.
*/
setBumpMultiplier(value) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.BUMP_MULTIPLIER_TYPE);
}
this.#bumpMultiplier = value;
}
/**
* Sets displacement scale.
*
* @param {number} value - Displacement scale factor.
*/
setDisplacementScale(value) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.DISPLACEMENT_SCALE_TYPE);
}
this.#displacementScale = value;
}
/**
* Sets a diffuse texture and its UV-transform.
*
* @param {Texture2D} texture - Diffuse texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setDiffuseMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.DIFFUSE, texture, options);
}
/**
* Sets an ambient texture and its UV-transform.
*
* @param {Texture2D} texture - Ambient texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setAmbientMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.AMBIENT, texture, options);
}
/**
* Sets a specular texture and its UV-transform.
*
* @param {Texture2D} texture - Specular texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setSpecularMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.SPECULAR, texture, options);
}
/**
* Sets an alpha texture and its UV-transform.
*
* @param {Texture2D} texture - Alpha texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setAlphaMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.ALPHA, texture, options);
}
/**
* Sets a bump texture and its UV-transform.
*
* @param {Texture2D} texture - Bump texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setBumpMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.BUMP, texture, options);
}
/**
* Sets a displacement texture and its UV-transform.
*
* @param {Texture2D} texture - Displacement texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setDisplacementMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.DISPLACEMENT, texture, options);
}
/**
* Sets a reflection texture and its UV-transform.
*
* @param {Texture2D} texture - Reflection texture.
* @param {MtlStandardMaterialMapOptions} [options] - Map options.
*/
setReflectionMap(texture, options = {}) {
this.#setMap(MaterialConstants.MTL_STANDARD_MATERIAL_MAP_INDEXES.REFLECTION, texture, options);
}
/**
* Releases the fallback texture and program, leaving external maps intact.
*/
dispose() {
if (this.isDisposed) {
return;
}
this.#fallbackTexture.dispose();
super.dispose();
}
/**
* Allocates per-map state and applies the initial shading options.
*
* @param {WebGL2RenderingContext} webglContext - Texture allocation context.
* @param {MtlStandardMaterialOptions} options - Initial material settings.
* @private
*/
#initialize(webglContext, options) {
this.#fallbackTexture = new Texture2D(webglContext);
this.#maps = MaterialConstants.MTL_STANDARD_MATERIAL_MAPS.map((descriptor) => ({
descriptor,
texture : this.#fallbackTexture,
textureUnitIndex : descriptor.TEXTURE_UNIT,
uvOffset : new Float32Array(MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.UV_OFFSET),
uvScale : new Float32Array(MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.UV_SCALE),
enabled : false
}));
this.#applyOptions(options);
}
/**
* Applies initial colors and optional specular settings.
*
* @param {MtlStandardMaterialOptions} options - Initial material settings.
* @private
*/
#applyOptions(options) {
const { ambientColor, specularColor, emissiveColor, shininess, specularStrength, opticalDensity } = options;
this.setAmbientColor(ambientColor || SharedMaterialConstants.MATERIAL_DEFAULT_COLOR);
this.setSpecularColor(specularColor || SharedMaterialConstants.MATERIAL_DEFAULT_COLOR);
this.setEmissiveColor(emissiveColor || MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.EMISSIVE_COLOR);
if (shininess !== undefined) {
this.setShininess(shininess);
}
if (specularStrength !== undefined) {
this.setSpecularStrength(specularStrength);
}
if (opticalDensity !== undefined) {
this.setOpticalDensity(opticalDensity);
}
}
/**
* Uploads colors and specular settings without creating temporary buffers.
*
* @param {ShaderProgram} shaderProgram - Active material program.
* @private
*/
#applyShadingUniforms(shaderProgram) {
const uniforms = MaterialConstants.MTL_STANDARD_MATERIAL_UNIFORMS;
const sharedUniforms = SharedMaterialConstants.MATERIAL_UNIFORMS;
const floatFlags = SharedMaterialConstants.MATERIAL_BOOLEAN_FLOATS;
shaderProgram.setVector3(uniforms.AMBIENT_COLOR, this.#ambientColor);
shaderProgram.setVector3(sharedUniforms.SPECULAR_COLOR, this.#specularColor);
shaderProgram.setVector3(uniforms.EMISSIVE_COLOR, this.#emissiveColor);
shaderProgram.setFloat(sharedUniforms.SPECULAR_STRENGTH, this.#specularStrength);
shaderProgram.setFloat(sharedUniforms.SHININESS, this.#shininess);
shaderProgram.setFloat(uniforms.SPECULAR_ENABLED, this.#specularEnabled ? floatFlags.TRUE : floatFlags.FALSE);
}
/**
* Uploads optical and surface detail settings.
*
* @param {ShaderProgram} shaderProgram - Active material program.
* @private
*/
#applySurfaceUniforms(shaderProgram) {
const uniforms = MaterialConstants.MTL_STANDARD_MATERIAL_UNIFORMS;
shaderProgram.setFloat(uniforms.OPTICAL_DENSITY, this.#opticalDensity);
shaderProgram.setFloat(uniforms.BUMP_MULTIPLIER, this.#bumpMultiplier);
shaderProgram.setFloat(uniforms.DISPLACEMENT_SCALE, this.#displacementScale);
}
/**
* Uploads each map's independent UV buffers.
*
* @param {ShaderProgram} shaderProgram - Active material program.
* @private
*/
#applyUvTransforms(shaderProgram) {
for (const map of this.#maps) {
shaderProgram.setVector2(map.descriptor.UV_OFFSET, map.uvOffset);
shaderProgram.setVector2(map.descriptor.UV_SCALE, map.uvScale);
}
}
/**
* Uploads the enabled flag for every texture map.
*
* @param {ShaderProgram} shaderProgram - Active material program.
* @private
*/
#applyMapFlags(shaderProgram) {
const floatFlags = SharedMaterialConstants.MATERIAL_BOOLEAN_FLOATS;
for (const map of this.#maps) {
shaderProgram.setFloat(map.descriptor.ENABLED, map.enabled ? floatFlags.TRUE : floatFlags.FALSE);
}
}
/**
* Binds all map textures in their established sampler order.
*
* @param {ShaderProgram} shaderProgram - Active material program.
* @private
*/
#applyTextures(shaderProgram) {
for (const map of this.#maps) {
shaderProgram.setTexture2D(map.descriptor.SAMPLER, map.texture, map.textureUnitIndex);
}
}
/**
* Validates a replacement before changing any map state.
* Omitted texture units preserve the slot's current assignment.
*
* @param {number} index - Map slot index.
* @param {Texture2D} texture - Externally owned texture.
* @param {MtlStandardMaterialMapOptions} options - Map settings.
* @private
*/
#setMap(index, texture, options) {
const map = this.#maps[index];
const context = map.descriptor.CONTEXT;
if (!(texture instanceof Texture2D)) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_TEXTURE_SUFFIX);
}
if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT || Array.isArray(options)) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_OPTIONS_OBJECT_SUFFIX);
}
const settings = MtlStandardMaterial.#resolveMapOptions(context, options, map.textureUnitIndex);
map.texture = texture;
map.textureUnitIndex = settings.textureUnitIndex;
map.enabled = true;
map.uvOffset.set(settings.uvOffset);
map.uvScale.set(settings.uvScale);
}
/**
* Resolves a map's texture unit and UV transforms.
*
* @param {string} context - Method name for errors.
* @param {MtlStandardMaterialMapOptions} options - Map settings.
* @param {number} defaultTextureUnit - Current texture unit.
* @returns {MtlStandardMaterialMapOptions} - Validated map settings.
* @private
*/
static #resolveMapOptions(context, options, defaultTextureUnit) {
const {
textureUnitIndex = defaultTextureUnit,
uvOffset = MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.UV_OFFSET,
uvScale = MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.UV_SCALE
} = options;
if (!Number.isInteger(textureUnitIndex) || textureUnitIndex < SharedMaterialConstants.MATERIAL_TEXTURE_UNITS.MIN) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_TEXTURE_UNIT_INDEX_SUFFIX);
}
const contexts = MaterialConstants.MTL_STANDARD_MATERIAL_CONTEXTS;
MtlStandardMaterial.#assertVector2(context + contexts.UV_OFFSET, uvOffset);
MtlStandardMaterial.#assertVector2(context + contexts.UV_SCALE, uvScale);
return { textureUnitIndex, uvOffset, uvScale };
}
/**
* Validates the `vector2` arrays.
*
* @param {string} context - Error context.
* @param {Float32Array | number[]} value - Vector to validate.
* @returns {void}
* @private
*/
static #assertVector2(context, value) {
if (!Array.isArray(value) && !(value instanceof Float32Array)) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_VECTOR2_TYPE_SUFFIX);
}
if (value.length !== MaterialConstants.MTL_STANDARD_MATERIAL_DEFAULTS.UV_OFFSET.length) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_VECTOR2_COMPONENTS_SUFFIX);
}
}
/**
* Validates the `vector3` arrays.
*
* @param {string} context - Error context.
* @param {Float32Array | number[]} value - Vector to validate.
* @returns {void}
* @private
*/
static #assertVector3(context, value) {
if (!Array.isArray(value) && !(value instanceof Float32Array)) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_VECTOR2_TYPE_SUFFIX);
}
if (value.length !== DirectionalMaterialConstants.DIRECTIONAL_LIGHT_MATERIAL_VECTOR3_ELEMENT_COUNT) {
throw new TypeError(context + MTL_STANDARD_MATERIAL_EXCEPTION_MESSAGES.EXPECTS_VECTOR3_COMPONENTS_SUFFIX);
}
}
}