Remove modular implementation -> will be introduced in subsequent PR.

This commit is contained in:
Aiosa 2023-11-21 12:54:09 +01:00
parent 6447009c18
commit f4efe2970a
6 changed files with 0 additions and 4213 deletions

View File

@ -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 <image*>
*
* Implemented texture loaders support
* - working with <image*> object - image data chunks are vertically concatenated
* - working with [<image*>] 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);

View File

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

View File

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

View File

@ -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.<string, OpenSeadragon.WebGLModule.ShaderLayerConfig>
* }} OpenSeadragon.WebGLModule.RenderingConfig
*
* //use_channel[X] name
* @template {Object<string,any>} TUseChannel
* //use_[fitler_name]
* @template {Object<string,number>} TUseFilter
* @template {Object<string,(string|any)>} 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 `<div class="configurable-border"><div class="shader-part-name">${title}</div>${html}</div>`;
};
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 `<div id="test-inner-${this.uniqueId}-webgl">
<b>WebGL Processing I/O (debug mode)</b>
<div id="test-${this.uniqueId}-webgl-log"></div>
Input: <br><div style="border: 1px solid;display: inline-block; overflow: auto;" id='test-${this.uniqueId}-webgl-input'>No input.</div><br>
Output:<br><div style="border: 1px solid;display: inline-block; overflow: auto;" id="test-${this.uniqueId}-webgl-output">No output.</div>`;
}
_loadDebugInfo() {
if (!this.debug) {
return;
}
let container = document.getElementById(`test-${this.uniqueId}-webgl`);
if (!container) {
if (!this.htmlControlsId) {
document.body.innerHTML += `<div id="test-${this.uniqueId}-webgl" style="position:absolute; top:0; right:0; width: 250px">${this._getDebugInfoPanel()}</div>`;
} else {
//safe as we do this before handlers are attached
document.getElementById(this.htmlControlsId).parentElement.innerHTML += `<div id="test-${this.uniqueId}-webgl" style="width: 100%;">${this._getDebugInfoPanel()}</div>`;
}
}
}
_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.
<br><b>Specification setup:</b></br> <code>${JSON.stringify(specification, $.WebGLModule.jsonReplacer)}</code>
<br><b>Dynamic shader data:</b></br><code>${JSON.stringify(specification.data)}</code>`);
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);

File diff suppressed because it is too large Load Diff

View File

@ -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 <key: code> 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
* // <<use some_blending_name_etc() to blend input onto output color of the shader using a clipping mask>>
* }
*
* void blend(vec4 input) { //must be called blend, API
* // <<use some_blending_name_etc() to blend input onto output color of the shader>>
* }
*
* //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);