Files
openlayers/src/ol/renderer/webgl/PointsLayer.js
2019-09-27 11:01:50 +02:00

492 lines
17 KiB
JavaScript

/**
* @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<CustomAttribute>} [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.<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process steps
* Please note that `u_texture` is reserved for the main texture slot.
* @property {Array<import("./Layer").PostProcessesOptions>} [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<import('../../webgl/Helper.js').AttributeDescription>}
*/
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<import('../../webgl/Helper.js').AttributeDescription>}
*/
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;