Source: LayerBRDF.js

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 }));
			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 }