import { ShaderProgram } from '../shader/shader-program.js';
import {
DirectionalLightMaterial,
POSITION_ATTRIBUTE_LOCATION,
NORMAL_ATTRIBUTE_LOCATION,
FINAL_MATRIX_UNIFORM_NAME,
WORLD_MATRIX_UNIFORM_NAME,
WORLD_INVERSE_TRANSPOSE_MATRIX_UNIFORM_NAME,
COLOR_UNIFORM_NAME,
LIGHT_DIRECTION_UNIFORM_NAME,
CAMERA_POSITION_UNIFORM_NAME,
AMBIENT_STRENGTH_UNIFORM_NAME,
DIRECTIONAL_STRENGTH_UNIFORM_NAME,
LIGHTING_ENABLED_UNIFORM_NAME,
OPACITY_UNIFORM_NAME,
VECTOR3_ELEMENT_COUNT
} from './directional-light-material.js';
/**
* Specular color uniform name.
*
* @type {string}
*/
const SPECULAR_COLOR_UNIFORM_NAME = 'u_specularColor';
/**
* Specular strength uniform name.
*
* @type {string}
*/
const SPECULAR_STRENGTH_UNIFORM_NAME = 'u_specularStrength';
/**
* Shininess uniform name (specular exponent).
*
* @type {string}
*/
const SHININESS_UNIFORM_NAME = 'u_shininess';
/**
* Default specular color (RGB).
*
* @type {Float32Array}
*/
const DEFAULT_SPECULAR_COLOR = new Float32Array([1.0, 1.0, 1.0]);
/**
* Default specular strength multiplier.
*
* @type {number}
*/
const DEFAULT_SPECULAR_STRENGTH = 1.0;
/**
* Default shininess exponent.
*
* @type {number}
*/
const DEFAULT_SHININESS = 16.0;
/**
* GLSL vertex shader source code.
*
* Requires:
* - position attribute at location 0
* - normal attribute at location 3
*
* @type {string}
*/
const VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
layout(location = ${POSITION_ATTRIBUTE_LOCATION}) in vec3 a_position;
layout(location = ${NORMAL_ATTRIBUTE_LOCATION}) in vec3 a_normal;
uniform mat4 ${FINAL_MATRIX_UNIFORM_NAME};
uniform mat4 ${WORLD_MATRIX_UNIFORM_NAME};
uniform mat4 ${WORLD_INVERSE_TRANSPOSE_MATRIX_UNIFORM_NAME};
out vec3 v_worldPosition;
out vec3 v_normal;
void main() {
gl_Position = ${FINAL_MATRIX_UNIFORM_NAME} * vec4(a_position, 1.0);
v_worldPosition = (${WORLD_MATRIX_UNIFORM_NAME} * vec4(a_position, 1.0)).xyz;
v_normal = (${WORLD_INVERSE_TRANSPOSE_MATRIX_UNIFORM_NAME} * vec4(a_normal, 0.0)).xyz;
}
`;
/**
* GLSL fragment shader source code.
*
* Implements classic Phong lighting: `ambient + diffuse + specular`.
*
* @type {string}
*/
const FRAGMENT_SHADER_SOURCE = `#version 300 es
precision mediump float;
in vec3 v_worldPosition;
in vec3 v_normal;
uniform vec3 ${COLOR_UNIFORM_NAME};
uniform vec3 ${SPECULAR_COLOR_UNIFORM_NAME};
uniform vec3 ${LIGHT_DIRECTION_UNIFORM_NAME};
uniform vec3 ${CAMERA_POSITION_UNIFORM_NAME};
uniform float ${AMBIENT_STRENGTH_UNIFORM_NAME};
uniform float ${DIRECTIONAL_STRENGTH_UNIFORM_NAME};
uniform float ${LIGHTING_ENABLED_UNIFORM_NAME};
uniform float ${SPECULAR_STRENGTH_UNIFORM_NAME};
uniform float ${SHININESS_UNIFORM_NAME};
uniform float ${OPACITY_UNIFORM_NAME};
out vec4 outColor;
void main() {
vec3 surface_normal = normalize(v_normal);
if (!gl_FrontFacing) {
surface_normal = -surface_normal;
}
vec3 light_direction = normalize(${LIGHT_DIRECTION_UNIFORM_NAME});
vec3 view_direction = normalize(${CAMERA_POSITION_UNIFORM_NAME} - v_worldPosition);
float lighting_enabled = ${LIGHTING_ENABLED_UNIFORM_NAME};
float diffuse_intensity = max(dot(surface_normal, light_direction), 0.0) * ${DIRECTIONAL_STRENGTH_UNIFORM_NAME};
float specular_intensity = 0.0;
if (diffuse_intensity > 0.0) {
vec3 reflection_direction = reflect(-light_direction, surface_normal);
float specular_base = max(dot(view_direction, reflection_direction), 0.0);
specular_intensity = pow(specular_base, ${SHININESS_UNIFORM_NAME});
}
vec3 ambient = ${COLOR_UNIFORM_NAME} * ${AMBIENT_STRENGTH_UNIFORM_NAME};
vec3 diffuse = ${COLOR_UNIFORM_NAME} * (diffuse_intensity * lighting_enabled);
vec3 specular = ${SPECULAR_COLOR_UNIFORM_NAME}
* (specular_intensity * ${SPECULAR_STRENGTH_UNIFORM_NAME}
* ${DIRECTIONAL_STRENGTH_UNIFORM_NAME} * lighting_enabled);
vec3 rgb = ambient + diffuse + specular;
outColor = vec4(rgb, ${OPACITY_UNIFORM_NAME});
}
`;
/**
* 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(VECTOR3_ELEMENT_COUNT);
/**
* Specular strength multiplier.
*
* @type {number}
* @private
*/
#specularStrength = DEFAULT_SPECULAR_STRENGTH;
/**
* Shininess exponent (specular power).
*
* @type {number}
* @private
*/
#shininess = DEFAULT_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 !== 'object' || Array.isArray(options)) {
throw new TypeError('`PhongMaterial` expects an options object (plain object).');
}
const shaderProgram = new ShaderProgram(webglContext, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE);
super(webglContext, shaderProgram, options, { ownsShaderProgram: true });
this.#specularColor.set(DEFAULT_SPECULAR_COLOR);
this.#specularStrength = DEFAULT_SPECULAR_STRENGTH;
this.#shininess = DEFAULT_SHININESS;
const { specularColor, specularStrength, shininess } = options;
if (specularColor !== undefined) {
this.setSpecularColor(specularColor);
}
if (specularStrength !== undefined) {
this.setSpecularStrength(specularStrength);
}
if (shininess !== undefined) {
this.setShininess(shininess);
}
}
/**
* 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) {
this.shaderProgram.setMatrix4(WORLD_MATRIX_UNIFORM_NAME , worldMatrix);
this.shaderProgram.setVector3(CAMERA_POSITION_UNIFORM_NAME , cameraPosition);
this.shaderProgram.setVector3(SPECULAR_COLOR_UNIFORM_NAME , this.#specularColor);
this.shaderProgram.setFloat(SPECULAR_STRENGTH_UNIFORM_NAME , this.#specularStrength);
this.shaderProgram.setFloat(SHININESS_UNIFORM_NAME , this.#shininess);
}
/**
* Sets the specular RGB-color.
*
* @param {Float32Array | number[]} color - [red, green, blue] in [0..1] range.
*/
setSpecularColor(color) {
DirectionalLightMaterial.assertVector3('`PhongMaterial.setSpecularColor`', color);
this.#specularColor[0] = color[0];
this.#specularColor[1] = color[1];
this.#specularColor[2] = color[2];
}
/**
* Sets specular strength multiplier.
*
* @param {number} value - Specular multiplier.
*/
setSpecularStrength(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError('`PhongMaterial.setSpecularStrength` expects a finite number.');
}
this.#specularStrength = value;
}
/**
* Sets shininess exponent for the specular highlight.
*
* @param {number} value - Shininess exponent.
*/
setShininess(value) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError('`PhongMaterial.setShininess` expects a finite number.');
}
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;
}
}