import { Layer } from './Layer.js'
import { Raster } from './Raster.js'
import { ShaderBRDF } from './ShaderBRDF.js'
/**
* @typedef {Object} LayerBRDFOptions
* @property {Object} channels - Required channels for BRDF rendering
* @property {string} channels.kd - URL to diffuse color map (required)
* @property {string} channels.ks - URL to specular color map (optional)
* @property {string} channels.normals - URL to normal map (required)
* @property {string} channels.gloss - URL to glossiness/roughness map (optional)
* @property {Object} [colorspaces] - Color space definitions for material properties
* @property {('linear'|'srgb')} [colorspaces.kd='linear'] - Color space for diffuse map
* @property {('linear'|'srgb')} [colorspaces.ks='linear'] - Color space for specular map
* @property {number} [brightness=1.0] - Overall brightness adjustment
* @property {number} [gamma=2.2] - Gamma correction value
* @property {number[]} [alphaLimits=[0.01, 0.5]] - Range for glossiness/roughness
* @property {number[]} [monochromeMaterial=[0.80, 0.79, 0.75]] - RGB color for monochrome rendering
* @property {number} [kAmbient=0.1] - Ambient light coefficient
* @extends LayerOptions
*/
/**
* LayerBRDF implements real-time BRDF (Bidirectional Reflectance Distribution Function) rendering.
*
* The BRDF model describes how light reflects off a surface, taking into account:
* - Diffuse reflection (rough, matte surfaces)
* - Specular reflection (mirror-like reflections)
* - Surface normals (microscopic surface orientation)
* - Glossiness/roughness (surface micro-structure)
*
* Features:
* - Real-time light direction control
* - Multiple material channels support
* - Customizable material properties
* - Interactive lighting model
* - Gamma correction
* - Ambient light component
*
* Technical implementation:
* - Uses normal mapping for surface detail
* - Supports both linear and sRGB color spaces
* - Implements spherical light projection
* - Handles multi-channel textures
* - GPU-accelerated rendering
*
* @extends Layer
*
* @example
* ```javascript
* // Create BRDF layer with all channels
* const brdfLayer = new OpenLIME.LayerBRDF({
* channels: {
* kd: 'diffuse.jpg',
* ks: 'specular.jpg',
* normals: 'normals.jpg',
* gloss: 'gloss.jpg'
* },
* colorspaces: {
* kd: 'srgb',
* ks: 'linear'
* },
* brightness: 1.2,
* gamma: 2.2
* });
*
* // Update light direction
* brdfLayer.setLight([0.5, 0.5], 500, 'ease-out');
* ```
*/
class LayerBRDF extends Layer {
/**
* Creates a new LayerBRDF instance
* @param {LayerBRDFOptions} options - Configuration options
* @throws {Error} If required channels (kd, normals) are not provided
* @throws {Error} If rasters option is not empty
*/
constructor(options) {
options = Object.assign({
brightness: 1.0,
gamma: 2.2,
alphaLimits: [0.01, 0.5],
monochromeMaterial: [0.80, 0.79, 0.75],
kAmbient: 0.1
}, options);
super(options);
if (Object.keys(this.rasters).length != 0)
throw "Rasters options should be empty!";
if (!this.channels)
throw "channels option is required";
if (!this.channels.kd || !this.channels.normals)
throw "kd and normals channels are required";
if (!this.colorspaces) {
console.log("LayerBRDF: missing colorspaces: force both to linear");
this.colorspaces['kd'] = 'linear';
this.colorspaces['ks'] = 'linear';
}
let id = 0;
let urls = [];
let samplers = [];
let brdfSamplersMap = {
kd: { format: 'vec3', name: 'uTexKd' },
ks: { format: 'vec3', name: 'uTexKs' },
normals: { format: 'vec3', name: 'uTexNormals' },
gloss: { format: 'float', name: 'uTexGloss' }
};
for (let c in this.channels) {
this.rasters.push(new Raster({ format: brdfSamplersMap[c].format, isLinear: true }));
samplers.push({ 'id': id, 'name': brdfSamplersMap[c].name });
urls[id] = this.channels[c];
id++;
}
this.layout.setUrls(urls);
this.addControl('light', [0, 0]); // This is a projection to the z=0 plane.
let shader = new ShaderBRDF({
'label': 'Rgb',
'samplers': samplers,
'colorspaces': this.colorspaces,
'brightness': this.brightness,
'gamma': this.gamma,
'alphaLimits': this.alphaLimits,
'monochromeMaterial': this.monochromeMaterial,
'kAmbient': this.kAmbient
});
this.shaders['brdf'] = shader;
this.setShader('brdf');
}
/**
* Projects a 2D point onto a sphere surface
* Used for converting 2D mouse/touch input to 3D light direction
* @param {number[]} p - 2D point [x, y] in range [-1, 1]
* @returns {number[]} 3D normalized vector [x, y, z] on sphere surface
* @static
*/
static projectToSphere(p) {
let px = p[0];
let py = p[1];
let r2 = px * px + py * py;
if (r2 > 1.0) {
let r = Math.sqrt(r2);
px /= r;
py /= r;
r2 = 1.0;
}
let z = Math.sqrt(1 - r2);
return [px, py, z];
}
/**
* Projects a 2D point onto a flattened sphere using SGI trackball algorithm.
* This provides more intuitive light control by avoiding acceleration near edges.
* Based on SIGGRAPH 1988 paper on SGI trackball implementation.
*
* @param {number[]} p - 2D point [x, y] in range [-1, 1]
* @returns {number[]} 3D normalized vector [x, y, z] on flattened sphere
* @static
*/
static projectToFlattenedSphere(p) {
const R = 0.8; const R2 = R * R;
const RR = R * Math.SQRT1_2; const RR2 = RR * RR;
let px = Math.min(Math.max(p[0], -1.0), 1.0);
let py = Math.min(Math.max(p[1], -1.0), 1.0);
let z = 0.0;
let d2 = px * px + py * py;
if (d2 < RR2) {
// Inside sphere
z = Math.sqrt(R2 - d2);
} else {
// On hyperbola
z = RR2 / Math.sqrt(d2);
}
let r = Math.sqrt(d2 + z * z);
return [px / r, py / r, z / r];
}
/**
* Sets the light direction with optional animation
* @param {number[]} light - 2D vector [x, y] representing light direction
* @param {number} [dt] - Animation duration in milliseconds
* @param {string} [easing='linear'] - Animation easing function
*/
setLight(light, dt, easing = 'linear') {
this.setControl('light', light, dt, easing);
}
/**
* Updates light control interpolation and shader uniforms
* @returns {boolean} Whether all interpolations are complete
* @override
* @private
*/
interpolateControls() { // FIXME Wrong normalization
let done = super.interpolateControls();
// let light = LayerBRDF.projectToSphere(this.controls['light'].current.value);
let light = LayerBRDF.projectToFlattenedSphere(this.controls['light'].current.value);
this.shader.setLight([light[0], light[1], light[2], 0]);
return done;
}
}
/**
* Register this layer type with the Layer factory
* @type {Function}
* @private
*/
Layer.prototype.types['brdf'] = (options) => { return new LayerBRDF(options); }
export { LayerBRDF }