From b5fbed5437fec665ec8e01cd44a72f88a231cf07 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 9 Mar 2022 22:57:09 +0100 Subject: [PATCH 01/27] Introduce MixedGeometryBatch for webgl rendering This class keeps an up-to-date list of line, polygon and point geometries to render as well as vertices counts, geometries count, rings count etc. --- src/ol/render/webgl/MixedGeometryBatch.js | 336 ++++++++ .../render/webgl/mixedgeometrybatch.test.js | 778 ++++++++++++++++++ 2 files changed, 1114 insertions(+) create mode 100644 src/ol/render/webgl/MixedGeometryBatch.js create mode 100644 test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js new file mode 100644 index 0000000000..c0270657c1 --- /dev/null +++ b/src/ol/render/webgl/MixedGeometryBatch.js @@ -0,0 +1,336 @@ +/** + * @module ol/render/webgl/MixedGeometryBatch + */ +import {getUid} from '../../util.js'; +import GeometryType from '../../geom/GeometryType.js'; +import WebGLArrayBuffer from '../../webgl/Buffer.js'; +import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js'; +import {create as createTransform} from '../../transform.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 {Object} properties Feature properties + * @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 {Object} AbstractGeometryBatch + * @abstract + * @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 + * @property {WebGLArrayBuffer} indicesBuffer + * @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 + */ + +/** + * @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons + * @extends {AbstractGeometryBatch} + * @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 + * @extends {AbstractGeometryBatch} + * @property {number} verticesCount Amount of vertices from geometries in the batch. + */ + +/** + * @typedef {Object} PointGeometryBatch A geometry batch specific to points + * @extends {AbstractGeometryBatch} + */ + +/** + * @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 {import("../../Feature").default[]} features + */ + addFeatures(features) { + for (let i = 0; i < features.length; i++) { + this.addFeature(features[i]); + } + } + + /** + * @param {import("../../Feature").default} feature + */ + addFeature(feature) { + const geometry = feature.getGeometry(); + if (!geometry) { + return; + } + this.addGeometry_(geometry, feature); + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInPointBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.pointBatch.entries)) { + this.pointBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + }; + } + return this.pointBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInLineStringBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.lineStringBatch.entries)) { + this.lineStringBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + verticesCount: 0, + }; + } + return this.lineStringBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInPolygonBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.polygonBatch.entries)) { + this.polygonBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + verticesCount: 0, + ringsCount: 0, + ringsVerticesCounts: [], + }; + } + return this.polygonBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} 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 + * @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 + * @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 + * @param {import("../../Feature").default} feature + * @private + */ + addGeometry_(geometry, feature) { + const type = geometry.getType(); + let flatCoords; + let verticesCount; + let batchEntry; + switch (type) { + case GeometryType.GEOMETRY_COLLECTION: + geometry + .getGeometries() + .map((geom) => this.addGeometry_(geom, feature)); + break; + case GeometryType.MULTI_POLYGON: + geometry + .getPolygons() + .map((polygon) => this.addGeometry_(polygon, feature)); + break; + case GeometryType.MULTI_LINE_STRING: + geometry + .getLineStrings() + .map((line) => this.addGeometry_(line, feature)); + break; + case GeometryType.MULTI_POINT: + geometry.getPoints().map((point) => this.addGeometry_(point, feature)); + break; + case GeometryType.POLYGON: + batchEntry = this.addFeatureEntryInPolygonBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + verticesCount = flatCoords.length / 2; + const ringsCount = geometry.getLinearRingCount(); + const ringsVerticesCount = geometry + .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; + geometry + .getLinearRings() + .map((ring) => this.addGeometry_(ring, feature)); + break; + case GeometryType.POINT: + batchEntry = this.addFeatureEntryInPointBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + this.pointBatch.geometriesCount++; + batchEntry.flatCoordss.push(flatCoords); + break; + case GeometryType.LINE_STRING: + case GeometryType.LINEAR_RING: + batchEntry = this.addFeatureEntryInLineStringBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + verticesCount = flatCoords.length / 2; + this.lineStringBatch.verticesCount += verticesCount; + this.lineStringBatch.geometriesCount++; + batchEntry.flatCoordss.push(flatCoords); + batchEntry.verticesCount += verticesCount; + break; + } + } + + /** + * @param {import("../../Feature").default} 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 + */ + 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/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..cd28c50e3d --- /dev/null +++ b/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js @@ -0,0 +1,778 @@ +import Feature from '../../../../../../src/ol/Feature.js'; +import Point from '../../../../../../src/ol/geom/Point.js'; +import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; +import LineString from '../../../../../../src/ol/geom/LineString.js'; +import {getUid} from '../../../../../../src/ol/index.js'; +import Polygon from '../../../../../../src/ol/geom/Polygon.js'; +import LinearRing from '../../../../../../src/ol/geom/LinearRing.js'; +import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.js'; +import GeometryCollection from '../../../../../../src/ol/geom/GeometryCollection.js'; +import MultiLineString from '../../../../../../src/ol/geom/MultiLineString.js'; +import MultiPoint from '../../../../../../src/ol/geom/MultiPoint.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, + properties: feature1.getProperties(), + flatCoordss: [[0, 1]], + }); + expect(mixedBatch.pointBatch.entries[uid2]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + 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.properties.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, + properties: feature1.getProperties(), + flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7]], + verticesCount: 4, + }); + expect(mixedBatch.lineStringBatch.entries[uid2]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + 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.properties.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, + properties: feature1.getProperties(), + 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, + properties: feature2.getProperties(), + 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, + properties: feature1.getProperties(), + 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, + properties: feature2.getProperties(), + 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.properties.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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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, + properties: feature.getProperties(), + 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); + }); + }); +}); From eb0db9e3dfe7eac89eb84be2495104a752453a85 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 9 Mar 2022 22:59:31 +0100 Subject: [PATCH 02/27] Introduce batch renderers for each type of geometry Batch renderers are responsible for generating render instructions and interacting with the worker to obtain the final webgl buffers --- src/ol/render/webgl/BatchRenderer.js | 197 +++++++++++++ .../render/webgl/LineStringBatchRenderer.js | 108 +++++++ src/ol/render/webgl/PointBatchRenderer.js | 90 ++++++ src/ol/render/webgl/PolygonBatchRenderer.js | 111 +++++++ .../ol/render/webgl/batchrenderer.test.js | 276 ++++++++++++++++++ 5 files changed, 782 insertions(+) create mode 100644 src/ol/render/webgl/BatchRenderer.js create mode 100644 src/ol/render/webgl/LineStringBatchRenderer.js create mode 100644 src/ol/render/webgl/PointBatchRenderer.js create mode 100644 src/ol/render/webgl/PolygonBatchRenderer.js create mode 100644 test/browser/spec/ol/render/webgl/batchrenderer.test.js diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js new file mode 100644 index 0000000000..406ac8c7f2 --- /dev/null +++ b/src/ol/render/webgl/BatchRenderer.js @@ -0,0 +1,197 @@ +/** + * @module ol/render/webgl/BatchRenderer + */ +import GeometryType from '../../geom/GeometryType.js'; +import { + create as createTransform, + makeInverse as makeInverseTransform, + multiply as multiplyTransform, +} from '../../transform.js'; +import {abstract} from '../../util.js'; +import {WebGLWorkerMessageType} from './constants.js'; + +/** + * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different + * for each feature. + * @property {string} name Attribute name. + * @property {function(import("../../Feature").default, Object):number} callback This callback computes the numerical value of the + * attribute for a given feature (properties are available as 2nd arg for quicker access). + */ + +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 + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { + /** + * @type {import("../../webgl/Helper.js").default} + * @protected + */ + this.helper_ = helper; + + /** + * @type {Worker} + * @protected + */ + this.worker_ = worker; + + /** + * @type {WebGLProgram} + * @protected + */ + 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").AbstractGeometryBatch} batch + * @param {import("../../PluggableMap").FrameState} frameState Frame state. + * @param {import("../../geom/GeometryType.js").default} geometryType + */ + rebuild(batch, frameState, geometryType) { + // store transform for rendering instructions + batch.renderInstructionsTransform = this.helper_.makeProjectionTransform( + frameState, + createTransform() + ); + this.generateRenderInstructions_(batch); + this.generateBuffers_(batch, geometryType); + } + + /** + * 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").AbstractGeometryBatch} batch + * @param {import("../../transform.js").Transform} currentTransform + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + */ + render(batch, currentTransform, frameState) { + // multiply the current projection transform with the invert of the one used to fill buffers + this.helper_.makeProjectionTransform(frameState, currentTransform); + 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").default} 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").AbstractGeometryBatch} batch + * @param {import("../../geom/GeometryType.js").default} geometryType + * @protected + */ + generateBuffers_(batch, geometryType) { + const messageId = workerMessageCounter++; + + let messageType; + switch (geometryType) { + case GeometryType.POLYGON: + messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS; + break; + case GeometryType.POINT: + messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS; + break; + case GeometryType.LINE_STRING: + messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS; + break; + } + + /** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */ + const message = { + id: messageId, + type: messageType, + renderInstructions: batch.renderInstructions.buffer, + renderInstructionsTransform: batch.renderInstructionsTransform, + customAttributesCount: this.customAttributes_.length, + }; + // additional properties will be sent back as-is by the worker + message['projectionTransform'] = batch.renderInstructionsTransform; + 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.projectionTransform; + 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 + ); + + // TODO: call layer.changed somehow for the layer to rerender!!!1 + }.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..31ef0e4e6a --- /dev/null +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -0,0 +1,108 @@ +/** + * @module ol/render/webgl/LineStringBatchRenderer + */ +import {AttributeType} from '../../webgl/Helper.js'; +import {transform2D} from '../../geom/flat/transform.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class LineStringBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + 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: 'a_segmentStart', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_segmentEnd', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_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").PointGeometryBatch} 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, + batchEntry.properties + ); + 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/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js new file mode 100644 index 0000000000..cc7ddb3318 --- /dev/null +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -0,0 +1,90 @@ +/** + * @module ol/render/webgl/PointBatchRenderer + */ + +import {apply as applyTransform} from '../../transform.js'; +import {AttributeType} from '../../webgl/Helper.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class PointBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + 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: 'a_position', + size: 2, + type: AttributeType.FLOAT, + }, + { + name: 'a_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 + * @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, + batchEntry.properties + ); + 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..b99f22659d --- /dev/null +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -0,0 +1,111 @@ +/** + * @module ol/render/webgl/PolygonBatchRenderer + */ +import {AttributeType} from '../../webgl/Helper.js'; +import {transform2D} from '../../geom/flat/transform.js'; +import AbstractBatchRenderer from './BatchRenderer.js'; + +class PolygonBatchRenderer extends AbstractBatchRenderer { + /** + * @param {import("../../webgl/Helper.js").default} helper + * @param {Worker} worker + * @param {string} vertexShader + * @param {string} fragmentShader + * @param {Array} customAttributes + */ + 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: 'a_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 + * @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, + batchEntry.properties + ); + 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/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..ef0c4e37cf --- /dev/null +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -0,0 +1,276 @@ +import PointBatchRenderer from '../../../../../../src/ol/render/webgl/PointBatchRenderer.js'; +import WebGLHelper from '../../../../../../src/ol/webgl/Helper.js'; +import {create as createWebGLWorker} from '../../../../../../src/ol/worker/webgl.js'; +import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; +import Feature from '../../../../../../src/ol/Feature.js'; +import Point from '../../../../../../src/ol/geom/Point.js'; +import Polygon from '../../../../../../src/ol/geom/Polygon.js'; +import LineString from '../../../../../../src/ol/geom/LineString.js'; +import GeometryType from '../../../../../../src/ol/geom/GeometryType.js'; +import {create as createTransform} from '../../../../../../src/ol/transform.js'; +import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js'; +import LineStringBatchRenderer from '../../../../../../src/ol/render/webgl/LineStringBatchRenderer.js'; +import {FLOAT} from '../../../../../../src/ol/webgl.js'; +import PolygonBatchRenderer from '../../../../../../src/ol/render/webgl/PolygonBatchRenderer.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 properties.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 () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.pointBatch, + SAMPLE_FRAMESTATE, + GeometryType.POINT + ); + // 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, + ]); + }); + }); + describe('#render (from parent)', function () { + beforeEach(function () { + sinon.spy(helper, 'makeProjectionTransform'); + sinon.spy(helper, 'useProgram'); + sinon.spy(helper, 'bindBuffer'); + sinon.spy(helper, 'enableAttributes'); + sinon.spy(helper, 'drawElements'); + + const transform = createTransform(); + batchRenderer.render( + mixedBatch.pointBatch, + transform, + SAMPLE_FRAMESTATE + ); + }); + it('computes current transform', function () { + expect(helper.makeProjectionTransform.calledOnce).to.be(true); + }); + 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 () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.lineStringBatch, + SAMPLE_FRAMESTATE, + GeometryType.LINE_STRING + ); + // 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); + }); + }); + }); + + 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 () { + beforeEach(function (done) { + sinon.spy(helper, 'flushBufferData'); + batchRenderer.rebuild( + mixedBatch.polygonBatch, + SAMPLE_FRAMESTATE, + GeometryType.POLYGON + ); + // 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); + }); + }); + }); +}); From a18ffaed54c444b8c3574f03814a04041e48fc0b Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 18:06:15 +0100 Subject: [PATCH 03/27] Move webgl utils out of ol/renderer/webgl/Layer module into their own module --- src/ol/render/webgl/utils.js | 142 ++++++++++++ src/ol/renderer/webgl/Layer.js | 160 -------------- .../spec/ol/render/webgl/utils.test.js | 207 ++++++++++++++++++ .../spec/ol/renderer/webgl/Layer.test.js | 206 +---------------- 4 files changed, 353 insertions(+), 362 deletions(-) create mode 100644 src/ol/render/webgl/utils.js create mode 100644 test/browser/spec/ol/render/webgl/utils.test.js diff --git a/src/ol/render/webgl/utils.js b/src/ol/render/webgl/utils.js new file mode 100644 index 0000000000..3ddd7ebac6 --- /dev/null +++ b/src/ol/render/webgl/utils.js @@ -0,0 +1,142 @@ +/** + * @module ol/render/webgl/utils + */ +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; +} 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/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..9adac57c38 --- /dev/null +++ b/test/browser/spec/ol/render/webgl/utils.test.js @@ -0,0 +1,207 @@ +import { + colorDecodeId, + colorEncodeId, + getBlankImageData, + writePointFeatureToBuffers, +} from '../../../../../../src/ol/render/webgl/utils.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('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..e50d7ecfec 100644 --- a/test/browser/spec/ol/renderer/webgl/Layer.test.js +++ b/test/browser/spec/ol/renderer/webgl/Layer.test.js @@ -6,13 +6,14 @@ 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, { +import WebGLLayerRenderer from '../../../../../../src/ol/renderer/webgl/Layer.js'; +import {getUid} from '../../../../../../src/ol/util.js'; +import { colorDecodeId, colorEncodeId, getBlankImageData, writePointFeatureToBuffers, -} from '../../../../../../src/ol/renderer/webgl/Layer.js'; -import {getUid} from '../../../../../../src/ol/util.js'; +} from '../../../../../../src/ol/render/webgl/utils.js'; describe('ol/renderer/webgl/Layer', function () { describe('constructor', function () { @@ -36,205 +37,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(() => { From 143c19ca0391bcd27ceee5dfa8bef523a586a57b Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 18:09:44 +0100 Subject: [PATCH 04/27] Add utils for generating webgl buffers from lines and polygons Uses @mapbox/earcut for polygon (what else), and a home made logic for lines --- package-lock.json | 11 + package.json | 1 + src/ol/render/webgl/constants.js | 27 ++ src/ol/render/webgl/utils.js | 221 +++++++++++++- .../spec/ol/render/webgl/utils.test.js | 270 ++++++++++++++++++ 5 files changed, 523 insertions(+), 7 deletions(-) create mode 100644 src/ol/render/webgl/constants.js diff --git a/package-lock.json b/package-lock.json index cf9997f4d3..046ec1b7a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "6.14.2-dev", "license": "BSD-2-Clause", "dependencies": { + "earcut": "^2.2.3", "geotiff": "2.0.4", "ol-mapbox-style": "^8.0.5", "pbf": "3.2.1", @@ -3828,6 +3829,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", @@ -13170,6 +13176,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 c698db44d3..d80f281685 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/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 index 3ddd7ebac6..9bfa7895c8 100644 --- a/src/ol/render/webgl/utils.js +++ b/src/ol/render/webgl/utils.js @@ -1,14 +1,11 @@ /** * @module ol/render/webgl/utils */ -const tmpArray_ = []; -const bufferPositions_ = {vertexPosition: 0, indexPosition: 0}; +import {apply as applyTransform} from '../../transform.js'; +import {clamp} from '../../math.js'; +import earcut from 'earcut'; -function writePointVertex(buffer, pos, x, y, index) { - buffer[pos + 0] = x; - buffer[pos + 1] = y; - buffer[pos + 2] = index; -} +const tmpArray_ = []; /** * An object holding positions both in an index and a vertex buffer. @@ -16,6 +13,13 @@ function writePointVertex(buffer, pos, x, y, index) { * @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 @@ -91,6 +95,209 @@ export function writePointFeatureToBuffers( 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 {number[]} vertexArray Array containing vertices. + * @param {number[]} indexArray Array containing indices. + * @param {number[]} 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; + let 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 {number[]} vertexArray Array containing vertices. + * @param {number[]} 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 diff --git a/test/browser/spec/ol/render/webgl/utils.test.js b/test/browser/spec/ol/render/webgl/utils.test.js index 9adac57c38..8e94ef3684 100644 --- a/test/browser/spec/ol/render/webgl/utils.test.js +++ b/test/browser/spec/ol/render/webgl/utils.test.js @@ -2,8 +2,15 @@ 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 () { @@ -158,6 +165,269 @@ describe('webgl render utils', function () { }); }); + 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(); From 7892c31715df20ab5af729018a57efd1c38a31e3 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 18:11:08 +0100 Subject: [PATCH 05/27] Adapt the WebGL worker to use the new buffer generation utils --- src/ol/worker/webgl.js | 194 +++++++++++++++++----- test/browser/spec/ol/worker/webgl.test.js | 164 +++++++++++++++--- 2 files changed, 293 insertions(+), 65 deletions(-) diff --git a/src/ol/worker/webgl.js b/src/ol/worker/webgl.js index b4c5a9a6b5..3b50434792 100644 --- a/src/ol/worker/webgl.js +++ b/src/ol/worker/webgl.js @@ -2,60 +2,174 @@ * A worker that does cpu-heavy tasks related to webgl rendering. * @module ol/worker/webgl */ -import { - WebGLWorkerMessageType, - writePointFeatureToBuffers, -} from '../renderer/webgl/Layer.js'; import {assign} from '../obj.js'; +import {WebGLWorkerMessageType} from '../render/webgl/constants.js'; +import { + writeLineSegmentToBuffers, + writePointFeatureToBuffers, + writePolygonTrianglesToBuffers, +} from '../render/webgl/utils.js'; +import { + create as createTransform, + makeInverse as makeInverseTransform, +} from '../transform.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; + } } }; 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); }); }); }); From cfaf9a14e5394e4eacc8196c3bec80a4d3f352f3 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 9 Mar 2022 23:24:27 +0100 Subject: [PATCH 06/27] Rework a bit the webgl helper to allow having several programs Without this, doing render passes with different programs using one helper instance was not really doable --- src/ol/renderer/webgl/PointsLayer.js | 4 +- src/ol/renderer/webgl/TileLayer.js | 2 +- src/ol/webgl/Helper.js | 50 +++++++++++------------ test/browser/spec/ol/webgl/helper.test.js | 47 ++++++++++++--------- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 2d41b7cead..b259041c07 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -540,7 +540,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 @@ -726,7 +726,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/webgl/Helper.js b/src/ol/webgl/Helper.js index 383b06e848..ba38afaf53 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -49,6 +49,7 @@ export const DefaultUniform = { TIME: 'u_time', ZOOM: 'u_zoom', RESOLUTION: 'u_resolution', + SIZE_PX: 'u_sizePx', }; /** @@ -206,11 +207,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. @@ -564,8 +566,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 +581,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,10 +605,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); } /** @@ -709,6 +701,7 @@ class WebGLHelper extends Disposable { DefaultUniform.RESOLUTION, frameState.viewState.resolution ); + this.setUniformFloatVec2(DefaultUniform.SIZE_PX, [size[0], size[1]]); } /** @@ -803,22 +796,20 @@ 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. + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @api */ - 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); } /** @@ -959,6 +950,15 @@ class WebGLHelper extends Disposable { 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 diff --git a/test/browser/spec/ol/webgl/helper.test.js b/test/browser/spec/ol/webgl/helper.test.js index 1b4a194eba..67fcef2fc4 100644 --- a/test/browser/spec/ol/webgl/helper.test.js +++ b/test/browser/spec/ol/webgl/helper.test.js @@ -57,6 +57,15 @@ 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], + }, +}; + describe('ol/webgl/WebGLHelper', function () { let h; afterEach(function () { @@ -117,7 +126,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], @@ -164,7 +176,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 +221,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 +385,8 @@ describe('ol/webgl/WebGLHelper', function () { void main(void) { gl_Position = vec4(u_test, attr3, 0.0, 1.0); }` - ) + ), + SAMPLE_FRAMESTATE ); }); From c555315014b1a6e84c9c9191ed665aedd3f0cff6 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 9 Mar 2022 23:32:29 +0100 Subject: [PATCH 07/27] Add a new WebGLVectorLayer renderer This relies on a mixed geometry batch and separate batch renderers (lines, points and polygons). A different shader program is used for each of these geometries, and three rendering passes are made. --- src/ol/renderer/webgl/VectorLayer.js | 345 +++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/ol/renderer/webgl/VectorLayer.js diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js new file mode 100644 index 0000000000..4542590906 --- /dev/null +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -0,0 +1,345 @@ +/** + * @module ol/renderer/webgl/VectorLayer + */ +import WebGLLayerRenderer from './Layer.js'; +import {create as createTransform} from '../../transform.js'; +import {DefaultUniform} from '../../webgl/Helper.js'; +import {buffer, createEmpty, equals} from '../../extent.js'; +import {create as createWebGLWorker} from '../../worker/webgl.js'; +import {listen, unlistenByKey} from '../../events.js'; +import VectorEventType from '../../source/VectorEventType.js'; +import ViewHint from '../../ViewHint.js'; +import BaseVector from '../../layer/BaseVector.js'; +import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js'; +import GeometryType from '../../geom/GeometryType.js'; +import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js'; +import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js'; +import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js'; +import WebGLRenderTarget from '../../webgl/RenderTarget.js'; + +/** + * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different + * for each feature. + * @property {string} name Attribute name. + * @property {function(import("../../Feature").default, Object):number} callback This callback computes the numerical value of the + * attribute for a given feature (properties are available as 2nd arg for quicker access). + */ + +/** + * @typedef {Object} Options + * @property {string} [className='ol-layer'] A CSS class name to set to the canvas element. + * @property {Array} [attributes] These attributes will be read from the features in the source + * and then passed to the GPU. The `name` property of each attribute will serve as its identifier: + * * In the vertex shader as an `attribute` by prefixing it with `a_` + * * In the fragment shader as a `varying` by prefixing it with `v_` + * Please note that these can only be numerical values. + * @property {string} polygonVertexShader Vertex shader source, mandatory. + * @property {string} polygonFragmentShader Fragment shader source, mandatory. + * @property {string} lineStringVertexShader Vertex shader source, mandatory. + * @property {string} lineStringFragmentShader Fragment shader source, mandatory. + * @property {string} pointVertexShader Vertex shader source, mandatory. + * @property {string} pointFragmentShader Fragment shader source, mandatory. + * @property {string} [hitVertexShader] Vertex shader source for hit detection rendering. + * @property {string} [hitFragmentShader] Fragment shader source for hit detection rendering. + * @property {Object} [uniforms] Uniform definitions for the post process steps + * Please note that `u_texture` is reserved for the main texture slot. + * @property {Array} [postProcesses] Post-processes definitions + */ + +/** + * @classdesc + * Experimental WebGL vector renderer. Supports polygons and lines. + * + * You need to provide vertex and fragment shaders for rendering. This can be done using + * {@link module:ol/webgl/ShaderBuilder} utilities. + * + * To include variable attributes in the shaders, you need to declare them using the `attributes` property of + * the options object like so: + * ```js + * new WebGLPointsLayerRenderer(layer, { + * attributes: [ + * { + * name: 'size', + * callback: function(feature) { + * // compute something with the feature + * } + * }, + * { + * name: 'weight', + * callback: function(feature) { + * // compute something with the feature + * } + * }, + * ], + * vertexShader: + * // shader using attribute a_weight and a_size + * fragmentShader: + * // shader using varying v_weight and v_size + * ``` + * + * To enable hit detection, you must as well provide dedicated shaders using the `hitVertexShader` + * and `hitFragmentShader` properties. These shall expect the `a_hitColor` attribute to contain + * the final color that will have to be output for hit detection to work. + * + * The following uniform is used for the main texture: `u_texture`. + * + * Please note that the main shader output should have premultiplied alpha, otherwise visual anomalies may occur. + * + * Polygons are broken down into triangles using the @mapbox/earcut package. + * Lines are rendered into strips of quads. + * + * + * This uses {@link module:ol/webgl/Helper~WebGLHelper} internally. + * + * @api + */ +class WebGLVectorLayerRenderer extends WebGLLayerRenderer { + /** + * @param {import("../../layer/Layer.js").default} layer Layer. + * @param {Options} options Options. + */ + constructor(layer, options) { + const uniforms = options.uniforms || {}; + const projectionMatrixTransform = createTransform(); + uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform; + + super(layer, { + uniforms: uniforms, + postProcesses: options.postProcesses, + }); + + this.sourceRevision_ = -1; + + this.previousExtent_ = createEmpty(); + + /** + * This transform is updated on every frame and is the composition of: + * - invert of the world->screen transform that was used when rebuilding buffers (see `this.renderTransform_`) + * - current world->screen transform + * @type {import("../../transform.js").Transform} + * @private + */ + this.currentTransform_ = projectionMatrixTransform; + + this.polygonVertexShader_ = options.polygonVertexShader; + this.polygonFragmentShader_ = options.polygonFragmentShader; + this.pointVertexShader_ = options.pointVertexShader; + this.pointFragmentShader_ = options.pointFragmentShader; + this.lineStringVertexShader_ = options.lineStringVertexShader; + this.lineStringFragmentShader_ = options.lineStringFragmentShader; + this.attributes_ = options.attributes; + + this.worker_ = createWebGLWorker(); + + this.batch_ = new MixedGeometryBatch(); + + const source = this.getLayer().getSource(); + this.batch_.addFeatures(source.getFeatures()); + this.sourceListenKeys_ = [ + listen( + source, + VectorEventType.ADDFEATURE, + this.handleSourceFeatureAdded_, + this + ), + listen( + source, + VectorEventType.CHANGEFEATURE, + this.handleSourceFeatureChanged_, + this + ), + listen( + source, + VectorEventType.REMOVEFEATURE, + this.handleSourceFeatureDelete_, + this + ), + listen( + source, + VectorEventType.CLEAR, + this.handleSourceFeatureClear_, + this + ), + ]; + } + + afterHelperCreated() { + this.polygonRenderer_ = new PolygonBatchRenderer( + this.helper, + this.worker_, + this.polygonVertexShader_, + this.polygonFragmentShader_, + this.attributes_ || [] + ); + this.pointRenderer_ = new PointBatchRenderer( + this.helper, + this.worker_, + this.pointVertexShader_, + this.pointFragmentShader_, + this.attributes_ || [] + ); + this.lineStringRenderer_ = new LineStringBatchRenderer( + this.helper, + this.worker_, + this.lineStringVertexShader_, + this.lineStringFragmentShader_, + this.attributes_ || [] + ); + } + + /** + * @param {import("../../source/Vector.js").VectorSourceEvent} event Event. + * @private + */ + handleSourceFeatureAdded_(event) { + const feature = event.feature; + this.batch_.addFeature(feature); + } + + /** + * @param {import("../../source/Vector.js").VectorSourceEvent} event Event. + * @private + */ + handleSourceFeatureChanged_(event) { + const feature = event.feature; + this.batch_.changeFeature(feature); + } + + /** + * @param {import("../../source/Vector.js").VectorSourceEvent} event Event. + * @private + */ + handleSourceFeatureDelete_(event) { + const feature = event.feature; + this.batch_.removeFeature(feature); + } + + /** + * @private + */ + handleSourceFeatureClear_() { + this.batch_.clear(); + } + + /** + * Render the layer. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @return {HTMLElement} The rendered element. + */ + renderFrame(frameState) { + const gl = this.helper.getGL(); + this.preRender(gl, frameState); + this.polygonRenderer_.render( + this.batch_.polygonBatch, + this.currentTransform_, + frameState + ); + this.lineStringRenderer_.render( + this.batch_.lineStringBatch, + this.currentTransform_, + frameState + ); + this.pointRenderer_.render( + this.batch_.pointBatch, + this.currentTransform_, + frameState + ); + this.helper.finalizeDraw(frameState); + + const canvas = this.helper.getCanvas(); + const layerState = frameState.layerStatesArray[frameState.layerIndex]; + const opacity = layerState.opacity; + if (opacity !== parseFloat(canvas.style.opacity)) { + canvas.style.opacity = String(opacity); + } + + this.postRender(gl, frameState); + return canvas; + } + + /** + * Determine whether renderFrame should be called. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @return {boolean} Layer is ready to be rendered. + */ + prepareFrameInternal(frameState) { + const layer = this.getLayer(); + const vectorSource = layer.getSource(); + const viewState = frameState.viewState; + const viewNotMoving = + !frameState.viewHints[ViewHint.ANIMATING] && + !frameState.viewHints[ViewHint.INTERACTING]; + const extentChanged = !equals(this.previousExtent_, frameState.extent); + const sourceChanged = this.sourceRevision_ < vectorSource.getRevision(); + + if (sourceChanged) { + this.sourceRevision_ = vectorSource.getRevision(); + } + + if (viewNotMoving && (extentChanged || sourceChanged)) { + const projection = viewState.projection; + const resolution = viewState.resolution; + + const renderBuffer = + layer instanceof BaseVector ? layer.getRenderBuffer() : 0; + const extent = buffer(frameState.extent, renderBuffer * resolution); + vectorSource.loadFeatures(extent, resolution, projection); + + this.polygonRenderer_.rebuild( + this.batch_.polygonBatch, + frameState, + GeometryType.POLYGON + ); + this.lineStringRenderer_.rebuild( + this.batch_.lineStringBatch, + frameState, + GeometryType.LINE_STRING + ); + this.pointRenderer_.rebuild( + this.batch_.pointBatch, + frameState, + GeometryType.POINT + ); + this.previousExtent_ = frameState.extent.slice(); + } + + this.helper.makeProjectionTransform(frameState, this.currentTransform_); + this.helper.prepareDraw(frameState); + + return true; + } + + /** + * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {number} hitTolerance Hit tolerance in pixels. + * @param {import("../vector.js").FeatureCallback} 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; From 979dfd3a5522af8cc849cce25240287faf4f742a Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 9 Mar 2022 23:34:17 +0100 Subject: [PATCH 08/27] Add an example for the new webgl vector layer This example accomodates for hit detection whih is not functional yet. --- examples/webgl-vector-layer.html | 10 ++ examples/webgl-vector-layer.js | 182 +++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 examples/webgl-vector-layer.html create mode 100644 examples/webgl-vector-layer.js diff --git a/examples/webgl-vector-layer.html b/examples/webgl-vector-layer.html new file mode 100644 index 0000000000..f297fba476 --- /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. Information about ecoregions is shown on hover and click. +tags: "vector, geojson, webgl" +--- +
+
 
diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js new file mode 100644 index 0000000000..3b8672d826 --- /dev/null +++ b/examples/webgl-vector-layer.js @@ -0,0 +1,182 @@ +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'; + +class WebGLLayer extends Layer { + createRenderer() { + return new WebGLVectorLayerRenderer(this, { + className: this.getClassName(), + polygonVertexShader: ` + precision mediump float; + uniform mat4 u_projectionMatrix; + attribute vec2 a_position; + attribute float a_color; + + varying vec3 v_color; + + void main(void) { + gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0); + v_color = vec3( + floor(a_color / 256.0 / 256.0) / 256.0, + fract(floor(a_color / 256.0) / 256.0), + fract(a_color / 256.0) + ); + }`, + polygonFragmentShader: ` + precision mediump float; + varying vec3 v_color; + + void main(void) { + gl_FragColor = vec4(v_color.rgb, 1.0); + gl_FragColor *= 0.75; + }`, + lineStringVertexShader: ` + 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; + varying vec2 v_segmentStart; + varying vec2 v_segmentEnd; + varying float v_angleStart; + varying float v_angleEnd; + + varying vec3 v_color; + + 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; + } + + float lineWidth = 2.0; + + 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) * lineWidth * 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 = vec3( + floor(a_color / 256.0 / 256.0) / 256.0, + fract(floor(a_color / 256.0) / 256.0), + fract(a_color / 256.0) + ); + }`, + lineStringFragmentShader: ` + precision mediump float; + varying vec2 v_segmentStart; + varying vec2 v_segmentEnd; + varying float v_angleStart; + varying float v_angleEnd; + + varying vec3 v_color; + + 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); + } + + float lineWidth = 1.5; + + void main(void) { + gl_FragColor = vec4(v_color.rgb * 0.75, 1.0); + gl_FragColor *= segmentDistanceField(gl_FragCoord.xy, v_segmentStart, v_segmentEnd, lineWidth); + }`, + pointVertexShader: ` + precision mediump float; + uniform mat4 u_projectionMatrix; + uniform mat4 u_offsetScaleMatrix; + attribute vec2 a_position; + attribute float a_index; + varying vec2 v_texCoord; + + 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); + }`, + pointFragmentShader: ` + precision mediump float; + varying vec2 v_texCoord; + + void main(void) { + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); + }`, + attributes: [ + { + name: 'color', + callback: function (feature, properties) { + const color = asArray(properties.COLOR || '#eee'); + // RGB components are encoded into one value + return color[0] * 256 * 256 + color[1] * 256 + color[2]; + }, + }, + ], + }); + } +} + +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, + }), +}); From a2ba7ecaa737685d9102d5e5928b40b3ba7bc8e5 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 17:53:12 +0100 Subject: [PATCH 09/27] Make some adaptations to the PointsLayer renderer to make it work --- src/ol/renderer/webgl/PointsLayer.js | 20 ++++++++--------- .../ol/renderer/webgl/PointsLayer.test.js | 22 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index b259041c07..bb3ca67fa8 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -5,11 +5,7 @@ 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'; @@ -25,6 +21,8 @@ import {buffer, createEmpty, equals, getWidth} from '../../extent.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; import {getUid} from '../../util.js'; import {listen, unlistenByKey} from '../../events.js'; +import {colorDecodeId, colorEncodeId} from '../../render/webgl/utils.js'; +import {WebGLWorkerMessageType} from '../../render/webgl/constants.js'; /** * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different @@ -303,7 +301,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); @@ -637,9 +635,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 +649,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, }; 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], }, }, ], From 7e9c620914927c228c3e4f48db19ac46f29d4bbb Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 22:05:24 +0100 Subject: [PATCH 10/27] Fix linting and typechecking errors --- src/ol/render/webgl/BatchRenderer.js | 31 ++--- .../render/webgl/LineStringBatchRenderer.js | 14 +-- src/ol/render/webgl/MixedGeometryBatch.js | 113 +++++++++++------- src/ol/render/webgl/PointBatchRenderer.js | 16 +-- src/ol/render/webgl/PolygonBatchRenderer.js | 14 +-- src/ol/render/webgl/utils.js | 18 +-- src/ol/renderer/webgl/PointsLayer.js | 4 +- src/ol/renderer/webgl/VectorLayer.js | 25 ++-- src/ol/worker/webgl.js | 12 +- .../ol/render/webgl/batchrenderer.test.js | 20 ++-- .../render/webgl/mixedgeometrybatch.test.js | 14 +-- .../spec/ol/renderer/webgl/Layer.test.js | 6 - 12 files changed, 160 insertions(+), 127 deletions(-) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index 406ac8c7f2..a19dff4360 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -2,13 +2,13 @@ * @module ol/render/webgl/BatchRenderer */ import GeometryType from '../../geom/GeometryType.js'; +import {WebGLWorkerMessageType} from './constants.js'; +import {abstract} from '../../util.js'; import { create as createTransform, makeInverse as makeInverseTransform, multiply as multiplyTransform, } from '../../transform.js'; -import {abstract} from '../../util.js'; -import {WebGLWorkerMessageType} from './constants.js'; /** * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different @@ -27,11 +27,11 @@ let workerMessageCounter = 0; */ class AbstractBatchRenderer { /** - * @param {import("../../webgl/Helper.js").default} helper - * @param {Worker} worker - * @param {string} vertexShader - * @param {string} fragmentShader - * @param {Array} customAttributes + * @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) { /** @@ -69,9 +69,9 @@ class AbstractBatchRenderer { /** * Rebuild rendering instructions and webgl buffers based on the provided frame state * Note: this is a costly operation. - * @param {import("./MixedGeometryBatch.js").AbstractGeometryBatch} batch + * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @param {import("../../PluggableMap").FrameState} frameState Frame state. - * @param {import("../../geom/GeometryType.js").default} geometryType + * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type */ rebuild(batch, frameState, geometryType) { // store transform for rendering instructions @@ -86,12 +86,13 @@ class AbstractBatchRenderer { /** * 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").AbstractGeometryBatch} batch - * @param {import("../../transform.js").Transform} currentTransform + * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch + * @param {import("../../transform.js").Transform} currentTransform Transform * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. */ render(batch, currentTransform, frameState) { // multiply the current projection transform with the invert of the one used to fill buffers + // FIXME: this should probably be done directly in the layer renderer this.helper_.makeProjectionTransform(frameState, currentTransform); multiplyTransform(currentTransform, batch.invertVerticesBufferTransform); @@ -108,7 +109,7 @@ class AbstractBatchRenderer { /** * 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").default} batch + * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @protected */ generateRenderInstructions_(batch) { @@ -118,8 +119,8 @@ class AbstractBatchRenderer { /** * 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").AbstractGeometryBatch} batch - * @param {import("../../geom/GeometryType.js").default} geometryType + * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch + * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type * @protected */ generateBuffers_(batch, geometryType) { @@ -136,6 +137,8 @@ class AbstractBatchRenderer { case GeometryType.LINE_STRING: messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS; break; + default: + // pass } /** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */ diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js index 31ef0e4e6a..3c5e1509d6 100644 --- a/src/ol/render/webgl/LineStringBatchRenderer.js +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -1,17 +1,17 @@ /** * @module ol/render/webgl/LineStringBatchRenderer */ +import AbstractBatchRenderer from './BatchRenderer.js'; import {AttributeType} from '../../webgl/Helper.js'; import {transform2D} from '../../geom/flat/transform.js'; -import AbstractBatchRenderer from './BatchRenderer.js'; class LineStringBatchRenderer extends AbstractBatchRenderer { /** - * @param {import("../../webgl/Helper.js").default} helper - * @param {Worker} worker - * @param {string} vertexShader - * @param {string} fragmentShader - * @param {Array} customAttributes + * @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); @@ -47,7 +47,7 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { /** * Render instructions for lines are structured like so: * [ customAttr0, ... , customAttrN, numberOfVertices0, x0, y0, ... , xN, yN, numberOfVertices1, ... ] - * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch + * @param {import("./MixedGeometryBatch.js").LineStringGeometryBatch} batch Linestring geometry batch * @override */ generateRenderInstructions_(batch) { diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js index c0270657c1..dc144bf26e 100644 --- a/src/ol/render/webgl/MixedGeometryBatch.js +++ b/src/ol/render/webgl/MixedGeometryBatch.js @@ -1,11 +1,11 @@ /** * @module ol/render/webgl/MixedGeometryBatch */ -import {getUid} from '../../util.js'; import GeometryType from '../../geom/GeometryType.js'; 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 @@ -18,36 +18,52 @@ import {create as createTransform} from '../../transform.js'; */ /** - * @typedef {Object} AbstractGeometryBatch - * @abstract + * @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 - * @property {WebGLArrayBuffer} indicesBuffer + * @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 - */ - -/** - * @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons - * @extends {AbstractGeometryBatch} * @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 - * @extends {AbstractGeometryBatch} + * @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 - * @extends {AbstractGeometryBatch} + * @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 */ /** @@ -118,7 +134,7 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default[]} features + * @param {Array} features Array of features to add to the batch */ addFeatures(features) { for (let i = 0; i < features.length; i++) { @@ -127,7 +143,7 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature + * @param {import("../../Feature").default} feature Feature to add to the batch */ addFeature(feature) { const geometry = feature.getGeometry(); @@ -138,8 +154,8 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature - * @return {GeometryBatchItem} + * @param {import("../../Feature").default} feature Feature + * @return {GeometryBatchItem} Batch item added (or existing one) * @private */ addFeatureEntryInPointBatch_(feature) { @@ -155,8 +171,8 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature - * @return {GeometryBatchItem} + * @param {import("../../Feature").default} feature Feature + * @return {GeometryBatchItem} Batch item added (or existing one) * @private */ addFeatureEntryInLineStringBatch_(feature) { @@ -173,8 +189,8 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature - * @return {GeometryBatchItem} + * @param {import("../../Feature").default} feature Feature + * @return {GeometryBatchItem} Batch item added (or existing one) * @private */ addFeatureEntryInPolygonBatch_(feature) { @@ -193,35 +209,41 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature + * @param {import("../../Feature").default} feature Feature * @private */ clearFeatureEntryInPointBatch_(feature) { const entry = this.pointBatch.entries[getUid(feature)]; - if (!entry) return; + if (!entry) { + return; + } this.pointBatch.geometriesCount -= entry.flatCoordss.length; delete this.pointBatch.entries[getUid(feature)]; } /** - * @param {import("../../Feature").default} feature + * @param {import("../../Feature").default} feature Feature * @private */ clearFeatureEntryInLineStringBatch_(feature) { const entry = this.lineStringBatch.entries[getUid(feature)]; - if (!entry) return; + 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 + * @param {import("../../Feature").default} feature Feature * @private */ clearFeatureEntryInPolygonBatch_(feature) { const entry = this.polygonBatch.entries[getUid(feature)]; - if (!entry) return; + if (!entry) { + return; + } this.polygonBatch.verticesCount -= entry.verticesCount; this.polygonBatch.ringsCount -= entry.ringsCount; this.polygonBatch.geometriesCount -= entry.flatCoordss.length; @@ -229,8 +251,8 @@ class MixedGeometryBatch { } /** - * @param {import("../../geom").Geometry} geometry - * @param {import("../../Feature").default} feature + * @param {import("../../geom").Geometry} geometry Geometry + * @param {import("../../Feature").default} feature Feature * @private */ addGeometry_(geometry, feature) { @@ -240,29 +262,34 @@ class MixedGeometryBatch { let batchEntry; switch (type) { case GeometryType.GEOMETRY_COLLECTION: - geometry + /** @type {import("../../geom").GeometryCollection} */ (geometry) .getGeometries() .map((geom) => this.addGeometry_(geom, feature)); break; case GeometryType.MULTI_POLYGON: - geometry + /** @type {import("../../geom").MultiPolygon} */ (geometry) .getPolygons() .map((polygon) => this.addGeometry_(polygon, feature)); break; case GeometryType.MULTI_LINE_STRING: - geometry + /** @type {import("../../geom").MultiLineString} */ (geometry) .getLineStrings() .map((line) => this.addGeometry_(line, feature)); break; case GeometryType.MULTI_POINT: - geometry.getPoints().map((point) => this.addGeometry_(point, feature)); + /** @type {import("../../geom").MultiPoint} */ (geometry) + .getPoints() + .map((point) => this.addGeometry_(point, feature)); break; case GeometryType.POLYGON: + const polygonGeom = /** @type {import("../../geom").Polygon} */ ( + geometry + ); batchEntry = this.addFeatureEntryInPolygonBatch_(feature); - flatCoords = geometry.getFlatCoordinates(); + flatCoords = polygonGeom.getFlatCoordinates(); verticesCount = flatCoords.length / 2; - const ringsCount = geometry.getLinearRingCount(); - const ringsVerticesCount = geometry + const ringsCount = polygonGeom.getLinearRingCount(); + const ringsVerticesCount = polygonGeom .getEnds() .map((end, ind, arr) => ind > 0 ? (end - arr[ind - 1]) / 2 : end / 2 @@ -274,31 +301,37 @@ class MixedGeometryBatch { batchEntry.ringsVerticesCounts.push(ringsVerticesCount); batchEntry.verticesCount += verticesCount; batchEntry.ringsCount += ringsCount; - geometry + polygonGeom .getLinearRings() .map((ring) => this.addGeometry_(ring, feature)); break; case GeometryType.POINT: + const pointGeom = /** @type {import("../../geom").Point} */ (geometry); batchEntry = this.addFeatureEntryInPointBatch_(feature); - flatCoords = geometry.getFlatCoordinates(); + flatCoords = pointGeom.getFlatCoordinates(); this.pointBatch.geometriesCount++; batchEntry.flatCoordss.push(flatCoords); break; case GeometryType.LINE_STRING: case GeometryType.LINEAR_RING: + const lineGeom = /** @type {import("../../geom").LineString} */ ( + geometry + ); batchEntry = this.addFeatureEntryInLineStringBatch_(feature); - flatCoords = geometry.getFlatCoordinates(); + 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 + * @param {import("../../Feature").default} feature Feature */ changeFeature(feature) { this.clearFeatureEntryInPointBatch_(feature); @@ -312,7 +345,7 @@ class MixedGeometryBatch { } /** - * @param {import("../../Feature").default} feature + * @param {import("../../Feature").default} feature Feature */ removeFeature(feature) { this.clearFeatureEntryInPointBatch_(feature); diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js index cc7ddb3318..09ca001250 100644 --- a/src/ol/render/webgl/PointBatchRenderer.js +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -2,17 +2,17 @@ * @module ol/render/webgl/PointBatchRenderer */ -import {apply as applyTransform} from '../../transform.js'; -import {AttributeType} from '../../webgl/Helper.js'; import AbstractBatchRenderer from './BatchRenderer.js'; +import {AttributeType} from '../../webgl/Helper.js'; +import {apply as applyTransform} from '../../transform.js'; class PointBatchRenderer extends AbstractBatchRenderer { /** - * @param {import("../../webgl/Helper.js").default} helper - * @param {Worker} worker - * @param {string} vertexShader - * @param {string} fragmentShader - * @param {Array} customAttributes + * @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); @@ -43,7 +43,7 @@ class PointBatchRenderer extends AbstractBatchRenderer { /** * Render instructions for lines are structured like so: * [ x0, y0, customAttr0, ... , xN, yN, customAttrN ] - * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch + * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch Point geometry batch * @override */ generateRenderInstructions_(batch) { diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js index b99f22659d..897b94ece3 100644 --- a/src/ol/render/webgl/PolygonBatchRenderer.js +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -1,17 +1,17 @@ /** * @module ol/render/webgl/PolygonBatchRenderer */ +import AbstractBatchRenderer from './BatchRenderer.js'; import {AttributeType} from '../../webgl/Helper.js'; import {transform2D} from '../../geom/flat/transform.js'; -import AbstractBatchRenderer from './BatchRenderer.js'; class PolygonBatchRenderer extends AbstractBatchRenderer { /** - * @param {import("../../webgl/Helper.js").default} helper - * @param {Worker} worker - * @param {string} vertexShader - * @param {string} fragmentShader - * @param {Array} customAttributes + * @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); @@ -37,7 +37,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { /** * Render instructions for polygons are structured like so: * [ customAttr0, ..., customAttrN, numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, ..., xN, yN, numberOfRings,... ] - * @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch + * @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch Polygon geometry batch * @override */ generateRenderInstructions_(batch) { diff --git a/src/ol/render/webgl/utils.js b/src/ol/render/webgl/utils.js index 9bfa7895c8..74605c26d9 100644 --- a/src/ol/render/webgl/utils.js +++ b/src/ol/render/webgl/utils.js @@ -1,9 +1,9 @@ /** * @module ol/render/webgl/utils */ +import earcut from 'earcut'; import {apply as applyTransform} from '../../transform.js'; import {clamp} from '../../math.js'; -import earcut from 'earcut'; const tmpArray_ = []; @@ -103,9 +103,9 @@ export function writePointFeatureToBuffers( * @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 {number[]} vertexArray Array containing vertices. - * @param {number[]} indexArray Array containing indices. - * @param {number[]} customAttributes Array of custom attributes value + * @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 @@ -125,7 +125,7 @@ export function writeLineSegmentToBuffers( // 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; - let baseIndex = vertexArray.length / stride; + 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 @@ -253,8 +253,8 @@ export function writeLineSegmentToBuffers( * 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 {number[]} vertexArray Array containing vertices. - * @param {number[]} indexArray Array containing indices. + * @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 @@ -279,7 +279,9 @@ export function writePolygonTrianglesToBuffers( const holes = new Array(ringsCount - 1); for (let i = 0; i < ringsCount; i++) { verticesCount += instructions[instructionsIndex++]; - if (i < ringsCount - 1) holes[i] = verticesCount; + if (i < ringsCount - 1) { + holes[i] = verticesCount; + } } const flatCoords = instructions.slice( instructionsIndex, diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index bb3ca67fa8..3553d6121f 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -9,6 +9,7 @@ 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, @@ -18,11 +19,10 @@ 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'; -import {colorDecodeId, colorEncodeId} from '../../render/webgl/utils.js'; -import {WebGLWorkerMessageType} from '../../render/webgl/constants.js'; /** * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 4542590906..5c4333cc7f 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -1,21 +1,20 @@ /** * @module ol/renderer/webgl/VectorLayer */ -import WebGLLayerRenderer from './Layer.js'; -import {create as createTransform} from '../../transform.js'; -import {DefaultUniform} from '../../webgl/Helper.js'; -import {buffer, createEmpty, equals} from '../../extent.js'; -import {create as createWebGLWorker} from '../../worker/webgl.js'; -import {listen, unlistenByKey} from '../../events.js'; +import BaseVector from '../../layer/BaseVector.js'; +import GeometryType from '../../geom/GeometryType.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 BaseVector from '../../layer/BaseVector.js'; -import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js'; -import GeometryType from '../../geom/GeometryType.js'; -import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js'; -import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js'; -import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js'; -import WebGLRenderTarget from '../../webgl/RenderTarget.js'; +import WebGLLayerRenderer from './Layer.js'; +import {DefaultUniform} from '../../webgl/Helper.js'; +import {buffer, createEmpty, equals} 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 {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different diff --git a/src/ol/worker/webgl.js b/src/ol/worker/webgl.js index 3b50434792..34cb5509f5 100644 --- a/src/ol/worker/webgl.js +++ b/src/ol/worker/webgl.js @@ -2,17 +2,17 @@ * A worker that does cpu-heavy tasks related to webgl rendering. * @module ol/worker/webgl */ -import {assign} from '../obj.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'; -import { - create as createTransform, - makeInverse as makeInverseTransform, -} from '../transform.js'; /** @type {any} */ const worker = self; @@ -170,6 +170,8 @@ worker.onmessage = (event) => { ]); 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 index ef0c4e37cf..7d47eaad3d 100644 --- a/test/browser/spec/ol/render/webgl/batchrenderer.test.js +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -1,17 +1,17 @@ -import PointBatchRenderer from '../../../../../../src/ol/render/webgl/PointBatchRenderer.js'; -import WebGLHelper from '../../../../../../src/ol/webgl/Helper.js'; -import {create as createWebGLWorker} from '../../../../../../src/ol/worker/webgl.js'; -import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; import Feature from '../../../../../../src/ol/Feature.js'; -import Point from '../../../../../../src/ol/geom/Point.js'; -import Polygon from '../../../../../../src/ol/geom/Polygon.js'; -import LineString from '../../../../../../src/ol/geom/LineString.js'; import GeometryType from '../../../../../../src/ol/geom/GeometryType.js'; -import {create as createTransform} from '../../../../../../src/ol/transform.js'; -import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js'; +import LineString from '../../../../../../src/ol/geom/LineString.js'; import LineStringBatchRenderer from '../../../../../../src/ol/render/webgl/LineStringBatchRenderer.js'; -import {FLOAT} from '../../../../../../src/ol/webgl.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} 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) {}`; diff --git a/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js b/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js index cd28c50e3d..dba8e79485 100644 --- a/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js +++ b/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js @@ -1,14 +1,14 @@ import Feature from '../../../../../../src/ol/Feature.js'; -import Point from '../../../../../../src/ol/geom/Point.js'; -import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; -import LineString from '../../../../../../src/ol/geom/LineString.js'; -import {getUid} from '../../../../../../src/ol/index.js'; -import Polygon from '../../../../../../src/ol/geom/Polygon.js'; -import LinearRing from '../../../../../../src/ol/geom/LinearRing.js'; -import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.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; diff --git a/test/browser/spec/ol/renderer/webgl/Layer.test.js b/test/browser/spec/ol/renderer/webgl/Layer.test.js index e50d7ecfec..60a5581990 100644 --- a/test/browser/spec/ol/renderer/webgl/Layer.test.js +++ b/test/browser/spec/ol/renderer/webgl/Layer.test.js @@ -8,12 +8,6 @@ import VectorSource from '../../../../../../src/ol/source/Vector.js'; import View from '../../../../../../src/ol/View.js'; import WebGLLayerRenderer from '../../../../../../src/ol/renderer/webgl/Layer.js'; import {getUid} from '../../../../../../src/ol/util.js'; -import { - colorDecodeId, - colorEncodeId, - getBlankImageData, - writePointFeatureToBuffers, -} from '../../../../../../src/ol/render/webgl/utils.js'; describe('ol/renderer/webgl/Layer', function () { describe('constructor', function () { From c9f3665237fc57af46399ee1bcd4b1b96632ef97 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 31 Mar 2022 22:19:54 +0200 Subject: [PATCH 11/27] Remove @api on webgl vector layer renderer as well as utilities This removes the WebGL vector layer renderer as well as the WebGL helper class from the API. --- src/ol/renderer/webgl/VectorLayer.js | 2 -- src/ol/webgl/Helper.js | 18 ------------------ 2 files changed, 20 deletions(-) diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 5c4333cc7f..f8c3eebe13 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -89,8 +89,6 @@ import {listen, unlistenByKey} from '../../events.js'; * * * This uses {@link module:ol/webgl/Helper~WebGLHelper} internally. - * - * @api */ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { /** diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index ba38afaf53..032391ad74 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -304,8 +304,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 { /** @@ -486,7 +484,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(); @@ -507,7 +504,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(); @@ -553,7 +549,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(); @@ -611,7 +606,6 @@ class WebGLHelper extends Disposable { * 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(); @@ -652,7 +646,6 @@ class WebGLHelper extends Disposable { /** * @return {HTMLCanvasElement} Canvas. - * @api */ getCanvas() { return this.canvas_; @@ -661,7 +654,6 @@ class WebGLHelper extends Disposable { /** * Get the WebGL rendering context * @return {WebGLRenderingContext} The rendering context. - * @api */ getGL() { return this.gl_; @@ -800,7 +792,6 @@ class WebGLHelper extends Disposable { * in the program will be set based on the current frame state and the helper configuration. * @param {WebGLProgram} program Program. * @param {import("../PluggableMap.js").FrameState} frameState Frame state. - * @api */ useProgram(program, frameState) { const gl = this.getGL(); @@ -834,7 +825,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(); @@ -884,7 +874,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) { @@ -900,7 +889,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) { @@ -918,7 +906,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; @@ -944,7 +931,6 @@ 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); @@ -972,7 +958,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 +999,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 +1040,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 +1086,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; From 7d2b1a9f480a0de95a1dbf06102a8f6ce39a4019 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 31 Mar 2022 22:34:28 +0200 Subject: [PATCH 12/27] Make the newWebGL vector example experimental And do not mention hit detection for now --- examples/webgl-vector-layer.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/webgl-vector-layer.html b/examples/webgl-vector-layer.html index f297fba476..e4cf6e9f5b 100644 --- a/examples/webgl-vector-layer.html +++ b/examples/webgl-vector-layer.html @@ -3,8 +3,9 @@ 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. Information about ecoregions is shown on hover and click. + The ecoregions are loaded from a GeoJSON file. tags: "vector, geojson, webgl" +experimental: true ---
 
From 8769ea519e4397b84934158fd4b36033b3e18365 Mon Sep 17 00:00:00 2001 From: burleight <43864121+burleight@users.noreply.github.com> Date: Fri, 1 Apr 2022 20:22:43 +1300 Subject: [PATCH 13/27] WebGL / render multiple worlds to wrap X in vector renderer From https://github.com/jahow/openlayers/pull/1 Adds logic in WebGLVectorLayerRenderer to handle multiple worlds visible at once. Co-authored-by: Tomas Burleigh Co-authored-by: Olivier Guyot --- src/ol/render/webgl/BatchRenderer.js | 6 ++- src/ol/renderer/webgl/VectorLayer.js | 53 +++++++++++++------ .../ol/render/webgl/batchrenderer.test.js | 20 +++++-- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index a19dff4360..3de9c69305 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -8,6 +8,7 @@ import { create as createTransform, makeInverse as makeInverseTransform, multiply as multiplyTransform, + translate as translateTransform, } from '../../transform.js'; /** @@ -89,11 +90,12 @@ class AbstractBatchRenderer { * @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) { + render(batch, currentTransform, frameState, offsetX) { // multiply the current projection transform with the invert of the one used to fill buffers - // FIXME: this should probably be done directly in the layer renderer this.helper_.makeProjectionTransform(frameState, currentTransform); + translateTransform(currentTransform, offsetX, 0); multiplyTransform(currentTransform, batch.invertVerticesBufferTransform); // enable program, buffers and attributes diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index f8c3eebe13..507af098ef 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -11,7 +11,7 @@ import VectorEventType from '../../source/VectorEventType.js'; import ViewHint from '../../ViewHint.js'; import WebGLLayerRenderer from './Layer.js'; import {DefaultUniform} from '../../webgl/Helper.js'; -import {buffer, createEmpty, equals} from '../../extent.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'; @@ -226,21 +226,42 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { renderFrame(frameState) { const gl = this.helper.getGL(); this.preRender(gl, frameState); - this.polygonRenderer_.render( - this.batch_.polygonBatch, - this.currentTransform_, - frameState - ); - this.lineStringRenderer_.render( - this.batch_.lineStringBatch, - this.currentTransform_, - frameState - ); - this.pointRenderer_.render( - this.batch_.pointBatch, - this.currentTransform_, - frameState - ); + + 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(); diff --git a/test/browser/spec/ol/render/webgl/batchrenderer.test.js b/test/browser/spec/ol/render/webgl/batchrenderer.test.js index 7d47eaad3d..fa00eee520 100644 --- a/test/browser/spec/ol/render/webgl/batchrenderer.test.js +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -10,7 +10,10 @@ import PolygonBatchRenderer from '../../../../../../src/ol/render/webgl/PolygonB 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} from '../../../../../../src/ol/transform.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; @@ -133,6 +136,8 @@ describe('Batch renderers', function () { }); }); describe('#render (from parent)', function () { + let transform; + const offsetX = 12; beforeEach(function () { sinon.spy(helper, 'makeProjectionTransform'); sinon.spy(helper, 'useProgram'); @@ -140,16 +145,25 @@ describe('Batch renderers', function () { sinon.spy(helper, 'enableAttributes'); sinon.spy(helper, 'drawElements'); - const transform = createTransform(); + transform = createTransform(); batchRenderer.render( mixedBatch.pointBatch, transform, - SAMPLE_FRAMESTATE + 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); From 9e35acaa0a660429d74555fbd9aabe5a01019bbf Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 4 May 2022 11:15:30 +0200 Subject: [PATCH 14/27] Adapt the batch renderers to trigger a repaint after buffer rebuild --- src/ol/render/webgl/BatchRenderer.js | 10 ++++---- src/ol/renderer/webgl/VectorLayer.js | 17 ++++++++++--- .../ol/render/webgl/batchrenderer.test.js | 24 ++++++++++++++++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index 3de9c69305..c41d030cd0 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -73,15 +73,16 @@ class AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @param {import("../../PluggableMap").FrameState} frameState Frame state. * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type + * @param {function(): void} callback Function called once the render buffers are updated */ - rebuild(batch, frameState, geometryType) { + rebuild(batch, frameState, geometryType, callback) { // store transform for rendering instructions batch.renderInstructionsTransform = this.helper_.makeProjectionTransform( frameState, createTransform() ); this.generateRenderInstructions_(batch); - this.generateBuffers_(batch, geometryType); + this.generateBuffers_(batch, geometryType, callback); } /** @@ -123,9 +124,10 @@ class AbstractBatchRenderer { * This is asynchronous: webgl buffers wil _not_ be updated right away * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type + * @param {function(): void} callback Function called once the render buffers are updated * @protected */ - generateBuffers_(batch, geometryType) { + generateBuffers_(batch, geometryType, callback) { const messageId = workerMessageCounter++; let messageType; @@ -192,7 +194,7 @@ class AbstractBatchRenderer { received.renderInstructions ); - // TODO: call layer.changed somehow for the layer to rerender!!!1 + callback(); }.bind(this); this.worker_.addEventListener('message', handleMessage); diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 507af098ef..ba8dcd42c5 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -303,20 +303,31 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { 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, - GeometryType.POLYGON + GeometryType.POLYGON, + rebuildCb ); this.lineStringRenderer_.rebuild( this.batch_.lineStringBatch, frameState, - GeometryType.LINE_STRING + GeometryType.LINE_STRING, + rebuildCb ); this.pointRenderer_.rebuild( this.batch_.pointBatch, frameState, - GeometryType.POINT + GeometryType.POINT, + rebuildCb ); this.previousExtent_ = frameState.extent.slice(); } diff --git a/test/browser/spec/ol/render/webgl/batchrenderer.test.js b/test/browser/spec/ol/render/webgl/batchrenderer.test.js index fa00eee520..6c7a7a629c 100644 --- a/test/browser/spec/ol/render/webgl/batchrenderer.test.js +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -98,12 +98,15 @@ describe('Batch renderers', function () { }); }); describe('#rebuild', function () { + let rebuildCb; beforeEach(function (done) { sinon.spy(helper, 'flushBufferData'); + rebuildCb = sinon.spy(); batchRenderer.rebuild( mixedBatch.pointBatch, SAMPLE_FRAMESTATE, - GeometryType.POINT + GeometryType.POINT, + rebuildCb ); // wait for worker response for our specific message worker.addEventListener('message', function (event) { @@ -134,6 +137,9 @@ describe('Batch renderers', function () { 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; @@ -196,12 +202,15 @@ describe('Batch renderers', function () { }); }); describe('#rebuild', function () { + let rebuildCb; beforeEach(function (done) { sinon.spy(helper, 'flushBufferData'); + rebuildCb = sinon.spy(); batchRenderer.rebuild( mixedBatch.lineStringBatch, SAMPLE_FRAMESTATE, - GeometryType.LINE_STRING + GeometryType.LINE_STRING, + rebuildCb ); // wait for worker response for our specific message worker.addEventListener('message', function (event) { @@ -231,6 +240,9 @@ describe('Batch renderers', function () { ).to.be.greaterThan(0); expect(helper.flushBufferData.calledTwice).to.be(true); }); + it('calls the provided callback', function () { + expect(rebuildCb.calledOnce).to.be(true); + }); }); }); @@ -253,12 +265,15 @@ describe('Batch renderers', function () { }); }); describe('#rebuild', function () { + let rebuildCb; beforeEach(function (done) { sinon.spy(helper, 'flushBufferData'); + rebuildCb = sinon.spy(); batchRenderer.rebuild( mixedBatch.polygonBatch, SAMPLE_FRAMESTATE, - GeometryType.POLYGON + GeometryType.POLYGON, + rebuildCb ); // wait for worker response for our specific message worker.addEventListener('message', function (event) { @@ -285,6 +300,9 @@ describe('Batch renderers', function () { ).to.be.greaterThan(0); expect(helper.flushBufferData.calledTwice).to.be(true); }); + it('calls the provided callback', function () { + expect(rebuildCb.calledOnce).to.be(true); + }); }); }); }); From f603ce7456ab5f69771dd365badea1e98da8a4a6 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Tue, 7 Jun 2022 21:43:01 +0200 Subject: [PATCH 15/27] WebGL / Support DPR > 1 in linestring shader A u_pixelRatio uniform was added to be used in the shaders. this is necessary since we're relying on the builtin gl_FragCoord vector, which will be scaled relative to the u_sizePx uniform in case of a device pixel ratio != 1. Also added tests for computed uniform values, instead of just testing that they were indeed set. --- examples/webgl-vector-layer.js | 4 +- src/ol/webgl/Helper.js | 3 ++ test/browser/spec/ol/webgl/helper.test.js | 57 +++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index 3b8672d826..320faecaac 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -100,6 +100,7 @@ class WebGLLayer extends Layer { }`, lineStringFragmentShader: ` precision mediump float; + uniform float u_pixelRatio; varying vec2 v_segmentStart; varying vec2 v_segmentEnd; varying float v_angleStart; @@ -118,8 +119,9 @@ class WebGLLayer extends Layer { float lineWidth = 1.5; void main(void) { + vec2 v_currentPoint = gl_FragCoord.xy / u_pixelRatio; gl_FragColor = vec4(v_color.rgb * 0.75, 1.0); - gl_FragColor *= segmentDistanceField(gl_FragCoord.xy, v_segmentStart, v_segmentEnd, lineWidth); + gl_FragColor *= segmentDistanceField(v_currentPoint, v_segmentStart, v_segmentEnd, lineWidth); }`, pointVertexShader: ` precision mediump float; diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 032391ad74..9167fcd02e 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -50,6 +50,7 @@ export const DefaultUniform = { ZOOM: 'u_zoom', RESOLUTION: 'u_resolution', SIZE_PX: 'u_sizePx', + PIXEL_RATIO: 'u_pixelRatio', }; /** @@ -666,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]); @@ -693,6 +695,7 @@ class WebGLHelper extends Disposable { DefaultUniform.RESOLUTION, frameState.viewState.resolution ); + this.setUniformFloatValue(DefaultUniform.PIXEL_RATIO, pixelRatio); this.setUniformFloatVec2(DefaultUniform.SIZE_PX, [size[0], size[1]]); } diff --git a/test/browser/spec/ol/webgl/helper.test.js b/test/browser/spec/ol/webgl/helper.test.js index 67fcef2fc4..f44527a8e8 100644 --- a/test/browser/spec/ol/webgl/helper.test.js +++ b/test/browser/spec/ol/webgl/helper.test.js @@ -63,6 +63,7 @@ const SAMPLE_FRAMESTATE = { rotation: 0.4, resolution: 2, center: [10, 20], + zoom: 3, }, }; @@ -154,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 () { @@ -413,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], + ]); + }); + }); }); From 6848df97f89d88a66a3733c3c829b1474e0f0c8f Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 8 Jun 2022 10:28:00 +0200 Subject: [PATCH 16/27] WebGL / Address review comments --- src/ol/render/webgl/BatchRenderer.js | 10 ++++------ src/ol/render/webgl/LineStringBatchRenderer.js | 2 +- src/ol/render/webgl/PointBatchRenderer.js | 2 +- src/ol/render/webgl/PolygonBatchRenderer.js | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index c41d030cd0..29c4f4e9d4 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -81,7 +81,7 @@ class AbstractBatchRenderer { frameState, createTransform() ); - this.generateRenderInstructions_(batch); + this.generateRenderInstructions(batch); this.generateBuffers_(batch, geometryType, callback); } @@ -115,7 +115,7 @@ class AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @protected */ - generateRenderInstructions_(batch) { + generateRenderInstructions(batch) { abstract(); } @@ -125,7 +125,7 @@ class AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type * @param {function(): void} callback Function called once the render buffers are updated - * @protected + * @private */ generateBuffers_(batch, geometryType, callback) { const messageId = workerMessageCounter++; @@ -153,8 +153,6 @@ class AbstractBatchRenderer { renderInstructionsTransform: batch.renderInstructionsTransform, customAttributesCount: this.customAttributes_.length, }; - // additional properties will be sent back as-is by the worker - message['projectionTransform'] = batch.renderInstructionsTransform; this.worker_.postMessage(message, [batch.renderInstructions.buffer]); // leave ownership of render instructions @@ -177,7 +175,7 @@ class AbstractBatchRenderer { this.worker_.removeEventListener('message', handleMessage); // store transform & invert transform for webgl buffers - batch.verticesBufferTransform = received.projectionTransform; + batch.verticesBufferTransform = received.renderInstructionsTransform; makeInverseTransform( batch.invertVerticesBufferTransform, batch.verticesBufferTransform diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js index 3c5e1509d6..08fc5a1775 100644 --- a/src/ol/render/webgl/LineStringBatchRenderer.js +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -50,7 +50,7 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").LineStringGeometryBatch} batch Linestring geometry batch * @override */ - generateRenderInstructions_(batch) { + 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 diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js index 09ca001250..b2fef90696 100644 --- a/src/ol/render/webgl/PointBatchRenderer.js +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -46,7 +46,7 @@ class PointBatchRenderer extends AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch Point geometry batch * @override */ - generateRenderInstructions_(batch) { + 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 diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js index 897b94ece3..d295d9139a 100644 --- a/src/ol/render/webgl/PolygonBatchRenderer.js +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -40,7 +40,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { * @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch Polygon geometry batch * @override */ - generateRenderInstructions_(batch) { + 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 From 79c974d63d2b4bdad1857768dbaa5d2ce7cb8438 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 9 Jun 2022 13:04:37 +0200 Subject: [PATCH 17/27] WebGL / Add new module with default shaders for VectorLayer --- src/ol/renderer/webgl/VectorLayer.js | 1 + src/ol/renderer/webgl/shaders.js | 213 +++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 src/ol/renderer/webgl/shaders.js diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index ba8dcd42c5..0165264cf4 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -15,6 +15,7 @@ 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'; +import './shaders.js'; // this is to make sure that default shaders are part of the bundle /** * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different diff --git a/src/ol/renderer/webgl/shaders.js b/src/ol/renderer/webgl/shaders.js new file mode 100644 index 0000000000..8acea1fd19 --- /dev/null +++ b/src/ol/renderer/webgl/shaders.js @@ -0,0 +1,213 @@ +/** + * @module ol/renderer/webgl/shaders + */ +import {asArray} from '../../color.js'; + +/** + * Attribute names used in the default shaders. + * @enum {string} + */ +export const DefaultAttributes = { + COLOR: 'color', + OPACITY: 'opacity', + WIDTH: 'width', +}; + +/** + * Packs red/green/blue channels of a color into a single float value; alpha is ignored. + * This is how DefaultAttributes.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 + * @api + */ +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 DefaultAttributes.COLOR and DefaultAttributes.OPACITY. + * @type {string} + * @api + */ +export const DEFAULT_POLYGON_VERTEX = ` + 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} + * @api + */ +export const DEFAULT_POLYGON_FRAGMENT = ` + 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 DefaultAttributes.COLOR, DefaultAttributes.OPACITY and DefaultAttributes.WIDTH. + * @type {string} + * @api + */ +export const DEFAULT_LINESTRING_VERTEX = ` + 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} + * @api + */ +export const DEFAULT_LINESTRING_FRAGMENT = ` + 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 DefaultAttributes.COLOR and DefaultAttributes.OPACITY. + * @type {string} + * @api + */ +export const DEFAULT_POINT_VERTEX = ` + 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} + * @api + */ +export const DEFAULT_POINT_FRAGMENT = ` + precision mediump float; + varying vec3 v_color; + varying float v_opacity; + + void main(void) { + gl_FragColor = vec4(v_color, 1.0) * v_opacity; + }`; From 52279967c4a744e4faf8018d1adecdd4ab097d0e Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 9 Jun 2022 13:06:08 +0200 Subject: [PATCH 18/27] WebGL / Reorganize VectorLayerRenderer options, update example Now different attributes can be provided for each type of geometry. Also updated the example to accomodate for this and use the default shaders. --- examples/webgl-vector-layer.js | 172 +++++---------------------- src/ol/renderer/webgl/VectorLayer.js | 117 +++++++++++++----- 2 files changed, 114 insertions(+), 175 deletions(-) diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index 320faecaac..c1fee56403 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -6,159 +6,43 @@ 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 { + DefaultAttributes, + packColor, +} from '../src/ol/renderer/webgl/shaders.js'; import {asArray} from '../src/ol/color.js'; class WebGLLayer extends Layer { createRenderer() { return new WebGLVectorLayerRenderer(this, { className: this.getClassName(), - polygonVertexShader: ` - precision mediump float; - uniform mat4 u_projectionMatrix; - attribute vec2 a_position; - attribute float a_color; - - varying vec3 v_color; - - void main(void) { - gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0); - v_color = vec3( - floor(a_color / 256.0 / 256.0) / 256.0, - fract(floor(a_color / 256.0) / 256.0), - fract(a_color / 256.0) - ); - }`, - polygonFragmentShader: ` - precision mediump float; - varying vec3 v_color; - - void main(void) { - gl_FragColor = vec4(v_color.rgb, 1.0); - gl_FragColor *= 0.75; - }`, - lineStringVertexShader: ` - 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; - varying vec2 v_segmentStart; - varying vec2 v_segmentEnd; - varying float v_angleStart; - varying float v_angleEnd; - - varying vec3 v_color; - - 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; - } - - float lineWidth = 2.0; - - 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) * lineWidth * 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 = vec3( - floor(a_color / 256.0 / 256.0) / 256.0, - fract(floor(a_color / 256.0) / 256.0), - fract(a_color / 256.0) - ); - }`, - lineStringFragmentShader: ` - 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; - - 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); - } - - float lineWidth = 1.5; - - void main(void) { - vec2 v_currentPoint = gl_FragCoord.xy / u_pixelRatio; - gl_FragColor = vec4(v_color.rgb * 0.75, 1.0); - gl_FragColor *= segmentDistanceField(v_currentPoint, v_segmentStart, v_segmentEnd, lineWidth); - }`, - pointVertexShader: ` - precision mediump float; - uniform mat4 u_projectionMatrix; - uniform mat4 u_offsetScaleMatrix; - attribute vec2 a_position; - attribute float a_index; - varying vec2 v_texCoord; - - 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); - }`, - pointFragmentShader: ` - precision mediump float; - varying vec2 v_texCoord; - - void main(void) { - gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); - }`, - attributes: [ - { - name: 'color', - callback: function (feature, properties) { + polygonShader: { + attributes: { + [DefaultAttributes.COLOR]: function (feature, properties) { const color = asArray(properties.COLOR || '#eee'); - // RGB components are encoded into one value - return color[0] * 256 * 256 + color[1] * 256 + color[2]; + color[3] = 0.85; + return packColor(color); + }, + [DefaultAttributes.OPACITY]: function () { + return 0.6; }, }, - ], + }, + lineStringShader: { + attributes: { + [DefaultAttributes.COLOR]: function (feature, properties) { + const color = [...asArray(properties.COLOR || '#eee')]; + color.forEach((_, i) => (color[i] = Math.round(color[i] * 0.75))); // darken slightly + return packColor(color); + }, + [DefaultAttributes.WIDTH]: function () { + return 1.5; + }, + [DefaultAttributes.OPACITY]: function () { + return 1; + }, + }, + }, }); } } diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 0165264cf4..934fefeb68 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -10,39 +10,44 @@ import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js'; import VectorEventType from '../../source/VectorEventType.js'; import ViewHint from '../../ViewHint.js'; import WebGLLayerRenderer from './Layer.js'; +import { + DEFAULT_LINESTRING_FRAGMENT, + DEFAULT_LINESTRING_VERTEX, + DEFAULT_POINT_FRAGMENT, + DEFAULT_POINT_VERTEX, + DEFAULT_POLYGON_FRAGMENT, + DEFAULT_POLYGON_VERTEX, + DefaultAttributes, + packColor, +} from './shaders.js'; import {DefaultUniform} from '../../webgl/Helper.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'; -import './shaders.js'; // this is to make sure that default shaders are part of the bundle /** - * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different - * for each feature. - * @property {string} name Attribute name. - * @property {function(import("../../Feature").default, Object):number} callback This callback computes the numerical value of the - * attribute for a given feature (properties are available as 2nd arg for quicker access). + * @typedef {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 {Array} [attributes] These attributes will be read from the features in the source - * and then passed to the GPU. The `name` property of each attribute will serve as its identifier: - * * In the vertex shader as an `attribute` by prefixing it with `a_` - * * In the fragment shader as a `varying` by prefixing it with `v_` - * Please note that these can only be numerical values. - * @property {string} polygonVertexShader Vertex shader source, mandatory. - * @property {string} polygonFragmentShader Fragment shader source, mandatory. - * @property {string} lineStringVertexShader Vertex shader source, mandatory. - * @property {string} lineStringFragmentShader Fragment shader source, mandatory. - * @property {string} pointVertexShader Vertex shader source, mandatory. - * @property {string} pointFragmentShader Fragment shader source, mandatory. - * @property {string} [hitVertexShader] Vertex shader source for hit detection rendering. - * @property {string} [hitFragmentShader] Fragment shader source for hit detection rendering. - * @property {Object} [uniforms] Uniform definitions for the post process steps - * Please note that `u_texture` is reserved for the main texture slot. + * @property {ShaderProgram} [polygonShader] Vertex shaders for polygons; using default shader if unspecified + * @property {ShaderProgram} [lineStringShader] Vertex shaders for line strings; using default shader if unspecified + * @property {ShaderProgram} [pointShader] Vertex shaders for points; using default shader if unspecified + * @property {Object} [uniforms] Uniform definitions. * @property {Array} [postProcesses] Post-processes definitions */ @@ -119,13 +124,63 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { */ this.currentTransform_ = projectionMatrixTransform; - this.polygonVertexShader_ = options.polygonVertexShader; - this.polygonFragmentShader_ = options.polygonFragmentShader; - this.pointVertexShader_ = options.pointVertexShader; - this.pointFragmentShader_ = options.pointFragmentShader; - this.lineStringVertexShader_ = options.lineStringVertexShader; - this.lineStringFragmentShader_ = options.lineStringFragmentShader; - this.attributes_ = options.attributes; + const polygonAttributesWithDefault = { + [DefaultAttributes.COLOR]: function () { + return packColor('#ddd'); + }, + [DefaultAttributes.OPACITY]: function () { + return 1; + }, + ...(options.polygonShader && options.polygonShader.attributes), + }; + const lineAttributesWithDefault = { + [DefaultAttributes.COLOR]: function () { + return packColor('#eee'); + }, + [DefaultAttributes.OPACITY]: function () { + return 1; + }, + [DefaultAttributes.WIDTH]: function () { + return 1.5; + }, + ...(options.lineStringShader && options.lineStringShader.attributes), + }; + const pointAttributesWithDefault = { + [DefaultAttributes.COLOR]: function () { + return packColor('#eee'); + }, + [DefaultAttributes.OPACITY]: function () { + return 1; + }, + ...(options.pointShader && options.pointShader.attributes), + }; + function toAttributesArray(obj) { + return Object.keys(obj).map((key) => ({name: key, callback: obj[key]})); + } + + this.polygonVertexShader_ = + (options.polygonShader && options.polygonShader.vertexShader) || + DEFAULT_POLYGON_VERTEX; + this.polygonFragmentShader_ = + (options.polygonShader && options.polygonShader.fragmentShader) || + DEFAULT_POLYGON_FRAGMENT; + this.polygonAttributes_ = toAttributesArray(polygonAttributesWithDefault); + + this.lineStringVertexShader_ = + (options.lineStringShader && options.lineStringShader.vertexShader) || + DEFAULT_LINESTRING_VERTEX; + this.lineStringFragmentShader_ = + (options.lineStringShader && options.lineStringShader.fragmentShader) || + DEFAULT_LINESTRING_FRAGMENT; + this.lineStringAttributes_ = toAttributesArray(lineAttributesWithDefault); + + this.pointVertexShader_ = + (options.pointShader && options.pointShader.vertexShader) || + DEFAULT_POINT_VERTEX; + this.pointFragmentShader_ = + (options.pointShader && options.pointShader.fragmentShader) || + DEFAULT_POINT_FRAGMENT; + this.pointAttributes_ = toAttributesArray(pointAttributesWithDefault); this.worker_ = createWebGLWorker(); @@ -167,21 +222,21 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { this.worker_, this.polygonVertexShader_, this.polygonFragmentShader_, - this.attributes_ || [] + this.polygonAttributes_ ); this.pointRenderer_ = new PointBatchRenderer( this.helper, this.worker_, this.pointVertexShader_, this.pointFragmentShader_, - this.attributes_ || [] + this.pointAttributes_ ); this.lineStringRenderer_ = new LineStringBatchRenderer( this.helper, this.worker_, this.lineStringVertexShader_, this.lineStringFragmentShader_, - this.attributes_ || [] + this.lineStringAttributes_ ); } From cd83424867f054c34e6f8c6bd1b2960567b92373 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 9 Jun 2022 13:28:50 +0200 Subject: [PATCH 19/27] WebGL / improve doc for Helper and VectorLayerRenderer Also created enums for attributes (like uniforms), in an attempt to clarify what is accessible to the vertex shaders. --- .../render/webgl/LineStringBatchRenderer.js | 17 +++++-- src/ol/render/webgl/PointBatchRenderer.js | 14 ++++- src/ol/render/webgl/PolygonBatchRenderer.js | 11 +++- src/ol/renderer/webgl/VectorLayer.js | 51 +++++-------------- src/ol/webgl/Helper.js | 4 +- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js index 08fc5a1775..1ec065ade9 100644 --- a/src/ol/render/webgl/LineStringBatchRenderer.js +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -5,6 +5,17 @@ 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 @@ -19,17 +30,17 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { // vertices for lines must hold both a position (x,y) and an offset (dx,dy) this.attributes_ = [ { - name: 'a_segmentStart', + name: Attributes.SEGMENT_START, size: 2, type: AttributeType.FLOAT, }, { - name: 'a_segmentEnd', + name: Attributes.SEGMENT_END, size: 2, type: AttributeType.FLOAT, }, { - name: 'a_parameters', + name: Attributes.PARAMETERS, size: 1, type: AttributeType.FLOAT, }, diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js index b2fef90696..3565989770 100644 --- a/src/ol/render/webgl/PointBatchRenderer.js +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -6,6 +6,16 @@ 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 @@ -20,12 +30,12 @@ class PointBatchRenderer extends AbstractBatchRenderer { // vertices for point must hold both a position (x,y) and an index (their position in the quad) this.attributes_ = [ { - name: 'a_position', + name: Attributes.POSITION, size: 2, type: AttributeType.FLOAT, }, { - name: 'a_index', + name: Attributes.INDEX, size: 1, type: AttributeType.FLOAT, }, diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js index d295d9139a..15a4378ccb 100644 --- a/src/ol/render/webgl/PolygonBatchRenderer.js +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -5,6 +5,15 @@ 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 @@ -19,7 +28,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { // By default only a position attribute is required to render polygons this.attributes_ = [ { - name: 'a_position', + name: Attributes.POSITION, size: 2, type: AttributeType.FLOAT, }, diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 934fefeb68..de155b8172 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -53,48 +53,23 @@ import {listen, unlistenByKey} from '../../events.js'; /** * @classdesc - * Experimental WebGL vector renderer. Supports polygons and lines. + * 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 for rendering. This can be done using - * {@link module:ol/webgl/ShaderBuilder} utilities. + * 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} * - * To include variable attributes in the shaders, you need to declare them using the `attributes` property of - * the options object like so: - * ```js - * new WebGLPointsLayerRenderer(layer, { - * attributes: [ - * { - * name: 'size', - * callback: function(feature) { - * // compute something with the feature - * } - * }, - * { - * name: 'weight', - * callback: function(feature) { - * // compute something with the feature - * } - * }, - * ], - * vertexShader: - * // shader using attribute a_weight and a_size - * fragmentShader: - * // shader using varying v_weight and v_size - * ``` + * Please note that the fragment shaders output should have premultiplied alpha, otherwise visual anomalies may occur. * - * To enable hit detection, you must as well provide dedicated shaders using the `hitVertexShader` - * and `hitFragmentShader` properties. These shall expect the `a_hitColor` attribute to contain - * the final color that will have to be output for hit detection to work. + * Note: this uses {@link module:ol/webgl/Helper~WebGLHelper} internally. * - * The following uniform is used for the main texture: `u_texture`. - * - * Please note that the main shader output should have premultiplied alpha, otherwise visual anomalies may occur. - * - * Polygons are broken down into triangles using the @mapbox/earcut package. - * Lines are rendered into strips of quads. - * - * - * This uses {@link module:ol/webgl/Helper~WebGLHelper} internally. + * @api */ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { /** diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 9167fcd02e..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 = { From 5d21e548ae42a1ed8b93ec3f756a53b0e8c06f4f Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Fri, 17 Jun 2022 09:39:29 +0200 Subject: [PATCH 20/27] Webgl / add rendering tests for the vector layer renderer --- .../rendering/cases/webgl-vector/expected.png | Bin 0 -> 69763 bytes test/rendering/cases/webgl-vector/main.js | 44 ++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 test/rendering/cases/webgl-vector/expected.png create mode 100644 test/rendering/cases/webgl-vector/main.js diff --git a/test/rendering/cases/webgl-vector/expected.png b/test/rendering/cases/webgl-vector/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ccfa1d381d19a3c71cd85703bd18f8c79962366a GIT binary patch literal 69763 zcmXt9WmFv95?p+P%i;ug2=4Cg!6CQ=2`<6iHE3|R;1FDcy9RfM;O@I`zaMY@?Cd!+ zXXec9zTH(_6{V&ki-t^u3;+Po1DGUIJ2>mg1DYyFXpR0zf1fXV$-7-vCmJjXHh%umy0`1_tnag@31#fvEc5fXe~FVVhS+#k$f*Vsu?CZD zKUs8QU%e+}qxVispf0W=4BK%Lg_`QVZP+%+&HsK91i}st4Yi);zAnF7Mz|hrJaV>8 zF#@SKek0A9!PrqRC`A7L2ENPN>enBin3xEj_9-vcuEZK<2OJsS2&=qYwdrC}!UqoO z#<3%sGs_Uqag}ISlh++rMSuwDdJgE(Hv{r^f1W#}=NR8%H~UT;8a|yg$~VS-7P+HP z@qb;z_XP)Jzk5x+zdRn7;1zLeSOcqJVp3p({t(4266%KZ2G;|b7xkt5nL-6kV`DKT z$U~*%K1h%k11L65i;LGzDT~DrqnQ;y>?N|Ho_J(hjFM~#9dVv@_M^LXd;u_~A_L~< z=f{myd(dOqC#I(2eJB6^1+lft^syDseg!zKee`Ttp(VzzUDBU-XsOU-93;xJ<0f>B z{lxchcgJN)U{*~t-e5WXxRd7Hr2>*3+}dzRS1G%#^UEaxWAwDvJ{_=*HpKRT-+4A6 zE36G$#45$B1Z6APvOxK{(>aHxpIJ6@-{^i@Wl4#Tysa}&`~Q0Y6x#@BJj0}D0N(D7 zdI0nerRnb$9; z5d4;l9-}ki-DUJ01(BNkW#K?ZN1P>{8fhtsa zqqp;BkePBdlS&4n`6g1H}SjN&j0pU^S1cloT$%!044ug#pNS_ID? zU-nwBI?^emBgF#Qb?xeg%-(BIfID~@?1&{Go3!if4G(dJsKrc&mhts(>T;$d;gNNfcFIL)n?;M#T_WV0&5QGJN8o%Pc znmfHmt~GD@0txf%hP>PVK7YvmzWUl`eQzsr{XFgZuFR`msa?Ow!OI)5n7Oi|2VE3X zF|eR~nk6(9I5;_P>%1F`5K+Ze?g5c~iTw{g6fK)h!o;=u>CYLjKX?vm)*vzjy85n_ zUK=owv4m;C)GZ?ybWzAZy64#y>xOe2QjEJVf7A<`5VxV39JkcDLtoVtYAV;H|B3ls zPz3_SspJtGVy`ZiHA6L}k&zQmE74_6wZ)~tRX5y8n*w{F@8xB^vf=r1dd(%*#@Yu8*Cib1@Dpc$WqEH}*u$=|r z2FRkG5!>J{|BJW8X2{*t(b7@qKrXG@>bOWo2de zeNlh){Go93Bmjxi!@`Hl2OG=EyOPm0^fV#Ixhd^M^15a;jLu{k7*w@9YL|8op;4yY|^!mdu~K?0B+J zjHi0(cGeTP`@L~jE!o*$FHb&il%zW!kwFH;UoVHH(|zGgiYa`hvO$QwmqbSG8xE#4 zWRE{(ofkY=&8Dr-23{%`O|87SJ-7UjrZ>OQ6{irGBd1$hS%ou?|IEuXb`h5hC^)NM z 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%0D_fI&Yw} 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 FV1jpqGcN!D literal 0 HcmV?d00001 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', +}); From 998dc82592e7f423eba04b120b00029a126ad944 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Fri, 15 Jul 2022 22:49:54 +0200 Subject: [PATCH 21/27] fixes after rebase --- src/ol/render/webgl/BatchRenderer.js | 11 +++++------ src/ol/render/webgl/MixedGeometryBatch.js | 17 ++++++++--------- src/ol/renderer/webgl/VectorLayer.js | 7 +++---- .../spec/ol/render/webgl/batchrenderer.test.js | 7 +++---- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index 29c4f4e9d4..e8554757e8 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -1,7 +1,6 @@ /** * @module ol/render/webgl/BatchRenderer */ -import GeometryType from '../../geom/GeometryType.js'; import {WebGLWorkerMessageType} from './constants.js'; import {abstract} from '../../util.js'; import { @@ -72,7 +71,7 @@ class AbstractBatchRenderer { * Note: this is a costly operation. * @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch * @param {import("../../PluggableMap").FrameState} frameState Frame state. - * @param {import("../../geom/GeometryType.js").default} geometryType Geometry type + * @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) { @@ -123,7 +122,7 @@ class AbstractBatchRenderer { * 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/GeometryType.js").default} geometryType Geometry type + * @param {import("../../geom/Geometry.js").Type} geometryType Geometry type * @param {function(): void} callback Function called once the render buffers are updated * @private */ @@ -132,13 +131,13 @@ class AbstractBatchRenderer { let messageType; switch (geometryType) { - case GeometryType.POLYGON: + case 'Polygon': messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS; break; - case GeometryType.POINT: + case 'Point': messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS; break; - case GeometryType.LINE_STRING: + case 'LineString': messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS; break; default: diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js index dc144bf26e..a6b21013fc 100644 --- a/src/ol/render/webgl/MixedGeometryBatch.js +++ b/src/ol/render/webgl/MixedGeometryBatch.js @@ -1,7 +1,6 @@ /** * @module ol/render/webgl/MixedGeometryBatch */ -import GeometryType from '../../geom/GeometryType.js'; import WebGLArrayBuffer from '../../webgl/Buffer.js'; import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js'; import {create as createTransform} from '../../transform.js'; @@ -261,27 +260,27 @@ class MixedGeometryBatch { let verticesCount; let batchEntry; switch (type) { - case GeometryType.GEOMETRY_COLLECTION: + case 'GeometryCollection': /** @type {import("../../geom").GeometryCollection} */ (geometry) .getGeometries() .map((geom) => this.addGeometry_(geom, feature)); break; - case GeometryType.MULTI_POLYGON: + case 'MultiPolygon': /** @type {import("../../geom").MultiPolygon} */ (geometry) .getPolygons() .map((polygon) => this.addGeometry_(polygon, feature)); break; - case GeometryType.MULTI_LINE_STRING: + case 'MultiLineString': /** @type {import("../../geom").MultiLineString} */ (geometry) .getLineStrings() .map((line) => this.addGeometry_(line, feature)); break; - case GeometryType.MULTI_POINT: + case 'MultiPoint': /** @type {import("../../geom").MultiPoint} */ (geometry) .getPoints() .map((point) => this.addGeometry_(point, feature)); break; - case GeometryType.POLYGON: + case 'Polygon': const polygonGeom = /** @type {import("../../geom").Polygon} */ ( geometry ); @@ -305,15 +304,15 @@ class MixedGeometryBatch { .getLinearRings() .map((ring) => this.addGeometry_(ring, feature)); break; - case GeometryType.POINT: + 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 GeometryType.LINE_STRING: - case GeometryType.LINEAR_RING: + case 'LineString': + case 'LinearRing': const lineGeom = /** @type {import("../../geom").LineString} */ ( geometry ); diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index de155b8172..635db2f67d 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -2,7 +2,6 @@ * @module ol/renderer/webgl/VectorLayer */ import BaseVector from '../../layer/BaseVector.js'; -import GeometryType from '../../geom/GeometryType.js'; import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js'; import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js'; import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js'; @@ -345,19 +344,19 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { this.polygonRenderer_.rebuild( this.batch_.polygonBatch, frameState, - GeometryType.POLYGON, + 'Polygon', rebuildCb ); this.lineStringRenderer_.rebuild( this.batch_.lineStringBatch, frameState, - GeometryType.LINE_STRING, + 'LineString', rebuildCb ); this.pointRenderer_.rebuild( this.batch_.pointBatch, frameState, - GeometryType.POINT, + 'Point', rebuildCb ); this.previousExtent_ = frameState.extent.slice(); diff --git a/test/browser/spec/ol/render/webgl/batchrenderer.test.js b/test/browser/spec/ol/render/webgl/batchrenderer.test.js index 6c7a7a629c..08050c091d 100644 --- a/test/browser/spec/ol/render/webgl/batchrenderer.test.js +++ b/test/browser/spec/ol/render/webgl/batchrenderer.test.js @@ -1,5 +1,4 @@ import Feature from '../../../../../../src/ol/Feature.js'; -import GeometryType from '../../../../../../src/ol/geom/GeometryType.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'; @@ -105,7 +104,7 @@ describe('Batch renderers', function () { batchRenderer.rebuild( mixedBatch.pointBatch, SAMPLE_FRAMESTATE, - GeometryType.POINT, + 'Point', rebuildCb ); // wait for worker response for our specific message @@ -209,7 +208,7 @@ describe('Batch renderers', function () { batchRenderer.rebuild( mixedBatch.lineStringBatch, SAMPLE_FRAMESTATE, - GeometryType.LINE_STRING, + 'LineString', rebuildCb ); // wait for worker response for our specific message @@ -272,7 +271,7 @@ describe('Batch renderers', function () { batchRenderer.rebuild( mixedBatch.polygonBatch, SAMPLE_FRAMESTATE, - GeometryType.POLYGON, + 'Polygon', rebuildCb ); // wait for worker response for our specific message From 5182b164525ec608f70fad0dee9d275475c34100 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 13:03:10 -0700 Subject: [PATCH 22/27] Private if not used elsewhere; underscore suffix only if private --- src/ol/render/webgl/BatchRenderer.js | 14 +++++++------- src/ol/render/webgl/LineStringBatchRenderer.js | 8 ++++---- src/ol/render/webgl/PointBatchRenderer.js | 8 ++++---- src/ol/render/webgl/PolygonBatchRenderer.js | 8 ++++---- src/ol/renderer/webgl/PointsLayer.js | 4 ++++ src/ol/renderer/webgl/VectorLayer.js | 6 ++++++ ...batchrenderer.test.js => BatchRenderer.test.js} | 6 +++--- ...trybatch.test.js => MixedGeometryBatch.test.js} | 0 8 files changed, 32 insertions(+), 22 deletions(-) rename test/browser/spec/ol/render/webgl/{batchrenderer.test.js => BatchRenderer.test.js} (98%) rename test/browser/spec/ol/render/webgl/{mixedgeometrybatch.test.js => MixedGeometryBatch.test.js} (100%) diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index e8554757e8..5745e4c502 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -36,19 +36,19 @@ class AbstractBatchRenderer { constructor(helper, worker, vertexShader, fragmentShader, customAttributes) { /** * @type {import("../../webgl/Helper.js").default} - * @protected + * @private */ this.helper_ = helper; /** * @type {Worker} - * @protected + * @private */ this.worker_ = worker; /** * @type {WebGLProgram} - * @protected + * @private */ this.program_ = this.helper_.getProgram(fragmentShader, vertexShader); @@ -57,13 +57,13 @@ class AbstractBatchRenderer { * @type {Array} * @protected */ - this.attributes_ = []; + this.attributes = []; /** * @type {Array} * @protected */ - this.customAttributes_ = customAttributes; + this.customAttributes = customAttributes; } /** @@ -102,7 +102,7 @@ class AbstractBatchRenderer { this.helper_.useProgram(this.program_, frameState); this.helper_.bindBuffer(batch.verticesBuffer); this.helper_.bindBuffer(batch.indicesBuffer); - this.helper_.enableAttributes(this.attributes_); + this.helper_.enableAttributes(this.attributes); const renderCount = batch.indicesBuffer.getSize(); this.helper_.drawElements(0, renderCount); @@ -150,7 +150,7 @@ class AbstractBatchRenderer { type: messageType, renderInstructions: batch.renderInstructions.buffer, renderInstructionsTransform: batch.renderInstructionsTransform, - customAttributesCount: this.customAttributes_.length, + customAttributesCount: this.customAttributes.length, }; this.worker_.postMessage(message, [batch.renderInstructions.buffer]); diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js index 1ec065ade9..7698e835c7 100644 --- a/src/ol/render/webgl/LineStringBatchRenderer.js +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -28,7 +28,7 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { super(helper, worker, vertexShader, fragmentShader, customAttributes); // vertices for lines must hold both a position (x,y) and an offset (dx,dy) - this.attributes_ = [ + this.attributes = [ { name: Attributes.SEGMENT_START, size: 2, @@ -68,7 +68,7 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { // + 1 instruction per line (for vertices count) const totalInstructionsCount = 2 * batch.verticesCount + - (1 + this.customAttributes_.length) * batch.geometriesCount; + (1 + this.customAttributes.length) * batch.geometriesCount; if ( !batch.renderInstructions || batch.renderInstructions.length !== totalInstructionsCount @@ -95,8 +95,8 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { ); // custom attributes - for (let k = 0, kk = this.customAttributes_.length; k < kk; k++) { - value = this.customAttributes_[k].callback( + for (let k = 0, kk = this.customAttributes.length; k < kk; k++) { + value = this.customAttributes[k].callback( batchEntry.feature, batchEntry.properties ); diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js index 3565989770..d593499f69 100644 --- a/src/ol/render/webgl/PointBatchRenderer.js +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -28,7 +28,7 @@ class PointBatchRenderer extends AbstractBatchRenderer { 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_ = [ + this.attributes = [ { name: Attributes.POSITION, size: 2, @@ -61,7 +61,7 @@ class PointBatchRenderer extends AbstractBatchRenderer { // 2 instructions per vertex for position (x and y) // + 1 instruction per vertex per custom attributes const totalInstructionsCount = - (2 + this.customAttributes_.length) * batch.geometriesCount; + (2 + this.customAttributes.length) * batch.geometriesCount; if ( !batch.renderInstructions || batch.renderInstructions.length !== totalInstructionsCount @@ -85,8 +85,8 @@ class PointBatchRenderer extends AbstractBatchRenderer { batch.renderInstructions[renderIndex++] = tmpCoords[1]; // pushing custom attributes - for (let j = 0, jj = this.customAttributes_.length; j < jj; j++) { - value = this.customAttributes_[j].callback( + for (let j = 0, jj = this.customAttributes.length; j < jj; j++) { + value = this.customAttributes[j].callback( batchEntry.feature, batchEntry.properties ); diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js index 15a4378ccb..b102840e1a 100644 --- a/src/ol/render/webgl/PolygonBatchRenderer.js +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -26,7 +26,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { super(helper, worker, vertexShader, fragmentShader, customAttributes); // By default only a position attribute is required to render polygons - this.attributes_ = [ + this.attributes = [ { name: Attributes.POSITION, size: 2, @@ -57,7 +57,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { // + 1 instruction per ring (for vertices count in ring) const totalInstructionsCount = 2 * batch.verticesCount + - (1 + this.customAttributes_.length) * batch.geometriesCount + + (1 + this.customAttributes.length) * batch.geometriesCount + batch.ringsCount; if ( !batch.renderInstructions || @@ -85,8 +85,8 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { ); // custom attributes - for (let k = 0, kk = this.customAttributes_.length; k < kk; k++) { - value = this.customAttributes_[k].callback( + for (let k = 0, kk = this.customAttributes.length; k < kk; k++) { + value = this.customAttributes[k].callback( batchEntry.feature, batchEntry.properties ); diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 3553d6121f..c7eca3db00 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -292,7 +292,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { */ this.generateBuffersRun_ = 0; + /** + * @private + */ this.worker_ = createWebGLWorker(); + this.worker_.addEventListener( 'message', /** diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 635db2f67d..f6704b04b9 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -156,8 +156,14 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { DEFAULT_POINT_FRAGMENT; this.pointAttributes_ = toAttributesArray(pointAttributesWithDefault); + /** + * @private + */ this.worker_ = createWebGLWorker(); + /** + * @private + */ this.batch_ = new MixedGeometryBatch(); const source = this.getLayer().getSource(); diff --git a/test/browser/spec/ol/render/webgl/batchrenderer.test.js b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js similarity index 98% rename from test/browser/spec/ol/render/webgl/batchrenderer.test.js rename to test/browser/spec/ol/render/webgl/BatchRenderer.test.js index 08050c091d..f4bdfdaa57 100644 --- a/test/browser/spec/ol/render/webgl/batchrenderer.test.js +++ b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js @@ -89,7 +89,7 @@ describe('Batch renderers', function () { }); describe('constructor', function () { it('generates the attributes list', function () { - expect(batchRenderer.attributes_).to.eql([ + 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}, @@ -192,7 +192,7 @@ describe('Batch renderers', function () { }); describe('constructor', function () { it('generates the attributes list', function () { - expect(batchRenderer.attributes_).to.eql([ + 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}, @@ -257,7 +257,7 @@ describe('Batch renderers', function () { }); describe('constructor', function () { it('generates the attributes list', function () { - expect(batchRenderer.attributes_).to.eql([ + expect(batchRenderer.attributes).to.eql([ {name: 'a_position', size: 2, type: FLOAT}, {name: 'a_test', size: 1, type: FLOAT}, ]); diff --git a/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js b/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js similarity index 100% rename from test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js rename to test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js From f21dd84c91e895f2f1d7a23fc9c40806067b267f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 13:08:18 -0700 Subject: [PATCH 23/27] Remove api annotation for experimental features --- src/ol/renderer/webgl/VectorLayer.js | 2 -- src/ol/renderer/webgl/shaders.js | 7 ------- 2 files changed, 9 deletions(-) diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index f6704b04b9..2ce81e126a 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -67,8 +67,6 @@ import {listen, unlistenByKey} from '../../events.js'; * 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. - * - * @api */ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { /** diff --git a/src/ol/renderer/webgl/shaders.js b/src/ol/renderer/webgl/shaders.js index 8acea1fd19..f8150cb379 100644 --- a/src/ol/renderer/webgl/shaders.js +++ b/src/ol/renderer/webgl/shaders.js @@ -18,7 +18,6 @@ export const DefaultAttributes = { * This is how DefaultAttributes.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 - * @api */ export function packColor(color) { const array = asArray(color); @@ -38,7 +37,6 @@ const DECODE_COLOR_EXPRESSION = `vec3( * Default polygon vertex shader. * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. * @type {string} - * @api */ export const DEFAULT_POLYGON_VERTEX = ` precision mediump float; @@ -58,7 +56,6 @@ export const DEFAULT_POLYGON_VERTEX = ` /** * Default polygon fragment shader. * @type {string} - * @api */ export const DEFAULT_POLYGON_FRAGMENT = ` precision mediump float; @@ -73,7 +70,6 @@ export const DEFAULT_POLYGON_FRAGMENT = ` * Default linestring vertex shader. * Relies on DefaultAttributes.COLOR, DefaultAttributes.OPACITY and DefaultAttributes.WIDTH. * @type {string} - * @api */ export const DEFAULT_LINESTRING_VERTEX = ` precision mediump float; @@ -139,7 +135,6 @@ export const DEFAULT_LINESTRING_VERTEX = ` /** * Default linestring fragment shader. * @type {string} - * @api */ export const DEFAULT_LINESTRING_FRAGMENT = ` precision mediump float; @@ -170,7 +165,6 @@ export const DEFAULT_LINESTRING_FRAGMENT = ` * Default point vertex shader. * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. * @type {string} - * @api */ export const DEFAULT_POINT_VERTEX = ` precision mediump float; @@ -201,7 +195,6 @@ export const DEFAULT_POINT_VERTEX = ` /** * Default point fragment shader. * @type {string} - * @api */ export const DEFAULT_POINT_FRAGMENT = ` precision mediump float; From bd9e73a5340a7a30bc1ba7016f951a8b32195f0f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 13:30:22 -0700 Subject: [PATCH 24/27] Renaming fill and stroke attributes and shaders --- examples/webgl-vector-layer.js | 4 +- src/ol/renderer/webgl/VectorLayer.js | 78 ++++++++++++++-------------- src/ol/renderer/webgl/shaders.js | 12 ++--- 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index c1fee56403..e7e60bc762 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -16,7 +16,7 @@ class WebGLLayer extends Layer { createRenderer() { return new WebGLVectorLayerRenderer(this, { className: this.getClassName(), - polygonShader: { + fill: { attributes: { [DefaultAttributes.COLOR]: function (feature, properties) { const color = asArray(properties.COLOR || '#eee'); @@ -28,7 +28,7 @@ class WebGLLayer extends Layer { }, }, }, - lineStringShader: { + stroke: { attributes: { [DefaultAttributes.COLOR]: function (feature, properties) { const color = [...asArray(properties.COLOR || '#eee')]; diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index 2ce81e126a..aa2ff1c25f 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -10,13 +10,13 @@ import VectorEventType from '../../source/VectorEventType.js'; import ViewHint from '../../ViewHint.js'; import WebGLLayerRenderer from './Layer.js'; import { - DEFAULT_LINESTRING_FRAGMENT, - DEFAULT_LINESTRING_VERTEX, - DEFAULT_POINT_FRAGMENT, - DEFAULT_POINT_VERTEX, - DEFAULT_POLYGON_FRAGMENT, - DEFAULT_POLYGON_VERTEX, DefaultAttributes, + FILL_FRAGMENT_SHADER, + FILL_VERTEX_SHADER, + POINT_FRAGMENT_SHADER, + POINT_VERTEX_SHADER, + STROKE_FRAGMENT_SHADER, + STROKE_VERTEX_SHADER, packColor, } from './shaders.js'; import {DefaultUniform} from '../../webgl/Helper.js'; @@ -43,9 +43,9 @@ import {listen, unlistenByKey} from '../../events.js'; /** * @typedef {Object} Options * @property {string} [className='ol-layer'] A CSS class name to set to the canvas element. - * @property {ShaderProgram} [polygonShader] Vertex shaders for polygons; using default shader if unspecified - * @property {ShaderProgram} [lineStringShader] Vertex shaders for line strings; using default shader if unspecified - * @property {ShaderProgram} [pointShader] Vertex shaders for points; using default shader if unspecified + * @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 */ @@ -96,16 +96,17 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { */ this.currentTransform_ = projectionMatrixTransform; - const polygonAttributesWithDefault = { + const fillAttributes = { [DefaultAttributes.COLOR]: function () { return packColor('#ddd'); }, [DefaultAttributes.OPACITY]: function () { return 1; }, - ...(options.polygonShader && options.polygonShader.attributes), + ...(options.fill && options.fill.attributes), }; - const lineAttributesWithDefault = { + + const strokeAttributes = { [DefaultAttributes.COLOR]: function () { return packColor('#eee'); }, @@ -115,44 +116,41 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { [DefaultAttributes.WIDTH]: function () { return 1.5; }, - ...(options.lineStringShader && options.lineStringShader.attributes), + ...(options.stroke && options.stroke.attributes), }; - const pointAttributesWithDefault = { + + const pointAttributes = { [DefaultAttributes.COLOR]: function () { return packColor('#eee'); }, [DefaultAttributes.OPACITY]: function () { return 1; }, - ...(options.pointShader && options.pointShader.attributes), + ...(options.point && options.point.attributes), }; + function toAttributesArray(obj) { return Object.keys(obj).map((key) => ({name: key, callback: obj[key]})); } - this.polygonVertexShader_ = - (options.polygonShader && options.polygonShader.vertexShader) || - DEFAULT_POLYGON_VERTEX; - this.polygonFragmentShader_ = - (options.polygonShader && options.polygonShader.fragmentShader) || - DEFAULT_POLYGON_FRAGMENT; - this.polygonAttributes_ = toAttributesArray(polygonAttributesWithDefault); + 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.lineStringVertexShader_ = - (options.lineStringShader && options.lineStringShader.vertexShader) || - DEFAULT_LINESTRING_VERTEX; - this.lineStringFragmentShader_ = - (options.lineStringShader && options.lineStringShader.fragmentShader) || - DEFAULT_LINESTRING_FRAGMENT; - this.lineStringAttributes_ = toAttributesArray(lineAttributesWithDefault); + 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.pointShader && options.pointShader.vertexShader) || - DEFAULT_POINT_VERTEX; + (options.point && options.point.vertexShader) || POINT_VERTEX_SHADER; this.pointFragmentShader_ = - (options.pointShader && options.pointShader.fragmentShader) || - DEFAULT_POINT_FRAGMENT; - this.pointAttributes_ = toAttributesArray(pointAttributesWithDefault); + (options.point && options.point.fragmentShader) || POINT_FRAGMENT_SHADER; + this.pointAttributes_ = toAttributesArray(pointAttributes); /** * @private @@ -198,9 +196,9 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { this.polygonRenderer_ = new PolygonBatchRenderer( this.helper, this.worker_, - this.polygonVertexShader_, - this.polygonFragmentShader_, - this.polygonAttributes_ + this.fillVertexShader_, + this.fillFragmentShader_, + this.fillAttributes_ ); this.pointRenderer_ = new PointBatchRenderer( this.helper, @@ -212,9 +210,9 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { this.lineStringRenderer_ = new LineStringBatchRenderer( this.helper, this.worker_, - this.lineStringVertexShader_, - this.lineStringFragmentShader_, - this.lineStringAttributes_ + this.strokeVertexShader_, + this.strokeFragmentShader_, + this.strokeAttributes_ ); } diff --git a/src/ol/renderer/webgl/shaders.js b/src/ol/renderer/webgl/shaders.js index f8150cb379..4494cbc74f 100644 --- a/src/ol/renderer/webgl/shaders.js +++ b/src/ol/renderer/webgl/shaders.js @@ -38,7 +38,7 @@ const DECODE_COLOR_EXPRESSION = `vec3( * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. * @type {string} */ -export const DEFAULT_POLYGON_VERTEX = ` +export const FILL_VERTEX_SHADER = ` precision mediump float; uniform mat4 u_projectionMatrix; attribute vec2 a_position; @@ -57,7 +57,7 @@ export const DEFAULT_POLYGON_VERTEX = ` * Default polygon fragment shader. * @type {string} */ -export const DEFAULT_POLYGON_FRAGMENT = ` +export const FILL_FRAGMENT_SHADER = ` precision mediump float; varying vec3 v_color; varying float v_opacity; @@ -71,7 +71,7 @@ export const DEFAULT_POLYGON_FRAGMENT = ` * Relies on DefaultAttributes.COLOR, DefaultAttributes.OPACITY and DefaultAttributes.WIDTH. * @type {string} */ -export const DEFAULT_LINESTRING_VERTEX = ` +export const STROKE_VERTEX_SHADER = ` precision mediump float; uniform mat4 u_projectionMatrix; uniform vec2 u_sizePx; @@ -136,7 +136,7 @@ export const DEFAULT_LINESTRING_VERTEX = ` * Default linestring fragment shader. * @type {string} */ -export const DEFAULT_LINESTRING_FRAGMENT = ` +export const STROKE_FRAGMENT_SHADER = ` precision mediump float; uniform float u_pixelRatio; varying vec2 v_segmentStart; @@ -166,7 +166,7 @@ export const DEFAULT_LINESTRING_FRAGMENT = ` * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. * @type {string} */ -export const DEFAULT_POINT_VERTEX = ` +export const POINT_VERTEX_SHADER = ` precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; @@ -196,7 +196,7 @@ export const DEFAULT_POINT_VERTEX = ` * Default point fragment shader. * @type {string} */ -export const DEFAULT_POINT_FRAGMENT = ` +export const POINT_FRAGMENT_SHADER = ` precision mediump float; varying vec3 v_color; varying float v_opacity; From 7e424be66bb635629f38adb2277879989e648247 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 13:43:53 -0700 Subject: [PATCH 25/27] Only pass the feature to the attribute getter --- examples/webgl-vector-layer.js | 8 +++---- src/ol/render/webgl/BatchRenderer.js | 4 ++-- .../render/webgl/LineStringBatchRenderer.js | 5 +---- src/ol/render/webgl/MixedGeometryBatch.js | 4 ---- src/ol/render/webgl/PointBatchRenderer.js | 5 +---- src/ol/render/webgl/PolygonBatchRenderer.js | 5 +---- .../ol/render/webgl/BatchRenderer.test.js | 2 +- .../render/webgl/MixedGeometryBatch.test.js | 21 +++---------------- 8 files changed, 13 insertions(+), 41 deletions(-) diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index e7e60bc762..68dd7442b1 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -18,8 +18,8 @@ class WebGLLayer extends Layer { className: this.getClassName(), fill: { attributes: { - [DefaultAttributes.COLOR]: function (feature, properties) { - const color = asArray(properties.COLOR || '#eee'); + [DefaultAttributes.COLOR]: function (feature) { + const color = asArray(feature.get('COLOR') || '#eee'); color[3] = 0.85; return packColor(color); }, @@ -30,8 +30,8 @@ class WebGLLayer extends Layer { }, stroke: { attributes: { - [DefaultAttributes.COLOR]: function (feature, properties) { - const color = [...asArray(properties.COLOR || '#eee')]; + [DefaultAttributes.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); }, diff --git a/src/ol/render/webgl/BatchRenderer.js b/src/ol/render/webgl/BatchRenderer.js index 5745e4c502..aae3e73733 100644 --- a/src/ol/render/webgl/BatchRenderer.js +++ b/src/ol/render/webgl/BatchRenderer.js @@ -14,8 +14,8 @@ import { * @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different * for each feature. * @property {string} name Attribute name. - * @property {function(import("../../Feature").default, Object):number} callback This callback computes the numerical value of the - * attribute for a given feature (properties are available as 2nd arg for quicker access). + * @property {function(import("../../Feature").default):number} callback This callback computes the numerical value of the + * attribute for a given feature. */ let workerMessageCounter = 0; diff --git a/src/ol/render/webgl/LineStringBatchRenderer.js b/src/ol/render/webgl/LineStringBatchRenderer.js index 7698e835c7..c4ee9f0bf8 100644 --- a/src/ol/render/webgl/LineStringBatchRenderer.js +++ b/src/ol/render/webgl/LineStringBatchRenderer.js @@ -96,10 +96,7 @@ class LineStringBatchRenderer extends AbstractBatchRenderer { // custom attributes for (let k = 0, kk = this.customAttributes.length; k < kk; k++) { - value = this.customAttributes[k].callback( - batchEntry.feature, - batchEntry.properties - ); + value = this.customAttributes[k].callback(batchEntry.feature); batch.renderInstructions[renderIndex++] = value; } diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js index a6b21013fc..2ae4728901 100644 --- a/src/ol/render/webgl/MixedGeometryBatch.js +++ b/src/ol/render/webgl/MixedGeometryBatch.js @@ -9,7 +9,6 @@ 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 {Object} properties Feature properties * @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 @@ -162,7 +161,6 @@ class MixedGeometryBatch { if (!(uid in this.pointBatch.entries)) { this.pointBatch.entries[uid] = { feature: feature, - properties: feature.getProperties(), flatCoordss: [], }; } @@ -179,7 +177,6 @@ class MixedGeometryBatch { if (!(uid in this.lineStringBatch.entries)) { this.lineStringBatch.entries[uid] = { feature: feature, - properties: feature.getProperties(), flatCoordss: [], verticesCount: 0, }; @@ -197,7 +194,6 @@ class MixedGeometryBatch { if (!(uid in this.polygonBatch.entries)) { this.polygonBatch.entries[uid] = { feature: feature, - properties: feature.getProperties(), flatCoordss: [], verticesCount: 0, ringsCount: 0, diff --git a/src/ol/render/webgl/PointBatchRenderer.js b/src/ol/render/webgl/PointBatchRenderer.js index d593499f69..3c211c3661 100644 --- a/src/ol/render/webgl/PointBatchRenderer.js +++ b/src/ol/render/webgl/PointBatchRenderer.js @@ -86,10 +86,7 @@ class PointBatchRenderer extends AbstractBatchRenderer { // pushing custom attributes for (let j = 0, jj = this.customAttributes.length; j < jj; j++) { - value = this.customAttributes[j].callback( - batchEntry.feature, - batchEntry.properties - ); + value = this.customAttributes[j].callback(batchEntry.feature); batch.renderInstructions[renderIndex++] = value; } } diff --git a/src/ol/render/webgl/PolygonBatchRenderer.js b/src/ol/render/webgl/PolygonBatchRenderer.js index b102840e1a..1c6b262820 100644 --- a/src/ol/render/webgl/PolygonBatchRenderer.js +++ b/src/ol/render/webgl/PolygonBatchRenderer.js @@ -86,10 +86,7 @@ class PolygonBatchRenderer extends AbstractBatchRenderer { // custom attributes for (let k = 0, kk = this.customAttributes.length; k < kk; k++) { - value = this.customAttributes[k].callback( - batchEntry.feature, - batchEntry.properties - ); + value = this.customAttributes[k].callback(batchEntry.feature); batch.renderInstructions[renderIndex++] = value; } diff --git a/test/browser/spec/ol/render/webgl/BatchRenderer.test.js b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js index f4bdfdaa57..1c46a69679 100644 --- a/test/browser/spec/ol/render/webgl/BatchRenderer.test.js +++ b/test/browser/spec/ol/render/webgl/BatchRenderer.test.js @@ -39,7 +39,7 @@ describe('Batch renderers', function () { { name: 'test', callback: function (feature, properties) { - return properties.test; + return feature.get('test'); }, }, ]; diff --git a/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js b/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js index dba8e79485..d870a9f8a4 100644 --- a/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js +++ b/test/browser/spec/ol/render/webgl/MixedGeometryBatch.test.js @@ -62,12 +62,10 @@ describe('MixedGeometryBatch', function () { expect(keys).to.eql([uid1, uid2]); expect(mixedBatch.pointBatch.entries[uid1]).to.eql({ feature: feature1, - properties: feature1.getProperties(), flatCoordss: [[0, 1]], }); expect(mixedBatch.pointBatch.entries[uid2]).to.eql({ feature: feature2, - properties: feature2.getProperties(), flatCoordss: [[2, 3]], }); }); @@ -95,7 +93,7 @@ describe('MixedGeometryBatch', function () { }); it('updates the modified properties and geometry in the point batch', () => { const entry = mixedBatch.pointBatch.entries[getUid(feature1)]; - expect(entry.properties.prop1).to.eql('changed'); + expect(entry.feature.get('prop1')).to.eql('changed'); }); it('keeps geometry count the same', () => { expect(mixedBatch.pointBatch.geometriesCount).to.be(2); @@ -173,13 +171,11 @@ describe('MixedGeometryBatch', function () { expect(keys).to.eql([uid1, uid2]); expect(mixedBatch.lineStringBatch.entries[uid1]).to.eql({ feature: feature1, - properties: feature1.getProperties(), flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7]], verticesCount: 4, }); expect(mixedBatch.lineStringBatch.entries[uid2]).to.eql({ feature: feature2, - properties: feature2.getProperties(), flatCoordss: [[8, 9, 10, 11, 12, 13]], verticesCount: 3, }); @@ -208,7 +204,7 @@ describe('MixedGeometryBatch', function () { }); it('updates the modified properties and geometry in the linestring batch', () => { const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)]; - expect(entry.properties.prop1).to.eql('changed'); + 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], @@ -316,7 +312,6 @@ describe('MixedGeometryBatch', function () { expect(keys).to.eql([uid1, uid2]); expect(mixedBatch.polygonBatch.entries[uid1]).to.eql({ feature: feature1, - properties: feature1.getProperties(), flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25]], verticesCount: 7, ringsCount: 2, @@ -324,7 +319,6 @@ describe('MixedGeometryBatch', function () { }); expect(mixedBatch.polygonBatch.entries[uid2]).to.eql({ feature: feature2, - properties: feature2.getProperties(), flatCoordss: [ [ 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, @@ -346,7 +340,6 @@ describe('MixedGeometryBatch', function () { expect(keys).to.eql([getUid(feature1), getUid(feature2)]); expect(mixedBatch.lineStringBatch.entries[getUid(feature1)]).to.eql({ feature: feature1, - properties: feature1.getProperties(), flatCoordss: [ [0, 1, 2, 3, 4, 5, 6, 7], [20, 21, 22, 23, 24, 25], @@ -355,7 +348,6 @@ describe('MixedGeometryBatch', function () { }); expect(mixedBatch.lineStringBatch.entries[getUid(feature2)]).to.eql({ feature: feature2, - properties: feature2.getProperties(), flatCoordss: [ [8, 9, 10, 11, 12, 13], [30, 31, 32, 33, 34, 35], @@ -393,7 +385,7 @@ describe('MixedGeometryBatch', function () { }); it('updates the modified properties and geometry in the polygon batch', () => { const entry = mixedBatch.polygonBatch.entries[getUid(feature1)]; - expect(entry.properties.prop1).to.eql('changed'); + 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]]); @@ -529,7 +521,6 @@ describe('MixedGeometryBatch', function () { const uid = getUid(feature); expect(mixedBatch.polygonBatch.entries[uid]).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [ [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25], [ @@ -549,7 +540,6 @@ describe('MixedGeometryBatch', function () { const uid = getUid(feature); expect(mixedBatch.lineStringBatch.entries[uid]).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [ [0, 1, 2, 3, 4, 5, 6, 7], [20, 21, 22, 23, 24, 25], @@ -566,7 +556,6 @@ describe('MixedGeometryBatch', function () { const uid = getUid(feature); expect(mixedBatch.pointBatch.entries[uid]).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [ [101, 102], [201, 202], @@ -619,7 +608,6 @@ describe('MixedGeometryBatch', function () { const entry = mixedBatch.polygonBatch.entries[getUid(feature)]; expect(entry).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [ [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25], [ @@ -637,7 +625,6 @@ describe('MixedGeometryBatch', function () { const entry = mixedBatch.lineStringBatch.entries[getUid(feature)]; expect(entry).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [ [0, 1, 2, 3, 4, 5, 6, 7], [20, 21, 22, 23, 24, 25], @@ -680,7 +667,6 @@ describe('MixedGeometryBatch', function () { const entry = mixedBatch.polygonBatch.entries[getUid(feature)]; expect(entry).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]], verticesCount: 4, ringsCount: 1, @@ -691,7 +677,6 @@ describe('MixedGeometryBatch', function () { const entry = mixedBatch.lineStringBatch.entries[getUid(feature)]; expect(entry).to.eql({ feature: feature, - properties: feature.getProperties(), flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]], verticesCount: 4, }); From 01f3536d29c680a0b90ef253bcdc5698eed5f731 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 14:08:16 -0700 Subject: [PATCH 26/27] Using a union type instead of a string enum --- examples/webgl-vector-layer.js | 15 ++++++-------- src/ol/renderer/webgl/VectorLayer.js | 31 +++++++++++++++------------- src/ol/renderer/webgl/shaders.js | 18 +++++----------- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index 68dd7442b1..a53303719c 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -6,11 +6,8 @@ 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 { - DefaultAttributes, - packColor, -} from '../src/ol/renderer/webgl/shaders.js'; import {asArray} from '../src/ol/color.js'; +import {packColor} from '../src/ol/renderer/webgl/shaders.js'; class WebGLLayer extends Layer { createRenderer() { @@ -18,27 +15,27 @@ class WebGLLayer extends Layer { className: this.getClassName(), fill: { attributes: { - [DefaultAttributes.COLOR]: function (feature) { + color: function (feature) { const color = asArray(feature.get('COLOR') || '#eee'); color[3] = 0.85; return packColor(color); }, - [DefaultAttributes.OPACITY]: function () { + opacity: function () { return 0.6; }, }, }, stroke: { attributes: { - [DefaultAttributes.COLOR]: function (feature) { + 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); }, - [DefaultAttributes.WIDTH]: function () { + width: function () { return 1.5; }, - [DefaultAttributes.OPACITY]: function () { + opacity: function () { return 1; }, }, diff --git a/src/ol/renderer/webgl/VectorLayer.js b/src/ol/renderer/webgl/VectorLayer.js index aa2ff1c25f..bb15ce2ccd 100644 --- a/src/ol/renderer/webgl/VectorLayer.js +++ b/src/ol/renderer/webgl/VectorLayer.js @@ -9,8 +9,8 @@ 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 { - DefaultAttributes, FILL_FRAGMENT_SHADER, FILL_VERTEX_SHADER, POINT_FRAGMENT_SHADER, @@ -19,7 +19,6 @@ import { STROKE_VERTEX_SHADER, packColor, } from './shaders.js'; -import {DefaultUniform} from '../../webgl/Helper.js'; import {buffer, createEmpty, equals, getWidth} from '../../extent.js'; import {create as createTransform} from '../../transform.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; @@ -35,7 +34,7 @@ import {listen, unlistenByKey} from '../../events.js'; * @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. + * @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}. */ @@ -50,6 +49,14 @@ import {listen, unlistenByKey} from '../../events.js'; * @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: @@ -97,42 +104,38 @@ class WebGLVectorLayerRenderer extends WebGLLayerRenderer { this.currentTransform_ = projectionMatrixTransform; const fillAttributes = { - [DefaultAttributes.COLOR]: function () { + color: function () { return packColor('#ddd'); }, - [DefaultAttributes.OPACITY]: function () { + opacity: function () { return 1; }, ...(options.fill && options.fill.attributes), }; const strokeAttributes = { - [DefaultAttributes.COLOR]: function () { + color: function () { return packColor('#eee'); }, - [DefaultAttributes.OPACITY]: function () { + opacity: function () { return 1; }, - [DefaultAttributes.WIDTH]: function () { + width: function () { return 1.5; }, ...(options.stroke && options.stroke.attributes), }; const pointAttributes = { - [DefaultAttributes.COLOR]: function () { + color: function () { return packColor('#eee'); }, - [DefaultAttributes.OPACITY]: function () { + opacity: function () { return 1; }, ...(options.point && options.point.attributes), }; - function toAttributesArray(obj) { - return Object.keys(obj).map((key) => ({name: key, callback: obj[key]})); - } - this.fillVertexShader_ = (options.fill && options.fill.vertexShader) || FILL_VERTEX_SHADER; this.fillFragmentShader_ = diff --git a/src/ol/renderer/webgl/shaders.js b/src/ol/renderer/webgl/shaders.js index 4494cbc74f..da79e3ed05 100644 --- a/src/ol/renderer/webgl/shaders.js +++ b/src/ol/renderer/webgl/shaders.js @@ -3,19 +3,11 @@ */ import {asArray} from '../../color.js'; -/** - * Attribute names used in the default shaders. - * @enum {string} - */ -export const DefaultAttributes = { - COLOR: 'color', - OPACITY: 'opacity', - WIDTH: 'width', -}; +/** @typedef {'color'|'opacity'|'width'} DefaultAttributes */ /** * Packs red/green/blue channels of a color into a single float value; alpha is ignored. - * This is how DefaultAttributes.COLOR is expected to be computed. + * 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 */ @@ -35,7 +27,7 @@ const DECODE_COLOR_EXPRESSION = `vec3( /** * Default polygon vertex shader. - * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. + * Relies on the color and opacity attributes. * @type {string} */ export const FILL_VERTEX_SHADER = ` @@ -68,7 +60,7 @@ export const FILL_FRAGMENT_SHADER = ` /** * Default linestring vertex shader. - * Relies on DefaultAttributes.COLOR, DefaultAttributes.OPACITY and DefaultAttributes.WIDTH. + * Relies on color, opacity and width attributes. * @type {string} */ export const STROKE_VERTEX_SHADER = ` @@ -163,7 +155,7 @@ export const STROKE_FRAGMENT_SHADER = ` /** * Default point vertex shader. - * Relies on DefaultAttributes.COLOR and DefaultAttributes.OPACITY. + * Relies on color and opacity attributes. * @type {string} */ export const POINT_VERTEX_SHADER = ` From 8aa8684d814114e6f7eccea3c76633b4b05df24c Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 21 Jul 2022 14:30:32 -0700 Subject: [PATCH 27/27] Unused element and class name --- examples/webgl-vector-layer.html | 1 - examples/webgl-vector-layer.js | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/webgl-vector-layer.html b/examples/webgl-vector-layer.html index e4cf6e9f5b..32fe331f5a 100644 --- a/examples/webgl-vector-layer.html +++ b/examples/webgl-vector-layer.html @@ -8,4 +8,3 @@ tags: "vector, geojson, webgl" experimental: true ---
-
 
diff --git a/examples/webgl-vector-layer.js b/examples/webgl-vector-layer.js index a53303719c..d9ee44a00e 100644 --- a/examples/webgl-vector-layer.js +++ b/examples/webgl-vector-layer.js @@ -12,7 +12,6 @@ import {packColor} from '../src/ol/renderer/webgl/shaders.js'; class WebGLLayer extends Layer { createRenderer() { return new WebGLVectorLayerRenderer(this, { - className: this.getClassName(), fill: { attributes: { color: function (feature) {