/** * @module ol/renderer/webgl/PointsLayer */ import WebGLArrayBuffer from '../../webgl/Buffer.js'; import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js'; import {AttributeType, DefaultUniform} from '../../webgl/Helper.js'; import GeometryType from '../../geom/GeometryType.js'; import WebGLLayerRenderer, {colorDecodeId, colorEncodeId, WebGLWorkerMessageType} from './Layer.js'; import ViewHint from '../../ViewHint.js'; import {createEmpty, equals} from '../../extent.js'; import { apply as applyTransform, create as createTransform, makeInverse as makeInverseTransform, multiply as multiplyTransform } from '../../transform.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; import {getUid} from '../../util.js'; import WebGLRenderTarget from '../../webgl/RenderTarget.js'; import {assert} from '../../asserts.js'; /** * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different * for each feature. * @property {string} name Attribute name. * @property {function(import("../../Feature").default):number} callback This callback computes the numerical value of the * attribute for a given feature. */ /** * @typedef {Object} Options * @property {Array} [attributes] These attributes will be read from the features in the source and then * passed to the GPU. The `name` property of each attribute will serve as its identifier: * * In the vertex shader as an `attribute` by prefixing it with `a_` * * In the fragment shader as a `varying` by prefixing it with `v_` * Please note that these can only be numerical values. * @property {string} vertexShader Vertex shader source, mandatory. * @property {string} fragmentShader Fragment shader source, mandatory. * @property {string} [hitVertexShader] Vertex shader source for hit detection rendering. * @property {string} [hitFragmentShader] Fragment shader source for hit detection rendering. * @property {Object.} [uniforms] Uniform definitions for the post process steps * Please note that `u_texture` is reserved for the main texture slot. * @property {Array} [postProcesses] Post-processes definitions */ /** * @classdesc * WebGL vector renderer optimized for points. * All features will be rendered as quads (two triangles forming a square). New data will be flushed to the GPU * every time the vector source changes. * * You need to provide vertex and fragment shaders for rendering. This can be done using * {@link module:ol/webgl/ShaderBuilder} utilities. These shaders shall expect a `a_position` attribute * containing the screen-space projected center of the quad, as well as a `a_index` attribute * whose value (0, 1, 2 or 3) indicates which quad vertex is currently getting processed (see structure below). * * To include variable attributes in the shaders, you need to declare them using the `attributes` property of * the options object like so: * ```js * new WebGLPointsLayerRenderer(layer, { * attributes: [ * { * name: 'size', * callback: function(feature) { * // compute something with the feature * } * }, * { * name: 'weight', * callback: function(feature) { * // compute something with the feature * } * }, * ], * vertexShader: * // shader using attribute a_weight and a_size * fragmentShader: * // shader using varying v_weight and v_size * ``` * * To enable hit detection, you must as well provide dedicated shaders using the `hitVertexShader` * and `hitFragmentShader` properties. These shall expect the `a_hitColor` attribute to contain * the final color that will have to be output for hit detection to work. * * The following uniform is used for the main texture: `u_texture`. * * Please note that the main shader output should have premultiplied alpha, otherwise visual anomalies may occur. * * Points are rendered as quads with the following structure: * * ``` * (u0, v1) (u1, v1) * [3]----------[2] * |` | * | ` | * | ` | * | ` | * | ` | * | ` | * [0]----------[1] * (u0, v0) (u1, v0) * ``` * * This uses {@link module:ol/webgl/Helper~WebGLHelper} internally. * * @api */ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { /** * @param {import("../../layer/Layer.js").default} layer Layer. * @param {Options} options Options. */ constructor(layer, options) { const uniforms = options.uniforms || {}; const projectionMatrixTransform = createTransform(); uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform; super(layer, { uniforms: uniforms, postProcesses: options.postProcesses }); 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( options.fragmentShader, options.vertexShader ); /** * @type {boolean} * @private */ this.hitDetectionEnabled_ = options.hitFragmentShader && options.hitVertexShader ? true : false; this.hitProgram_ = this.hitDetectionEnabled_ && this.helper.getProgram( options.hitFragmentShader, options.hitVertexShader ); const customAttributes = options.attributes ? options.attributes.map(function(attribute) { return { name: 'a_' + attribute.name, size: 1, type: AttributeType.FLOAT }; }) : []; /** * A list of attributes used by the renderer. By default only the position and * index of the vertex (0 to 3) are required. * @type {Array} */ this.attributes = [{ name: 'a_position', size: 2, type: AttributeType.FLOAT }, { name: 'a_index', size: 1, type: AttributeType.FLOAT }].concat(customAttributes); /** * A list of attributes used for hit detection. * @type {Array} */ this.hitDetectionAttributes = [{ name: 'a_position', size: 2, type: AttributeType.FLOAT }, { name: 'a_index', size: 1, type: AttributeType.FLOAT }, { name: 'a_hitColor', size: 4, type: AttributeType.FLOAT }, { name: 'a_featureUid', size: 1, type: AttributeType.FLOAT }].concat(customAttributes); this.customAttributes = options.attributes ? options.attributes : []; this.previousExtent_ = createEmpty(); /** * This transform is updated on every frame and is the composition of: * - invert of the world->screen transform that was used when rebuilding buffers (see `this.renderTransform_`) * - current world->screen transform * @type {import("../../transform.js").Transform} * @private */ this.currentTransform_ = projectionMatrixTransform; /** * This transform is updated when buffers are rebuilt and converts world space coordinates to screen space * @type {import("../../transform.js").Transform} * @private */ this.renderTransform_ = createTransform(); /** * @type {import("../../transform.js").Transform} * @private */ this.invertRenderTransform_ = createTransform(); /** * @type {Float32Array} * @private */ this.renderInstructions_ = new Float32Array(0); /** * These instructions are used for hit detection * @type {Float32Array} * @private */ this.hitRenderInstructions_ = new Float32Array(0); /** * @type {WebGLRenderTarget} * @private */ this.hitRenderTarget_ = this.hitDetectionEnabled_ && new WebGLRenderTarget(this.helper); this.worker_ = createWebGLWorker(); this.worker_.addEventListener('message', function(event) { const received = event.data; if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) { const projectionTransform = received.projectionTransform; 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.indicesBuffer_); this.renderTransform_ = projectionTransform; makeInverseTransform(this.invertRenderTransform_, this.renderTransform_); if (received.hitDetection) { this.hitRenderInstructions_ = new Float32Array(event.data.renderInstructions); } else { this.renderInstructions_ = new Float32Array(event.data.renderInstructions); } this.getLayer().changed(); } }.bind(this)); } /** * @inheritDoc */ renderFrame(frameState) { const renderCount = this.indicesBuffer_.getSize(); this.helper.drawElements(0, renderCount); this.helper.finalizeDraw(frameState); const canvas = this.helper.getCanvas(); const layerState = frameState.layerStatesArray[frameState.layerIndex]; const opacity = layerState.opacity; if (opacity !== parseFloat(canvas.style.opacity)) { canvas.style.opacity = opacity; } if (this.hitDetectionEnabled_) { this.renderHitDetection(frameState); this.hitRenderTarget_.clearCachedData(); } return canvas; } /** * @inheritDoc */ prepareFrame(frameState) { const layer = this.getLayer(); const vectorSource = layer.getSource(); const viewState = frameState.viewState; // the source has changed: clear the feature cache & reload features const sourceChanged = this.sourceRevision_ < vectorSource.getRevision(); if (sourceChanged) { this.sourceRevision_ = vectorSource.getRevision(); const projection = viewState.projection; const resolution = viewState.resolution; vectorSource.loadFeatures([-Infinity, -Infinity, Infinity, Infinity], resolution, projection); } const viewNotMoving = !frameState.viewHints[ViewHint.ANIMATING] && !frameState.viewHints[ViewHint.INTERACTING]; const extentChanged = !equals(this.previousExtent_, frameState.extent); if ((sourceChanged || extentChanged) && viewNotMoving) { this.rebuildBuffers_(frameState); this.previousExtent_ = frameState.extent.slice(); } // apply the current projection transform with the invert of the one used to fill buffers this.helper.makeProjectionTransform(frameState, this.currentTransform_); multiplyTransform(this.currentTransform_, this.invertRenderTransform_); this.helper.useProgram(this.program_); this.helper.prepareDraw(frameState); // write new data this.helper.bindBuffer(this.verticesBuffer_); this.helper.bindBuffer(this.indicesBuffer_); this.helper.enableAttributes(this.attributes); return true; } /** * Rebuild internal webgl buffers based on current view extent; costly, should not be called too much * @param {import("../../PluggableMap").FrameState} frameState Frame state. * @private */ rebuildBuffers_(frameState) { const layer = this.getLayer(); const vectorSource = layer.getSource(); // saves the projection transform for the current frame state const projectionTransform = createTransform(); this.helper.makeProjectionTransform(frameState, projectionTransform); const features = vectorSource.getFeatures(); // here we anticipate the amount of render instructions that we well generate // this can be done since we know that for normal render we only have x, y as base instructions, // and x, y, r, g, b, a and featureUid for hit render instructions // and we also know the amount of custom attributes to append to these const totalInstructionsCount = (2 + this.customAttributes.length) * features.length; if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) { this.renderInstructions_ = new Float32Array(totalInstructionsCount); } if (this.hitDetectionEnabled_) { const totalHitInstructionsCount = (7 + this.customAttributes.length) * features.length; if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalHitInstructionsCount) { this.hitRenderInstructions_ = new Float32Array(totalHitInstructionsCount); } } // loop on features to fill the buffer let feature; const tmpCoords = []; const tmpColor = []; let renderIndex = 0; let hitIndex = 0; let hitColor; for (let i = 0; i < features.length; i++) { feature = features[i]; if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) { continue; } tmpCoords[0] = feature.getGeometry().getFlatCoordinates()[0]; tmpCoords[1] = feature.getGeometry().getFlatCoordinates()[1]; applyTransform(projectionTransform, tmpCoords); hitColor = colorEncodeId(hitIndex + 6, tmpColor); this.renderInstructions_[renderIndex++] = tmpCoords[0]; this.renderInstructions_[renderIndex++] = tmpCoords[1]; // 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 if (this.hitDetectionEnabled_) { this.hitRenderInstructions_[hitIndex++] = tmpCoords[0]; this.hitRenderInstructions_[hitIndex++] = tmpCoords[1]; this.hitRenderInstructions_[hitIndex++] = hitColor[0]; this.hitRenderInstructions_[hitIndex++] = hitColor[1]; this.hitRenderInstructions_[hitIndex++] = hitColor[2]; this.hitRenderInstructions_[hitIndex++] = hitColor[3]; this.hitRenderInstructions_[hitIndex++] = Number(getUid(feature)); } // pushing custom attributes let value; for (let j = 0; j < this.customAttributes.length; j++) { value = this.customAttributes[j].callback(feature); this.renderInstructions_[renderIndex++] = value; if (this.hitDetectionEnabled_) { this.hitRenderInstructions_[hitIndex++] = value; } } } /** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */ const message = { type: WebGLWorkerMessageType.GENERATE_BUFFERS, renderInstructions: this.renderInstructions_.buffer, customAttributesCount: this.customAttributes.length }; // 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 */ if (this.hitDetectionEnabled_) { const hitMessage = { type: WebGLWorkerMessageType.GENERATE_BUFFERS, renderInstructions: this.hitRenderInstructions_.buffer, customAttributesCount: 5 + this.customAttributes.length }; hitMessage['projectionTransform'] = projectionTransform; hitMessage['hitDetection'] = true; this.worker_.postMessage(hitMessage, [this.hitRenderInstructions_.buffer]); this.hitRenderInstructions_ = null; } } /** * @inheritDoc */ forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { assert(this.hitDetectionEnabled_, 66); if (!this.hitRenderInstructions_) { return; } const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice()); const data = this.hitRenderTarget_.readPixel(pixel[0], pixel[1]); const color = [ data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255 ]; 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); 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) { // skip render entirely if vertex buffers not ready/generated yet if (!this.hitVerticesBuffer_.getSize()) { return; } this.hitRenderTarget_.setSize(frameState.size); this.helper.useProgram(this.hitProgram_); this.helper.prepareDrawToRenderTarget(frameState, this.hitRenderTarget_, true); this.helper.bindBuffer(this.hitVerticesBuffer_); this.helper.bindBuffer(this.indicesBuffer_); this.helper.enableAttributes(this.hitDetectionAttributes); const renderCount = this.indicesBuffer_.getSize(); this.helper.drawElements(0, renderCount); } /** * @inheritDoc */ disposeInternal() { this.worker_.terminate(); super.disposeInternal(); } } export default WebGLPointsLayerRenderer;