import { Shader } from './Shader.js'
/**
* ShaderHDR provides enhanced HDR tone mapping capabilities.
* It extends the base Shader class to include tone mapping operations
* and additional uniforms for HDR rendering.
*
* Features:
* - Multiple tone mapping operators: Reinhard, ACES, and Exposure
* - Configurable parameters for each operator
* - Linear space processing
*
* @extends Shader
*/
class ShaderHDR extends Shader {
/**
* Creates a new enhanced ShaderHDR instance.
*
* @param {Object} options - Shader configuration options
* @param {boolean} [options.isLinear=true] - Whether the shader operates in linear space
* @param {string[]} [options.modes=['reinhard', 'aces', 'exposure']] - Available tone mapping modes
* @param {string} [options.mode='reinhard'] - Default tone mapping mode
* @param {Object[]} [options.samplers] - Texture samplers for the shader
*/
constructor(options) {
// Set default options
options = Object.assign({
isLinear: true, // Important: we work in linear space!
format: 'rgba16f',
}, options);
super(options);
this.modes = ['reinhard', 'aces', 'exposure', 'balanced'];
this.mode = options.mode || 'reinhard';
this.uniforms = {
'whitePoint': { type: 'float', needsUpdate: true, value: 1.0 },
'shadowLift': { type: 'float', needsUpdate: true, value: 0.0 },
'acesContrast': { type: 'float', needsUpdate: true, value: 1.6 },
'exposure': { type: 'float', needsUpdate: true, value: 1.0 },
'highlightCompression': { type: 'float', needsUpdate: true, value: 1.0 },
}
this.samplers.push({ id: 0, name: 'source', type: this.format });
/**
* Tone mapping operations available in the shader.
* @type {Object.<string, string>}
*/
this.toneMapOperations = {
// Enhanced Reinhard operator with highlight preservation
'reinhard': `
// Enhanced Reinhard with both whitePoint and shadowLift parameters
// Apply shadowLift pre-tone mapping to brighten shadows
color.rgb = mix(color.rgb, pow(color.rgb, vec3(0.4)), shadowLift);
// Reinhard tone mapping with more sensitive whitePoint
float wp = max(0.1, whitePoint);
color.rgb = (color.rgb * (1.0 + color.rgb/(wp))) / (1.0 + color.rgb);
// Apply shadowLift post-tone mapping for more pronounced effect
color.rgb = mix(color.rgb, pow(color.rgb, vec3(0.6)), shadowLift * 0.5);
`,
// ACES filmic tone mapping operator
'aces': `
// ACES filmic tone mapping approximation
// Based on formula from Krzysztof Narkowicz
// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
// Allows for contrast adjustment with acesContrast parameter
// Apply contrast parameter
color.rgb *= acesContrast;
// ACES tone mapping formula
const vec3 a = vec3(2.51);
const vec3 b = vec3(0.03);
const vec3 c = vec3(2.43);
const vec3 d = vec3(0.59);
const vec3 e = vec3(0.14);
color.rgb = (color.rgb * (a * color.rgb + b)) / (color.rgb * (c * color.rgb + d) + e);
// Clamp to prevent artifacts
color.rgb = clamp(color.rgb, 0.0, 1.0);
`,
// Simple exposure-based tone mapping
'exposure': `
// Apply exposure adjustment
// exposure = 1.0 means no change, > 1.0 brightens, < 1.0 darkens
color.rgb = vec3(1.0) - exp(-color.rgb * exposure);
`,
// New balanced operator
'balanced': `
// Lift shadows slightly to enhance details in dark areas
color.rgb = mix(color.rgb, pow(color.rgb, vec3(0.5)), 0.05);
// Adaptive scaling for highlights based on highlightCompression
float hc = max(0.1, highlightCompression); // Avoid division by zero
color.rgb = color.rgb / (color.rgb + vec3(hc));
// Apply logarithmic compression for highlights
color.rgb = log(1.0 + color.rgb) / log(1.0 + hc);
// Clamp to prevent overexposure
color.rgb = clamp(color.rgb, 0.0, 1.0);
`
};
// Set default samplers if not provided
if (!this.samplers || this.samplers.length === 0) {
this.samplers = [
{ id: 0, name: 'source', type: 'rgba16f' }
];
}
}
/**
* Generates the fragment shader source code with enhanced HDR tone mapping.
*
* @returns {string} GLSL source code for the fragment shader
* @override
*/
fragShaderSrc() {
// Get the selected tone mapping operation
const toneMapOperation = this.toneMapOperations[this.mode] || this.toneMapOperations['reinhard'];
console.log(this.mode);
console.log(toneMapOperation);
return `
in vec2 v_texcoord;
// Uniforms for tone mapping and enhancements
uniform float whitePoint;
uniform float shadowLift;
uniform float acesContrast;
uniform float exposure;
uniform float highlightCompression;
vec4 data() {
// Sample the HDR texture (already in linear space)
vec4 color = texture(source, v_texcoord);
// Apply selected tone mapping operation to compress HDR values
${toneMapOperation}
// Return the tone-mapped color in linear space
// The final gamma correction will be applied by Canvas.js
return vec4(color.rgb, color.a);
}
`;
}
/**
* Sets the white point uniform for the shader.
*
* @param {number} whitePoint - The new value for the white point
*/
setWhitePoint(whitePoint) {
this.setUniform('whitePoint', whitePoint);
}
/**
* Sets the shadow lift parameter for the shader.
*
* @param {number} shadowLift - The new value for shadow lift
*/
setShadowLift(shadowLift) {
this.setUniform('shadowLift', shadowLift);
}
/**
* Sets the ACES contrast parameter for the shader.
*
* @param {number} acesContrast - The new value for ACES contrast
*/
setAcesContrast(acesContrast) {
this.setUniform('acesContrast', acesContrast);
}
/**
* Sets the exposure parameter for the shader.
*
* @param {number} exposure - The new value for the exposure
*/
setExposure(exposure) {
this.setUniform('exposure', exposure);
}
/**
* Sets the highlight compression parameter for the shader.
*
* @param {number} highlightCompression - The new value for highlight compression
*/
setHighlightCompression(highlightCompression) {
this.setUniform('highlightCompression', highlightCompression);
}
}
export { ShaderHDR };