diff --git a/src/webgl/dataLoader.js.deprecated b/src/webgl/dataLoader.js.deprecated deleted file mode 100644 index 604c338d..00000000 --- a/src/webgl/dataLoader.js.deprecated +++ /dev/null @@ -1,545 +0,0 @@ - -(function($) { - -/** - * IDataLoader conforms to a specific texture type and WebGL version. - * It provides API for uniform handling of textures: - * - texture loading - * - GLSL texture handling - */ -$.WebGLModule.IDataLoader = class { - /** - * Creation - * @param {WebGLRenderingContextBase} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * */ - constructor(gl, webglVersion, options) { - //texture cache to keep track of loaded GPU data - this.__cache = new Map(); - this.wrap = options.wrap; - this.minFilter = options.minFilter; - this.magFilter = options.magFilter; - this.maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); - - /** - * Loader strategy based on toString result, extend with your type if necessary. - * If your type cannot use the given version strategy (TEXTURE_2D_ARRAY UNIT), you have - * to re-define the whole API. - * - * When a data is sent to the shader for processing, `toString` method is called to - * get the data identifier. A typeLoaders key must be present to handle loading - * of that texture(s) data. - * @member typeLoaders - * @memberOf OpenSeadragon.WebGLModule.IDataLoader - * - * @return {object} whatever you need to stare in the cache to later free the object - */ - this.typeLoaders = {}; - } - - /** - * @param {string} version - * @return {boolean} true if given webgl version is supported by the loader - */ - supportsWebglVersion(version) { - throw("::supportsWebglVersion must be implemented!"); - } - - /** - * Get stored options under ID - * @param id - * @return {unknown} options stored by setLoaded - */ - getLoaded(id) { - return this.__cache.get(id); - } - - /** - * Store options object - * @param id - * @param options - */ - setLoaded(id, options) { - this.__cache.set(id, options); - } - - /** - * Unload stored options - * @param id - */ - setUnloaded(id) { - this.__cache.delete(id); - } - - /** - * Set texture sampling parameters - * @param {string} name one of 'minFilter', 'magFilter', 'wrap' - * @param {GLuint} value - */ - setTextureParam(name, value) { - if (!['minFilter', 'magFilter', 'wrap'].includes(name)) { - return; - } - this[name] = value; - } - - /** - * - * @param renderer - * @param id - * @param options - */ - unloadTexture(renderer, id, options) { - throw("::unloadTexture must be implemented!"); - } - - /** - * @param {OpenSeadragon.WebGLModule} renderer renderer renderer reference - * @param id - * @param data - * @param width - * @param height - * @param {[number]} shaderDataIndexToGlobalDataIndex mapping of array indices to data indices, e.g. texture 0 for - * this shader corresponds to index shaderDataIndexToGlobalDataIndex[0] in the data array, - * -1 value used for textures not loaded - */ - load(renderer, id, data, width, height, shaderDataIndexToGlobalDataIndex) { - if (!data) { - $.console.warn("Attempt to draw nullable data!"); - return; - } - const textureLoader = this.typeLoaders[toString.apply(data)]; - if (!textureLoader) { - throw "WebGL Renderer cannot load data as texture: " + toString.apply(data); - } - this.setLoaded(id, textureLoader(this, renderer, data, width, height, shaderDataIndexToGlobalDataIndex)); - } - - /** - * - * @param renderer - * @param id - */ - free(renderer, id) { - const loaded = this.getLoaded(id); - if (loaded) { - this.unloadTexture(renderer, id, this.getLoaded(id)); - this.setUnloaded(id); - } - } - - /** - * Called when the program is being loaded (set as active) - * @param {OpenSeadragon.WebGLModule} renderer - * @param {WebGLRenderingContextBase} gl WebGL context - * @param {WebGLProgram} program - * @param {object} specification reference to the specification object used - */ - programLoaded(renderer, gl, program, specification) { - //not needed - } - - /** - * Called when tile is processed - * @param {OpenSeadragon.WebGLModule} renderer renderer renderer reference - * @param {object} specification reference to the current active specification object - * @param {*} id data object present in the texture cache - * @param {WebGLProgram} program current WebGLProgram - * @param {WebGL2RenderingContext} gl - */ - programUsed(renderer, specification, id, program, gl) { - - } - - /** - * Sample texture - * @param {number|string} index texture index, must respect index re-mapping (see declare()) - * @param {string} vec2coords GLSL expression that evaluates to vec2 - * @return {string} GLSL expression (unterminated) that evaluates to vec4 - */ - sample(index, vec2coords) { - return `texture(_vis_data_sampler_array, vec3(${vec2coords}, _vis_data_sampler_array_indices[${index}]))`; - } - - /** - * Declare GLSL texture logic (global scope) in the GLSL shader - * @param {[number]} shaderDataIndexToGlobalDataIndex mapping of array indices to data indices, e.g. texture 0 for - * this shader corresponds to index shaderDataIndexToGlobalDataIndex[0] in the data array, - * -1 value used for textures not loaded - * @return {string} GLSL declaration (terminated with semicolon) of necessary elements for textures - */ - declare(shaderDataIndexToGlobalDataIndex) { - return ` -vec4 osd_texture(float index, vec2 coords) { - //This method must be implemented! -} - -//TODO: is this relevant? -// vec2 osd_texture_size() { -// //This method must be implemented! -// } -`; - } -}; - -/** - * Data loading strategies for different WebGL versions. - * Should you have your own data format, change/re-define these - * to correctly load the textures to GPU, based on the WebGL version used. - * - * The processing accepts arrays of images to feed to the shader built from configuration. - * This implementation supports data as Image or Canvas objects. We will refer to them as - * - * Implemented texture loaders support - * - working with object - image data chunks are vertically concatenated - * - working with [] object - images are in array - * - * @namespace OpenSeadragon.WebGLModule.Loaders - */ -$.WebGLModule.Loaders = { - - /** - * //TODO: ugly - * In case the system is fed by anything but 'Image' (or the like) data object, - * implement here conversion so that debug mode can draw it. - * @param {*} data - * @return {HTMLElement} Dom Element - */ - dataAsHtmlElement: function(data) { - return { - "[object HTMLImageElement]": () => data, - "[object HTMLCanvasElement]": () => data, - //Image objects in Array, we assume image objects only - "[object Array]": function() { - const node = document.createElement("div"); - for (let image of data) { - node.append(image); - } - return node; - } - }[toString.apply(data)](); - }, - - /** - * Data loader for WebGL 2.0. Must load the data to a Texture2DArray. - * The name of the texture is a constant. The order od the textures in - * the z-stacking is defined in shaderDataIndexToGlobalDataIndex. - * - * For details, please, see the implementation. - * @class OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D_ARRAY - */ - TEXTURE_2D_ARRAY: class /**@lends $.WebGLModule.Loaders.TEXTURE_2D_ARRAY */ extends OpenSeadragon.WebGLModule.IDataLoader { - unloadTexture(renderer, id, options) { - renderer.gl.deleteTexture(options); - } - - /** - * Creation - * @param {WebGL2RenderingContext} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * @memberOf OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D_ARRAY - * */ - constructor(gl, webglVersion, options) { - super(gl, webglVersion, options); - - if (webglVersion !== "2.0") { - throw "Incompatible WebGL version for TEXTURE_2D_ARRAY data loader!"; - } - - // this.batchSize = 5; - // let lastBatch = null; - - this.typeLoaders["[object HTMLImageElement]"] = - this.typeLoaders["[object HTMLCanvasElement]"] = function (self, webglModule, data, width, height, - shaderDataIndexToGlobalDataIndex) { - - const NUM_IMAGES = Math.round(data.height / height); - const gl = webglModule.gl; - - // //todo different tile sizes are problems - // if (!lastBatch || lastBatch.length < NUM_IMAGES) { - // lastBatch = { - // texId: gl.createTexture(), - // length: this.batchSize, - // texCount: 0, - // }; - // gl.bindTexture(gl.TEXTURE_2D_ARRAY, options.texId); - // gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, data[0].width, data[0].height, data.length + 1); - // } else { - // gl.bindTexture(gl.TEXTURE_2D_ARRAY, options.texId); - // } - - const textureId = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureId); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap); - - gl.texImage3D( - gl.TEXTURE_2D_ARRAY, - 0, - gl.RGBA, - width, - height, - NUM_IMAGES, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - data - ); - - return textureId; - }; - - //Image objects in Array, we assume image objects only todo ugly, can be array of anything - this.typeLoaders["[object Array]"] = function (self, webglModule, data, options, width, height, shaderDataIndexToGlobalDataIndex) { - const gl = webglModule.gl; - const textureId = gl.createTexture(); - - //gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureId); - gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, data[0].width, data[0].height, data.length + 1); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, 0); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap); - - let index = 0; - for (let image of data) { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, index++, image.width, image.height, - 1, gl.RGBA, gl.UNSIGNED_BYTE, image); - } - return textureId; - }; - } - - supportsWebglVersion(version) { - return version === "2.0"; - } - - programUsed(renderer, specification, id, program, gl) { - gl.activeTexture(gl.TEXTURE0); - const tid = this.getLoaded(id); - gl.bindTexture(gl.TEXTURE_2D_ARRAY, tid); - } - - sample(index, vec2coords) { - return `texture(_vis_data_sampler_array, vec3(${vec2coords}, _vis_data_sampler_array_indices[${index}]))`; - } - - declare(shaderDataIndexToGlobalDataIndex) { - return `uniform sampler2DArray _vis_data_sampler_array; -int _vis_data_sampler_array_indices[${shaderDataIndexToGlobalDataIndex.length}] = int[${shaderDataIndexToGlobalDataIndex.length}]( - ${shaderDataIndexToGlobalDataIndex.join(",")} -); - -vec4 osd_texture(int index, vec2 coords) { - return ${this.sample("index", "coords")}; -} -`; - } - }, - - - /** - * Data loader for WebGL 2.0. Must load the data to a Texture2DArray. - * The name of the texture is a constant. The order od the textures in - * the z-stacking is defined in shaderDataIndexToGlobalDataIndex. - * - * For details, please, see the implementation. - * @class OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D - */ - TEXTURE_2D: class /**@lends $.WebGLModule.Loaders.TEXTURE_2D */ extends OpenSeadragon.WebGLModule.IDataLoader { - - /** - * Creation - * @param {WebGL2RenderingContext} gl - * @param {string} webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - * @memberOf OpenSeadragon.WebGLModule.Loaders.TEXTURE_2D - * */ - constructor(gl, webglVersion, options) { - super(gl, webglVersion, options); - - this._samples = webglVersion === "1.0" ? "texture2D" : "texture"; - - this.typeLoaders["[object HTMLImageElement]"] = - this.typeLoaders["[object HTMLCanvasElement]"] = function (self, webglModule, data, width, height, - shaderDataIndexToGlobalDataIndex) { - - //Avoid canvas slicing if possible - const NUM_IMAGES = Math.round(data.height / height); - if (NUM_IMAGES === 1) { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - data); - return [texture]; - } - - if (!self._canvas) { - self._canvas = document.createElement('canvas'); - self._canvasReader = self._canvas.getContext('2d', {willReadFrequently: true}); - self._canvasConverter = document.createElement('canvas'); - self._canvasConverterReader = self._canvasConverter.getContext('2d', - {willReadFrequently: true}); - } - - let index = 0; - width = Math.round(width); - height = Math.round(height); - - const units = []; - - //we read from here - self._canvas.width = data.width; - self._canvas.height = data.height; - self._canvasReader.drawImage(data, 0, 0); - - //Allowed texture size dimension only 256+ and power of two... - - //it worked for arbitrary size until we begun with image arrays... is it necessary? - const IMAGE_SIZE = data.width < 256 ? 256 : Math.pow(2, Math.ceil(Math.log2(data.width))); - self._canvasConverter.width = IMAGE_SIZE; - self._canvasConverter.height = IMAGE_SIZE; - - //just load all images and let shaders reference them... - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] < 0) { - continue; - } - if (index >= NUM_IMAGES) { - console.warn("The visualisation contains less data than layers. Skipping layers ..."); - return units; - } - - units.push(gl.createTexture()); - gl.bindTexture(gl.TEXTURE_2D, units[index]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - - let pixels; - if (width !== IMAGE_SIZE || height !== IMAGE_SIZE) { - self._canvasConverterReader.drawImage(self._canvas, 0, - shaderDataIndexToGlobalDataIndex[i] * height, - width, height, 0, 0, IMAGE_SIZE, IMAGE_SIZE); - - pixels = self._canvasConverterReader.getImageData(0, 0, IMAGE_SIZE, IMAGE_SIZE); - } else { - //load data - pixels = self._canvasReader.getImageData(0, - shaderDataIndexToGlobalDataIndex[i] * height, width, height); - } - - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - pixels); - index++; - } - return units; - }; - - //Image objects in Array, we assume image objects only todo ugly, can be array of anything - this.typeLoaders["[object Array]"] = function (self, webglModule, data, options, width, height, shaderDataIndexToGlobalDataIndex) { - const gl = webglModule.gl; - - let index = 0; - const NUM_IMAGES = data.length; - const units = []; - - //just load all images and let shaders reference them... - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] < 0) { - continue; - } - if (index >= NUM_IMAGES) { - console.warn("The visualisation contains less data than layers. Skipping layers ..."); - return units; - } - - //create textures - units.push(gl.createTexture()); - gl.bindTexture(gl.TEXTURE_2D, units[index]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.magFilter); - //do not check the image size, we render what wwe - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - data[index++] - ); - } - return units; - }; - } - - unloadTexture(renderer, id, options) { - for (let textureUnit of options) { - renderer.gl.deleteTexture(textureUnit); - } - } - - supportsWebglVersion(version) { - return true; - } - - programUsed(renderer, specification, id, program, gl) { - const units = this.getLoaded(id); - for (let i = 0; i < units.length; i++) { - let textureUnit = units[i]; - let bindConst = `TEXTURE${i}`; - gl.activeTexture(gl[bindConst]); - gl.bindTexture(gl.TEXTURE_2D, textureUnit); - let location = gl.getUniformLocation(program, `vis_data_sampler_${i}`); - gl.uniform1i(location, i); - } - } - - sample(index, vec2coords) { - return `${this._samples}(vis_data_sampler_${index}, ${vec2coords})`; - } - - declare(shaderDataIndexToGlobalDataIndex) { - let samplers = 'uniform vec2 sampler_size;'; - for (let i = 0; i < shaderDataIndexToGlobalDataIndex.length; i++) { - if (shaderDataIndexToGlobalDataIndex[i] === -1) { - continue; - } - samplers += `uniform sampler2D vis_data_sampler_${i};`; - } - return samplers; - } - }, -}; - -})(OpenSeadragon); diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js deleted file mode 100644 index d34d3188..00000000 --- a/src/webgl/drawer.js +++ /dev/null @@ -1,662 +0,0 @@ - -/* - * OpenSeadragon - WebGLDrawer - * - * Copyright (C) 2010-2023 OpenSeadragon contributors - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * - Neither the name of CodePlex Foundation nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -(function( $ ){ - -/** - * @class WebGLDrawer - * @memberof OpenSeadragon - * @classdesc Default implementation of WebGLDrawer for an {@link OpenSeadragon.Viewer}. - * @param {Object} options - Options for this Drawer. - * @param {OpenSeadragon.Viewer} options.viewer - The Viewer that owns this Drawer. - * @param {OpenSeadragon.Viewport} options.viewport - Reference to Viewer viewport. - * @param {boolean} options.twoPassRendering - * @param {Element} options.element - Parent element. - * @param {Number} [options.debugGridColor] - See debugGridColor in {@link OpenSeadragon.Options} for details. - */ - -$.WebGL = class WebGL extends OpenSeadragon.DrawerBase { - constructor(options){ - super(options); - - const gl = this.renderer.gl; - this.maxTextureUnits = 4 || gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); - this.maxDrawBufferUnits = gl.getParameter(gl.MAX_DRAW_BUFFERS); - - this._createSinglePassShader('TEXTURE_2D'); - - const size = this._calculateCanvasSize(); - this.renderer.init(size.x, size.y); - this._size = size; - this.renderer.setDataBlendingEnabled(true); - - this.destroyed = false; - this._textureMap = {}; - this._renderOffScreenBuffer = gl.createFramebuffer(); - this._renderOffScreenTextures = []; - //batch rendering (artifacts) - // this._tileTexturePositions = new Float32Array(this.maxTextureUnits * 8); - // this._transformMatrices = new Float32Array(this.maxTextureUnits * 9); - - - this.viewer.addHandler("resize", this._resizeRenderer.bind(this)); - // Add listeners for events that require modifying the scene or camera - this.viewer.addHandler("tile-ready", this._tileReadyHandler.bind(this)); - this.viewer.addHandler("image-unloaded", (e) => { - const tileData = this._textureMap[e.tile.cacheKey]; - if (tileData.texture) { - this.renderer.gl.deleteTexture(tileData.texture); - delete this._textureMap[e.tile.cacheKey]; - } - }); - this.viewer.world.addHandler("add-item", (e) => { - let shader = e.item.source.shader; - if (shader) { - const targetIndex = this.renderer.getSpecificationsCount(); - if (this.renderer.addRenderingSpecifications(shader)) { - shader._programIndexTarget = targetIndex; - return; - } - } else { - e.item.source.shader = shader = this.defaultRenderingSpecification; - } - //set default program: identity - shader._programIndexTarget = 0; - }); - this.viewer.world.addHandler("remove-item", (e) => { - const tIndex = e.item.source.shader._programIndexTarget; - if (tIndex > 0) { - this.renderer.setRenderingSpecification(tIndex, null); - } - }); - } - - // Public API required by all Drawer implementations - /** - * Clean up the renderer, removing all resources - */ - destroy(){ - if(this.destroyed){ - return; - } - //todo - const gl = this.renderer.gl; - this._renderOffScreenTextures.forEach(t => { - if (t) { - gl.deleteTexture(t); - } - }); - this._renderOffScreenTextures = []; - - if (this._renderOffScreenBuffer) { - gl.deleteFramebuffer(this._renderOffScreenBuffer); - } - this.destroyed = true; - } - - // Public API required by all Drawer implementations - /** - * - * @returns true if the drawer supports rotation - */ - canRotate(){ - return true; - } - - // Public API required by all Drawer implementations - - /** - * @returns {Boolean} returns true if canvas and webgl are supported - */ - static isSupported(){ - return true; //todo - } - - getType() { - return 'universal_webgl'; - } - - /** - * create the HTML element (canvas in this case) that the image will be drawn into - * @returns {Element} the canvas to draw into - */ - createDrawingElement(){ - this.renderer = new $.WebGLModule($.extend(this.options, { - uniqueId: "openseadragon", - "2.0": { - canvasOptions: { - stencil: true - } - } - })); - return this.renderer.canvas; - } - - enableStencilTest(enabled) { - if (enabled) { - if (!this._stencilTestEnabled) { - const gl = this.renderer.gl; - gl.enable(gl.STENCIL_TEST); - gl.stencilMask(0xff); - gl.stencilFunc(gl.GREATER, 1, 0xff); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); - this._stencilTestEnabled = true; - } - } else { - if (this._stencilTestEnabled) { - this._stencilTestEnabled = false; - const gl = this.renderer.gl; - gl.disable(gl.STENCIL_TEST); - } - } - } - - /** - * - * @param {Array} tiledImages Array of TiledImage objects to draw - */ - draw(tiledImages){ - let twoPassRendering = this.options.twoPassRendering; - if (!twoPassRendering) { - for (const tiledImage of tiledImages) { - if (tiledImage.blendTime > 0) { - twoPassRendering = false; //todo set true, now we debug single pass - } - } - } - - let viewport = { - bounds: this.viewport.getBoundsNoRotate(true), - center: this.viewport.getCenter(true), - rotation: this.viewport.getRotation(true) * Math.PI / 180, - zoom: this.viewport.getZoom(true) - }; - - let flipMultiplier = this.viewport.flipped ? -1 : 1; - // calculate view matrix for viewer - let posMatrix = $.Mat3.makeTranslation(-viewport.center.x, -viewport.center.y); - let scaleMatrix = $.Mat3.makeScaling(2 / viewport.bounds.width * flipMultiplier, -2 / viewport.bounds.height); - let rotMatrix = $.Mat3.makeRotation(-viewport.rotation); - let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); - this._batchTextures = Array(this.maxTextureUnits); - - if (twoPassRendering) { - this._resizeOffScreenTextures(0); - this.enableStencilTest(true); - this._drawTwoPass(tiledImages, viewport, viewMatrix); - } else { - this._resizeOffScreenTextures(tiledImages.length); - this.enableStencilTest(false); - this._drawSinglePass(tiledImages, viewport, viewMatrix); - } - } - - - tiledImageViewportToImageZoom(tiledImage, viewportZoom) { - var ratio = tiledImage._scaleSpring.current.value * - tiledImage.viewport._containerInnerSize.x / - tiledImage.source.dimensions.x; - return ratio * viewportZoom; - } - - - _drawSinglePass(tiledImages, viewport, viewMatrix) { - const gl = this.renderer.gl; - gl.clear(gl.COLOR_BUFFER_BIT); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - for (const tiledImage of tiledImages) { - let tilesToDraw = tiledImage.getTilesToDraw(); - - if (tilesToDraw.length === 0) { - continue; - } - - //todo better access to the rendering context - const shader = this.renderer.specification(0).shaders.renderShader._renderContext; - shader.setBlendMode(tiledImage.index === 0 ? - "source-over" : tiledImage.compositeOperation || this.viewer.compositeOperation); - - const sourceShader = tiledImage.source.shader; - if (tiledImage.debugMode !== this.renderer.getCompiled("debug", sourceShader._programIndexTarget)) { - this.buildOptions.debug = tiledImage.debugMode; - //todo per image-level debug info :/ - this.renderer.buildProgram(sourceShader._programIndexTarget, null, true, this.buildOptions); - } - - - this.renderer.useProgram(sourceShader._programIndexTarget); - gl.clear(gl.STENCIL_BUFFER_BIT); - - let overallMatrix = viewMatrix; - let imageRotation = tiledImage.getRotation(true); - // if needed, handle the tiledImage being rotated - if( imageRotation % 360 !== 0) { - let imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180); - let imageCenter = tiledImage.getBoundsNoRotate(true).getCenter(); - let t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y); - let t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y); - - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); - overallMatrix = viewMatrix.multiply(localMatrix); - } - let pixelSize = this.tiledImageViewportToImageZoom(tiledImage, viewport.zoom); - - //tile level opacity not supported with single pass rendering - shader.opacity.set(tiledImage.opacity); - - //batch rendering (artifacts) - //let batchSize = 0; - - // iterate over tiles and add data for each one to the buffers - for (let tileIndex = tilesToDraw.length - 1; tileIndex >= 0; tileIndex--){ - const tile = tilesToDraw[tileIndex].tile; - const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); - const tileData = this._textureMap[tile.cacheKey]; - - this.renderer.processData(tileData.texture, { - transform: matrix, - zoom: viewport.zoom, - pixelSize: pixelSize, - textureCoords: tileData.position, - }); - - //batch rendering (artifacts) - // this._transformMatrices.set(matrix, batchSize * 9); - // this._tileTexturePositions.set(tileData.position, batchSize * 8); - // this._batchTextures[batchSize] = tileData.texture; - // batchSize++; - // if (batchSize === this.maxTextureUnits) { - // console.log("tiles inside", this._tileTexturePositions); - // this.renderer.processData(this._batchTextures, { - // transform: this._transformMatrices, - // zoom: viewport.zoom, - // pixelSize: pixelSize, - // textureCoords: this._tileTexturePositions, - // instanceCount: batchSize - // }); - // batchSize = 0; - // } - } - - //batch rendering (artifacts) - // if (batchSize > 0) { - // console.log("tiles outside", this._tileTexturePositions); - // - // //todo possibly zero out unused, or limit drawing size - // this.renderer.processData(this._batchTextures, { - // transform: this._transformMatrices, - // zoom: viewport.zoom, - // pixelSize: pixelSize, - // textureCoords: this._tileTexturePositions, - // instanceCount: batchSize - // }); - // } - - // Fire tiled-image-drawn event. - // TODO: the image data may not be on the output canvas yet!! - if( this.viewer ){ - /** - * Raised when a tiled image is drawn to the canvas. Only valid - * for webgl drawer. - * - * @event tiled-image-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {Array} tiles - An array of Tile objects that were drawn. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'tiled-image-drawn', { - tiledImage: tiledImage, - tiles: tilesToDraw.map(info => info.tile), - }); - } - } - } - - _drawTwoPass(tiledImages, viewport, viewMatrix) { - const gl = this.renderer.gl; - gl.clear(gl.COLOR_BUFFER_BIT); - - let drawnItems = 0; - - for (const tiledImage of tiledImages) { - let tilesToDraw = tiledImage.getTilesToDraw(); - - if (tilesToDraw.length === 0) { - continue; - } - - //second pass first: check whether next render won't overflow batch size - //todo better access to the rendering context - const shader = this.renderer.specification(0).shaders.renderShader._renderContext; - shader.setBlendMode(tiledImage.index === 0 ? - "source-over" : tiledImage.compositeOperation || this.viewer.compositeOperation); - // const willDraw = drawnItems + shader.dataReferences.length; - // if (willDraw > this.maxTextureUnits) { - // //merge to the output screen - // this._bindOffScreenTexture(-1); - // - // //todo - // - // drawnItems = 0; - // } - - this.renderer.useProgram(0); //todo use program based on texture used, e.g. drawing multi output - - - - this._bindOffScreenTexture(drawnItems); - - let overallMatrix = viewMatrix; - let imageRotation = tiledImage.getRotation(true); - // if needed, handle the tiledImage being rotated - if( imageRotation % 360 !== 0){ - let imageRotationMatrix = $.Mat3.makeRotation(-imageRotation * Math.PI / 180); - let imageCenter = tiledImage.getBoundsNoRotate(true).getCenter(); - let t1 = $.Mat3.makeTranslation(imageCenter.x, imageCenter.y); - let t2 = $.Mat3.makeTranslation(-imageCenter.x, -imageCenter.y); - - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply(imageRotationMatrix).multiply(t2); - overallMatrix = viewMatrix.multiply(localMatrix); - } - - // iterate over tiles and add data for each one to the buffers - for (let tileIndex = tilesToDraw.length - 1; tileIndex >= 0; tileIndex--){ - const tile = tilesToDraw[tileIndex].tile; - - const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); - shader.opacity.set(tile.opacity * tiledImage.opacity); - const tileData = this._textureMap[tile.cacheKey]; - - //todo pixelSize value (not yet memoized) - this.renderer.processData(tileData.texture, { - transform: matrix, - zoom: viewport.zoom, - pixelSize: 0, - textureCoords: tileData.position - }); - } - - // Fire tiled-image-drawn event. - // TODO: the image data may not be on the output canvas yet!! - if( this.viewer ){ - /** - * Raised when a tiled image is drawn to the canvas. Only valid - * for webgl drawer. - * - * @event tiled-image-drawn - * @memberof OpenSeadragon.Viewer - * @type {object} - * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event. - * @property {OpenSeadragon.TiledImage} tiledImage - Which TiledImage is being drawn. - * @property {Array} tiles - An array of Tile objects that were drawn. - * @property {?Object} userData - Arbitrary subscriber-defined object. - */ - this.viewer.raiseEvent( 'tiled-image-drawn', { - tiledImage: tiledImage, - tiles: tilesToDraw.map(info => info.tile), - }); - } - } - } - - //single pass shaders are built-in shaders compiled from JSON - _createSinglePassShader(textureType) { - this.defaultRenderingSpecification = { - shaders: { - renderShader: { - type: "identity", - dataReferences: [0], - } - } - }; - this.buildOptions = { - textureType: textureType, - //batch rendering (artifacts) - //instanceCount: this.maxTextureUnits, - debug: false - }; - const index = this.renderer.getSpecificationsCount(); - this.renderer.addRenderingSpecifications(this.defaultRenderingSpecification); - this.renderer.buildProgram(index, null, true, this.buildOptions); - } - - //two pass shaders are special - _createTwoPassShaderForFirstPass(textureType) { - //custom program for two pass processing - const gl = this.renderer.gl; - const program = gl.createProgram(); - - //works only in version dependent matter! - const glContext = this.renderer.webglContext; - const options = { - textureType: textureType - }; - - glContext.compileVertexShader(program, ` -uniform mat3 transform_matrix; -const vec3 quad[4] = vec3[4] ( - vec3(0.0, 1.0, 1.0), - vec3(0.0, 0.0, 1.0), - vec3(1.0, 1.0, 1.0), - vec3(1.0, 0.0, 1.0) -);`, ` -gl_Position = vec4(transform_matrix * quad[gl_VertexID], 1);`, options); - glContext.compileFragmentShader(program, ` -uniform int texture_location;`, ` -blend(osd_texture(texture_location, osd_texture_coords), 0, false)`, options); - return program; - } - - /** - * Set the context2d imageSmoothingEnabled parameter - * @param {Boolean} enabled - */ - setImageSmoothingEnabled(enabled){ - //todo - // this._clippingContext.imageSmoothingEnabled = enabled; - // this._outputContext.imageSmoothingEnabled = enabled; - } - - // private - _getTileMatrix(tile, tiledImage, viewMatrix){ - // compute offsets that account for tile overlap; needed for calculating the transform matrix appropriately - // x, y, w, h in viewport coords - - let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); - let xOffset = tile.positionedBounds.width * overlapFraction.x; - let yOffset = tile.positionedBounds.height * overlapFraction.y; - - let x = tile.positionedBounds.x + (tile.x === 0 ? 0 : xOffset); - let y = tile.positionedBounds.y + (tile.y === 0 ? 0 : yOffset); - let right = tile.positionedBounds.x + tile.positionedBounds.width - (tile.isRightMost ? 0 : xOffset); - let bottom = tile.positionedBounds.y + tile.positionedBounds.height - (tile.isBottomMost ? 0 : yOffset); - let w = right - x; - let h = bottom - y; - - let matrix = new $.Mat3([ - w, 0, 0, - 0, h, 0, - x, y, 1, - ]); - - if(tile.flipped){ - // flip the tile around the center of the unit quad - let t1 = $.Mat3.makeTranslation(0.5, 0); - let t2 = $.Mat3.makeTranslation(-0.5, 0); - - // update the view matrix to account for this image's rotation - let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2); - matrix = matrix.multiply(localMatrix); - } - - let overallMatrix = viewMatrix.multiply(matrix); - return overallMatrix.values; - } - - _resizeRenderer(){ - const size = this._calculateCanvasSize(); - this.renderer.setDimensions(0, 0, size.x, size.y); - this._size = size; - } - - _bindOffScreenTexture(index) { - const gl = this.renderer.gl; - if (index < 0) { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } else { - let texture = this._renderOffScreenTextures[index]; - gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderOffScreenBuffer); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - } - } - - _resizeOffScreenTextures(count) { - //create at most count textures, with max texturing units constraint - const gl = this.renderer.gl; - - count = Math.min(count, this.maxTextureUnits); - - if (count > 0) { - //append or reinitialize textures - const rebuildStartIndex = - this._renderBufferSize === this._size ? - this._renderOffScreenTextures.length : 0; - - let i; - for (i = rebuildStartIndex; i < count; i++) { - let texture = this._renderOffScreenTextures[i]; - if (!texture) { - this._renderOffScreenTextures[i] = texture = gl.createTexture(); - } - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, - this._size.x, this._size.y, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - //destroy any textures that we don't need todo maybe just keep dont bother? - for (let j = this._renderOffScreenTextures.length - 1; j >= i; j--) { - let texture = this._renderOffScreenTextures.pop(); - gl.deleteTexture(texture); - } - - this._renderBufferSize = this._size; - return count; - } - //just leave the textures be, freeing consumes time - return 0; - } - - - _tileReadyHandler(event){ - //todo tile overlap - let tile = event.tile; - let tiledImage = event.tiledImage; - if (this._textureMap[tile.cacheKey]) { - return; - } - - let position, - overlap = tiledImage.source.tileOverlap; - if( overlap > 0){ - // calculate the normalized position of the rect to actually draw - // discarding overlap. - let overlapFraction = this._calculateOverlapFraction(tile, tiledImage); - - let left = tile.x === 0 ? 0 : overlapFraction.x; - let top = tile.y === 0 ? 0 : overlapFraction.y; - let right = tile.isRightMost ? 1 : 1 - overlapFraction.x; - let bottom = tile.isBottomMost ? 1 : 1 - overlapFraction.y; - position = new Float32Array([ - left, bottom, - left, top, - right, bottom, - right, top - ]); - } else { - // no overlap: this texture can use the unit quad as it's position data - position = new Float32Array([ - 0, 1, - 0, 0, - 1, 1, - 1, 0 - ]); - } - - //todo rewrite with new cache api, support data arrays - let data = tile.cacheImageRecord ? tile.cacheImageRecord.getData() : tile.getCanvasContext().canvas; - - const options = this.renderer.webglContext.options; - const gl = this.renderer.gl; - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, options.wrap); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, options.minFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.magFilter); - gl.texImage2D(gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - data); - this._textureMap[tile.cacheKey] = { - texture: texture, - position: position, - }; - } - - _calculateOverlapFraction(tile, tiledImage){ - let overlap = tiledImage.source.tileOverlap; - let nativeWidth = tile.sourceBounds.width; // in pixels - let nativeHeight = tile.sourceBounds.height; // in pixels - let overlapWidth = (tile.x === 0 ? 0 : overlap) + (tile.isRightMost ? 0 : overlap); // in pixels - let overlapHeight = (tile.y === 0 ? 0 : overlap) + (tile.isBottomMost ? 0 : overlap); // in pixels - let widthOverlapFraction = overlap / (nativeWidth + overlapWidth); // as a fraction of image including overlap - let heightOverlapFraction = overlap / (nativeHeight + overlapHeight); // as a fraction of image including overlap - return { - x: widthOverlapFraction, - y: heightOverlapFraction - }; - } -}; -}( OpenSeadragon )); diff --git a/src/webgl/plainShader.js b/src/webgl/plainShader.js deleted file mode 100644 index a8c4c7c5..00000000 --- a/src/webgl/plainShader.js +++ /dev/null @@ -1,42 +0,0 @@ -(function($) { -/** - * Identity shader - * - * data reference must contain one index to the data to render using identity - */ -$.WebGLModule.IdentityLayer = class extends $.WebGLModule.ShaderLayer { - - static type() { - return "identity"; - } - - static name() { - return "Identity"; - } - - static description() { - return "shows the data AS-IS"; - } - - static sources() { - return [{ - acceptsChannelCount: (x) => x === 4, - description: "4d texture to render AS-IS" - }]; - } - - getFragmentShaderExecution() { - return `return ${this.sampleChannel("osd_texture_coords")};`; - //return `return vec4(osd_texture_coords, .0, 1.0);`; - - } -}; - -//todo why cannot be inside object :/ -$.WebGLModule.IdentityLayer.defaultControls["use_channel0"] = { - required: "rgba" -}; - -$.WebGLModule.ShaderMediator.registerLayer($.WebGLModule.IdentityLayer); - -})(OpenSeadragon); diff --git a/src/webgl/renderer.js b/src/webgl/renderer.js deleted file mode 100644 index 849c8472..00000000 --- a/src/webgl/renderer.js +++ /dev/null @@ -1,902 +0,0 @@ - - -(function($) { - - -/** - * Wrapping the funcionality of WebGL to be suitable for tile processing and rendering. - * Written by Aiosa - * @class OpenSeadragon.WebGLModule - * @memberOf OpenSeadragon - */ -$.WebGLModule = class extends $.EventSource { - /** - * @typedef {{ - * name?: string, - * lossless?: boolean, - * shaders: Object. - * }} OpenSeadragon.WebGLModule.RenderingConfig - * - * //use_channel[X] name - * @template {Object} TUseChannel - * //use_[fitler_name] - * @template {Object} TUseFilter - * @template {Object} TIControlConfig - * @typedef OpenSeadragon.WebGLModule.ShaderLayerParams - * @type {{TUseChannel,TUseFilter,TIControlConfig}} - * - * @typedef {{ - * name?: string, - * type: string, - * visible?: boolean, - * dataReferences: number[], - * params?: OpenSeadragon.WebGLModule.ShaderLayerParams, - * }} OpenSeadragon.WebGLModule.ShaderLayerConfig - * - * - * @typedef OpenSeadragon.WebGLModule.UIControlsRenderer - * @type function - * @param {string} title - * @param {string} html - * @param {string} dataId - * @param {boolean} isVisible - * @param {OpenSeadragon.WebGLModule.ShaderLayer} layer - * @param {boolean} wasErrorWhenLoading - */ - - - /** - * @param {object} incomingOptions - * @param {string} incomingOptions.htmlControlsId: where to render html controls, - * @param {string} incomingOptions.webGlPreferredVersion prefered WebGL version, for now "1.0" or "2.0" - * @param {OpenSeadragon.WebGLModule.UIControlsRenderer} incomingOptions.htmlShaderPartHeader function that generates particular layer HTML - * @param {boolean} incomingOptions.debug debug mode default false - * @param {function} incomingOptions.ready function called when ready - * @param {function} incomingOptions.resetCallback function called when user input changed, e.g. changed output of the current rendering - * signature f({Visualization} oldVisualisation,{Visualization} newVisualisation) - * @constructor - * @memberOf OpenSeadragon.WebGLModule - */ - constructor(incomingOptions) { - super(); - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Default values overrideable from incomingOptions ///////////////// - ///////////////////////////////////////////////////////////////////////////////// - this.uniqueId = ""; - - //todo events instead - this.ready = function() { }; - this.htmlControlsId = null; - this.webGlPreferredVersion = "2.0"; - this.htmlShaderPartHeader = function(title, html, dataId, isVisible, layer, isControllable = true) { - return `
${title}
${html}
`; - }; - this.resetCallback = function() { }; - //called once a visualisation is compiled and linked (might not happen) - this.visualisationReady = function(i, visualisation) { }; - - /** - * Debug mode. - * @member {boolean} - */ - this.debug = false; - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Incoming Values /////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////// - - // Assign from incoming terms - for (let key in incomingOptions) { - if (incomingOptions[key]) { - this[key] = incomingOptions[key]; - } - } - - if (!this.constructor.idPattern.test(this.uniqueId)) { - throw "$.WebGLModule: invalid ID! Id can contain only letters, numbers and underscore. ID: " + this.uniqueId; - } - - /** - * Current rendering context - * @member {OpenSeadragon.WebGLModule.WebGLImplementation} - */ - this.webglContext = null; - - /** - * WebGL context - * @member {WebGLRenderingContext|WebGL2RenderingContext} - */ - this.gl = null; - - ///////////////////////////////////////////////////////////////////////////////// - ///////////// Internals ///////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////// - - this.reset(); - - try { - const canvas = document.createElement("canvas"); - for (let version of [this.webGlPreferredVersion, "2.0", "1.0"]) { - const contextOpts = incomingOptions[version] || {}; - - const Context = $.WebGLModule.determineContext(version); - //todo documment this - let glContext = Context && Context.create(canvas, contextOpts.canvasOptions || {}); - - if (glContext) { - this.gl = glContext; - - const readGlProp = function(prop, defaultValue) { - return glContext[contextOpts[prop] || defaultValue] || glContext[defaultValue]; - }; - - /** - * @param {object} options - * @param {string} options.wrap texture wrap parameteri - * @param {string} options.magFilter texture filter parameteri - * @param {string} options.minFilter texture filter parameteri - */ - const options = { - wrap: readGlProp("wrap", "MIRRORED_REPEAT"), - magFilter: readGlProp("magFilter", "LINEAR"), - minFilter: readGlProp("minFilter", "LINEAR"), - }; - this.webglContext = new Context(this, glContext, options); - } - } - - } catch (e) { - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "Unable to initialize the WebGL renderer.", - details: e}); - $.console.error(e); - return; - } - $.console.log(`WebGL ${this.webglContext.getVersion()} Rendering module (ID ${this.uniqueId})`); - } - - /** - * Reset the engine to the initial state - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - reset() { - if (this._programs) { - Object.values(this._programs).forEach(p => this._unloadProgram(p)); - } - this._programSpecifications = []; - this._dataSources = []; - this._origDataSources = []; - this._programs = {}; - this._program = -1; - this.running = false; - this._initialized = false; - } - - /** - * WebGL target canvas - * @return {HTMLCanvasElement} - */ - get canvas() { - return this.gl.canvas; - } - - /** - * WebGL active program - * @return {WebGLProgram} - */ - get program() { - return this._programs[this._program]; - } - - /** - * Check if init() was called. - * @return {boolean} - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - get isInitialized() { - return this._initialized; - } - - /** - * Change the dimensions, useful for borders, used by openSeadragonGL - * @instance - * @memberOf WebGLModule - */ - setDimensions(x, y, width, height) { - if (width === this.width && height === this.height) { - return; - } - - this.width = width; - this.height = height; - this.gl.canvas.width = width; - this.gl.canvas.height = height; - this.gl.viewport(x, y, width, height); - } - - /** - * - */ - getCompiled(name, programIndex = this._program) { - return this.webglContext.getCompiled(this._programs[programIndex], name); - } - - /** - * Set program shaders. Vertex shader is set by default a square. - * @param {RenderingConfig} specifications - objects that define the what to render (see Readme) - * @return {boolean} true if loaded successfully - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - addRenderingSpecifications(...specifications) { - for (let spec of specifications) { - const parsed = this._parseSpec(spec); - if (parsed) { - this._programSpecifications.push(parsed); - } - } - return true; - } - - setRenderingSpecification(i, spec) { - if (!spec) { - const program = this._programs[i]; - if (program) { - this._unloadProgram(); - } - delete this._programs[i]; - delete this._programSpecifications[i]; - this.getCurrentProgramIndex(); - return true; - } else { - const parsed = this._parseSpec(spec); - if (parsed) { - this._programSpecifications[i] = parsed; - return true; - } - } - return false; - } - - _parseSpec(spec) { - if (!spec.shaders) { - $.console.warn("Invalid visualization: no shaders defined", spec); - return undefined; - } - - let count = 0; - for (let sid in spec.shaders) { - const shader = spec.shaders[sid]; - if (!shader.params) { - shader.params = {}; - } - count++; - } - - if (count < 0) { - $.console.warn("Invalid rendering specs: no shader configuration present!", spec); - return undefined; - } - return spec; - } - - /** - * - * @param i - * @param order - * @param force - * @param {object} options - * @param {boolean} options.withHtml whether html should be also created (false if no UI controls are desired) - * @param {string} options.textureType id of texture to be used, supported are TEXTURE_2D, TEXTURE_2D_ARRAY, TEXTURE_3D - * @param {string} options.instanceCount number of instances to draw at once - * @param {boolean} options.debug draw debugging info - * @return {boolean} - */ - buildProgram(i, order, force, options) { - let vis = this._programSpecifications[i]; - - if (!vis) { - $.console.error("Invalid rendering program target!", i); - return false; - } - - if (order) { - vis.order = order; - } - - let program = this._programs && this._programs[i]; - force = force || (program && !program['VERTEX_SHADER']); - if (force) { - this._unloadProgram(program); - this._specificationToProgram(vis, i, options); - - if (i === this._program) { - this._forceSwitchShader(this._program); - } - return true; - } - return false; - } - - /** - * Rebuild specification and update scene - * @param {string[]|undefined} order of shaders, ID's of data as defined in setup JSON, last element - * is rendered last (top) - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - rebuildCurrentProgram(order = undefined) { - const program = this._programs[this._program]; - if (this.buildProgram(this._program, order, true, program && program._osdOptions)) { - this._forceSwitchShader(this._program); - } - } - - /** - * Get currently used specification - * @return {object} current specification - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - specification(index) { - return this._programSpecifications[index]; - } - - /** - * Get currently used specification ilayer.params,ndex - * @return {number} index of the current specification - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - currentSpecificationIndex() { - return this._program; - } - - /** - * Switch to program at index: this is the index (order) in which - * setShaders(...) was called. If you want to switch to shader that - * has been set with second setShaders(...) call, pass i=1. - * @param {Number} i program index or null if you wish to re-initialize the current one - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - useProgram(i) { - if (!this._initialized) { - $.console.warn("$.WebGLModule::useSpecification(): not initialized."); - return; - } - - if (this._program === i) { - return; - } - this._forceSwitchShader(i); - } - - useCustomProgram(program) { - this._program = -1; - this.webglContext.programLoaded(program, null); - } - - getSpecificationsCount() { - return this._programSpecifications.length; - } - - /** - * Get a list of image pyramids used to compose the current active specification - * @instance - * @memberOf WebGLModule - */ - getSources() { - return this._dataSources; - } - - /** - * Set data srouces - */ - setSources(sources) { - if (!this._initialized) { - $.console.warn("$.WebGLModule::useSpecification(): not initialized."); - return; - } - this._origDataSources = sources || []; - } - - /** - * Renders data using WebGL - * @param {GLuint|[GLuint]} texture or texture array for instanced drawing - * - * @param {object} tileOpts - * @param {number} tileOpts.zoom value passed to the shaders as zoom_level - * @param {number} tileOpts.pixelSize value passed to the shaders as pixel_size_in_fragments - * @param {OpenSeadragon.Mat3|[OpenSeadragon.Mat3]} tileOpts.transform position transform - * matrix or flat matrix array (instance drawing) - * @param {number?} tileOpts.instanceCount how many instances to draw in case instanced drawing is enabled - * - * @instance - * @memberOf WebGLModule - */ - processData(texture, tileOpts) { - const spec = this._programSpecifications[this._program]; - if (!spec) { - $.console.error("Cannot render using invalid specification: did you call useCustomProgram?", this._program); - } else { - this.webglContext.programUsed(this.program, spec, texture, tileOpts); - // if (this.debug) { - // //todo - // this._renderDebugIO(data, result); - // } - } - } - - processCustomData(texture, tileOpts) { - this.webglContext.programUsed(this.program, null, texture, tileOpts); - // if (this.debug) { - // //todo - // this._renderDebugIO(data, result); - // } - } - - /** - * Clear the output canvas - */ - clear() { - //todo: necessary? - this.gl.clearColor(0, 0, 0, 0); - this.gl.clear(this.gl.COLOR_BUFFER_BIT); - } - - /** - * Whether the webgl module renders UI - * @return {boolean|boolean} - * @instance - * @memberOf WebGLModule - */ - supportsHtmlControls() { - return typeof this.htmlControlsId === "string" && this.htmlControlsId.length > 0; - } - - /** - * Execute call on each visualization layer with no errors - * @param {object} vis current specification setup context - * @param {function} callback call to execute - * @param {function} onFail handle exception during execition - * @return {boolean} true if no exception occured - * @instance - * @memberOf WebGLModule - */ - static eachValidShaderLayer(vis, callback, - onFail = (layer, e) => { - layer.error = e.message; - $.console.error(e); - }) { - let shaders = vis.shaders; - if (!shaders) { - return true; - } - let noError = true; - for (let key in shaders) { - let shader = shaders[key]; - - if (shader && !shader.error) { - try { - callback(shader); - } catch (e) { - if (!onFail) { - throw e; - } - onFail(shader, e); - noError = false; - } - } - } - return noError; - } - - /** - * Execute call on each _visible_ specification layer with no errors. - * Visible is subset of valid. - * @param {object} vis current specification setup context - * @param {function} callback call to execute - * @param {function} onFail handle exception during execition - * @return {boolean} true if no exception occured - * @instance - * @memberOf WebGLModule - */ - static eachVisibleShaderLayer(vis, callback, - onFail = (layer, e) => { - layer.error = e.message; - $.console.error(e); - }) { - - let shaders = vis.shaders; - if (!shaders) { - return true; - } - let noError = true; - for (let key in shaders) { - //rendering == true means no error - let shader = shaders[key]; - if (shader && shader.rendering) { - try { - callback(shader); - } catch (e) { - if (!onFail) { - throw e; - } - onFail(shader, e); - noError = false; - } - } - } - return noError; - } - - ///////////////////////////////////////////////////////////////////////////////////// - //// YOU PROBABLY WANT TO READ FUNCTIONS BELOW SO YOU KNOW HOW TO SET UP YOUR SHADERS - //// BUT YOU SHOULD NOT CALL THEM DIRECTLY - ///////////////////////////////////////////////////////////////////////////////////// - - /** - * Get current program, reset if invalid - * @return {number} program index - */ - getCurrentProgramIndex() { - if (this._program < 0 || this._program >= this._programSpecifications.length) { - this._program = 0; - } - return this._program; - } - - /** - * Function to JSON.stringify replacer - * @param key key to the value - * @param value value to be exported - * @return {*} value if key passes exportable condition, undefined otherwise - */ - static jsonReplacer(key, value) { - return key.startsWith("_") || ["eventSource"].includes(key) ? undefined : value; - } - - /** - * Initialization. It is separated from preparation as this actually initiates the rendering, - * sometimes this can happen only when other things are ready. Must be performed after - * all the prepare() strategy finished: e.g. as onPrepared. Or use prepareAndInit(); - * - * @param {int} width width of the first tile going to be drawn - * @param {int} height height of the first tile going to be drawn - * @param firstProgram - */ - init(width = 1, height = 1, firstProgram = 0) { - if (this._initialized) { - $.console.error("Already initialized!"); - return; - } - if (this._programSpecifications.length < 1) { - $.console.error("No specification specified!"); - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "No specification specified!", - details: "::prepare() called with no specification set."}); - return; - } - this._program = firstProgram; - this.getCurrentProgramIndex(); //validates index - - this._initialized = true; - this.setDimensions(width, height); - - //todo rotate anticlockwise to cull backfaces - this.gl.enable(this.gl.CULL_FACE); - this.gl.cullFace(this.gl.FRONT); - - this.running = true; - - this._forceSwitchShader(null); - this.ready(); - } - - setDataBlendingEnabled(enabled) { - if (enabled) { - // this.gl.enable(this.gl.BLEND); - // this.gl.blendEquation(this.gl.FUNC_ADD); - // this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE); - this.gl.enable(this.gl.BLEND); - this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA); - } else { - this.gl.disable(this.gl.BLEND); - } - } - - ////////////////////////////////////////////////////////////////////////////// - ///////////// YOU PROBABLY DON'T WANT TO READ/CHANGE FUNCTIONS BELOW - ////////////////////////////////////////////////////////////////////////////// - - /** - * Forward glLoaded event to the active layer - * @param gl - * @param program - * @param vis - */ - glLoaded(gl, program, vis) { - $.WebGLModule.eachVisibleShaderLayer(vis, layer => layer._renderContext.glLoaded(program, gl)); - } - - /** - * Forward glDrawing event to the active layer - * @param gl - * @param program - * @param vis - * @param bounds - */ - glDrawing(gl, program, vis, bounds) { - $.WebGLModule.eachVisibleShaderLayer(vis, layer => layer._renderContext.glDrawing(program, gl)); - } - - /** - * Force switch shader (program), will reset even if the specified - * program is currently active, good if you need 'gl-loaded' to be - * invoked (e.g. some uniform variables changed) - * @param {Number} i program index or null if you wish to re-initialize the current one - * @param _reset - * @private - */ - _forceSwitchShader(i, _reset = true) { - if (isNaN(i) || i === null || i === undefined) { - i = this._program; - } - - let target = this._programSpecifications[i]; - if (!target) { - $.console.error("Invalid rendering target index!", i); - return; - } - - const program = this._programs[i]; - if (!program) { - this._specificationToProgram(target, i); - } else if (i !== this._program) { - this._updateRequiredDataSources(target); - } - - this._program = i; - if (target.error) { - if (this.supportsHtmlControls()) { - this._loadHtml(i, program); - } - this._loadScript(i); - this.running = false; - if (this._programSpecifications.length < 2) { - /** - * @event fatal-error - */ - this.raiseEvent('fatal-error', {message: "The only rendering specification left is invalid!", target: target}); - } else { - /** - * @event error - */ - this.raiseEvent('error', {message: "Currently chosen rendering specification is not valid!", target: target}); - } - } else { - this.running = true; - if (this.supportsHtmlControls()) { - this._loadHtml(program); - } - this._loadDebugInfo(); - if (!this._loadScript(i)) { - if (!_reset) { - throw "Could not build visualization"; - } - this._forceSwitchShader(i, false); //force reset in errors - return; - } - this.webglContext.programLoaded(program, target); - } - } - - _unloadProgram(program) { - if (program) { - //must remove before attaching new - this._detachShader(program, "VERTEX_SHADER"); - this._detachShader(program, "FRAGMENT_SHADER"); - } - } - - _loadHtml(program) { - let htmlControls = document.getElementById(this.htmlControlsId); - htmlControls.innerHTML = this.webglContext.getCompiled(program, "html") || ""; - } - - _loadScript(visId) { - return $.WebGLModule.eachValidShaderLayer(this._programSpecifications[visId], layer => layer._renderContext.init()); - } - - _getDebugInfoPanel() { - return `
-WebGL Processing I/O (debug mode) -
-Input:
No input.

-Output:
No output.
`; - } - - _loadDebugInfo() { - if (!this.debug) { - return; - } - - let container = document.getElementById(`test-${this.uniqueId}-webgl`); - if (!container) { - if (!this.htmlControlsId) { - document.body.innerHTML += `
${this._getDebugInfoPanel()}
`; - } else { - //safe as we do this before handlers are attached - document.getElementById(this.htmlControlsId).parentElement.innerHTML += `
${this._getDebugInfoPanel()}
`; - } - } - } - - _renderDebugIO(inputData, outputData) { - let input = document.getElementById(`test-${this.uniqueId}-webgl-input`); - let output = document.getElementById(`test-${this.uniqueId}-webgl-output`); - - input.innerHTML = ""; - input.append($.WebGLModule.Loaders.dataAsHtmlElement(inputData)); - - if (outputData) { - output.innerHTML = ""; - if (!this._ocanvas) { - this._ocanvas = document.createElement("canvas"); - } - this._ocanvas.width = outputData.width; - this._ocanvas.height = outputData.height; - let octx = this._ocanvas.getContext('2d'); - octx.drawImage(outputData, 0, 0); - output.append(this._ocanvas); - } else { - output.innerHTML = "No output!"; - } - } - - _buildFailed(specification, error) { - $.console.error(error); - specification.error = "Failed to compose this specification."; - specification.desc = error; - } - - _buildSpecification(program, order, specification, options) { - try { - options.withHtml = this.supportsHtmlControls(); - const usableShaderCount = this.webglContext.compileSpecification( - program, order, specification, options); - - if (usableShaderCount < 1) { - this._buildFailed(specification, `Empty specification: no valid specification has been specified. -
Specification setup:
${JSON.stringify(specification, $.WebGLModule.jsonReplacer)} -
Dynamic shader data:
${JSON.stringify(specification.data)}`); - return; - } - //preventive - delete specification.error; - delete specification.desc; - } catch (error) { - this._buildFailed(specification, error); - } - } - - _detachShader(program, type) { - let shader = program[type]; - if (shader) { - this.gl.detachShader(program, shader); - this.gl.deleteShader(shader); - program[type] = null; - } - } - - _specificationToProgram(spec, idx, options) { - this._updateRequiredDataSources(spec); - let gl = this.gl; - let program; - - if (!this._programs[idx]) { - program = gl.createProgram(); - this._programs[idx] = program; - - let index = 0; - //init shader factories and unique id's - for (let key in spec.shaders) { - let layer = spec.shaders[key]; - if (layer) { - let ShaderFactoryClass = $.WebGLModule.ShaderMediator.getClass(layer.type); - if (layer.type === "none") { - continue; - } - this._initializeShaderFactory(ShaderFactoryClass, layer, index++); - } - } - } else { - program = this._programs[idx]; - for (let key in spec.shaders) { - let layer = spec.shaders[key]; - - if (layer) { - if (!layer.error && - layer._renderContext && - layer._renderContext.constructor.type() === layer.type) { - continue; - } - delete layer.error; - delete layer.desc; - if (layer.type === "none") { - continue; - } - let ShaderFactoryClass = $.WebGLModule.ShaderMediator.getClass(layer.type); - this._initializeShaderFactory(ShaderFactoryClass, layer, layer._index); - } - } - } - - if (!Array.isArray(spec.order) || spec.order.length < 1) { - spec.order = Object.keys(spec.shaders); - } - - this._buildSpecification(program, spec.order, spec, options); - this.visualisationReady(idx, spec); - return idx; - } - - _initializeShaderFactory(ShaderFactoryClass, layer, idx) { - if (!ShaderFactoryClass) { - layer.error = "Unknown layer type."; - layer.desc = `The layer type '${layer.type}' has no associated factory. Missing in 'shaderSources'.`; - $.console.warn("Skipping layer " + layer.name); - return; - } - layer._index = idx; - layer.visible = layer.visible === undefined ? true : layer.visible; - layer._renderContext = new ShaderFactoryClass(`${this.uniqueId}${idx}`, layer.params || {}, { - layer: layer, - webgl: this.webglContext, - invalidate: this.resetCallback, - rebuild: this.rebuildCurrentProgram.bind(this, undefined) - }); - } - - _updateRequiredDataSources(specs) { - //for now just request all data, later decide in the context on what to really send - //might in the future decide to only request used data, now not supported - let usedIds = new Set(); - for (let key in specs.shaders) { - let layer = specs.shaders[key]; - if (layer) { - for (let x of layer.dataReferences) { - usedIds.add(x); - } - } - } - usedIds = [...usedIds].sort(); - this._dataSources = []; - - while (usedIds[usedIds.length - 1] >= this._origDataSources.length) { - //make sure values are set if user did not provide - this._origDataSources.push("__generated_do_not_use__"); - } - - for (let id of usedIds) { - this._dataSources.push(this._origDataSources[id]); - } - } -}; - -/** - * ID pattern allowed for module, ID's are used in GLSL - * to distinguish uniquely between static generated code parts - * @type {RegExp} - */ -$.WebGLModule.idPattern = /[0-9a-zA-Z_]*/; - -})(OpenSeadragon); diff --git a/src/webgl/shaderLayer.js b/src/webgl/shaderLayer.js deleted file mode 100644 index 628016f2..00000000 --- a/src/webgl/shaderLayer.js +++ /dev/null @@ -1,1373 +0,0 @@ -(function($) { - - /** - * Shader sharing point - * @class OpenSeadragon.WebGLModule.ShaderMediator - */ -$.WebGLModule.ShaderMediator = class { - - /** - * Register shader - * @param {typeof OpenSeadragon.WebGLModule.ShaderLayer} LayerRendererClass static class definition - */ - static registerLayer(LayerRendererClass) { - //todo why not hasOwnProperty check allowed by syntax checker - // if (this._layers.hasOwnProperty(LayerRendererClass.type())) { - // console.warn("Registering an already existing layer renderer:", LayerRendererClass.type()); - // } - // if (!$.WebGLModule.ShaderLayer.isPrototypeOf(LayerRendererClass)) { - // throw `${LayerRendererClass} does not inherit from ShaderLayer!`; - // } - if (!this.acceptsShaders) { - $.console.error("Registering layer renderer when registering disabled!", LayerRendererClass.type()); - } - this._layers[LayerRendererClass.type()] = LayerRendererClass; - } - - static setAcceptsRegistrations(accepts) { - this.acceptsShaders = accepts; - } - - /** - * Get the shader class by type id - * @param {string} id - * @return {function} class extends OpenSeadragon.WebGLModule.ShaderLayer - */ - static getClass(id) { - return this._layers[id]; - } - - /** - * Get all available shaders - * @return {typeof OpenSeadragon.WebGLModule.ShaderLayer[]} classes that extend OpenSeadragon.WebGLModule.ShaderLayer - */ - static availableShaders() { - return Object.values(this._layers); - } - - /** - * Get all available shaders - * @return {string[]} classes that extend OpenSeadragon.WebGLModule.ShaderLayer - */ - static availableTypes() { - return Object.keys(this._layers); - } -}; -//todo why cannot be inside object :/ -$.WebGLModule.ShaderMediator.acceptsShaders = true; -$.WebGLModule.ShaderMediator._layers = {}; - -$.WebGLModule.BLEND_MODE = { - 'source-over': 0, - 'source-in': 1, - 'source-out': 1, - 'source-atop': 1, - 'destination-over': 1, - 'destination-in': 1, - 'destination-out': 1, - 'destination-atop': 1, - lighten: 1, - darken: 1, - copy: 1, - xor: 1, - multiply: 1, - screen: 1, - overlay: 1, - 'color-dodge': 1, - 'color-burn': 1, - 'hard-light': 1, - 'soft-light': 1, - difference: 1, - exclusion: 1, - hue: 1, - saturation: 1, - color: 1, - luminosity: 1 -}; -$.WebGLModule.BLEND_MODE_MULTIPLY = 1; -/** - * Abstract interface to any Shader. - * @abstract - */ -$.WebGLModule.ShaderLayer = class { - - /** - * Override **static** type definition - * The class must be registered using the type - * @returns {string} unique id under which is the shader registered - */ - static type() { - throw "ShaderLayer::type() Type must be specified!"; - } - - /** - * Override **static** name definition - * @returns {string} name of the shader (user-friendly) - */ - static name() { - throw "ShaderLayer::name() Name must be specified!"; - } - - /** - * Provide description - * @returns {string} optional description - */ - static description() { - return "ShaderLayer::description() WebGL shader must provide description."; - } - - /** - * Declare the number of data sources it reads from (how many dataSources indexes should the shader contain) - * @return {Array.} array of source specifications: - * acceptsChannelCount: predicate that evaluates whether given number of channels (argument) is acceptable - * [optional] description: the description of the source - what it is being used for - */ - static sources() { - throw "ShaderLayer::sources() Shader must specify channel acceptance predicates for each source it uses!"; - } - - /** - * Global supported options - * @param {string} id unique ID among all webgl instances and shaders - * @param {OpenSeadragon.WebGLModule.ShaderLayerParams} options - * options.channel: "r", "g" or "b" channel to sample, default "r" - * options.use_mode: blending mode - default alpha ("show"), custom blending ("mask") and clipping mask blend ("mask_clip") - * options.use_[*]: filtering, gamma/exposure/logscale with a float filter parameter (e.g. "use_gamma" : 1.5) - * @param {object} privateOptions options that should not be touched, necessary for linking the layer to the core - */ - constructor(id, options, privateOptions) { - this.uid = id; - this._setContextShaderLayer(privateOptions.layer); - this.webglContext = privateOptions.webgl; - this.invalidate = privateOptions.invalidate; - //use with care... - this._rebuild = privateOptions.rebuild; - - this._buildControls(options); - this.resetChannel(options); - this.resetMode(options); - this._blendUniform = null; - this._clipUniform = null; - this.blendMode = $.WebGLModule.BLEND_MODE["source-over"]; - } - - /** - * Code placed outside fragment shader's main(...). - * By default, it includes all definitions of - * controls you defined in defaultControls - * - * NOTE THAT ANY VARIABLE NAME - * WITHIN THE GLOBAL SPACE MUST BE - * ESCAPED WITH UNIQUE ID: this.uid - * - * DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code - * - * WHEN OVERRIDING, INCLUDE THE OUTPUT OF THIS METHOD AT THE BEGINNING OF THE NEW OUTPUT. - * - * @return {string} - */ - getFragmentShaderDefinition() { - this._blendUniform = `${this.uid}_blend`; - this._clipUniform = `${this.uid}_clip`; - let controls = this.constructor.defaultControls, - glsl = [`uniform int ${this._blendUniform};`, `uniform bool ${this._clipUniform};`]; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - let code = controlObject.define(); - if (code) { - code = code.trim(); - glsl.push(code); - } - } - } - return glsl.join("\n"); - } - - setBlendMode(name) { - const modes = $.WebGLModule.BLEND_MODE; - this.blendMode = modes[name]; - if (this.blendMode === undefined) { - this.blendMode = modes["source-over"]; - } - } - - /** - * Code executed to create the output color. The code - * must always return a vec4 value, otherwise the visualization - * will fail to compile (this code actually runs inside a vec4 function). - * - * DO NOT SAMPLE TEXTURE MANUALLY: use this.sampleChannel(...) to generate the code - * - * @return {string} - */ - getFragmentShaderExecution() { - throw "ShaderLayer::getFragmentShaderExecution must be implemented!"; - } - - /** - * Called when an image is rendered - * @param {WebGLProgram} program WebglProgram instance - * @param {WebGLRenderingContextBase} gl - */ - glDrawing(program, gl) { - if (this._blendUniform) { - gl.uniform1i(this._blendLoc, this.blendMode); - gl.uniform1i(this._clipLoc, 0); //todo - } - - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.glDrawing(program, gl); - } - } - } - - /** - * Called when associated webgl program is switched to - * @param {WebGLProgram} program WebglProgram instance - * @param {WebGLRenderingContextBase} gl WebGL Context - */ - glLoaded(program, gl) { - if (!this._blendUniform) { - $.console.warn("Shader layer has autoblending disabled: are you sure you call super.getFragmentShaderDefinition()?"); - } else { - this._clipLoc = gl.getUniformLocation(program, this._clipUniform); - this._blendLoc = gl.getUniformLocation(program, this._blendUniform); - } - - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.glLoaded(program, gl); - } - } - } - - /** - * This function is called once at - * the beginning of the layer use - * (might be multiple times), after htmlControls() - */ - init() { - let controls = this.constructor.defaultControls; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - controlObject.init(); - } - } - } - - /** - * Get the shader UI controls - * @return {string} HTML controls for the particular shader - */ - htmlControls() { - let controls = this.constructor.defaultControls, - html = []; - for (let control in controls) { - if (control.startsWith("use_")) { - continue; - } - - const controlObject = this[control]; - if (controlObject) { - html.push(controlObject.toHtml(true)); - } - } - return html.join(""); - } - - /** - * Include GLSL shader code on global scope - * (e.g. define function that is repeatedly used) - * does not have to use unique ID extended names as this code is included only once - * @param {string} key a key under which is the code stored, so that the same key is not loaded twice - * @param {string} code GLSL code to add to the shader - */ - includeGlobalCode(key, code) { - let container = this.constructor.__globalIncludes; - if (!container[key]) { - container[key] = code; - } - } - - /** - * Parses value to a float string representation with given precision (length after decimal) - * @param {number} value value to convert - * @param {number} defaultValue default value on failure - * @param {number} precisionLen number of decimals - * @return {string} - */ - toShaderFloatString(value, defaultValue, precisionLen = 5) { - return this.constructor.toShaderFloatString(value, defaultValue, precisionLen); - } - - /** - * Parses value to a float string representation with given precision (length after decimal) - * @param {number} value value to convert - * @param {number} defaultValue default value on failure - * @param {number} precisionLen number of decimals - * @return {string} - */ - static toShaderFloatString(value, defaultValue, precisionLen = 5) { - if (!Number.isInteger(precisionLen) || precisionLen < 0 || precisionLen > 9) { - precisionLen = 5; - } - try { - return value.toFixed(precisionLen); - } catch (e) { - return defaultValue.toFixed(precisionLen); - } - } - - /** - * Sample only one channel (which is defined in options) - * @param {string} textureCoords valid GLSL vec2 object as string - * @param {number} otherDataIndex index of the data in self.dataReference JSON array - * @param {boolean} raw whether to output raw value from the texture (do not apply filters) - * @return {string} code for appropriate texture sampling within the shader, - * where only one channel is extracted or float with zero value if - * the reference is not valid - */ - sampleChannel(textureCoords, otherDataIndex = 0, raw = false) { - let refs = this.__visualisationLayer.dataReferences; - const chan = this.__channels[otherDataIndex]; - - if (otherDataIndex >= refs.length) { - switch (chan.length) { - case 1: return ".0"; - case 2: return "vec2(.0)"; - case 3: return "vec3(.0)"; - default: - return 'vec4(0.0)'; - } - } - let sampled = `${this.webglContext.sampleTexture(refs[otherDataIndex], textureCoords)}.${chan}`; - // if (raw) return sampled; - // return this.filter(sampled); - return sampled; - } - - /** - * For error detection, how many textures are available - * @return {number} number of textures available - */ - dataSourcesCount() { - return this.__visualisationLayer.dataReferences.length; - } - - /** - * Load value, useful for controls value caching - * @param {string} name value name - * @param {string} defaultValue default value if no stored value available - * @return {string} stored value or default value - */ - loadProperty(name, defaultValue) { - let selfType = this.constructor.type(); - if (!this.__visualisationLayer) { - return defaultValue; - } - - const value = this.__visualisationLayer.cache[selfType][name]; - return value === undefined ? defaultValue : value; - } - - /** - * Store value, useful for controls value caching - * @param {string} name value name - * @param {*} value value - */ - storeProperty(name, value) { - this.__visualisationLayer.cache[this.constructor.type()][name] = value; - } - - /** - * Evaluates option flag, e.g. any value that indicates boolean 'true' - * @param {*} value value to interpret - * @return {boolean} true if the value is considered boolean 'true' - */ - isFlag(value) { - return value === "1" || value === true || value === "true"; - } - - isFlagOrMissing(value) { - return value === undefined || this.isFlag(value); - } - - /** - * Get the mode we operate in - * @return {string} mode - */ - get mode() { - return this._mode; - } - - /** - * Returns number of textures available to this shader - * @return {number} number of textures available - */ - get texturesCount() { - return this.__visualisationLayer.dataReferences.length; - } - - /** - * Set sampling channel - * @param {object} options - * @param {string} options.use_channel[X] chanel swizzling definition to sample - */ - resetChannel(options) { - const parseChannel = (name, def, sourceDef) => { - const predefined = this.constructor.defaultControls[name]; - - if (options[name] || predefined) { - let channel = predefined ? (predefined.required ? predefined.required : predefined.default) : undefined; - if (!channel) { - channel = this.loadProperty(name, options[name]); - } - - if (!channel || typeof channel !== "string" || this.constructor.__chanPattern.exec(channel) === null) { - console.warn(`Invalid channel '${name}'. Will use channel '${def}'.`, channel, options); - this.storeProperty(name, "r"); - channel = def; - } - - if (!sourceDef.acceptsChannelCount(channel.length)) { - throw `${this.constructor.name()} does not support channel length for channel: ${channel}`; - } - - if (channel !== options[name]) { - this.storeProperty(name, channel); - } - return channel; - } - return def; - }; - this.__channels = this.constructor.sources().map((source, i) => parseChannel(`use_channel${i}`, "r", source)); - } - - /** - * Set blending mode - * @param {object} options - * @param {string} options.use_mode blending mode to use: "show" or "mask" - */ - resetMode(options) { - const predefined = this.constructor.defaultControls.use_mode; - if (options["use_mode"]) { - this._mode = predefined && predefined.required; - if (!this._mode) { - this._mode = this.loadProperty("use_mode", options.use_mode); - } - - if (this._mode !== options.use_mode) { - this.storeProperty("use_mode", this._mode); - } - } else { - this._mode = predefined ? (predefined.default || "show") : "show"; - } - - this.__mode = this.constructor.modes[this._mode] || "show"; - } - - //////////////////////////////////// - ////////// PRIVATE ///////////////// - //////////////////////////////////// - - - _buildControls(options) { - let controls = this.constructor.defaultControls; - - if (controls.opacity === undefined || (typeof controls.opacity === "object" && !controls.opacity.accepts("float"))) { - controls.opacity = { - default: {type: "range", default: 1, min: 0, max: 1, step: 0.1, title: "Opacity: "}, - accepts: (type, instance) => type === "float" - }; - } - - for (let control in controls) { - let buildContext = controls[control]; - - if (buildContext) { - if (control.startsWith("use_")) { - continue; - } - - this[control] = $.WebGLModule.UIControls.build(this, control, options[control], - buildContext.default, buildContext.accepts, buildContext.required); - } - } - } - - _setContextShaderLayer(visualisationLayer) { - this.__visualisationLayer = visualisationLayer; - if (!this.__visualisationLayer.cache) { - this.__visualisationLayer.cache = {}; - } - if (!this.__visualisationLayer.cache[this.constructor.type()]) { - this.__visualisationLayer.cache[this.constructor.type()] = {}; - } - } -}; - -/** - * Declare supported controls by a particular shader - * each controls is automatically created for the shader - * and this[controlId] instance set - * structure: - * { - * controlId: { - default: {type: <>, title: <>, interactive: true|false...}, - accepts: (type, instance) => <>, - required: {type: <> ...} [OPTIONAL] - * }, ... - * } - * - * use: controlId: false to disable a specific control (e.g. all shaders - * support opacity by default - use to remove this feature) - * - * - * Additionally, use_[...] value can be specified, such controls enable shader - * to specify default or required values for built-in use_[...] params. example: - * { - * use_channel0: { - * default: "bg" - * }, - * use_channel1: { - * required: "rg" - * }, - * use_gamma: { - * default: 0.5 - * } - * } - * reads by default for texture 1 channels 'bg', second texture is always forced to read 'rg', - * textures apply gamma filter with 0.5 by default if not overridden - * todo: allow to use_[filter][X] to distinguish between textures - * - * @member {object} - */ -$.WebGLModule.ShaderLayer.defaultControls = {}; - - -/** - * todo make blending more 'nice' - * Available use_mode modes - * @type {{show: string, mask: string}} - */ -$.WebGLModule.ShaderLayer.modes = { - show: "show", - mask: "blend" -}; -$.WebGLModule.ShaderLayer.modes["mask_clip"] = "blend_clip"; //todo parser error not camel case -$.WebGLModule.ShaderLayer.__globalIncludes = {}; -$.WebGLModule.ShaderLayer.__chanPattern = new RegExp('[rgba]{1,4}'); - -/** - * Factory Manager for predefined UIControls - * - you can manage all your UI control logic within your shader implementation - * and not to touch this class at all, but here you will find some most common - * or some advanced controls ready to use, simple and powerful - * - registering an IComponent implementation (or an UiElement) in the factory results in its support - * among all the shaders (given the GLSL type, result of sample(...) matches). - * - UiElements are objects to create simple controls quickly and get rid of code duplicity, - * for more info @see OpenSeadragon.WebGLModule.UIControls.register() - * @class OpenSeadragon.WebGLModule.UIControls - */ -$.WebGLModule.UIControls = class { - - /** - * Get all available control types - * @return {string[]} array of available control types - */ - static types() { - return Object.keys(this._items).concat(Object.keys(this._impls)); - } - - /** - * Get an element used to create simple controls, if you want - * an implementation of the controls themselves (IControl), use build(...) to instantiate - * @param {string} id type of the control - * @return {*} - */ - static getUiElement(id) { - let ctrl = this._items[id]; - if (!ctrl) { - console.error("Invalid control: " + id); - ctrl = this._items["number"]; - } - return ctrl; - } - - /** - * Get an element used to create advanced controls, if you want - * an implementation of simple controls, use build(...) to instantiate - * @param {string} id type of the control - * @return {OpenSeadragon.WebGLModule.UIControls.IControl} - */ - static getUiClass(id) { - let ctrl = this._impls[id]; - if (!ctrl) { - console.error("Invalid control: " + id); - ctrl = this._impls["colormap"]; - } - return ctrl; - } - - /** - * Build UI control object based on given parameters - * @param {OpenSeadragon.WebGLModule.ShaderLayer} context owner of the control - * @param {string} name name used for the layer, should be unique among different context types - * @param {object|*} params parameters passed to the control (defined by the control) or set as default value if not object - * @param {object} defaultParams default parameters that the shader might leverage above defaults of the control itself - * @param {function} accepts required GLSL type of the control predicate, for compatibility typechecking - * @param {object} requiredParams parameters that override anything sent by user or present by defaultParams - * @return {OpenSeadragon.WebGLModule.UIControls.IControl} - */ - static build(context, name, params, defaultParams = {}, accepts = () => true, requiredParams = {}) { - //if not an object, but a value: make it the default one - if (!(typeof params === 'object')) { - params = {default: params}; - } - let originalType = defaultParams.type; - - defaultParams = $.extend(true, {}, defaultParams, params, requiredParams); - - if (!this._items[defaultParams.type]) { - if (!this._impls[defaultParams.type]) { - return this._buildFallback(defaultParams.type, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } - - let cls = new this._impls[defaultParams.type]( - context, name, `${name}_${context.uid}`, defaultParams - ); - if (accepts(cls.type, cls)) { - return cls; - } - return this._buildFallback(defaultParams.type, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } else { - let contextComponent = this.getUiElement(defaultParams.type); - let comp = new $.WebGLModule.UIControls.SimpleUIControl( - context, name, `${name}_${context.uid}`, defaultParams, contextComponent - ); - if (accepts(comp.type, comp)) { - return comp; - } - return this._buildFallback(contextComponent.glType, originalType, context, - name, params, defaultParams, accepts, requiredParams); - } - } - - /** - * Register simple UI element by providing necessary object - * implementation: - * { defaults: function() {...}, // object with all default values for all supported parameters - html: function(uniqueId, params, css="") {...}, //how the HTML UI controls look like - glUniformFunName: function() {...}, //what function webGL uses to pass this attribute to GPU - decode: function(fromValue) {...}, //parse value obtained from HTML controls into something - gl[glUniformFunName()](...) can pass to GPU - glType: //what's the type of this parameter wrt. GLSL: int? vec3? - * @param type the identifier under which is this control used: lookup made against params.type - * @param uiElement the object to register, fulfilling the above-described contract - */ - static register(type, uiElement) { - function check(el, prop, desc) { - if (!el[prop]) { - console.warn(`Skipping UI control '${type}' due to '${prop}': missing ${desc}.`); - return false; - } - return true; - } - - if (check(uiElement, "defaults", "defaults():object") && - check(uiElement, "html", "html(uniqueId, params, css):htmlString") && - check(uiElement, "glUniformFunName", "glUniformFunName():string") && - check(uiElement, "decode", "decode(encodedValue):") && - check(uiElement, "normalize", "normalize(value, params):") && - check(uiElement, "sample", "sample(value, valueGlType):glslString") && - check(uiElement, "glType", "glType:string") - ) { - uiElement.prototype.getName = () => type; - if (this._items[type]) { - console.warn("Registering an already existing control component: ", type); - } - uiElement["uiType"] = type; - this._items[type] = uiElement; - } - } - - /** - * Register class as a UI control - * @param {string} type unique control name / identifier - * @param {OpenSeadragon.WebGLModule.UIControls.IControl} cls to register, implementation class of the controls - */ - static registerClass(type, cls) { - //todo not really possible with syntax checker :/ - // if ($.WebGLModule.UIControls.IControl.isPrototypeOf(cls)) { - cls.prototype.getName = () => type; - - if (this._items[type]) { - console.warn("Registering an already existing control component: ", type); - } - cls._uiType = type; - this._impls[type] = cls; - // } else { - // console.warn(`Skipping UI control '${type}': does not inherit from $.WebGLModule.UIControls.IControl.`); - // } - } - - ///////////////////////// - /////// PRIVATE ///////// - ///////////////////////// - - - static _buildFallback(newType, originalType, context, name, params, defaultParams, requiredType, requiredParams) { - //repeated check when building object from type - - params.interactive = false; - if (originalType === newType) { //if default and new equal, fail - recursion will not help - console.error(`Invalid parameter in shader '${params.type}': the parameter could not be built.`); - return undefined; - } else { //otherwise try to build with originalType (default) - params.type = originalType; - console.warn("Incompatible UI control type '" + newType + "': making the input non-interactive."); - return this.build(context, name, params, defaultParams, requiredType, requiredParams); - } - } -}; - -//implementation of UI control classes -//more complex functionality -$.WebGLModule.UIControls._impls = { - //colormap: $.WebGLModule.UIControls.ColorMap -}; -//implementation of UI control objects -//simple functionality -$.WebGLModule.UIControls._items = { - number: { - defaults: function() { - return {title: "Number", interactive: true, default: 0, min: 0, max: 100, step: 1}; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1f"; - }, - decode: function(fromValue) { - return Number.parseFloat(fromValue); - }, - normalize: function(value, params) { - return (value - params.min) / (params.max - params.min); - }, - sample: function(name, ratio) { - return name; - }, - glType: "float", - uiType: "number" - }, - - range: { - defaults: function() { - return {title: "Range", interactive: true, default: 0, min: 0, max: 100, step: 1}; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1f"; - }, - decode: function(fromValue) { - return Number.parseFloat(fromValue); - }, - normalize: function(value, params) { - return (value - params.min) / (params.max - params.min); - }, - sample: function(name, ratio) { - return name; - }, - glType: "float", - uiType: "range" - }, - - color: { - defaults: function() { - return { title: "Color", interactive: true, default: "#fff900" }; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - return `${title}`; - }, - glUniformFunName: function() { - return "uniform3fv"; - }, - decode: function(fromValue) { - try { - let index = fromValue.startsWith("#") ? 1 : 0; - return [ - parseInt(fromValue.slice(index, index + 2), 16) / 255, - parseInt(fromValue.slice(index + 2, index + 4), 16) / 255, - parseInt(fromValue.slice(index + 4, index + 6), 16) / 255 - ]; - } catch (e) { - return [0, 0, 0]; - } - }, - normalize: function(value, params) { - return value; - }, - sample: function(name, ratio) { - return name; - }, - glType: "vec3", - uiType: "color" - }, - - bool: { - defaults: function() { - return { title: "Checkbox", interactive: true, default: true }; - }, - html: function(uniqueId, params, css = "") { - let title = params.title ? ` ${params.title}` : ""; - let value = this.decode(params.default) ? "checked" : ""; - //note a bit dirty, but works :) - we want uniform access to 'value' property of all inputs - return `${title}`; - }, - glUniformFunName: function() { - return "uniform1i"; - }, - decode: function(fromValue) { - return fromValue && fromValue !== "false" ? 1 : 0; - }, - normalize: function(value, params) { - return value; - }, - sample: function(name, ratio) { - return name; - }, - glType: "bool", - uiType: "bool" - } -}; - -/** - * @interface - */ -$.WebGLModule.UIControls.IControl = class { - - /** - * Sets common properties needed to create the controls: - * this.context @extends WebGLModule.ShaderLayer - owner context - * this.name - name of the parameter for this.context.[load/store]Property(...) call - * this.id - unique ID for HTML id attribute, to be able to locate controls in DOM, - * created as ${uniq}${name}-${context.uid} - * this.webGLVariableName - unique webgl uniform variable name, to not to cause conflicts - * - * If extended (class-based definition, see registerCass) children should define constructor as - * - * @example - * constructor(context, name, webGLVariableName, params) { - * super(context, name, webGLVariableName); - * ... - * //possibly make use of params: - * this.params = this.getParams(params); - * - * //now access params: - * this.params... - * } - * - * @param {WebGLModule.ShaderLayer} context shader context owning this control - * @param {string} name name of the control (key to the params in the shader configuration) - * @param {string} webGLVariableName configuration parameters, - * depending on the params.type field (the only one required) - * @param {string} uniq another element to construct the DOM id from, mostly for compound controls - */ - constructor(context, name, webGLVariableName, uniq = "") { - this.context = context; - this.id = `${uniq}${name}-${context.uid}`; - this.name = name; - this.webGLVariableName = webGLVariableName; - this._params = {}; - this.__onchange = {}; - } - - /** - * Safely sets outer params with extension from 'supports' - * - overrides 'supports' values with the correct type (derived from supports or supportsAll) - * - sets 'supports' as defaults if not set - * @param params - */ - getParams(params) { - const t = this.constructor.getVarType; - function mergeSafeType(mask, from, possibleTypes) { - const to = Object.assign({}, mask); - Object.keys(from).forEach(key => { - const tVal = to[key], - fVal = from[key], - tType = t(tVal), - fType = t(fVal); - - const typeList = possibleTypes ? possibleTypes[key] : undefined, - pTypeList = typeList ? typeList.map(x => t(x)) : []; - - //our type detector distinguishes arrays and objects - if (tVal && fVal && tType === "object" && fType === "object") { - to[key] = mergeSafeType(tVal, fVal, typeList); - } else if (tVal === undefined || tType === fType || pTypeList.includes(fType)) { - to[key] = fVal; - } else if (fType === "string") { - //try parsing NOTE: parsing from supportsAll is ignored! - if (tType === "number") { - const parsed = Number.parseFloat(fVal); - if (!Number.isNaN(parsed)) { - to[key] = parsed; - } - } else if (tType === "boolean") { - const value = fVal.toLowerCase(); - if (value === "false") { - to[key] = false; - } - if (value === "true") { - to[key] = true; - } - } - } - }); - return to; - } - return mergeSafeType(this.supports, params, this.supportsAll); - } - - /** - * Safely check certain param value - * @param value value to check - * @param defaultValue default value to return if check fails - * @param paramName name of the param to check value type against - * @return {boolean|number|*} - */ - getSafeParam(value, defaultValue, paramName) { - const t = this.constructor.getVarType; - function nest(suppNode, suppAllNode) { - if (t(suppNode) !== "object") { - return [suppNode, suppAllNode]; - } - if (!suppNode[paramName]) { - return [undefined, undefined]; - } - return nest(suppNode[paramName], suppAllNode ? suppAllNode[paramName] : undefined); - } - const param = nest(this.supports, this.supportsAll), - tParam = t(param[0]); - - if (tParam === "object") { - console.warn("Parameters should not be stored at object level. No type inspection is done."); - return true; //no supported inspection - } - const tValue = t(value); - //supported type OR supports all types includes the type - if (tValue === tParam || (param[1] && param[1].map(t).includes(tValue))) { - return value; - } - - if (tValue === "string") { - //try parsing NOTE: parsing from supportsAll is ignored! - if (tParam === "number") { - const parsed = Number.parseFloat(value); - if (!Number.isNaN(parsed)) { - return parsed; - } - } else if (tParam === "boolean") { - const val = value.toLowerCase(); - if (val === "false") { - return false; - } - if (val === "true") { - return true; - } - } - } - - //todo test - console.debug("Failed to load safe param -> new feature, debugging! ", value, defaultValue, paramName); - return defaultValue; - } - - /** - * Uniform behaviour wrt type checking in shaders - * @param x - * @return {string} - */ - static getVarType(x) { - if (x === undefined) { - return "undefined"; - } - if (x === null) { - return "null"; - } - return Array.isArray(x) ? "array" : typeof x; - } - - /** - * JavaScript initialization - * - read/store default properties here using this.context.[load/store]Property(...) - * - work with own HTML elements already attached to the DOM - * - set change listeners, input values! - */ - init() { - throw "WebGLModule.UIControls.IControl::init() must be implemented."; - } - - /** - * TODO: improve overall setter API - * Allows to set the control value programatically. - * Does not trigger canvas re-rednreing, must be done manually (e.g. control.context.invalidate()) - * @param encodedValue any value the given control can support, encoded - * (e.g. as the control acts on the GUI - for input number of - * values between 5 and 42, the value can be '6' or 6 or 6.15 - */ - set(encodedValue) { - throw "WebGLModule.UIControls.IControl::set() must be implemented."; - } - - /** - * Called when an image is rendered - * @param program WebglProgram instance - * @param {WebGLRenderingContextBase} gl - */ - glDrawing(program, gl) { - //the control should send something to GPU - throw "WebGLModule.UIControls.IControl::glDrawing() must be implemented."; - } - - /** - * Called when associated webgl program is switched to - * @param program WebglProgram instance - * @param gl WebGL Context - */ - glLoaded(program, gl) { - //the control should send something to GPU - throw "WebGLModule.UIControls.IControl::glLoaded() must be implemented."; - } - - /** - * Get the UI HTML controls - * - these can be referenced in this.init(...) - * - should respect this.params.interactive attribute and return non-interactive output if interactive=false - * - don't forget to no to work with DOM elements in init(...) in this case - */ - toHtml(breakLine = true, controlCss = "") { - throw "WebGLModule.UIControls.IControl::toHtml() must be implemented."; - } - - /** - * Handles how the variable is being defined in GLSL - * - should use variable names derived from this.webGLVariableName - */ - define() { - throw "WebGLModule.UIControls.IControl::define() must be implemented."; - } - - /** - * Sample the parameter using ratio as interpolation, must be one-liner expression so that GLSL code can write - * `vec3 mySampledValue = ${this.color.sample("0.2")};` - * NOTE: you can define your own global-scope functions to keep one-lined sampling, - * see this.context.includeGlobalCode(...) - * @param {(string|undefined)} value openGL value/variable, used in a way that depends on the UI control currently active - * (do not pass arguments, i.e. 'undefined' just get that value, note that some inputs might require you do it..) - * @param {string} valueGlType GLSL type of the value - * @return {string} valid GLSL oneliner (wihtout ';') for sampling the value, or invalid code (e.g. error message) to signal error - */ - sample(value = undefined, valueGlType = 'void') { - throw "WebGLModule.UIControls.IControl::sample() must be implemented."; - } - - /** - * Parameters supported by this UI component, must contain at least - * - 'interactive' - type bool, enables and disables the control interactivity - * (by changing the content available when rendering html) - * - 'title' - type string, the control title - * - * Additionally, for compatibility reasons, you should, if possible, define - * - 'default' - type any; the default value for the particular control - * @return {{}} name: default value mapping - */ - get supports() { - throw "WebGLModule.UIControls.IControl::supports must be implemented."; - } - - /** - * Type definitions for supports. Can return empty object. In case of missing - * type definitions, the type is derived from the 'supports()' default value type. - * - * Each key must be an array of default values for the given key if applicable. - * This is an _extension_ to the supports() and can be used only for keys that have more - * than one default type applicable - * @return {{}} - */ - get supportsAll() { - throw "WebGLModule.UIControls.IControl::typeDefs must be implemented."; - } - - /** - * GLSL type of this control: what type is returned from this.sample(...) ? - * @return {string} - */ - get type() { - throw "WebGLModule.UIControls.IControl::type must be implemented."; - } - - /** - * Raw value sent to the GPU, note that not necessarily typeof raw() === type() - * some controls might send whole arrays of data (raw) and do smart sampling such that type is only a number - * @return {any} - */ - get raw() { - throw "WebGLModule.UIControls.IControl::raw must be implemented."; - } - - /** - * Encoded value as used in the UI, e.g. a name of particular colormap, or array of string values of breaks... - * @return {any} - */ - get encoded() { - throw "WebGLModule.UIControls.IControl::encoded must be implemented."; - } - - ////////////////////////////////////// - //////// COMMON API ////////////////// - ////////////////////////////////////// - - /** - * The control type component was registered with. Handled internally. - * @return {*} - */ - get uiControlType() { - return this.constructor._uiType; - } - - /** - * Get current control parameters - * the control should set the value as this._params = this.getParams(incomingParams); - * @return {{}} - */ - get params() { - return this._params; - } - - /** - * Automatically overridden to return the name of the control it was registered with - * @return {string} - */ - getName() { - return "IControl"; - } - - /** - * Load a value from cache to support its caching - should be used on all values - * that are available for the user to play around with and change using UI controls - * - * @param defaultValue value to return in case of no cached value - * @param paramName name of the parameter, must be equal to the name from 'supports' definition - * - default value can be empty string - * @return {*} cached or default value - */ - load(defaultValue, paramName = "") { - if (paramName === "default") { - paramName = ""; - } - const value = this.context.loadProperty(this.name + paramName, defaultValue); - //check param in case of input cache collision between shader types - return this.getSafeParam(value, defaultValue, paramName === "" ? "default" : paramName); - } - - /** - * Store a value from cache to support its caching - should be used on all values - * that are available for the user to play around with and change using UI controls - * - * @param value to store - * @param paramName name of the parameter, must be equal to the name from 'supports' definition - * - default value can be empty string - */ - store(value, paramName = "") { - if (paramName === "default") { - paramName = ""; - } - return this.context.storeProperty(this.name + paramName, value); - } - - /** - * On parameter change register self - * @param {string} event which event to fire on - * - events are with inputs the names of supported parameters (this.supports), separated by dot if nested - * - most controls support "default" event - change of default value - * - see specific control implementation to see what events are fired (Advanced Slider fires "breaks" and "mask" for instance) - * @param {function} clbck(rawValue, encodedValue, context) call once change occurs, context is the control instance - */ - on(event, clbck) { - this.__onchange[event] = clbck; //only one possible event -> rewrite? - } - - /** - * Clear events of the event type - * @param {string} event type - */ - off(event) { - delete this.__onchange[event]; - } - - /** - * Clear ALL events - */ - clearEvents() { - this.__onchange = {}; - } - - /** - * Invoke changed value event - * -- should invoke every time a value changes !driven by USER!, and use unique or compatible - * event name (event 'value') so that shader knows what changed - * @param event event to call - * @param value decoded value of encodedValue - * @param encodedValue value that was received from the UI input - * @param context self reference to bind to the callback - */ - changed(event, value, encodedValue, context) { - if (typeof this.__onchange[event] === "function") { - this.__onchange[event](value, encodedValue, context); - } - } - - -}; - - -/** - * Generic UI control implementations - * used if: - * { - * type: "CONTROL TYPE", - * ... - * } - * - * The subclass constructor should get the context reference, the name - * of the input and the parametrization. - * - * Further parameters passed are dependent on the control type, see - * @ WebGLModule.UIControls - * - * @class WebGLModule.UIControls.SimpleUIControl - */ -$.WebGLModule.UIControls.SimpleUIControl = class extends $.WebGLModule.UIControls.IControl { - - //uses intristicComponent that holds all specifications needed to work with the component uniformly - constructor(context, name, webGLVariableName, params, intristicComponent, uniq = "") { - super(context, name, webGLVariableName, uniq); - this.component = intristicComponent; - this._params = this.getParams(params); - - this.encodedValue = this.load(this.params.default); - //this unfortunatelly makes cache erasing and rebuilding vis impossible, the shader part has to be fully re-instantiated - this.params.default = this.encodedValue; - } - - init() { - this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params); - - if (this.params.interactive) { - const _this = this; - let node = document.getElementById(this.id); - if (node) { - let updater = function(e) { - _this.set(e.target.value); - _this.context.invalidate(); - }; - node.value = this.encodedValue; - node.addEventListener('change', updater); - } - } - } - - set(encodedValue) { - this.encodedValue = encodedValue; - this.value = this.component.normalize(this.component.decode(this.encodedValue), this.params); - this.changed("default", this.value, this.encodedValue, this); - this.store(this.encodedValue); - } - - glDrawing(program, gl) { - gl[this.component.glUniformFunName()](this.glLocation, this.value); - } - - glLoaded(program, gl) { - this.glLocation = gl.getUniformLocation(program, this.webGLVariableName); - } - - toHtml(breakLine = true, controlCss = "") { - if (!this.params.interactive) { - return ""; - } - const result = this.component.html(this.id, this.params, controlCss); - return breakLine ? `
${result}
` : result; - } - - define() { - return `uniform ${this.component.glType} ${this.webGLVariableName};`; - } - - sample(value = undefined, valueGlType = 'void') { - if (!value || valueGlType !== 'float') { - return this.webGLVariableName; - } - return this.component.sample(this.webGLVariableName, value); - } - - get uiControlType() { - return this.component["uiType"]; - } - - get supports() { - return this.component.defaults(); - } - - get supportsAll() { - return {}; - } - - get raw() { - return this.value; - } - - get encoded() { - return this.encodedValue; - } - - get type() { - return this.component.glType; - } -}; -})(OpenSeadragon); diff --git a/src/webgl/webGLContext.js b/src/webgl/webGLContext.js deleted file mode 100644 index 1064f5e1..00000000 --- a/src/webgl/webGLContext.js +++ /dev/null @@ -1,689 +0,0 @@ -(function($) { - -$.WebGLModule.determineContext = function( version ){ - const namespace = OpenSeadragon.WebGLModule; - for (let property in namespace) { - const context = namespace[ property ], - proto = context.prototype; - if( proto && - proto instanceof namespace.WebGLImplementation && - $.isFunction( proto.getVersion ) && - proto.getVersion.call( context ) === version - ){ - return context; - } - } - return null; -}; - -function iterate(n) { - let result = Array(n), - it = 0; - while (it < n) { - result[it] = it++; - } - return result; -} - -/** - * @interface OpenSeadragon.WebGLModule.webglContext - * Interface for the visualisation rendering implementation which can run - * on various GLSL versions - */ -$.WebGLModule.WebGLImplementation = class { - - /** - * Create a WebGL Renderer Context Implementation (version-dependent) - * @param {WebGLModule} renderer - * @param {WebGLRenderingContext|WebGL2RenderingContext} gl - * @param webglVersion - * @param {object} options - * @param {GLuint} options.wrap texture wrap parameteri - * @param {GLuint} options.magFilter texture filter parameteri - * @param {GLuint} options.minFilter texture filter parameteri - */ - constructor(renderer, gl, webglVersion, options) { - //Set default blending to be MASK - this.renderer = renderer; - this.gl = gl; - this.options = options; - } - - /** - * Static context creation (to avoid class instantiation in case of missing support) - * @param canvas - * @param options desired options used in the canvas webgl context creation - * @return {WebGLRenderingContextBase} //todo base is not common to all, remove from docs - */ - static create(canvas, options) { - throw("::create() must be implemented!"); - } - - /** - * @return {string} WebGL version used - */ - getVersion() { - return "undefined"; - } - - /** - * Get GLSL texture sampling code - * @return {string} GLSL code that is correct in texture sampling wrt. WebGL version used - */ - get texture() { - return this._texture; - } - - getCompiled(program, name) { - throw("::getCompiled() must be implemented!"); - } - - /** - * Create a visualisation from the given JSON params - * @param program - * @param {string[]} order keys of visualisation.shader in which order to build the visualization - * the order: painter's algorithm: the last drawn is the most visible - * @param {object} visualisation - * @param {object} options - * @param {boolean} options.withHtml whether html should be also created (false if no UI controls are desired) - * @param {string} options.textureType id of texture to be used, supported are TEXTURE_2D, TEXTURE_2D_ARRAY, TEXTURE_3D - * @param {string} options.instanceCount number of instances to draw at once - * @return {number} amount of usable shaders - */ - compileSpecification(program, order, visualisation, options) { - throw("::compileSpecification() must be implemented!"); - } - - /** - * Called once program is switched to: initialize all necessary items - * @param {WebGLProgram} program used program - * @param {OpenSeadragon.WebGLModule.RenderingConfig?} currentConfig JSON parameters used for this visualisation - */ - programLoaded(program, currentConfig = null) { - throw("::programLoaded() must be implemented!"); - } - - /** - * Draw on the canvas using given program - * @param {WebGLProgram} program used program - * @param {OpenSeadragon.WebGLModule.RenderingConfig?} currentConfig JSON parameters used for this visualisation - * @param {GLuint} texture - * @param {object} tileOpts - * @param {number} tileOpts.zoom value passed to the shaders as zoom_level - * @param {number} tileOpts.pixelSize value passed to the shaders as pixel_size_in_fragments - * @param {OpenSeadragon.Mat3|[OpenSeadragon.Mat3]} tileOpts.transform position transform - * @param {number?} tileOpts.instanceCount how many instances to draw in case instanced drawing is enabled - * matrix or flat matrix array (instance drawing) - */ - programUsed(program, currentConfig, texture, tileOpts = {}) { - throw("::programUsed() must be implemented!"); - } - - sampleTexture(index, vec2coords) { - throw("::sampleTexture() must be implemented!"); - } - - /** - * - * @param {WebGLProgram} program - * @param definition - * @param execution - * @param {object} options - * @param {string} options.textureType id of texture to be used, supported are TEXTURE_2D, TEXTURE_2D_ARRAY, TEXTURE_3D - * @param {string} options.instanceCount number of instances to draw at once - */ - compileFragmentShader(program, definition, execution, options) { - throw("::compileFragmentShader() must be implemented!"); - } - - /** - * - * @param {WebGLProgram} program - * @param definition - * @param execution - * @param {object} options - * @param {string} options.textureType id of texture to be used, supported are TEXTURE_2D, TEXTURE_2D_ARRAY, TEXTURE_3D - * @param {string} options.instanceCount number of instances to draw at once - */ - compileVertexShader(program, definition, execution, options) { - throw("::compileVertexShader() must be implemented!"); - } - - /** - * Code to be included only once, required by given shader type (keys are considered global) - * @param {string} type shader type - * @returns {object} global-scope code used by the shader in format - */ - globalCodeRequiredByShaderType(type) { - return $.WebGLModule.ShaderMediator.getClass(type).__globalIncludes; - } - - /** - * Blend equation sent from the outside, must be respected - * @param glslCode code for blending, using two variables: 'foreground', 'background' - * @example - * //The shader context must define the following: - * - * vec4 some_blending_name_etc(in vec4 background, in vec4 foreground) { - * // << glslCode >> - * } - * - * void blend_clip(vec4 input) { - * //for details on clipping mask approach see show() below - * // <> - * } - * - * void blend(vec4 input) { //must be called blend, API - * // <> - * } - * - * //Also, default alpha blending equation 'show' must be implemented: - * void show(vec4 color) { - * //pseudocode - * //note that the blending output should not immediatelly work with 'color' but perform caching of the color, - * //render the color given in previous call and at the execution end of main call show(vec4(.0)) - * //this way, the previous color is not yet blended for the next layer show/blend/blend_clip which can use it to create a clipping mask - * - * compute t = color.a + background.a - color.a*background.a; - * output vec4((color.rgb * color.a + background.rgb * background.a - background.rgb * (background.a * color.a)) / t, t) - * } - */ - setBlendEquation(glslCode) { - this.glslBlendCode = glslCode; - } - - _compileProgram(program, onError) { - const gl = this.gl; - function ok (kind, status, value, sh) { - if (!gl['get' + kind + 'Parameter'](value, gl[status + '_STATUS'])) { - $.console.error((sh || 'LINK') + ':\n' + gl['get' + kind + 'InfoLog'](value)); - return false; - } - return true; - } - - function useShader(gl, program, data, type) { - let shader = gl.createShader(gl[type]); - gl.shaderSource(shader, data); - gl.compileShader(shader); - gl.attachShader(program, shader); - program[type] = shader; - return ok('Shader', 'COMPILE', shader, type); - } - - function numberLines(str) { - //https://stackoverflow.com/questions/49714971/how-to-add-line-numbers-to-beginning-of-each-line-in-string-in-javascript - return str.split('\n').map((line, index) => `${index + 1} ${line}`).join('\n'); - } - - const opts = program._osdOptions; - if (!opts) { - $.console.error("Invalid program compilation! Did you build shaders using compile[Type]Shader() methods?"); - onError("Invalid program.", "Program not compatible with this renderer!"); - return; - } - - if (!useShader(gl, program, opts.vs, 'VERTEX_SHADER') || - !useShader(gl, program, opts.fs, 'FRAGMENT_SHADER')) { - onError("Unable to use this specification.", - "Compilation of shader failed. For more information, see logs in the $.console."); - $.console.warn("VERTEX SHADER\n", numberLines( opts.vs )); - $.console.warn("FRAGMENT SHADER\n", numberLines( opts.fs )); - } else { - gl.linkProgram(program); - if (!ok('Program', 'LINK', program)) { - onError("Unable to use this specification.", - "Linking of shader failed. For more information, see logs in the $.console."); - } else { //if (this.renderer.debug) { //todo uncomment in production - $.console.info("VERTEX SHADER\n", numberLines( opts.vs )); - $.console.info("FRAGMENT SHADER\n", numberLines( opts.fs )); - } - } - } -}; - -$.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { - /** - * - * @param {OpenSeadragon.WebGLModule} renderer - * @param {WebGL2RenderingContext} gl - * @param options - */ - constructor(renderer, gl, options) { - super(renderer, gl, "2.0", options); - - // this.vao = gl.createVertexArray(); - this._bufferTexturePosition = gl.createBuffer(); - - - // Create a texture. - this.glyphTex = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this.glyphTex); -// Fill the texture with a 1x1 blue pixel. - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, - new Uint8Array([0, 0, 255, 255])); -// Asynchronously load an image - var image = new Image(); - image.src = "8x8-font.png"; - - const _this = this; - image.addEventListener('load', function() { - // Now that the image has loaded make copy it to the texture. - gl.bindTexture(gl.TEXTURE_2D, _this.glyphTex); - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); - 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); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - }); - } - - getVersion() { - return "2.0"; - } - - static create(canvas, options) { - options.alpha = true; - options.premultipliedAlpha = true; - return canvas.getContext('webgl2', options); - } - - getCompiled(program, name) { - return program._osdOptions[name]; - } - - //todo try to implement on the global scope version-independntly - compileSpecification(program, order, specification, options) { - var definition = "", - execution = "", - html = "", - _this = this, - usableShaders = 0, - dataCount = 0, - globalScopeCode = {}; - - order.forEach(dataId => { - let layer = specification.shaders[dataId]; - layer.rendering = false; - - if (layer.type === "none") { - //prevents the layer from being accounted for - layer.error = "Not an error - layer type none."; - } else if (layer.error) { - if (options.withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, layer.error, dataId, false, layer, false) + html; - } - $.console.warn(layer.error, layer["desc"]); - - } else if (layer._renderContext && (layer._index || layer._index === 0)) { - //todo consider html generating in the renderer - let visible = false; - usableShaders++; - - //make visible textures if 'visible' flag set - //todo either allways visible or ensure textures do not get loaded - if (layer.visible) { - let renderCtx = layer._renderContext; - definition += renderCtx.getFragmentShaderDefinition() + ` -vec4 lid_${layer._index}_xo() { - ${renderCtx.getFragmentShaderExecution()} -}`; - if (renderCtx.opacity) { - execution += ` - vec4 l${layer._index}_out = lid_${layer._index}_xo(); - l${layer._index}_out.a *= ${renderCtx.opacity.sample()}; - blend(l${layer._index}_out, ${renderCtx._blendUniform}, ${renderCtx._clipUniform});`; - } else { - execution += ` - blend(lid_${layer._index}_xo(), ${renderCtx._blendUniform}, ${renderCtx._clipUniform});`; //todo remove ${renderCtx.__mode} - } - - layer.rendering = true; - visible = true; - $.extend(globalScopeCode, _this.globalCodeRequiredByShaderType(layer.type)); - dataCount += layer.dataReferences.length; - } - - //reverse order append to show first the last drawn element (top) - if (options.withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, - layer._renderContext.htmlControls(), dataId, visible, layer, true) + html; - } - } else { - if (options.withHtml) { - html = _this.renderer.htmlShaderPartHeader(layer.name, - `The requested specification type does not work properly.`, dataId, false, layer, false) + html; - } - $.console.warn("Invalid shader part.", "Missing one of the required elements.", layer); - } - }); - - if (!options.textureType) { - if (dataCount === 1) { - options.textureType = "TEXTURE_2D"; - } - if (dataCount > 1) { - options.textureType = "TEXTURE_2D_ARRAY"; - } - } - - options.html = html; - options.dataUrls = this.renderer._dataSources; - options.onError = function(message, description) { - specification.error = message; - specification.desc = description; - }; - - const matrixType = options.instanceCount > 2 ? "in" : "uniform"; - - //hack use 'invalid' key to attach item - globalScopeCode[null] = definition; - this.compileVertexShader( - program, ` -${matrixType} mat3 osd_transform_matrix; -const vec3 quad[4] = vec3[4] ( - vec3(0.0, 1.0, 1.0), - vec3(0.0, 0.0, 1.0), - vec3(1.0, 1.0, 1.0), - vec3(1.0, 0.0, 1.0) -);`, ` - gl_Position = vec4(osd_transform_matrix * quad[gl_VertexID], 1);`, options); - this.compileFragmentShader( - program, - Object.values(globalScopeCode).join("\n"), - execution, - options); - - return usableShaders; - } - - getTextureSampling(options) { - const type = options.textureType; - if (!type) { //no texture is also allowed option todo test if valid, defined since we read its location - return ` -ivec2 osd_texture_size() { - return ivec2(0); -} -uniform sampler2D _vis_data_sampler[0]; -vec4 osd_texture(int index, vec2 coords) { - return vec(.0); -}`; - } - const numOfTextures = options.instanceCount = - Math.max(options.instanceCount || 0, 1); - - function samplingCode(coords) { - if (numOfTextures === 1) { - return `return texture(_vis_data_sampler[0], ${coords});`; - } - //sampling hardcode switch to sample with constant indexes - return `switch(osd_texture_id) { - ${iterate(options.instanceCount).map(i => ` - case ${i}: - return texture(_vis_data_sampler[${i}], ${coords});`).join("")} - } - return vec4(1.0);`; - } - - //todo consider sampling with vec3 for universality - if (type === "TEXTURE_2D") { - return ` -uniform sampler2D _vis_data_sampler[${numOfTextures}]; -ivec2 osd_texture_size() { - return textureSize(_vis_data_sampler[0], 0); -} -vec4 osd_texture(int index, vec2 coords) { - ${samplingCode('coords')} -}`; - } - if (type === "TEXTURE_2D_ARRAY") { - return ` -uniform sampler2DArray _vis_data_sampler[${numOfTextures}]; -ivec2 osd_texture_size() { - return textureSize(_vis_data_sampler[0], 0).xy; -} -vec4 osd_texture(int index, vec2 coords) { - ${samplingCode('vec3(coords, index)')} -}`; - } else if (type === "TEXTURE_3D") { - //todo broken api, but pointless sending vec2 with 3d tex - return ` -uniform sampler3D _vis_data_sampler[${numOfTextures}]; -ivec3 osd_texture_size() { - return textureSize(_vis_data_sampler[0], 0).xy; -} -vec4 osd_texture(int index, vec2 coords) { - ${samplingCode('vec3(coords, index)')} -}`; - } - return 'Error: invalid texture: unsupported sampling type ' + type; - } - - sampleTexture(index, vec2coords) { - return `osd_texture(${index}, ${vec2coords})`; - } - - compileFragmentShader(program, definition, execution, options) { - const debug = options.debug ? ` - float twoPixels = 1.0 / float(osd_texture_size().x) * 2.0; - vec2 distance = abs(osd_texture_bounds - osd_texture_coords); - if (distance.x <= twoPixels || distance.y <= twoPixels) { - final_color = vec4(1.0, .0, .0, 1.0); - return; - } -` : ""; - - options.fs = `#version 300 es -precision mediump float; -precision mediump sampler2DArray; -precision mediump sampler2D; -precision mediump sampler3D; - -uniform float pixel_size_in_fragments; -uniform float zoom_level; - -in vec2 osd_texture_coords; -flat in vec2 osd_texture_bounds; -flat in int osd_texture_id; - -${this.getTextureSampling(options)} - -out vec4 final_color; - -vec4 _last_rendered_color = vec4(.0); - -bool close(float value, float target) { - return abs(target - value) < 0.001; -} - -int _last_mode = 0; -bool _last_clip = false; -void blend(vec4 color, int mode, bool clip) { - //premultiplied alpha blending - //if (_last_clip) { - // todo - //} else { - vec4 fg = _last_rendered_color; - vec4 pre_fg = vec4(fg.rgb * fg.a, fg.a); - - if (_last_mode == 0) { - final_color = pre_fg + (1.0-fg.a)*final_color; - } else if (_last_mode == 1) { - final_color = vec4(pre_fg.rgb * final_color.rgb, pre_fg.a + final_color.a); - } else { - final_color = vec4(.0, .0, 1.0, 1.0); - } - //} - _last_rendered_color = color; - _last_mode = mode; - _last_clip = clip; -} - -${definition} - -void main() { - ${debug} - - ${execution} - - //blend last level - blend(vec4(.0), 0, false); -}`; - if (options.vs) { - program._osdOptions = options; - this._compileProgram(program, options.onError || $.console.error); - delete options.fs; - delete options.vs; - } - } - - compileVertexShader(program, definition, execution, options) { - const textureId = options.instanceCount > 1 ? 'gl_InstanceID' : '0'; - - options.vs = `#version 300 es -precision mediump float; -in vec2 osd_tile_texture_position; -flat out int osd_texture_id; -out vec2 osd_texture_coords; -flat out vec2 osd_texture_bounds; - -${definition} - -void main() { - osd_texture_id = ${textureId}; - // vec3 vertex = quad[gl_VertexID]; - // vec2 texCoords = vec2(vertex.x, -vertex.y); - // osd_texture_coords = texCoords; - // osd_texture_bounds = texCoords; - - osd_texture_coords = osd_tile_texture_position; - osd_texture_bounds = osd_tile_texture_position; - ${execution} -} -`; - if (options.fs) { - program._osdOptions = options; - this._compileProgram(program, options.onError || $.console.error); - delete options.fs; - delete options.vs; - } - } - - programLoaded(program, currentConfig = null) { - if (!this.renderer.running) { - return; - } - - const gl = this.gl; - // Allow for custom loading - gl.useProgram(program); - if (currentConfig) { - this.renderer.glLoaded(gl, program, currentConfig); - } - - // gl.bindVertexArray(this.vao); - - this._locationPixelSize = gl.getUniformLocation(program, "pixel_size_in_fragments"); - this._locationZoomLevel = gl.getUniformLocation(program, "zoom_level"); - - const options = program._osdOptions; - if (options.instanceCount > 1) { - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferTexturePosition); - this._locationTexturePosition = gl.getAttribLocation(program, 'osd_tile_texture_position'); - //vec2 * 4 bytes per element - const vertexSizeByte = 2 * 4; - gl.bufferData(gl.ARRAY_BUFFER, options.instanceCount * 4 * vertexSizeByte, gl.STREAM_DRAW); - gl.enableVertexAttribArray(this._locationTexturePosition); - gl.vertexAttribPointer(this._locationTexturePosition, 2, gl.FLOAT, false, 0, 0); - gl.vertexAttribDivisor(this._locationTexturePosition, 0); - - this._bufferMatrices = this._bufferMatrices || gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferMatrices); - this._locationMatrices = gl.getAttribLocation(program, "osd_transform_matrix"); - gl.bufferData(gl.ARRAY_BUFFER, 4 * 9 * options.instanceCount, gl.STREAM_DRAW); - //matrix 3x3 (9) * 4 bytes per element - const bytesPerMatrix = 4 * 9; - for (let i = 0; i < 3; ++i) { - const loc = this._locationMatrices + i; - gl.enableVertexAttribArray(loc); - // note the stride and offset - const offset = i * 12; // 3 floats per row, 4 bytes per float - gl.vertexAttribPointer( - loc, // location - 3, // size (num values to pull from buffer per iteration) - gl.FLOAT, // type of data in buffer - false, // normalize - bytesPerMatrix, // stride, num bytes to advance to get to next set of values - offset - ); - // this line says this attribute only changes for each 1 instance - gl.vertexAttribDivisor(loc, 1); - } - - this._textureLoc = gl.getUniformLocation(program, "_vis_data_sampler"); - gl.uniform1iv(this._textureLoc, iterate(options.instanceCount)); - - } else { - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferTexturePosition); - this._locationTexturePosition = gl.getAttribLocation(program, 'osd_tile_texture_position'); - gl.enableVertexAttribArray(this._locationTexturePosition); - gl.vertexAttribPointer(this._locationTexturePosition, 2, gl.FLOAT, false, 0, 0); - - this._locationMatrices = gl.getUniformLocation(program, "osd_transform_matrix"); - } - } - - programUsed(program, currentConfig, texture, tileOpts = {}) { - if (!this.renderer.running) { - return; - } - // Allow for custom drawing in webGL and possibly avoid using webGL at all - - let context = this.renderer, - gl = this.gl; - - if (currentConfig) { - context.glDrawing(gl, program, currentConfig, tileOpts); - } - - // Set Attributes for GLSL - gl.uniform1f(this._locationPixelSize, tileOpts.pixelSize || 1); - gl.uniform1f(this._locationZoomLevel, tileOpts.zoom || 1); - - const options = program._osdOptions; - //if compiled as instanced drawing - if (options.instanceCount > 1) { - - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferTexturePosition); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, tileOpts.textureCoords); - - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferMatrices); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, tileOpts.transform); - - let drawInstanceCount = tileOpts.instanceCount || Infinity; - drawInstanceCount = Math.min(drawInstanceCount, options.instanceCount); - - for (let i = 0; i <= drawInstanceCount; i++){ - gl.activeTexture(gl.TEXTURE0 + i); - gl.bindTexture(gl.TEXTURE_2D, texture[i]); - } - - gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, drawInstanceCount); - } else { - gl.bindBuffer(gl.ARRAY_BUFFER, this._bufferTexturePosition); - gl.bufferData(gl.ARRAY_BUFFER, tileOpts.textureCoords, gl.STATIC_DRAW); - - gl.uniformMatrix3fv(this._locationMatrices, false, tileOpts.transform || $.Mat3.makeIdentity()); - - // Upload texture, only one texture active, no preparation - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl[options.textureType], texture); - - // Draw triangle strip (two triangles) from a static array defined in the vertex shader - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - } - } -}; - -})(OpenSeadragon);