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){ +