diff --git a/examples/webgl-vector-layer.html b/examples/webgl-vector-layer.html
new file mode 100644
index 0000000000..32fe331f5a
--- /dev/null
+++ b/examples/webgl-vector-layer.html
@@ -0,0 +1,10 @@
+---
+layout: example.html
+title: WebGL Vector Layer
+shortdesc: Example of a vector layer rendered using WebGL
+docs: >
+ The ecoregions are loaded from a GeoJSON file.
+tags: "vector, geojson, webgl"
+experimental: true
+---
+
diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js
new file mode 100644
index 0000000000..d9ee44a00e
--- /dev/null
+++ b/examples/webgl-vector-layer.js
@@ -0,0 +1,64 @@
+import GeoJSON from '../src/ol/format/GeoJSON.js';
+import Layer from '../src/ol/layer/Layer.js';
+import Map from '../src/ol/Map.js';
+import OSM from '../src/ol/source/OSM.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import VectorSource from '../src/ol/source/Vector.js';
+import View from '../src/ol/View.js';
+import WebGLVectorLayerRenderer from '../src/ol/renderer/webgl/VectorLayer.js';
+import {asArray} from '../src/ol/color.js';
+import {packColor} from '../src/ol/renderer/webgl/shaders.js';
+
+class WebGLLayer extends Layer {
+ createRenderer() {
+ return new WebGLVectorLayerRenderer(this, {
+ fill: {
+ attributes: {
+ color: function (feature) {
+ const color = asArray(feature.get('COLOR') || '#eee');
+ color[3] = 0.85;
+ return packColor(color);
+ },
+ opacity: function () {
+ return 0.6;
+ },
+ },
+ },
+ stroke: {
+ attributes: {
+ color: function (feature) {
+ const color = [...asArray(feature.get('COLOR') || '#eee')];
+ color.forEach((_, i) => (color[i] = Math.round(color[i] * 0.75))); // darken slightly
+ return packColor(color);
+ },
+ width: function () {
+ return 1.5;
+ },
+ opacity: function () {
+ return 1;
+ },
+ },
+ },
+ });
+ }
+}
+
+const osm = new TileLayer({
+ source: new OSM(),
+});
+
+const vectorLayer = new WebGLLayer({
+ source: new VectorSource({
+ url: 'https://openlayers.org/data/vector/ecoregions.json',
+ format: new GeoJSON(),
+ }),
+});
+
+const map = new Map({
+ layers: [osm, vectorLayer],
+ target: 'map',
+ view: new View({
+ center: [0, 0],
+ zoom: 1,
+ }),
+});
diff --git a/package-lock.json b/package-lock.json
index 261862e41c..bccfcedc49 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "7.0.0-dev",
"license": "BSD-2-Clause",
"dependencies": {
+ "earcut": "^2.2.3",
"geotiff": "2.0.4",
"ol-mapbox-style": "^8.0.5",
"pbf": "3.2.1",
@@ -3838,6 +3839,11 @@
"void-elements": "^2.0.0"
}
},
+ "node_modules/earcut": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
+ "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -13182,6 +13188,11 @@
"void-elements": "^2.0.0"
}
},
+ "earcut": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
+ "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
+ },
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
diff --git a/package.json b/package.json
index cc9dd2e996..991811cea4 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"url": "https://opencollective.com/openlayers"
},
"dependencies": {
+ "earcut": "^2.2.3",
"geotiff": "2.0.4",
"ol-mapbox-style": "^8.0.5",
"pbf": "3.2.1",
diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js
new file mode 100644
index 0000000000..aae3e73733
--- /dev/null
+++ b/src/ol/render/webgl/BatchRenderer.js
@@ -0,0 +1,201 @@
+/**
+ * @module ol/render/webgl/BatchRenderer
+ */
+import {WebGLWorkerMessageType} from './constants.js';
+import {abstract} from '../../util.js';
+import {
+ create as createTransform,
+ makeInverse as makeInverseTransform,
+ multiply as multiplyTransform,
+ translate as translateTransform,
+} from '../../transform.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.
+ */
+
+let workerMessageCounter = 0;
+
+/**
+ * @classdesc Abstract class for batch renderers.
+ * Batch renderers are meant to render the geometries contained in a {@link module:ol/render/webgl/GeometryBatch}
+ * instance. They are responsible for generating render instructions and transforming them into WebGL buffers.
+ */
+class AbstractBatchRenderer {
+ /**
+ * @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
+ * @param {Worker} worker WebGL worker instance
+ * @param {string} vertexShader Vertex shader
+ * @param {string} fragmentShader Fragment shader
+ * @param {Array} customAttributes List of custom attributes
+ */
+ constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
+ /**
+ * @type {import("../../webgl/Helper.js").default}
+ * @private
+ */
+ this.helper_ = helper;
+
+ /**
+ * @type {Worker}
+ * @private
+ */
+ this.worker_ = worker;
+
+ /**
+ * @type {WebGLProgram}
+ * @private
+ */
+ this.program_ = this.helper_.getProgram(fragmentShader, vertexShader);
+
+ /**
+ * A list of attributes used by the renderer.
+ * @type {Array}
+ * @protected
+ */
+ this.attributes = [];
+
+ /**
+ * @type {Array}
+ * @protected
+ */
+ this.customAttributes = customAttributes;
+ }
+
+ /**
+ * Rebuild rendering instructions and webgl buffers based on the provided frame state
+ * Note: this is a costly operation.
+ * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
+ * @param {import("../../PluggableMap").FrameState} frameState Frame state.
+ * @param {import("../../geom/Geometry.js").Type} geometryType Geometry type
+ * @param {function(): void} callback Function called once the render buffers are updated
+ */
+ rebuild(batch, frameState, geometryType, callback) {
+ // store transform for rendering instructions
+ batch.renderInstructionsTransform = this.helper_.makeProjectionTransform(
+ frameState,
+ createTransform()
+ );
+ this.generateRenderInstructions(batch);
+ this.generateBuffers_(batch, geometryType, callback);
+ }
+
+ /**
+ * Render the geometries in the batch. This will also update the current transform used for rendering according to
+ * the invert transform of the webgl buffers
+ * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
+ * @param {import("../../transform.js").Transform} currentTransform Transform
+ * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
+ * @param {number} offsetX X offset
+ */
+ render(batch, currentTransform, frameState, offsetX) {
+ // multiply the current projection transform with the invert of the one used to fill buffers
+ this.helper_.makeProjectionTransform(frameState, currentTransform);
+ translateTransform(currentTransform, offsetX, 0);
+ multiplyTransform(currentTransform, batch.invertVerticesBufferTransform);
+
+ // enable program, buffers and attributes
+ this.helper_.useProgram(this.program_, frameState);
+ this.helper_.bindBuffer(batch.verticesBuffer);
+ this.helper_.bindBuffer(batch.indicesBuffer);
+ this.helper_.enableAttributes(this.attributes);
+
+ const renderCount = batch.indicesBuffer.getSize();
+ this.helper_.drawElements(0, renderCount);
+ }
+
+ /**
+ * Rebuild rendering instructions based on the provided frame state
+ * This is specific to the geometry type and has to be implemented by subclasses.
+ * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
+ * @protected
+ */
+ generateRenderInstructions(batch) {
+ abstract();
+ }
+
+ /**
+ * Rebuild internal webgl buffers for rendering based on the current rendering instructions;
+ * This is asynchronous: webgl buffers wil _not_ be updated right away
+ * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
+ * @param {import("../../geom/Geometry.js").Type} geometryType Geometry type
+ * @param {function(): void} callback Function called once the render buffers are updated
+ * @private
+ */
+ generateBuffers_(batch, geometryType, callback) {
+ const messageId = workerMessageCounter++;
+
+ let messageType;
+ switch (geometryType) {
+ case 'Polygon':
+ messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS;
+ break;
+ case 'Point':
+ messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS;
+ break;
+ case 'LineString':
+ messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS;
+ break;
+ default:
+ // pass
+ }
+
+ /** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */
+ const message = {
+ id: messageId,
+ type: messageType,
+ renderInstructions: batch.renderInstructions.buffer,
+ renderInstructionsTransform: batch.renderInstructionsTransform,
+ customAttributesCount: this.customAttributes.length,
+ };
+ this.worker_.postMessage(message, [batch.renderInstructions.buffer]);
+
+ // leave ownership of render instructions
+ batch.renderInstructions = null;
+
+ const handleMessage =
+ /**
+ * @param {*} event Event.
+ * @this {AbstractBatchRenderer}
+ */
+ function (event) {
+ const received = event.data;
+
+ // this is not the response to our request: skip
+ if (received.id !== messageId) {
+ return;
+ }
+
+ // we've received our response: stop listening
+ this.worker_.removeEventListener('message', handleMessage);
+
+ // store transform & invert transform for webgl buffers
+ batch.verticesBufferTransform = received.renderInstructionsTransform;
+ makeInverseTransform(
+ batch.invertVerticesBufferTransform,
+ batch.verticesBufferTransform
+ );
+
+ // copy & flush received buffers to GPU
+ batch.verticesBuffer.fromArrayBuffer(received.vertexBuffer);
+ this.helper_.flushBufferData(batch.verticesBuffer);
+ batch.indicesBuffer.fromArrayBuffer(received.indexBuffer);
+ this.helper_.flushBufferData(batch.indicesBuffer);
+
+ // take back ownership of the render instructions for further use
+ batch.renderInstructions = new Float32Array(
+ received.renderInstructions
+ );
+
+ callback();
+ }.bind(this);
+
+ this.worker_.addEventListener('message', handleMessage);
+ }
+}
+
+export default AbstractBatchRenderer;
diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js
new file mode 100644
index 0000000000..c4ee9f0bf8
--- /dev/null
+++ b/src/ol/render/webgl/LineStringBatchRenderer.js
@@ -0,0 +1,116 @@
+/**
+ * @module ol/render/webgl/LineStringBatchRenderer
+ */
+import AbstractBatchRenderer from './BatchRenderer.js';
+import {AttributeType} from '../../webgl/Helper.js';
+import {transform2D} from '../../geom/flat/transform.js';
+
+/**
+ * Names of attributes made available to the vertex shader.
+ * Please note: changing these *will* break custom shaders!
+ * @enum {string}
+ */
+export const Attributes = {
+ SEGMENT_START: 'a_segmentStart',
+ SEGMENT_END: 'a_segmentEnd',
+ PARAMETERS: 'a_parameters',
+};
+
+class LineStringBatchRenderer extends AbstractBatchRenderer {
+ /**
+ * @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
+ * @param {Worker} worker WebGL worker instance
+ * @param {string} vertexShader Vertex shader
+ * @param {string} fragmentShader Fragment shader
+ * @param {Array} customAttributes List of custom attributes
+ */
+ constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
+ super(helper, worker, vertexShader, fragmentShader, customAttributes);
+
+ // vertices for lines must hold both a position (x,y) and an offset (dx,dy)
+ this.attributes = [
+ {
+ name: Attributes.SEGMENT_START,
+ size: 2,
+ type: AttributeType.FLOAT,
+ },
+ {
+ name: Attributes.SEGMENT_END,
+ size: 2,
+ type: AttributeType.FLOAT,
+ },
+ {
+ name: Attributes.PARAMETERS,
+ size: 1,
+ type: AttributeType.FLOAT,
+ },
+ ].concat(
+ customAttributes.map(function (attribute) {
+ return {
+ name: 'a_' + attribute.name,
+ size: 1,
+ type: AttributeType.FLOAT,
+ };
+ })
+ );
+ }
+
+ /**
+ * Render instructions for lines are structured like so:
+ * [ customAttr0, ... , customAttrN, numberOfVertices0, x0, y0, ... , xN, yN, numberOfVertices1, ... ]
+ * @param {import("./MixedGeometryBatch.js").LineStringGeometryBatch} batch Linestring geometry batch
+ * @override
+ */
+ generateRenderInstructions(batch) {
+ // here we anticipate the amount of render instructions for lines:
+ // 2 instructions per vertex for position (x and y)
+ // + 1 instruction per line per custom attributes
+ // + 1 instruction per line (for vertices count)
+ const totalInstructionsCount =
+ 2 * batch.verticesCount +
+ (1 + this.customAttributes.length) * batch.geometriesCount;
+ if (
+ !batch.renderInstructions ||
+ batch.renderInstructions.length !== totalInstructionsCount
+ ) {
+ batch.renderInstructions = new Float32Array(totalInstructionsCount);
+ }
+
+ // loop on features to fill the render instructions
+ let batchEntry;
+ const flatCoords = [];
+ let renderIndex = 0;
+ let value;
+ for (const featureUid in batch.entries) {
+ batchEntry = batch.entries[featureUid];
+ for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
+ flatCoords.length = batchEntry.flatCoordss[i].length;
+ transform2D(
+ batchEntry.flatCoordss[i],
+ 0,
+ flatCoords.length,
+ 2,
+ batch.renderInstructionsTransform,
+ flatCoords
+ );
+
+ // custom attributes
+ for (let k = 0, kk = this.customAttributes.length; k < kk; k++) {
+ value = this.customAttributes[k].callback(batchEntry.feature);
+ batch.renderInstructions[renderIndex++] = value;
+ }
+
+ // vertices count
+ batch.renderInstructions[renderIndex++] = flatCoords.length / 2;
+
+ // looping on points for positions
+ for (let j = 0, jj = flatCoords.length; j < jj; j += 2) {
+ batch.renderInstructions[renderIndex++] = flatCoords[j];
+ batch.renderInstructions[renderIndex++] = flatCoords[j + 1];
+ }
+ }
+ }
+ }
+}
+
+export default LineStringBatchRenderer;
diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js
new file mode 100644
index 0000000000..2ae4728901
--- /dev/null
+++ b/src/ol/render/webgl/MixedGeometryBatch.js
@@ -0,0 +1,364 @@
+/**
+ * @module ol/render/webgl/MixedGeometryBatch
+ */
+import WebGLArrayBuffer from '../../webgl/Buffer.js';
+import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
+import {create as createTransform} from '../../transform.js';
+import {getUid} from '../../util.js';
+
+/**
+ * @typedef {Object} GeometryBatchItem Object that holds a reference to a feature as well as the raw coordinates of its various geometries
+ * @property {import("../../Feature").default} feature Feature
+ * @property {Array>} flatCoordss Array of flat coordinates arrays, one for each geometry related to the feature
+ * @property {number} [verticesCount] Only defined for linestring and polygon batches
+ * @property {number} [ringsCount] Only defined for polygon batches
+ * @property {Array>} [ringsVerticesCounts] Array of vertices counts in each ring for each geometry; only defined for polygons batches
+ */
+
+/**
+ * @typedef {PointGeometryBatch|LineStringGeometryBatch|PolygonGeometryBatch} GeometryBatch
+ */
+
+/**
+ * @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons
+ * @property {Object} entries Dictionary of all entries in the batch with associated computed values.
+ * One entry corresponds to one feature. Key is feature uid.
+ * @property {number} geometriesCount Amount of geometries in the batch.
+ * @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
+ * [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
+ * @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
+ * @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
+ * @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
+ * @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
+ * @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
+ * @property {number} verticesCount Amount of vertices from geometries in the batch.
+ * @property {number} ringsCount How many outer and inner rings in this batch.
+ */
+
+/**
+ * @typedef {Object} LineStringGeometryBatch A geometry batch specific to lines
+ * @property {Object} entries Dictionary of all entries in the batch with associated computed values.
+ * One entry corresponds to one feature. Key is feature uid.
+ * @property {number} geometriesCount Amount of geometries in the batch.
+ * @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
+ * [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
+ * @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
+ * @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
+ * @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
+ * @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
+ * @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
+ * @property {number} verticesCount Amount of vertices from geometries in the batch.
+ */
+
+/**
+ * @typedef {Object} PointGeometryBatch A geometry batch specific to points
+ * @property {Object} entries Dictionary of all entries in the batch with associated computed values.
+ * One entry corresponds to one feature. Key is feature uid.
+ * @property {number} geometriesCount Amount of geometries in the batch.
+ * @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
+ * [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
+ * @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
+ * @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
+ * @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
+ * @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
+ * @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
+ */
+
+/**
+ * @classdesc This class is used to group several geometries of various types together for faster rendering.
+ * Three inner batches are maintained for polygons, lines and points. Each time a feature is added, changed or removed
+ * from the batch, these inner batches are modified accordingly in order to keep them up-to-date.
+ *
+ * A feature can be present in several inner batches, for example a polygon geometry will be present in the polygon batch
+ * and its linar rings will be present in the line batch. Multi geometries are also broken down into individual geometries
+ * and added to the corresponding batches in a recursive manner.
+ *
+ * Corresponding {@link module:ol/render/webgl/BatchRenderer} instances are then used to generate the render instructions
+ * and WebGL buffers (vertices and indices) for each inner batches; render instructions are stored on the inner batches,
+ * alongside the transform used to convert world coords to screen coords at the time these instructions were generated.
+ * The resulting WebGL buffers are stored on the batches as well.
+ *
+ * An important aspect of geometry batches is that there is no guarantee that render instructions and WebGL buffers
+ * are synchronized, i.e. render instructions can describe a new state while WebGL buffers might not have been written yet.
+ * This is why two world-to-screen transforms are stored on each batch: one for the render instructions and one for
+ * the WebGL buffers.
+ */
+class MixedGeometryBatch {
+ constructor() {
+ /**
+ * @type {PolygonGeometryBatch}
+ */
+ this.polygonBatch = {
+ entries: {},
+ geometriesCount: 0,
+ verticesCount: 0,
+ ringsCount: 0,
+ renderInstructions: new Float32Array(0),
+ verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
+ indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
+ renderInstructionsTransform: createTransform(),
+ verticesBufferTransform: createTransform(),
+ invertVerticesBufferTransform: createTransform(),
+ };
+
+ /**
+ * @type {PointGeometryBatch}
+ */
+ this.pointBatch = {
+ entries: {},
+ geometriesCount: 0,
+ renderInstructions: new Float32Array(0),
+ verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
+ indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
+ renderInstructionsTransform: createTransform(),
+ verticesBufferTransform: createTransform(),
+ invertVerticesBufferTransform: createTransform(),
+ };
+
+ /**
+ * @type {LineStringGeometryBatch}
+ */
+ this.lineStringBatch = {
+ entries: {},
+ geometriesCount: 0,
+ verticesCount: 0,
+ renderInstructions: new Float32Array(0),
+ verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
+ indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
+ renderInstructionsTransform: createTransform(),
+ verticesBufferTransform: createTransform(),
+ invertVerticesBufferTransform: createTransform(),
+ };
+ }
+
+ /**
+ * @param {Array} features Array of features to add to the batch
+ */
+ addFeatures(features) {
+ for (let i = 0; i < features.length; i++) {
+ this.addFeature(features[i]);
+ }
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature to add to the batch
+ */
+ addFeature(feature) {
+ const geometry = feature.getGeometry();
+ if (!geometry) {
+ return;
+ }
+ this.addGeometry_(geometry, feature);
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @return {GeometryBatchItem} Batch item added (or existing one)
+ * @private
+ */
+ addFeatureEntryInPointBatch_(feature) {
+ const uid = getUid(feature);
+ if (!(uid in this.pointBatch.entries)) {
+ this.pointBatch.entries[uid] = {
+ feature: feature,
+ flatCoordss: [],
+ };
+ }
+ return this.pointBatch.entries[uid];
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @return {GeometryBatchItem} Batch item added (or existing one)
+ * @private
+ */
+ addFeatureEntryInLineStringBatch_(feature) {
+ const uid = getUid(feature);
+ if (!(uid in this.lineStringBatch.entries)) {
+ this.lineStringBatch.entries[uid] = {
+ feature: feature,
+ flatCoordss: [],
+ verticesCount: 0,
+ };
+ }
+ return this.lineStringBatch.entries[uid];
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @return {GeometryBatchItem} Batch item added (or existing one)
+ * @private
+ */
+ addFeatureEntryInPolygonBatch_(feature) {
+ const uid = getUid(feature);
+ if (!(uid in this.polygonBatch.entries)) {
+ this.polygonBatch.entries[uid] = {
+ feature: feature,
+ flatCoordss: [],
+ verticesCount: 0,
+ ringsCount: 0,
+ ringsVerticesCounts: [],
+ };
+ }
+ return this.polygonBatch.entries[uid];
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @private
+ */
+ clearFeatureEntryInPointBatch_(feature) {
+ const entry = this.pointBatch.entries[getUid(feature)];
+ if (!entry) {
+ return;
+ }
+ this.pointBatch.geometriesCount -= entry.flatCoordss.length;
+ delete this.pointBatch.entries[getUid(feature)];
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @private
+ */
+ clearFeatureEntryInLineStringBatch_(feature) {
+ const entry = this.lineStringBatch.entries[getUid(feature)];
+ if (!entry) {
+ return;
+ }
+ this.lineStringBatch.verticesCount -= entry.verticesCount;
+ this.lineStringBatch.geometriesCount -= entry.flatCoordss.length;
+ delete this.lineStringBatch.entries[getUid(feature)];
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ * @private
+ */
+ clearFeatureEntryInPolygonBatch_(feature) {
+ const entry = this.polygonBatch.entries[getUid(feature)];
+ if (!entry) {
+ return;
+ }
+ this.polygonBatch.verticesCount -= entry.verticesCount;
+ this.polygonBatch.ringsCount -= entry.ringsCount;
+ this.polygonBatch.geometriesCount -= entry.flatCoordss.length;
+ delete this.polygonBatch.entries[getUid(feature)];
+ }
+
+ /**
+ * @param {import("../../geom").Geometry} geometry Geometry
+ * @param {import("../../Feature").default} feature Feature
+ * @private
+ */
+ addGeometry_(geometry, feature) {
+ const type = geometry.getType();
+ let flatCoords;
+ let verticesCount;
+ let batchEntry;
+ switch (type) {
+ case 'GeometryCollection':
+ /** @type {import("../../geom").GeometryCollection} */ (geometry)
+ .getGeometries()
+ .map((geom) => this.addGeometry_(geom, feature));
+ break;
+ case 'MultiPolygon':
+ /** @type {import("../../geom").MultiPolygon} */ (geometry)
+ .getPolygons()
+ .map((polygon) => this.addGeometry_(polygon, feature));
+ break;
+ case 'MultiLineString':
+ /** @type {import("../../geom").MultiLineString} */ (geometry)
+ .getLineStrings()
+ .map((line) => this.addGeometry_(line, feature));
+ break;
+ case 'MultiPoint':
+ /** @type {import("../../geom").MultiPoint} */ (geometry)
+ .getPoints()
+ .map((point) => this.addGeometry_(point, feature));
+ break;
+ case 'Polygon':
+ const polygonGeom = /** @type {import("../../geom").Polygon} */ (
+ geometry
+ );
+ batchEntry = this.addFeatureEntryInPolygonBatch_(feature);
+ flatCoords = polygonGeom.getFlatCoordinates();
+ verticesCount = flatCoords.length / 2;
+ const ringsCount = polygonGeom.getLinearRingCount();
+ const ringsVerticesCount = polygonGeom
+ .getEnds()
+ .map((end, ind, arr) =>
+ ind > 0 ? (end - arr[ind - 1]) / 2 : end / 2
+ );
+ this.polygonBatch.verticesCount += verticesCount;
+ this.polygonBatch.ringsCount += ringsCount;
+ this.polygonBatch.geometriesCount++;
+ batchEntry.flatCoordss.push(flatCoords);
+ batchEntry.ringsVerticesCounts.push(ringsVerticesCount);
+ batchEntry.verticesCount += verticesCount;
+ batchEntry.ringsCount += ringsCount;
+ polygonGeom
+ .getLinearRings()
+ .map((ring) => this.addGeometry_(ring, feature));
+ break;
+ case 'Point':
+ const pointGeom = /** @type {import("../../geom").Point} */ (geometry);
+ batchEntry = this.addFeatureEntryInPointBatch_(feature);
+ flatCoords = pointGeom.getFlatCoordinates();
+ this.pointBatch.geometriesCount++;
+ batchEntry.flatCoordss.push(flatCoords);
+ break;
+ case 'LineString':
+ case 'LinearRing':
+ const lineGeom = /** @type {import("../../geom").LineString} */ (
+ geometry
+ );
+ batchEntry = this.addFeatureEntryInLineStringBatch_(feature);
+ flatCoords = lineGeom.getFlatCoordinates();
+ verticesCount = flatCoords.length / 2;
+ this.lineStringBatch.verticesCount += verticesCount;
+ this.lineStringBatch.geometriesCount++;
+ batchEntry.flatCoordss.push(flatCoords);
+ batchEntry.verticesCount += verticesCount;
+ break;
+ default:
+ // pass
+ }
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ */
+ changeFeature(feature) {
+ this.clearFeatureEntryInPointBatch_(feature);
+ this.clearFeatureEntryInPolygonBatch_(feature);
+ this.clearFeatureEntryInLineStringBatch_(feature);
+ const geometry = feature.getGeometry();
+ if (!geometry) {
+ return;
+ }
+ this.addGeometry_(geometry, feature);
+ }
+
+ /**
+ * @param {import("../../Feature").default} feature Feature
+ */
+ removeFeature(feature) {
+ this.clearFeatureEntryInPointBatch_(feature);
+ this.clearFeatureEntryInPolygonBatch_(feature);
+ this.clearFeatureEntryInLineStringBatch_(feature);
+ }
+
+ clear() {
+ this.polygonBatch.entries = {};
+ this.polygonBatch.geometriesCount = 0;
+ this.polygonBatch.verticesCount = 0;
+ this.polygonBatch.ringsCount = 0;
+ this.lineStringBatch.entries = {};
+ this.lineStringBatch.geometriesCount = 0;
+ this.lineStringBatch.verticesCount = 0;
+ this.pointBatch.entries = {};
+ this.pointBatch.geometriesCount = 0;
+ }
+}
+
+export default MixedGeometryBatch;
diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js
new file mode 100644
index 0000000000..3c211c3661
--- /dev/null
+++ b/src/ol/render/webgl/PointBatchRenderer.js
@@ -0,0 +1,97 @@
+/**
+ * @module ol/render/webgl/PointBatchRenderer
+ */
+
+import AbstractBatchRenderer from './BatchRenderer.js';
+import {AttributeType} from '../../webgl/Helper.js';
+import {apply as applyTransform} from '../../transform.js';
+
+/**
+ * Names of attributes made available to the vertex shader.
+ * Please note: changing these *will* break custom shaders!
+ * @enum {string}
+ */
+export const Attributes = {
+ POSITION: 'a_position',
+ INDEX: 'a_index',
+};
+
+class PointBatchRenderer extends AbstractBatchRenderer {
+ /**
+ * @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
+ * @param {Worker} worker WebGL worker instance
+ * @param {string} vertexShader Vertex shader
+ * @param {string} fragmentShader Fragment shader
+ * @param {Array} customAttributes List of custom attributes
+ */
+ constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
+ super(helper, worker, vertexShader, fragmentShader, customAttributes);
+
+ // vertices for point must hold both a position (x,y) and an index (their position in the quad)
+ this.attributes = [
+ {
+ name: Attributes.POSITION,
+ size: 2,
+ type: AttributeType.FLOAT,
+ },
+ {
+ name: Attributes.INDEX,
+ size: 1,
+ type: AttributeType.FLOAT,
+ },
+ ].concat(
+ customAttributes.map(function (attribute) {
+ return {
+ name: 'a_' + attribute.name,
+ size: 1,
+ type: AttributeType.FLOAT,
+ };
+ })
+ );
+ }
+
+ /**
+ * Render instructions for lines are structured like so:
+ * [ x0, y0, customAttr0, ... , xN, yN, customAttrN ]
+ * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch Point geometry batch
+ * @override
+ */
+ generateRenderInstructions(batch) {
+ // here we anticipate the amount of render instructions for points:
+ // 2 instructions per vertex for position (x and y)
+ // + 1 instruction per vertex per custom attributes
+ const totalInstructionsCount =
+ (2 + this.customAttributes.length) * batch.geometriesCount;
+ if (
+ !batch.renderInstructions ||
+ batch.renderInstructions.length !== totalInstructionsCount
+ ) {
+ batch.renderInstructions = new Float32Array(totalInstructionsCount);
+ }
+
+ // loop on features to fill the render instructions
+ let batchEntry;
+ const tmpCoords = [];
+ let renderIndex = 0;
+ let value;
+ for (const featureUid in batch.entries) {
+ batchEntry = batch.entries[featureUid];
+ for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
+ tmpCoords[0] = batchEntry.flatCoordss[i][0];
+ tmpCoords[1] = batchEntry.flatCoordss[i][1];
+ applyTransform(batch.renderInstructionsTransform, tmpCoords);
+
+ batch.renderInstructions[renderIndex++] = tmpCoords[0];
+ batch.renderInstructions[renderIndex++] = tmpCoords[1];
+
+ // pushing custom attributes
+ for (let j = 0, jj = this.customAttributes.length; j < jj; j++) {
+ value = this.customAttributes[j].callback(batchEntry.feature);
+ batch.renderInstructions[renderIndex++] = value;
+ }
+ }
+ }
+ }
+}
+
+export default PointBatchRenderer;
diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js
new file mode 100644
index 0000000000..1c6b262820
--- /dev/null
+++ b/src/ol/render/webgl/PolygonBatchRenderer.js
@@ -0,0 +1,117 @@
+/**
+ * @module ol/render/webgl/PolygonBatchRenderer
+ */
+import AbstractBatchRenderer from './BatchRenderer.js';
+import {AttributeType} from '../../webgl/Helper.js';
+import {transform2D} from '../../geom/flat/transform.js';
+
+/**
+ * Names of attributes made available to the vertex shader.
+ * Please note: changing these *will* break custom shaders!
+ * @enum {string}
+ */
+export const Attributes = {
+ POSITION: 'a_position',
+};
+
+class PolygonBatchRenderer extends AbstractBatchRenderer {
+ /**
+ * @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
+ * @param {Worker} worker WebGL worker instance
+ * @param {string} vertexShader Vertex shader
+ * @param {string} fragmentShader Fragment shader
+ * @param {Array} customAttributes List of custom attributes
+ */
+ constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
+ super(helper, worker, vertexShader, fragmentShader, customAttributes);
+
+ // By default only a position attribute is required to render polygons
+ this.attributes = [
+ {
+ name: Attributes.POSITION,
+ size: 2,
+ type: AttributeType.FLOAT,
+ },
+ ].concat(
+ customAttributes.map(function (attribute) {
+ return {
+ name: 'a_' + attribute.name,
+ size: 1,
+ type: AttributeType.FLOAT,
+ };
+ })
+ );
+ }
+
+ /**
+ * Render instructions for polygons are structured like so:
+ * [ customAttr0, ..., customAttrN, numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, ..., xN, yN, numberOfRings,... ]
+ * @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch Polygon geometry batch
+ * @override
+ */
+ generateRenderInstructions(batch) {
+ // here we anticipate the amount of render instructions for polygons:
+ // 2 instructions per vertex for position (x and y)
+ // + 1 instruction per polygon per custom attributes
+ // + 1 instruction per polygon (for vertices count in polygon)
+ // + 1 instruction per ring (for vertices count in ring)
+ const totalInstructionsCount =
+ 2 * batch.verticesCount +
+ (1 + this.customAttributes.length) * batch.geometriesCount +
+ batch.ringsCount;
+ if (
+ !batch.renderInstructions ||
+ batch.renderInstructions.length !== totalInstructionsCount
+ ) {
+ batch.renderInstructions = new Float32Array(totalInstructionsCount);
+ }
+
+ // loop on features to fill the render instructions
+ let batchEntry;
+ const flatCoords = [];
+ let renderIndex = 0;
+ let value;
+ for (const featureUid in batch.entries) {
+ batchEntry = batch.entries[featureUid];
+ for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
+ flatCoords.length = batchEntry.flatCoordss[i].length;
+ transform2D(
+ batchEntry.flatCoordss[i],
+ 0,
+ flatCoords.length,
+ 2,
+ batch.renderInstructionsTransform,
+ flatCoords
+ );
+
+ // custom attributes
+ for (let k = 0, kk = this.customAttributes.length; k < kk; k++) {
+ value = this.customAttributes[k].callback(batchEntry.feature);
+ batch.renderInstructions[renderIndex++] = value;
+ }
+
+ // ring count
+ batch.renderInstructions[renderIndex++] =
+ batchEntry.ringsVerticesCounts[i].length;
+
+ // vertices count in each ring
+ for (
+ let j = 0, jj = batchEntry.ringsVerticesCounts[i].length;
+ j < jj;
+ j++
+ ) {
+ batch.renderInstructions[renderIndex++] =
+ batchEntry.ringsVerticesCounts[i][j];
+ }
+
+ // looping on points for positions
+ for (let j = 0, jj = flatCoords.length; j < jj; j += 2) {
+ batch.renderInstructions[renderIndex++] = flatCoords[j];
+ batch.renderInstructions[renderIndex++] = flatCoords[j + 1];
+ }
+ }
+ }
+ }
+}
+
+export default PolygonBatchRenderer;
diff --git a/src/ol/render/webgl/constants.js b/src/ol/render/webgl/constants.js
new file mode 100644
index 0000000000..edd36810e2
--- /dev/null
+++ b/src/ol/render/webgl/constants.js
@@ -0,0 +1,27 @@
+/**
+ * @module ol/render/webgl/constants
+ */
+
+/**
+ * @enum {string}
+ */
+export const WebGLWorkerMessageType = {
+ GENERATE_POLYGON_BUFFERS: 'GENERATE_POLYGON_BUFFERS',
+ GENERATE_POINT_BUFFERS: 'GENERATE_POINT_BUFFERS',
+ GENERATE_LINE_STRING_BUFFERS: 'GENERATE_LINE_STRING_BUFFERS',
+};
+
+/**
+ * @typedef {Object} WebGLWorkerGenerateBuffersMessage
+ * This message will trigger the generation of a vertex and an index buffer based on the given render instructions.
+ * When the buffers are generated, the worked will send a message of the same type to the main thread, with
+ * the generated buffers in it.
+ * Note that any addition properties present in the message *will* be sent back to the main thread.
+ * @property {number} id Message id; will be used both in request and response as a means of identification
+ * @property {WebGLWorkerMessageType} type Message type
+ * @property {ArrayBuffer} renderInstructions Polygon render instructions raw binary buffer.
+ * @property {number} [customAttributesCount] Amount of custom attributes count in the polygon render instructions.
+ * @property {ArrayBuffer} [vertexBuffer] Vertices array raw binary buffer (sent by the worker).
+ * @property {ArrayBuffer} [indexBuffer] Indices array raw binary buffer (sent by the worker).
+ * @property {import("../../transform").Transform} [renderInstructionsTransform] Transformation matrix used to project the instructions coordinates
+ */
diff --git a/src/ol/render/webgl/utils.js b/src/ol/render/webgl/utils.js
new file mode 100644
index 0000000000..74605c26d9
--- /dev/null
+++ b/src/ol/render/webgl/utils.js
@@ -0,0 +1,351 @@
+/**
+ * @module ol/render/webgl/utils
+ */
+import earcut from 'earcut';
+import {apply as applyTransform} from '../../transform.js';
+import {clamp} from '../../math.js';
+
+const tmpArray_ = [];
+
+/**
+ * An object holding positions both in an index and a vertex buffer.
+ * @typedef {Object} BufferPositions
+ * @property {number} vertexPosition Position in the vertex buffer
+ * @property {number} indexPosition Position in the index buffer
+ */
+const bufferPositions_ = {vertexPosition: 0, indexPosition: 0};
+
+function writePointVertex(buffer, pos, x, y, index) {
+ buffer[pos + 0] = x;
+ buffer[pos + 1] = y;
+ buffer[pos + 2] = index;
+}
+
+/**
+ * Pushes a quad (two triangles) based on a point geometry
+ * @param {Float32Array} instructions Array of render instructions for points.
+ * @param {number} elementIndex Index from which render instructions will be read.
+ * @param {Float32Array} vertexBuffer Buffer in the form of a typed array.
+ * @param {Uint32Array} indexBuffer Buffer in the form of a typed array.
+ * @param {number} customAttributesCount Amount of custom attributes for each element.
+ * @param {BufferPositions} [bufferPositions] Buffer write positions; if not specified, positions will be set at 0.
+ * @return {BufferPositions} New buffer positions where to write next
+ * @property {number} vertexPosition New position in the vertex buffer where future writes should start.
+ * @property {number} indexPosition New position in the index buffer where future writes should start.
+ * @private
+ */
+export function writePointFeatureToBuffers(
+ instructions,
+ elementIndex,
+ vertexBuffer,
+ indexBuffer,
+ customAttributesCount,
+ bufferPositions
+) {
+ // This is for x, y and index
+ const baseVertexAttrsCount = 3;
+ const baseInstructionsCount = 2;
+ const stride = baseVertexAttrsCount + customAttributesCount;
+
+ const x = instructions[elementIndex + 0];
+ const y = instructions[elementIndex + 1];
+
+ // read custom numerical attributes on the feature
+ const customAttrs = tmpArray_;
+ customAttrs.length = customAttributesCount;
+ for (let i = 0; i < customAttrs.length; i++) {
+ customAttrs[i] = instructions[elementIndex + baseInstructionsCount + i];
+ }
+
+ let vPos = bufferPositions ? bufferPositions.vertexPosition : 0;
+ let iPos = bufferPositions ? bufferPositions.indexPosition : 0;
+ const baseIndex = vPos / stride;
+
+ // push vertices for each of the four quad corners (first standard then custom attributes)
+ writePointVertex(vertexBuffer, vPos, x, y, 0);
+ customAttrs.length &&
+ vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
+ vPos += stride;
+
+ writePointVertex(vertexBuffer, vPos, x, y, 1);
+ customAttrs.length &&
+ vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
+ vPos += stride;
+
+ writePointVertex(vertexBuffer, vPos, x, y, 2);
+ customAttrs.length &&
+ vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
+ vPos += stride;
+
+ writePointVertex(vertexBuffer, vPos, x, y, 3);
+ customAttrs.length &&
+ vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
+ vPos += stride;
+
+ indexBuffer[iPos++] = baseIndex;
+ indexBuffer[iPos++] = baseIndex + 1;
+ indexBuffer[iPos++] = baseIndex + 3;
+ indexBuffer[iPos++] = baseIndex + 1;
+ indexBuffer[iPos++] = baseIndex + 2;
+ indexBuffer[iPos++] = baseIndex + 3;
+
+ bufferPositions_.vertexPosition = vPos;
+ bufferPositions_.indexPosition = iPos;
+
+ return bufferPositions_;
+}
+
+/**
+ * Pushes a single quad to form a line segment; also includes a computation for the join angles with previous and next
+ * segment, in order to be able to offset the vertices correctly in the shader
+ * @param {Float32Array} instructions Array of render instructions for lines.
+ * @param {number} segmentStartIndex Index of the segment start point from which render instructions will be read.
+ * @param {number} segmentEndIndex Index of the segment start point from which render instructions will be read.
+ * @param {number|null} beforeSegmentIndex Index of the point right before the segment (null if none, e.g this is a line start)
+ * @param {number|null} afterSegmentIndex Index of the point right after the segment (null if none, e.g this is a line end)
+ * @param {Array} vertexArray Array containing vertices.
+ * @param {Array} indexArray Array containing indices.
+ * @param {Array} customAttributes Array of custom attributes value
+ * @param {import('../../transform.js').Transform} instructionsTransform Transform matrix used to project coordinates in instructions
+ * @param {import('../../transform.js').Transform} invertInstructionsTransform Transform matrix used to project coordinates in instructions
+ * @private
+ */
+export function writeLineSegmentToBuffers(
+ instructions,
+ segmentStartIndex,
+ segmentEndIndex,
+ beforeSegmentIndex,
+ afterSegmentIndex,
+ vertexArray,
+ indexArray,
+ customAttributes,
+ instructionsTransform,
+ invertInstructionsTransform
+) {
+ // compute the stride to determine how many vertices were already pushed
+ const baseVertexAttrsCount = 5; // base attributes: x0, y0, x1, y1, params (vertex number [0-3], join angle 1, join angle 2)
+ const stride = baseVertexAttrsCount + customAttributes.length;
+ const baseIndex = vertexArray.length / stride;
+
+ // The segment is composed of two positions called P0[x0, y0] and P1[x1, y1]
+ // Depending on whether there are points before and after the segment, its final shape
+ // will be different
+ const p0 = [
+ instructions[segmentStartIndex + 0],
+ instructions[segmentStartIndex + 1],
+ ];
+ const p1 = [instructions[segmentEndIndex], instructions[segmentEndIndex + 1]];
+
+ // to compute offsets from the line center we need to reproject
+ // coordinates back in world units and compute the length of the segment
+ const p0world = applyTransform(invertInstructionsTransform, [...p0]);
+ const p1world = applyTransform(invertInstructionsTransform, [...p1]);
+
+ function computeVertexParameters(vertexNumber, joinAngle1, joinAngle2) {
+ const shift = 10000;
+ const anglePrecision = 1500;
+ return (
+ Math.round(joinAngle1 * anglePrecision) +
+ Math.round(joinAngle2 * anglePrecision) * shift +
+ vertexNumber * shift * shift
+ );
+ }
+
+ // compute the angle between p0pA and p0pB
+ // returns a value in [0, 2PI]
+ function angleBetween(p0, pA, pB) {
+ const lenA = Math.sqrt(
+ (pA[0] - p0[0]) * (pA[0] - p0[0]) + (pA[1] - p0[1]) * (pA[1] - p0[1])
+ );
+ const tangentA = [(pA[0] - p0[0]) / lenA, (pA[1] - p0[1]) / lenA];
+ const orthoA = [-tangentA[1], tangentA[0]];
+ const lenB = Math.sqrt(
+ (pB[0] - p0[0]) * (pB[0] - p0[0]) + (pB[1] - p0[1]) * (pB[1] - p0[1])
+ );
+ const tangentB = [(pB[0] - p0[0]) / lenB, (pB[1] - p0[1]) / lenB];
+
+ // this angle can be clockwise or anticlockwise; hence the computation afterwards
+ const angle =
+ lenA === 0 || lenB === 0
+ ? 0
+ : Math.acos(
+ clamp(tangentB[0] * tangentA[0] + tangentB[1] * tangentA[1], -1, 1)
+ );
+ const isClockwise = tangentB[0] * orthoA[0] + tangentB[1] * orthoA[1] > 0;
+ return !isClockwise ? Math.PI * 2 - angle : angle;
+ }
+
+ const joinBefore = beforeSegmentIndex !== null;
+ const joinAfter = afterSegmentIndex !== null;
+
+ let angle0 = 0;
+ let angle1 = 0;
+
+ // add vertices and adapt offsets for P0 in case of join
+ if (joinBefore) {
+ // B for before
+ const pB = [
+ instructions[beforeSegmentIndex],
+ instructions[beforeSegmentIndex + 1],
+ ];
+ const pBworld = applyTransform(invertInstructionsTransform, [...pB]);
+ angle0 = angleBetween(p0world, p1world, pBworld);
+ }
+ // adapt offsets for P1 in case of join
+ if (joinAfter) {
+ // A for after
+ const pA = [
+ instructions[afterSegmentIndex],
+ instructions[afterSegmentIndex + 1],
+ ];
+ const pAworld = applyTransform(invertInstructionsTransform, [...pA]);
+ angle1 = angleBetween(p1world, p0world, pAworld);
+ }
+
+ // add main segment triangles
+ vertexArray.push(
+ p0[0],
+ p0[1],
+ p1[0],
+ p1[1],
+ computeVertexParameters(0, angle0, angle1)
+ );
+ vertexArray.push(...customAttributes);
+
+ vertexArray.push(
+ p0[0],
+ p0[1],
+ p1[0],
+ p1[1],
+ computeVertexParameters(1, angle0, angle1)
+ );
+ vertexArray.push(...customAttributes);
+
+ vertexArray.push(
+ p0[0],
+ p0[1],
+ p1[0],
+ p1[1],
+ computeVertexParameters(2, angle0, angle1)
+ );
+ vertexArray.push(...customAttributes);
+
+ vertexArray.push(
+ p0[0],
+ p0[1],
+ p1[0],
+ p1[1],
+ computeVertexParameters(3, angle0, angle1)
+ );
+ vertexArray.push(...customAttributes);
+
+ indexArray.push(
+ baseIndex,
+ baseIndex + 1,
+ baseIndex + 2,
+ baseIndex + 1,
+ baseIndex + 3,
+ baseIndex + 2
+ );
+}
+
+/**
+ * Pushes several triangles to form a polygon, including holes
+ * @param {Float32Array} instructions Array of render instructions for lines.
+ * @param {number} polygonStartIndex Index of the polygon start point from which render instructions will be read.
+ * @param {Array} vertexArray Array containing vertices.
+ * @param {Array} indexArray Array containing indices.
+ * @param {number} customAttributesCount Amount of custom attributes for each element.
+ * @return {number} Next polygon instructions index
+ * @private
+ */
+export function writePolygonTrianglesToBuffers(
+ instructions,
+ polygonStartIndex,
+ vertexArray,
+ indexArray,
+ customAttributesCount
+) {
+ const instructionsPerVertex = 2; // x, y
+ const attributesPerVertex = 2 + customAttributesCount;
+ let instructionsIndex = polygonStartIndex;
+ const customAttributes = instructions.slice(
+ instructionsIndex,
+ instructionsIndex + customAttributesCount
+ );
+ instructionsIndex += customAttributesCount;
+ const ringsCount = instructions[instructionsIndex++];
+ let verticesCount = 0;
+ const holes = new Array(ringsCount - 1);
+ for (let i = 0; i < ringsCount; i++) {
+ verticesCount += instructions[instructionsIndex++];
+ if (i < ringsCount - 1) {
+ holes[i] = verticesCount;
+ }
+ }
+ const flatCoords = instructions.slice(
+ instructionsIndex,
+ instructionsIndex + verticesCount * instructionsPerVertex
+ );
+
+ // pushing to vertices and indices!! this is where the magic happens
+ const result = earcut(flatCoords, holes, instructionsPerVertex);
+ for (let i = 0; i < result.length; i++) {
+ indexArray.push(result[i] + vertexArray.length / attributesPerVertex);
+ }
+ for (let i = 0; i < flatCoords.length; i += 2) {
+ vertexArray.push(flatCoords[i], flatCoords[i + 1], ...customAttributes);
+ }
+
+ return instructionsIndex + verticesCount * instructionsPerVertex;
+}
+
+/**
+ * Returns a texture of 1x1 pixel, white
+ * @private
+ * @return {ImageData} Image data.
+ */
+export function getBlankImageData() {
+ const canvas = document.createElement('canvas');
+ const image = canvas.getContext('2d').createImageData(1, 1);
+ image.data[0] = 255;
+ image.data[1] = 255;
+ image.data[2] = 255;
+ image.data[3] = 255;
+ 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;
+}
diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js
index a6d9b1c1b4..1587e8c9d8 100644
--- a/src/ol/renderer/webgl/Layer.js
+++ b/src/ol/renderer/webgl/Layer.js
@@ -13,26 +13,6 @@ import {
} from '../../transform.js';
import {containsCoordinate} from '../../extent.js';
-/**
- * @enum {string}
- */
-export const WebGLWorkerMessageType = {
- GENERATE_BUFFERS: 'GENERATE_BUFFERS',
-};
-
-/**
- * @typedef {Object} WebGLWorkerGenerateBuffersMessage
- * This message will trigger the generation of a vertex and an index buffer based on the given render instructions.
- * When the buffers are generated, the worked will send a message of the same type to the main thread, with
- * the generated buffers in it.
- * Note that any addition properties present in the message *will* be sent back to the main thread.
- * @property {WebGLWorkerMessageType} type Message type
- * @property {ArrayBuffer} renderInstructions Render instructions raw binary buffer.
- * @property {ArrayBuffer} [vertexBuffer] Vertices array raw binary buffer (sent by the worker).
- * @property {ArrayBuffer} [indexBuffer] Indices array raw binary buffer (sent by the worker).
- * @property {number} [customAttributesCount] Amount of custom attributes count in the render instructions.
- */
-
/**
* @typedef {Object} PostProcessesOptions
* @property {number} [scaleRatio] Scale ratio; if < 1, the post process will render to a texture smaller than
@@ -343,144 +323,4 @@ class WebGLLayerRenderer extends LayerRenderer {
}
}
-const tmpArray_ = [];
-const bufferPositions_ = {vertexPosition: 0, indexPosition: 0};
-
-function writePointVertex(buffer, pos, x, y, index) {
- buffer[pos + 0] = x;
- buffer[pos + 1] = y;
- buffer[pos + 2] = index;
-}
-
-/**
- * An object holding positions both in an index and a vertex buffer.
- * @typedef {Object} BufferPositions
- * @property {number} vertexPosition Position in the vertex buffer
- * @property {number} indexPosition Position in the index buffer
- */
-
-/**
- * Pushes a quad (two triangles) based on a point geometry
- * @param {Float32Array} instructions Array of render instructions for points.
- * @param {number} elementIndex Index from which render instructions will be read.
- * @param {Float32Array} vertexBuffer Buffer in the form of a typed array.
- * @param {Uint32Array} indexBuffer Buffer in the form of a typed array.
- * @param {number} customAttributesCount Amount of custom attributes for each element.
- * @param {BufferPositions} [bufferPositions] Buffer write positions; if not specified, positions will be set at 0.
- * @return {BufferPositions} New buffer positions where to write next
- * @property {number} vertexPosition New position in the vertex buffer where future writes should start.
- * @property {number} indexPosition New position in the index buffer where future writes should start.
- * @private
- */
-export function writePointFeatureToBuffers(
- instructions,
- elementIndex,
- vertexBuffer,
- indexBuffer,
- customAttributesCount,
- bufferPositions
-) {
- // This is for x, y and index
- const baseVertexAttrsCount = 3;
- const baseInstructionsCount = 2;
- const stride = baseVertexAttrsCount + customAttributesCount;
-
- const x = instructions[elementIndex + 0];
- const y = instructions[elementIndex + 1];
-
- // read custom numerical attributes on the feature
- const customAttrs = tmpArray_;
- customAttrs.length = customAttributesCount;
- for (let i = 0; i < customAttrs.length; i++) {
- customAttrs[i] = instructions[elementIndex + baseInstructionsCount + i];
- }
-
- let vPos = bufferPositions ? bufferPositions.vertexPosition : 0;
- let iPos = bufferPositions ? bufferPositions.indexPosition : 0;
- const baseIndex = vPos / stride;
-
- // push vertices for each of the four quad corners (first standard then custom attributes)
- writePointVertex(vertexBuffer, vPos, x, y, 0);
- customAttrs.length &&
- vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
- vPos += stride;
-
- writePointVertex(vertexBuffer, vPos, x, y, 1);
- customAttrs.length &&
- vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
- vPos += stride;
-
- writePointVertex(vertexBuffer, vPos, x, y, 2);
- customAttrs.length &&
- vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
- vPos += stride;
-
- writePointVertex(vertexBuffer, vPos, x, y, 3);
- customAttrs.length &&
- vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
- vPos += stride;
-
- indexBuffer[iPos++] = baseIndex;
- indexBuffer[iPos++] = baseIndex + 1;
- indexBuffer[iPos++] = baseIndex + 3;
- indexBuffer[iPos++] = baseIndex + 1;
- indexBuffer[iPos++] = baseIndex + 2;
- indexBuffer[iPos++] = baseIndex + 3;
-
- bufferPositions_.vertexPosition = vPos;
- bufferPositions_.indexPosition = iPos;
-
- return bufferPositions_;
-}
-
-/**
- * Returns a texture of 1x1 pixel, white
- * @private
- * @return {ImageData} Image data.
- */
-export function getBlankImageData() {
- const canvas = document.createElement('canvas');
- const image = canvas.getContext('2d').createImageData(1, 1);
- image.data[0] = 255;
- image.data[1] = 255;
- image.data[2] = 255;
- image.data[3] = 255;
- 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/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js
index 2d41b7cead..c7eca3db00 100644
--- a/src/ol/renderer/webgl/PointsLayer.js
+++ b/src/ol/renderer/webgl/PointsLayer.js
@@ -5,14 +5,11 @@ import BaseVector from '../../layer/BaseVector.js';
import VectorEventType from '../../source/VectorEventType.js';
import ViewHint from '../../ViewHint.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
-import WebGLLayerRenderer, {
- WebGLWorkerMessageType,
- colorDecodeId,
- colorEncodeId,
-} from './Layer.js';
+import WebGLLayerRenderer from './Layer.js';
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
import {AttributeType, DefaultUniform} from '../../webgl/Helper.js';
+import {WebGLWorkerMessageType} from '../../render/webgl/constants.js';
import {
apply as applyTransform,
create as createTransform,
@@ -22,6 +19,7 @@ import {
} from '../../transform.js';
import {assert} from '../../asserts.js';
import {buffer, createEmpty, equals, getWidth} from '../../extent.js';
+import {colorDecodeId, colorEncodeId} from '../../render/webgl/utils.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {getUid} from '../../util.js';
import {listen, unlistenByKey} from '../../events.js';
@@ -294,7 +292,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
*/
this.generateBuffersRun_ = 0;
+ /**
+ * @private
+ */
this.worker_ = createWebGLWorker();
+
this.worker_.addEventListener(
'message',
/**
@@ -303,7 +305,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
*/
function (event) {
const received = event.data;
- if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
+ if (received.type === WebGLWorkerMessageType.GENERATE_POINT_BUFFERS) {
const projectionTransform = received.projectionTransform;
if (received.hitDetection) {
this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer);
@@ -540,7 +542,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.previousExtent_ = frameState.extent.slice();
}
- this.helper.useProgram(this.program_);
+ this.helper.useProgram(this.program_, frameState);
this.helper.prepareDraw(frameState);
// write new data
@@ -637,9 +639,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
}
}
- /** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
+ /** @type {import('../../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = {
- type: WebGLWorkerMessageType.GENERATE_BUFFERS,
+ id: 0,
+ type: WebGLWorkerMessageType.GENERATE_POINT_BUFFERS,
renderInstructions: this.renderInstructions_.buffer,
customAttributesCount: this.customAttributes.length,
};
@@ -650,10 +653,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
this.renderInstructions_ = null;
- /** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
+ /** @type {import('../../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
if (this.hitDetectionEnabled_) {
const hitMessage = {
- type: WebGLWorkerMessageType.GENERATE_BUFFERS,
+ id: 0,
+ type: WebGLWorkerMessageType.GENERATE_POINT_BUFFERS,
renderInstructions: this.hitRenderInstructions_.buffer,
customAttributesCount: 5 + this.customAttributes.length,
};
@@ -726,7 +730,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
Math.floor(frameState.size[1] / 2),
]);
- this.helper.useProgram(this.hitProgram_);
+ this.helper.useProgram(this.hitProgram_, frameState);
this.helper.prepareDrawToRenderTarget(
frameState,
this.hitRenderTarget_,
diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js
index 7dbf1cb13e..836d6580de 100644
--- a/src/ol/renderer/webgl/TileLayer.js
+++ b/src/ol/renderer/webgl/TileLayer.js
@@ -508,7 +508,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
}
}
- this.helper.useProgram(this.program_);
+ this.helper.useProgram(this.program_, frameState);
this.helper.prepareDraw(frameState, !blend);
const zs = Object.keys(tileTexturesByZ)
diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js
new file mode 100644
index 0000000000..bb15ce2ccd
--- /dev/null
+++ b/src/ol/renderer/webgl/VectorLayer.js
@@ -0,0 +1,409 @@
+/**
+ * @module ol/renderer/webgl/VectorLayer
+ */
+import BaseVector from '../../layer/BaseVector.js';
+import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js';
+import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js';
+import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js';
+import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js';
+import VectorEventType from '../../source/VectorEventType.js';
+import ViewHint from '../../ViewHint.js';
+import WebGLLayerRenderer from './Layer.js';
+import {DefaultUniform} from '../../webgl/Helper.js';
+import {
+ FILL_FRAGMENT_SHADER,
+ FILL_VERTEX_SHADER,
+ POINT_FRAGMENT_SHADER,
+ POINT_VERTEX_SHADER,
+ STROKE_FRAGMENT_SHADER,
+ STROKE_VERTEX_SHADER,
+ packColor,
+} from './shaders.js';
+import {buffer, createEmpty, equals, getWidth} from '../../extent.js';
+import {create as createTransform} from '../../transform.js';
+import {create as createWebGLWorker} from '../../worker/webgl.js';
+import {listen, unlistenByKey} from '../../events.js';
+
+/**
+ * @typedef {function(import("../../Feature").default, Object):number} CustomAttributeCallback A callback computing
+ * the value of a custom attribute (different for each feature) to be passed on to the GPU.
+ * Properties are available as 2nd arg for quicker access.
+ */
+
+/**
+ * @typedef {Object} ShaderProgram An object containing both shaders (vertex and fragment) as well as the required attributes
+ * @property {string} [vertexShader] Vertex shader source (using the default one if unspecified).
+ * @property {string} [fragmentShader] Fragment shader source (using the default one if unspecified).
+ * @property {Object} attributes Custom attributes made available in the vertex shader.
+ * Keys are the names of the attributes which are then accessible in the vertex shader using the `a_` prefix, e.g.: `a_opacity`.
+ * Default shaders rely on the attributes in {@link module:ol/render/webgl/shaders~DefaultAttributes}.
+ */
+
+/**
+ * @typedef {Object} Options
+ * @property {string} [className='ol-layer'] A CSS class name to set to the canvas element.
+ * @property {ShaderProgram} [fill] Attributes and shaders for filling polygons.
+ * @property {ShaderProgram} [stroke] Attributes and shaders for line strings and polygon strokes.
+ * @property {ShaderProgram} [point] Attributes and shaders for points.
+ * @property {Object} [uniforms] Uniform definitions.
+ * @property {Array} [postProcesses] Post-processes definitions
+ */
+
+/**
+ * @param {Object} obj Lookup of attribute getters.
+ * @return {Array} An array of attribute descriptors.
+ */
+function toAttributesArray(obj) {
+ return Object.keys(obj).map((key) => ({name: key, callback: obj[key]}));
+}
+
+/**
+ * @classdesc
+ * Experimental WebGL vector renderer. Supports polygons, lines and points:
+ * * Polygons are broken down into triangles
+ * * Lines are rendered as strips of quads
+ * * Points are rendered as quads
+ *
+ * You need to provide vertex and fragment shaders as well as custom attributes for each type of geometry. All shaders
+ * can access the uniforms in the {@link module:ol/webgl/Helper~DefaultUniform} enum.
+ * The vertex shaders can access the following attributes depending on the geometry type:
+ * * For polygons: {@link module:ol/render/webgl/PolygonBatchRenderer~Attributes}
+ * * For line strings: {@link module:ol/render/webgl/LineStringBatchRenderer~Attributes}
+ * * For points: {@link module:ol/render/webgl/PointBatchRenderer~Attributes}
+ *
+ * Please note that the fragment shaders output should have premultiplied alpha, otherwise visual anomalies may occur.
+ *
+ * Note: this uses {@link module:ol/webgl/Helper~WebGLHelper} internally.
+ */
+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;
+
+ const fillAttributes = {
+ color: function () {
+ return packColor('#ddd');
+ },
+ opacity: function () {
+ return 1;
+ },
+ ...(options.fill && options.fill.attributes),
+ };
+
+ const strokeAttributes = {
+ color: function () {
+ return packColor('#eee');
+ },
+ opacity: function () {
+ return 1;
+ },
+ width: function () {
+ return 1.5;
+ },
+ ...(options.stroke && options.stroke.attributes),
+ };
+
+ const pointAttributes = {
+ color: function () {
+ return packColor('#eee');
+ },
+ opacity: function () {
+ return 1;
+ },
+ ...(options.point && options.point.attributes),
+ };
+
+ this.fillVertexShader_ =
+ (options.fill && options.fill.vertexShader) || FILL_VERTEX_SHADER;
+ this.fillFragmentShader_ =
+ (options.fill && options.fill.fragmentShader) || FILL_FRAGMENT_SHADER;
+ this.fillAttributes_ = toAttributesArray(fillAttributes);
+
+ this.strokeVertexShader_ =
+ (options.stroke && options.stroke.vertexShader) || STROKE_VERTEX_SHADER;
+ this.strokeFragmentShader_ =
+ (options.stroke && options.stroke.fragmentShader) ||
+ STROKE_FRAGMENT_SHADER;
+ this.strokeAttributes_ = toAttributesArray(strokeAttributes);
+
+ this.pointVertexShader_ =
+ (options.point && options.point.vertexShader) || POINT_VERTEX_SHADER;
+ this.pointFragmentShader_ =
+ (options.point && options.point.fragmentShader) || POINT_FRAGMENT_SHADER;
+ this.pointAttributes_ = toAttributesArray(pointAttributes);
+
+ /**
+ * @private
+ */
+ this.worker_ = createWebGLWorker();
+
+ /**
+ * @private
+ */
+ 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.fillVertexShader_,
+ this.fillFragmentShader_,
+ this.fillAttributes_
+ );
+ this.pointRenderer_ = new PointBatchRenderer(
+ this.helper,
+ this.worker_,
+ this.pointVertexShader_,
+ this.pointFragmentShader_,
+ this.pointAttributes_
+ );
+ this.lineStringRenderer_ = new LineStringBatchRenderer(
+ this.helper,
+ this.worker_,
+ this.strokeVertexShader_,
+ this.strokeFragmentShader_,
+ this.strokeAttributes_
+ );
+ }
+
+ /**
+ * @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);
+
+ const layer = this.getLayer();
+ const vectorSource = layer.getSource();
+ const projection = frameState.viewState.projection;
+ const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
+ const projectionExtent = projection.getExtent();
+ const extent = frameState.extent;
+ const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
+ const endWorld = multiWorld
+ ? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1
+ : 1;
+ let world = multiWorld
+ ? Math.floor((extent[0] - projectionExtent[0]) / worldWidth)
+ : 0;
+
+ do {
+ this.polygonRenderer_.render(
+ this.batch_.polygonBatch,
+ this.currentTransform_,
+ frameState,
+ world * worldWidth
+ );
+ this.lineStringRenderer_.render(
+ this.batch_.lineStringBatch,
+ this.currentTransform_,
+ frameState,
+ world * worldWidth
+ );
+ this.pointRenderer_.render(
+ this.batch_.pointBatch,
+ this.currentTransform_,
+ frameState,
+ world * worldWidth
+ );
+ } while (++world < endWorld);
+
+ 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.ready = false;
+ let remaining = 3;
+ const rebuildCb = () => {
+ remaining--;
+ this.ready = remaining <= 0;
+ this.getLayer().changed();
+ };
+
+ this.polygonRenderer_.rebuild(
+ this.batch_.polygonBatch,
+ frameState,
+ 'Polygon',
+ rebuildCb
+ );
+ this.lineStringRenderer_.rebuild(
+ this.batch_.lineStringBatch,
+ frameState,
+ 'LineString',
+ rebuildCb
+ );
+ this.pointRenderer_.rebuild(
+ this.batch_.pointBatch,
+ frameState,
+ 'Point',
+ rebuildCb
+ );
+ 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} callback Feature callback.
+ * @param {Array>} 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;
diff --git a/src/ol/renderer/webgl/shaders.js b/src/ol/renderer/webgl/shaders.js
new file mode 100644
index 0000000000..da79e3ed05
--- /dev/null
+++ b/src/ol/renderer/webgl/shaders.js
@@ -0,0 +1,198 @@
+/**
+ * @module ol/renderer/webgl/shaders
+ */
+import {asArray} from '../../color.js';
+
+/** @typedef {'color'|'opacity'|'width'} DefaultAttributes */
+
+/**
+ * Packs red/green/blue channels of a color into a single float value; alpha is ignored.
+ * This is how the color is expected to be computed.
+ * @param {import("../../color.js").Color|string} color Color as array of numbers or string
+ * @return {number} Float value containing the color
+ */
+export function packColor(color) {
+ const array = asArray(color);
+ const r = array[0] * 256 * 256;
+ const g = array[1] * 256;
+ const b = array[2];
+ return r + g + b;
+}
+
+const DECODE_COLOR_EXPRESSION = `vec3(
+ fract(floor(a_color / 256.0 / 256.0) / 256.0),
+ fract(floor(a_color / 256.0) / 256.0),
+ fract(a_color / 256.0)
+);`;
+
+/**
+ * Default polygon vertex shader.
+ * Relies on the color and opacity attributes.
+ * @type {string}
+ */
+export const FILL_VERTEX_SHADER = `
+ precision mediump float;
+ uniform mat4 u_projectionMatrix;
+ attribute vec2 a_position;
+ attribute float a_color;
+ attribute float a_opacity;
+ varying vec3 v_color;
+ varying float v_opacity;
+
+ void main(void) {
+ gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0);
+ v_color = ${DECODE_COLOR_EXPRESSION}
+ v_opacity = a_opacity;
+ }`;
+
+/**
+ * Default polygon fragment shader.
+ * @type {string}
+ */
+export const FILL_FRAGMENT_SHADER = `
+ precision mediump float;
+ varying vec3 v_color;
+ varying float v_opacity;
+
+ void main(void) {
+ gl_FragColor = vec4(v_color, 1.0) * v_opacity;
+ }`;
+
+/**
+ * Default linestring vertex shader.
+ * Relies on color, opacity and width attributes.
+ * @type {string}
+ */
+export const STROKE_VERTEX_SHADER = `
+ precision mediump float;
+ uniform mat4 u_projectionMatrix;
+ uniform vec2 u_sizePx;
+ attribute vec2 a_segmentStart;
+ attribute vec2 a_segmentEnd;
+ attribute float a_parameters;
+ attribute float a_color;
+ attribute float a_opacity;
+ attribute float a_width;
+ varying vec2 v_segmentStart;
+ varying vec2 v_segmentEnd;
+ varying float v_angleStart;
+ varying float v_angleEnd;
+ varying vec3 v_color;
+ varying float v_opacity;
+ varying float v_width;
+
+ vec2 worldToPx(vec2 worldPos) {
+ vec4 screenPos = u_projectionMatrix * vec4(worldPos, 0.0, 1.0);
+ return (0.5 * screenPos.xy + 0.5) * u_sizePx;
+ }
+
+ vec4 pxToScreen(vec2 pxPos) {
+ vec2 screenPos = pxPos * 4.0 / u_sizePx;
+ return vec4(screenPos.xy, 0.0, 0.0);
+ }
+
+ vec2 getOffsetDirection(vec2 normalPx, vec2 tangentPx, float joinAngle) {
+ if (cos(joinAngle) > 0.93) return normalPx - tangentPx;
+ float halfAngle = joinAngle / 2.0;
+ vec2 angleBisectorNormal = vec2(
+ sin(halfAngle) * normalPx.x + cos(halfAngle) * normalPx.y,
+ -cos(halfAngle) * normalPx.x + sin(halfAngle) * normalPx.y
+ );
+ float length = 1.0 / sin(halfAngle);
+ return angleBisectorNormal * length;
+ }
+
+ void main(void) {
+ float anglePrecision = 1500.0;
+ float paramShift = 10000.0;
+ v_angleStart = fract(a_parameters / paramShift) * paramShift / anglePrecision;
+ v_angleEnd = fract(floor(a_parameters / paramShift + 0.5) / paramShift) * paramShift / anglePrecision;
+ float vertexNumber = floor(a_parameters / paramShift / paramShift + 0.0001);
+ vec2 tangentPx = worldToPx(a_segmentEnd) - worldToPx(a_segmentStart);
+ tangentPx = normalize(tangentPx);
+ vec2 normalPx = vec2(-tangentPx.y, tangentPx.x);
+ float normalDir = vertexNumber < 0.5 || (vertexNumber > 1.5 && vertexNumber < 2.5) ? 1.0 : -1.0;
+ float tangentDir = vertexNumber < 1.5 ? 1.0 : -1.0;
+ float angle = vertexNumber < 1.5 ? v_angleStart : v_angleEnd;
+ vec2 offsetPx = getOffsetDirection(normalPx * normalDir, tangentDir * tangentPx, angle) * a_width * 0.5;
+ vec2 position = vertexNumber < 1.5 ? a_segmentStart : a_segmentEnd;
+ gl_Position = u_projectionMatrix * vec4(position, 0.0, 1.0) + pxToScreen(offsetPx);
+ v_segmentStart = worldToPx(a_segmentStart);
+ v_segmentEnd = worldToPx(a_segmentEnd);
+ v_color = ${DECODE_COLOR_EXPRESSION}
+ v_opacity = a_opacity;
+ v_width = a_width;
+ }`;
+
+/**
+ * Default linestring fragment shader.
+ * @type {string}
+ */
+export const STROKE_FRAGMENT_SHADER = `
+ precision mediump float;
+ uniform float u_pixelRatio;
+ varying vec2 v_segmentStart;
+ varying vec2 v_segmentEnd;
+ varying float v_angleStart;
+ varying float v_angleEnd;
+ varying vec3 v_color;
+ varying float v_opacity;
+ varying float v_width;
+
+ float segmentDistanceField(vec2 point, vec2 start, vec2 end, float radius) {
+ vec2 startToPoint = point - start;
+ vec2 startToEnd = end - start;
+ float ratio = clamp(dot(startToPoint, startToEnd) / dot(startToEnd, startToEnd), 0.0, 1.0);
+ float dist = length(startToPoint - ratio * startToEnd);
+ return 1.0 - smoothstep(radius - 1.0, radius, dist);
+ }
+
+ void main(void) {
+ vec2 v_currentPoint = gl_FragCoord.xy / u_pixelRatio;
+ gl_FragColor = vec4(v_color, 1.0) * v_opacity;
+ gl_FragColor *= segmentDistanceField(v_currentPoint, v_segmentStart, v_segmentEnd, v_width);
+ }`;
+
+/**
+ * Default point vertex shader.
+ * Relies on color and opacity attributes.
+ * @type {string}
+ */
+export const POINT_VERTEX_SHADER = `
+ precision mediump float;
+ uniform mat4 u_projectionMatrix;
+ uniform mat4 u_offsetScaleMatrix;
+ attribute vec2 a_position;
+ attribute float a_index;
+ attribute float a_color;
+ attribute float a_opacity;
+ varying vec2 v_texCoord;
+ varying vec3 v_color;
+ varying float v_opacity;
+
+ void main(void) {
+ mat4 offsetMatrix = u_offsetScaleMatrix;
+ float size = 6.0;
+ float offsetX = a_index == 0.0 || a_index == 3.0 ? -size / 2.0 : size / 2.0;
+ float offsetY = a_index == 0.0 || a_index == 1.0 ? -size / 2.0 : size / 2.0;
+ vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
+ gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
+ float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;
+ float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;
+ v_texCoord = vec2(u, v);
+ v_color = ${DECODE_COLOR_EXPRESSION}
+ v_opacity = a_opacity;
+ }`;
+
+/**
+ * Default point fragment shader.
+ * @type {string}
+ */
+export const POINT_FRAGMENT_SHADER = `
+ precision mediump float;
+ varying vec3 v_color;
+ varying float v_opacity;
+
+ void main(void) {
+ gl_FragColor = vec4(v_color, 1.0) * v_opacity;
+ }`;
diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js
index 383b06e848..b3c39e3a74 100644
--- a/src/ol/webgl/Helper.js
+++ b/src/ol/webgl/Helper.js
@@ -38,8 +38,8 @@ export const ShaderType = {
};
/**
- * Uniform names used in the default shaders: `PROJECTION_MATRIX`, `OFFSET_SCALE_MATRIX`.
- * and `OFFSET_ROTATION_MATRIX`.
+ * Names of uniforms made available to all shaders.
+ * Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const DefaultUniform = {
@@ -49,6 +49,8 @@ export const DefaultUniform = {
TIME: 'u_time',
ZOOM: 'u_zoom',
RESOLUTION: 'u_resolution',
+ SIZE_PX: 'u_sizePx',
+ PIXEL_RATIO: 'u_pixelRatio',
};
/**
@@ -206,11 +208,12 @@ function releaseCanvas(key) {
* Shaders must be compiled and assembled into a program like so:
* ```js
* // here we simply create two shaders and assemble them in a program which is then used
- * // for subsequent rendering calls
+ * // for subsequent rendering calls; note how a frameState is required to set up a program,
+ * // as several default uniforms are computed from it (projection matrix, zoom level, etc.)
* const vertexShader = new WebGLVertex(VERTEX_SHADER);
* const fragmentShader = new WebGLFragment(FRAGMENT_SHADER);
* const program = this.context.getProgram(fragmentShader, vertexShader);
- * helper.useProgram(this.program);
+ * helper.useProgram(this.program, frameState);
* ```
*
* Uniforms are defined using the `uniforms` option and can either be explicit values or callbacks taking the frame state as argument.
@@ -302,8 +305,6 @@ function releaseCanvas(key) {
* ```
*
* For an example usage of this class, refer to {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
- *
- * @api
*/
class WebGLHelper extends Disposable {
/**
@@ -484,7 +485,6 @@ class WebGLHelper extends Disposable {
* the WebGL buffer, bind it, populate it, and add an entry to
* the cache.
* @param {import("./Buffer").default} buffer Buffer.
- * @api
*/
bindBuffer(buffer) {
const gl = this.getGL();
@@ -505,7 +505,6 @@ class WebGLHelper extends Disposable {
* Update the data contained in the buffer array; this is required for the
* new data to be rendered
* @param {import("./Buffer").default} buffer Buffer.
- * @api
*/
flushBufferData(buffer) {
const gl = this.getGL();
@@ -551,7 +550,6 @@ class WebGLHelper extends Disposable {
* subsequent draw calls.
* @param {import("../PluggableMap.js").FrameState} frameState current frame state
* @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen.
- * @api
*/
prepareDraw(frameState, opt_disableAlphaBlend) {
const gl = this.getGL();
@@ -564,8 +562,6 @@ class WebGLHelper extends Disposable {
canvas.style.width = size[0] + 'px';
canvas.style.height = size[1] + 'px';
- gl.useProgram(this.currentProgram_);
-
// loop backwards in post processes list
for (let i = this.postProcessPasses_.length - 1; i >= 0; i--) {
this.postProcessPasses_[i].init(frameState);
@@ -581,10 +577,6 @@ class WebGLHelper extends Disposable {
gl.ONE,
opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA
);
-
- gl.useProgram(this.currentProgram_);
- this.applyFrameState(frameState);
- this.applyUniforms(frameState);
}
/**
@@ -609,17 +601,12 @@ class WebGLHelper extends Disposable {
gl.ONE,
opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA
);
-
- gl.useProgram(this.currentProgram_);
- this.applyFrameState(frameState);
- this.applyUniforms(frameState);
}
/**
* Execute a draw call based on the currently bound program, texture, buffers, attributes.
* @param {number} start Start index.
* @param {number} end End index.
- * @api
*/
drawElements(start, end) {
const gl = this.getGL();
@@ -660,7 +647,6 @@ class WebGLHelper extends Disposable {
/**
* @return {HTMLCanvasElement} Canvas.
- * @api
*/
getCanvas() {
return this.canvas_;
@@ -669,7 +655,6 @@ class WebGLHelper extends Disposable {
/**
* Get the WebGL rendering context
* @return {WebGLRenderingContext} The rendering context.
- * @api
*/
getGL() {
return this.gl_;
@@ -682,6 +667,7 @@ class WebGLHelper extends Disposable {
applyFrameState(frameState) {
const size = frameState.size;
const rotation = frameState.viewState.rotation;
+ const pixelRatio = frameState.pixelRatio;
const offsetScaleMatrix = resetTransform(this.offsetScaleMatrix_);
scaleTransform(offsetScaleMatrix, 2 / size[0], 2 / size[1]);
@@ -709,6 +695,8 @@ class WebGLHelper extends Disposable {
DefaultUniform.RESOLUTION,
frameState.viewState.resolution
);
+ this.setUniformFloatValue(DefaultUniform.PIXEL_RATIO, pixelRatio);
+ this.setUniformFloatVec2(DefaultUniform.SIZE_PX, [size[0], size[1]]);
}
/**
@@ -803,22 +791,19 @@ class WebGLHelper extends Disposable {
}
/**
- * Use a program. If the program is already in use, this will return `false`.
+ * Set up a program for use. The program will be set as the current one. Then, the uniforms used
+ * in the program will be set based on the current frame state and the helper configuration.
* @param {WebGLProgram} program Program.
- * @return {boolean} Changed.
- * @api
+ * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
- useProgram(program) {
- if (program == this.currentProgram_) {
- return false;
- } else {
- const gl = this.getGL();
- gl.useProgram(program);
- this.currentProgram_ = program;
- this.uniformLocations_ = {};
- this.attribLocations_ = {};
- return true;
- }
+ useProgram(program, frameState) {
+ const gl = this.getGL();
+ gl.useProgram(program);
+ this.currentProgram_ = program;
+ this.uniformLocations_ = {};
+ this.attribLocations_ = {};
+ this.applyFrameState(frameState);
+ this.applyUniforms(frameState);
}
/**
@@ -843,7 +828,6 @@ class WebGLHelper extends Disposable {
* @param {string} fragmentShaderSource Fragment shader source.
* @param {string} vertexShaderSource Vertex shader source.
* @return {WebGLProgram} Program
- * @api
*/
getProgram(fragmentShaderSource, vertexShaderSource) {
const gl = this.getGL();
@@ -893,7 +877,6 @@ class WebGLHelper extends Disposable {
* Will get the location from the shader or the cache
* @param {string} name Uniform name
* @return {WebGLUniformLocation} uniformLocation
- * @api
*/
getUniformLocation(name) {
if (this.uniformLocations_[name] === undefined) {
@@ -909,7 +892,6 @@ class WebGLHelper extends Disposable {
* Will get the location from the shader or the cache
* @param {string} name Attribute name
* @return {number} attribLocation
- * @api
*/
getAttributeLocation(name) {
if (this.attribLocations_[name] === undefined) {
@@ -927,7 +909,6 @@ class WebGLHelper extends Disposable {
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../transform").Transform} transform Transform to update.
* @return {import("../transform").Transform} The updated transform object.
- * @api
*/
makeProjectionTransform(frameState, transform) {
const size = frameState.size;
@@ -953,12 +934,20 @@ class WebGLHelper extends Disposable {
* Give a value for a standard float uniform
* @param {string} uniform Uniform name
* @param {number} value Value
- * @api
*/
setUniformFloatValue(uniform, value) {
this.getGL().uniform1f(this.getUniformLocation(uniform), value);
}
+ /**
+ * Give a value for a vec2 uniform
+ * @param {string} uniform Uniform name
+ * @param {Array} value Array of length 4.
+ */
+ setUniformFloatVec2(uniform, value) {
+ this.getGL().uniform2fv(this.getUniformLocation(uniform), value);
+ }
+
/**
* Give a value for a vec4 uniform
* @param {string} uniform Uniform name
@@ -972,7 +961,6 @@ class WebGLHelper extends Disposable {
* Give a value for a standard matrix4 uniform
* @param {string} uniform Uniform name
* @param {Array} value Matrix value
- * @api
*/
setUniformMatrixValue(uniform, value) {
this.getGL().uniformMatrix4fv(
@@ -1014,7 +1002,6 @@ class WebGLHelper extends Disposable {
* i.e. tell the GPU where to read the different attributes in the buffer. An error in the
* size/type/order of attributes will most likely break the rendering and throw a WebGL exception.
* @param {Array} attributes Ordered list of attributes to read from the buffer
- * @api
*/
enableAttributes(attributes) {
const stride = computeAttributesStride(attributes);
@@ -1056,7 +1043,6 @@ class WebGLHelper extends Disposable {
* @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
*/
createTexture(size, opt_data, opt_texture) {
const gl = this.getGL();
@@ -1103,7 +1089,6 @@ class WebGLHelper extends Disposable {
* Compute a stride in bytes based on a list of attributes
* @param {Array} attributes Ordered list of attributes
* @return {number} Stride, ie amount of values for each vertex in the vertex buffer
- * @api
*/
export function computeAttributesStride(attributes) {
let stride = 0;
diff --git a/src/ol/worker/webgl.js b/src/ol/worker/webgl.js
index b4c5a9a6b5..34cb5509f5 100644
--- a/src/ol/worker/webgl.js
+++ b/src/ol/worker/webgl.js
@@ -2,60 +2,176 @@
* A worker that does cpu-heavy tasks related to webgl rendering.
* @module ol/worker/webgl
*/
-import {
- WebGLWorkerMessageType,
- writePointFeatureToBuffers,
-} from '../renderer/webgl/Layer.js';
+import {WebGLWorkerMessageType} from '../render/webgl/constants.js';
import {assign} from '../obj.js';
+import {
+ create as createTransform,
+ makeInverse as makeInverseTransform,
+} from '../transform.js';
+import {
+ writeLineSegmentToBuffers,
+ writePointFeatureToBuffers,
+ writePolygonTrianglesToBuffers,
+} from '../render/webgl/utils.js';
/** @type {any} */
const worker = self;
worker.onmessage = (event) => {
const received = event.data;
- if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
- // This is specific to point features (x, y, index)
- const baseVertexAttrsCount = 3;
- const baseInstructionsCount = 2;
+ switch (received.type) {
+ case WebGLWorkerMessageType.GENERATE_POINT_BUFFERS: {
+ // This is specific to point features (x, y, index)
+ const baseVertexAttrsCount = 3;
+ const baseInstructionsCount = 2;
- const customAttrsCount = received.customAttributesCount;
- const instructionsCount = baseInstructionsCount + customAttrsCount;
- const renderInstructions = new Float32Array(received.renderInstructions);
+ const customAttrsCount = received.customAttributesCount;
+ const instructionsCount = baseInstructionsCount + customAttrsCount;
+ const renderInstructions = new Float32Array(received.renderInstructions);
- const elementsCount = renderInstructions.length / instructionsCount;
- const indicesCount = elementsCount * 6;
- const verticesCount =
- elementsCount * 4 * (customAttrsCount + baseVertexAttrsCount);
- const indexBuffer = new Uint32Array(indicesCount);
- const vertexBuffer = new Float32Array(verticesCount);
+ const elementsCount = renderInstructions.length / instructionsCount;
+ const indicesCount = elementsCount * 6;
+ const verticesCount =
+ elementsCount * 4 * (customAttrsCount + baseVertexAttrsCount);
+ const indexBuffer = new Uint32Array(indicesCount);
+ const vertexBuffer = new Float32Array(verticesCount);
- let bufferPositions;
- for (let i = 0; i < renderInstructions.length; i += instructionsCount) {
- bufferPositions = writePointFeatureToBuffers(
- renderInstructions,
- i,
- vertexBuffer,
- indexBuffer,
- customAttrsCount,
- bufferPositions
+ let bufferPositions;
+ for (let i = 0; i < renderInstructions.length; i += instructionsCount) {
+ bufferPositions = writePointFeatureToBuffers(
+ renderInstructions,
+ i,
+ vertexBuffer,
+ indexBuffer,
+ customAttrsCount,
+ bufferPositions
+ );
+ }
+
+ /** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
+ const message = assign(
+ {
+ vertexBuffer: vertexBuffer.buffer,
+ indexBuffer: indexBuffer.buffer,
+ renderInstructions: renderInstructions.buffer,
+ },
+ received
);
+
+ worker.postMessage(message, [
+ vertexBuffer.buffer,
+ indexBuffer.buffer,
+ renderInstructions.buffer,
+ ]);
+ break;
}
+ case WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS: {
+ const vertices = [];
+ const indices = [];
- /** @type {import('../renderer/webgl/Layer').WebGLWorkerGenerateBuffersMessage} */
- const message = assign(
- {
- vertexBuffer: vertexBuffer.buffer,
- indexBuffer: indexBuffer.buffer,
- renderInstructions: renderInstructions.buffer,
- },
- received
- );
+ const customAttrsCount = received.customAttributesCount;
+ const instructionsPerVertex = 2;
- worker.postMessage(message, [
- vertexBuffer.buffer,
- indexBuffer.buffer,
- renderInstructions.buffer,
- ]);
+ const renderInstructions = new Float32Array(received.renderInstructions);
+ let currentInstructionsIndex = 0;
+
+ const transform = received.renderInstructionsTransform;
+ const invertTransform = createTransform();
+ makeInverseTransform(invertTransform, transform);
+
+ let verticesCount, customAttributes;
+ while (currentInstructionsIndex < renderInstructions.length) {
+ customAttributes = Array.from(
+ renderInstructions.slice(
+ currentInstructionsIndex,
+ currentInstructionsIndex + customAttrsCount
+ )
+ );
+ currentInstructionsIndex += customAttrsCount;
+ verticesCount = renderInstructions[currentInstructionsIndex++];
+
+ // last point is only a segment end, do not loop over it
+ for (let i = 0; i < verticesCount - 1; i++) {
+ writeLineSegmentToBuffers(
+ renderInstructions,
+ currentInstructionsIndex + i * instructionsPerVertex,
+ currentInstructionsIndex + (i + 1) * instructionsPerVertex,
+ i > 0
+ ? currentInstructionsIndex + (i - 1) * instructionsPerVertex
+ : null,
+ i < verticesCount - 2
+ ? currentInstructionsIndex + (i + 2) * instructionsPerVertex
+ : null,
+ vertices,
+ indices,
+ customAttributes,
+ transform,
+ invertTransform
+ );
+ }
+ currentInstructionsIndex += verticesCount * instructionsPerVertex;
+ }
+
+ const indexBuffer = Uint32Array.from(indices);
+ const vertexBuffer = Float32Array.from(vertices);
+
+ /** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
+ const message = assign(
+ {
+ vertexBuffer: vertexBuffer.buffer,
+ indexBuffer: indexBuffer.buffer,
+ renderInstructions: renderInstructions.buffer,
+ },
+ received
+ );
+
+ worker.postMessage(message, [
+ vertexBuffer.buffer,
+ indexBuffer.buffer,
+ renderInstructions.buffer,
+ ]);
+ break;
+ }
+ case WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS: {
+ const vertices = [];
+ const indices = [];
+
+ const customAttrsCount = received.customAttributesCount;
+ const renderInstructions = new Float32Array(received.renderInstructions);
+
+ let currentInstructionsIndex = 0;
+ while (currentInstructionsIndex < renderInstructions.length) {
+ currentInstructionsIndex = writePolygonTrianglesToBuffers(
+ renderInstructions,
+ currentInstructionsIndex,
+ vertices,
+ indices,
+ customAttrsCount
+ );
+ }
+
+ const indexBuffer = Uint32Array.from(indices);
+ const vertexBuffer = Float32Array.from(vertices);
+
+ /** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
+ const message = assign(
+ {
+ vertexBuffer: vertexBuffer.buffer,
+ indexBuffer: indexBuffer.buffer,
+ renderInstructions: renderInstructions.buffer,
+ },
+ received
+ );
+
+ worker.postMessage(message, [
+ vertexBuffer.buffer,
+ indexBuffer.buffer,
+ renderInstructions.buffer,
+ ]);
+ break;
+ }
+ default:
+ // pass
}
};
diff --git a/test/browser/spec/ol/render/webgl/BatchRenderer.test.js b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js
new file mode 100644
index 0000000000..1c46a69679
--- /dev/null
+++ b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js
@@ -0,0 +1,307 @@
+import Feature from '../../../../../../src/ol/Feature.js';
+import LineString from '../../../../../../src/ol/geom/LineString.js';
+import LineStringBatchRenderer from '../../../../../../src/ol/render/webgl/LineStringBatchRenderer.js';
+import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js';
+import Point from '../../../../../../src/ol/geom/Point.js';
+import PointBatchRenderer from '../../../../../../src/ol/render/webgl/PointBatchRenderer.js';
+import Polygon from '../../../../../../src/ol/geom/Polygon.js';
+import PolygonBatchRenderer from '../../../../../../src/ol/render/webgl/PolygonBatchRenderer.js';
+import WebGLHelper from '../../../../../../src/ol/webgl/Helper.js';
+import {FLOAT} from '../../../../../../src/ol/webgl.js';
+import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js';
+import {
+ create as createTransform,
+ translate as translateTransform,
+} from '../../../../../../src/ol/transform.js';
+import {create as createWebGLWorker} from '../../../../../../src/ol/worker/webgl.js';
+
+const POINT_VERTEX_SHADER = `precision mediump float;
+void main(void) {}`;
+const POINT_FRAGMENT_SHADER = `precision mediump float;
+void main(void) {}`;
+
+const SAMPLE_FRAMESTATE = {
+ viewState: {
+ center: [0, 10],
+ resolution: 1,
+ rotation: 0,
+ },
+ size: [10, 10],
+};
+
+describe('Batch renderers', function () {
+ let batchRenderer, helper, mixedBatch, worker, attributes;
+
+ beforeEach(function () {
+ helper = new WebGLHelper();
+ worker = createWebGLWorker();
+ attributes = [
+ {
+ name: 'test',
+ callback: function (feature, properties) {
+ return feature.get('test');
+ },
+ },
+ ];
+
+ mixedBatch = new MixedGeometryBatch();
+ mixedBatch.addFeatures([
+ new Feature({
+ test: 1000,
+ geometry: new Point([10, 20]),
+ }),
+ new Feature({
+ test: 2000,
+ geometry: new Point([30, 40]),
+ }),
+ new Feature({
+ test: 3000,
+ geometry: new Polygon([
+ [
+ [10, 10],
+ [20, 10],
+ [30, 20],
+ [20, 40],
+ [10, 10],
+ ],
+ ]),
+ }),
+ new Feature({
+ test: 4000,
+ geometry: new LineString([
+ [100, 200],
+ [300, 400],
+ [500, 600],
+ ]),
+ }),
+ ]);
+ });
+
+ describe('PointBatchRenderer', function () {
+ beforeEach(function () {
+ batchRenderer = new PointBatchRenderer(
+ helper,
+ worker,
+ POINT_VERTEX_SHADER,
+ POINT_FRAGMENT_SHADER,
+ attributes
+ );
+ });
+ describe('constructor', function () {
+ it('generates the attributes list', function () {
+ expect(batchRenderer.attributes).to.eql([
+ {name: 'a_position', size: 2, type: FLOAT},
+ {name: 'a_index', size: 1, type: FLOAT},
+ {name: 'a_test', size: 1, type: FLOAT},
+ ]);
+ });
+ });
+ describe('#rebuild', function () {
+ let rebuildCb;
+ beforeEach(function (done) {
+ sinon.spy(helper, 'flushBufferData');
+ rebuildCb = sinon.spy();
+ batchRenderer.rebuild(
+ mixedBatch.pointBatch,
+ SAMPLE_FRAMESTATE,
+ 'Point',
+ rebuildCb
+ );
+ // wait for worker response for our specific message
+ worker.addEventListener('message', function (event) {
+ if (
+ event.data.type === WebGLWorkerMessageType.GENERATE_POINT_BUFFERS &&
+ event.data.renderInstructions.byteLength > 0
+ ) {
+ done();
+ }
+ });
+ });
+ it('generates render instructions and updates buffers from the worker response', function () {
+ expect(Array.from(mixedBatch.pointBatch.renderInstructions)).to.eql([
+ 2, 2, 1000, 6, 6, 2000,
+ ]);
+ });
+ it('updates buffers', function () {
+ expect(
+ mixedBatch.pointBatch.verticesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(
+ mixedBatch.pointBatch.indicesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(helper.flushBufferData.calledTwice).to.be(true);
+ });
+ it('updates the instructions transform', function () {
+ expect(mixedBatch.pointBatch.renderInstructionsTransform).to.eql([
+ 0.2, 0, 0, 0.2, 0, -2,
+ ]);
+ });
+ it('calls the provided callback', function () {
+ expect(rebuildCb.calledOnce).to.be(true);
+ });
+ });
+ describe('#render (from parent)', function () {
+ let transform;
+ const offsetX = 12;
+ beforeEach(function () {
+ sinon.spy(helper, 'makeProjectionTransform');
+ sinon.spy(helper, 'useProgram');
+ sinon.spy(helper, 'bindBuffer');
+ sinon.spy(helper, 'enableAttributes');
+ sinon.spy(helper, 'drawElements');
+
+ transform = createTransform();
+ batchRenderer.render(
+ mixedBatch.pointBatch,
+ transform,
+ SAMPLE_FRAMESTATE,
+ offsetX
+ );
+ });
+ it('computes current transform', function () {
+ expect(helper.makeProjectionTransform.calledOnce).to.be(true);
+ });
+ it('includes the X offset in the transform used for rendering', function () {
+ const expected = helper.makeProjectionTransform(
+ SAMPLE_FRAMESTATE,
+ createTransform()
+ );
+ translateTransform(expected, offsetX, 0);
+ expect(transform).to.eql(expected);
+ });
+ it('computes sets up render parameters', function () {
+ expect(helper.useProgram.calledOnce).to.be(true);
+ expect(helper.enableAttributes.calledOnce).to.be(true);
+ expect(helper.bindBuffer.calledTwice).to.be(true);
+ });
+ it('renders elements', function () {
+ expect(helper.drawElements.calledOnce).to.be(true);
+ });
+ });
+ });
+
+ describe('LineStringBatchRenderer', function () {
+ beforeEach(function () {
+ batchRenderer = new LineStringBatchRenderer(
+ helper,
+ worker,
+ POINT_VERTEX_SHADER,
+ POINT_FRAGMENT_SHADER,
+ attributes
+ );
+ });
+ describe('constructor', function () {
+ it('generates the attributes list', function () {
+ expect(batchRenderer.attributes).to.eql([
+ {name: 'a_segmentStart', size: 2, type: FLOAT},
+ {name: 'a_segmentEnd', size: 2, type: FLOAT},
+ {name: 'a_parameters', size: 1, type: FLOAT},
+ {name: 'a_test', size: 1, type: FLOAT},
+ ]);
+ });
+ });
+ describe('#rebuild', function () {
+ let rebuildCb;
+ beforeEach(function (done) {
+ sinon.spy(helper, 'flushBufferData');
+ rebuildCb = sinon.spy();
+ batchRenderer.rebuild(
+ mixedBatch.lineStringBatch,
+ SAMPLE_FRAMESTATE,
+ 'LineString',
+ rebuildCb
+ );
+ // wait for worker response for our specific message
+ worker.addEventListener('message', function (event) {
+ if (
+ event.data.type ===
+ WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS &&
+ event.data.renderInstructions.byteLength > 0
+ ) {
+ done();
+ }
+ });
+ });
+ it('generates render instructions and updates buffers from the worker response', function () {
+ expect(
+ Array.from(mixedBatch.lineStringBatch.renderInstructions)
+ ).to.eql([
+ 3000, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0, 4000, 3, 20, 38, 60, 78, 100,
+ 118,
+ ]);
+ });
+ it('updates buffers', function () {
+ expect(
+ mixedBatch.lineStringBatch.verticesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(
+ mixedBatch.lineStringBatch.indicesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(helper.flushBufferData.calledTwice).to.be(true);
+ });
+ it('calls the provided callback', function () {
+ expect(rebuildCb.calledOnce).to.be(true);
+ });
+ });
+ });
+
+ describe('PolygonBatchRenderer', function () {
+ beforeEach(function () {
+ batchRenderer = new PolygonBatchRenderer(
+ helper,
+ worker,
+ POINT_VERTEX_SHADER,
+ POINT_FRAGMENT_SHADER,
+ attributes
+ );
+ });
+ describe('constructor', function () {
+ it('generates the attributes list', function () {
+ expect(batchRenderer.attributes).to.eql([
+ {name: 'a_position', size: 2, type: FLOAT},
+ {name: 'a_test', size: 1, type: FLOAT},
+ ]);
+ });
+ });
+ describe('#rebuild', function () {
+ let rebuildCb;
+ beforeEach(function (done) {
+ sinon.spy(helper, 'flushBufferData');
+ rebuildCb = sinon.spy();
+ batchRenderer.rebuild(
+ mixedBatch.polygonBatch,
+ SAMPLE_FRAMESTATE,
+ 'Polygon',
+ rebuildCb
+ );
+ // wait for worker response for our specific message
+ worker.addEventListener('message', function (event) {
+ if (
+ event.data.type ===
+ WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS &&
+ event.data.renderInstructions.byteLength > 0
+ ) {
+ done();
+ }
+ });
+ });
+ it('generates render instructions and updates buffers from the worker response', function () {
+ expect(Array.from(mixedBatch.polygonBatch.renderInstructions)).to.eql([
+ 3000, 1, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0,
+ ]);
+ });
+ it('updates buffers', function () {
+ expect(
+ mixedBatch.polygonBatch.verticesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(
+ mixedBatch.polygonBatch.indicesBuffer.getArray().length
+ ).to.be.greaterThan(0);
+ expect(helper.flushBufferData.calledTwice).to.be(true);
+ });
+ it('calls the provided callback', function () {
+ expect(rebuildCb.calledOnce).to.be(true);
+ });
+ });
+ });
+});
diff --git a/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js b/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js
new file mode 100644
index 0000000000..d870a9f8a4
--- /dev/null
+++ b/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js
@@ -0,0 +1,763 @@
+import Feature from '../../../../../../src/ol/Feature.js';
+import GeometryCollection from '../../../../../../src/ol/geom/GeometryCollection.js';
+import LineString from '../../../../../../src/ol/geom/LineString.js';
+import LinearRing from '../../../../../../src/ol/geom/LinearRing.js';
+import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js';
+import MultiLineString from '../../../../../../src/ol/geom/MultiLineString.js';
+import MultiPoint from '../../../../../../src/ol/geom/MultiPoint.js';
+import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.js';
+import Point from '../../../../../../src/ol/geom/Point.js';
+import Polygon from '../../../../../../src/ol/geom/Polygon.js';
+import {getUid} from '../../../../../../src/ol/index.js';
+
+describe('MixedGeometryBatch', function () {
+ let mixedBatch;
+
+ beforeEach(() => {
+ mixedBatch = new MixedGeometryBatch();
+ });
+
+ describe('#addFeatures', () => {
+ let features, spy;
+ beforeEach(() => {
+ features = [new Feature(), new Feature(), new Feature()];
+ spy = sinon.spy(mixedBatch, 'addFeature');
+ mixedBatch.addFeatures(features);
+ });
+ it('calls addFeature for each feature', () => {
+ expect(spy.callCount).to.be(3);
+ expect(spy.args[0][0]).to.be(features[0]);
+ expect(spy.args[1][0]).to.be(features[1]);
+ expect(spy.args[2][0]).to.be(features[2]);
+ });
+ });
+
+ describe('features with Point geometries', () => {
+ let geometry1, feature1, geometry2, feature2;
+
+ beforeEach(() => {
+ geometry1 = new Point([0, 1]);
+ feature1 = new Feature({
+ geometry: geometry1,
+ prop1: 'abcd',
+ prop2: 'efgh',
+ });
+ geometry2 = new Point([2, 3]);
+ feature2 = new Feature({
+ geometry: geometry2,
+ prop3: '1234',
+ prop4: '5678',
+ });
+ });
+
+ describe('#addFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ it('puts the geometries in the point batch', () => {
+ const keys = Object.keys(mixedBatch.pointBatch.entries);
+ const uid1 = getUid(feature1);
+ const uid2 = getUid(feature2);
+ expect(keys).to.eql([uid1, uid2]);
+ expect(mixedBatch.pointBatch.entries[uid1]).to.eql({
+ feature: feature1,
+ flatCoordss: [[0, 1]],
+ });
+ expect(mixedBatch.pointBatch.entries[uid2]).to.eql({
+ feature: feature2,
+ flatCoordss: [[2, 3]],
+ });
+ });
+ it('computes the geometries count', () => {
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
+ });
+ it('leaves other batches untouched', () => {
+ expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
+ expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length(
+ 0
+ );
+ });
+ });
+
+ describe('#changeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ describe('modifying geometry and props', () => {
+ beforeEach(() => {
+ feature1.set('prop1', 'changed');
+ geometry1.setCoordinates([100, 101]);
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the modified properties and geometry in the point batch', () => {
+ const entry = mixedBatch.pointBatch.entries[getUid(feature1)];
+ expect(entry.feature.get('prop1')).to.eql('changed');
+ });
+ it('keeps geometry count the same', () => {
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
+ });
+ });
+ describe('changing the geometry', () => {
+ let newGeom;
+ beforeEach(() => {
+ newGeom = new Point([40, 41]);
+ feature1.setGeometry(newGeom);
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the geometry in the point batch', () => {
+ const entry = mixedBatch.pointBatch.entries[getUid(feature1)];
+ expect(entry.flatCoordss).to.eql([[40, 41]]);
+ });
+ it('keeps geometry count the same', () => {
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
+ });
+ });
+ });
+
+ describe('#removeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ mixedBatch.removeFeature(feature1);
+ });
+ it('clears the entry related to this feature', () => {
+ const keys = Object.keys(mixedBatch.pointBatch.entries);
+ expect(keys).to.not.contain(getUid(feature1));
+ });
+ it('recompute geometry count', () => {
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(1);
+ });
+ });
+ });
+
+ describe('features with LineString geometries', () => {
+ let geometry1, feature1, geometry2, feature2;
+
+ beforeEach(() => {
+ geometry1 = new LineString([
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7],
+ ]);
+ feature1 = new Feature({
+ geometry: geometry1,
+ prop1: 'abcd',
+ prop2: 'efgh',
+ });
+ geometry2 = new LineString([
+ [8, 9],
+ [10, 11],
+ [12, 13],
+ ]);
+ feature2 = new Feature({
+ geometry: geometry2,
+ prop3: '1234',
+ prop4: '5678',
+ });
+ });
+
+ describe('#addFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ it('puts the geometries in the linestring batch', () => {
+ const keys = Object.keys(mixedBatch.lineStringBatch.entries);
+ const uid1 = getUid(feature1);
+ const uid2 = getUid(feature2);
+ expect(keys).to.eql([uid1, uid2]);
+ expect(mixedBatch.lineStringBatch.entries[uid1]).to.eql({
+ feature: feature1,
+ flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7]],
+ verticesCount: 4,
+ });
+ expect(mixedBatch.lineStringBatch.entries[uid2]).to.eql({
+ feature: feature2,
+ flatCoordss: [[8, 9, 10, 11, 12, 13]],
+ verticesCount: 3,
+ });
+ });
+ it('computes the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(7);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
+ });
+ it('leaves other batches untouched', () => {
+ expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
+ expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
+ });
+ });
+
+ describe('#changeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ describe('modifying geometry and props', () => {
+ beforeEach(() => {
+ feature1.set('prop1', 'changed');
+ geometry1.appendCoordinate([100, 101]);
+ geometry1.appendCoordinate([102, 103]);
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the modified properties and geometry in the linestring batch', () => {
+ const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)];
+ expect(entry.feature.get('prop1')).to.eql('changed');
+ expect(entry.verticesCount).to.eql(6);
+ expect(entry.flatCoordss).to.eql([
+ [0, 1, 2, 3, 4, 5, 6, 7, 100, 101, 102, 103],
+ ]);
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(9);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
+ });
+ });
+ describe('changing the geometry', () => {
+ let newGeom;
+ beforeEach(() => {
+ newGeom = new LineString([
+ [40, 41],
+ [42, 43],
+ ]);
+ feature1.setGeometry(newGeom);
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the geometry in the linestring batch', () => {
+ const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)];
+ expect(entry.flatCoordss).to.eql([[40, 41, 42, 43]]);
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(5);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
+ });
+ });
+ });
+
+ describe('#removeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ mixedBatch.removeFeature(feature1);
+ });
+ it('clears the entry related to this feature', () => {
+ const keys = Object.keys(mixedBatch.lineStringBatch.entries);
+ expect(keys).to.not.contain(getUid(feature1));
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(3);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1);
+ });
+ });
+ });
+
+ describe('features with Polygon geometries', () => {
+ let geometry1, feature1, geometry2, feature2;
+
+ beforeEach(() => {
+ geometry1 = new Polygon([
+ [
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7],
+ ],
+ [
+ [20, 21],
+ [22, 23],
+ [24, 25],
+ ],
+ ]);
+ feature1 = new Feature({
+ geometry: geometry1,
+ prop1: 'abcd',
+ prop2: 'efgh',
+ });
+ geometry2 = new Polygon([
+ [
+ [8, 9],
+ [10, 11],
+ [12, 13],
+ ],
+ [
+ [30, 31],
+ [32, 33],
+ [34, 35],
+ ],
+ [
+ [40, 41],
+ [42, 43],
+ [44, 45],
+ [46, 47],
+ ],
+ ]);
+ feature2 = new Feature({
+ geometry: geometry2,
+ prop3: '1234',
+ prop4: '5678',
+ });
+ });
+
+ describe('#addFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ it('puts the polygons in the polygon batch', () => {
+ const keys = Object.keys(mixedBatch.polygonBatch.entries);
+ const uid1 = getUid(feature1);
+ const uid2 = getUid(feature2);
+ expect(keys).to.eql([uid1, uid2]);
+ expect(mixedBatch.polygonBatch.entries[uid1]).to.eql({
+ feature: feature1,
+ flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25]],
+ verticesCount: 7,
+ ringsCount: 2,
+ ringsVerticesCounts: [[4, 3]],
+ });
+ expect(mixedBatch.polygonBatch.entries[uid2]).to.eql({
+ feature: feature2,
+ flatCoordss: [
+ [
+ 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44,
+ 45, 46, 47,
+ ],
+ ],
+ verticesCount: 10,
+ ringsCount: 3,
+ ringsVerticesCounts: [[3, 3, 4]],
+ });
+ });
+ it('computes the aggregated metrics on all polygons', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(17);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(5);
+ });
+ it('puts the linear rings in the linestring batch', () => {
+ const keys = Object.keys(mixedBatch.lineStringBatch.entries);
+ expect(keys).to.eql([getUid(feature1), getUid(feature2)]);
+ expect(mixedBatch.lineStringBatch.entries[getUid(feature1)]).to.eql({
+ feature: feature1,
+ flatCoordss: [
+ [0, 1, 2, 3, 4, 5, 6, 7],
+ [20, 21, 22, 23, 24, 25],
+ ],
+ verticesCount: 7,
+ });
+ expect(mixedBatch.lineStringBatch.entries[getUid(feature2)]).to.eql({
+ feature: feature2,
+ flatCoordss: [
+ [8, 9, 10, 11, 12, 13],
+ [30, 31, 32, 33, 34, 35],
+ [40, 41, 42, 43, 44, 45, 46, 47],
+ ],
+ verticesCount: 10,
+ });
+ });
+ it('computes the aggregated metrics on all linestrings', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(17);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(5);
+ });
+ it('leaves point batch untouched', () => {
+ expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
+ });
+ });
+
+ describe('#changeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ });
+ describe('modifying geometry and props', () => {
+ beforeEach(() => {
+ feature1.set('prop1', 'changed');
+ geometry1.appendLinearRing(
+ new LinearRing([
+ [201, 202],
+ [203, 204],
+ [205, 206],
+ [207, 208],
+ ])
+ );
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the modified properties and geometry in the polygon batch', () => {
+ const entry = mixedBatch.polygonBatch.entries[getUid(feature1)];
+ expect(entry.feature.get('prop1')).to.eql('changed');
+ expect(entry.verticesCount).to.eql(11);
+ expect(entry.ringsCount).to.eql(3);
+ expect(entry.ringsVerticesCounts).to.eql([[4, 3, 4]]);
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(21);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(6);
+ });
+ });
+ describe('changing the geometry', () => {
+ let newGeom;
+ beforeEach(() => {
+ newGeom = new Polygon([
+ [
+ [201, 202],
+ [203, 204],
+ [205, 206],
+ [207, 208],
+ ],
+ ]);
+ feature1.setGeometry(newGeom);
+ mixedBatch.changeFeature(feature1);
+ });
+ it('updates the geometry in the polygon batch', () => {
+ const entry = mixedBatch.polygonBatch.entries[getUid(feature1)];
+ expect(entry.feature).to.be(feature1);
+ expect(entry.verticesCount).to.eql(4);
+ expect(entry.ringsCount).to.eql(1);
+ expect(entry.ringsVerticesCounts).to.eql([[4]]);
+ expect(entry.flatCoordss).to.eql([
+ [201, 202, 203, 204, 205, 206, 207, 208],
+ ]);
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(14);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(4);
+ });
+ });
+ });
+
+ describe('#removeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ mixedBatch.removeFeature(feature1);
+ });
+ it('clears the entry related to this feature', () => {
+ const keys = Object.keys(mixedBatch.polygonBatch.entries);
+ expect(keys).to.not.contain(getUid(feature1));
+ });
+ it('updates the aggregated metrics on all geoms', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(10);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(1);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(3);
+ });
+ });
+ });
+
+ describe('feature with nested geometries (collection, multi)', () => {
+ let feature, geomCollection, multiPolygon, multiPoint, multiLine;
+
+ beforeEach(() => {
+ multiPoint = new MultiPoint([
+ [101, 102],
+ [201, 202],
+ [301, 302],
+ ]);
+ multiLine = new MultiLineString([
+ [
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7],
+ ],
+ [
+ [8, 9],
+ [10, 11],
+ [12, 13],
+ ],
+ ]);
+ multiPolygon = new MultiPolygon([
+ [
+ [
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7],
+ ],
+ [
+ [20, 21],
+ [22, 23],
+ [24, 25],
+ ],
+ ],
+ [
+ [
+ [8, 9],
+ [10, 11],
+ [12, 13],
+ ],
+ [
+ [30, 31],
+ [32, 33],
+ [34, 35],
+ ],
+ [
+ [40, 41],
+ [42, 43],
+ [44, 45],
+ [46, 47],
+ ],
+ ],
+ ]);
+ geomCollection = new GeometryCollection([
+ multiPolygon,
+ multiLine,
+ multiPoint,
+ ]);
+ feature = new Feature({
+ geometry: geomCollection,
+ prop3: '1234',
+ prop4: '5678',
+ });
+ });
+
+ describe('#addFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature);
+ });
+ it('puts the polygons in the polygon batch', () => {
+ const uid = getUid(feature);
+ expect(mixedBatch.polygonBatch.entries[uid]).to.eql({
+ feature: feature,
+ flatCoordss: [
+ [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25],
+ [
+ 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44,
+ 45, 46, 47,
+ ],
+ ],
+ verticesCount: 17,
+ ringsCount: 5,
+ ringsVerticesCounts: [
+ [4, 3],
+ [3, 3, 4],
+ ],
+ });
+ });
+ it('puts the polygon rings and linestrings in the linestring batch', () => {
+ const uid = getUid(feature);
+ expect(mixedBatch.lineStringBatch.entries[uid]).to.eql({
+ feature: feature,
+ flatCoordss: [
+ [0, 1, 2, 3, 4, 5, 6, 7],
+ [20, 21, 22, 23, 24, 25],
+ [8, 9, 10, 11, 12, 13],
+ [30, 31, 32, 33, 34, 35],
+ [40, 41, 42, 43, 44, 45, 46, 47],
+ [0, 1, 2, 3, 4, 5, 6, 7],
+ [8, 9, 10, 11, 12, 13],
+ ],
+ verticesCount: 24,
+ });
+ });
+ it('puts the points in the linestring batch', () => {
+ const uid = getUid(feature);
+ expect(mixedBatch.pointBatch.entries[uid]).to.eql({
+ feature: feature,
+ flatCoordss: [
+ [101, 102],
+ [201, 202],
+ [301, 302],
+ ],
+ });
+ });
+ it('computes the aggregated metrics on all polygons', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(17);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(5);
+ });
+ it('computes the aggregated metrics on all linestring', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(24);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(7);
+ });
+ it('computes the aggregated metrics on all points', () => {
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(3);
+ });
+ });
+
+ describe('#changeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature);
+ });
+ describe('modifying geometry', () => {
+ beforeEach(() => {
+ multiLine.appendLineString(
+ new LineString([
+ [500, 501],
+ [502, 503],
+ [504, 505],
+ [506, 507],
+ ])
+ );
+ multiPolygon.appendPolygon(
+ new Polygon([
+ [
+ [201, 202],
+ [203, 204],
+ [205, 206],
+ [207, 208],
+ [209, 210],
+ ],
+ ])
+ );
+ mixedBatch.changeFeature(feature);
+ });
+ it('updates the geometries in the polygon batch', () => {
+ const entry = mixedBatch.polygonBatch.entries[getUid(feature)];
+ expect(entry).to.eql({
+ feature: feature,
+ flatCoordss: [
+ [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25],
+ [
+ 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43,
+ 44, 45, 46, 47,
+ ],
+ [201, 202, 203, 204, 205, 206, 207, 208, 209, 210],
+ ],
+ verticesCount: 22,
+ ringsCount: 6,
+ ringsVerticesCounts: [[4, 3], [3, 3, 4], [5]],
+ });
+ });
+ it('updates the geometries in the linestring batch', () => {
+ const entry = mixedBatch.lineStringBatch.entries[getUid(feature)];
+ expect(entry).to.eql({
+ feature: feature,
+ flatCoordss: [
+ [0, 1, 2, 3, 4, 5, 6, 7],
+ [20, 21, 22, 23, 24, 25],
+ [8, 9, 10, 11, 12, 13],
+ [30, 31, 32, 33, 34, 35],
+ [40, 41, 42, 43, 44, 45, 46, 47],
+ [201, 202, 203, 204, 205, 206, 207, 208, 209, 210],
+ [0, 1, 2, 3, 4, 5, 6, 7],
+ [8, 9, 10, 11, 12, 13],
+ [500, 501, 502, 503, 504, 505, 506, 507],
+ ],
+ verticesCount: 33,
+ });
+ });
+ it('updates the aggregated metrics on the polygon batch', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(22);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(3);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(6);
+ });
+ it('updates the aggregated metrics on the linestring batch', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(33);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(9);
+ });
+ });
+ describe('changing the geometry', () => {
+ beforeEach(() => {
+ feature.setGeometry(
+ new Polygon([
+ [
+ [201, 202],
+ [203, 204],
+ [205, 206],
+ [207, 208],
+ ],
+ ])
+ );
+ mixedBatch.changeFeature(feature);
+ });
+ it('updates the geometries in the polygon batch', () => {
+ const entry = mixedBatch.polygonBatch.entries[getUid(feature)];
+ expect(entry).to.eql({
+ feature: feature,
+ flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]],
+ verticesCount: 4,
+ ringsCount: 1,
+ ringsVerticesCounts: [[4]],
+ });
+ });
+ it('updates the geometries in the linestring batch', () => {
+ const entry = mixedBatch.lineStringBatch.entries[getUid(feature)];
+ expect(entry).to.eql({
+ feature: feature,
+ flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]],
+ verticesCount: 4,
+ });
+ });
+ it('updates the aggregated metrics on the polygon batch', () => {
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(4);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(1);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(1);
+ });
+ it('updates the aggregated metrics on the linestring batch', () => {
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(4);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1);
+ });
+ it('updates the aggregated metrics on the point batch', () => {
+ const keys = Object.keys(mixedBatch.pointBatch.entries);
+ expect(keys).to.not.contain(getUid(feature));
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
+ });
+ });
+ });
+
+ describe('#removeFeature', () => {
+ beforeEach(() => {
+ mixedBatch.addFeature(feature);
+ mixedBatch.removeFeature(feature);
+ });
+ it('clears all entries in the polygon batch', () => {
+ const keys = Object.keys(mixedBatch.polygonBatch.entries);
+ expect(keys).to.have.length(0);
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(0);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(0);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(0);
+ });
+ it('clears all entries in the linestring batch', () => {
+ const keys = Object.keys(mixedBatch.lineStringBatch.entries);
+ expect(keys).to.have.length(0);
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(0);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0);
+ });
+ it('clears all entries in the point batch', () => {
+ const keys = Object.keys(mixedBatch.pointBatch.entries);
+ expect(keys).to.have.length(0);
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
+ });
+ });
+ });
+
+ describe('#clear', () => {
+ beforeEach(() => {
+ const feature1 = new Feature(
+ new Polygon([
+ [
+ [201, 202],
+ [203, 204],
+ [205, 206],
+ [207, 208],
+ ],
+ ])
+ );
+ const feature2 = new Feature(new Point([201, 202]));
+ mixedBatch.addFeature(feature1);
+ mixedBatch.addFeature(feature2);
+ mixedBatch.clear();
+ });
+
+ it('clears polygon batch', () => {
+ expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
+ expect(mixedBatch.polygonBatch.geometriesCount).to.be(0);
+ expect(mixedBatch.polygonBatch.verticesCount).to.be(0);
+ expect(mixedBatch.polygonBatch.ringsCount).to.be(0);
+ });
+
+ it('clears linestring batch', () => {
+ expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length(0);
+ expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0);
+ expect(mixedBatch.lineStringBatch.verticesCount).to.be(0);
+ });
+
+ it('clears point batch', () => {
+ expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
+ expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
+ });
+ });
+});
diff --git a/test/browser/spec/ol/render/webgl/utils.test.js b/test/browser/spec/ol/render/webgl/utils.test.js
new file mode 100644
index 0000000000..8e94ef3684
--- /dev/null
+++ b/test/browser/spec/ol/render/webgl/utils.test.js
@@ -0,0 +1,477 @@
+import {
+ colorDecodeId,
+ colorEncodeId,
+ getBlankImageData,
+ writeLineSegmentToBuffers,
+ writePointFeatureToBuffers,
+ writePolygonTrianglesToBuffers,
+} from '../../../../../../src/ol/render/webgl/utils.js';
+import {
+ compose as composeTransform,
+ create as createTransform,
+ makeInverse as makeInverseTransform,
+} from '../../../../../../src/ol/transform.js';
+
+describe('webgl render utils', function () {
+ describe('writePointFeatureToBuffers', function () {
+ let vertexBuffer, indexBuffer, instructions;
+
+ beforeEach(function () {
+ vertexBuffer = new Float32Array(100);
+ indexBuffer = new Uint32Array(100);
+ instructions = new Float32Array(100);
+
+ instructions.set([0, 0, 0, 0, 10, 11]);
+ });
+
+ it('writes correctly to the buffers (without custom attributes)', function () {
+ const stride = 3;
+ const positions = writePointFeatureToBuffers(
+ instructions,
+ 4,
+ vertexBuffer,
+ indexBuffer,
+ 0
+ );
+
+ expect(vertexBuffer[0]).to.eql(10);
+ expect(vertexBuffer[1]).to.eql(11);
+ expect(vertexBuffer[2]).to.eql(0);
+
+ expect(vertexBuffer[stride + 0]).to.eql(10);
+ expect(vertexBuffer[stride + 1]).to.eql(11);
+ expect(vertexBuffer[stride + 2]).to.eql(1);
+
+ expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
+ expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
+ expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
+
+ expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
+ expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
+ expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
+
+ expect(indexBuffer[0]).to.eql(0);
+ expect(indexBuffer[1]).to.eql(1);
+ expect(indexBuffer[2]).to.eql(3);
+ expect(indexBuffer[3]).to.eql(1);
+ expect(indexBuffer[4]).to.eql(2);
+ expect(indexBuffer[5]).to.eql(3);
+
+ expect(positions.indexPosition).to.eql(6);
+ expect(positions.vertexPosition).to.eql(stride * 4);
+ });
+
+ it('writes correctly to the buffers (with 2 custom attributes)', function () {
+ instructions.set([0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13]);
+ const stride = 5;
+ const positions = writePointFeatureToBuffers(
+ instructions,
+ 8,
+ vertexBuffer,
+ indexBuffer,
+ 2
+ );
+
+ expect(vertexBuffer[0]).to.eql(10);
+ expect(vertexBuffer[1]).to.eql(11);
+ expect(vertexBuffer[2]).to.eql(0);
+ expect(vertexBuffer[3]).to.eql(12);
+ expect(vertexBuffer[4]).to.eql(13);
+
+ expect(vertexBuffer[stride + 0]).to.eql(10);
+ expect(vertexBuffer[stride + 1]).to.eql(11);
+ expect(vertexBuffer[stride + 2]).to.eql(1);
+ expect(vertexBuffer[stride + 3]).to.eql(12);
+ expect(vertexBuffer[stride + 4]).to.eql(13);
+
+ expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
+ expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
+ expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
+ expect(vertexBuffer[stride * 2 + 3]).to.eql(12);
+ expect(vertexBuffer[stride * 2 + 4]).to.eql(13);
+
+ expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
+ expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
+ expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
+ expect(vertexBuffer[stride * 3 + 3]).to.eql(12);
+ expect(vertexBuffer[stride * 3 + 4]).to.eql(13);
+
+ expect(indexBuffer[0]).to.eql(0);
+ expect(indexBuffer[1]).to.eql(1);
+ expect(indexBuffer[2]).to.eql(3);
+ expect(indexBuffer[3]).to.eql(1);
+ expect(indexBuffer[4]).to.eql(2);
+ expect(indexBuffer[5]).to.eql(3);
+
+ expect(positions.indexPosition).to.eql(6);
+ expect(positions.vertexPosition).to.eql(stride * 4);
+ });
+
+ it('correctly chains buffer writes', function () {
+ instructions.set([10, 11, 20, 21, 30, 31]);
+ const stride = 3;
+ let positions = writePointFeatureToBuffers(
+ instructions,
+ 0,
+ vertexBuffer,
+ indexBuffer,
+ 0
+ );
+ positions = writePointFeatureToBuffers(
+ instructions,
+ 2,
+ vertexBuffer,
+ indexBuffer,
+ 0,
+ positions
+ );
+ positions = writePointFeatureToBuffers(
+ instructions,
+ 4,
+ vertexBuffer,
+ indexBuffer,
+ 0,
+ positions
+ );
+
+ expect(vertexBuffer[0]).to.eql(10);
+ expect(vertexBuffer[1]).to.eql(11);
+ expect(vertexBuffer[2]).to.eql(0);
+
+ expect(vertexBuffer[stride * 4 + 0]).to.eql(20);
+ expect(vertexBuffer[stride * 4 + 1]).to.eql(21);
+ expect(vertexBuffer[stride * 4 + 2]).to.eql(0);
+
+ expect(vertexBuffer[stride * 8 + 0]).to.eql(30);
+ expect(vertexBuffer[stride * 8 + 1]).to.eql(31);
+ expect(vertexBuffer[stride * 8 + 2]).to.eql(0);
+
+ expect(indexBuffer[6 + 0]).to.eql(4);
+ expect(indexBuffer[6 + 1]).to.eql(5);
+ expect(indexBuffer[6 + 2]).to.eql(7);
+ expect(indexBuffer[6 + 3]).to.eql(5);
+ expect(indexBuffer[6 + 4]).to.eql(6);
+ expect(indexBuffer[6 + 5]).to.eql(7);
+
+ expect(indexBuffer[6 * 2 + 0]).to.eql(8);
+ expect(indexBuffer[6 * 2 + 1]).to.eql(9);
+ expect(indexBuffer[6 * 2 + 2]).to.eql(11);
+ expect(indexBuffer[6 * 2 + 3]).to.eql(9);
+ expect(indexBuffer[6 * 2 + 4]).to.eql(10);
+ expect(indexBuffer[6 * 2 + 5]).to.eql(11);
+
+ expect(positions.indexPosition).to.eql(6 * 3);
+ expect(positions.vertexPosition).to.eql(stride * 4 * 3);
+ });
+ });
+
+ describe('writeLineSegmentToBuffers', function () {
+ let vertexArray, indexArray, instructions;
+ let instructionsTransform, invertInstructionsTransform;
+
+ beforeEach(function () {
+ vertexArray = [];
+ indexArray = [];
+
+ instructions = new Float32Array(100);
+
+ instructionsTransform = createTransform();
+ invertInstructionsTransform = createTransform();
+ composeTransform(instructionsTransform, 0, 0, 10, 20, 0, -50, 200);
+ makeInverseTransform(invertInstructionsTransform, instructionsTransform);
+ });
+
+ describe('isolated segment', function () {
+ beforeEach(function () {
+ instructions.set([0, 0, 0, 2, 5, 5, 25, 5]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 4,
+ 6,
+ null,
+ null,
+ vertexArray,
+ indexArray,
+ [],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('generates a quad for the segment', function () {
+ expect(vertexArray).to.have.length(20);
+ expect(vertexArray).to.eql([
+ 5, 5, 25, 5, 0, 5, 5, 25, 5, 100000000, 5, 5, 25, 5, 200000000, 5, 5,
+ 25, 5, 300000000,
+ ]);
+ expect(indexArray).to.have.length(6);
+ expect(indexArray).to.eql([0, 1, 2, 1, 3, 2]);
+ });
+ });
+
+ describe('isolated segment with custom attributes', function () {
+ beforeEach(function () {
+ instructions.set([888, 999, 2, 5, 5, 25, 5]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 3,
+ 5,
+ null,
+ null,
+ vertexArray,
+ indexArray,
+ [888, 999],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('adds custom attributes in the vertices buffer', function () {
+ expect(vertexArray).to.have.length(28);
+ expect(vertexArray).to.eql([
+ 5, 5, 25, 5, 0, 888, 999, 5, 5, 25, 5, 100000000, 888, 999, 5, 5, 25,
+ 5, 200000000, 888, 999, 5, 5, 25, 5, 300000000, 888, 999,
+ ]);
+ });
+ it('does not impact indices array', function () {
+ expect(indexArray).to.have.length(6);
+ });
+ });
+
+ describe('segment with a point coming before it, join angle < PI', function () {
+ beforeEach(function () {
+ instructions.set([2, 5, 5, 25, 5, 5, 20]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 1,
+ 3,
+ 5,
+ null,
+ vertexArray,
+ indexArray,
+ [],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('generate the correct amount of vertices', () => {
+ expect(vertexArray).to.have.length(20);
+ });
+ it('correctly encodes the join angle', () => {
+ expect(vertexArray[4]).to.eql(2356);
+ expect(vertexArray[9]).to.eql(100002356);
+ expect(vertexArray[14]).to.eql(200002356);
+ expect(vertexArray[19]).to.eql(300002356);
+ });
+ it('does not impact indices array', function () {
+ expect(indexArray).to.have.length(6);
+ });
+ });
+
+ describe('segment with a point coming before it, join angle > PI', function () {
+ beforeEach(function () {
+ instructions.set([2, 5, 5, 25, 5, 5, -10]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 1,
+ 3,
+ 5,
+ null,
+ vertexArray,
+ indexArray,
+ [],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('generate the correct amount of vertices', () => {
+ expect(vertexArray).to.have.length(20);
+ });
+ it('correctly encodes the join angle', () => {
+ expect(vertexArray[4]).to.eql(7069);
+ expect(vertexArray[9]).to.eql(100007069);
+ expect(vertexArray[14]).to.eql(200007069);
+ expect(vertexArray[19]).to.eql(300007069);
+ });
+ it('does not impact indices array', function () {
+ expect(indexArray).to.have.length(6);
+ });
+ });
+
+ describe('segment with a point coming after it, join angle < PI', function () {
+ beforeEach(function () {
+ instructions.set([2, 5, 5, 25, 5, 5, 20]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 1,
+ 3,
+ null,
+ 5,
+ vertexArray,
+ indexArray,
+ [],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('generate the correct amount of vertices', () => {
+ expect(vertexArray).to.have.length(20);
+ });
+ it('correctly encodes the join angle', () => {
+ expect(vertexArray[4]).to.eql(88870000);
+ expect(vertexArray[9]).to.eql(188870000);
+ expect(vertexArray[14]).to.eql(288870000);
+ expect(vertexArray[19]).to.eql(388870000);
+ });
+ it('does not impact indices array', function () {
+ expect(indexArray).to.have.length(6);
+ });
+ });
+
+ describe('segment with a point coming after it, join angle > PI', function () {
+ beforeEach(function () {
+ instructions.set([2, 5, 5, 25, 5, 25, -10]);
+ writeLineSegmentToBuffers(
+ instructions,
+ 1,
+ 3,
+ null,
+ 5,
+ vertexArray,
+ indexArray,
+ [],
+ instructionsTransform,
+ invertInstructionsTransform
+ );
+ });
+ it('generate the correct amount of vertices', () => {
+ expect(vertexArray).to.have.length(20);
+ });
+ it('correctly encodes join angles', () => {
+ expect(vertexArray[4]).to.eql(23560000);
+ expect(vertexArray[9]).to.eql(123560000);
+ expect(vertexArray[14]).to.eql(223560000);
+ expect(vertexArray[19]).to.eql(323560000);
+ });
+ it('does not impact indices array', function () {
+ expect(indexArray).to.have.length(6);
+ });
+ });
+ });
+
+ describe('writePolygonTrianglesToBuffers', function () {
+ let vertexArray, indexArray, instructions, newIndex;
+
+ beforeEach(function () {
+ vertexArray = [];
+ indexArray = [];
+ instructions = new Float32Array(100);
+ });
+
+ describe('polygon with a hole', function () {
+ beforeEach(function () {
+ instructions.set([
+ 0, 0, 0, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1,
+ 7, 3, 5, 5, 3, 3,
+ ]);
+ newIndex = writePolygonTrianglesToBuffers(
+ instructions,
+ 3,
+ vertexArray,
+ indexArray,
+ 0
+ );
+ });
+ it('generates triangles correctly', function () {
+ expect(vertexArray).to.have.length(22);
+ expect(vertexArray).to.eql([
+ 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1, 7, 3, 5, 5, 3, 3,
+ ]);
+ expect(indexArray).to.have.length(24);
+ expect(indexArray).to.eql([
+ 4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3,
+ 8,
+ ]);
+ });
+ it('correctly returns the new index', function () {
+ expect(newIndex).to.eql(28);
+ });
+ });
+
+ describe('polygon with a hole and custom attributes', function () {
+ beforeEach(function () {
+ instructions.set([
+ 0, 0, 0, 1234, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3,
+ 5, 1, 7, 3, 5, 5, 3, 3,
+ ]);
+ newIndex = writePolygonTrianglesToBuffers(
+ instructions,
+ 3,
+ vertexArray,
+ indexArray,
+ 1
+ );
+ });
+ it('generates triangles correctly', function () {
+ expect(vertexArray).to.have.length(33);
+ expect(vertexArray).to.eql([
+ 0, 0, 1234, 10, 0, 1234, 15, 6, 1234, 10, 12, 1234, 0, 12, 1234, 0, 0,
+ 1234, 3, 3, 1234, 5, 1, 1234, 7, 3, 1234, 5, 5, 1234, 3, 3, 1234,
+ ]);
+ expect(indexArray).to.have.length(24);
+ expect(indexArray).to.eql([
+ 4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3,
+ 8,
+ ]);
+ });
+ it('correctly returns the new index', function () {
+ expect(newIndex).to.eql(29);
+ });
+ });
+ });
+
+ describe('getBlankImageData', function () {
+ it('creates a 1x1 white texture', function () {
+ const texture = getBlankImageData();
+ expect(texture.height).to.eql(1);
+ expect(texture.width).to.eql(1);
+ expect(texture.data[0]).to.eql(255);
+ expect(texture.data[1]).to.eql(255);
+ expect(texture.data[2]).to.eql(255);
+ expect(texture.data[3]).to.eql(255);
+ });
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/test/browser/spec/ol/renderer/webgl/Layer.test.js b/test/browser/spec/ol/renderer/webgl/Layer.test.js
index 908c5dfd2a..60a5581990 100644
--- a/test/browser/spec/ol/renderer/webgl/Layer.test.js
+++ b/test/browser/spec/ol/renderer/webgl/Layer.test.js
@@ -6,12 +6,7 @@ import TileLayer from '../../../../../../src/ol/layer/WebGLTile.js';
import VectorLayer from '../../../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../../../src/ol/source/Vector.js';
import View from '../../../../../../src/ol/View.js';
-import WebGLLayerRenderer, {
- colorDecodeId,
- colorEncodeId,
- getBlankImageData,
- writePointFeatureToBuffers,
-} from '../../../../../../src/ol/renderer/webgl/Layer.js';
+import WebGLLayerRenderer from '../../../../../../src/ol/renderer/webgl/Layer.js';
import {getUid} from '../../../../../../src/ol/util.js';
describe('ol/renderer/webgl/Layer', function () {
@@ -36,205 +31,6 @@ describe('ol/renderer/webgl/Layer', function () {
});
});
- describe('writePointFeatureToBuffers', function () {
- let vertexBuffer, indexBuffer, instructions;
-
- beforeEach(function () {
- vertexBuffer = new Float32Array(100);
- indexBuffer = new Uint32Array(100);
- instructions = new Float32Array(100);
-
- instructions.set([0, 0, 0, 0, 10, 11]);
- });
-
- it('writes correctly to the buffers (without custom attributes)', function () {
- const stride = 3;
- const positions = writePointFeatureToBuffers(
- instructions,
- 4,
- vertexBuffer,
- indexBuffer,
- 0
- );
-
- expect(vertexBuffer[0]).to.eql(10);
- expect(vertexBuffer[1]).to.eql(11);
- expect(vertexBuffer[2]).to.eql(0);
-
- expect(vertexBuffer[stride + 0]).to.eql(10);
- expect(vertexBuffer[stride + 1]).to.eql(11);
- expect(vertexBuffer[stride + 2]).to.eql(1);
-
- expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
- expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
- expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
-
- expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
- expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
- expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
-
- expect(indexBuffer[0]).to.eql(0);
- expect(indexBuffer[1]).to.eql(1);
- expect(indexBuffer[2]).to.eql(3);
- expect(indexBuffer[3]).to.eql(1);
- expect(indexBuffer[4]).to.eql(2);
- expect(indexBuffer[5]).to.eql(3);
-
- expect(positions.indexPosition).to.eql(6);
- expect(positions.vertexPosition).to.eql(stride * 4);
- });
-
- it('writes correctly to the buffers (with 2 custom attributes)', function () {
- instructions.set([0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13]);
- const stride = 5;
- const positions = writePointFeatureToBuffers(
- instructions,
- 8,
- vertexBuffer,
- indexBuffer,
- 2
- );
-
- expect(vertexBuffer[0]).to.eql(10);
- expect(vertexBuffer[1]).to.eql(11);
- expect(vertexBuffer[2]).to.eql(0);
- expect(vertexBuffer[3]).to.eql(12);
- expect(vertexBuffer[4]).to.eql(13);
-
- expect(vertexBuffer[stride + 0]).to.eql(10);
- expect(vertexBuffer[stride + 1]).to.eql(11);
- expect(vertexBuffer[stride + 2]).to.eql(1);
- expect(vertexBuffer[stride + 3]).to.eql(12);
- expect(vertexBuffer[stride + 4]).to.eql(13);
-
- expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
- expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
- expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
- expect(vertexBuffer[stride * 2 + 3]).to.eql(12);
- expect(vertexBuffer[stride * 2 + 4]).to.eql(13);
-
- expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
- expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
- expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
- expect(vertexBuffer[stride * 3 + 3]).to.eql(12);
- expect(vertexBuffer[stride * 3 + 4]).to.eql(13);
-
- expect(indexBuffer[0]).to.eql(0);
- expect(indexBuffer[1]).to.eql(1);
- expect(indexBuffer[2]).to.eql(3);
- expect(indexBuffer[3]).to.eql(1);
- expect(indexBuffer[4]).to.eql(2);
- expect(indexBuffer[5]).to.eql(3);
-
- expect(positions.indexPosition).to.eql(6);
- expect(positions.vertexPosition).to.eql(stride * 4);
- });
-
- it('correctly chains buffer writes', function () {
- instructions.set([10, 11, 20, 21, 30, 31]);
- const stride = 3;
- let positions = writePointFeatureToBuffers(
- instructions,
- 0,
- vertexBuffer,
- indexBuffer,
- 0
- );
- positions = writePointFeatureToBuffers(
- instructions,
- 2,
- vertexBuffer,
- indexBuffer,
- 0,
- positions
- );
- positions = writePointFeatureToBuffers(
- instructions,
- 4,
- vertexBuffer,
- indexBuffer,
- 0,
- positions
- );
-
- expect(vertexBuffer[0]).to.eql(10);
- expect(vertexBuffer[1]).to.eql(11);
- expect(vertexBuffer[2]).to.eql(0);
-
- expect(vertexBuffer[stride * 4 + 0]).to.eql(20);
- expect(vertexBuffer[stride * 4 + 1]).to.eql(21);
- expect(vertexBuffer[stride * 4 + 2]).to.eql(0);
-
- expect(vertexBuffer[stride * 8 + 0]).to.eql(30);
- expect(vertexBuffer[stride * 8 + 1]).to.eql(31);
- expect(vertexBuffer[stride * 8 + 2]).to.eql(0);
-
- expect(indexBuffer[6 + 0]).to.eql(4);
- expect(indexBuffer[6 + 1]).to.eql(5);
- expect(indexBuffer[6 + 2]).to.eql(7);
- expect(indexBuffer[6 + 3]).to.eql(5);
- expect(indexBuffer[6 + 4]).to.eql(6);
- expect(indexBuffer[6 + 5]).to.eql(7);
-
- expect(indexBuffer[6 * 2 + 0]).to.eql(8);
- expect(indexBuffer[6 * 2 + 1]).to.eql(9);
- expect(indexBuffer[6 * 2 + 2]).to.eql(11);
- expect(indexBuffer[6 * 2 + 3]).to.eql(9);
- expect(indexBuffer[6 * 2 + 4]).to.eql(10);
- expect(indexBuffer[6 * 2 + 5]).to.eql(11);
-
- expect(positions.indexPosition).to.eql(6 * 3);
- expect(positions.vertexPosition).to.eql(stride * 4 * 3);
- });
- });
-
- describe('getBlankImageData', function () {
- it('creates a 1x1 white texture', function () {
- const texture = getBlankImageData();
- expect(texture.height).to.eql(1);
- expect(texture.width).to.eql(1);
- expect(texture.data[0]).to.eql(255);
- expect(texture.data[1]).to.eql(255);
- expect(texture.data[2]).to.eql(255);
- expect(texture.data[3]).to.eql(255);
- });
- });
-
- 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);
- });
- });
-
describe('context sharing', () => {
let target;
beforeEach(() => {
diff --git a/test/browser/spec/ol/renderer/webgl/PointsLayer.test.js b/test/browser/spec/ol/renderer/webgl/PointsLayer.test.js
index 62fa1939ac..bd89302aa6 100644
--- a/test/browser/spec/ol/renderer/webgl/PointsLayer.test.js
+++ b/test/browser/spec/ol/renderer/webgl/PointsLayer.test.js
@@ -8,7 +8,7 @@ import View from '../../../../../../src/ol/View.js';
import ViewHint from '../../../../../../src/ol/ViewHint.js';
import WebGLPointsLayer from '../../../../../../src/ol/layer/WebGLPoints.js';
import WebGLPointsLayerRenderer from '../../../../../../src/ol/renderer/webgl/PointsLayer.js';
-import {WebGLWorkerMessageType} from '../../../../../../src/ol/renderer/webgl/Layer.js';
+import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js';
import {
compose as composeTransform,
create as createTransform,
@@ -156,7 +156,7 @@ describe('ol/renderer/webgl/PointsLayer', function () {
const attributePerVertex = 3;
renderer.worker_.addEventListener('message', function (event) {
- if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
+ if (event.data.type !== WebGLWorkerMessageType.GENERATE_POINT_BUFFERS) {
return;
}
expect(renderer.verticesBuffer_.getArray().length).to.eql(
@@ -192,7 +192,7 @@ describe('ol/renderer/webgl/PointsLayer', function () {
const attributePerVertex = 8;
renderer.worker_.addEventListener('message', function (event) {
- if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
+ if (event.data.type !== WebGLWorkerMessageType.GENERATE_POINT_BUFFERS) {
return;
}
if (!renderer.hitVerticesBuffer_.getArray()) {
@@ -231,7 +231,7 @@ describe('ol/renderer/webgl/PointsLayer', function () {
renderer.prepareFrame(frameState);
renderer.worker_.addEventListener('message', function (event) {
- if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
+ if (event.data.type !== WebGLWorkerMessageType.GENERATE_POINT_BUFFERS) {
return;
}
const attributePerVertex = 3;
@@ -627,14 +627,14 @@ describe('ol/renderer/webgl/PointsLayer', function () {
beforeEach(function () {
source = new VectorSource({
features: new GeoJSON().readFeatures({
- 'type': 'FeatureCollection',
- 'features': [
+ type: 'FeatureCollection',
+ features: [
{
- 'type': 'Feature',
- 'properties': {},
- 'geometry': {
- 'type': 'Point',
- 'coordinates': [13, 52],
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ type: 'Point',
+ coordinates: [13, 52],
},
},
],
diff --git a/test/browser/spec/ol/webgl/helper.test.js b/test/browser/spec/ol/webgl/helper.test.js
index 1b4a194eba..f44527a8e8 100644
--- a/test/browser/spec/ol/webgl/helper.test.js
+++ b/test/browser/spec/ol/webgl/helper.test.js
@@ -57,6 +57,16 @@ const INVALID_FRAGMENT_SHADER = `
gl_FragColor = vec4(oops, 1.0, 1.0, 1.0);
}`;
+const SAMPLE_FRAMESTATE = {
+ size: [100, 150],
+ viewState: {
+ rotation: 0.4,
+ resolution: 2,
+ center: [10, 20],
+ zoom: 3,
+ },
+};
+
describe('ol/webgl/WebGLHelper', function () {
let h;
afterEach(function () {
@@ -117,7 +127,10 @@ describe('ol/webgl/WebGLHelper', function () {
u_test4: createTransform(),
},
});
- h.useProgram(h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER));
+ h.useProgram(
+ h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER),
+ SAMPLE_FRAMESTATE
+ );
h.prepareDraw({
pixelRatio: 2,
size: [50, 80],
@@ -142,6 +155,16 @@ describe('ol/webgl/WebGLHelper', function () {
h.uniformLocations_[DefaultUniform.OFFSET_SCALE_MATRIX]
).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.TIME]).not.to.eql(undefined);
+ expect(h.uniformLocations_[DefaultUniform.ZOOM]).not.to.eql(undefined);
+ expect(h.uniformLocations_[DefaultUniform.RESOLUTION]).not.to.eql(
+ undefined
+ );
+ expect(h.uniformLocations_[DefaultUniform.SIZE_PX]).not.to.eql(
+ undefined
+ );
+ expect(h.uniformLocations_[DefaultUniform.PIXEL_RATIO]).not.to.eql(
+ undefined
+ );
});
it('has processed uniforms', function () {
@@ -164,7 +187,7 @@ describe('ol/webgl/WebGLHelper', function () {
h = new WebGLHelper();
p = h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER);
- h.useProgram(p);
+ h.useProgram(p, SAMPLE_FRAMESTATE);
});
it('has saved the program', function () {
@@ -209,34 +232,30 @@ describe('ol/webgl/WebGLHelper', function () {
});
describe('#makeProjectionTransform', function () {
- let frameState;
beforeEach(function () {
h = new WebGLHelper();
-
- frameState = {
- size: [100, 150],
- viewState: {
- rotation: 0.4,
- resolution: 2,
- center: [10, 20],
- },
- };
});
it('gives out the correct transform', function () {
- const scaleX = 2 / frameState.size[0] / frameState.viewState.resolution;
- const scaleY = 2 / frameState.size[1] / frameState.viewState.resolution;
+ const scaleX =
+ 2 /
+ SAMPLE_FRAMESTATE.size[0] /
+ SAMPLE_FRAMESTATE.viewState.resolution;
+ const scaleY =
+ 2 /
+ SAMPLE_FRAMESTATE.size[1] /
+ SAMPLE_FRAMESTATE.viewState.resolution;
const given = createTransform();
const expected = createTransform();
scaleTransform(expected, scaleX, scaleY);
- rotateTransform(expected, -frameState.viewState.rotation);
+ rotateTransform(expected, -SAMPLE_FRAMESTATE.viewState.rotation);
translateTransform(
expected,
- -frameState.viewState.center[0],
- -frameState.viewState.center[1]
+ -SAMPLE_FRAMESTATE.viewState.center[0],
+ -SAMPLE_FRAMESTATE.viewState.center[1]
);
- h.makeProjectionTransform(frameState, given);
+ h.makeProjectionTransform(SAMPLE_FRAMESTATE, given);
expect(given.map((val) => val.toFixed(15))).to.eql(
expected.map((val) => val.toFixed(15))
@@ -377,7 +396,8 @@ describe('ol/webgl/WebGLHelper', function () {
void main(void) {
gl_Position = vec4(u_test, attr3, 0.0, 1.0);
}`
- )
+ ),
+ SAMPLE_FRAMESTATE
);
});
@@ -404,4 +424,50 @@ describe('ol/webgl/WebGLHelper', function () {
expect(spy.getCall(2).args[4]).to.eql(5 * bytesPerFloat);
});
});
+
+ describe('#applyFrameState', function () {
+ let stubMatrix, stubFloat, stubVec2, stubTime;
+ beforeEach(function () {
+ stubTime = sinon.stub(Date, 'now');
+ stubTime.returns(1000);
+ h = new WebGLHelper();
+ stubMatrix = sinon.stub(h, 'setUniformMatrixValue');
+ stubFloat = sinon.stub(h, 'setUniformFloatValue');
+ stubVec2 = sinon.stub(h, 'setUniformFloatVec2');
+
+ stubTime.returns(2000);
+ h.applyFrameState({...SAMPLE_FRAMESTATE, pixelRatio: 2});
+ });
+
+ afterEach(function () {
+ stubTime.restore();
+ });
+
+ it('sets the default uniforms according the frame state', function () {
+ expect(stubMatrix.getCall(0).args).to.eql([
+ DefaultUniform.OFFSET_SCALE_MATRIX,
+ [
+ 0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
+ 0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
+ ],
+ ]);
+ expect(stubMatrix.getCall(1).args).to.eql([
+ DefaultUniform.OFFSET_ROTATION_MATRIX,
+ [
+ 0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
+ 0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
+ ],
+ ]);
+
+ expect(stubFloat.getCall(0).args).to.eql([DefaultUniform.TIME, 1]);
+ expect(stubFloat.getCall(1).args).to.eql([DefaultUniform.ZOOM, 3]);
+ expect(stubFloat.getCall(2).args).to.eql([DefaultUniform.RESOLUTION, 2]);
+ expect(stubFloat.getCall(3).args).to.eql([DefaultUniform.PIXEL_RATIO, 2]);
+
+ expect(stubVec2.getCall(0).args).to.eql([
+ DefaultUniform.SIZE_PX,
+ [100, 150],
+ ]);
+ });
+ });
});
diff --git a/test/browser/spec/ol/worker/webgl.test.js b/test/browser/spec/ol/worker/webgl.test.js
index 9ddabb8643..cc0276f343 100644
--- a/test/browser/spec/ol/worker/webgl.test.js
+++ b/test/browser/spec/ol/worker/webgl.test.js
@@ -1,10 +1,14 @@
-import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
+import {WebGLWorkerMessageType} from '../../../../../src/ol/render/webgl/constants.js';
import {create} from '../../../../../src/ol/worker/webgl.js';
+import {create as createTransform} from '../../../../../src/ol/transform.js';
describe('ol/worker/webgl', function () {
let worker;
beforeEach(function () {
worker = create();
+ worker.addEventListener('error', function (error) {
+ expect().fail(error.message);
+ });
});
afterEach(function () {
@@ -15,35 +19,145 @@ describe('ol/worker/webgl', function () {
});
describe('messaging', function () {
- describe('GENERATE_BUFFERS', function () {
- it('responds with buffer data', function (done) {
- worker.addEventListener('error', function (error) {
- expect().fail(error.message);
- });
-
- worker.addEventListener('message', function (event) {
- expect(event.data.type).to.eql(
- WebGLWorkerMessageType.GENERATE_BUFFERS
- );
- expect(event.data.renderInstructions.byteLength).to.greaterThan(0);
- expect(event.data.indexBuffer.byteLength).to.greaterThan(0);
- expect(event.data.vertexBuffer.byteLength).to.greaterThan(0);
- expect(event.data.testInt).to.be(101);
- expect(event.data.testString).to.be('abcd');
- done();
- });
-
- const instructions = new Float32Array(10);
-
+ describe('GENERATE_POINT_BUFFERS', function () {
+ let responseData;
+ beforeEach(function (done) {
+ const renderInstructions = Float32Array.from([0, 10, 111, 20, 30, 222]);
+ const id = Math.floor(Math.random() * 10000);
const message = {
- type: WebGLWorkerMessageType.GENERATE_BUFFERS,
- renderInstructions: instructions,
- customAttributesCount: 0,
+ type: WebGLWorkerMessageType.GENERATE_POINT_BUFFERS,
+ renderInstructions,
+ customAttributesCount: 1,
testInt: 101,
testString: 'abcd',
+ id,
};
-
+ responseData = null;
worker.postMessage(message);
+
+ worker.addEventListener('message', function (event) {
+ if (event.data.id === id) {
+ responseData = event.data;
+ done();
+ }
+ });
+ });
+ it('responds with info passed in the message', function () {
+ expect(responseData.type).to.eql(
+ WebGLWorkerMessageType.GENERATE_POINT_BUFFERS
+ );
+ expect(responseData.renderInstructions.byteLength).to.greaterThan(0);
+ expect(responseData.testInt).to.be(101);
+ expect(responseData.testString).to.be('abcd');
+ });
+ it('responds with buffer data', function () {
+ const indices = Array.from(new Uint32Array(responseData.indexBuffer));
+ const vertices = Array.from(
+ new Float32Array(responseData.vertexBuffer)
+ );
+ expect(indices).to.eql([0, 1, 3, 1, 2, 3, 4, 5, 7, 5, 6, 7]);
+ expect(vertices).to.eql([
+ 0, 10, 0, 111, 0, 10, 1, 111, 0, 10, 2, 111, 0, 10, 3, 111, 20, 30, 0,
+ 222, 20, 30, 1, 222, 20, 30, 2, 222, 20, 30, 3, 222,
+ ]);
+ });
+ });
+
+ describe('GENERATE_LINE_STRING_BUFFERS', function () {
+ let responseData;
+ beforeEach(function (done) {
+ const renderInstructions = Float32Array.from([
+ 111, 4, 20, 30, 40, 50, 6, 7, 80, 90,
+ ]);
+ const id = Math.floor(Math.random() * 10000);
+ const renderInstructionsTransform = createTransform();
+ const message = {
+ type: WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS,
+ renderInstructions,
+ customAttributesCount: 1,
+ testInt: 101,
+ testString: 'abcd',
+ id,
+ renderInstructionsTransform,
+ };
+ responseData = null;
+ worker.postMessage(message);
+
+ worker.addEventListener('message', function (event) {
+ if (event.data.id === id) {
+ responseData = event.data;
+ done();
+ }
+ });
+ });
+ it('responds with info passed in the message', function () {
+ expect(responseData.type).to.eql(
+ WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS
+ );
+ expect(responseData.renderInstructions.byteLength).to.greaterThan(0);
+ expect(responseData.testInt).to.be(101);
+ expect(responseData.testString).to.be('abcd');
+ });
+ it('responds with buffer data', function () {
+ const indices = Array.from(new Uint32Array(responseData.indexBuffer));
+ const vertices = Array.from(
+ new Float32Array(responseData.vertexBuffer)
+ );
+ expect(indices).to.eql([
+ 0, 1, 2, 1, 3, 2, 4, 5, 6, 5, 7, 6, 8, 9, 10, 9, 11, 10,
+ ]);
+ expect(vertices).to.eql([
+ 20, 30, 40, 50, 1750000, 111, 20, 30, 40, 50, 101750000, 111, 20, 30,
+ 40, 50, 201750000, 111, 20, 30, 40, 50, 301750016, 111, 40, 50, 6, 7,
+ 93369248, 111, 40, 50, 6, 7, 193369248, 111, 40, 50, 6, 7, 293369248,
+ 111, 40, 50, 6, 7, 393369248, 111, 6, 7, 80, 90, 89, 111, 6, 7, 80,
+ 90, 100000088, 111, 6, 7, 80, 90, 200000096, 111, 6, 7, 80, 90,
+ 300000096, 111,
+ ]);
+ });
+ });
+
+ describe('GENERATE_POLYGON_BUFFERS', function () {
+ let responseData;
+ beforeEach(function (done) {
+ const renderInstructions = Float32Array.from([
+ 1234, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1, 7,
+ 3, 5, 5, 3, 3,
+ ]);
+ const id = Math.floor(Math.random() * 10000);
+ const message = {
+ type: WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS,
+ renderInstructions,
+ customAttributesCount: 1,
+ testInt: 101,
+ testString: 'abcd',
+ id,
+ };
+ responseData = null;
+ worker.postMessage(message);
+
+ worker.addEventListener('message', function (event) {
+ if (event.data.id === id) {
+ responseData = event.data;
+ done();
+ }
+ });
+ });
+ it('responds with info passed in the message', function () {
+ expect(responseData.type).to.eql(
+ WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS
+ );
+ expect(responseData.renderInstructions.byteLength).to.greaterThan(0);
+ expect(responseData.testInt).to.be(101);
+ expect(responseData.testString).to.be('abcd');
+ });
+ it('responds with buffer data', function () {
+ const indices = Array.from(new Uint32Array(responseData.indexBuffer));
+ const vertices = Array.from(
+ new Float32Array(responseData.vertexBuffer)
+ );
+ expect(indices).to.have.length(24);
+ expect(vertices).to.have.length(33);
});
});
});
diff --git a/test/rendering/cases/webgl-vector/expected.png b/test/rendering/cases/webgl-vector/expected.png
new file mode 100644
index 0000000000..ccfa1d381d
Binary files /dev/null and b/test/rendering/cases/webgl-vector/expected.png differ
diff --git a/test/rendering/cases/webgl-vector/main.js b/test/rendering/cases/webgl-vector/main.js
new file mode 100644
index 0000000000..a1d74c3246
--- /dev/null
+++ b/test/rendering/cases/webgl-vector/main.js
@@ -0,0 +1,44 @@
+import GeoJSON from '../../../../src/ol/format/GeoJSON.js';
+import Layer from '../../../../src/ol/layer/Layer.js';
+import Map from '../../../../src/ol/Map.js';
+import TileLayer from '../../../../src/ol/layer/Tile.js';
+import VectorSource from '../../../../src/ol/source/Vector.js';
+import View from '../../../../src/ol/View.js';
+import WebGLVectorLayerRenderer from '../../../../src/ol/renderer/webgl/VectorLayer.js';
+import XYZ from '../../../../src/ol/source/XYZ.js';
+
+class WebGLLayer extends Layer {
+ createRenderer() {
+ return new WebGLVectorLayerRenderer(this, {
+ className: this.getClassName(),
+ });
+ }
+}
+
+const vector = new WebGLLayer({
+ source: new VectorSource({
+ url: '/data/countries.json',
+ format: new GeoJSON(),
+ }),
+});
+
+const raster = new TileLayer({
+ source: new XYZ({
+ url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
+ transition: 0,
+ }),
+});
+
+new Map({
+ layers: [raster, vector],
+ target: 'map',
+ view: new View({
+ center: [0, 0],
+ zoom: 1,
+ }),
+});
+
+render({
+ message:
+ 'Countries are rendered as grey polygons using webgl and default shaders',
+});