From 034e0be76fd52fafccc148a0f2b038c1f6b342e1 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sat, 1 Jun 2019 16:22:12 +0200 Subject: [PATCH 01/14] Vector Source / index all features by uid Previously features were indexed by uid only when they did not have a defined id. A new method was added: `getFeatureByUid`. This is not part of the public API. This will facilitate the lookup of features for hit detection. --- src/ol/source/Vector.js | 45 ++++++++++++++----------- test/spec/ol/source/vector.test.js | 54 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index 23aa25a30c..9f4a0f39f5 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -241,11 +241,11 @@ class VectorSource extends Source { this.idIndex_ = {}; /** - * A lookup of features without id (keyed by getUid(feature)). + * A lookup of features by uid (using getUid(feature)). * @private * @type {!Object>} */ - this.undefIdIndex_ = {}; + this.uidIndex_ = {}; /** * @private @@ -359,10 +359,11 @@ class VectorSource extends Source { } else { valid = false; } - } else { - assert(!(featureKey in this.undefIdIndex_), + } + if (valid) { + assert(!(featureKey in this.uidIndex_), 30); // The passed `feature` was already added to the source - this.undefIdIndex_[featureKey] = feature; + this.uidIndex_[featureKey] = feature; } return valid; } @@ -489,7 +490,7 @@ class VectorSource extends Source { if (!this.featuresCollection_) { this.featureChangeKeys_ = {}; this.idIndex_ = {}; - this.undefIdIndex_ = {}; + this.uidIndex_ = {}; } } else { if (this.featuresRtree_) { @@ -770,6 +771,18 @@ class VectorSource extends Source { } + /** + * Get a feature by its internal unique identifier (using `getUid`). + * + * @param {string} uid Feature identifier. + * @return {import("../Feature.js").default} The feature (or `null` if not found). + */ + getFeatureByUid(uid) { + const feature = this.uidIndex_[uid]; + return feature !== undefined ? feature : null; + } + + /** * Get the format associated with this source. * @@ -831,20 +844,13 @@ class VectorSource extends Source { const id = feature.getId(); if (id !== undefined) { const sid = id.toString(); - if (featureKey in this.undefIdIndex_) { - delete this.undefIdIndex_[featureKey]; + if (this.idIndex_[sid] !== feature) { + this.removeFromIdIndex_(feature); this.idIndex_[sid] = feature; - } else { - if (this.idIndex_[sid] !== feature) { - this.removeFromIdIndex_(feature); - this.idIndex_[sid] = feature; - } } } else { - if (!(featureKey in this.undefIdIndex_)) { - this.removeFromIdIndex_(feature); - this.undefIdIndex_[featureKey] = feature; - } + this.removeFromIdIndex_(feature); + this.uidIndex_[featureKey] = feature; } this.changed(); this.dispatchEvent(new VectorSourceEvent( @@ -862,7 +868,7 @@ class VectorSource extends Source { if (id !== undefined) { return id in this.idIndex_; } else { - return getUid(feature) in this.undefIdIndex_; + return getUid(feature) in this.uidIndex_; } } @@ -964,9 +970,8 @@ class VectorSource extends Source { const id = feature.getId(); if (id !== undefined) { delete this.idIndex_[id.toString()]; - } else { - delete this.undefIdIndex_[featureKey]; } + delete this.uidIndex_[featureKey]; this.dispatchEvent(new VectorSourceEvent( VectorEventType.REMOVEFEATURE, feature)); } diff --git a/test/spec/ol/source/vector.test.js b/test/spec/ol/source/vector.test.js index d48d2962a3..9fdb56a983 100644 --- a/test/spec/ol/source/vector.test.js +++ b/test/spec/ol/source/vector.test.js @@ -10,6 +10,7 @@ import {bbox as bboxStrategy} from '../../../../src/ol/loadingstrategy.js'; import {get as getProjection, transformExtent, fromLonLat} from '../../../../src/ol/proj.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; import GeoJSON from '../../../../src/ol/format/GeoJSON.js'; +import {getUid} from '../../../../src/ol/util.js'; describe('ol.source.Vector', function() { @@ -519,6 +520,59 @@ describe('ol.source.Vector', function() { }); + describe('#getFeatureByUid()', function() { + let source; + beforeEach(function() { + source = new VectorSource(); + }); + + it('returns a feature with an id', function() { + const feature = new Feature(); + feature.setId('abcd'); + source.addFeature(feature); + expect(source.getFeatureByUid(getUid(feature))).to.be(feature); + }); + + it('returns a feature without id', function() { + const feature = new Feature(); + source.addFeature(feature); + expect(source.getFeatureByUid(getUid(feature))).to.be(feature); + }); + + it('returns null when no feature is found', function() { + const feature = new Feature(); + feature.setId('abcd'); + source.addFeature(feature); + const wrongId = 'abcd'; + expect(source.getFeatureByUid(wrongId)).to.be(null); + }); + + it('returns null after removing feature', function() { + const feature = new Feature(); + feature.setId('abcd'); + source.addFeature(feature); + const uid = getUid(feature); + expect(source.getFeatureByUid(uid)).to.be(feature); + source.removeFeature(feature); + expect(source.getFeatureByUid(uid)).to.be(null); + }); + + it('returns null after clear', function() { + const feature = new Feature(); + feature.setId('abcd'); + source.addFeature(feature); + const uid = getUid(feature); + expect(source.getFeatureByUid(uid)).to.be(feature); + source.clear(); + expect(source.getFeatureByUid(uid)).to.be(null); + }); + + it('returns null when no features are present', function() { + expect(source.getFeatureByUid('abcd')).to.be(null); + }); + + }); + describe('#loadFeatures', function() { describe('with the "bbox" strategy', function() { From 8145b358c0045a9ab18d8f3913d7372fd1065c9a Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 19 May 2019 11:55:54 +0200 Subject: [PATCH 02/14] Webgl renderer / add id encode/decode utils --- src/ol/renderer/webgl/Layer.js | 36 +++++++++++++++++++++++ test/spec/ol/renderer/webgl/layer.test.js | 33 +++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index b50b54d1b2..60b6344a34 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -238,4 +238,40 @@ export function getBlankTexture() { return image; } +/** + * Generates a color array based on a numerical id + * Note: the range for each component is 0 to 1 with 256 steps + * @param {number} id Id + * @param {Array} [opt_array] Reusable array + * @return {Array} Color array containing the encoded id + */ +export function colorEncodeId(id, opt_array) { + const array = opt_array || []; + const radix = 256; + const divide = radix - 1; + array[0] = Math.floor(id / radix / radix / radix) / divide; + array[1] = (Math.floor(id / radix / radix) % radix) / divide; + array[2] = (Math.floor(id / radix) % radix) / divide; + array[3] = (id % radix) / divide; + return array; +} + + +/** + * Reads an id from a color-encoded array + * Note: the expected range for each component is 0 to 1 with 256 steps. + * @param {Array} color Color array containing the encoded id + * @return {number} Decoded id + */ +export function colorDecodeId(color) { + let id = 0; + const radix = 256; + const mult = radix - 1; + id += Math.round(color[0] * radix * radix * radix * mult); + id += Math.round(color[1] * radix * radix * mult); + id += Math.round(color[2] * radix * mult); + id += Math.round(color[3] * mult); + return id; +} + export default WebGLLayerRenderer; diff --git a/test/spec/ol/renderer/webgl/layer.test.js b/test/spec/ol/renderer/webgl/layer.test.js index cb9f30f3c1..fdde057f6b 100644 --- a/test/spec/ol/renderer/webgl/layer.test.js +++ b/test/spec/ol/renderer/webgl/layer.test.js @@ -1,4 +1,6 @@ import WebGLLayerRenderer, { + colorDecodeId, + colorEncodeId, getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, writePointFeatureInstructions, writePointFeatureToBuffers } from '../../../../../src/ol/renderer/webgl/Layer.js'; @@ -249,4 +251,35 @@ describe('ol.renderer.webgl.Layer', function() { }); }); + describe('colorEncodeId and colorDecodeId', function() { + it('correctly encodes and decodes ids', function() { + expect(colorDecodeId(colorEncodeId(0))).to.eql(0); + expect(colorDecodeId(colorEncodeId(1))).to.eql(1); + expect(colorDecodeId(colorEncodeId(123))).to.eql(123); + expect(colorDecodeId(colorEncodeId(12345))).to.eql(12345); + expect(colorDecodeId(colorEncodeId(123456))).to.eql(123456); + expect(colorDecodeId(colorEncodeId(91612))).to.eql(91612); + expect(colorDecodeId(colorEncodeId(1234567890))).to.eql(1234567890); + }); + + it('correctly reuses array', function() { + const arr = []; + expect(colorEncodeId(123, arr)).to.be(arr); + }); + + it('is compatible with Uint8Array storage', function() { + const encoded = colorEncodeId(91612); + const typed = Uint8Array.of(encoded[0] * 255, encoded[1] * 255, + encoded[2] * 255, encoded[3] * 255); + const arr = [ + typed[0] / 255, + typed[1] / 255, + typed[2] / 255, + typed[3] / 255 + ]; + const decoded = colorDecodeId(arr); + expect(decoded).to.eql(91612); + }); + }); + }); From 2b5e5459ab7533068a61b38ad51634319b799bec Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 19 May 2019 13:28:48 +0200 Subject: [PATCH 03/14] Webgl points / add hit detection buffers generation Hit detection is done by rendering features with their id encoded in the color attribute. A parallel set of render instructions and a second vertex buffer is used specifically for that. --- src/ol/renderer/webgl/PointsLayer.js | 124 +++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 17 deletions(-) diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 05dab3ef5f..88b02024e1 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -6,6 +6,7 @@ import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../web import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js'; import GeometryType from '../../geom/GeometryType.js'; import WebGLLayerRenderer, { + colorEncodeId, getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType, writePointFeatureInstructions @@ -19,6 +20,7 @@ import { apply as applyTransform } from '../../transform.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; +import {getUid} from '../../util.js'; const VERTEX_SHADER = ` precision mediump float; @@ -68,6 +70,26 @@ const FRAGMENT_SHADER = ` gl_FragColor.rgb *= gl_FragColor.a; }`; +const HIT_FRAGMENT_SHADER = ` + precision mediump float; + + uniform sampler2D u_texture; + + varying vec2 v_texCoord; + varying float v_opacity; + varying vec4 v_color; + + void main(void) { + if (v_opacity == 0.0) { + discard; + } + vec4 textureColor = texture2D(u_texture, v_texCoord); + if (textureColor.a < 0.1) { + discard; + } + gl_FragColor = v_color; + }`; + /** * @typedef {Object} Options * @property {function(import("../../Feature").default):number} [sizeCallback] Will be called on every feature in the @@ -212,6 +234,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { this.sourceRevision_ = -1; this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW); + this.hitVerticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW); this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW); this.program_ = this.helper.getProgram( @@ -274,21 +297,49 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { */ this.renderInstructions_ = new Float32Array(0); + /** + * @type {import("../../transform.js").Transform} + * @private + */ + this.hitRenderTransform_ = createTransform(); + + /** + * @type {import("../../transform.js").Transform} + * @private + */ + this.invertHitRenderTransform_ = createTransform(); + + /** + * These instructions are used for hit detection + * @type {Float32Array} + * @private + */ + this.hitRenderInstructions_ = new Float32Array(0); + this.worker_ = createWebGLWorker(); this.worker_.addEventListener('message', function(event) { const received = event.data; if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) { const projectionTransform = received.projectionTransform; - this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer); + if (received.hitDetection) { + this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer); + this.helper.flushBufferData(this.hitVerticesBuffer_); + } else { + this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer); + this.helper.flushBufferData(this.verticesBuffer_); + } this.indicesBuffer_.fromArrayBuffer(received.indexBuffer); - this.helper.flushBufferData(this.verticesBuffer_); this.helper.flushBufferData(this.indicesBuffer_); - // saves the projection transform for the current frame state - this.renderTransform_ = projectionTransform; - makeInverseTransform(this.invertRenderTransform_, this.renderTransform_); - - this.renderInstructions_ = new Float32Array(event.data.renderInstructions); + if (received.hitDetection) { + this.hitRenderTransform_ = projectionTransform; + makeInverseTransform(this.invertHitRenderTransform_, this.hitRenderTransform_); + this.hitRenderInstructions_ = new Float32Array(event.data.renderInstructions); + } else { + this.renderTransform_ = projectionTransform; + makeInverseTransform(this.invertRenderTransform_, this.renderTransform_); + this.renderInstructions_ = new Float32Array(event.data.renderInstructions); + } } }.bind(this)); } @@ -384,11 +435,16 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) { this.renderInstructions_ = new Float32Array(totalInstructionsCount); } + if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalInstructionsCount) { + this.hitRenderInstructions_ = new Float32Array(totalInstructionsCount); + } // loop on features to fill the buffer let feature; const tmpCoords = []; + const tmpColor = []; let elementIndex = 0; + let u0, v0, u1, v1, size, opacity, rotateWithView, color; for (let i = 0; i < features.length; i++) { feature = features[i]; if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) { @@ -399,19 +455,43 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { tmpCoords[1] = this.coordCallback_(feature, 1); applyTransform(projectionTransform, tmpCoords); - elementIndex = writePointFeatureInstructions( + u0 = this.texCoordCallback_(feature, 0); + v0 = this.texCoordCallback_(feature, 1); + u1 = this.texCoordCallback_(feature, 2); + v1 = this.texCoordCallback_(feature, 3); + size = this.sizeCallback_(feature); + opacity = this.opacityCallback_(feature); + rotateWithView = this.rotateWithViewCallback_(feature); + color = this.colorCallback_(feature, this.colorArray_); + + writePointFeatureInstructions( this.renderInstructions_, elementIndex, tmpCoords[0], tmpCoords[1], - this.texCoordCallback_(feature, 0), - this.texCoordCallback_(feature, 1), - this.texCoordCallback_(feature, 2), - this.texCoordCallback_(feature, 3), - this.sizeCallback_(feature), - this.opacityCallback_(feature), - this.rotateWithViewCallback_(feature), - this.colorCallback_(feature, this.colorArray_) + u0, + v0, + u1, + v1, + size, + opacity, + rotateWithView, + color + ); + + elementIndex = writePointFeatureInstructions( + this.hitRenderInstructions_, + elementIndex, + tmpCoords[0], + tmpCoords[1], + u0, + v0, + u1, + v1, + size, + opacity, + rotateWithView, + colorEncodeId(parseInt(getUid(feature)), tmpColor) ); } @@ -422,9 +502,19 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { }; // additional properties will be sent back as-is by the worker message['projectionTransform'] = projectionTransform; - this.worker_.postMessage(message, [this.renderInstructions_.buffer]); this.renderInstructions_ = null; + + /** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */ + const hitMessage = { + type: WebGLWorkerMessageType.GENERATE_BUFFERS, + renderInstructions: this.hitRenderInstructions_.buffer, + useShortIndices: !this.helper.getElementIndexUintEnabled() + }; + hitMessage['projectionTransform'] = projectionTransform; + hitMessage['hitDetection'] = true; + this.worker_.postMessage(hitMessage, [this.hitRenderInstructions_.buffer]); + this.hitRenderInstructions_ = null; } From b6425187de6b3bcab483ea8be6be5637b665da21 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sat, 1 Jun 2019 23:09:50 +0200 Subject: [PATCH 04/14] Remove unused WebGLShader class --- src/ol/webgl/Shader.js | 48 ------------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 src/ol/webgl/Shader.js diff --git a/src/ol/webgl/Shader.js b/src/ol/webgl/Shader.js deleted file mode 100644 index ed9e0f0566..0000000000 --- a/src/ol/webgl/Shader.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @module ol/webgl/Shader - */ -import {abstract} from '../util.js'; - -/** - * @abstract - */ -class WebGLShader { - - /** - * @param {string} source Source. - */ - constructor(source) { - - /** - * @private - * @type {string} - */ - this.source_ = source; - - } - - /** - * @return {boolean} Is animated? - */ - isAnimated() { - return false; - } - - /** - * @abstract - * @return {number} Type. - */ - getType() { - return abstract(); - } - - /** - * @return {string} Source. - */ - getSource() { - return this.source_; - } -} - - -export default WebGLShader; From f25a16d90c83d072fde76b63e7a44adb7ad888c4 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 19 May 2019 15:25:59 +0200 Subject: [PATCH 05/14] Webgl helper / rework create texture utils Now only one util is available: `createTexture`, which is tested and allows binding an image and reusing an existing texture. --- src/ol/webgl/Helper.js | 71 +++++++++++-------------------- test/spec/ol/webgl/helper.test.js | 69 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 1bc6e2d0db..eb1771864f 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -742,57 +742,36 @@ class WebGLHelper extends Disposable { // TODO: shutdown program /** - * TODO: these are not used and should be reworked - * @param {number=} opt_wrapS wrapS. - * @param {number=} opt_wrapT wrapT. - * @return {WebGLTexture} The texture. + * Will create or reuse a given webgl texture and apply the given size. If no image data + * specified, the texture will be empty, otherwise image data will be used and the `size` + * parameter will be ignored. + * Note: wrap parameters are set to clamp to edge, min filter is set to linear. + * @param {Array} size Expected size of the texture + * @param {ImageData|HTMLImageElement|HTMLCanvasElement} [opt_data] Image data/object to bind to the texture + * @param {WebGLTexture} [opt_texture] Existing texture to reuse + * @return {WebGLTexture} The generated texture + * @api */ - createTextureInternal(opt_wrapS, opt_wrapT) { + createTexture(size, opt_data, opt_texture) { const gl = this.getGL(); - const texture = gl.createTexture(); + const texture = opt_texture || gl.createTexture(); + + // set params & size + const level = 0; + const internalFormat = gl.RGBA; + const border = 0; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + if (opt_data) { + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, format, type, opt_data); + } else { + gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, size[0], size[1], border, format, type, null); + } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - if (opt_wrapS !== undefined) { - gl.texParameteri( - gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, opt_wrapS); - } - if (opt_wrapT !== undefined) { - gl.texParameteri( - gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, opt_wrapT); - } - - return texture; - } - - /** - * TODO: these are not used and should be reworked - * @param {number} width Width. - * @param {number} height Height. - * @param {number=} opt_wrapS wrapS. - * @param {number=} opt_wrapT wrapT. - * @return {WebGLTexture} The texture. - */ - createEmptyTexture(width, height, opt_wrapS, opt_wrapT) { - const gl = this.getGL(); - const texture = this.createTextureInternal(opt_wrapS, opt_wrapT); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); - return texture; - } - - - /** - * TODO: these are not used and should be reworked - * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image. - * @param {number=} opt_wrapS wrapS. - * @param {number=} opt_wrapT wrapT. - * @return {WebGLTexture} The texture. - */ - createTexture(image, opt_wrapS, opt_wrapT) { - const gl = this.getGL(); - const texture = this.createTextureInternal(opt_wrapS, opt_wrapT); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); return texture; } } diff --git a/test/spec/ol/webgl/helper.test.js b/test/spec/ol/webgl/helper.test.js index c474941c60..e53f4c2d0d 100644 --- a/test/spec/ol/webgl/helper.test.js +++ b/test/spec/ol/webgl/helper.test.js @@ -220,5 +220,74 @@ describe('ol.webgl.WebGLHelper', function() { }); }); + describe('#createTexture', function() { + let h; + beforeEach(function() { + h = new WebGLHelper(); + }); + + it('creates an empty texture from scratch', function() { + const width = 4; + const height = 4; + const t = h.createTexture([width, height]); + const gl = h.getGL(); + + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0); + const data = new Uint8Array(width * height * 4); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); + gl.deleteFramebuffer(fb); + + expect(data[0]).to.eql(0); + expect(data[1]).to.eql(0); + expect(data[2]).to.eql(0); + expect(data[3]).to.eql(0); + expect(data[4]).to.eql(0); + expect(data[5]).to.eql(0); + expect(data[6]).to.eql(0); + expect(data[7]).to.eql(0); + }); + + it('creates a texture from image data', function() { + const width = 4; + const height = 4; + const canvas = document.createElement('canvas'); + const image = canvas.getContext('2d').createImageData(width, height); + for (let i = 0; i < image.data.length; i += 4) { + image.data[i] = 100; + image.data[i + 1] = 150; + image.data[i + 2] = 200; + image.data[i + 3] = 250; + } + const t = h.createTexture([width, height], image); + const gl = h.getGL(); + + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0); + const data = new Uint8Array(width * height * 4); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); + gl.deleteFramebuffer(fb); + + expect(data[0]).to.eql(100); + expect(data[1]).to.eql(150); + expect(data[2]).to.eql(200); + expect(data[3]).to.eql(250); + expect(data[4]).to.eql(100); + expect(data[5]).to.eql(150); + expect(data[6]).to.eql(200); + expect(data[7]).to.eql(250); + }); + + it('reuses a given texture', function() { + const width = 4; + const height = 4; + const gl = h.getGL(); + const t1 = gl.createTexture(); + const t2 = h.createTexture([width, height], undefined, t1); + expect(t1).to.be(t2); + }); + }); }); }); From 1257ade199eb6011b3e32dac0cda24776ab018cd Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 19 May 2019 17:15:49 +0200 Subject: [PATCH 06/14] Webgl renderer / rename function to avoid confusion `getBlankTexture` was too close to `WebGLHelper#createTexture` --- src/ol/renderer/webgl/Layer.js | 2 +- src/ol/renderer/webgl/PointsLayer.js | 4 ++-- test/spec/ol/renderer/webgl/layer.test.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index 60b6344a34..eb404953b6 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -231,7 +231,7 @@ export function writePointFeatureToBuffers(instructions, elementIndex, vertexBuf * @private * @return {ImageData} Image data. */ -export function getBlankTexture() { +export function getBlankImageData() { const canvas = document.createElement('canvas'); const image = canvas.getContext('2d').createImageData(1, 1); image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255; diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 88b02024e1..60778337dc 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -7,7 +7,7 @@ import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js'; import GeometryType from '../../geom/GeometryType.js'; import WebGLLayerRenderer, { colorEncodeId, - getBlankTexture, + getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType, writePointFeatureInstructions } from './Layer.js'; @@ -222,7 +222,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { const options = opt_options || {}; const uniforms = options.uniforms || {}; - uniforms.u_texture = options.texture || getBlankTexture(); + uniforms.u_texture = options.texture || getBlankImageData(); const projectionMatrixTransform = createTransform(); uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform; diff --git a/test/spec/ol/renderer/webgl/layer.test.js b/test/spec/ol/renderer/webgl/layer.test.js index fdde057f6b..8bbd676850 100644 --- a/test/spec/ol/renderer/webgl/layer.test.js +++ b/test/spec/ol/renderer/webgl/layer.test.js @@ -1,7 +1,7 @@ import WebGLLayerRenderer, { colorDecodeId, colorEncodeId, - getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, + getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, writePointFeatureInstructions, writePointFeatureToBuffers } from '../../../../../src/ol/renderer/webgl/Layer.js'; import Layer from '../../../../../src/ol/layer/Layer.js'; @@ -239,9 +239,9 @@ describe('ol.renderer.webgl.Layer', function() { }); - describe('getBlankTexture', function() { + describe('getBlankImageData', function() { it('creates a 1x1 white texture', function() { - const texture = getBlankTexture(); + const texture = getBlankImageData(); expect(texture.height).to.eql(1); expect(texture.width).to.eql(1); expect(texture.data[0]).to.eql(255); From 6224d749c440e43502296dd7323d2f25dbce1c27 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sat, 1 Jun 2019 23:58:00 +0200 Subject: [PATCH 07/14] WebGL / Introduced the WebGLRenderTarget class This utility class simplifies rendering to a texture & reading the results of the render. It also allows clearing its content before a new render. --- src/ol/webgl/RenderTarget.js | 124 ++++++++++++++++++++++++ test/spec/ol/webgl/rendertarget.test.js | 80 +++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/ol/webgl/RenderTarget.js create mode 100644 test/spec/ol/webgl/rendertarget.test.js diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js new file mode 100644 index 0000000000..c9cde76b0d --- /dev/null +++ b/src/ol/webgl/RenderTarget.js @@ -0,0 +1,124 @@ +/** + * A wrapper class to simplify rendering to a texture instead of the final canvas + * @module ol/webgl/RenderTarget + */ +import {equals} from '../array.js'; + + +/** + * @classdesc + * This class is a wrapper around the association of both a `WebGLTexture` and a `WebGLFramebuffer` instances, + * simplifying initialization and binding for rendering. + * @api + */ +class WebGLRenderTarget { + + /** + * @param {import("./Helper.js").default} helper WebGL helper; mandatory. + * @param {Array} [opt_size] Expected size of the render target texture; note: this can be changed later on. + */ + constructor(helper, opt_size) { + /** + * @private + * @type {import("./Helper.js").default} + */ + this.helper_ = helper; + const gl = helper.getGL(); + + /** + * @private + * @type {WebGLTexture} + */ + this.texture_ = gl.createTexture(); + + /** + * @private + * @type {WebGLFramebuffer} + */ + this.framebuffer_ = gl.createFramebuffer(); + + /** + * @type {Array} + * @private + */ + this.size_ = opt_size || [1, 1]; + + /** + * @type {Uint8Array} + * @private + */ + this.data_ = new Uint8Array(1); + + this.updateSize_(); + } + + /** + * Changes the size of the render target texture. Note: will do nothing if the size + * is already the same. + * @param {Array} size Expected size of the render target texture + * @api + */ + setSize(size) { + if (equals(size, this.size_)) { + return; + } + this.size_[0] = size[0]; + this.size_[1] = size[1]; + this.updateSize_(); + } + + /** + * Returns the size of the render target texture + * @return {Array} Size of the render target texture + * @api + */ + getSize() { + return this.size_; + } + + /** + * Returns the content of the render target texture as raw data (series of r,g,b,a values) + * @return {Uint8Array} Integer array of color values + * @api + */ + read() { + const size = this.size_; + const gl = this.helper_.getGL(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_); + gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.UNSIGNED_BYTE, this.data_); + return this.data_; + } + + /** + * @return {WebGLTexture} Texture to render to + */ + getTexture() { + return this.texture_; + } + + /** + * @return {WebGLFramebuffer} Frame buffer of the render target + */ + getFramebuffer() { + return this.framebuffer_; + } + + /** + * @private + */ + updateSize_() { + const size = this.size_; + const gl = this.helper_.getGL(); + + this.texture_ = this.helper_.createTexture(size, null, this.texture_); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_); + gl.viewport(0, 0, size[0], size[1]); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture_, 0); + + this.data_ = new Uint8Array(size[0] * size[1] * 4); + } +} + +export default WebGLRenderTarget; diff --git a/test/spec/ol/webgl/rendertarget.test.js b/test/spec/ol/webgl/rendertarget.test.js new file mode 100644 index 0000000000..d405fe9fab --- /dev/null +++ b/test/spec/ol/webgl/rendertarget.test.js @@ -0,0 +1,80 @@ +import WebGLRenderTarget from '../../../../src/ol/webgl/RenderTarget.js'; +import WebGLHelper from '../../../../src/ol/webgl/Helper.js'; + + +describe('ol.webgl.RenderTarget', function() { + let helper, testImage_4x4; + + beforeEach(function() { + helper = new WebGLHelper(); + + const canvas = document.createElement('canvas'); + testImage_4x4 = canvas.getContext('2d').createImageData(4, 4); + for (let i = 0; i < testImage_4x4.data.length; i += 4) { + testImage_4x4.data[i] = 100; + testImage_4x4.data[i + 1] = 150; + testImage_4x4.data[i + 2] = 200; + testImage_4x4.data[i + 3] = 250; + } + }); + + describe('constructor', function() { + + it('creates a target of size 1x1', function() { + const rt = new WebGLRenderTarget(helper); + expect(rt.getSize()).to.eql([1, 1]); + }); + + it('creates a target of specified size', function() { + const rt = new WebGLRenderTarget(helper, [12, 34]); + expect(rt.getSize()).to.eql([12, 34]); + }); + + }); + + describe('#setSize', function() { + + it('updates the target size', function() { + const rt = new WebGLRenderTarget(helper, [12, 34]); + expect(rt.getSize()).to.eql([12, 34]); + rt.setSize([45, 67]); + expect(rt.getSize()).to.eql([45, 67]); + }); + + it('does nothing if the size has not changed', function() { + const rt = new WebGLRenderTarget(helper, [12, 34]); + const spy = sinon.spy(rt, 'updateSize_'); + rt.setSize([12, 34]); + expect(spy.called).to.be(false); + rt.setSize([12, 345]); + expect(spy.called).to.be(true); + }); + + }); + + describe('#readData', function() { + + it('returns 1-pixel data with the default options', function() { + const rt = new WebGLRenderTarget(helper); + expect(rt.read().length).to.eql(4); + }); + + it('returns the content of the texture', function() { + const rt = new WebGLRenderTarget(helper, [4, 4]); + helper.createTexture([4, 4], testImage_4x4, rt.getTexture()); + const data = rt.read(); + + expect(data[0]).to.eql(100); + expect(data[1]).to.eql(150); + expect(data[2]).to.eql(200); + expect(data[3]).to.eql(250); + expect(data[4]).to.eql(100); + expect(data[5]).to.eql(150); + expect(data[6]).to.eql(200); + expect(data[7]).to.eql(250); + expect(data.length).to.eql(4 * 4 * 4); + }); + + }); + +}); From 38920867fba371644991724f80666856967daf93 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Sun, 2 Jun 2019 10:33:32 +0200 Subject: [PATCH 08/14] Webgl Helper / Add a method to prepare drawing to render targets --- src/ol/webgl/Helper.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index eb1771864f..e844e255ae 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -432,6 +432,30 @@ class WebGLHelper extends Disposable { gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.useProgram(this.currentProgram_); + this.applyFrameState(frameState); + this.applyUniforms(frameState); + } + + /** + * Clear the render target & bind it for future draw operations. + * This is similar to `prepareDraw`, only post processes will not be applied. + * Note: the whole viewport will be drawn to the render target, regardless of its size. + * @param {import("../PluggableMap.js").FrameState} frameState current frame state + * @param {import("./RenderTarget.js").default} renderTarget Render target to draw to + * @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen. + */ + prepareDrawToRenderTarget(frameState, renderTarget, opt_disableAlphaBlend) { + const gl = this.getGL(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, renderTarget.getFramebuffer()); + gl.bindTexture(gl.TEXTURE_2D, renderTarget.getTexture()); + gl.clearColor(0.0, 0.0, 0.0, 0.0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA); + + gl.useProgram(this.currentProgram_); this.applyFrameState(frameState); this.applyUniforms(frameState); } From 5ffca0633c1751a9d20e7defc463a29ea1608e51 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 3 Jun 2019 09:17:00 +0200 Subject: [PATCH 09/14] Webgl Points / Add support for feature hit detection For now only `forEachFeatureAtCoordinate` is implemented. Each time the viewport is rendered, another similar render pass is done using the specific hit detection instructions. Feature uid's are encoded in the r,g,b,a channels and can then be decoded on the fly. Note: the `readPixels` operation is taking a lot of time, around 10-20ms each frame. --- src/ol/renderer/webgl/PointsLayer.js | 76 ++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 60778337dc..b387fa332e 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -6,6 +6,7 @@ import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../web import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js'; import GeometryType from '../../geom/GeometryType.js'; import WebGLLayerRenderer, { + colorDecodeId, colorEncodeId, getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType, @@ -21,6 +22,7 @@ import { } from '../../transform.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; import {getUid} from '../../util.js'; +import WebGLRenderTarget from '../../webgl/RenderTarget.js'; const VERTEX_SHADER = ` precision mediump float; @@ -241,8 +243,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { options.fragmentShader || FRAGMENT_SHADER, options.vertexShader || VERTEX_SHADER ); - - this.helper.useProgram(this.program_); + this.hitProgram_ = this.helper.getProgram( + HIT_FRAGMENT_SHADER, + options.vertexShader || VERTEX_SHADER + ); this.sizeCallback_ = options.sizeCallback || function() { return 1; @@ -316,6 +320,12 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { */ this.hitRenderInstructions_ = new Float32Array(0); + /** + * @type {WebGLRenderTarget} + * @private + */ + this.hitRenderTarget_ = new WebGLRenderTarget(this.helper); + this.worker_ = createWebGLWorker(); this.worker_.addEventListener('message', function(event) { const received = event.data; @@ -366,6 +376,8 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { canvas.style.opacity = opacity; } + this.renderHitDetection(frameState); + return canvas; } @@ -400,6 +412,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { this.helper.makeProjectionTransform(frameState, this.currentTransform_); multiplyTransform(this.currentTransform_, this.invertRenderTransform_); + this.helper.useProgram(this.program_); this.helper.prepareDraw(frameState); // write new data @@ -508,8 +521,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { /** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */ const hitMessage = { type: WebGLWorkerMessageType.GENERATE_BUFFERS, - renderInstructions: this.hitRenderInstructions_.buffer, - useShortIndices: !this.helper.getElementIndexUintEnabled() + renderInstructions: this.hitRenderInstructions_.buffer }; hitMessage['projectionTransform'] = projectionTransform; hitMessage['hitDetection'] = true; @@ -517,7 +529,63 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { this.hitRenderInstructions_ = null; } + /** + * @inheritDoc + */ + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { + const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice(0, 2)); + const width = frameState.size[0]; + const height = frameState.size[1]; + const data = this.hitRenderTarget_.read(); + const index = Math.floor(pixel[0]) + (height - Math.floor(pixel[1])) * width; + const color = [ + data[index * 4] / 255, + data[index * 4 + 1] / 255, + data[index * 4 + 2] / 255, + data[index * 4 + 3] / 255 + ]; + const uid = colorDecodeId(color).toString(); + + const source = this.getLayer().getSource(); + const feature = source.getFeatureByUid(uid); + if (feature) { + return callback(feature, this.getLayer()); + } + } + + /** + * Render the hit detection data to the corresponding render target + * @param {import("../../PluggableMap.js").FrameState} frameState current frame state + */ + renderHitDetection(frameState) { + const width = frameState.size[0]; + const height = frameState.size[1]; + const size = [width, height]; + + this.hitRenderTarget_.setSize(size); + + this.helper.useProgram(this.hitProgram_); + this.helper.prepareDrawToRenderTarget(frameState, this.hitRenderTarget_, true); + + this.helper.makeProjectionTransform(frameState, this.currentTransform_); + multiplyTransform(this.currentTransform_, this.invertHitRenderTransform_); + + this.helper.bindBuffer(this.hitVerticesBuffer_); + this.helper.bindBuffer(this.indicesBuffer_); + + const stride = POINT_VERTEX_STRIDE; + const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT; + this.helper.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * stride, 0); + this.helper.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 2); + this.helper.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 4); + this.helper.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 6); + this.helper.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 7); + this.helper.enableAttributeArray(DefaultAttrib.COLOR, 4, FLOAT, bytesPerFloat * stride, bytesPerFloat * 8); + + const renderCount = this.indicesBuffer_.getArray() ? this.indicesBuffer_.getArray().length : 0; + this.helper.drawElements(0, renderCount); + } } export default WebGLPointsLayerRenderer; From 917950a32bde9f35b26d558daec9914d90b46276 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 3 Jun 2019 09:53:14 +0200 Subject: [PATCH 10/14] Improve icon-sprite-webgl example to include hit detection --- examples/icon-sprite-webgl.html | 1 + examples/icon-sprite-webgl.js | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html index f465510d4c..c6dc366cd0 100644 --- a/examples/icon-sprite-webgl.html +++ b/examples/icon-sprite-webgl.html @@ -18,3 +18,4 @@ cloak: value: Your Mapbox access token from https://mapbox.com/ here ---
+
Current sighting:
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 7c0e1da6bd..08f875e634 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -103,7 +103,7 @@ function loadData() { loadData(); -new Map({ +const map = new Map({ layers: [ new TileLayer({ source: new TileJSON({ @@ -121,3 +121,18 @@ new Map({ zoom: 2 }) }); + +const info = document.getElementById('info'); +map.on('pointermove', function(evt) { + if (map.getView().getInteracting()) { + return; + } + const pixel = evt.pixel; + info.innerText = ''; + map.forEachFeatureAtPixel(pixel, function(feature) { + const datetime = feature.get('datetime'); + const duration = feature.get('duration'); + const shape = feature.get('shape'); + info.innerText = 'On ' + datetime + ', lasted ' + duration + ' seconds and had a "' + shape + '" shape.'; + }); +}); From e852294938bc1af73e408ef2406475ae44beb62f Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 6 Jun 2019 10:21:00 +0200 Subject: [PATCH 11/14] Webgl / improve reading of render targets data Now two methods are available: `readAll` and `readPixel`, and the data from the render target is not re-read every time unless `clearCachedData` is called. --- src/ol/webgl/RenderTarget.js | 53 +++++++++++++++--- test/spec/ol/webgl/rendertarget.test.js | 74 ++++++++++++++++++++----- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js index c9cde76b0d..7c0f6ef1f3 100644 --- a/src/ol/webgl/RenderTarget.js +++ b/src/ol/webgl/RenderTarget.js @@ -4,6 +4,8 @@ */ import {equals} from '../array.js'; +// for pixel color reading +const tmpArray4 = new Uint8Array(4); /** * @classdesc @@ -47,7 +49,13 @@ class WebGLRenderTarget { * @type {Uint8Array} * @private */ - this.data_ = new Uint8Array(1); + this.data_ = new Uint8Array(0); + + /** + * @type {boolean} + * @private + */ + this.dataCacheDirty_ = true; this.updateSize_(); } @@ -77,19 +85,50 @@ class WebGLRenderTarget { } /** - * Returns the content of the render target texture as raw data (series of r,g,b,a values) + * This will cause following calls to `#readAll` or `#readPixel` to download the content of the + * render target into memory, which is an expensive operation. + * This content will be kept in cache but should be cleared after each new render. + * @api + */ + clearCachedData() { + this.dataCacheDirty_ = true; + } + + /** + * Returns the full content of the frame buffer as a series of r, g, b, a components + * in the 0-255 range (unsigned byte). * @return {Uint8Array} Integer array of color values * @api */ - read() { - const size = this.size_; - const gl = this.helper_.getGL(); + readAll() { + if (this.dataCacheDirty_) { + const size = this.size_; + const gl = this.helper_.getGL(); - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_); - gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.UNSIGNED_BYTE, this.data_); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_); + gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.UNSIGNED_BYTE, this.data_); + this.dataCacheDirty_ = false; + } return this.data_; } + /** + * Reads one pixel of the frame buffer as an array of r, g, b, a components + * in the 0-255 range (unsigned byte). + * @param {number} x Pixel coordinate + * @param {number} y Pixel coordinate + * @returns {Uint8Array} Integer array with one color value (4 components) + */ + readPixel(x, y) { + this.readAll(); + const index = Math.floor(x) + (this.size_[1] - Math.floor(y) - 1) * this.size_[0]; + tmpArray4[0] = this.data_[index * 4]; + tmpArray4[1] = this.data_[index * 4 + 1]; + tmpArray4[2] = this.data_[index * 4 + 2]; + tmpArray4[3] = this.data_[index * 4 + 3]; + return tmpArray4; + } + /** * @return {WebGLTexture} Texture to render to */ diff --git a/test/spec/ol/webgl/rendertarget.test.js b/test/spec/ol/webgl/rendertarget.test.js index d405fe9fab..3468b16c26 100644 --- a/test/spec/ol/webgl/rendertarget.test.js +++ b/test/spec/ol/webgl/rendertarget.test.js @@ -11,10 +11,10 @@ describe('ol.webgl.RenderTarget', function() { const canvas = document.createElement('canvas'); testImage_4x4 = canvas.getContext('2d').createImageData(4, 4); for (let i = 0; i < testImage_4x4.data.length; i += 4) { - testImage_4x4.data[i] = 100; - testImage_4x4.data[i + 1] = 150; - testImage_4x4.data[i + 2] = 200; - testImage_4x4.data[i + 3] = 250; + testImage_4x4.data[i] = 100 + i / 4; + testImage_4x4.data[i + 1] = 100 + i / 4; + testImage_4x4.data[i + 2] = 200 + i / 4; + testImage_4x4.data[i + 3] = 200 + i / 4; } }); @@ -52,29 +52,77 @@ describe('ol.webgl.RenderTarget', function() { }); - describe('#readData', function() { + describe('#readAll', function() { it('returns 1-pixel data with the default options', function() { const rt = new WebGLRenderTarget(helper); - expect(rt.read().length).to.eql(4); + expect(rt.readAll().length).to.eql(4); }); it('returns the content of the texture', function() { const rt = new WebGLRenderTarget(helper, [4, 4]); helper.createTexture([4, 4], testImage_4x4, rt.getTexture()); - const data = rt.read(); + const data = rt.readAll(); expect(data[0]).to.eql(100); - expect(data[1]).to.eql(150); + expect(data[1]).to.eql(100); expect(data[2]).to.eql(200); - expect(data[3]).to.eql(250); - expect(data[4]).to.eql(100); - expect(data[5]).to.eql(150); - expect(data[6]).to.eql(200); - expect(data[7]).to.eql(250); + expect(data[3]).to.eql(200); + expect(data[4]).to.eql(101); + expect(data[5]).to.eql(101); + expect(data[6]).to.eql(201); + expect(data[7]).to.eql(201); expect(data.length).to.eql(4 * 4 * 4); }); + it('does not call gl.readPixels again when #clearCachedData is not called', function() { + const rt = new WebGLRenderTarget(helper, [4, 4]); + helper.createTexture([4, 4], testImage_4x4, rt.getTexture()); + const spy = sinon.spy(rt.helper_.getGL(), 'readPixels'); + rt.readAll(); + expect(spy.callCount).to.eql(1); + rt.readAll(); + expect(spy.callCount).to.eql(1); + rt.clearCachedData(); + rt.readAll(); + expect(spy.callCount).to.eql(2); + }); + + }); + + describe('#readPixel', function() { + + it('returns the content of one pixel', function() { + const rt = new WebGLRenderTarget(helper, [4, 4]); + helper.createTexture([4, 4], testImage_4x4, rt.getTexture()); + + let data = rt.readPixel(0, 0); + expect(data[0]).to.eql(112); + expect(data[1]).to.eql(112); + expect(data[2]).to.eql(212); + expect(data[3]).to.eql(212); + + data = rt.readPixel(3, 3); + expect(data[0]).to.eql(103); + expect(data[1]).to.eql(103); + expect(data[2]).to.eql(203); + expect(data[3]).to.eql(203); + expect(data.length).to.eql(4); + }); + + it('does not call gl.readPixels again when #clearCachedData is not called', function() { + const rt = new WebGLRenderTarget(helper, [4, 4]); + helper.createTexture([4, 4], testImage_4x4, rt.getTexture()); + const spy = sinon.spy(rt.helper_.getGL(), 'readPixels'); + rt.readPixel(0, 0); + expect(spy.callCount).to.eql(1); + rt.readPixel(1, 1); + expect(spy.callCount).to.eql(1); + rt.clearCachedData(); + rt.readPixel(2, 2); + expect(spy.callCount).to.eql(2); + }); + }); }); From 28b99767f86f631d604b595126485aaa68cfecd7 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 6 Jun 2019 11:38:59 +0200 Subject: [PATCH 12/14] Webgl points / read only one pixel for feature hit detection Also implements `hasFeatureAtCoordinate`. `hitTolerance` is not supported for now. --- src/ol/renderer/webgl/Layer.js | 9 +++ src/ol/renderer/webgl/PointsLayer.js | 16 ++--- src/ol/webgl/RenderTarget.js | 1 + .../ol/renderer/webgl/pointslayer.test.js | 66 +++++++++++++++++++ 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index eb404953b6..1f32ccf5b6 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -3,6 +3,7 @@ */ import LayerRenderer from '../Layer.js'; import WebGLHelper from '../../webgl/Helper.js'; +import {TRUE} from '../../functions.js'; /** @@ -81,6 +82,14 @@ class WebGLLayerRenderer extends LayerRenderer { getShaderCompileErrors() { return this.helper.getShaderCompileErrors(); } + + /** + * @inheritDoc + */ + hasFeatureAtCoordinate(coordinate, frameState) { + const feature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, null); + return feature !== undefined; + } } diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index b387fa332e..bf2994136b 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -377,6 +377,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { } this.renderHitDetection(frameState); + this.hitRenderTarget_.clearCachedData(); return canvas; } @@ -533,17 +534,14 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { * @inheritDoc */ forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { - const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice(0, 2)); - const width = frameState.size[0]; - const height = frameState.size[1]; + const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice()); - const data = this.hitRenderTarget_.read(); - const index = Math.floor(pixel[0]) + (height - Math.floor(pixel[1])) * width; + const data = this.hitRenderTarget_.readPixel(pixel[0], pixel[1]); const color = [ - data[index * 4] / 255, - data[index * 4 + 1] / 255, - data[index * 4 + 2] / 255, - data[index * 4 + 3] / 255 + data[0] / 255, + data[1] / 255, + data[2] / 255, + data[3] / 255 ]; const uid = colorDecodeId(color).toString(); diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js index 7c0f6ef1f3..4500495dc1 100644 --- a/src/ol/webgl/RenderTarget.js +++ b/src/ol/webgl/RenderTarget.js @@ -118,6 +118,7 @@ class WebGLRenderTarget { * @param {number} x Pixel coordinate * @param {number} y Pixel coordinate * @returns {Uint8Array} Integer array with one color value (4 components) + * @api */ readPixel(x, y) { this.readAll(); diff --git a/test/spec/ol/renderer/webgl/pointslayer.test.js b/test/spec/ol/renderer/webgl/pointslayer.test.js index e08f32ed8f..1f0361851a 100644 --- a/test/spec/ol/renderer/webgl/pointslayer.test.js +++ b/test/spec/ol/renderer/webgl/pointslayer.test.js @@ -6,6 +6,7 @@ import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/Point import {get as getProjection} from '../../../../../src/ol/proj.js'; import ViewHint from '../../../../../src/ol/ViewHint.js'; import {POINT_VERTEX_STRIDE, WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js'; +import {create as createTransform, translate as translateTransform} from '../../../../../src/ol/transform.js'; describe('ol.renderer.webgl.PointsLayer', function() { @@ -146,4 +147,69 @@ describe('ol.renderer.webgl.PointsLayer', function() { }); }); + describe('#forEachFeatureAtCoordinate and #hasFeatureAtCoordinate', function() { + let layer, renderer, feature; + + beforeEach(function() { + feature = new Feature(new Point([0, 0])); + layer = new VectorLayer({ + source: new VectorSource({ + features: [feature] + }) + }); + renderer = new WebGLPointsLayerRenderer(layer, { + sizeCallback: function() { + return 4; + } + }); + }); + + it('correctly hit detects a feature', function(done) { + const transform = translateTransform(createTransform(), 20, 20); + const projection = getProjection('EPSG:3857'); + const frameState = { + viewState: { + projection: projection, + resolution: 1, + rotation: 0, + center: [0, 0] + }, + layerStatesArray: [{}], + layerIndex: 0, + extent: [-20, -20, 20, 20], + size: [40, 40], + viewHints: [], + coordinateToPixelTransform: transform + }; + let found, hit; + const cb = function(feature) { + found = feature; + }; + + renderer.prepareFrame(frameState); + renderer.worker_.addEventListener('message', function() { + if (!renderer.hitRenderInstructions_) { + return; + } + renderer.renderFrame(frameState); + + function checkHit(x, y, expected) { + found = null; + renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null); + hit = renderer.hasFeatureAtCoordinate([x, y], frameState); + expect(found).to.be(expected ? feature : null); + expect(hit).to.eql(expected); + } + + checkHit(0, 0, true); + checkHit(1, -2, true); + checkHit(-2, 1, true); + checkHit(2, 0, false); + checkHit(1, -3, false); + + done(); + }); + }); + }); + }); From 3bca9b52972232d09034243a3d3ebc948d746646 Mon Sep 17 00:00:00 2001 From: jahow Date: Wed, 19 Jun 2019 15:06:24 +0200 Subject: [PATCH 13/14] Webgl / use feature index for hit detection in points layer For each feature its opacity value index is encoded on 4 bytes in the color values, and the uid is stored in the opacity value, allowing for a much higher range of uids to be read. --- src/ol/renderer/webgl/PointsLayer.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index bf2994136b..6f430792c5 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -493,6 +493,8 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { color ); + // for hit detection, the feature uid is saved in the opacity value + // and the index of the opacity value is encoded in the color values elementIndex = writePointFeatureInstructions( this.hitRenderInstructions_, elementIndex, @@ -503,9 +505,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { u1, v1, size, - opacity, + opacity > 0 ? Number(getUid(feature)) : 0, rotateWithView, - colorEncodeId(parseInt(getUid(feature)), tmpColor) + colorEncodeId(elementIndex + 7, tmpColor) ); } @@ -534,6 +536,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { * @inheritDoc */ forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { + if (!this.hitRenderInstructions_) { + return; + } + const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice()); const data = this.hitRenderTarget_.readPixel(pixel[0], pixel[1]); @@ -543,7 +549,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { data[2] / 255, data[3] / 255 ]; - const uid = colorDecodeId(color).toString(); + const index = colorDecodeId(color); + const opacity = this.hitRenderInstructions_[index]; + const uid = Math.floor(opacity).toString(); const source = this.getLayer().getSource(); const feature = source.getFeatureByUid(uid); From 933a6297bb0cad35026f9e3e1f8ea2a4738059c2 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Fri, 28 Jun 2019 10:58:21 +0200 Subject: [PATCH 14/14] Remove unused hasFeatureAtCoordinate from ol/renderer/Layer --- src/ol/renderer/Layer.js | 9 --------- src/ol/renderer/webgl/Layer.js | 8 -------- test/spec/ol/renderer/webgl/pointslayer.test.js | 6 ++---- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index cde8519918..9c0187c1e7 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -132,15 +132,6 @@ class LayerRenderer extends Observable { } } - /** - * @param {import("../coordinate.js").Coordinate} coordinate Coordinate. - * @param {import("../PluggableMap.js").FrameState} frameState Frame state. - * @return {boolean} Is there a feature at the given coordinate? - */ - hasFeatureAtCoordinate(coordinate, frameState) { - return false; - } - /** * Load the image if not already loaded, and register the image change * listener if needed. diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index 1f32ccf5b6..c8e6cbbc20 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -3,7 +3,6 @@ */ import LayerRenderer from '../Layer.js'; import WebGLHelper from '../../webgl/Helper.js'; -import {TRUE} from '../../functions.js'; /** @@ -83,13 +82,6 @@ class WebGLLayerRenderer extends LayerRenderer { return this.helper.getShaderCompileErrors(); } - /** - * @inheritDoc - */ - hasFeatureAtCoordinate(coordinate, frameState) { - const feature = this.forEachFeatureAtCoordinate(coordinate, frameState, 0, TRUE, null); - return feature !== undefined; - } } diff --git a/test/spec/ol/renderer/webgl/pointslayer.test.js b/test/spec/ol/renderer/webgl/pointslayer.test.js index 1f0361851a..7d67294960 100644 --- a/test/spec/ol/renderer/webgl/pointslayer.test.js +++ b/test/spec/ol/renderer/webgl/pointslayer.test.js @@ -147,7 +147,7 @@ describe('ol.renderer.webgl.PointsLayer', function() { }); }); - describe('#forEachFeatureAtCoordinate and #hasFeatureAtCoordinate', function() { + describe('#forEachFeatureAtCoordinate', function() { let layer, renderer, feature; beforeEach(function() { @@ -181,7 +181,7 @@ describe('ol.renderer.webgl.PointsLayer', function() { viewHints: [], coordinateToPixelTransform: transform }; - let found, hit; + let found; const cb = function(feature) { found = feature; }; @@ -196,9 +196,7 @@ describe('ol.renderer.webgl.PointsLayer', function() { function checkHit(x, y, expected) { found = null; renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null); - hit = renderer.hasFeatureAtCoordinate([x, y], frameState); expect(found).to.be(expected ? feature : null); - expect(hit).to.eql(expected); } checkHit(0, 0, true);