import { Material } from './material.js';
import { ShaderProgram } from '../shader/shader-program.js';
/**
* Attribute location used by `vec3` position.
* Must match geometry's `POSITION_ATTRIBUTE_LOCATION`.
*
* @type {number}
*/
export const POSITION_ATTRIBUTE_LOCATION = 0;
/**
* Attribute location used by `vec3` normal.
* Must match geometry's `NORMAL_ATTRIBUTE_LOCATION`.
*
* @type {number}
*/
export const NORMAL_ATTRIBUTE_LOCATION = 3;
/**
* Name of the final transformation matrix uniform: `view projection * world`.
*
* @type {string}
*/
export const FINAL_MATRIX_UNIFORM_NAME = 'u_matrix';
/**
* Name of the world inverse transpose matrix uniform: `(world ^ -1) ^ T`.
* Used to correctly transform normals into world space under non-uniform scale.
*
* @type {string}
*/
export const WORLD_INVERSE_TRANSPOSE_MATRIX_UNIFORM_NAME = 'u_worldInverseTranspose';
/**
* Name of the world matrix uniform.
* Used by materials, that require world-space position reconstruction in shaders.
*
* @type {string}
*/
export const WORLD_MATRIX_UNIFORM_NAME = 'u_worldMatrix';
/**
* Diffuse/base color uniform name.
*
* @type {string}
*/
export const COLOR_UNIFORM_NAME = 'u_color';
/**
* Directional light direction uniform name (world space).
* Vector points from surface to the light (towards the light).
*
* @type {string}
*/
export const LIGHT_DIRECTION_UNIFORM_NAME = 'u_lightDirection';
/**
* Camera position uniform name (world space).
*
* @type {string}
*/
export const CAMERA_POSITION_UNIFORM_NAME = 'u_cameraPosition';
/**
* Ambient strength uniform name.
*
* @type {string}
*/
export const AMBIENT_STRENGTH_UNIFORM_NAME = 'u_ambientStrength';
/**
* Directional strength uniform name.
*
* @type {string}
*/
export const DIRECTIONAL_STRENGTH_UNIFORM_NAME = 'u_directionalStrength';
/**
* Lighting enabled uniform name.
*
* @type {string}
*/
export const LIGHTING_ENABLED_UNIFORM_NAME = 'u_lightingEnabled';
/**
* Opacity uniform name.
*
* @type {string}
*/
export const OPACITY_UNIFORM_NAME = 'u_opacity';
/**
* Vector3 length (component count).
*
* @type {number}
*/
export const VECTOR3_ELEMENT_COUNT = 3;
/**
* Default diffuse/base color (RGB).
*
* @type {Float32Array}
*/
export const DEFAULT_COLOR = new Float32Array([0.85, 0.85, 0.85]);
/**
* Default directional light direction in world space (points from surface to light).
*
* @type {Float32Array}
*/
export const DEFAULT_LIGHT_DIRECTION = new Float32Array([0.5, 0.7, 1.0]);
/**
* Default ambient term strength.
*
* @type {number}
*/
export const DEFAULT_AMBIENT_STRENGTH = 0.2;
/**
* Default directional strength.
*
* @type {number}
*/
export const DEFAULT_DIRECTIONAL_STRENGTH = 1.0;
/**
* Default lighting enabled flag (float).
*
* @type {number}
*/
export const DEFAULT_LIGHTING_ENABLED = 1.0;
/**
* Minimum allowed squared length for a direction vector. Used to reject the zero-length direction.
*
* @type {number}
*/
const MIN_DIRECTION_LENGTH_SQUARED = 0.0;
/**
* `Boolean as-float` value for false.
*
* @type {number}
*/
const FLOAT_FALSE = 0.0;
/**
* `Boolean-as-float` value for true.
*
* @type {number}
*/
const FLOAT_TRUE = 1.0;
/**
* Minimum accepted lighting enabled value.
*
* @type {number}
*/
const MIN_LIGHTING_ENABLED = 0.0;
/**
* Maximum accepted lighting enabled value.
*
* @type {number}
*/
const MAX_LIGHTING_ENABLED = 1.0;
/**
* Directional strength value used for disabling directional light.
*
* @type {number}
*/
const DIRECTIONAL_STRENGTH_DISABLED = 0.0;
/**
* Threshold used to interpret the lighting enabled uniform as a boolean.
*
* @type {number}
*/
const LIGHTING_ENABLED_THRESHOLD = 0.5;
/**
* Numerator used when computing inverse vector length: `1 / sqrt(lengthSquared)`.
*
* @type {number}
*/
const INVERSE_LENGTH_NUMERATOR = 1.0;
/**
* Error message for invalid lighting enabled value types.
*
* @type {string}
*/
const ERROR_LIGHTING_ENABLED_TYPE = '`DirectionalLightMaterial.setLightingEnabled` expects a boolean or a finite number.';
/**
* Error message for invalid lighting enabled value range.
*
* @type {string}
*/
const ERROR_LIGHTING_ENABLED_RANGE = '`DirectionalLightMaterial.setLightingEnabled` expects a value in [0..1].';
/**
* Error message for invalid directional strength values.
*
* @type {string}
*/
const ERROR_DIRECTIONAL_STRENGTH_TYPE = '`DirectionalLightMaterial.setDirectionalStrength` expects a finite number.';
/**
* Error message for invalid directional enabled values.
*
* @type {string}
*/
const ERROR_DIRECTIONAL_ENABLED_TYPE = '`DirectionalLightMaterial.setDirectionalEnabled` expects a boolean.';
/**
* Options common to directional-light materials.
*
* @typedef {Object} DirectionalLightMaterialOptions
* @property {Float32Array | number[]} [color] - Diffuse RGB color [red, green, blue] in [0..1] range.
* @property {Float32Array | number[]} [lightDirection] - Directional light direction (world space), normalized internally.
* @property {number} [ambientStrength] - Ambient term multiplier.
* @property {number} [directionalStrength] - Directional light strength multiplier.
* @property {boolean | number} [lightingEnabled] - Lighting enabled flag (boolean or 0..1 float).
*/
/**
* Material base options used by `DirectionalLightMaterial`.
*
* @typedef {Object} DirectionalLightMaterialBaseOptions
* @property {boolean} [ownsShaderProgram=true] - Whether this material owns and disposes the shader program.
*/
/**
* Base class for materials that use a single directional light and require normals.
*
* Provides:
* - shared constants (attribute locations, common uniform names)
* - shared option parsing (color, lightDirection, ambientStrength)
* - shared setters with validation
* - a unified `apply(finalMatrix, worldMatrix, worldInverseTransposeMatrix, cameraPosition)` contract
*
* Subclasses may override `applyAdditionalUniforms(worldMatrix, cameraPosition)`.
*/
export class DirectionalLightMaterial extends Material {
/**
* Diffuse/base color (RGB).
*
* @type {Float32Array}
* @private
*/
#color = new Float32Array(VECTOR3_ELEMENT_COUNT);
/**
* Directional light direction (world space, normalized).
*
* @type {Float32Array}
* @private
*/
#lightDirection = new Float32Array(VECTOR3_ELEMENT_COUNT);
/**
* Ambient term multiplier.
*
* @type {number}
* @private
*/
#ambientStrength = DEFAULT_AMBIENT_STRENGTH;
/**
* Directional strength multiplier.
*
* @type {number}
* @private
*/
#directionalStrength = DEFAULT_DIRECTIONAL_STRENGTH;
/**
* Lighting enabled flag stored as a float.
*
* @type {number}
* @private
*/
#lightingEnabled = DEFAULT_LIGHTING_ENABLED;
/**
* Creates a new directional-light material.
*
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context used to create the GPU resources.
* @param {ShaderProgram} shaderProgram - Compiled shader program instance.
* @param {DirectionalLightMaterialOptions} [options] - Common material options.
* @param {DirectionalLightMaterialBaseOptions} [materialOptions] - Material base options.
*/
constructor(webglContext, shaderProgram, options = {}, materialOptions = {}) {
if (!(webglContext instanceof WebGL2RenderingContext)) {
throw new TypeError('`DirectionalLightMaterial` expects a WebGL2RenderingContext.');
}
if (!(shaderProgram instanceof ShaderProgram)) {
throw new TypeError('`DirectionalLightMaterial` expects a ShaderProgram instance.');
}
DirectionalLightMaterial.#assertPlainObject('`DirectionalLightMaterial`', options);
DirectionalLightMaterial.#assertPlainObject('`DirectionalLightMaterial`', materialOptions);
const { ownsShaderProgram = true } = materialOptions;
if (typeof ownsShaderProgram !== 'boolean') {
throw new TypeError('`DirectionalLightMaterial` option "ownsShaderProgram" must be a boolean.');
}
super(webglContext, shaderProgram, { ownsShaderProgram });
this.#color.set(DEFAULT_COLOR);
this.setLightDirection(DEFAULT_LIGHT_DIRECTION);
this.#ambientStrength = DEFAULT_AMBIENT_STRENGTH;
this.#directionalStrength = DEFAULT_DIRECTIONAL_STRENGTH;
const {
color,
lightDirection,
ambientStrength,
directionalStrength,
lightingEnabled
} = options;
if (color !== undefined) {
this.setColor(color);
}
if (lightDirection !== undefined) {
this.setLightDirection(lightDirection);
}
if (ambientStrength !== undefined) {
this.setAmbientStrength(ambientStrength);
}
if (directionalStrength !== undefined) {
this.setDirectionalStrength(directionalStrength);
}
if (lightingEnabled !== undefined) {
this.setLightingEnabled(lightingEnabled);
}
}
/**
* Uploads per-object uniforms for a draw call. Unified contract for directional-light materials.
*
* Renderer passes:
* - finalMatrix (view projection * world)
* - worldMatrix
* - worldInverseTransposeMatrix
* - cameraPosition
*
* @param {Float32Array} finalMatrix - View projection * world matrix.
* @param {Float32Array} worldMatrix - World matrix.
* @param {Float32Array} worldInverseTransposeMatrix - `(world ^ -1) ^ T` used to transform normals.
* @param {Float32Array} cameraPosition - Camera position, world space.
*/
apply(finalMatrix, worldMatrix, worldInverseTransposeMatrix, cameraPosition) {
this.shaderProgram.setMatrix4(FINAL_MATRIX_UNIFORM_NAME, finalMatrix);
this.shaderProgram.setMatrix4(WORLD_INVERSE_TRANSPOSE_MATRIX_UNIFORM_NAME, worldInverseTransposeMatrix);
this.shaderProgram.setVector3(COLOR_UNIFORM_NAME, this.#color);
this.shaderProgram.setVector3(LIGHT_DIRECTION_UNIFORM_NAME, this.#lightDirection);
this.shaderProgram.setFloat(AMBIENT_STRENGTH_UNIFORM_NAME, this.#ambientStrength);
this.shaderProgram.setFloat(DIRECTIONAL_STRENGTH_UNIFORM_NAME, this.#directionalStrength);
this.shaderProgram.setFloat(LIGHTING_ENABLED_UNIFORM_NAME, this.#lightingEnabled);
this.shaderProgram.setFloat(OPACITY_UNIFORM_NAME, this.opacity);
this.applyAdditionalUniforms(worldMatrix, cameraPosition);
}
/**
* Hook for subclasses to upload additional per-object uniforms.
* Default implementation in this class does nothing.
*
* @param {Float32Array} worldMatrix - World matrix.
* @param {Float32Array} cameraPosition - Camera position, world space.
* @protected
*/
applyAdditionalUniforms(worldMatrix, cameraPosition) {
void worldMatrix;
void cameraPosition;
}
/**
* Sets the diffuse/base RGB color.
*
* @param {Float32Array | number[]} color - [red, green, blue] in [0..1] range.
*/
setColor(color) {
DirectionalLightMaterial.assertVector3('`DirectionalLightMaterial.setColor`', color);
this.#color[0] = color[0];
this.#color[1] = color[1];
this.#color[2] = color[2];
}
/**
* Sets the light direction (world space). The direction is normalized internally.
*
* @param {Float32Array | number[]} direction - [x, y, z] direction vector (non-zero).
*/
setLightDirection(direction) {
DirectionalLightMaterial.assertVector3('`DirectionalLightMaterial.setLightDirection`', direction);
const directionX = direction[0];
const directionY = direction[1];
const directionZ = direction[2];
const directionLengthSquared =
directionX * directionX +
directionY * directionY +
directionZ * directionZ;
if (!Number.isFinite(directionLengthSquared) || directionLengthSquared <= MIN_DIRECTION_LENGTH_SQUARED) {
throw new TypeError('`DirectionalLightMaterial.setLightDirection` expects a non-zero finite vector.');
}
const inverseDirectionLength = INVERSE_LENGTH_NUMERATOR / Math.sqrt(directionLengthSquared);
this.#lightDirection[0] = directionX * inverseDirectionLength;
this.#lightDirection[1] = directionY * inverseDirectionLength;
this.#lightDirection[2] = directionZ * inverseDirectionLength;
}
/**
* Sets ambient strength multiplier.
*
* @param {number} value - Ambient multiplier.
*/
setAmbientStrength(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError('`DirectionalLightMaterial.setAmbientStrength` expects a finite number.');
}
this.#ambientStrength = value;
}
/**
* Sets directional strength multiplier.
*
* @param {number} value - Directional strength multiplier.
* @returns {void}
* @throws {TypeError} When the value is invalid.
*/
setDirectionalStrength(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError(ERROR_DIRECTIONAL_STRENGTH_TYPE);
}
this.#directionalStrength = value;
}
/**
* Enables or disables the directional light contribution.
*
* @param {boolean} enabled - Whether directional lighting should be enabled.
* @returns {void}
* @throws {TypeError} When the value is invalid.
*/
setDirectionalEnabled(enabled) {
if (typeof enabled !== 'boolean') {
throw new TypeError(ERROR_DIRECTIONAL_ENABLED_TYPE);
}
this.#directionalStrength = enabled ? DEFAULT_DIRECTIONAL_STRENGTH : DIRECTIONAL_STRENGTH_DISABLED;
}
/**
* Sets lighting enabled state.
*
* @param {boolean | number} enabled - Boolean or a [0..1] numeric flag.
* @returns {void}
* @throws {TypeError} When the value type is invalid.
* @throws {RangeError} When the value is outside [0..1].
*/
setLightingEnabled(enabled) {
if (typeof enabled === 'boolean') {
this.#lightingEnabled = enabled ? FLOAT_TRUE : FLOAT_FALSE;
return;
}
if (typeof enabled !== 'number' || !Number.isFinite(enabled)) {
throw new TypeError(ERROR_LIGHTING_ENABLED_TYPE);
}
if (enabled < MIN_LIGHTING_ENABLED || enabled > MAX_LIGHTING_ENABLED) {
throw new RangeError(ERROR_LIGHTING_ENABLED_RANGE);
}
this.#lightingEnabled = enabled;
}
/**
* @returns {boolean} - Returns current lighting enabled state.
*/
isLightingEnabled() {
return this.#lightingEnabled > LIGHTING_ENABLED_THRESHOLD;
}
/**
* @returns {Float32Array} - Returns the internal diffuse/base color buffer.
*/
get color() {
return this.#color;
}
/**
* @returns {Float32Array} - Returns the internal normalized light direction buffer.
*/
get lightDirection() {
return this.#lightDirection;
}
/**
* @returns {number} - Ambient strength multiplier.
*/
get ambientStrength() {
return this.#ambientStrength;
}
/**
* @returns {number} - Returns the directional strength multiplier value.
*/
getDirectionalStrength() {
return this.#directionalStrength;
}
/**
* @returns {number} - Gettet for the directional strength multiplier.
*/
get directionalStrength() {
return this.#directionalStrength;
}
/**
* Validates a vector3-like input.
*
* @param {string} methodName - Method name for error messages.
* @param {Float32Array | number[]} vector3 - Vector to validate.
*/
static assertVector3(methodName, vector3) {
if (!Array.isArray(vector3) && !(vector3 instanceof Float32Array)) {
throw new TypeError(`${methodName} expects a number[] or Float32Array.`);
}
if (vector3.length !== VECTOR3_ELEMENT_COUNT) {
throw new TypeError(`${methodName} expects exactly 3 components [x, y, z].`);
}
if (!Number.isFinite(vector3[0]) || !Number.isFinite(vector3[1]) || !Number.isFinite(vector3[2])) {
throw new TypeError(`${methodName} expects all components to be finite numbers.`);
}
}
/**
* Validates a plain options object.
*
* @param {string} methodName - Method or class name for error messages.
* @param {Object} object - Object to validate.
* @private
*/
static #assertPlainObject(methodName, object) {
if (object === null || typeof object !== 'object' || Array.isArray(object)) {
throw new TypeError(`${methodName} expects an options object (plain object).`);
}
}
}