Source: material/phong-material.js

import * as SharedMaterialConstants          from '../constants/material.js';
import * as DirectionalMaterialConstants     from '../constants/directional-light-material.js';
import * as MaterialConstants                from '../constants/phong-material.js';
import { ECMASCRIPT_TYPEOF_RESULTS }         from '../constants/ecmascript-types.js';
import { PHONG_MATERIAL_EXCEPTION_MESSAGES } from '../exception-messages/phong-material.js';
import { DirectionalLightMaterial }          from './directional-light-material.js';
import { ShaderProgram }                     from '../shader/shader-program.js';

/**
 * Options used by `PhongMaterial`.
 *
 * @typedef {Object} PhongMaterialOptions
 * @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 {Float32Array | number[]} [specularColor]  - Specular RGB color [red, green, blue] in [0..1] range.
 * @property {number} [specularStrength]                - Specular term multiplier.
 * @property {number} [shininess]                       - Shininess exponent for specular highlight.
 */

/**
 * Phong material with one directional light.
 *
 * This material expects geometry to provide normals at attribute `location 3`.
 */
export class PhongMaterial extends DirectionalLightMaterial {

    /**
     * Specular color (RGB).
     *
     * @type {Float32Array}
     * @private
     */
    #specularColor = new Float32Array(DirectionalMaterialConstants.DIRECTIONAL_LIGHT_MATERIAL_VECTOR3_ELEMENT_COUNT);

    /**
     * Specular strength multiplier.
     *
     * @type {number}
     * @private
     */
    #specularStrength = SharedMaterialConstants.MATERIAL_SPECULAR_DEFAULTS.STRENGTH;

    /**
     * Shininess exponent (specular power).
     *
     * @type {number}
     * @private
     */
    #shininess = SharedMaterialConstants.MATERIAL_SPECULAR_DEFAULTS.SHININESS;

    /**
     * Creates a new `PhongMaterial`.
     *
     * @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context, used to compile shaders.
     * @param {PhongMaterialOptions} [options]      - Material options.
     */
    constructor(webglContext, options = {}) {
        if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT || Array.isArray(options)) {
            throw new TypeError(PHONG_MATERIAL_EXCEPTION_MESSAGES.OPTIONS_OBJECT);
        }

        const sources       = MaterialConstants.PHONG_MATERIAL_SHADERS;
        const shaderProgram = new ShaderProgram(webglContext, sources.VERTEX, sources.FRAGMENT);

        super(webglContext, shaderProgram, options, {
            ownsShaderProgram: SharedMaterialConstants.MATERIAL_DEFAULTS.OWNS_GENERATED_SHADER_PROGRAM
        });

        try {
            this.#initialize(options);
        } catch (error) {
            shaderProgram.dispose();
            throw error;
        }
    }

    /**
     * Uploads Phong-specific uniforms (world matrix, camera position and specular settings).
     *
     * @param {Float32Array} worldMatrix    - World matrix.
     * @param {Float32Array} cameraPosition - Camera position, world space.
     * @protected
     */
    applyAdditionalUniforms(worldMatrix, cameraPosition) {
        const shaderProgram  = this.shaderProgram;
        const lightUniforms  = DirectionalMaterialConstants.DIRECTIONAL_LIGHT_MATERIAL_UNIFORMS;
        const sharedUniforms = SharedMaterialConstants.MATERIAL_UNIFORMS;

        shaderProgram.setMatrix4(lightUniforms.WORLD_MATRIX, worldMatrix);
        shaderProgram.setVector3(lightUniforms.CAMERA_POSITION, cameraPosition);
        shaderProgram.setVector3(sharedUniforms.SPECULAR_COLOR, this.#specularColor);
        shaderProgram.setFloat(sharedUniforms.SPECULAR_STRENGTH, this.#specularStrength);
        shaderProgram.setFloat(sharedUniforms.SHININESS, this.#shininess);
    }

    /**
     * Sets the specular RGB-color.
     *
     * @param {Float32Array | number[]} color - [red, green, blue] in [0..1] range.
     */
    setSpecularColor(color) {
        DirectionalLightMaterial.assertVector3(MaterialConstants.PHONG_MATERIAL_COLOR_CONTEXT, color);
        this.#specularColor.set(color);
    }

    /**
     * Sets specular strength multiplier.
     *
     * @param {number} value - Specular multiplier.
     */
    setSpecularStrength(value) {
        if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
            throw new TypeError(PHONG_MATERIAL_EXCEPTION_MESSAGES.SPECULAR_STRENGTH_TYPE);
        }

        this.#specularStrength = value;
    }

    /**
     * Sets shininess exponent for the specular highlight.
     *
     * @param {number} value - Shininess exponent.
     */
    setShininess(value) {
        if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
            throw new TypeError(PHONG_MATERIAL_EXCEPTION_MESSAGES.SHININESS_TYPE);
        }

        this.#shininess = value;
    }

    /**
     * Returns the internal specular color buffer.
     *
     * @returns {Float32Array}
     */
    get specularColor() {
        return this.#specularColor;
    }

    /**
     * @returns {number} Specular strength multiplier.
     */
    get specularStrength() {
        return this.#specularStrength;
    }

    /**
     * @returns {number} Shininess exponent.
     */
    get shininess() {
        return this.#shininess;
    }

    /**
     * Applies the initial settings after the shader program has been created.
     *
     * @param {PhongMaterialOptions} options - Initial material settings.
     * @private
     */
    #initialize(options) {
        this.#specularColor.set(SharedMaterialConstants.MATERIAL_DEFAULT_COLOR);

        const { specularColor, specularStrength, shininess } = options;

        if (specularColor !== undefined) {
            this.setSpecularColor(specularColor);
        }

        if (specularStrength !== undefined) {
            this.setSpecularStrength(specularStrength);
        }

        if (shininess !== undefined) {
            this.setShininess(shininess);
        }
    }
}