From e3024deb4661d1d236bf15ef16e1d39dc9223e98 Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 26 Sep 2023 15:31:43 +0200 Subject: [PATCH 1/8] Modular webgl2 drawer: fix small bugs. Add drawer IDs to demo page urls to allow refreshing/direct running --- src/webgl/drawer.js | 51 +++++++++++++++------- src/webgl/renderer.js | 8 ++-- src/webgl/webGLContext.js | 18 +++----- test/demo/basic.html | 13 +++++- test/demo/drawercomparison.html | 4 +- test/demo/drawercomparison.js | 72 ++++++++++++++++++++++++-------- test/demo/drawerperformance.html | 4 +- test/demo/drawerperformance.js | 50 ++++++++++++++++++---- 8 files changed, 156 insertions(+), 64 deletions(-) diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js index 3b20f8ea..cc461ed8 100644 --- a/src/webgl/drawer.js +++ b/src/webgl/drawer.js @@ -114,6 +114,30 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { engine.init(size.x, size.y); this.viewer.addHandler("resize", this._resizeRenderer.bind(this)); this.renderer = engine; + this.renderer.setDataBlendingEnabled(true); + + // const gl = this.renderer.gl; + // this._renderToTexture = gl.createTexture(); + // gl.activeTexture(gl.TEXTURE0); + // gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture); + // gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size.x, 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.CLAMP_TO_EDGE); + // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + // + // // set up the framebuffer for render-to-texture + // this._glFrameBuffer = gl.createFramebuffer(); + // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + // gl.framebufferTexture2D( + // gl.FRAMEBUFFER, + // gl.COLOR_ATTACHMENT0, // attach texture as COLOR_ATTACHMENT0 + // gl.TEXTURE_2D, // attach a 2D texture + // this._renderToTexture, // the texture to attach + // 0 + // ); + // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + // gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0); + return engine.canvas; } @@ -136,23 +160,19 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { let rotMatrix = $.Mat3.makeRotation(-viewport.rotation); let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); - this.renderer.clear(); - this.renderer.setDataBlendingEnabled(false); + // const gl = this.renderer.gl; + // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); + // clear the buffer to draw a new image + // gl.clear(gl.COLOR_BUFFER_BIT); //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed - - let drawn = 0; - for (const tiledImage of tiledImages) { + console.log("START TILED IMAGE"); let tilesToDraw = tiledImage.getTilesToDraw(); - if (tilesToDraw.length === 0) { - break; - } - - if (drawn === 1) { - this.renderer.setDataBlendingEnabled(true); - } + // if (tilesToDraw.length === 0) { + // break; + // } let overallMatrix = viewMatrix; let imageRotation = tiledImage.getRotation(true); @@ -168,12 +188,15 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { overallMatrix = viewMatrix.multiply(localMatrix); } + //todo better access to the rendering context const shader = this.renderer.specification(0).shaders.renderShader._renderContext; - // iterate over tiles and add data for each one to the buffers for (let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){ const tile = tilesToDraw[tileIndex].tile; + + console.log("TILE " + tile.level + "-" + tile.x + "_" + tile.y); + const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); shader.opacity.set(tile.opacity * tiledImage.opacity); @@ -205,8 +228,6 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { tiles: tilesToDraw.map(info => info.tile), }); } - - drawn++; } } diff --git a/src/webgl/renderer.js b/src/webgl/renderer.js index 33654dc3..7be24859 100644 --- a/src/webgl/renderer.js +++ b/src/webgl/renderer.js @@ -144,7 +144,7 @@ $.WebGLModule = class extends $.EventSource { const options = { wrap: readGlProp("wrap", "MIRRORED_REPEAT"), magFilter: readGlProp("magFilter", "LINEAR"), - minFilter: readGlProp("minFilter", "NEAREST"), + minFilter: readGlProp("minFilter", "LINEAR"), dataLoader: contextOpts.dataLoader || "TEXTURE_2D" }; this.webglContext = new Context(this, glContext, options); @@ -592,9 +592,11 @@ $.WebGLModule = class extends $.EventSource { 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.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.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA); } else { this.gl.disable(this.gl.BLEND); } diff --git a/src/webgl/webGLContext.js b/src/webgl/webGLContext.js index 72e3e5ad..8056836c 100644 --- a/src/webgl/webGLContext.js +++ b/src/webgl/webGLContext.js @@ -185,7 +185,7 @@ $.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { } static create(canvas) { - return canvas.getContext('webgl2', { premultipliedAlpha: false, alpha: true }); + return canvas.getContext('webgl2', { premultipliedAlpha: true, alpha: true }); } //todo try to implement on the global scope version-independntly @@ -263,6 +263,7 @@ vec4 lid_${layer._index}_xo() { return `#version 300 es precision mediump float; precision mediump sampler2DArray; +precision mediump sampler2D; ${this.texture.declare(shaderDataIndexToGlobalDataIndex)} uniform float pixel_size_in_fragments; @@ -283,17 +284,7 @@ void show(vec4 color) { vec4 fg = _last_rendered_color; _last_rendered_color = color; vec4 pre_fg = vec4(fg.rgb * fg.a, fg.a); - final_color = pre_fg + final_color * (1.0-fg.a); -} - -void finalize() { - show(vec4(.0)); - - if (close(final_color.a, 0.0)) { - final_color = vec4(0.); - } else { - final_color = vec4(final_color.rgb/final_color.a, final_color.a); - } + final_color = pre_fg + final_color; } vec4 blend_equation(in vec4 foreground, in vec4 background) { @@ -317,7 +308,8 @@ ${definition} void main() { ${execution} - finalize(); + //blend last level + show(vec4(.0)); }`; } diff --git a/test/demo/basic.html b/test/demo/basic.html index 6a677420..5f4262ab 100644 --- a/test/demo/basic.html +++ b/test/demo/basic.html @@ -20,12 +20,23 @@
diff --git a/test/demo/drawercomparison.html b/test/demo/drawercomparison.html index 3e007419..d40c8ef8 100644 --- a/test/demo/drawercomparison.html +++ b/test/demo/drawercomparison.html @@ -65,12 +65,12 @@

Compare behavior of Context2d and WebGL drawers

-

Context2d drawer (default in OSD <= 4.1.0)

+

Loading...

-

New WebGL drawer

+

Loading...

diff --git a/test/demo/drawercomparison.js b/test/demo/drawercomparison.js index 59bdd11b..17d178d5 100644 --- a/test/demo/drawercomparison.js +++ b/test/demo/drawercomparison.js @@ -13,6 +13,19 @@ const labels = { bblue: 'Blue B', duomo: 'Duomo', } +const drawers = { + canvas: "Context2d drawer (default in OSD <= 4.1.0)", + webgl: "New WebGL drawer", + universal_webgl: "New WebGL (Modular)" +} + +//Support drawer type from the url +const url = new URL(window.location.href); +const drawer1 = url.searchParams.get("left") || 'canvas'; +const drawer2 = url.searchParams.get("right") || 'webgl'; + +$("#title-w1").html(drawers[drawer1]); +$("#title-w2").html(drawers[drawer2]); //Double viewer setup for comparison - CanvasDrawer and WebGLDrawer // viewer1: canvas drawer @@ -25,7 +38,7 @@ let viewer1 = window.viewer1 = OpenSeadragon({ crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false, // maxImageCacheCount: 30, - drawer:'canvas', + drawer:drawer1, blendTime:0 }); @@ -39,25 +52,23 @@ let viewer2 = window.viewer2 = OpenSeadragon({ crossOriginPolicy: 'Anonymous', ajaxWithCredentials: false, // maxImageCacheCount: 30, - drawer:'webgl', + drawer:drawer2, blendTime:0, }); -// viewer3: html drawer -var viewer3 = window.viewer3 = OpenSeadragon({ - id: "htmldrawer", - drawer:'html', - blendTime:2, - prefixUrl: "../../build/openseadragon/images/", - minZoomImageRatio:0.01, - customDrawer: OpenSeadragon.HTMLDrawer, - tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], - sequenceMode: true, - crossOriginPolicy: 'Anonymous', - ajaxWithCredentials: false -}); - - +// // viewer3: html drawer, unused +// var viewer3 = window.viewer3 = OpenSeadragon({ +// id: "htmldrawer", +// drawer:'html', +// blendTime:2, +// prefixUrl: "../../build/openseadragon/images/", +// minZoomImageRatio:0.01, +// customDrawer: OpenSeadragon.HTMLDrawer, +// tileSources: [sources['leaves'], sources['rainbow'], sources['duomo']], +// sequenceMode: true, +// crossOriginPolicy: 'Anonymous', +// ajaxWithCredentials: false +// }); // Sync navigation of viewer1 and viewer 2 @@ -126,6 +137,8 @@ Object.keys(sources).forEach((key, index)=>{ } }) +$('#image-picker').append(makeComparisonSwitcher()); + $('#image-picker input.toggle').on('change',function(){ let data = $(this).data(); if(this.checked){ @@ -288,6 +301,30 @@ function addTileSource(viewer, image, checkbox){ } } +function getAvailableDrawerSelect(name, selectedDrawer) { + return ` +`; +} + +function makeComparisonSwitcher() { + const left = getAvailableDrawerSelect("left", drawer1), + right = getAvailableDrawerSelect("right", drawer2); + return ` +
+ Note: you can run the comparison with desired drawers like this: drawercomparison.html?left=[type]&right=[type] +
+ ${left} + ${right} + +
+
`; +} + function makeImagePickerElement(key, label){ return $(`
@@ -306,7 +343,6 @@ function makeImagePickerElement(key, label){
- `.replaceAll('data-image=""', `data-image="${key}"`).replace('__title__', label)); } diff --git a/test/demo/drawerperformance.html b/test/demo/drawerperformance.html index 70715641..17e70637 100644 --- a/test/demo/drawerperformance.html +++ b/test/demo/drawerperformance.html @@ -27,9 +27,7 @@
diff --git a/test/demo/drawerperformance.js b/test/demo/drawerperformance.js index 6a52c920..03d6b946 100644 --- a/test/demo/drawerperformance.js +++ b/test/demo/drawerperformance.js @@ -13,6 +13,14 @@ const labels = { bblue: 'Blue B', duomo: 'Duomo', } +const drawers = { + canvas: "Context2d drawer (default in OSD <= 4.1.0)", + webgl: "New WebGL drawer", + universal_webgl: "New WebGL (Modular)", + html: "" +} + + let viewer; ( function () { @@ -408,6 +416,10 @@ function rStats ( settings ) { _base = document.createElement( 'div' ); _base.className = 'rs-base'; + _base.style.bottom = '0px'; + _base.style.right = '0px'; + _base.style.top = 'initial'; + _base.style.left = 'initial'; _div = document.createElement( 'div' ); _div.className = 'rs-container'; _div.style.height = 'auto'; @@ -509,6 +521,8 @@ function rStats ( settings ) { } + + var glStats = function() { var _rS = null; @@ -898,16 +912,9 @@ Stats.Panel = function ( name, fg, bg ) { // })(); - -$('#create-drawer').on('click',function(){ - let drawerType = $('#select-drawer').val(); - let num = Math.floor($('#input-number').val()); - rS('other').start(); - run(drawerType, num); -}); - - function run(drawerType, num) { + rS('other').start(); + if(viewer){ viewer.destroy(); } @@ -969,3 +976,28 @@ function makeTileSources(num){ } +const url = new URL(window.location.href); +const drawer = url.searchParams.get("drawer"); +const numberOfSources = Number.parseInt(url.searchParams.get("sources")) || 1; + +$('#create-drawer').on('click',function(){ + const drawer = $('#select-drawer').val(); + let num = Math.floor($('#input-number').val()); + + url.searchParams.set("drawer", drawer); + url.searchParams.set("sources", num); + if ("undefined" !== typeof history.replaceState) { + history.replaceState(null, window.location.title, url.toString()); + } + run(drawer, num); +}); + +$('#input-number').val(numberOfSources); +$("#select-drawer").html(Object.entries(drawers).map(([k, v]) => { + const selected = drawer === k ? "selected" : ""; + return ``; +}).join("\n")); +if (drawer) { + run(drawer, numberOfSources); +} + From 174c4c709ab60e2b9c4ebeec2e01e59ea0a52f4e Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 26 Sep 2023 15:47:50 +0200 Subject: [PATCH 2/8] Remove logs from debugging. --- src/webgl/drawer.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js index cc461ed8..0da257b2 100644 --- a/src/webgl/drawer.js +++ b/src/webgl/drawer.js @@ -160,20 +160,15 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { let rotMatrix = $.Mat3.makeRotation(-viewport.rotation); let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); - // const gl = this.renderer.gl; + const gl = this.renderer.gl; // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); // clear the buffer to draw a new image - // gl.clear(gl.COLOR_BUFFER_BIT); + gl.clear(gl.COLOR_BUFFER_BIT); //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed for (const tiledImage of tiledImages) { - console.log("START TILED IMAGE"); let tilesToDraw = tiledImage.getTilesToDraw(); - // if (tilesToDraw.length === 0) { - // break; - // } - let overallMatrix = viewMatrix; let imageRotation = tiledImage.getRotation(true); // if needed, handle the tiledImage being rotated @@ -195,15 +190,13 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { for (let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){ const tile = tilesToDraw[tileIndex].tile; - console.log("TILE " + tile.level + "-" + tile.x + "_" + tile.y); - const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); shader.opacity.set(tile.opacity * tiledImage.opacity); //todo pixelSize value (not yet memoized) this.renderer.processData(tile.cacheKey, { - transform: matrix, zoom: - viewport.zoom, + transform: matrix, + zoom: viewport.zoom, pixelSize: 0 }); } From 31f9a7110918b667932d4c28d8a4fee69d9390ca Mon Sep 17 00:00:00 2001 From: Aiosa Date: Wed, 27 Sep 2023 15:16:23 +0200 Subject: [PATCH 3/8] Working tiledimage-level transparency in a single pass. --- src/webgl/drawer.js | 25 +++++++++++++++++++++---- src/webgl/renderer.js | 8 +++++--- src/webgl/webGLContext.js | 9 ++++++--- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js index 0da257b2..d0bbee3d 100644 --- a/src/webgl/drawer.js +++ b/src/webgl/drawer.js @@ -97,6 +97,11 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { const engine = new $.WebGLModule($.extend(this.options, { uniqueId: "openseadragon", + "2.0": { + canvasOptions: { + stencil: true + } + } })); engine.addRenderingSpecifications({ @@ -116,7 +121,7 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { this.renderer = engine; this.renderer.setDataBlendingEnabled(true); - // const gl = this.renderer.gl; + const gl = this.renderer.gl; // this._renderToTexture = gl.createTexture(); // gl.activeTexture(gl.TEXTURE0); // gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture); @@ -138,6 +143,10 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); // gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0); + gl.enable(gl.STENCIL_TEST); + gl.stencilMask(0xff); + gl.stencilFunc(gl.GREATER, 1, 0xff); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); return engine.canvas; } @@ -153,10 +162,11 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { zoom: this.viewport.getZoom(true) }; - let flipMultiplier = this.viewport.flipped ? -1 : 1; + + // 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 scaleMatrix = $.Mat3.makeScaling(2 / viewport.bounds.width, -2 / viewport.bounds.height); let rotMatrix = $.Mat3.makeRotation(-viewport.rotation); let viewMatrix = scaleMatrix.multiply(rotMatrix).multiply(posMatrix); @@ -169,6 +179,12 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { for (const tiledImage of tiledImages) { let tilesToDraw = tiledImage.getTilesToDraw(); + if (tilesToDraw.length === 0) { + continue; + } + + gl.clear(gl.STENCIL_BUFFER_BIT); + let overallMatrix = viewMatrix; let imageRotation = tiledImage.getRotation(true); // if needed, handle the tiledImage being rotated @@ -187,7 +203,7 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { //todo better access to the rendering context const shader = this.renderer.specification(0).shaders.renderShader._renderContext; // iterate over tiles and add data for each one to the buffers - for (let tileIndex = 0; tileIndex < tilesToDraw.length; tileIndex++){ + for (let tileIndex = tilesToDraw.length - 1; tileIndex >= 0; tileIndex--){ const tile = tilesToDraw[tileIndex].tile; const matrix = this._getTileMatrix(tile, tiledImage, overallMatrix); @@ -264,6 +280,7 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { let localMatrix = t1.multiply($.Mat3.makeScaling(-1, 1)).multiply(t2); matrix = matrix.multiply(localMatrix); } + let overallMatrix = viewMatrix.multiply(matrix); return overallMatrix.values; } diff --git a/src/webgl/renderer.js b/src/webgl/renderer.js index 7be24859..35d94f76 100644 --- a/src/webgl/renderer.js +++ b/src/webgl/renderer.js @@ -81,7 +81,6 @@ $.WebGLModule = class extends $.EventSource { this.visualisationInUse = function(visualisation) { }; this.visualisationChanged = function(oldVis, newVis) { }; - /** * Debug mode. * @member {boolean} @@ -124,12 +123,15 @@ $.WebGLModule = class extends $.EventSource { 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); - let glContext = Context && Context.create(canvas); + //todo documment this + let glContext = Context && Context.create(canvas, contextOpts.canvasOptions || {}); if (glContext) { this.gl = glContext; - const contextOpts = incomingOptions[version] || {}; + const readGlProp = function(prop, defaultValue) { return glContext[contextOpts[prop] || defaultValue] || glContext[defaultValue]; }; diff --git a/src/webgl/webGLContext.js b/src/webgl/webGLContext.js index 8056836c..4e92f4ad 100644 --- a/src/webgl/webGLContext.js +++ b/src/webgl/webGLContext.js @@ -60,9 +60,10 @@ $.WebGLModule.WebGLImplementation = class { /** * 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) { + static create(canvas, options) { throw("::create() must be implemented!"); } @@ -184,8 +185,10 @@ $.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { return "2.0"; } - static create(canvas) { - return canvas.getContext('webgl2', { premultipliedAlpha: true, alpha: true }); + static create(canvas, options) { + options.alpha = true; + options.premultipliedAlpha = true; + return canvas.getContext('webgl2', options); } //todo try to implement on the global scope version-independntly From cd9d340038afc889dcf7c9af197d0cf19901e658 Mon Sep 17 00:00:00 2001 From: Aiosa Date: Mon, 23 Oct 2023 16:03:47 +0200 Subject: [PATCH 4/8] Removed date loader (will be handled by future OSD cache system). Attempt to use instanced rendering. Refactoring of the module. --- Gruntfile.js | 1 - src/drawerbase.js | 8 +- src/tilecache.js | 3 +- ...dataLoader.js => dataLoader.js.deprecated} | 7 +- src/webgl/drawer.js | 468 ++++++++++++-- src/webgl/plainShader.js | 52 +- src/webgl/renderer.js | 542 +++++++--------- src/webgl/shaderLayer.js | 82 ++- src/webgl/webGLContext.js | 587 +++++++++++++----- test/demo/drawercomparison.js | 15 +- 10 files changed, 1207 insertions(+), 558 deletions(-) rename src/webgl/{dataLoader.js => dataLoader.js.deprecated} (99%) diff --git a/Gruntfile.js b/Gruntfile.js index b0f452ce..9ffaea7d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -70,7 +70,6 @@ module.exports = function(grunt) { //Aiosa's webgl drawer - needs optimization, polishing, trimming "src/webgl/renderer.js", "src/webgl/shaderLayer.js", - "src/webgl/dataLoader.js", "src/webgl/webGLContext.js", "src/webgl/drawer.js", "src/webgl/plainShader.js", diff --git a/src/drawerbase.js b/src/drawerbase.js index 62cb1e04..ac7b3bd1 100644 --- a/src/drawerbase.js +++ b/src/drawerbase.js @@ -38,6 +38,12 @@ $.DrawerOptions = class DrawerOptions{ constructor(options){} }; +/** + * @typedef {Object} Point + * @property {number} x + * @property {number} y + */ + /** * @class DrawerBase * @memberof OpenSeadragon @@ -297,7 +303,7 @@ $.DrawerBase = class DrawerBase{ * @inner * Calculate width and height of the canvas based on viewport dimensions * and pixelDensityRatio - * @returns {Dictionary} {x, y} size of the canvas + * @returns {Point} {x, y} size of the canvas */ _calculateCanvasSize() { var pixelDensityRatio = $.pixelDensityRatio; diff --git a/src/tilecache.js b/src/tilecache.js index 7d9e5478..c776a077 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -272,7 +272,8 @@ $.TileCache.prototype = { * @property {CanvasRenderingContext2D} context2D - The context that is being unloaded */ tiledImage.viewer.raiseEvent("image-unloaded", { - context2D: context2D + context2D: context2D, + tile: tile }); } diff --git a/src/webgl/dataLoader.js b/src/webgl/dataLoader.js.deprecated similarity index 99% rename from src/webgl/dataLoader.js rename to src/webgl/dataLoader.js.deprecated index 6e800aba..604c338d 100644 --- a/src/webgl/dataLoader.js +++ b/src/webgl/dataLoader.js.deprecated @@ -125,8 +125,11 @@ $.WebGLModule.IDataLoader = class { * @param id */ free(renderer, id) { - this.unloadTexture(renderer, id, this.getLoaded(id)); - this.setUnloaded(id); + const loaded = this.getLoaded(id); + if (loaded) { + this.unloadTexture(renderer, id, this.getLoaded(id)); + this.setUnloaded(id); + } } /** diff --git a/src/webgl/drawer.js b/src/webgl/drawer.js index d0bbee3d..d34d3188 100644 --- a/src/webgl/drawer.js +++ b/src/webgl/drawer.js @@ -41,6 +41,7 @@ * @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. */ @@ -49,10 +50,56 @@ $.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", this.renderer.freeData.bind(this.renderer)); + 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 @@ -64,6 +111,17 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { 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; } @@ -94,8 +152,7 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { * @returns {Element} the canvas to draw into */ createDrawingElement(){ - - const engine = new $.WebGLModule($.extend(this.options, { + this.renderer = new $.WebGLModule($.extend(this.options, { uniqueId: "openseadragon", "2.0": { canvasOptions: { @@ -103,51 +160,26 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { } } })); + return this.renderer.canvas; + } - engine.addRenderingSpecifications({ - shaders: { - renderShader: { - type: "identity", - dataReferences: [0], - } + 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; } - }); - - engine.prepare(); - - const size = this._calculateCanvasSize(); - engine.init(size.x, size.y); - this.viewer.addHandler("resize", this._resizeRenderer.bind(this)); - this.renderer = engine; - this.renderer.setDataBlendingEnabled(true); - - const gl = this.renderer.gl; - // this._renderToTexture = gl.createTexture(); - // gl.activeTexture(gl.TEXTURE0); - // gl.bindTexture(gl.TEXTURE_2D, this._renderToTexture); - // gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size.x, 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.CLAMP_TO_EDGE); - // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - // - // // set up the framebuffer for render-to-texture - // this._glFrameBuffer = gl.createFramebuffer(); - // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); - // gl.framebufferTexture2D( - // gl.FRAMEBUFFER, - // gl.COLOR_ATTACHMENT0, // attach texture as COLOR_ATTACHMENT0 - // gl.TEXTURE_2D, // attach a 2D texture - // this._renderToTexture, // the texture to attach - // 0 - // ); - // gl.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); - // gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._renderToTexture, 0); - - gl.enable(gl.STENCIL_TEST); - gl.stencilMask(0xff); - gl.stencilFunc(gl.GREATER, 1, 0xff); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); - return engine.canvas; + } else { + if (this._stencilTestEnabled) { + this._stencilTestEnabled = false; + const gl = this.renderer.gl; + gl.disable(gl.STENCIL_TEST); + } + } } /** @@ -155,6 +187,15 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { * @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), @@ -162,20 +203,39 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { zoom: this.viewport.getZoom(true) }; - - // let flipMultiplier = this.viewport.flipped ? -1 : 1; + 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, -2 / viewport.bounds.height); + 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.bindFramebuffer(gl.FRAMEBUFFER, this._glFrameBuffer); - // clear the buffer to draw a new image gl.clear(gl.COLOR_BUFFER_BIT); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); - //iterate over tiled images and draw each one using a two-pass rendering pipeline if needed for (const tiledImage of tiledImages) { let tilesToDraw = tiledImage.getTilesToDraw(); @@ -183,8 +243,145 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { 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 @@ -199,21 +396,20 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { overallMatrix = viewMatrix.multiply(localMatrix); } - - //todo better access to the rendering context - const shader = this.renderer.specification(0).shaders.renderShader._renderContext; // 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(tile.cacheKey, { + this.renderer.processData(tileData.texture, { transform: matrix, zoom: viewport.zoom, - pixelSize: 0 + pixelSize: 0, + textureCoords: tileData.position }); } @@ -240,6 +436,54 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { } } + //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 @@ -253,11 +497,12 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { // 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; - // x, y, w, h in viewport coords 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); @@ -288,19 +533,116 @@ $.WebGL = class WebGL extends OpenSeadragon.DrawerBase { _resizeRenderer(){ const size = this._calculateCanvasSize(); this.renderer.setDimensions(0, 0, size.x, size.y); + this._size = size; } - _imageUnloadedHandler(event){ - this.renderer.freeData(event.tile.cacheKey); + _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; - //todo fix cache system and then this line - //access by default raw tile data, and only access canvas if not cache set + 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; - this.renderer.loadData(tile.cacheKey, data, tile.sourceBounds.width, tile.sourceBounds.height); + + 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){ diff --git a/src/webgl/plainShader.js b/src/webgl/plainShader.js index ed7e7f05..a8c4c7c5 100644 --- a/src/webgl/plainShader.js +++ b/src/webgl/plainShader.js @@ -1,34 +1,36 @@ (function($) { - /** - * Identity shader - * - * data reference must contain one index to the data to render using identity - */ - $.WebGLModule.IdentityLayer = class extends $.WebGLModule.ShaderLayer { +/** + * 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 type() { + return "identity"; + } - static name() { - return "Identity"; - } + static name() { + return "Identity"; + } - static description() { - return "shows the data AS-IS"; - } + static description() { + return "shows the data AS-IS"; + } - static sources() { - return [{ - acceptsChannelCount: (x) => x === 4, - description: "4d texture to render AS-IS" - }]; - } + static sources() { + return [{ + acceptsChannelCount: (x) => x === 4, + description: "4d texture to render AS-IS" + }]; + } - getFragmentShaderExecution() { - return `return ${this.sampleChannel("tile_texture_coords")};`; - } - }; + 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"] = { diff --git a/src/webgl/renderer.js b/src/webgl/renderer.js index 35d94f76..849c8472 100644 --- a/src/webgl/renderer.js +++ b/src/webgl/renderer.js @@ -12,8 +12,8 @@ $.WebGLModule = class extends $.EventSource { /** * @typedef {{ - * name: string, - * lossless: boolean, + * name?: string, + * lossless?: boolean, * shaders: Object. * }} OpenSeadragon.WebGLModule.RenderingConfig * @@ -26,11 +26,11 @@ $.WebGLModule = class extends $.EventSource { * @type {{TUseChannel,TUseFilter,TIControlConfig}} * * @typedef {{ - * name: string, + * name?: string, * type: string, - * visible: boolean, + * visible?: boolean, * dataReferences: number[], - * params: OpenSeadragon.WebGLModule.ShaderLayerParams + * params?: OpenSeadragon.WebGLModule.ShaderLayerParams, * }} OpenSeadragon.WebGLModule.ShaderLayerConfig * * @@ -53,8 +53,6 @@ $.WebGLModule = class extends $.EventSource { * @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 - * @param {function} incomingOptions.visualisationInUse function called when a specification is initialized and run - * @param {function} incomingOptions.visualisationChanged function called when a visualization swap is performed: * signature f({Visualization} oldVisualisation,{Visualization} newVisualisation) * @constructor * @memberOf OpenSeadragon.WebGLModule @@ -77,9 +75,6 @@ $.WebGLModule = class extends $.EventSource { this.resetCallback = function() { }; //called once a visualisation is compiled and linked (might not happen) this.visualisationReady = function(i, visualisation) { }; - //called once a visualisation is switched to (including first run) - this.visualisationInUse = function(visualisation) { }; - this.visualisationChanged = function(oldVis, newVis) { }; /** * Debug mode. @@ -110,7 +105,7 @@ $.WebGLModule = class extends $.EventSource { /** * WebGL context - * @member {WebGLRenderingContextBase} + * @member {WebGLRenderingContext|WebGL2RenderingContext} */ this.gl = null; @@ -141,13 +136,11 @@ $.WebGLModule = class extends $.EventSource { * @param {string} options.wrap texture wrap parameteri * @param {string} options.magFilter texture filter parameteri * @param {string} options.minFilter texture filter parameteri - * @param {string|WebGLModule.IDataLoader} options.dataLoader class name or implementation of a given loader */ const options = { wrap: readGlProp("wrap", "MIRRORED_REPEAT"), magFilter: readGlProp("magFilter", "LINEAR"), minFilter: readGlProp("minFilter", "LINEAR"), - dataLoader: contextOpts.dataLoader || "TEXTURE_2D" }; this.webglContext = new Context(this, glContext, options); } @@ -159,10 +152,10 @@ $.WebGLModule = class extends $.EventSource { */ this.raiseEvent('fatal-error', {message: "Unable to initialize the WebGL renderer.", details: e}); - console.error(e); + $.console.error(e); return; } - console.log(`WebGL ${this.webglContext.getVersion()} Rendering module (ID ${this.uniqueId})`); + $.console.log(`WebGL ${this.webglContext.getVersion()} Rendering module (ID ${this.uniqueId})`); } /** @@ -171,28 +164,18 @@ $.WebGLModule = class extends $.EventSource { * @memberOf OpenSeadragon.WebGLModule */ reset() { - this._unloadCurrentProgram(); - this._programConfigurations = []; + if (this._programs) { + Object.values(this._programs).forEach(p => this._unloadProgram(p)); + } + this._programSpecifications = []; this._dataSources = []; - this._shaderDataIndexToGlobalDataIndex = []; this._origDataSources = []; this._programs = {}; this._program = -1; - this._prepared = false; this.running = false; this._initialized = false; } - /** - * Check if prepare() was called. - * @return {boolean} - * @instance - * @memberOf OpenSeadragon.WebGLModule - */ - get isPrepared() { - return this._prepared; - } - /** * WebGL target canvas * @return {HTMLCanvasElement} @@ -236,50 +219,108 @@ $.WebGLModule = class extends $.EventSource { 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} configurations - objects that define the what to render (see Readme) + * @param {RenderingConfig} specifications - objects that define the what to render (see Readme) * @return {boolean} true if loaded successfully * @instance * @memberOf OpenSeadragon.WebGLModule */ - addRenderingSpecifications(...configurations) { - if (this._prepared) { - console.error("New specification cannot be introduced after the visualiser was prepared."); - return false; - } - for (let config of configurations) { - if (!config.shaders) { - console.warn("Invalid visualization: no shaders defined", config); - continue; + addRenderingSpecifications(...specifications) { + for (let spec of specifications) { + const parsed = this._parseSpec(spec); + if (parsed) { + this._programSpecifications.push(parsed); } - - let count = 0; - for (let sid in config.shaders) { - const shader = config.shaders[sid]; - if (!shader.params) { - shader.params = {}; - } - count++; - } - - if (count < 0) { - console.warn("Invalid configualization: no shader configuration present!", config); - continue; - } - this._programConfigurations.push(config); } 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; + } + /** - * Runs a callback on each specification - * @param {function} call callback to perform on each specification (its object given as the only parameter) - * @instance - * @memberOf OpenSeadragon.WebGLModule + * + * @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} */ - foreachRenderingSpecification(call) { - this._programConfigurations.forEach(vis => call(vis)); + 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; } /** @@ -289,15 +330,11 @@ $.WebGLModule = class extends $.EventSource { * @instance * @memberOf OpenSeadragon.WebGLModule */ - rebuildSpecification(order = undefined) { - let vis = this._programConfigurations[this._program]; - - if (order) { - vis.order = order; + rebuildCurrentProgram(order = undefined) { + const program = this._programs[this._program]; + if (this.buildProgram(this._program, order, true, program && program._osdOptions)) { + this._forceSwitchShader(this._program); } - this._unloadCurrentProgram(); - this._specificationToProgram(vis, this._program); - this._forceSwitchShader(this._program); } /** @@ -307,7 +344,7 @@ $.WebGLModule = class extends $.EventSource { * @memberOf OpenSeadragon.WebGLModule */ specification(index) { - return this._programConfigurations[Math.min(index, this._programConfigurations.length - 1)]; + return this._programSpecifications[index]; } /** @@ -328,17 +365,25 @@ $.WebGLModule = class extends $.EventSource { * @instance * @memberOf OpenSeadragon.WebGLModule */ - useSpecification(i) { + useProgram(i) { if (!this._initialized) { - console.warn("$.WebGLModule::useSpecification(): not initialized."); + $.console.warn("$.WebGLModule::useSpecification(): not initialized."); return; } + if (this._program === i) { return; } - let oldIndex = this._program; this._forceSwitchShader(i); - this.visualisationChanged(this._programConfigurations[oldIndex], this._programConfigurations[i]); + } + + useCustomProgram(program) { + this._program = -1; + this.webglContext.programLoaded(program, null); + } + + getSpecificationsCount() { + return this._programSpecifications.length; } /** @@ -347,59 +392,55 @@ $.WebGLModule = class extends $.EventSource { * @memberOf WebGLModule */ getSources() { - //return this._programConfigurations[this._program].dziExtendedUrl; return this._dataSources; } /** - * Supported: 'wrap', 'minFilter', 'magFilter' - * @param {string} name WebGL name of the parameter - * @param {GLuint} value + * Set data srouces */ - setTextureParam(name, value) { - this.webglContext.texture.setTextureParam(name, value); - } - - /** - * @param id - * @param data - * @param width - * @param height - */ - loadData(id, data, width, height) { - this.webglContext.texture.load(this, id, data, width, height); + setSources(sources) { + if (!this._initialized) { + $.console.warn("$.WebGLModule::useSpecification(): not initialized."); + return; + } + this._origDataSources = sources || []; } /** * Renders data using WebGL - * @param {string} id used in loadImage() + * @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} tileOpts.transform position of the rendered tile + * @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(id, tileOpts) { - this.webglContext.programUsed( - this.program, - this._programConfigurations[this._program], - id, - tileOpts - ); + 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); // } } - freeData(id) { - - } - /** * Clear the output canvas */ @@ -431,9 +472,12 @@ $.WebGLModule = class extends $.EventSource { static eachValidShaderLayer(vis, callback, onFail = (layer, e) => { layer.error = e.message; - console.error(e); + $.console.error(e); }) { let shaders = vis.shaders; + if (!shaders) { + return true; + } let noError = true; for (let key in shaders) { let shader = shaders[key]; @@ -466,10 +510,13 @@ $.WebGLModule = class extends $.EventSource { static eachVisibleShaderLayer(vis, callback, onFail = (layer, e) => { layer.error = e.message; - console.error(e); + $.console.error(e); }) { let shaders = vis.shaders; + if (!shaders) { + return true; + } let noError = true; for (let key in shaders) { //rendering == true means no error @@ -499,7 +546,7 @@ $.WebGLModule = class extends $.EventSource { * @return {number} program index */ getCurrentProgramIndex() { - if (this._program < 0 || this._program >= this._programConfigurations.length) { + if (this._program < 0 || this._program >= this._programSpecifications.length) { this._program = 0; } return this._program; @@ -516,36 +563,21 @@ $.WebGLModule = class extends $.EventSource { } /** - * For easy initialization, do both in once call. - * For separate initialization (prepare|init), see functions below. - * @param {string[]|undefined} dataSources a list of data identifiers available to the specifications - * - specification configurations should not reference data not present in this array - * - the module gives you current list of required subset of this list for particular active visualization goal - * @param width initialization width - * @param height initialization height - */ - prepareAndInit(dataSources = undefined, width = 1, height = 1) { - this.prepare(dataSources); - this.init(width, height); - } - - /** - * Prepares the WebGL wrapper for being initialized. It is separated from - * initialization as this must be finished before OSD is ready (we must be ready to draw when the data comes). - * The idea is to open the protocol for OSD in onPrepared. - * Shaders are fetched from `specification.url` parameter. + * 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 {string[]|undefined} dataSources id's of data such that server can understand which image to send (usually paths) - * @param {number} visIndex index of the initial specification + * @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 */ - prepare(dataSources = undefined, visIndex = 0) { - if (this._prepared) { - console.error("Already prepared!"); + init(width = 1, height = 1, firstProgram = 0) { + if (this._initialized) { + $.console.error("Already initialized!"); return; } - - if (this._programConfigurations.length < 1) { - console.error("No specification specified!"); + if (this._programSpecifications.length < 1) { + $.console.error("No specification specified!"); /** * @event fatal-error */ @@ -553,35 +585,12 @@ $.WebGLModule = class extends $.EventSource { details: "::prepare() called with no specification set."}); return; } - this._origDataSources = dataSources || []; - this._program = visIndex; + this._program = firstProgram; + this.getCurrentProgramIndex(); //validates index - this._prepared = true; - this.getCurrentProgramIndex(); //resets index - this._specificationToProgram(this._programConfigurations[this._program], this._program); - } - - /** - * 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 - */ - init(width = 1, height = 1) { - if (!this._prepared) { - console.error("The viaGL was not yet prepared. Call prepare() before init()!"); - return; - } - if (this._initialized) { - console.error("Already initialized!"); - return; - } 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); @@ -604,23 +613,6 @@ $.WebGLModule = class extends $.EventSource { } } - /** - * Supported are two modes: show and blend - * show is the default option, stacking layers by generalized alpha blending - * blend is a custom alternative, default is a mask (remove background where foreground.a > 0.001) - * - * vec4 my_blend(vec4 foreground, vec4 background) { - * <> //here goes your blending code - * } - * - * @param code GLSL code to blend - must return vec4() and can use - * two variables: background, foreground - */ - setLayerBlending(code) { - this.webglContext.setBlendEquation(code); - this.rebuildSpecification(); - } - ////////////////////////////////////////////////////////////////////////////// ///////////// YOU PROBABLY DON'T WANT TO READ/CHANGE FUNCTIONS BELOW ////////////////////////////////////////////////////////////////////////////// @@ -659,16 +651,14 @@ $.WebGLModule = class extends $.EventSource { i = this._program; } - if (i >= this._programConfigurations.length) { - console.error("Invalid specification index ", i, "trying to use index 0..."); - if (i === 0) { - return; - } - i = 0; + let target = this._programSpecifications[i]; + if (!target) { + $.console.error("Invalid rendering target index!", i); + return; } - let target = this._programConfigurations[i]; - if (!this._programs[i]) { + const program = this._programs[i]; + if (!program) { this._specificationToProgram(target, i); } else if (i !== this._program) { this._updateRequiredDataSources(target); @@ -677,11 +667,11 @@ $.WebGLModule = class extends $.EventSource { this._program = i; if (target.error) { if (this.supportsHtmlControls()) { - this._loadHtml(i, this._program); + this._loadHtml(i, program); } - this._loadScript(i, this._program); + this._loadScript(i); this.running = false; - if (this._programConfigurations.length < 2) { + if (this._programSpecifications.length < 2) { /** * @event fatal-error */ @@ -695,22 +685,21 @@ $.WebGLModule = class extends $.EventSource { } else { this.running = true; if (this.supportsHtmlControls()) { - this._loadHtml(i, this._program); + this._loadHtml(program); } this._loadDebugInfo(); - if (!this._loadScript(i, this._program)) { + if (!this._loadScript(i)) { if (!_reset) { throw "Could not build visualization"; } this._forceSwitchShader(i, false); //force reset in errors return; } - this.webglContext.programLoaded(this._programs[i], target); + this.webglContext.programLoaded(program, target); } } - _unloadCurrentProgram() { - let program = this._programs && this._programs[this._program]; + _unloadProgram(program) { if (program) { //must remove before attaching new this._detachShader(program, "VERTEX_SHADER"); @@ -718,13 +707,13 @@ $.WebGLModule = class extends $.EventSource { } } - _loadHtml(visId) { + _loadHtml(program) { let htmlControls = document.getElementById(this.htmlControlsId); - htmlControls.innerHTML = this._programConfigurations[visId]._built["html"]; + htmlControls.innerHTML = this.webglContext.getCompiled(program, "html") || ""; } _loadScript(visId) { - return $.WebGLModule.eachValidShaderLayer(this._programConfigurations[visId], layer => layer._renderContext.init()); + return $.WebGLModule.eachValidShaderLayer(this._programSpecifications[visId], layer => layer._renderContext.init()); } _getDebugInfoPanel() { @@ -774,33 +763,29 @@ Output:
Specification setup:
${JSON.stringify(specification, $.WebGLModule.jsonReplacer)}
Dynamic shader data:
${JSON.stringify(specification.data)}`); - return null; + return; } - data.dziExtendedUrl = data.dataUrls.join(","); - specification._built = data; - //preventive delete specification.error; delete specification.desc; - return data; } catch (error) { this._buildFailed(specification, error); } - return null; } _detachShader(program, type) { @@ -812,73 +797,9 @@ Output:
= this._origDataSources.length) { - //make sure values are set if user did not provide - this._origDataSources.push("__generated_do_not_use__"); - } - - this._shaderDataIndexToGlobalDataIndex = new Array( - Math.max(this._origDataSources.length, usedIds[usedIds.length - 1]) - ).fill(-1); - - for (let id of usedIds) { - this._shaderDataIndexToGlobalDataIndex[id] = this._dataSources.length; - this._dataSources.push(this._origDataSources[id]); - while (id > this._shaderDataIndexToGlobalDataIndex.length) { - this._shaderDataIndexToGlobalDataIndex.push(-1); - } - } - } - - _processSpecification(spec, idx) { - let gl = this.gl, - err = function(message, description) { - spec.error = message; - spec.desc = description; - }; - + _specificationToProgram(spec, idx, options) { + this._updateRequiredDataSources(spec); + let gl = this.gl; let program; if (!this._programs[idx]) { @@ -923,55 +844,50 @@ Output:
= this._origDataSources.length) { + //make sure values are set if user did not provide + this._origDataSources.push("__generated_do_not_use__"); } - 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'); - } - - if (!useShader(gl, program, VS, 'VERTEX_SHADER') || - !useShader(gl, program, 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( VS )); - console.warn("FRAGMENT SHADER\n", numberLines( 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 (isDebugMode) { //todo testing - console.info("FRAGMENT SHADER\n", numberLines( FS )); - } + for (let id of usedIds) { + this._dataSources.push(this._origDataSources[id]); } } }; diff --git a/src/webgl/shaderLayer.js b/src/webgl/shaderLayer.js index 23ffd4eb..628016f2 100644 --- a/src/webgl/shaderLayer.js +++ b/src/webgl/shaderLayer.js @@ -8,7 +8,7 @@ $.WebGLModule.ShaderMediator = class { /** * Register shader - * @param {function} LayerRendererClass class extends OpenSeadragon.WebGLModule.ShaderLayer + * @param {typeof OpenSeadragon.WebGLModule.ShaderLayer} LayerRendererClass static class definition */ static registerLayer(LayerRendererClass) { //todo why not hasOwnProperty check allowed by syntax checker @@ -18,9 +18,16 @@ $.WebGLModule.ShaderMediator = class { // 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 @@ -32,16 +39,52 @@ $.WebGLModule.ShaderMediator = class { /** * Get all available shaders - * @return {function[]} classes that extend OpenSeadragon.WebGLModule.ShaderLayer + * @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 @@ -103,6 +146,9 @@ $.WebGLModule.ShaderLayer = class { this._buildControls(options); this.resetChannel(options); this.resetMode(options); + this._blendUniform = null; + this._clipUniform = null; + this.blendMode = $.WebGLModule.BLEND_MODE["source-over"]; } /** @@ -121,8 +167,10 @@ $.WebGLModule.ShaderLayer = class { * @return {string} */ getFragmentShaderDefinition() { + this._blendUniform = `${this.uid}_blend`; + this._clipUniform = `${this.uid}_clip`; let controls = this.constructor.defaultControls, - html = []; + glsl = [`uniform int ${this._blendUniform};`, `uniform bool ${this._clipUniform};`]; for (let control in controls) { if (control.startsWith("use_")) { continue; @@ -133,11 +181,19 @@ $.WebGLModule.ShaderLayer = class { let code = controlObject.define(); if (code) { code = code.trim(); - html.push(code); + glsl.push(code); } } } - return html.join("\n"); + return glsl.join("\n"); + } + + setBlendMode(name) { + const modes = $.WebGLModule.BLEND_MODE; + this.blendMode = modes[name]; + if (this.blendMode === undefined) { + this.blendMode = modes["source-over"]; + } } /** @@ -159,6 +215,11 @@ $.WebGLModule.ShaderLayer = class { * @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_")) { @@ -178,6 +239,13 @@ $.WebGLModule.ShaderLayer = class { * @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_")) { @@ -295,7 +363,7 @@ $.WebGLModule.ShaderLayer = class { return 'vec4(0.0)'; } } - let sampled = `${this.webglContext.texture.sample(refs[otherDataIndex], textureCoords)}.${chan}`; + let sampled = `${this.webglContext.sampleTexture(refs[otherDataIndex], textureCoords)}.${chan}`; // if (raw) return sampled; // return this.filter(sampled); return sampled; diff --git a/src/webgl/webGLContext.js b/src/webgl/webGLContext.js index 4e92f4ad..1064f5e1 100644 --- a/src/webgl/webGLContext.js +++ b/src/webgl/webGLContext.js @@ -16,6 +16,15 @@ $.WebGLModule.determineContext = function( version ){ 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 @@ -26,35 +35,18 @@ $.WebGLModule.WebGLImplementation = class { /** * Create a WebGL Renderer Context Implementation (version-dependent) * @param {WebGLModule} renderer - * @param {WebGLRenderingContextBase} gl + * @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 - * @param {string|WebGLModule.IDataLoader} options.dataLoader class name or implementation of a given loader */ constructor(renderer, gl, webglVersion, options) { //Set default blending to be MASK this.renderer = renderer; this.gl = gl; - this.glslBlendCode = "return background * (step(0.001, foreground.a));"; - - let Loader = options.dataLoader; - if (typeof Loader === "string") { - Loader = $.WebGLModule.Loaders[Loader]; - } - if (!Loader) { - throw("Unknown data loader: " + options.dataLoader); - } - if (!(Loader.prototype instanceof $.WebGLModule.IDataLoader)) { - throw("Incompatible texture loader used: " + options.dataLoader); - } - - this._texture = new Loader(gl, webglVersion, options); - if (!this.texture.supportsWebglVersion(this.getVersion())) { - throw("Incompatible texture loader version to the renderer context version! Context WebGL" + this.getVersion()); - } + this.options = options; } /** @@ -82,49 +74,81 @@ $.WebGLModule.WebGLImplementation = class { 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 {[number]} shaderDataIndexToGlobalDataIndex - * @param {boolean} withHtml whether html should be also created (false if no UI controls are desired) - * @return {object} compiled specification object ready to be used by the wrapper, with the following keys: - {string} object.vertexShader vertex shader code - {string} object.fragmentShader fragment shader code - {string} object.html html for the UI - {number} object.usableShaders how many layers are going to be visualised - {(array|string[])} object.dataUrls ID's of data in use (keys of visualisation.shaders object) in desired order - the data is guaranteed to arrive in this order (images stacked below each other in imageElement) + * @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(order, visualisation, shaderDataIndexToGlobalDataIndex, withHtml) { + 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 + * @param {OpenSeadragon.WebGLModule.RenderingConfig?} currentConfig JSON parameters used for this visualisation */ - programLoaded(program, currentConfig) { + 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 {string} id dataId + * @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} tileOpts.transform position of the rendered tile + * @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, id, tileOpts) { + 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 @@ -167,6 +191,55 @@ $.WebGLModule.WebGLImplementation = class { 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 { @@ -178,7 +251,32 @@ $.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { */ constructor(renderer, gl, options) { super(renderer, gl, "2.0", options); - this.emptyBuffer = gl.createBuffer(); + + // 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() { @@ -191,33 +289,40 @@ $.WebGLModule.WebGL20 = class extends $.WebGLModule.WebGLImplementation { return canvas.getContext('webgl2', options); } + getCompiled(program, name) { + return program._osdOptions[name]; + } + //todo try to implement on the global scope version-independntly - compileSpecification(order, visualisation, shaderDataIndexToGlobalDataIndex, withHtml) { + compileSpecification(program, order, specification, options) { var definition = "", execution = "", html = "", _this = this, usableShaders = 0, + dataCount = 0, globalScopeCode = {}; order.forEach(dataId => { - let layer = visualisation.shaders[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 (withHtml) { + if (options.withHtml) { html = _this.renderer.htmlShaderPartHeader(layer.name, layer.error, dataId, false, layer, false) + html; } - console.warn(layer.error, layer["desc"]); + $.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() + ` @@ -228,137 +333,308 @@ vec4 lid_${layer._index}_xo() { execution += ` vec4 l${layer._index}_out = lid_${layer._index}_xo(); l${layer._index}_out.a *= ${renderCtx.opacity.sample()}; - ${renderCtx.__mode}(l${layer._index}_out);`; + blend(l${layer._index}_out, ${renderCtx._blendUniform}, ${renderCtx._clipUniform});`; } else { execution += ` - ${renderCtx.__mode}(lid_${layer._index}_xo());`; + blend(lid_${layer._index}_xo(), ${renderCtx._blendUniform}, ${renderCtx._clipUniform});`; //todo remove ${renderCtx.__mode} } layer.rendering = true; visible = true; - OpenSeadragon.extend(globalScopeCode, _this.globalCodeRequiredByShaderType(layer.type)); + $.extend(globalScopeCode, _this.globalCodeRequiredByShaderType(layer.type)); + dataCount += layer.dataReferences.length; } //reverse order append to show first the last drawn element (top) - if (withHtml) { + if (options.withHtml) { html = _this.renderer.htmlShaderPartHeader(layer.name, layer._renderContext.htmlControls(), dataId, visible, layer, true) + html; } } else { - if (withHtml) { + if (options.withHtml) { html = _this.renderer.htmlShaderPartHeader(layer.name, - `The requested visualisation type does not work properly.`, dataId, false, layer, false) + html; + `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); + $.console.warn("Invalid shader part.", "Missing one of the required elements.", layer); } }); - return { - vertexShader: this.getVertexShader(), - fragmentShader: this.getFragmentShader(definition, execution, shaderDataIndexToGlobalDataIndex, globalScopeCode), - html: html, - usableShaders: usableShaders, - dataUrls: this.renderer._dataSources + 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; }; - } - getFragmentShader(definition, execution, shaderDataIndexToGlobalDataIndex, globalScopeCode) { - return `#version 300 es -precision mediump float; -precision mediump sampler2DArray; -precision mediump sampler2D; + const matrixType = options.instanceCount > 2 ? "in" : "uniform"; -${this.texture.declare(shaderDataIndexToGlobalDataIndex)} -uniform float pixel_size_in_fragments; -uniform float zoom_level; -uniform vec2 u_tile_size; -vec4 _last_rendered_color = vec4(.0); - -in vec2 tile_texture_coords; - -out vec4 final_color; - -bool close(float value, float target) { - return abs(target - value) < 0.001; -} - -void show(vec4 color) { - //premultiplied alpha blending - vec4 fg = _last_rendered_color; - _last_rendered_color = color; - vec4 pre_fg = vec4(fg.rgb * fg.a, fg.a); - final_color = pre_fg + final_color; -} - -vec4 blend_equation(in vec4 foreground, in vec4 background) { -${this.glslBlendCode} -} - -void blend_clip(vec4 foreground) { - _last_rendered_color = blend_equation(foreground, _last_rendered_color); -} - -void blend(vec4 foreground) { - show(_last_rendered_color); - final_color = blend_equation(foreground, final_color); - _last_rendered_color = vec4(.0); -} - -${Object.values(globalScopeCode).join("\n")} - -${definition} - -void main() { - ${execution} - - //blend last level - show(vec4(.0)); -}`; - } - - getVertexShader() { - //UNPACK_FLIP_Y_WEBGL not supported with 3D textures so sample bottom up - return `#version 300 es -precision mediump float; - -uniform mat3 transform_matrix; -out vec2 tile_texture_coords; + //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); -void main() { - vec3 vertex = quad[gl_VertexID]; - tile_texture_coords = vec2(vertex.x, -vertex.y); - gl_Position = vec4(transform_matrix * vertex, 1); -} -`; + return usableShaders; } - programLoaded(program, currentConfig) { + 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; } - let context = this.renderer, - gl = this.gl; - + const gl = this.gl; // Allow for custom loading gl.useProgram(program); - context.visualisationInUse(currentConfig); - context.glLoaded(gl, program, currentConfig); + if (currentConfig) { + this.renderer.glLoaded(gl, program, currentConfig); + } - //Note that the drawing strategy is not to resize canvas, and simply draw everyhing on squares - this.texture.programLoaded(context, gl, program, currentConfig); + // gl.bindVertexArray(this.vao); - //Empty ARRAY: get the vertices directly from the shader - gl.bindBuffer(gl.ARRAY_BUFFER, this.emptyBuffer); + 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, id, tileOpts) { + programUsed(program, currentConfig, texture, tileOpts = {}) { if (!this.renderer.running) { return; } @@ -367,19 +643,46 @@ void main() { let context = this.renderer, gl = this.gl; - context.glDrawing(gl, program, currentConfig, tileOpts); + if (currentConfig) { + context.glDrawing(gl, program, currentConfig, tileOpts); + } // Set Attributes for GLSL - gl.uniform1f(gl.getUniformLocation(program, "pixel_size_in_fragments"), tileOpts.pixelSize || 1); - gl.uniform1f(gl.getUniformLocation(program, "zoom_level"), tileOpts.zoom || 1); - gl.uniformMatrix3fv(gl.getUniformLocation(program, "transform_matrix"), false, - tileOpts.transform || OpenSeadragon.Mat3.makeIdentity()); + gl.uniform1f(this._locationPixelSize, tileOpts.pixelSize || 1); + gl.uniform1f(this._locationZoomLevel, tileOpts.zoom || 1); - // Upload textures - this.texture.programUsed(context, currentConfig, id, program, gl); + const options = program._osdOptions; + //if compiled as instanced drawing + if (options.instanceCount > 1) { - // Draw triangle strip (two triangles) from a static array defined in the vertex shader - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + 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); + } } }; diff --git a/test/demo/drawercomparison.js b/test/demo/drawercomparison.js index 17d178d5..eaf8bda3 100644 --- a/test/demo/drawercomparison.js +++ b/test/demo/drawercomparison.js @@ -129,6 +129,9 @@ $('#image-picker').sortable({ } }); +$('#image-picker').append(`
+ +
`); Object.keys(sources).forEach((key, index)=>{ let element = makeImagePickerElement(key, labels[key]) $('#image-picker').append(element); @@ -163,9 +166,10 @@ $('#image-picker input:not(.toggle)').on('change',function(){ }); function updateTiledImage(tiledImage, data, value, item){ + let field = data.field; + if(tiledImage){ //item = tiledImage - let field = data.field; if(field == 'x'){ let bounds = tiledImage.getBoundsNoRotate(); let position = new OpenSeadragon.Point(Number(value), bounds.y); @@ -198,14 +202,18 @@ function updateTiledImage(tiledImage, data, value, item){ } else { tiledImage.setClip(null); } - } - else if (field == 'debug'){ + } else if (field == 'debug'){ if( $(item).prop('checked') ){ tiledImage.debugMode = true; } else { tiledImage.debugMode = false; } } + } else { + //viewer-level option + if (field == "blend-time") { + //todo + } } } @@ -339,6 +347,7 @@ function makeImagePickerElement(key, label){ + From 6447009c18170eedd8d19e3ac588cb9e1317ee3e Mon Sep 17 00:00:00 2001 From: Aiosa Date: Mon, 23 Oct 2023 16:05:18 +0200 Subject: [PATCH 5/8] Remove blend time from comparison demo (not functional). --- test/demo/drawercomparison.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/demo/drawercomparison.js b/test/demo/drawercomparison.js index eaf8bda3..7c006f22 100644 --- a/test/demo/drawercomparison.js +++ b/test/demo/drawercomparison.js @@ -129,9 +129,6 @@ $('#image-picker').sortable({ } }); -$('#image-picker').append(`
- -
`); Object.keys(sources).forEach((key, index)=>{ let element = makeImagePickerElement(key, labels[key]) $('#image-picker').append(element); @@ -211,9 +208,6 @@ function updateTiledImage(tiledImage, data, value, item){ } } else { //viewer-level option - if (field == "blend-time") { - //todo - } } } From f4efe2970a79e07f83af2a36fe1e8c63b3e07d76 Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 21 Nov 2023 12:54:09 +0100 Subject: [PATCH 6/8] Remove modular implementation -> will be introduced in subsequent PR. --- src/webgl/dataLoader.js.deprecated | 545 ----------- src/webgl/drawer.js | 662 -------------- src/webgl/plainShader.js | 42 - src/webgl/renderer.js | 902 ------------------ src/webgl/shaderLayer.js | 1373 ---------------------------- src/webgl/webGLContext.js | 689 -------------- 6 files changed, 4213 deletions(-) delete mode 100644 src/webgl/dataLoader.js.deprecated delete mode 100644 src/webgl/drawer.js delete mode 100644 src/webgl/plainShader.js delete mode 100644 src/webgl/renderer.js delete mode 100644 src/webgl/shaderLayer.js delete mode 100644 src/webgl/webGLContext.js 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); From 23496cb049ddc8b8a858e0f142091addc3d20aba Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 21 Nov 2023 12:56:19 +0100 Subject: [PATCH 7/8] Remove also file references. --- Gruntfile.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9ffaea7d..fc440893 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -66,13 +66,6 @@ module.exports = function(grunt) { "src/tiledimage.js", "src/tilecache.js", "src/world.js", - - //Aiosa's webgl drawer - needs optimization, polishing, trimming - "src/webgl/renderer.js", - "src/webgl/shaderLayer.js", - "src/webgl/webGLContext.js", - "src/webgl/drawer.js", - "src/webgl/plainShader.js", ]; var banner = "//! <%= pkg.name %> <%= pkg.version %>\n" + From 191ba39cbb8fe7baf5b21a80cf61f22fbd6a6179 Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 21 Nov 2023 20:53:06 +0100 Subject: [PATCH 8/8] Remove demo references to the modular renderer. --- test/demo/drawercomparison.js | 3 +-- test/demo/drawerperformance.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/demo/drawercomparison.js b/test/demo/drawercomparison.js index 7c006f22..03720816 100644 --- a/test/demo/drawercomparison.js +++ b/test/demo/drawercomparison.js @@ -15,8 +15,7 @@ const labels = { } const drawers = { canvas: "Context2d drawer (default in OSD <= 4.1.0)", - webgl: "New WebGL drawer", - universal_webgl: "New WebGL (Modular)" + webgl: "New WebGL drawer" } //Support drawer type from the url diff --git a/test/demo/drawerperformance.js b/test/demo/drawerperformance.js index 03d6b946..0fa6bfef 100644 --- a/test/demo/drawerperformance.js +++ b/test/demo/drawerperformance.js @@ -16,7 +16,6 @@ const labels = { const drawers = { canvas: "Context2d drawer (default in OSD <= 4.1.0)", webgl: "New WebGL drawer", - universal_webgl: "New WebGL (Modular)", html: "" }