import { Shader } from './Shader.js' /** * A shader class implementing various BRDF (Bidirectional Reflectance Distribution Function) rendering modes. * Extends the base Shader class to provide specialized material rendering capabilities. * * Shader Features: * - Implements the Ward BRDF model for physically-based rendering * - Supports both directional and spot lights * - Handles normal mapping for more detailed surface rendering * - Supports different color spaces (linear and sRGB) for input textures * - Multiple visualization modes for material analysis (diffuse, specular, normals, monochrome, etc.) * - Configurable surface roughness range for varying material appearance * - Ambient light contribution to simulate indirect light * * Required Textures: * - uTexKd: Diffuse color texture (optional) * - uTexKs: Specular color texture (optional) * - uTexNormals: Normal map for surface detail * - uTexGloss: Glossiness map (optional) * * @example * // Create a basic BRDF shader with default settings * const shader = new ShaderBRDF({}); * * @example * // Create a BRDF shader with custom settings * const shader = new ShaderBRDF({ * mode: 'color', * colorspaces: { kd: 'sRGB', ks: 'linear' }, * brightness: 1.2, * gamma: 2.2, * alphaLimits: [0.05, 0.4], * kAmbient: 0.03 * }); * * @extends Shader */ class ShaderBRDF extends Shader { /** * Creates a new ShaderBRDF instance. * @param {Object} [options={}] - Configuration options for the shader. * @param {string} [options.mode='color'] - Rendering mode to use: * - 'color': Full BRDF rendering using Ward model with ambient light * - 'diffuse': Shows only diffuse component (kd) * - 'specular': Shows only specular component (ks * spec * NdotL) * - 'normals': Visualizes surface normals * - 'monochrome': Renders using a single material color with diffuse lighting * @param {Object} [options.colorspaces] - Color space configurations. * @param {string} [options.colorspaces.kd='sRGB'] - Color space for diffuse texture ('linear' or 'sRGB'). * @param {string} [options.colorspaces.ks='linear'] - Color space for specular texture ('linear' or 'sRGB'). * @param {number} [options.brightness=1.0] - Overall brightness multiplier. * @param {number} [options.gamma=2.2] - Gamma correction value. * @param {number[]} [options.alphaLimits=[0.01, 0.5]] - Range for surface roughness [min, max]. * @param {number[]} [options.monochromeMaterial=[0.80, 0.79, 0.75]] - RGB color for monochrome mode. * @param {number} [options.kAmbient=0.02] - Ambient light coefficient. * */ constructor(options) { super(options); this.modes = ['color', 'diffuse', 'specular', 'normals', 'monochrome']; this.mode = 'color'; Object.assign(this, options); const kdCS = this.colorspaces['kd'] == 'linear' ? 0 : 1; const ksCS = this.colorspaces['ks'] == 'linear' ? 0 : 1; const brightness = options.brightness ? options.brightness : 1.0; const gamma = options.gamma ? options.gamma : 2.2; const alphaLimits = options.alphaLimits ? options.alphaLimits : [0.01, 0.5]; const monochromeMaterial = options.monochromeMaterial ? options.monochromeMaterial : [0.80, 0.79, 0.75]; const kAmbient = options.kAmbient ? options.kAmbient : 0.02; this.uniforms = { uLightInfo: { type: 'vec4', needsUpdate: true, size: 4, value: [0.1, 0.1, 0.9, 0] }, uAlphaLimits: { type: 'vec2', needsUpdate: true, size: 2, value: alphaLimits }, uBrightnessGamma: { type: 'vec2', needsUpdate: true, size: 2, value: [brightness, gamma] }, uInputColorSpaceKd: { type: 'int', needsUpdate: true, size: 1, value: kdCS }, uInputColorSpaceKs: { type: 'int', needsUpdate: true, size: 1, value: ksCS }, uMonochromeMaterial: { type: 'vec3', needsUpdate: true, size: 3, value: monochromeMaterial }, uKAmbient: { type: 'float', needsUpdate: true, size: 1, value: kAmbient }, } this.innerCode = ''; this.setMode(this.mode); } /** * Sets the light properties for the shader. * * @param {number[]} light - 4D vector containing light information * @param {number} light[0] - X coordinate of light position/direction * @param {number} light[1] - Y coordinate of light position/direction * @param {number} light[2] - Z coordinate of light position/direction * @param {number} light[3] - Light type flag (0 for directional, 1 for spot) */ setLight(light) { // Light with 4 components (Spot: 4th==1, Dir: 4th==0) this.setUniform('uLightInfo', light); } /** * Sets the rendering mode for the shader. * * @param {string} mode - The rendering mode to use * @throws {Error} If an invalid mode is specified */ setMode(mode) { this.mode = mode; switch (mode) { case 'color': this.innerCode = `vec3 linearColor = (kd + ks * spec) * NdotL; linearColor += kd * uKAmbient; // HACK! adding just a bit of ambient` break; case 'diffuse': this.innerCode = `vec3 linearColor = kd;` break; case 'specular': this.innerCode = `vec3 linearColor = clamp((ks * spec) * NdotL, 0.0, 1.0);` break; case 'normals': this.innerCode = `vec3 linearColor = (N+vec3(1.))/2.; applyGamma = false;` break; case 'monochrome': this.innerCode = 'vec3 linearColor = kd * NdotL + kd * uKAmbient;' break; default: console.log("ShaderBRDF: Unknown mode: " + mode); throw Error("ShaderBRDF: Unknown mode: " + mode); break; } this.needsUpdate = true; } /** * Generates the fragment shader source code based on current configuration. * * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - The WebGL context * @returns {string} The complete fragment shader source code * @private */ fragShaderSrc(gl) { let gl2 = !(gl instanceof WebGLRenderingContext); let hasKd = this.samplers.findIndex(s => s.name == 'uTexKd') != -1 && this.mode != 'monochrome'; let hasGloss = this.samplers.findIndex(s => s.name == 'uTexGloss') != -1 && this.mode != 'monochrome'; let hasKs = this.samplers.findIndex(s => s.name == 'uTexKs') != -1; let str = ` #define NULL_NORMAL vec3(0,0,0) #define SQR(x) ((x)*(x)) #define PI (3.14159265359) #define ISO_WARD_EXPONENT (4.0) ${gl2 ? 'in' : 'varying'} vec2 v_texcoord; uniform sampler2D uTexKd; uniform sampler2D uTexKs; uniform sampler2D uTexNormals; uniform sampler2D uTexGloss; uniform vec4 uLightInfo; // [x,y,z,w] (if .w==0 => Directional, if w==1 => Spot) uniform vec2 uAlphaLimits; uniform vec2 uBrightnessGamma; uniform vec3 uMonochromeMaterial; uniform float uKAmbient; uniform int uInputColorSpaceKd; // 0: Linear; 1: sRGB uniform int uInputColorSpaceKs; // 0: Linear; 1: sRGB vec3 getNormal(const in vec2 texCoord) { vec3 n = texture(uTexNormals, texCoord).xyz; n = 2. * n - vec3(1.); float norm = length(n); if(norm < 0.5) return NULL_NORMAL; else return n/norm; } vec3 linear2sRGB(vec3 linearRGB) { bvec3 cutoff = lessThan(linearRGB, vec3(0.0031308)); vec3 higher = vec3(1.055)*pow(linearRGB, vec3(1.0/2.4)) - vec3(0.055); vec3 lower = linearRGB * vec3(12.92); return mix(higher, lower, cutoff); } vec3 sRGB2Linear(vec3 sRGB) { bvec3 cutoff = lessThan(sRGB, vec3(0.04045)); vec3 higher = pow((sRGB + vec3(0.055))/vec3(1.055), vec3(2.4)); vec3 lower = sRGB/vec3(12.92); return mix(higher, lower, cutoff); } float ward(in vec3 V, in vec3 L, in vec3 N, in vec3 X, in vec3 Y, in float alpha) { vec3 H = normalize(V + L); float H_dot_N = dot(H, N); float sqr_alpha_H_dot_N = SQR(alpha * H_dot_N); if(sqr_alpha_H_dot_N < 0.00001) return 0.0; float L_dot_N_mult_N_dot_V = dot(L,N) * dot(N,V); if(L_dot_N_mult_N_dot_V <= 0.0) return 0.0; float spec = 1.0 / (4.0 * PI * alpha * alpha * sqrt(L_dot_N_mult_N_dot_V)); //float exponent = -(SQR(dot(H,X)) + SQR(dot(H,Y))) / sqr_alpha_H_dot_N; // Anisotropic float exponent = -SQR(tan(acos(H_dot_N))) / SQR(alpha); // Isotropic spec *= exp( exponent ); return spec; } vec4 data() { vec3 N = getNormal(v_texcoord); if(N == NULL_NORMAL) { return vec4(0.0); } vec3 L = (uLightInfo.w == 0.0) ? normalize(uLightInfo.xyz) : normalize(uLightInfo.xyz - gl_FragCoord.xyz); vec3 V = vec3(0.0,0.0,1.0); vec3 H = normalize(L + V); float NdotL = max(dot(N,L),0.0); vec3 kd = ${hasKd ? 'texture(uTexKd, v_texcoord).xyz' : 'uMonochromeMaterial'}; vec3 ks = ${hasKs ? 'texture(uTexKs, v_texcoord).xyz' : 'vec3(0.0, 0.0, 0.0)'}; if(uInputColorSpaceKd == 1) { kd = sRGB2Linear(kd); } if(uInputColorSpaceKs == 1) { ks = sRGB2Linear(ks); } kd /= PI; float gloss = ${hasGloss ? 'texture(uTexGloss, v_texcoord).x' : '0.0'}; float minGloss = 1.0 - pow(uAlphaLimits[1], 1.0 / ISO_WARD_EXPONENT); float maxGloss = 1.0 - pow(uAlphaLimits[0], 1.0 / ISO_WARD_EXPONENT); float alpha = pow(1.0 - gloss * (maxGloss - minGloss) - minGloss, ISO_WARD_EXPONENT); vec3 e = vec3(0.0,0.0,1.0); vec3 T = normalize(cross(N,e)); vec3 B = normalize(cross(N,T)); float spec = ward(V, L, N, T, B, alpha); bool applyGamma = true; ${this.innerCode} vec3 finalColor = applyGamma ? pow(linearColor * uBrightnessGamma[0], vec3(1.0/uBrightnessGamma[1])) : linearColor; return vec4(finalColor, 1.0); } `; return str; } } export { ShaderBRDF }