import { Layer } from './Layer.js' /** * @typedef {Object} LayerCombinerOptions * @property {Layer[]} layers - Array of layers to be combined (required) * @property {Object.<string, Shader>} [shaders] - Map of available shaders * @property {string} [type='combiner'] - Must be 'combiner' when using Layer factory * @property {boolean} [visible=true] - Whether combined output is visible * @extends LayerOptions */ /** * LayerCombiner provides functionality to combine multiple layers using framebuffer operations * and custom shaders. It enables complex visual effects by compositing layers in real-time * using GPU-accelerated rendering. * * Features: * - Real-time layer composition * - Custom shader-based effects * - Framebuffer management * - Dynamic texture allocation * - Resolution-independent rendering * - GPU-accelerated compositing * * Use Cases: * - Layer blending and mixing * - Image comparison tools * - Lens effects (see {@link LayerLens}) * - Custom visual effects * - Multi-layer compositing * * Technical Details: * - Creates framebuffers for each input layer * - Manages WebGL textures and resources * - Supports dynamic viewport resizing * - Handles shader-based composition * - Maintains proper resource cleanup * * @extends Layer * * @example * ```javascript * // Create two base layers * const baseLayer = new OpenLIME.Layer({ * type: 'image', * url: 'base.jpg' * }); * * const overlayLayer = new OpenLIME.Layer({ * type: 'image', * url: 'overlay.jpg' * }); * * // Create combiner with custom shader * const combiner = new OpenLIME.Layer({ * type: 'combiner', * layers: [baseLayer, overlayLayer], * visible: true * }); * * // Set up blend shader * const shader = new OpenLIME.ShaderCombiner(); * shader.mode = 'blend'; * combiner.shaders = { 'standard': shader }; * combiner.setShader('standard'); * * // Add to viewer * viewer.addLayer('combined', combiner); * ``` */ class LayerCombiner extends Layer { /** * Creates a new LayerCombiner instance * @param {LayerCombinerOptions} options - Configuration options * @throws {Error} If rasters option is not empty (rasters should be defined in source layers) */ constructor(options) { super(options); if (Object.keys(this.rasters).length != 0) throw "Rasters options should be empty!"; /* let shader = new ShaderCombiner({ 'label': 'Combiner', 'samplers': [{ id:0, name:'source1', type:'vec3' }, { id:1, name:'source2', type:'vec3' }], }); this.shaders = {'standard': shader }; this.setShader('standard'); */ //todo if layers check for importjson this.textures = []; this.framebuffers = []; this.status = 'ready'; } /** * Renders the combined layers using framebuffer operations * Handles framebuffer creation, layer rendering, and final composition * @param {Transform} transform - Current view transform * @param {Object} viewport - Current viewport parameters * @param {number} viewport.x - Viewport X position * @param {number} viewport.y - Viewport Y position * @param {number} viewport.dx - Viewport width * @param {number} viewport.dy - Viewport height * @param {number} viewport.w - Total width * @param {number} viewport.h - Total height * @throws {Error} If shader is not specified * @private */ draw(transform, viewport) { for (let layer of this.layers) if (layer.status != 'ready') return; if (!this.shader) throw "Shader not specified!"; let w = viewport.dx; let h = viewport.dy; if (!this.framebuffers.length || this.layout.width != w || this.layout.height != h) { this.deleteFramebuffers(); this.layout.width = w; this.layout.height = h; this.createFramebuffers(); } let gl = this.gl; var b = [0, 0, 0, 0]; gl.clearColor(b[0], b[1], b[2], b[3]); //TODO optimize: render to texture ONLY if some parameters change! //provider di textures... max memory and reference counting. for (let i = 0; i < this.layers.length; i++) { gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]); gl.clear(gl.COLOR_BUFFER_BIT); this.layers[i].draw(transform, { x: 0, y: 0, dx: w, dy: h, w: w, h: h }); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } this.prepareWebGL(); for (let i = 0; i < this.layers.length; i++) { gl.uniform1i(this.shader.samplers[i].location, i); gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, this.textures[i]); } this.updateTileBuffers( new Float32Array([-1, -1, 0, -1, 1, 0, 1, 1, 0, 1, -1, 0]), new Float32Array([0, 0, 0, 1, 1, 1, 1, 0])); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); } /** * Creates framebuffers and textures for layer composition * Initializes WebGL resources for each input layer * @private */ createFramebuffers() { let gl = this.gl; for (let i = 0; i < this.layers.length; i++) { //TODO for thing like lens, we might want to create SMALLER textures for some layers. const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); const level = 0; const internalFormat = gl.RGBA; const border = 0; const format = gl.RGBA; const type = gl.UNSIGNED_BYTE; gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, this.layout.width, this.layout.height, border, format, type, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); const framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.bindFramebuffer(gl.FRAMEBUFFER, null); this.textures[i] = texture; this.framebuffers[i] = framebuffer; } } //TODO release textures and framebuffers /** * Cleans up framebuffer and texture resources * Should be called when resizing or destroying the layer * @private */ deleteFramebuffers() { } /** * Computes combined bounding box of all input layers * @returns {BoundingBox} Combined bounding box * @override * @private */ boundingBox() { // Combiner ask the combination of all its children boxes // keeping the hidden, because they could be hidden, but revealed by the combiner const discardHidden = false; let result = Layer.computeLayersBBox(this.layers, discardHidden); if (this.transform != null && this.transform != undefined) { result = this.transform.transformBox(result); } return result; } /** * Computes minimum scale across all input layers * @returns {number} Combined scale factor * @override * @private */ scale() { //Combiner ask the scale of all its children //keeping the hidden, because they could be hidden, but revealed by the combiner const discardHidden = false; let scale = Layer.computeLayersMinScale(this.layers, discardHidden); scale *= this.transform.z; return scale; } } /** * Register this layer type with the Layer factory * @type {Function} * @private */ Layer.prototype.types['combiner'] = (options) => { return new LayerCombiner(options); } export { LayerCombiner }