import { Material } from './material.js';
import { ShaderProgram } from '../shader/shader-program.js';
import { Texture2D } from '../texture/texture2d.js';
/**
* Attribute location used by vec3 position.
*
* @type {number}
*/
const POSITION_ATTRIBUTE_LOCATION = 0;
/**
* Attribute location used by vec2 UV coordinates.
*
* @type {number}
*/
const UV_ATTRIBUTE_LOCATION = 2;
/**
* Default texture unit index used for binding the diffuse texture.
*
* @type {number}
*/
const DEFAULT_TEXTURE_UNIT_INDEX = 0;
/**
* Minimal allowed texture unit index.
* Zero corresponds to `TEXTURE0`.
*
* @type {number}
*/
const MIN_TEXTURE_UNIT_INDEX = 0;
/**
* Name of the matrix uniform in the shader.
*
* @type {string}
*/
const MATRIX_UNIFORM_NAME = 'u_matrix';
/**
* Name of the `sampler2D` uniform in the shader.
*
* @type {string}
*/
const DIFFUSE_TEXTURE_UNIFORM_NAME = 'u_diffuseTexture';
/**
* Opacity uniform name.
*
* @type {string}
*/
const OPACITY_UNIFORM_NAME = 'u_opacity';
/**
* GLSL vertex shader source code.
*
* @type {string}
*/
const VERTEX_SHADER_SOURCE = `#version 300 es
precision mediump float;
layout(location = ${POSITION_ATTRIBUTE_LOCATION}) in vec3 a_position;
layout(location = ${UV_ATTRIBUTE_LOCATION}) in vec2 a_uv;
uniform mat4 ${MATRIX_UNIFORM_NAME};
out vec2 v_uv;
void main() {
gl_Position = ${MATRIX_UNIFORM_NAME} * vec4(a_position, 1.0);
v_uv = a_uv;
}
`;
/**
* GLSL fragment shader source code.
*
* @type {string}
*/
const FRAGMENT_SHADER_SOURCE = `#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D ${DIFFUSE_TEXTURE_UNIFORM_NAME};
uniform float ${OPACITY_UNIFORM_NAME};
out vec4 outColor;
void main() {
vec4 sampledColor = texture(${DIFFUSE_TEXTURE_UNIFORM_NAME}, v_uv);
outColor = vec4(sampledColor.rgb, sampledColor.a * ${OPACITY_UNIFORM_NAME});
}
`;
/**
* Options used by `TexturedMaterial` constructor.
*
* @typedef {Object} TexturedMaterialOptions
* @property {Texture2D} [texture] - Optional `Texture2D` instance.
* @property {number} [textureUnitIndex = 0] - Texture unit index used for binding.
* @property {boolean} [ownsTexture = false] - If true, dispose will dispose the texture.
* @property {boolean} [ownsShaderProgram = true] - If true, dispose will dispose the shader program.
*/
/**
* `TexturedMaterial` renders geometry using a single diffuse texture (sampler2D) and UV coordinates.
*/
export class TexturedMaterial extends Material {
/**
* Diffuse texture bound to the shader sampler.
*
* @type {Texture2D}
* @private
*/
#diffuseTexture;
/**
* WebGL texture unit index used to bind the diffuse texture (TEXTURE0 + unitIndex).
*
* @type {number}
* @private
*/
#textureUnitIndex;
/**
* Indicates whether this material owns the diffuse texture resource.
* When true, `TexturedMaterial.dispose()` will dispose the texture.
*
* @type {boolean}
* @private
*/
#ownsDiffuseTexture = false;
/**
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context used to create shaders and upload uniforms.
* @param {TexturedMaterialOptions} [options] - Optional configuration for the material.
*/
constructor(webglContext, options = {}) {
if (options === null || typeof options !== 'object') {
throw new TypeError('TexturedMaterial expects options as an object.');
}
const {
texture,
textureUnitIndex = DEFAULT_TEXTURE_UNIT_INDEX,
ownsTexture = false,
ownsShaderProgram = true
} = options;
if (texture !== undefined && !(texture instanceof Texture2D)) {
throw new TypeError('`TexturedMaterial` expects `options.texture` as `Texture2D`.');
}
if (!Number.isInteger(textureUnitIndex) || textureUnitIndex < MIN_TEXTURE_UNIT_INDEX) {
throw new TypeError('`TexturedMaterial` expects `options.textureUnitIndex` as a non-negative integer.');
}
if (typeof ownsTexture !== 'boolean') {
throw new TypeError('`TexturedMaterial` expects `options.ownsTexture` as boolean.');
}
if (typeof ownsShaderProgram !== 'boolean') {
throw new TypeError('`TexturedMaterial` expects `options.ownsShaderProgram` as boolean.');
}
const shaderProgram = new ShaderProgram(webglContext, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE);
super(webglContext, shaderProgram, { ownsShaderProgram });
this.#diffuseTexture = texture || new Texture2D(webglContext);
this.#textureUnitIndex = textureUnitIndex;
this.#ownsDiffuseTexture = ownsTexture || !texture;
}
/**
* Uploads uniforms and binds the texture for a draw call.
*
* @param {Float32Array} matrix4 - Model-View-Projection matrix (4x4).
*/
apply(matrix4) {
this.use();
this.shaderProgram.setMatrix4(MATRIX_UNIFORM_NAME, matrix4);
this.shaderProgram.setTexture2D(DIFFUSE_TEXTURE_UNIFORM_NAME, this.#diffuseTexture, this.#textureUnitIndex);
this.shaderProgram.setFloat(OPACITY_UNIFORM_NAME, this.opacity);
}
/**
* Returns the current diffuse texture.
*
* @returns {Texture2D}
*/
get diffuseTexture() {
return this.#diffuseTexture;
}
/**
* Replaces the diffuse texture.
*
* @param {Texture2D} texture - New texture instance.
* @param {Object} [options] - Optional ownership configuration.
* @param {boolean} [options.ownsTexture = false] - If true, dispose will dispose the texture.
*/
setDiffuseTexture(texture, options = {}) {
if (!(texture instanceof Texture2D)) {
throw new TypeError('`TexturedMaterial.setDiffuseTexture` expects texture as `Texture2D`.');
}
if (options === null || typeof options !== 'object') {
throw new TypeError('`TexturedMaterial.setDiffuseTexture` expects options as an object.');
}
const { ownsTexture = false } = options;
if (typeof ownsTexture !== 'boolean') {
throw new TypeError('`TexturedMaterial.setDiffuseTexture` expects options.ownsTexture as boolean.');
}
if (this.#ownsDiffuseTexture) {
this.#diffuseTexture.dispose();
}
this.#diffuseTexture = texture;
this.#ownsDiffuseTexture = ownsTexture;
}
/**
* Disposes GPU resources owned by the material.
*/
dispose() {
if (this.isDisposed) {
return;
}
if (this.#ownsDiffuseTexture) {
this.#diffuseTexture.dispose();
this.#ownsDiffuseTexture = false;
}
super.dispose();
}
}