Add a new WebGLVectorLayer renderer

This relies on a mixed geometry batch and separate batch renderers (lines,
points and polygons). A different shader program is used for each of these
geometries, and three rendering passes are made.
This commit is contained in:
Olivier Guyot
2022-03-09 23:32:29 +01:00
parent cfaf9a14e5
commit c555315014

View File

@@ -0,0 +1,345 @@
/**
* @module ol/renderer/webgl/VectorLayer
*/
import WebGLLayerRenderer from './Layer.js';
import {create as createTransform} from '../../transform.js';
import {DefaultUniform} from '../../webgl/Helper.js';
import {buffer, createEmpty, equals} from '../../extent.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {listen, unlistenByKey} from '../../events.js';
import VectorEventType from '../../source/VectorEventType.js';
import ViewHint from '../../ViewHint.js';
import BaseVector from '../../layer/BaseVector.js';
import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js';
import GeometryType from '../../geom/GeometryType.js';
import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js';
import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js';
import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js';
import WebGLRenderTarget from '../../webgl/RenderTarget.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, Object<string, *>):number} callback This callback computes the numerical value of the
* attribute for a given feature (properties are available as 2nd arg for quicker access).
*/
/**
* @typedef {Object} Options
* @property {string} [className='ol-layer'] A CSS class name to set to the canvas element.
* @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} polygonVertexShader Vertex shader source, mandatory.
* @property {string} polygonFragmentShader Fragment shader source, mandatory.
* @property {string} lineStringVertexShader Vertex shader source, mandatory.
* @property {string} lineStringFragmentShader Fragment shader source, mandatory.
* @property {string} pointVertexShader Vertex shader source, mandatory.
* @property {string} pointFragmentShader 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
* Experimental WebGL vector renderer. Supports polygons and lines.
*
* You need to provide vertex and fragment shaders for rendering. This can be done using
* {@link module:ol/webgl/ShaderBuilder} utilities.
*
* 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.
*
* Polygons are broken down into triangles using the @mapbox/earcut package.
* Lines are rendered into strips of quads.
*
*
* This uses {@link module:ol/webgl/Helper~WebGLHelper} internally.
*
* @api
*/
class WebGLVectorLayerRenderer 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.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.polygonVertexShader_ = options.polygonVertexShader;
this.polygonFragmentShader_ = options.polygonFragmentShader;
this.pointVertexShader_ = options.pointVertexShader;
this.pointFragmentShader_ = options.pointFragmentShader;
this.lineStringVertexShader_ = options.lineStringVertexShader;
this.lineStringFragmentShader_ = options.lineStringFragmentShader;
this.attributes_ = options.attributes;
this.worker_ = createWebGLWorker();
this.batch_ = new MixedGeometryBatch();
const source = this.getLayer().getSource();
this.batch_.addFeatures(source.getFeatures());
this.sourceListenKeys_ = [
listen(
source,
VectorEventType.ADDFEATURE,
this.handleSourceFeatureAdded_,
this
),
listen(
source,
VectorEventType.CHANGEFEATURE,
this.handleSourceFeatureChanged_,
this
),
listen(
source,
VectorEventType.REMOVEFEATURE,
this.handleSourceFeatureDelete_,
this
),
listen(
source,
VectorEventType.CLEAR,
this.handleSourceFeatureClear_,
this
),
];
}
afterHelperCreated() {
this.polygonRenderer_ = new PolygonBatchRenderer(
this.helper,
this.worker_,
this.polygonVertexShader_,
this.polygonFragmentShader_,
this.attributes_ || []
);
this.pointRenderer_ = new PointBatchRenderer(
this.helper,
this.worker_,
this.pointVertexShader_,
this.pointFragmentShader_,
this.attributes_ || []
);
this.lineStringRenderer_ = new LineStringBatchRenderer(
this.helper,
this.worker_,
this.lineStringVertexShader_,
this.lineStringFragmentShader_,
this.attributes_ || []
);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureAdded_(event) {
const feature = event.feature;
this.batch_.addFeature(feature);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureChanged_(event) {
const feature = event.feature;
this.batch_.changeFeature(feature);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureDelete_(event) {
const feature = event.feature;
this.batch_.removeFeature(feature);
}
/**
* @private
*/
handleSourceFeatureClear_() {
this.batch_.clear();
}
/**
* Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
const gl = this.helper.getGL();
this.preRender(gl, frameState);
this.polygonRenderer_.render(
this.batch_.polygonBatch,
this.currentTransform_,
frameState
);
this.lineStringRenderer_.render(
this.batch_.lineStringBatch,
this.currentTransform_,
frameState
);
this.pointRenderer_.render(
this.batch_.pointBatch,
this.currentTransform_,
frameState
);
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 = String(opacity);
}
this.postRender(gl, frameState);
return canvas;
}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrameInternal(frameState) {
const layer = this.getLayer();
const vectorSource = layer.getSource();
const viewState = frameState.viewState;
const viewNotMoving =
!frameState.viewHints[ViewHint.ANIMATING] &&
!frameState.viewHints[ViewHint.INTERACTING];
const extentChanged = !equals(this.previousExtent_, frameState.extent);
const sourceChanged = this.sourceRevision_ < vectorSource.getRevision();
if (sourceChanged) {
this.sourceRevision_ = vectorSource.getRevision();
}
if (viewNotMoving && (extentChanged || sourceChanged)) {
const projection = viewState.projection;
const resolution = viewState.resolution;
const renderBuffer =
layer instanceof BaseVector ? layer.getRenderBuffer() : 0;
const extent = buffer(frameState.extent, renderBuffer * resolution);
vectorSource.loadFeatures(extent, resolution, projection);
this.polygonRenderer_.rebuild(
this.batch_.polygonBatch,
frameState,
GeometryType.POLYGON
);
this.lineStringRenderer_.rebuild(
this.batch_.lineStringBatch,
frameState,
GeometryType.LINE_STRING
);
this.pointRenderer_.rebuild(
this.batch_.pointBatch,
frameState,
GeometryType.POINT
);
this.previousExtent_ = frameState.extent.slice();
}
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
this.helper.prepareDraw(frameState);
return true;
}
/**
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
return undefined;
}
/**
* Clean up.
*/
disposeInternal() {
this.worker_.terminate();
this.layer_ = null;
this.sourceListenKeys_.forEach(function (key) {
unlistenByKey(key);
});
this.sourceListenKeys_ = null;
super.disposeInternal();
}
}
export default WebGLVectorLayerRenderer;