From 5ffca0633c1751a9d20e7defc463a29ea1608e51 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Mon, 3 Jun 2019 09:17:00 +0200 Subject: [PATCH] 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;