Merge pull request #13461 from jahow/webgl-shape-renderer

WebGL vector renderer for polygons, lines and points
This commit is contained in:
Olivier Guyot
2022-07-22 10:05:31 +02:00
committed by GitHub
27 changed files with 3996 additions and 518 deletions

View File

@@ -0,0 +1,10 @@
---
layout: example.html
title: WebGL Vector Layer
shortdesc: Example of a vector layer rendered using WebGL
docs: >
The ecoregions are loaded from a GeoJSON file.
tags: "vector, geojson, webgl"
experimental: true
---
<div id="map" class="map"></div>

View File

@@ -0,0 +1,64 @@
import GeoJSON from '../src/ol/format/GeoJSON.js';
import Layer from '../src/ol/layer/Layer.js';
import Map from '../src/ol/Map.js';
import OSM from '../src/ol/source/OSM.js';
import TileLayer from '../src/ol/layer/WebGLTile.js';
import VectorSource from '../src/ol/source/Vector.js';
import View from '../src/ol/View.js';
import WebGLVectorLayerRenderer from '../src/ol/renderer/webgl/VectorLayer.js';
import {asArray} from '../src/ol/color.js';
import {packColor} from '../src/ol/renderer/webgl/shaders.js';
class WebGLLayer extends Layer {
createRenderer() {
return new WebGLVectorLayerRenderer(this, {
fill: {
attributes: {
color: function (feature) {
const color = asArray(feature.get('COLOR') || '#eee');
color[3] = 0.85;
return packColor(color);
},
opacity: function () {
return 0.6;
},
},
},
stroke: {
attributes: {
color: function (feature) {
const color = [...asArray(feature.get('COLOR') || '#eee')];
color.forEach((_, i) => (color[i] = Math.round(color[i] * 0.75))); // darken slightly
return packColor(color);
},
width: function () {
return 1.5;
},
opacity: function () {
return 1;
},
},
},
});
}
}
const osm = new TileLayer({
source: new OSM(),
});
const vectorLayer = new WebGLLayer({
source: new VectorSource({
url: 'https://openlayers.org/data/vector/ecoregions.json',
format: new GeoJSON(),
}),
});
const map = new Map({
layers: [osm, vectorLayer],
target: 'map',
view: new View({
center: [0, 0],
zoom: 1,
}),
});

11
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "7.0.0-dev",
"license": "BSD-2-Clause",
"dependencies": {
"earcut": "^2.2.3",
"geotiff": "2.0.4",
"ol-mapbox-style": "^8.0.5",
"pbf": "3.2.1",
@@ -3838,6 +3839,11 @@
"void-elements": "^2.0.0"
}
},
"node_modules/earcut": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
"integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -13182,6 +13188,11 @@
"void-elements": "^2.0.0"
}
},
"earcut": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz",
"integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug=="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",

View File

@@ -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",

View File

@@ -0,0 +1,201 @@
/**
* @module ol/render/webgl/BatchRenderer
*/
import {WebGLWorkerMessageType} from './constants.js';
import {abstract} from '../../util.js';
import {
create as createTransform,
makeInverse as makeInverseTransform,
multiply as multiplyTransform,
translate as translateTransform,
} from '../../transform.js';
/**
* @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different
* for each feature.
* @property {string} name Attribute name.
* @property {function(import("../../Feature").default):number} callback This callback computes the numerical value of the
* attribute for a given feature.
*/
let workerMessageCounter = 0;
/**
* @classdesc Abstract class for batch renderers.
* Batch renderers are meant to render the geometries contained in a {@link module:ol/render/webgl/GeometryBatch}
* instance. They are responsible for generating render instructions and transforming them into WebGL buffers.
*/
class AbstractBatchRenderer {
/**
* @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
* @param {Worker} worker WebGL worker instance
* @param {string} vertexShader Vertex shader
* @param {string} fragmentShader Fragment shader
* @param {Array<CustomAttribute>} customAttributes List of custom attributes
*/
constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
/**
* @type {import("../../webgl/Helper.js").default}
* @private
*/
this.helper_ = helper;
/**
* @type {Worker}
* @private
*/
this.worker_ = worker;
/**
* @type {WebGLProgram}
* @private
*/
this.program_ = this.helper_.getProgram(fragmentShader, vertexShader);
/**
* A list of attributes used by the renderer.
* @type {Array<import('../../webgl/Helper.js').AttributeDescription>}
* @protected
*/
this.attributes = [];
/**
* @type {Array<CustomAttribute>}
* @protected
*/
this.customAttributes = customAttributes;
}
/**
* Rebuild rendering instructions and webgl buffers based on the provided frame state
* Note: this is a costly operation.
* @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
* @param {import("../../PluggableMap").FrameState} frameState Frame state.
* @param {import("../../geom/Geometry.js").Type} geometryType Geometry type
* @param {function(): void} callback Function called once the render buffers are updated
*/
rebuild(batch, frameState, geometryType, callback) {
// store transform for rendering instructions
batch.renderInstructionsTransform = this.helper_.makeProjectionTransform(
frameState,
createTransform()
);
this.generateRenderInstructions(batch);
this.generateBuffers_(batch, geometryType, callback);
}
/**
* Render the geometries in the batch. This will also update the current transform used for rendering according to
* the invert transform of the webgl buffers
* @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
* @param {import("../../transform.js").Transform} currentTransform Transform
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} offsetX X offset
*/
render(batch, currentTransform, frameState, offsetX) {
// multiply the current projection transform with the invert of the one used to fill buffers
this.helper_.makeProjectionTransform(frameState, currentTransform);
translateTransform(currentTransform, offsetX, 0);
multiplyTransform(currentTransform, batch.invertVerticesBufferTransform);
// enable program, buffers and attributes
this.helper_.useProgram(this.program_, frameState);
this.helper_.bindBuffer(batch.verticesBuffer);
this.helper_.bindBuffer(batch.indicesBuffer);
this.helper_.enableAttributes(this.attributes);
const renderCount = batch.indicesBuffer.getSize();
this.helper_.drawElements(0, renderCount);
}
/**
* Rebuild rendering instructions based on the provided frame state
* This is specific to the geometry type and has to be implemented by subclasses.
* @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
* @protected
*/
generateRenderInstructions(batch) {
abstract();
}
/**
* Rebuild internal webgl buffers for rendering based on the current rendering instructions;
* This is asynchronous: webgl buffers wil _not_ be updated right away
* @param {import("./MixedGeometryBatch.js").GeometryBatch} batch Geometry batch
* @param {import("../../geom/Geometry.js").Type} geometryType Geometry type
* @param {function(): void} callback Function called once the render buffers are updated
* @private
*/
generateBuffers_(batch, geometryType, callback) {
const messageId = workerMessageCounter++;
let messageType;
switch (geometryType) {
case 'Polygon':
messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS;
break;
case 'Point':
messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS;
break;
case 'LineString':
messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS;
break;
default:
// pass
}
/** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = {
id: messageId,
type: messageType,
renderInstructions: batch.renderInstructions.buffer,
renderInstructionsTransform: batch.renderInstructionsTransform,
customAttributesCount: this.customAttributes.length,
};
this.worker_.postMessage(message, [batch.renderInstructions.buffer]);
// leave ownership of render instructions
batch.renderInstructions = null;
const handleMessage =
/**
* @param {*} event Event.
* @this {AbstractBatchRenderer}
*/
function (event) {
const received = event.data;
// this is not the response to our request: skip
if (received.id !== messageId) {
return;
}
// we've received our response: stop listening
this.worker_.removeEventListener('message', handleMessage);
// store transform & invert transform for webgl buffers
batch.verticesBufferTransform = received.renderInstructionsTransform;
makeInverseTransform(
batch.invertVerticesBufferTransform,
batch.verticesBufferTransform
);
// copy & flush received buffers to GPU
batch.verticesBuffer.fromArrayBuffer(received.vertexBuffer);
this.helper_.flushBufferData(batch.verticesBuffer);
batch.indicesBuffer.fromArrayBuffer(received.indexBuffer);
this.helper_.flushBufferData(batch.indicesBuffer);
// take back ownership of the render instructions for further use
batch.renderInstructions = new Float32Array(
received.renderInstructions
);
callback();
}.bind(this);
this.worker_.addEventListener('message', handleMessage);
}
}
export default AbstractBatchRenderer;

View File

@@ -0,0 +1,116 @@
/**
* @module ol/render/webgl/LineStringBatchRenderer
*/
import AbstractBatchRenderer from './BatchRenderer.js';
import {AttributeType} from '../../webgl/Helper.js';
import {transform2D} from '../../geom/flat/transform.js';
/**
* Names of attributes made available to the vertex shader.
* Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const Attributes = {
SEGMENT_START: 'a_segmentStart',
SEGMENT_END: 'a_segmentEnd',
PARAMETERS: 'a_parameters',
};
class LineStringBatchRenderer extends AbstractBatchRenderer {
/**
* @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
* @param {Worker} worker WebGL worker instance
* @param {string} vertexShader Vertex shader
* @param {string} fragmentShader Fragment shader
* @param {Array<import('./BatchRenderer.js').CustomAttribute>} customAttributes List of custom attributes
*/
constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
super(helper, worker, vertexShader, fragmentShader, customAttributes);
// vertices for lines must hold both a position (x,y) and an offset (dx,dy)
this.attributes = [
{
name: Attributes.SEGMENT_START,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.SEGMENT_END,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.PARAMETERS,
size: 1,
type: AttributeType.FLOAT,
},
].concat(
customAttributes.map(function (attribute) {
return {
name: 'a_' + attribute.name,
size: 1,
type: AttributeType.FLOAT,
};
})
);
}
/**
* Render instructions for lines are structured like so:
* [ customAttr0, ... , customAttrN, numberOfVertices0, x0, y0, ... , xN, yN, numberOfVertices1, ... ]
* @param {import("./MixedGeometryBatch.js").LineStringGeometryBatch} batch Linestring geometry batch
* @override
*/
generateRenderInstructions(batch) {
// here we anticipate the amount of render instructions for lines:
// 2 instructions per vertex for position (x and y)
// + 1 instruction per line per custom attributes
// + 1 instruction per line (for vertices count)
const totalInstructionsCount =
2 * batch.verticesCount +
(1 + this.customAttributes.length) * batch.geometriesCount;
if (
!batch.renderInstructions ||
batch.renderInstructions.length !== totalInstructionsCount
) {
batch.renderInstructions = new Float32Array(totalInstructionsCount);
}
// loop on features to fill the render instructions
let batchEntry;
const flatCoords = [];
let renderIndex = 0;
let value;
for (const featureUid in batch.entries) {
batchEntry = batch.entries[featureUid];
for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
flatCoords.length = batchEntry.flatCoordss[i].length;
transform2D(
batchEntry.flatCoordss[i],
0,
flatCoords.length,
2,
batch.renderInstructionsTransform,
flatCoords
);
// custom attributes
for (let k = 0, kk = this.customAttributes.length; k < kk; k++) {
value = this.customAttributes[k].callback(batchEntry.feature);
batch.renderInstructions[renderIndex++] = value;
}
// vertices count
batch.renderInstructions[renderIndex++] = flatCoords.length / 2;
// looping on points for positions
for (let j = 0, jj = flatCoords.length; j < jj; j += 2) {
batch.renderInstructions[renderIndex++] = flatCoords[j];
batch.renderInstructions[renderIndex++] = flatCoords[j + 1];
}
}
}
}
}
export default LineStringBatchRenderer;

View File

@@ -0,0 +1,364 @@
/**
* @module ol/render/webgl/MixedGeometryBatch
*/
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
import {create as createTransform} from '../../transform.js';
import {getUid} from '../../util.js';
/**
* @typedef {Object} GeometryBatchItem Object that holds a reference to a feature as well as the raw coordinates of its various geometries
* @property {import("../../Feature").default} feature Feature
* @property {Array<Array<number>>} 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<Array<number>>} [ringsVerticesCounts] Array of vertices counts in each ring for each geometry; only defined for polygons batches
*/
/**
* @typedef {PointGeometryBatch|LineStringGeometryBatch|PolygonGeometryBatch} GeometryBatch
*/
/**
* @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons
* @property {Object<string, GeometryBatchItem>} entries Dictionary of all entries in the batch with associated computed values.
* One entry corresponds to one feature. Key is feature uid.
* @property {number} geometriesCount Amount of geometries in the batch.
* @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
* [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
* @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
* @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
* @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
* @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
* @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
* @property {number} verticesCount Amount of vertices from geometries in the batch.
* @property {number} ringsCount How many outer and inner rings in this batch.
*/
/**
* @typedef {Object} LineStringGeometryBatch A geometry batch specific to lines
* @property {Object<string, GeometryBatchItem>} entries Dictionary of all entries in the batch with associated computed values.
* One entry corresponds to one feature. Key is feature uid.
* @property {number} geometriesCount Amount of geometries in the batch.
* @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
* [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
* @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
* @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
* @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
* @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
* @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
* @property {number} verticesCount Amount of vertices from geometries in the batch.
*/
/**
* @typedef {Object} PointGeometryBatch A geometry batch specific to points
* @property {Object<string, GeometryBatchItem>} entries Dictionary of all entries in the batch with associated computed values.
* One entry corresponds to one feature. Key is feature uid.
* @property {number} geometriesCount Amount of geometries in the batch.
* @property {Float32Array} renderInstructions Render instructions for polygons are structured like so:
* [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ]
* @property {WebGLArrayBuffer} verticesBuffer Vertices WebGL buffer
* @property {WebGLArrayBuffer} indicesBuffer Indices WebGL buffer
* @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions
* @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer
* @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer
*/
/**
* @classdesc This class is used to group several geometries of various types together for faster rendering.
* Three inner batches are maintained for polygons, lines and points. Each time a feature is added, changed or removed
* from the batch, these inner batches are modified accordingly in order to keep them up-to-date.
*
* A feature can be present in several inner batches, for example a polygon geometry will be present in the polygon batch
* and its linar rings will be present in the line batch. Multi geometries are also broken down into individual geometries
* and added to the corresponding batches in a recursive manner.
*
* Corresponding {@link module:ol/render/webgl/BatchRenderer} instances are then used to generate the render instructions
* and WebGL buffers (vertices and indices) for each inner batches; render instructions are stored on the inner batches,
* alongside the transform used to convert world coords to screen coords at the time these instructions were generated.
* The resulting WebGL buffers are stored on the batches as well.
*
* An important aspect of geometry batches is that there is no guarantee that render instructions and WebGL buffers
* are synchronized, i.e. render instructions can describe a new state while WebGL buffers might not have been written yet.
* This is why two world-to-screen transforms are stored on each batch: one for the render instructions and one for
* the WebGL buffers.
*/
class MixedGeometryBatch {
constructor() {
/**
* @type {PolygonGeometryBatch}
*/
this.polygonBatch = {
entries: {},
geometriesCount: 0,
verticesCount: 0,
ringsCount: 0,
renderInstructions: new Float32Array(0),
verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
renderInstructionsTransform: createTransform(),
verticesBufferTransform: createTransform(),
invertVerticesBufferTransform: createTransform(),
};
/**
* @type {PointGeometryBatch}
*/
this.pointBatch = {
entries: {},
geometriesCount: 0,
renderInstructions: new Float32Array(0),
verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
renderInstructionsTransform: createTransform(),
verticesBufferTransform: createTransform(),
invertVerticesBufferTransform: createTransform(),
};
/**
* @type {LineStringGeometryBatch}
*/
this.lineStringBatch = {
entries: {},
geometriesCount: 0,
verticesCount: 0,
renderInstructions: new Float32Array(0),
verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW),
indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW),
renderInstructionsTransform: createTransform(),
verticesBufferTransform: createTransform(),
invertVerticesBufferTransform: createTransform(),
};
}
/**
* @param {Array<import("../../Feature").default>} features Array of features to add to the batch
*/
addFeatures(features) {
for (let i = 0; i < features.length; i++) {
this.addFeature(features[i]);
}
}
/**
* @param {import("../../Feature").default} feature Feature to add to the batch
*/
addFeature(feature) {
const geometry = feature.getGeometry();
if (!geometry) {
return;
}
this.addGeometry_(geometry, feature);
}
/**
* @param {import("../../Feature").default} feature Feature
* @return {GeometryBatchItem} Batch item added (or existing one)
* @private
*/
addFeatureEntryInPointBatch_(feature) {
const uid = getUid(feature);
if (!(uid in this.pointBatch.entries)) {
this.pointBatch.entries[uid] = {
feature: feature,
flatCoordss: [],
};
}
return this.pointBatch.entries[uid];
}
/**
* @param {import("../../Feature").default} feature Feature
* @return {GeometryBatchItem} Batch item added (or existing one)
* @private
*/
addFeatureEntryInLineStringBatch_(feature) {
const uid = getUid(feature);
if (!(uid in this.lineStringBatch.entries)) {
this.lineStringBatch.entries[uid] = {
feature: feature,
flatCoordss: [],
verticesCount: 0,
};
}
return this.lineStringBatch.entries[uid];
}
/**
* @param {import("../../Feature").default} feature Feature
* @return {GeometryBatchItem} Batch item added (or existing one)
* @private
*/
addFeatureEntryInPolygonBatch_(feature) {
const uid = getUid(feature);
if (!(uid in this.polygonBatch.entries)) {
this.polygonBatch.entries[uid] = {
feature: feature,
flatCoordss: [],
verticesCount: 0,
ringsCount: 0,
ringsVerticesCounts: [],
};
}
return this.polygonBatch.entries[uid];
}
/**
* @param {import("../../Feature").default} feature Feature
* @private
*/
clearFeatureEntryInPointBatch_(feature) {
const entry = this.pointBatch.entries[getUid(feature)];
if (!entry) {
return;
}
this.pointBatch.geometriesCount -= entry.flatCoordss.length;
delete this.pointBatch.entries[getUid(feature)];
}
/**
* @param {import("../../Feature").default} feature Feature
* @private
*/
clearFeatureEntryInLineStringBatch_(feature) {
const entry = this.lineStringBatch.entries[getUid(feature)];
if (!entry) {
return;
}
this.lineStringBatch.verticesCount -= entry.verticesCount;
this.lineStringBatch.geometriesCount -= entry.flatCoordss.length;
delete this.lineStringBatch.entries[getUid(feature)];
}
/**
* @param {import("../../Feature").default} feature Feature
* @private
*/
clearFeatureEntryInPolygonBatch_(feature) {
const entry = this.polygonBatch.entries[getUid(feature)];
if (!entry) {
return;
}
this.polygonBatch.verticesCount -= entry.verticesCount;
this.polygonBatch.ringsCount -= entry.ringsCount;
this.polygonBatch.geometriesCount -= entry.flatCoordss.length;
delete this.polygonBatch.entries[getUid(feature)];
}
/**
* @param {import("../../geom").Geometry} geometry Geometry
* @param {import("../../Feature").default} feature Feature
* @private
*/
addGeometry_(geometry, feature) {
const type = geometry.getType();
let flatCoords;
let verticesCount;
let batchEntry;
switch (type) {
case 'GeometryCollection':
/** @type {import("../../geom").GeometryCollection} */ (geometry)
.getGeometries()
.map((geom) => this.addGeometry_(geom, feature));
break;
case 'MultiPolygon':
/** @type {import("../../geom").MultiPolygon} */ (geometry)
.getPolygons()
.map((polygon) => this.addGeometry_(polygon, feature));
break;
case 'MultiLineString':
/** @type {import("../../geom").MultiLineString} */ (geometry)
.getLineStrings()
.map((line) => this.addGeometry_(line, feature));
break;
case 'MultiPoint':
/** @type {import("../../geom").MultiPoint} */ (geometry)
.getPoints()
.map((point) => this.addGeometry_(point, feature));
break;
case 'Polygon':
const polygonGeom = /** @type {import("../../geom").Polygon} */ (
geometry
);
batchEntry = this.addFeatureEntryInPolygonBatch_(feature);
flatCoords = polygonGeom.getFlatCoordinates();
verticesCount = flatCoords.length / 2;
const ringsCount = polygonGeom.getLinearRingCount();
const ringsVerticesCount = polygonGeom
.getEnds()
.map((end, ind, arr) =>
ind > 0 ? (end - arr[ind - 1]) / 2 : end / 2
);
this.polygonBatch.verticesCount += verticesCount;
this.polygonBatch.ringsCount += ringsCount;
this.polygonBatch.geometriesCount++;
batchEntry.flatCoordss.push(flatCoords);
batchEntry.ringsVerticesCounts.push(ringsVerticesCount);
batchEntry.verticesCount += verticesCount;
batchEntry.ringsCount += ringsCount;
polygonGeom
.getLinearRings()
.map((ring) => this.addGeometry_(ring, feature));
break;
case 'Point':
const pointGeom = /** @type {import("../../geom").Point} */ (geometry);
batchEntry = this.addFeatureEntryInPointBatch_(feature);
flatCoords = pointGeom.getFlatCoordinates();
this.pointBatch.geometriesCount++;
batchEntry.flatCoordss.push(flatCoords);
break;
case 'LineString':
case 'LinearRing':
const lineGeom = /** @type {import("../../geom").LineString} */ (
geometry
);
batchEntry = this.addFeatureEntryInLineStringBatch_(feature);
flatCoords = lineGeom.getFlatCoordinates();
verticesCount = flatCoords.length / 2;
this.lineStringBatch.verticesCount += verticesCount;
this.lineStringBatch.geometriesCount++;
batchEntry.flatCoordss.push(flatCoords);
batchEntry.verticesCount += verticesCount;
break;
default:
// pass
}
}
/**
* @param {import("../../Feature").default} feature Feature
*/
changeFeature(feature) {
this.clearFeatureEntryInPointBatch_(feature);
this.clearFeatureEntryInPolygonBatch_(feature);
this.clearFeatureEntryInLineStringBatch_(feature);
const geometry = feature.getGeometry();
if (!geometry) {
return;
}
this.addGeometry_(geometry, feature);
}
/**
* @param {import("../../Feature").default} feature Feature
*/
removeFeature(feature) {
this.clearFeatureEntryInPointBatch_(feature);
this.clearFeatureEntryInPolygonBatch_(feature);
this.clearFeatureEntryInLineStringBatch_(feature);
}
clear() {
this.polygonBatch.entries = {};
this.polygonBatch.geometriesCount = 0;
this.polygonBatch.verticesCount = 0;
this.polygonBatch.ringsCount = 0;
this.lineStringBatch.entries = {};
this.lineStringBatch.geometriesCount = 0;
this.lineStringBatch.verticesCount = 0;
this.pointBatch.entries = {};
this.pointBatch.geometriesCount = 0;
}
}
export default MixedGeometryBatch;

View File

@@ -0,0 +1,97 @@
/**
* @module ol/render/webgl/PointBatchRenderer
*/
import AbstractBatchRenderer from './BatchRenderer.js';
import {AttributeType} from '../../webgl/Helper.js';
import {apply as applyTransform} from '../../transform.js';
/**
* Names of attributes made available to the vertex shader.
* Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const Attributes = {
POSITION: 'a_position',
INDEX: 'a_index',
};
class PointBatchRenderer extends AbstractBatchRenderer {
/**
* @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
* @param {Worker} worker WebGL worker instance
* @param {string} vertexShader Vertex shader
* @param {string} fragmentShader Fragment shader
* @param {Array<import('./BatchRenderer.js').CustomAttribute>} customAttributes List of custom attributes
*/
constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
super(helper, worker, vertexShader, fragmentShader, customAttributes);
// vertices for point must hold both a position (x,y) and an index (their position in the quad)
this.attributes = [
{
name: Attributes.POSITION,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.INDEX,
size: 1,
type: AttributeType.FLOAT,
},
].concat(
customAttributes.map(function (attribute) {
return {
name: 'a_' + attribute.name,
size: 1,
type: AttributeType.FLOAT,
};
})
);
}
/**
* Render instructions for lines are structured like so:
* [ x0, y0, customAttr0, ... , xN, yN, customAttrN ]
* @param {import("./MixedGeometryBatch.js").PointGeometryBatch} batch Point geometry batch
* @override
*/
generateRenderInstructions(batch) {
// here we anticipate the amount of render instructions for points:
// 2 instructions per vertex for position (x and y)
// + 1 instruction per vertex per custom attributes
const totalInstructionsCount =
(2 + this.customAttributes.length) * batch.geometriesCount;
if (
!batch.renderInstructions ||
batch.renderInstructions.length !== totalInstructionsCount
) {
batch.renderInstructions = new Float32Array(totalInstructionsCount);
}
// loop on features to fill the render instructions
let batchEntry;
const tmpCoords = [];
let renderIndex = 0;
let value;
for (const featureUid in batch.entries) {
batchEntry = batch.entries[featureUid];
for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
tmpCoords[0] = batchEntry.flatCoordss[i][0];
tmpCoords[1] = batchEntry.flatCoordss[i][1];
applyTransform(batch.renderInstructionsTransform, tmpCoords);
batch.renderInstructions[renderIndex++] = tmpCoords[0];
batch.renderInstructions[renderIndex++] = tmpCoords[1];
// pushing custom attributes
for (let j = 0, jj = this.customAttributes.length; j < jj; j++) {
value = this.customAttributes[j].callback(batchEntry.feature);
batch.renderInstructions[renderIndex++] = value;
}
}
}
}
}
export default PointBatchRenderer;

View File

@@ -0,0 +1,117 @@
/**
* @module ol/render/webgl/PolygonBatchRenderer
*/
import AbstractBatchRenderer from './BatchRenderer.js';
import {AttributeType} from '../../webgl/Helper.js';
import {transform2D} from '../../geom/flat/transform.js';
/**
* Names of attributes made available to the vertex shader.
* Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const Attributes = {
POSITION: 'a_position',
};
class PolygonBatchRenderer extends AbstractBatchRenderer {
/**
* @param {import("../../webgl/Helper.js").default} helper WebGL helper instance
* @param {Worker} worker WebGL worker instance
* @param {string} vertexShader Vertex shader
* @param {string} fragmentShader Fragment shader
* @param {Array<import('./BatchRenderer.js').CustomAttribute>} customAttributes List of custom attributes
*/
constructor(helper, worker, vertexShader, fragmentShader, customAttributes) {
super(helper, worker, vertexShader, fragmentShader, customAttributes);
// By default only a position attribute is required to render polygons
this.attributes = [
{
name: Attributes.POSITION,
size: 2,
type: AttributeType.FLOAT,
},
].concat(
customAttributes.map(function (attribute) {
return {
name: 'a_' + attribute.name,
size: 1,
type: AttributeType.FLOAT,
};
})
);
}
/**
* Render instructions for polygons are structured like so:
* [ customAttr0, ..., customAttrN, numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, ..., xN, yN, numberOfRings,... ]
* @param {import("./MixedGeometryBatch.js").PolygonGeometryBatch} batch Polygon geometry batch
* @override
*/
generateRenderInstructions(batch) {
// here we anticipate the amount of render instructions for polygons:
// 2 instructions per vertex for position (x and y)
// + 1 instruction per polygon per custom attributes
// + 1 instruction per polygon (for vertices count in polygon)
// + 1 instruction per ring (for vertices count in ring)
const totalInstructionsCount =
2 * batch.verticesCount +
(1 + this.customAttributes.length) * batch.geometriesCount +
batch.ringsCount;
if (
!batch.renderInstructions ||
batch.renderInstructions.length !== totalInstructionsCount
) {
batch.renderInstructions = new Float32Array(totalInstructionsCount);
}
// loop on features to fill the render instructions
let batchEntry;
const flatCoords = [];
let renderIndex = 0;
let value;
for (const featureUid in batch.entries) {
batchEntry = batch.entries[featureUid];
for (let i = 0, ii = batchEntry.flatCoordss.length; i < ii; i++) {
flatCoords.length = batchEntry.flatCoordss[i].length;
transform2D(
batchEntry.flatCoordss[i],
0,
flatCoords.length,
2,
batch.renderInstructionsTransform,
flatCoords
);
// custom attributes
for (let k = 0, kk = this.customAttributes.length; k < kk; k++) {
value = this.customAttributes[k].callback(batchEntry.feature);
batch.renderInstructions[renderIndex++] = value;
}
// ring count
batch.renderInstructions[renderIndex++] =
batchEntry.ringsVerticesCounts[i].length;
// vertices count in each ring
for (
let j = 0, jj = batchEntry.ringsVerticesCounts[i].length;
j < jj;
j++
) {
batch.renderInstructions[renderIndex++] =
batchEntry.ringsVerticesCounts[i][j];
}
// looping on points for positions
for (let j = 0, jj = flatCoords.length; j < jj; j += 2) {
batch.renderInstructions[renderIndex++] = flatCoords[j];
batch.renderInstructions[renderIndex++] = flatCoords[j + 1];
}
}
}
}
}
export default PolygonBatchRenderer;

View File

@@ -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
*/

View File

@@ -0,0 +1,351 @@
/**
* @module ol/render/webgl/utils
*/
import earcut from 'earcut';
import {apply as applyTransform} from '../../transform.js';
import {clamp} from '../../math.js';
const tmpArray_ = [];
/**
* An object holding positions both in an index and a vertex buffer.
* @typedef {Object} BufferPositions
* @property {number} vertexPosition Position in the vertex buffer
* @property {number} indexPosition Position in the index buffer
*/
const bufferPositions_ = {vertexPosition: 0, indexPosition: 0};
function writePointVertex(buffer, pos, x, y, index) {
buffer[pos + 0] = x;
buffer[pos + 1] = y;
buffer[pos + 2] = index;
}
/**
* Pushes a quad (two triangles) based on a point geometry
* @param {Float32Array} instructions Array of render instructions for points.
* @param {number} elementIndex Index from which render instructions will be read.
* @param {Float32Array} vertexBuffer Buffer in the form of a typed array.
* @param {Uint32Array} indexBuffer Buffer in the form of a typed array.
* @param {number} customAttributesCount Amount of custom attributes for each element.
* @param {BufferPositions} [bufferPositions] Buffer write positions; if not specified, positions will be set at 0.
* @return {BufferPositions} New buffer positions where to write next
* @property {number} vertexPosition New position in the vertex buffer where future writes should start.
* @property {number} indexPosition New position in the index buffer where future writes should start.
* @private
*/
export function writePointFeatureToBuffers(
instructions,
elementIndex,
vertexBuffer,
indexBuffer,
customAttributesCount,
bufferPositions
) {
// This is for x, y and index
const baseVertexAttrsCount = 3;
const baseInstructionsCount = 2;
const stride = baseVertexAttrsCount + customAttributesCount;
const x = instructions[elementIndex + 0];
const y = instructions[elementIndex + 1];
// read custom numerical attributes on the feature
const customAttrs = tmpArray_;
customAttrs.length = customAttributesCount;
for (let i = 0; i < customAttrs.length; i++) {
customAttrs[i] = instructions[elementIndex + baseInstructionsCount + i];
}
let vPos = bufferPositions ? bufferPositions.vertexPosition : 0;
let iPos = bufferPositions ? bufferPositions.indexPosition : 0;
const baseIndex = vPos / stride;
// push vertices for each of the four quad corners (first standard then custom attributes)
writePointVertex(vertexBuffer, vPos, x, y, 0);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 1);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 2);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 3);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
indexBuffer[iPos++] = baseIndex;
indexBuffer[iPos++] = baseIndex + 1;
indexBuffer[iPos++] = baseIndex + 3;
indexBuffer[iPos++] = baseIndex + 1;
indexBuffer[iPos++] = baseIndex + 2;
indexBuffer[iPos++] = baseIndex + 3;
bufferPositions_.vertexPosition = vPos;
bufferPositions_.indexPosition = iPos;
return bufferPositions_;
}
/**
* Pushes a single quad to form a line segment; also includes a computation for the join angles with previous and next
* segment, in order to be able to offset the vertices correctly in the shader
* @param {Float32Array} instructions Array of render instructions for lines.
* @param {number} segmentStartIndex Index of the segment start point from which render instructions will be read.
* @param {number} segmentEndIndex Index of the segment start point from which render instructions will be read.
* @param {number|null} beforeSegmentIndex Index of the point right before the segment (null if none, e.g this is a line start)
* @param {number|null} afterSegmentIndex Index of the point right after the segment (null if none, e.g this is a line end)
* @param {Array<number>} vertexArray Array containing vertices.
* @param {Array<number>} indexArray Array containing indices.
* @param {Array<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;
const baseIndex = vertexArray.length / stride;
// The segment is composed of two positions called P0[x0, y0] and P1[x1, y1]
// Depending on whether there are points before and after the segment, its final shape
// will be different
const p0 = [
instructions[segmentStartIndex + 0],
instructions[segmentStartIndex + 1],
];
const p1 = [instructions[segmentEndIndex], instructions[segmentEndIndex + 1]];
// to compute offsets from the line center we need to reproject
// coordinates back in world units and compute the length of the segment
const p0world = applyTransform(invertInstructionsTransform, [...p0]);
const p1world = applyTransform(invertInstructionsTransform, [...p1]);
function computeVertexParameters(vertexNumber, joinAngle1, joinAngle2) {
const shift = 10000;
const anglePrecision = 1500;
return (
Math.round(joinAngle1 * anglePrecision) +
Math.round(joinAngle2 * anglePrecision) * shift +
vertexNumber * shift * shift
);
}
// compute the angle between p0pA and p0pB
// returns a value in [0, 2PI]
function angleBetween(p0, pA, pB) {
const lenA = Math.sqrt(
(pA[0] - p0[0]) * (pA[0] - p0[0]) + (pA[1] - p0[1]) * (pA[1] - p0[1])
);
const tangentA = [(pA[0] - p0[0]) / lenA, (pA[1] - p0[1]) / lenA];
const orthoA = [-tangentA[1], tangentA[0]];
const lenB = Math.sqrt(
(pB[0] - p0[0]) * (pB[0] - p0[0]) + (pB[1] - p0[1]) * (pB[1] - p0[1])
);
const tangentB = [(pB[0] - p0[0]) / lenB, (pB[1] - p0[1]) / lenB];
// this angle can be clockwise or anticlockwise; hence the computation afterwards
const angle =
lenA === 0 || lenB === 0
? 0
: Math.acos(
clamp(tangentB[0] * tangentA[0] + tangentB[1] * tangentA[1], -1, 1)
);
const isClockwise = tangentB[0] * orthoA[0] + tangentB[1] * orthoA[1] > 0;
return !isClockwise ? Math.PI * 2 - angle : angle;
}
const joinBefore = beforeSegmentIndex !== null;
const joinAfter = afterSegmentIndex !== null;
let angle0 = 0;
let angle1 = 0;
// add vertices and adapt offsets for P0 in case of join
if (joinBefore) {
// B for before
const pB = [
instructions[beforeSegmentIndex],
instructions[beforeSegmentIndex + 1],
];
const pBworld = applyTransform(invertInstructionsTransform, [...pB]);
angle0 = angleBetween(p0world, p1world, pBworld);
}
// adapt offsets for P1 in case of join
if (joinAfter) {
// A for after
const pA = [
instructions[afterSegmentIndex],
instructions[afterSegmentIndex + 1],
];
const pAworld = applyTransform(invertInstructionsTransform, [...pA]);
angle1 = angleBetween(p1world, p0world, pAworld);
}
// add main segment triangles
vertexArray.push(
p0[0],
p0[1],
p1[0],
p1[1],
computeVertexParameters(0, angle0, angle1)
);
vertexArray.push(...customAttributes);
vertexArray.push(
p0[0],
p0[1],
p1[0],
p1[1],
computeVertexParameters(1, angle0, angle1)
);
vertexArray.push(...customAttributes);
vertexArray.push(
p0[0],
p0[1],
p1[0],
p1[1],
computeVertexParameters(2, angle0, angle1)
);
vertexArray.push(...customAttributes);
vertexArray.push(
p0[0],
p0[1],
p1[0],
p1[1],
computeVertexParameters(3, angle0, angle1)
);
vertexArray.push(...customAttributes);
indexArray.push(
baseIndex,
baseIndex + 1,
baseIndex + 2,
baseIndex + 1,
baseIndex + 3,
baseIndex + 2
);
}
/**
* Pushes several triangles to form a polygon, including holes
* @param {Float32Array} instructions Array of render instructions for lines.
* @param {number} polygonStartIndex Index of the polygon start point from which render instructions will be read.
* @param {Array<number>} vertexArray Array containing vertices.
* @param {Array<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
* @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<number>} [opt_array] Reusable array
* @return {Array<number>} 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<number>} 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;
}

View File

@@ -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<number>} [opt_array] Reusable array
* @return {Array<number>} 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<number>} 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;

View File

@@ -5,14 +5,11 @@ import BaseVector from '../../layer/BaseVector.js';
import VectorEventType from '../../source/VectorEventType.js';
import ViewHint from '../../ViewHint.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import WebGLLayerRenderer, {
WebGLWorkerMessageType,
colorDecodeId,
colorEncodeId,
} from './Layer.js';
import WebGLLayerRenderer from './Layer.js';
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
import {AttributeType, DefaultUniform} from '../../webgl/Helper.js';
import {WebGLWorkerMessageType} from '../../render/webgl/constants.js';
import {
apply as applyTransform,
create as createTransform,
@@ -22,6 +19,7 @@ import {
} from '../../transform.js';
import {assert} from '../../asserts.js';
import {buffer, createEmpty, equals, getWidth} from '../../extent.js';
import {colorDecodeId, colorEncodeId} from '../../render/webgl/utils.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {getUid} from '../../util.js';
import {listen, unlistenByKey} from '../../events.js';
@@ -294,7 +292,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
*/
this.generateBuffersRun_ = 0;
/**
* @private
*/
this.worker_ = createWebGLWorker();
this.worker_.addEventListener(
'message',
/**
@@ -303,7 +305,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
*/
function (event) {
const received = event.data;
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
if (received.type === WebGLWorkerMessageType.GENERATE_POINT_BUFFERS) {
const projectionTransform = received.projectionTransform;
if (received.hitDetection) {
this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer);
@@ -540,7 +542,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.previousExtent_ = frameState.extent.slice();
}
this.helper.useProgram(this.program_);
this.helper.useProgram(this.program_, frameState);
this.helper.prepareDraw(frameState);
// write new data
@@ -637,9 +639,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
}
}
/** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
/** @type {import('../../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = {
type: WebGLWorkerMessageType.GENERATE_BUFFERS,
id: 0,
type: WebGLWorkerMessageType.GENERATE_POINT_BUFFERS,
renderInstructions: this.renderInstructions_.buffer,
customAttributesCount: this.customAttributes.length,
};
@@ -650,10 +653,11 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
this.renderInstructions_ = null;
/** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
/** @type {import('../../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
if (this.hitDetectionEnabled_) {
const hitMessage = {
type: WebGLWorkerMessageType.GENERATE_BUFFERS,
id: 0,
type: WebGLWorkerMessageType.GENERATE_POINT_BUFFERS,
renderInstructions: this.hitRenderInstructions_.buffer,
customAttributesCount: 5 + this.customAttributes.length,
};
@@ -726,7 +730,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
Math.floor(frameState.size[1] / 2),
]);
this.helper.useProgram(this.hitProgram_);
this.helper.useProgram(this.hitProgram_, frameState);
this.helper.prepareDrawToRenderTarget(
frameState,
this.hitRenderTarget_,

View File

@@ -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)

View File

@@ -0,0 +1,409 @@
/**
* @module ol/renderer/webgl/VectorLayer
*/
import BaseVector from '../../layer/BaseVector.js';
import LineStringBatchRenderer from '../../render/webgl/LineStringBatchRenderer.js';
import MixedGeometryBatch from '../../render/webgl/MixedGeometryBatch.js';
import PointBatchRenderer from '../../render/webgl/PointBatchRenderer.js';
import PolygonBatchRenderer from '../../render/webgl/PolygonBatchRenderer.js';
import VectorEventType from '../../source/VectorEventType.js';
import ViewHint from '../../ViewHint.js';
import WebGLLayerRenderer from './Layer.js';
import {DefaultUniform} from '../../webgl/Helper.js';
import {
FILL_FRAGMENT_SHADER,
FILL_VERTEX_SHADER,
POINT_FRAGMENT_SHADER,
POINT_VERTEX_SHADER,
STROKE_FRAGMENT_SHADER,
STROKE_VERTEX_SHADER,
packColor,
} from './shaders.js';
import {buffer, createEmpty, equals, getWidth} from '../../extent.js';
import {create as createTransform} from '../../transform.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {listen, unlistenByKey} from '../../events.js';
/**
* @typedef {function(import("../../Feature").default, Object<string, *>):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<import("./shaders.js").DefaultAttributes,CustomAttributeCallback>} attributes Custom attributes made available in the vertex shader.
* Keys are the names of the attributes which are then accessible in the vertex shader using the `a_` prefix, e.g.: `a_opacity`.
* Default shaders rely on the attributes in {@link module:ol/render/webgl/shaders~DefaultAttributes}.
*/
/**
* @typedef {Object} Options
* @property {string} [className='ol-layer'] A CSS class name to set to the canvas element.
* @property {ShaderProgram} [fill] Attributes and shaders for filling polygons.
* @property {ShaderProgram} [stroke] Attributes and shaders for line strings and polygon strokes.
* @property {ShaderProgram} [point] Attributes and shaders for points.
* @property {Object<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions.
* @property {Array<import("./Layer").PostProcessesOptions>} [postProcesses] Post-processes definitions
*/
/**
* @param {Object<import("./shaders.js").DefaultAttributes,CustomAttributeCallback>} obj Lookup of attribute getters.
* @return {Array<import("../../render/webgl/BatchRenderer").CustomAttribute>} An array of attribute descriptors.
*/
function toAttributesArray(obj) {
return Object.keys(obj).map((key) => ({name: key, callback: obj[key]}));
}
/**
* @classdesc
* Experimental WebGL vector renderer. Supports polygons, lines and points:
* * Polygons are broken down into triangles
* * Lines are rendered as strips of quads
* * Points are rendered as quads
*
* You need to provide vertex and fragment shaders as well as custom attributes for each type of geometry. All shaders
* can access the uniforms in the {@link module:ol/webgl/Helper~DefaultUniform} enum.
* The vertex shaders can access the following attributes depending on the geometry type:
* * For polygons: {@link module:ol/render/webgl/PolygonBatchRenderer~Attributes}
* * For line strings: {@link module:ol/render/webgl/LineStringBatchRenderer~Attributes}
* * For points: {@link module:ol/render/webgl/PointBatchRenderer~Attributes}
*
* Please note that the fragment shaders output should have premultiplied alpha, otherwise visual anomalies may occur.
*
* Note: this uses {@link module:ol/webgl/Helper~WebGLHelper} internally.
*/
class WebGLVectorLayerRenderer extends WebGLLayerRenderer {
/**
* @param {import("../../layer/Layer.js").default} layer Layer.
* @param {Options} options Options.
*/
constructor(layer, options) {
const uniforms = options.uniforms || {};
const projectionMatrixTransform = createTransform();
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
super(layer, {
uniforms: uniforms,
postProcesses: options.postProcesses,
});
this.sourceRevision_ = -1;
this.previousExtent_ = createEmpty();
/**
* This transform is updated on every frame and is the composition of:
* - invert of the world->screen transform that was used when rebuilding buffers (see `this.renderTransform_`)
* - current world->screen transform
* @type {import("../../transform.js").Transform}
* @private
*/
this.currentTransform_ = projectionMatrixTransform;
const fillAttributes = {
color: function () {
return packColor('#ddd');
},
opacity: function () {
return 1;
},
...(options.fill && options.fill.attributes),
};
const strokeAttributes = {
color: function () {
return packColor('#eee');
},
opacity: function () {
return 1;
},
width: function () {
return 1.5;
},
...(options.stroke && options.stroke.attributes),
};
const pointAttributes = {
color: function () {
return packColor('#eee');
},
opacity: function () {
return 1;
},
...(options.point && options.point.attributes),
};
this.fillVertexShader_ =
(options.fill && options.fill.vertexShader) || FILL_VERTEX_SHADER;
this.fillFragmentShader_ =
(options.fill && options.fill.fragmentShader) || FILL_FRAGMENT_SHADER;
this.fillAttributes_ = toAttributesArray(fillAttributes);
this.strokeVertexShader_ =
(options.stroke && options.stroke.vertexShader) || STROKE_VERTEX_SHADER;
this.strokeFragmentShader_ =
(options.stroke && options.stroke.fragmentShader) ||
STROKE_FRAGMENT_SHADER;
this.strokeAttributes_ = toAttributesArray(strokeAttributes);
this.pointVertexShader_ =
(options.point && options.point.vertexShader) || POINT_VERTEX_SHADER;
this.pointFragmentShader_ =
(options.point && options.point.fragmentShader) || POINT_FRAGMENT_SHADER;
this.pointAttributes_ = toAttributesArray(pointAttributes);
/**
* @private
*/
this.worker_ = createWebGLWorker();
/**
* @private
*/
this.batch_ = new MixedGeometryBatch();
const source = this.getLayer().getSource();
this.batch_.addFeatures(source.getFeatures());
this.sourceListenKeys_ = [
listen(
source,
VectorEventType.ADDFEATURE,
this.handleSourceFeatureAdded_,
this
),
listen(
source,
VectorEventType.CHANGEFEATURE,
this.handleSourceFeatureChanged_,
this
),
listen(
source,
VectorEventType.REMOVEFEATURE,
this.handleSourceFeatureDelete_,
this
),
listen(
source,
VectorEventType.CLEAR,
this.handleSourceFeatureClear_,
this
),
];
}
afterHelperCreated() {
this.polygonRenderer_ = new PolygonBatchRenderer(
this.helper,
this.worker_,
this.fillVertexShader_,
this.fillFragmentShader_,
this.fillAttributes_
);
this.pointRenderer_ = new PointBatchRenderer(
this.helper,
this.worker_,
this.pointVertexShader_,
this.pointFragmentShader_,
this.pointAttributes_
);
this.lineStringRenderer_ = new LineStringBatchRenderer(
this.helper,
this.worker_,
this.strokeVertexShader_,
this.strokeFragmentShader_,
this.strokeAttributes_
);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureAdded_(event) {
const feature = event.feature;
this.batch_.addFeature(feature);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureChanged_(event) {
const feature = event.feature;
this.batch_.changeFeature(feature);
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureDelete_(event) {
const feature = event.feature;
this.batch_.removeFeature(feature);
}
/**
* @private
*/
handleSourceFeatureClear_() {
this.batch_.clear();
}
/**
* Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
const gl = this.helper.getGL();
this.preRender(gl, frameState);
const layer = this.getLayer();
const vectorSource = layer.getSource();
const projection = frameState.viewState.projection;
const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
const projectionExtent = projection.getExtent();
const extent = frameState.extent;
const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
const endWorld = multiWorld
? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1
: 1;
let world = multiWorld
? Math.floor((extent[0] - projectionExtent[0]) / worldWidth)
: 0;
do {
this.polygonRenderer_.render(
this.batch_.polygonBatch,
this.currentTransform_,
frameState,
world * worldWidth
);
this.lineStringRenderer_.render(
this.batch_.lineStringBatch,
this.currentTransform_,
frameState,
world * worldWidth
);
this.pointRenderer_.render(
this.batch_.pointBatch,
this.currentTransform_,
frameState,
world * worldWidth
);
} while (++world < endWorld);
this.helper.finalizeDraw(frameState);
const canvas = this.helper.getCanvas();
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const opacity = layerState.opacity;
if (opacity !== parseFloat(canvas.style.opacity)) {
canvas.style.opacity = String(opacity);
}
this.postRender(gl, frameState);
return canvas;
}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrameInternal(frameState) {
const layer = this.getLayer();
const vectorSource = layer.getSource();
const viewState = frameState.viewState;
const viewNotMoving =
!frameState.viewHints[ViewHint.ANIMATING] &&
!frameState.viewHints[ViewHint.INTERACTING];
const extentChanged = !equals(this.previousExtent_, frameState.extent);
const sourceChanged = this.sourceRevision_ < vectorSource.getRevision();
if (sourceChanged) {
this.sourceRevision_ = vectorSource.getRevision();
}
if (viewNotMoving && (extentChanged || sourceChanged)) {
const projection = viewState.projection;
const resolution = viewState.resolution;
const renderBuffer =
layer instanceof BaseVector ? layer.getRenderBuffer() : 0;
const extent = buffer(frameState.extent, renderBuffer * resolution);
vectorSource.loadFeatures(extent, resolution, projection);
this.ready = false;
let remaining = 3;
const rebuildCb = () => {
remaining--;
this.ready = remaining <= 0;
this.getLayer().changed();
};
this.polygonRenderer_.rebuild(
this.batch_.polygonBatch,
frameState,
'Polygon',
rebuildCb
);
this.lineStringRenderer_.rebuild(
this.batch_.lineStringBatch,
frameState,
'LineString',
rebuildCb
);
this.pointRenderer_.rebuild(
this.batch_.pointBatch,
frameState,
'Point',
rebuildCb
);
this.previousExtent_ = frameState.extent.slice();
}
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
this.helper.prepareDraw(frameState);
return true;
}
/**
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
return undefined;
}
/**
* Clean up.
*/
disposeInternal() {
this.worker_.terminate();
this.layer_ = null;
this.sourceListenKeys_.forEach(function (key) {
unlistenByKey(key);
});
this.sourceListenKeys_ = null;
super.disposeInternal();
}
}
export default WebGLVectorLayerRenderer;

View File

@@ -0,0 +1,198 @@
/**
* @module ol/renderer/webgl/shaders
*/
import {asArray} from '../../color.js';
/** @typedef {'color'|'opacity'|'width'} DefaultAttributes */
/**
* Packs red/green/blue channels of a color into a single float value; alpha is ignored.
* This is how the color is expected to be computed.
* @param {import("../../color.js").Color|string} color Color as array of numbers or string
* @return {number} Float value containing the color
*/
export function packColor(color) {
const array = asArray(color);
const r = array[0] * 256 * 256;
const g = array[1] * 256;
const b = array[2];
return r + g + b;
}
const DECODE_COLOR_EXPRESSION = `vec3(
fract(floor(a_color / 256.0 / 256.0) / 256.0),
fract(floor(a_color / 256.0) / 256.0),
fract(a_color / 256.0)
);`;
/**
* Default polygon vertex shader.
* Relies on the color and opacity attributes.
* @type {string}
*/
export const FILL_VERTEX_SHADER = `
precision mediump float;
uniform mat4 u_projectionMatrix;
attribute vec2 a_position;
attribute float a_color;
attribute float a_opacity;
varying vec3 v_color;
varying float v_opacity;
void main(void) {
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0);
v_color = ${DECODE_COLOR_EXPRESSION}
v_opacity = a_opacity;
}`;
/**
* Default polygon fragment shader.
* @type {string}
*/
export const FILL_FRAGMENT_SHADER = `
precision mediump float;
varying vec3 v_color;
varying float v_opacity;
void main(void) {
gl_FragColor = vec4(v_color, 1.0) * v_opacity;
}`;
/**
* Default linestring vertex shader.
* Relies on color, opacity and width attributes.
* @type {string}
*/
export const STROKE_VERTEX_SHADER = `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform vec2 u_sizePx;
attribute vec2 a_segmentStart;
attribute vec2 a_segmentEnd;
attribute float a_parameters;
attribute float a_color;
attribute float a_opacity;
attribute float a_width;
varying vec2 v_segmentStart;
varying vec2 v_segmentEnd;
varying float v_angleStart;
varying float v_angleEnd;
varying vec3 v_color;
varying float v_opacity;
varying float v_width;
vec2 worldToPx(vec2 worldPos) {
vec4 screenPos = u_projectionMatrix * vec4(worldPos, 0.0, 1.0);
return (0.5 * screenPos.xy + 0.5) * u_sizePx;
}
vec4 pxToScreen(vec2 pxPos) {
vec2 screenPos = pxPos * 4.0 / u_sizePx;
return vec4(screenPos.xy, 0.0, 0.0);
}
vec2 getOffsetDirection(vec2 normalPx, vec2 tangentPx, float joinAngle) {
if (cos(joinAngle) > 0.93) return normalPx - tangentPx;
float halfAngle = joinAngle / 2.0;
vec2 angleBisectorNormal = vec2(
sin(halfAngle) * normalPx.x + cos(halfAngle) * normalPx.y,
-cos(halfAngle) * normalPx.x + sin(halfAngle) * normalPx.y
);
float length = 1.0 / sin(halfAngle);
return angleBisectorNormal * length;
}
void main(void) {
float anglePrecision = 1500.0;
float paramShift = 10000.0;
v_angleStart = fract(a_parameters / paramShift) * paramShift / anglePrecision;
v_angleEnd = fract(floor(a_parameters / paramShift + 0.5) / paramShift) * paramShift / anglePrecision;
float vertexNumber = floor(a_parameters / paramShift / paramShift + 0.0001);
vec2 tangentPx = worldToPx(a_segmentEnd) - worldToPx(a_segmentStart);
tangentPx = normalize(tangentPx);
vec2 normalPx = vec2(-tangentPx.y, tangentPx.x);
float normalDir = vertexNumber < 0.5 || (vertexNumber > 1.5 && vertexNumber < 2.5) ? 1.0 : -1.0;
float tangentDir = vertexNumber < 1.5 ? 1.0 : -1.0;
float angle = vertexNumber < 1.5 ? v_angleStart : v_angleEnd;
vec2 offsetPx = getOffsetDirection(normalPx * normalDir, tangentDir * tangentPx, angle) * a_width * 0.5;
vec2 position = vertexNumber < 1.5 ? a_segmentStart : a_segmentEnd;
gl_Position = u_projectionMatrix * vec4(position, 0.0, 1.0) + pxToScreen(offsetPx);
v_segmentStart = worldToPx(a_segmentStart);
v_segmentEnd = worldToPx(a_segmentEnd);
v_color = ${DECODE_COLOR_EXPRESSION}
v_opacity = a_opacity;
v_width = a_width;
}`;
/**
* Default linestring fragment shader.
* @type {string}
*/
export const STROKE_FRAGMENT_SHADER = `
precision mediump float;
uniform float u_pixelRatio;
varying vec2 v_segmentStart;
varying vec2 v_segmentEnd;
varying float v_angleStart;
varying float v_angleEnd;
varying vec3 v_color;
varying float v_opacity;
varying float v_width;
float segmentDistanceField(vec2 point, vec2 start, vec2 end, float radius) {
vec2 startToPoint = point - start;
vec2 startToEnd = end - start;
float ratio = clamp(dot(startToPoint, startToEnd) / dot(startToEnd, startToEnd), 0.0, 1.0);
float dist = length(startToPoint - ratio * startToEnd);
return 1.0 - smoothstep(radius - 1.0, radius, dist);
}
void main(void) {
vec2 v_currentPoint = gl_FragCoord.xy / u_pixelRatio;
gl_FragColor = vec4(v_color, 1.0) * v_opacity;
gl_FragColor *= segmentDistanceField(v_currentPoint, v_segmentStart, v_segmentEnd, v_width);
}`;
/**
* Default point vertex shader.
* Relies on color and opacity attributes.
* @type {string}
*/
export const POINT_VERTEX_SHADER = `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
attribute vec2 a_position;
attribute float a_index;
attribute float a_color;
attribute float a_opacity;
varying vec2 v_texCoord;
varying vec3 v_color;
varying float v_opacity;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
float size = 6.0;
float offsetX = a_index == 0.0 || a_index == 3.0 ? -size / 2.0 : size / 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? -size / 2.0 : size / 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;
float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;
v_texCoord = vec2(u, v);
v_color = ${DECODE_COLOR_EXPRESSION}
v_opacity = a_opacity;
}`;
/**
* Default point fragment shader.
* @type {string}
*/
export const POINT_FRAGMENT_SHADER = `
precision mediump float;
varying vec3 v_color;
varying float v_opacity;
void main(void) {
gl_FragColor = vec4(v_color, 1.0) * v_opacity;
}`;

View File

@@ -38,8 +38,8 @@ export const ShaderType = {
};
/**
* Uniform names used in the default shaders: `PROJECTION_MATRIX`, `OFFSET_SCALE_MATRIX`.
* and `OFFSET_ROTATION_MATRIX`.
* Names of uniforms made available to all shaders.
* Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const DefaultUniform = {
@@ -49,6 +49,8 @@ export const DefaultUniform = {
TIME: 'u_time',
ZOOM: 'u_zoom',
RESOLUTION: 'u_resolution',
SIZE_PX: 'u_sizePx',
PIXEL_RATIO: 'u_pixelRatio',
};
/**
@@ -206,11 +208,12 @@ function releaseCanvas(key) {
* Shaders must be compiled and assembled into a program like so:
* ```js
* // here we simply create two shaders and assemble them in a program which is then used
* // for subsequent rendering calls
* // for subsequent rendering calls; note how a frameState is required to set up a program,
* // as several default uniforms are computed from it (projection matrix, zoom level, etc.)
* const vertexShader = new WebGLVertex(VERTEX_SHADER);
* const fragmentShader = new WebGLFragment(FRAGMENT_SHADER);
* const program = this.context.getProgram(fragmentShader, vertexShader);
* helper.useProgram(this.program);
* helper.useProgram(this.program, frameState);
* ```
*
* Uniforms are defined using the `uniforms` option and can either be explicit values or callbacks taking the frame state as argument.
@@ -302,8 +305,6 @@ function releaseCanvas(key) {
* ```
*
* For an example usage of this class, refer to {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
*
* @api
*/
class WebGLHelper extends Disposable {
/**
@@ -484,7 +485,6 @@ class WebGLHelper extends Disposable {
* the WebGL buffer, bind it, populate it, and add an entry to
* the cache.
* @param {import("./Buffer").default} buffer Buffer.
* @api
*/
bindBuffer(buffer) {
const gl = this.getGL();
@@ -505,7 +505,6 @@ class WebGLHelper extends Disposable {
* Update the data contained in the buffer array; this is required for the
* new data to be rendered
* @param {import("./Buffer").default} buffer Buffer.
* @api
*/
flushBufferData(buffer) {
const gl = this.getGL();
@@ -551,7 +550,6 @@ class WebGLHelper extends Disposable {
* subsequent draw calls.
* @param {import("../PluggableMap.js").FrameState} frameState current frame state
* @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen.
* @api
*/
prepareDraw(frameState, opt_disableAlphaBlend) {
const gl = this.getGL();
@@ -564,8 +562,6 @@ class WebGLHelper extends Disposable {
canvas.style.width = size[0] + 'px';
canvas.style.height = size[1] + 'px';
gl.useProgram(this.currentProgram_);
// loop backwards in post processes list
for (let i = this.postProcessPasses_.length - 1; i >= 0; i--) {
this.postProcessPasses_[i].init(frameState);
@@ -581,10 +577,6 @@ class WebGLHelper extends Disposable {
gl.ONE,
opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA
);
gl.useProgram(this.currentProgram_);
this.applyFrameState(frameState);
this.applyUniforms(frameState);
}
/**
@@ -609,17 +601,12 @@ class WebGLHelper extends Disposable {
gl.ONE,
opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA
);
gl.useProgram(this.currentProgram_);
this.applyFrameState(frameState);
this.applyUniforms(frameState);
}
/**
* Execute a draw call based on the currently bound program, texture, buffers, attributes.
* @param {number} start Start index.
* @param {number} end End index.
* @api
*/
drawElements(start, end) {
const gl = this.getGL();
@@ -660,7 +647,6 @@ class WebGLHelper extends Disposable {
/**
* @return {HTMLCanvasElement} Canvas.
* @api
*/
getCanvas() {
return this.canvas_;
@@ -669,7 +655,6 @@ class WebGLHelper extends Disposable {
/**
* Get the WebGL rendering context
* @return {WebGLRenderingContext} The rendering context.
* @api
*/
getGL() {
return this.gl_;
@@ -682,6 +667,7 @@ class WebGLHelper extends Disposable {
applyFrameState(frameState) {
const size = frameState.size;
const rotation = frameState.viewState.rotation;
const pixelRatio = frameState.pixelRatio;
const offsetScaleMatrix = resetTransform(this.offsetScaleMatrix_);
scaleTransform(offsetScaleMatrix, 2 / size[0], 2 / size[1]);
@@ -709,6 +695,8 @@ class WebGLHelper extends Disposable {
DefaultUniform.RESOLUTION,
frameState.viewState.resolution
);
this.setUniformFloatValue(DefaultUniform.PIXEL_RATIO, pixelRatio);
this.setUniformFloatVec2(DefaultUniform.SIZE_PX, [size[0], size[1]]);
}
/**
@@ -803,22 +791,19 @@ class WebGLHelper extends Disposable {
}
/**
* Use a program. If the program is already in use, this will return `false`.
* Set up a program for use. The program will be set as the current one. Then, the uniforms used
* in the program will be set based on the current frame state and the helper configuration.
* @param {WebGLProgram} program Program.
* @return {boolean} Changed.
* @api
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
useProgram(program) {
if (program == this.currentProgram_) {
return false;
} else {
const gl = this.getGL();
gl.useProgram(program);
this.currentProgram_ = program;
this.uniformLocations_ = {};
this.attribLocations_ = {};
return true;
}
useProgram(program, frameState) {
const gl = this.getGL();
gl.useProgram(program);
this.currentProgram_ = program;
this.uniformLocations_ = {};
this.attribLocations_ = {};
this.applyFrameState(frameState);
this.applyUniforms(frameState);
}
/**
@@ -843,7 +828,6 @@ class WebGLHelper extends Disposable {
* @param {string} fragmentShaderSource Fragment shader source.
* @param {string} vertexShaderSource Vertex shader source.
* @return {WebGLProgram} Program
* @api
*/
getProgram(fragmentShaderSource, vertexShaderSource) {
const gl = this.getGL();
@@ -893,7 +877,6 @@ class WebGLHelper extends Disposable {
* Will get the location from the shader or the cache
* @param {string} name Uniform name
* @return {WebGLUniformLocation} uniformLocation
* @api
*/
getUniformLocation(name) {
if (this.uniformLocations_[name] === undefined) {
@@ -909,7 +892,6 @@ class WebGLHelper extends Disposable {
* Will get the location from the shader or the cache
* @param {string} name Attribute name
* @return {number} attribLocation
* @api
*/
getAttributeLocation(name) {
if (this.attribLocations_[name] === undefined) {
@@ -927,7 +909,6 @@ class WebGLHelper extends Disposable {
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../transform").Transform} transform Transform to update.
* @return {import("../transform").Transform} The updated transform object.
* @api
*/
makeProjectionTransform(frameState, transform) {
const size = frameState.size;
@@ -953,12 +934,20 @@ class WebGLHelper extends Disposable {
* Give a value for a standard float uniform
* @param {string} uniform Uniform name
* @param {number} value Value
* @api
*/
setUniformFloatValue(uniform, value) {
this.getGL().uniform1f(this.getUniformLocation(uniform), value);
}
/**
* Give a value for a vec2 uniform
* @param {string} uniform Uniform name
* @param {Array<number>} value Array of length 4.
*/
setUniformFloatVec2(uniform, value) {
this.getGL().uniform2fv(this.getUniformLocation(uniform), value);
}
/**
* Give a value for a vec4 uniform
* @param {string} uniform Uniform name
@@ -972,7 +961,6 @@ class WebGLHelper extends Disposable {
* Give a value for a standard matrix4 uniform
* @param {string} uniform Uniform name
* @param {Array<number>} value Matrix value
* @api
*/
setUniformMatrixValue(uniform, value) {
this.getGL().uniformMatrix4fv(
@@ -1014,7 +1002,6 @@ class WebGLHelper extends Disposable {
* i.e. tell the GPU where to read the different attributes in the buffer. An error in the
* size/type/order of attributes will most likely break the rendering and throw a WebGL exception.
* @param {Array<AttributeDescription>} attributes Ordered list of attributes to read from the buffer
* @api
*/
enableAttributes(attributes) {
const stride = computeAttributesStride(attributes);
@@ -1056,7 +1043,6 @@ class WebGLHelper extends Disposable {
* @param {ImageData|HTMLImageElement|HTMLCanvasElement} [opt_data] Image data/object to bind to the texture
* @param {WebGLTexture} [opt_texture] Existing texture to reuse
* @return {WebGLTexture} The generated texture
* @api
*/
createTexture(size, opt_data, opt_texture) {
const gl = this.getGL();
@@ -1103,7 +1089,6 @@ class WebGLHelper extends Disposable {
* Compute a stride in bytes based on a list of attributes
* @param {Array<AttributeDescription>} 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;

View File

@@ -2,60 +2,176 @@
* A worker that does cpu-heavy tasks related to webgl rendering.
* @module ol/worker/webgl
*/
import {
WebGLWorkerMessageType,
writePointFeatureToBuffers,
} from '../renderer/webgl/Layer.js';
import {WebGLWorkerMessageType} from '../render/webgl/constants.js';
import {assign} from '../obj.js';
import {
create as createTransform,
makeInverse as makeInverseTransform,
} from '../transform.js';
import {
writeLineSegmentToBuffers,
writePointFeatureToBuffers,
writePolygonTrianglesToBuffers,
} from '../render/webgl/utils.js';
/** @type {any} */
const worker = self;
worker.onmessage = (event) => {
const received = event.data;
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
// This is specific to point features (x, y, index)
const baseVertexAttrsCount = 3;
const baseInstructionsCount = 2;
switch (received.type) {
case WebGLWorkerMessageType.GENERATE_POINT_BUFFERS: {
// This is specific to point features (x, y, index)
const baseVertexAttrsCount = 3;
const baseInstructionsCount = 2;
const customAttrsCount = received.customAttributesCount;
const instructionsCount = baseInstructionsCount + customAttrsCount;
const renderInstructions = new Float32Array(received.renderInstructions);
const customAttrsCount = received.customAttributesCount;
const instructionsCount = baseInstructionsCount + customAttrsCount;
const renderInstructions = new Float32Array(received.renderInstructions);
const elementsCount = renderInstructions.length / instructionsCount;
const indicesCount = elementsCount * 6;
const verticesCount =
elementsCount * 4 * (customAttrsCount + baseVertexAttrsCount);
const indexBuffer = new Uint32Array(indicesCount);
const vertexBuffer = new Float32Array(verticesCount);
const elementsCount = renderInstructions.length / instructionsCount;
const indicesCount = elementsCount * 6;
const verticesCount =
elementsCount * 4 * (customAttrsCount + baseVertexAttrsCount);
const indexBuffer = new Uint32Array(indicesCount);
const vertexBuffer = new Float32Array(verticesCount);
let bufferPositions;
for (let i = 0; i < renderInstructions.length; i += instructionsCount) {
bufferPositions = writePointFeatureToBuffers(
renderInstructions,
i,
vertexBuffer,
indexBuffer,
customAttrsCount,
bufferPositions
let bufferPositions;
for (let i = 0; i < renderInstructions.length; i += instructionsCount) {
bufferPositions = writePointFeatureToBuffers(
renderInstructions,
i,
vertexBuffer,
indexBuffer,
customAttrsCount,
bufferPositions
);
}
/** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = assign(
{
vertexBuffer: vertexBuffer.buffer,
indexBuffer: indexBuffer.buffer,
renderInstructions: renderInstructions.buffer,
},
received
);
worker.postMessage(message, [
vertexBuffer.buffer,
indexBuffer.buffer,
renderInstructions.buffer,
]);
break;
}
case WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS: {
const vertices = [];
const indices = [];
/** @type {import('../renderer/webgl/Layer').WebGLWorkerGenerateBuffersMessage} */
const message = assign(
{
vertexBuffer: vertexBuffer.buffer,
indexBuffer: indexBuffer.buffer,
renderInstructions: renderInstructions.buffer,
},
received
);
const customAttrsCount = received.customAttributesCount;
const instructionsPerVertex = 2;
worker.postMessage(message, [
vertexBuffer.buffer,
indexBuffer.buffer,
renderInstructions.buffer,
]);
const renderInstructions = new Float32Array(received.renderInstructions);
let currentInstructionsIndex = 0;
const transform = received.renderInstructionsTransform;
const invertTransform = createTransform();
makeInverseTransform(invertTransform, transform);
let verticesCount, customAttributes;
while (currentInstructionsIndex < renderInstructions.length) {
customAttributes = Array.from(
renderInstructions.slice(
currentInstructionsIndex,
currentInstructionsIndex + customAttrsCount
)
);
currentInstructionsIndex += customAttrsCount;
verticesCount = renderInstructions[currentInstructionsIndex++];
// last point is only a segment end, do not loop over it
for (let i = 0; i < verticesCount - 1; i++) {
writeLineSegmentToBuffers(
renderInstructions,
currentInstructionsIndex + i * instructionsPerVertex,
currentInstructionsIndex + (i + 1) * instructionsPerVertex,
i > 0
? currentInstructionsIndex + (i - 1) * instructionsPerVertex
: null,
i < verticesCount - 2
? currentInstructionsIndex + (i + 2) * instructionsPerVertex
: null,
vertices,
indices,
customAttributes,
transform,
invertTransform
);
}
currentInstructionsIndex += verticesCount * instructionsPerVertex;
}
const indexBuffer = Uint32Array.from(indices);
const vertexBuffer = Float32Array.from(vertices);
/** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = assign(
{
vertexBuffer: vertexBuffer.buffer,
indexBuffer: indexBuffer.buffer,
renderInstructions: renderInstructions.buffer,
},
received
);
worker.postMessage(message, [
vertexBuffer.buffer,
indexBuffer.buffer,
renderInstructions.buffer,
]);
break;
}
case WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS: {
const vertices = [];
const indices = [];
const customAttrsCount = received.customAttributesCount;
const renderInstructions = new Float32Array(received.renderInstructions);
let currentInstructionsIndex = 0;
while (currentInstructionsIndex < renderInstructions.length) {
currentInstructionsIndex = writePolygonTrianglesToBuffers(
renderInstructions,
currentInstructionsIndex,
vertices,
indices,
customAttrsCount
);
}
const indexBuffer = Uint32Array.from(indices);
const vertexBuffer = Float32Array.from(vertices);
/** @type {import('../render/webgl/constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = assign(
{
vertexBuffer: vertexBuffer.buffer,
indexBuffer: indexBuffer.buffer,
renderInstructions: renderInstructions.buffer,
},
received
);
worker.postMessage(message, [
vertexBuffer.buffer,
indexBuffer.buffer,
renderInstructions.buffer,
]);
break;
}
default:
// pass
}
};

View File

@@ -0,0 +1,307 @@
import Feature from '../../../../../../src/ol/Feature.js';
import LineString from '../../../../../../src/ol/geom/LineString.js';
import LineStringBatchRenderer from '../../../../../../src/ol/render/webgl/LineStringBatchRenderer.js';
import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js';
import Point from '../../../../../../src/ol/geom/Point.js';
import PointBatchRenderer from '../../../../../../src/ol/render/webgl/PointBatchRenderer.js';
import Polygon from '../../../../../../src/ol/geom/Polygon.js';
import PolygonBatchRenderer from '../../../../../../src/ol/render/webgl/PolygonBatchRenderer.js';
import WebGLHelper from '../../../../../../src/ol/webgl/Helper.js';
import {FLOAT} from '../../../../../../src/ol/webgl.js';
import {WebGLWorkerMessageType} from '../../../../../../src/ol/render/webgl/constants.js';
import {
create as createTransform,
translate as translateTransform,
} from '../../../../../../src/ol/transform.js';
import {create as createWebGLWorker} from '../../../../../../src/ol/worker/webgl.js';
const POINT_VERTEX_SHADER = `precision mediump float;
void main(void) {}`;
const POINT_FRAGMENT_SHADER = `precision mediump float;
void main(void) {}`;
const SAMPLE_FRAMESTATE = {
viewState: {
center: [0, 10],
resolution: 1,
rotation: 0,
},
size: [10, 10],
};
describe('Batch renderers', function () {
let batchRenderer, helper, mixedBatch, worker, attributes;
beforeEach(function () {
helper = new WebGLHelper();
worker = createWebGLWorker();
attributes = [
{
name: 'test',
callback: function (feature, properties) {
return feature.get('test');
},
},
];
mixedBatch = new MixedGeometryBatch();
mixedBatch.addFeatures([
new Feature({
test: 1000,
geometry: new Point([10, 20]),
}),
new Feature({
test: 2000,
geometry: new Point([30, 40]),
}),
new Feature({
test: 3000,
geometry: new Polygon([
[
[10, 10],
[20, 10],
[30, 20],
[20, 40],
[10, 10],
],
]),
}),
new Feature({
test: 4000,
geometry: new LineString([
[100, 200],
[300, 400],
[500, 600],
]),
}),
]);
});
describe('PointBatchRenderer', function () {
beforeEach(function () {
batchRenderer = new PointBatchRenderer(
helper,
worker,
POINT_VERTEX_SHADER,
POINT_FRAGMENT_SHADER,
attributes
);
});
describe('constructor', function () {
it('generates the attributes list', function () {
expect(batchRenderer.attributes).to.eql([
{name: 'a_position', size: 2, type: FLOAT},
{name: 'a_index', size: 1, type: FLOAT},
{name: 'a_test', size: 1, type: FLOAT},
]);
});
});
describe('#rebuild', function () {
let rebuildCb;
beforeEach(function (done) {
sinon.spy(helper, 'flushBufferData');
rebuildCb = sinon.spy();
batchRenderer.rebuild(
mixedBatch.pointBatch,
SAMPLE_FRAMESTATE,
'Point',
rebuildCb
);
// wait for worker response for our specific message
worker.addEventListener('message', function (event) {
if (
event.data.type === WebGLWorkerMessageType.GENERATE_POINT_BUFFERS &&
event.data.renderInstructions.byteLength > 0
) {
done();
}
});
});
it('generates render instructions and updates buffers from the worker response', function () {
expect(Array.from(mixedBatch.pointBatch.renderInstructions)).to.eql([
2, 2, 1000, 6, 6, 2000,
]);
});
it('updates buffers', function () {
expect(
mixedBatch.pointBatch.verticesBuffer.getArray().length
).to.be.greaterThan(0);
expect(
mixedBatch.pointBatch.indicesBuffer.getArray().length
).to.be.greaterThan(0);
expect(helper.flushBufferData.calledTwice).to.be(true);
});
it('updates the instructions transform', function () {
expect(mixedBatch.pointBatch.renderInstructionsTransform).to.eql([
0.2, 0, 0, 0.2, 0, -2,
]);
});
it('calls the provided callback', function () {
expect(rebuildCb.calledOnce).to.be(true);
});
});
describe('#render (from parent)', function () {
let transform;
const offsetX = 12;
beforeEach(function () {
sinon.spy(helper, 'makeProjectionTransform');
sinon.spy(helper, 'useProgram');
sinon.spy(helper, 'bindBuffer');
sinon.spy(helper, 'enableAttributes');
sinon.spy(helper, 'drawElements');
transform = createTransform();
batchRenderer.render(
mixedBatch.pointBatch,
transform,
SAMPLE_FRAMESTATE,
offsetX
);
});
it('computes current transform', function () {
expect(helper.makeProjectionTransform.calledOnce).to.be(true);
});
it('includes the X offset in the transform used for rendering', function () {
const expected = helper.makeProjectionTransform(
SAMPLE_FRAMESTATE,
createTransform()
);
translateTransform(expected, offsetX, 0);
expect(transform).to.eql(expected);
});
it('computes sets up render parameters', function () {
expect(helper.useProgram.calledOnce).to.be(true);
expect(helper.enableAttributes.calledOnce).to.be(true);
expect(helper.bindBuffer.calledTwice).to.be(true);
});
it('renders elements', function () {
expect(helper.drawElements.calledOnce).to.be(true);
});
});
});
describe('LineStringBatchRenderer', function () {
beforeEach(function () {
batchRenderer = new LineStringBatchRenderer(
helper,
worker,
POINT_VERTEX_SHADER,
POINT_FRAGMENT_SHADER,
attributes
);
});
describe('constructor', function () {
it('generates the attributes list', function () {
expect(batchRenderer.attributes).to.eql([
{name: 'a_segmentStart', size: 2, type: FLOAT},
{name: 'a_segmentEnd', size: 2, type: FLOAT},
{name: 'a_parameters', size: 1, type: FLOAT},
{name: 'a_test', size: 1, type: FLOAT},
]);
});
});
describe('#rebuild', function () {
let rebuildCb;
beforeEach(function (done) {
sinon.spy(helper, 'flushBufferData');
rebuildCb = sinon.spy();
batchRenderer.rebuild(
mixedBatch.lineStringBatch,
SAMPLE_FRAMESTATE,
'LineString',
rebuildCb
);
// wait for worker response for our specific message
worker.addEventListener('message', function (event) {
if (
event.data.type ===
WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS &&
event.data.renderInstructions.byteLength > 0
) {
done();
}
});
});
it('generates render instructions and updates buffers from the worker response', function () {
expect(
Array.from(mixedBatch.lineStringBatch.renderInstructions)
).to.eql([
3000, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0, 4000, 3, 20, 38, 60, 78, 100,
118,
]);
});
it('updates buffers', function () {
expect(
mixedBatch.lineStringBatch.verticesBuffer.getArray().length
).to.be.greaterThan(0);
expect(
mixedBatch.lineStringBatch.indicesBuffer.getArray().length
).to.be.greaterThan(0);
expect(helper.flushBufferData.calledTwice).to.be(true);
});
it('calls the provided callback', function () {
expect(rebuildCb.calledOnce).to.be(true);
});
});
});
describe('PolygonBatchRenderer', function () {
beforeEach(function () {
batchRenderer = new PolygonBatchRenderer(
helper,
worker,
POINT_VERTEX_SHADER,
POINT_FRAGMENT_SHADER,
attributes
);
});
describe('constructor', function () {
it('generates the attributes list', function () {
expect(batchRenderer.attributes).to.eql([
{name: 'a_position', size: 2, type: FLOAT},
{name: 'a_test', size: 1, type: FLOAT},
]);
});
});
describe('#rebuild', function () {
let rebuildCb;
beforeEach(function (done) {
sinon.spy(helper, 'flushBufferData');
rebuildCb = sinon.spy();
batchRenderer.rebuild(
mixedBatch.polygonBatch,
SAMPLE_FRAMESTATE,
'Polygon',
rebuildCb
);
// wait for worker response for our specific message
worker.addEventListener('message', function (event) {
if (
event.data.type ===
WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS &&
event.data.renderInstructions.byteLength > 0
) {
done();
}
});
});
it('generates render instructions and updates buffers from the worker response', function () {
expect(Array.from(mixedBatch.polygonBatch.renderInstructions)).to.eql([
3000, 1, 5, 2, 0, 4, 0, 6, 2, 4, 6, 2, 0,
]);
});
it('updates buffers', function () {
expect(
mixedBatch.polygonBatch.verticesBuffer.getArray().length
).to.be.greaterThan(0);
expect(
mixedBatch.polygonBatch.indicesBuffer.getArray().length
).to.be.greaterThan(0);
expect(helper.flushBufferData.calledTwice).to.be(true);
});
it('calls the provided callback', function () {
expect(rebuildCb.calledOnce).to.be(true);
});
});
});
});

View File

@@ -0,0 +1,763 @@
import Feature from '../../../../../../src/ol/Feature.js';
import GeometryCollection from '../../../../../../src/ol/geom/GeometryCollection.js';
import LineString from '../../../../../../src/ol/geom/LineString.js';
import LinearRing from '../../../../../../src/ol/geom/LinearRing.js';
import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js';
import MultiLineString from '../../../../../../src/ol/geom/MultiLineString.js';
import MultiPoint from '../../../../../../src/ol/geom/MultiPoint.js';
import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.js';
import Point from '../../../../../../src/ol/geom/Point.js';
import Polygon from '../../../../../../src/ol/geom/Polygon.js';
import {getUid} from '../../../../../../src/ol/index.js';
describe('MixedGeometryBatch', function () {
let mixedBatch;
beforeEach(() => {
mixedBatch = new MixedGeometryBatch();
});
describe('#addFeatures', () => {
let features, spy;
beforeEach(() => {
features = [new Feature(), new Feature(), new Feature()];
spy = sinon.spy(mixedBatch, 'addFeature');
mixedBatch.addFeatures(features);
});
it('calls addFeature for each feature', () => {
expect(spy.callCount).to.be(3);
expect(spy.args[0][0]).to.be(features[0]);
expect(spy.args[1][0]).to.be(features[1]);
expect(spy.args[2][0]).to.be(features[2]);
});
});
describe('features with Point geometries', () => {
let geometry1, feature1, geometry2, feature2;
beforeEach(() => {
geometry1 = new Point([0, 1]);
feature1 = new Feature({
geometry: geometry1,
prop1: 'abcd',
prop2: 'efgh',
});
geometry2 = new Point([2, 3]);
feature2 = new Feature({
geometry: geometry2,
prop3: '1234',
prop4: '5678',
});
});
describe('#addFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
it('puts the geometries in the point batch', () => {
const keys = Object.keys(mixedBatch.pointBatch.entries);
const uid1 = getUid(feature1);
const uid2 = getUid(feature2);
expect(keys).to.eql([uid1, uid2]);
expect(mixedBatch.pointBatch.entries[uid1]).to.eql({
feature: feature1,
flatCoordss: [[0, 1]],
});
expect(mixedBatch.pointBatch.entries[uid2]).to.eql({
feature: feature2,
flatCoordss: [[2, 3]],
});
});
it('computes the geometries count', () => {
expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
});
it('leaves other batches untouched', () => {
expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length(
0
);
});
});
describe('#changeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
describe('modifying geometry and props', () => {
beforeEach(() => {
feature1.set('prop1', 'changed');
geometry1.setCoordinates([100, 101]);
mixedBatch.changeFeature(feature1);
});
it('updates the modified properties and geometry in the point batch', () => {
const entry = mixedBatch.pointBatch.entries[getUid(feature1)];
expect(entry.feature.get('prop1')).to.eql('changed');
});
it('keeps geometry count the same', () => {
expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
});
});
describe('changing the geometry', () => {
let newGeom;
beforeEach(() => {
newGeom = new Point([40, 41]);
feature1.setGeometry(newGeom);
mixedBatch.changeFeature(feature1);
});
it('updates the geometry in the point batch', () => {
const entry = mixedBatch.pointBatch.entries[getUid(feature1)];
expect(entry.flatCoordss).to.eql([[40, 41]]);
});
it('keeps geometry count the same', () => {
expect(mixedBatch.pointBatch.geometriesCount).to.be(2);
});
});
});
describe('#removeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
mixedBatch.removeFeature(feature1);
});
it('clears the entry related to this feature', () => {
const keys = Object.keys(mixedBatch.pointBatch.entries);
expect(keys).to.not.contain(getUid(feature1));
});
it('recompute geometry count', () => {
expect(mixedBatch.pointBatch.geometriesCount).to.be(1);
});
});
});
describe('features with LineString geometries', () => {
let geometry1, feature1, geometry2, feature2;
beforeEach(() => {
geometry1 = new LineString([
[0, 1],
[2, 3],
[4, 5],
[6, 7],
]);
feature1 = new Feature({
geometry: geometry1,
prop1: 'abcd',
prop2: 'efgh',
});
geometry2 = new LineString([
[8, 9],
[10, 11],
[12, 13],
]);
feature2 = new Feature({
geometry: geometry2,
prop3: '1234',
prop4: '5678',
});
});
describe('#addFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
it('puts the geometries in the linestring batch', () => {
const keys = Object.keys(mixedBatch.lineStringBatch.entries);
const uid1 = getUid(feature1);
const uid2 = getUid(feature2);
expect(keys).to.eql([uid1, uid2]);
expect(mixedBatch.lineStringBatch.entries[uid1]).to.eql({
feature: feature1,
flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7]],
verticesCount: 4,
});
expect(mixedBatch.lineStringBatch.entries[uid2]).to.eql({
feature: feature2,
flatCoordss: [[8, 9, 10, 11, 12, 13]],
verticesCount: 3,
});
});
it('computes the aggregated metrics on all geoms', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(7);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
});
it('leaves other batches untouched', () => {
expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
});
});
describe('#changeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
describe('modifying geometry and props', () => {
beforeEach(() => {
feature1.set('prop1', 'changed');
geometry1.appendCoordinate([100, 101]);
geometry1.appendCoordinate([102, 103]);
mixedBatch.changeFeature(feature1);
});
it('updates the modified properties and geometry in the linestring batch', () => {
const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)];
expect(entry.feature.get('prop1')).to.eql('changed');
expect(entry.verticesCount).to.eql(6);
expect(entry.flatCoordss).to.eql([
[0, 1, 2, 3, 4, 5, 6, 7, 100, 101, 102, 103],
]);
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(9);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
});
});
describe('changing the geometry', () => {
let newGeom;
beforeEach(() => {
newGeom = new LineString([
[40, 41],
[42, 43],
]);
feature1.setGeometry(newGeom);
mixedBatch.changeFeature(feature1);
});
it('updates the geometry in the linestring batch', () => {
const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)];
expect(entry.flatCoordss).to.eql([[40, 41, 42, 43]]);
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(5);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2);
});
});
});
describe('#removeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
mixedBatch.removeFeature(feature1);
});
it('clears the entry related to this feature', () => {
const keys = Object.keys(mixedBatch.lineStringBatch.entries);
expect(keys).to.not.contain(getUid(feature1));
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(3);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1);
});
});
});
describe('features with Polygon geometries', () => {
let geometry1, feature1, geometry2, feature2;
beforeEach(() => {
geometry1 = new Polygon([
[
[0, 1],
[2, 3],
[4, 5],
[6, 7],
],
[
[20, 21],
[22, 23],
[24, 25],
],
]);
feature1 = new Feature({
geometry: geometry1,
prop1: 'abcd',
prop2: 'efgh',
});
geometry2 = new Polygon([
[
[8, 9],
[10, 11],
[12, 13],
],
[
[30, 31],
[32, 33],
[34, 35],
],
[
[40, 41],
[42, 43],
[44, 45],
[46, 47],
],
]);
feature2 = new Feature({
geometry: geometry2,
prop3: '1234',
prop4: '5678',
});
});
describe('#addFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
it('puts the polygons in the polygon batch', () => {
const keys = Object.keys(mixedBatch.polygonBatch.entries);
const uid1 = getUid(feature1);
const uid2 = getUid(feature2);
expect(keys).to.eql([uid1, uid2]);
expect(mixedBatch.polygonBatch.entries[uid1]).to.eql({
feature: feature1,
flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25]],
verticesCount: 7,
ringsCount: 2,
ringsVerticesCounts: [[4, 3]],
});
expect(mixedBatch.polygonBatch.entries[uid2]).to.eql({
feature: feature2,
flatCoordss: [
[
8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44,
45, 46, 47,
],
],
verticesCount: 10,
ringsCount: 3,
ringsVerticesCounts: [[3, 3, 4]],
});
});
it('computes the aggregated metrics on all polygons', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(17);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
expect(mixedBatch.polygonBatch.ringsCount).to.be(5);
});
it('puts the linear rings in the linestring batch', () => {
const keys = Object.keys(mixedBatch.lineStringBatch.entries);
expect(keys).to.eql([getUid(feature1), getUid(feature2)]);
expect(mixedBatch.lineStringBatch.entries[getUid(feature1)]).to.eql({
feature: feature1,
flatCoordss: [
[0, 1, 2, 3, 4, 5, 6, 7],
[20, 21, 22, 23, 24, 25],
],
verticesCount: 7,
});
expect(mixedBatch.lineStringBatch.entries[getUid(feature2)]).to.eql({
feature: feature2,
flatCoordss: [
[8, 9, 10, 11, 12, 13],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45, 46, 47],
],
verticesCount: 10,
});
});
it('computes the aggregated metrics on all linestrings', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(17);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(5);
});
it('leaves point batch untouched', () => {
expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
});
});
describe('#changeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
});
describe('modifying geometry and props', () => {
beforeEach(() => {
feature1.set('prop1', 'changed');
geometry1.appendLinearRing(
new LinearRing([
[201, 202],
[203, 204],
[205, 206],
[207, 208],
])
);
mixedBatch.changeFeature(feature1);
});
it('updates the modified properties and geometry in the polygon batch', () => {
const entry = mixedBatch.polygonBatch.entries[getUid(feature1)];
expect(entry.feature.get('prop1')).to.eql('changed');
expect(entry.verticesCount).to.eql(11);
expect(entry.ringsCount).to.eql(3);
expect(entry.ringsVerticesCounts).to.eql([[4, 3, 4]]);
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(21);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
expect(mixedBatch.polygonBatch.ringsCount).to.be(6);
});
});
describe('changing the geometry', () => {
let newGeom;
beforeEach(() => {
newGeom = new Polygon([
[
[201, 202],
[203, 204],
[205, 206],
[207, 208],
],
]);
feature1.setGeometry(newGeom);
mixedBatch.changeFeature(feature1);
});
it('updates the geometry in the polygon batch', () => {
const entry = mixedBatch.polygonBatch.entries[getUid(feature1)];
expect(entry.feature).to.be(feature1);
expect(entry.verticesCount).to.eql(4);
expect(entry.ringsCount).to.eql(1);
expect(entry.ringsVerticesCounts).to.eql([[4]]);
expect(entry.flatCoordss).to.eql([
[201, 202, 203, 204, 205, 206, 207, 208],
]);
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(14);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
expect(mixedBatch.polygonBatch.ringsCount).to.be(4);
});
});
});
describe('#removeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
mixedBatch.removeFeature(feature1);
});
it('clears the entry related to this feature', () => {
const keys = Object.keys(mixedBatch.polygonBatch.entries);
expect(keys).to.not.contain(getUid(feature1));
});
it('updates the aggregated metrics on all geoms', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(10);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(1);
expect(mixedBatch.polygonBatch.ringsCount).to.be(3);
});
});
});
describe('feature with nested geometries (collection, multi)', () => {
let feature, geomCollection, multiPolygon, multiPoint, multiLine;
beforeEach(() => {
multiPoint = new MultiPoint([
[101, 102],
[201, 202],
[301, 302],
]);
multiLine = new MultiLineString([
[
[0, 1],
[2, 3],
[4, 5],
[6, 7],
],
[
[8, 9],
[10, 11],
[12, 13],
],
]);
multiPolygon = new MultiPolygon([
[
[
[0, 1],
[2, 3],
[4, 5],
[6, 7],
],
[
[20, 21],
[22, 23],
[24, 25],
],
],
[
[
[8, 9],
[10, 11],
[12, 13],
],
[
[30, 31],
[32, 33],
[34, 35],
],
[
[40, 41],
[42, 43],
[44, 45],
[46, 47],
],
],
]);
geomCollection = new GeometryCollection([
multiPolygon,
multiLine,
multiPoint,
]);
feature = new Feature({
geometry: geomCollection,
prop3: '1234',
prop4: '5678',
});
});
describe('#addFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature);
});
it('puts the polygons in the polygon batch', () => {
const uid = getUid(feature);
expect(mixedBatch.polygonBatch.entries[uid]).to.eql({
feature: feature,
flatCoordss: [
[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25],
[
8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44,
45, 46, 47,
],
],
verticesCount: 17,
ringsCount: 5,
ringsVerticesCounts: [
[4, 3],
[3, 3, 4],
],
});
});
it('puts the polygon rings and linestrings in the linestring batch', () => {
const uid = getUid(feature);
expect(mixedBatch.lineStringBatch.entries[uid]).to.eql({
feature: feature,
flatCoordss: [
[0, 1, 2, 3, 4, 5, 6, 7],
[20, 21, 22, 23, 24, 25],
[8, 9, 10, 11, 12, 13],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45, 46, 47],
[0, 1, 2, 3, 4, 5, 6, 7],
[8, 9, 10, 11, 12, 13],
],
verticesCount: 24,
});
});
it('puts the points in the linestring batch', () => {
const uid = getUid(feature);
expect(mixedBatch.pointBatch.entries[uid]).to.eql({
feature: feature,
flatCoordss: [
[101, 102],
[201, 202],
[301, 302],
],
});
});
it('computes the aggregated metrics on all polygons', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(17);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(2);
expect(mixedBatch.polygonBatch.ringsCount).to.be(5);
});
it('computes the aggregated metrics on all linestring', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(24);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(7);
});
it('computes the aggregated metrics on all points', () => {
expect(mixedBatch.pointBatch.geometriesCount).to.be(3);
});
});
describe('#changeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature);
});
describe('modifying geometry', () => {
beforeEach(() => {
multiLine.appendLineString(
new LineString([
[500, 501],
[502, 503],
[504, 505],
[506, 507],
])
);
multiPolygon.appendPolygon(
new Polygon([
[
[201, 202],
[203, 204],
[205, 206],
[207, 208],
[209, 210],
],
])
);
mixedBatch.changeFeature(feature);
});
it('updates the geometries in the polygon batch', () => {
const entry = mixedBatch.polygonBatch.entries[getUid(feature)];
expect(entry).to.eql({
feature: feature,
flatCoordss: [
[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25],
[
8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43,
44, 45, 46, 47,
],
[201, 202, 203, 204, 205, 206, 207, 208, 209, 210],
],
verticesCount: 22,
ringsCount: 6,
ringsVerticesCounts: [[4, 3], [3, 3, 4], [5]],
});
});
it('updates the geometries in the linestring batch', () => {
const entry = mixedBatch.lineStringBatch.entries[getUid(feature)];
expect(entry).to.eql({
feature: feature,
flatCoordss: [
[0, 1, 2, 3, 4, 5, 6, 7],
[20, 21, 22, 23, 24, 25],
[8, 9, 10, 11, 12, 13],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45, 46, 47],
[201, 202, 203, 204, 205, 206, 207, 208, 209, 210],
[0, 1, 2, 3, 4, 5, 6, 7],
[8, 9, 10, 11, 12, 13],
[500, 501, 502, 503, 504, 505, 506, 507],
],
verticesCount: 33,
});
});
it('updates the aggregated metrics on the polygon batch', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(22);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(3);
expect(mixedBatch.polygonBatch.ringsCount).to.be(6);
});
it('updates the aggregated metrics on the linestring batch', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(33);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(9);
});
});
describe('changing the geometry', () => {
beforeEach(() => {
feature.setGeometry(
new Polygon([
[
[201, 202],
[203, 204],
[205, 206],
[207, 208],
],
])
);
mixedBatch.changeFeature(feature);
});
it('updates the geometries in the polygon batch', () => {
const entry = mixedBatch.polygonBatch.entries[getUid(feature)];
expect(entry).to.eql({
feature: feature,
flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]],
verticesCount: 4,
ringsCount: 1,
ringsVerticesCounts: [[4]],
});
});
it('updates the geometries in the linestring batch', () => {
const entry = mixedBatch.lineStringBatch.entries[getUid(feature)];
expect(entry).to.eql({
feature: feature,
flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]],
verticesCount: 4,
});
});
it('updates the aggregated metrics on the polygon batch', () => {
expect(mixedBatch.polygonBatch.verticesCount).to.be(4);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(1);
expect(mixedBatch.polygonBatch.ringsCount).to.be(1);
});
it('updates the aggregated metrics on the linestring batch', () => {
expect(mixedBatch.lineStringBatch.verticesCount).to.be(4);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1);
});
it('updates the aggregated metrics on the point batch', () => {
const keys = Object.keys(mixedBatch.pointBatch.entries);
expect(keys).to.not.contain(getUid(feature));
expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
});
});
});
describe('#removeFeature', () => {
beforeEach(() => {
mixedBatch.addFeature(feature);
mixedBatch.removeFeature(feature);
});
it('clears all entries in the polygon batch', () => {
const keys = Object.keys(mixedBatch.polygonBatch.entries);
expect(keys).to.have.length(0);
expect(mixedBatch.polygonBatch.verticesCount).to.be(0);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(0);
expect(mixedBatch.polygonBatch.ringsCount).to.be(0);
});
it('clears all entries in the linestring batch', () => {
const keys = Object.keys(mixedBatch.lineStringBatch.entries);
expect(keys).to.have.length(0);
expect(mixedBatch.lineStringBatch.verticesCount).to.be(0);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0);
});
it('clears all entries in the point batch', () => {
const keys = Object.keys(mixedBatch.pointBatch.entries);
expect(keys).to.have.length(0);
expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
});
});
});
describe('#clear', () => {
beforeEach(() => {
const feature1 = new Feature(
new Polygon([
[
[201, 202],
[203, 204],
[205, 206],
[207, 208],
],
])
);
const feature2 = new Feature(new Point([201, 202]));
mixedBatch.addFeature(feature1);
mixedBatch.addFeature(feature2);
mixedBatch.clear();
});
it('clears polygon batch', () => {
expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0);
expect(mixedBatch.polygonBatch.geometriesCount).to.be(0);
expect(mixedBatch.polygonBatch.verticesCount).to.be(0);
expect(mixedBatch.polygonBatch.ringsCount).to.be(0);
});
it('clears linestring batch', () => {
expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length(0);
expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0);
expect(mixedBatch.lineStringBatch.verticesCount).to.be(0);
});
it('clears point batch', () => {
expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0);
expect(mixedBatch.pointBatch.geometriesCount).to.be(0);
});
});
});

View File

@@ -0,0 +1,477 @@
import {
colorDecodeId,
colorEncodeId,
getBlankImageData,
writeLineSegmentToBuffers,
writePointFeatureToBuffers,
writePolygonTrianglesToBuffers,
} from '../../../../../../src/ol/render/webgl/utils.js';
import {
compose as composeTransform,
create as createTransform,
makeInverse as makeInverseTransform,
} from '../../../../../../src/ol/transform.js';
describe('webgl render utils', function () {
describe('writePointFeatureToBuffers', function () {
let vertexBuffer, indexBuffer, instructions;
beforeEach(function () {
vertexBuffer = new Float32Array(100);
indexBuffer = new Uint32Array(100);
instructions = new Float32Array(100);
instructions.set([0, 0, 0, 0, 10, 11]);
});
it('writes correctly to the buffers (without custom attributes)', function () {
const stride = 3;
const positions = writePointFeatureToBuffers(
instructions,
4,
vertexBuffer,
indexBuffer,
0
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[stride + 0]).to.eql(10);
expect(vertexBuffer[stride + 1]).to.eql(11);
expect(vertexBuffer[stride + 2]).to.eql(1);
expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
expect(indexBuffer[0]).to.eql(0);
expect(indexBuffer[1]).to.eql(1);
expect(indexBuffer[2]).to.eql(3);
expect(indexBuffer[3]).to.eql(1);
expect(indexBuffer[4]).to.eql(2);
expect(indexBuffer[5]).to.eql(3);
expect(positions.indexPosition).to.eql(6);
expect(positions.vertexPosition).to.eql(stride * 4);
});
it('writes correctly to the buffers (with 2 custom attributes)', function () {
instructions.set([0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13]);
const stride = 5;
const positions = writePointFeatureToBuffers(
instructions,
8,
vertexBuffer,
indexBuffer,
2
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[3]).to.eql(12);
expect(vertexBuffer[4]).to.eql(13);
expect(vertexBuffer[stride + 0]).to.eql(10);
expect(vertexBuffer[stride + 1]).to.eql(11);
expect(vertexBuffer[stride + 2]).to.eql(1);
expect(vertexBuffer[stride + 3]).to.eql(12);
expect(vertexBuffer[stride + 4]).to.eql(13);
expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
expect(vertexBuffer[stride * 2 + 3]).to.eql(12);
expect(vertexBuffer[stride * 2 + 4]).to.eql(13);
expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
expect(vertexBuffer[stride * 3 + 3]).to.eql(12);
expect(vertexBuffer[stride * 3 + 4]).to.eql(13);
expect(indexBuffer[0]).to.eql(0);
expect(indexBuffer[1]).to.eql(1);
expect(indexBuffer[2]).to.eql(3);
expect(indexBuffer[3]).to.eql(1);
expect(indexBuffer[4]).to.eql(2);
expect(indexBuffer[5]).to.eql(3);
expect(positions.indexPosition).to.eql(6);
expect(positions.vertexPosition).to.eql(stride * 4);
});
it('correctly chains buffer writes', function () {
instructions.set([10, 11, 20, 21, 30, 31]);
const stride = 3;
let positions = writePointFeatureToBuffers(
instructions,
0,
vertexBuffer,
indexBuffer,
0
);
positions = writePointFeatureToBuffers(
instructions,
2,
vertexBuffer,
indexBuffer,
0,
positions
);
positions = writePointFeatureToBuffers(
instructions,
4,
vertexBuffer,
indexBuffer,
0,
positions
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[stride * 4 + 0]).to.eql(20);
expect(vertexBuffer[stride * 4 + 1]).to.eql(21);
expect(vertexBuffer[stride * 4 + 2]).to.eql(0);
expect(vertexBuffer[stride * 8 + 0]).to.eql(30);
expect(vertexBuffer[stride * 8 + 1]).to.eql(31);
expect(vertexBuffer[stride * 8 + 2]).to.eql(0);
expect(indexBuffer[6 + 0]).to.eql(4);
expect(indexBuffer[6 + 1]).to.eql(5);
expect(indexBuffer[6 + 2]).to.eql(7);
expect(indexBuffer[6 + 3]).to.eql(5);
expect(indexBuffer[6 + 4]).to.eql(6);
expect(indexBuffer[6 + 5]).to.eql(7);
expect(indexBuffer[6 * 2 + 0]).to.eql(8);
expect(indexBuffer[6 * 2 + 1]).to.eql(9);
expect(indexBuffer[6 * 2 + 2]).to.eql(11);
expect(indexBuffer[6 * 2 + 3]).to.eql(9);
expect(indexBuffer[6 * 2 + 4]).to.eql(10);
expect(indexBuffer[6 * 2 + 5]).to.eql(11);
expect(positions.indexPosition).to.eql(6 * 3);
expect(positions.vertexPosition).to.eql(stride * 4 * 3);
});
});
describe('writeLineSegmentToBuffers', function () {
let vertexArray, indexArray, instructions;
let instructionsTransform, invertInstructionsTransform;
beforeEach(function () {
vertexArray = [];
indexArray = [];
instructions = new Float32Array(100);
instructionsTransform = createTransform();
invertInstructionsTransform = createTransform();
composeTransform(instructionsTransform, 0, 0, 10, 20, 0, -50, 200);
makeInverseTransform(invertInstructionsTransform, instructionsTransform);
});
describe('isolated segment', function () {
beforeEach(function () {
instructions.set([0, 0, 0, 2, 5, 5, 25, 5]);
writeLineSegmentToBuffers(
instructions,
4,
6,
null,
null,
vertexArray,
indexArray,
[],
instructionsTransform,
invertInstructionsTransform
);
});
it('generates a quad for the segment', function () {
expect(vertexArray).to.have.length(20);
expect(vertexArray).to.eql([
5, 5, 25, 5, 0, 5, 5, 25, 5, 100000000, 5, 5, 25, 5, 200000000, 5, 5,
25, 5, 300000000,
]);
expect(indexArray).to.have.length(6);
expect(indexArray).to.eql([0, 1, 2, 1, 3, 2]);
});
});
describe('isolated segment with custom attributes', function () {
beforeEach(function () {
instructions.set([888, 999, 2, 5, 5, 25, 5]);
writeLineSegmentToBuffers(
instructions,
3,
5,
null,
null,
vertexArray,
indexArray,
[888, 999],
instructionsTransform,
invertInstructionsTransform
);
});
it('adds custom attributes in the vertices buffer', function () {
expect(vertexArray).to.have.length(28);
expect(vertexArray).to.eql([
5, 5, 25, 5, 0, 888, 999, 5, 5, 25, 5, 100000000, 888, 999, 5, 5, 25,
5, 200000000, 888, 999, 5, 5, 25, 5, 300000000, 888, 999,
]);
});
it('does not impact indices array', function () {
expect(indexArray).to.have.length(6);
});
});
describe('segment with a point coming before it, join angle < PI', function () {
beforeEach(function () {
instructions.set([2, 5, 5, 25, 5, 5, 20]);
writeLineSegmentToBuffers(
instructions,
1,
3,
5,
null,
vertexArray,
indexArray,
[],
instructionsTransform,
invertInstructionsTransform
);
});
it('generate the correct amount of vertices', () => {
expect(vertexArray).to.have.length(20);
});
it('correctly encodes the join angle', () => {
expect(vertexArray[4]).to.eql(2356);
expect(vertexArray[9]).to.eql(100002356);
expect(vertexArray[14]).to.eql(200002356);
expect(vertexArray[19]).to.eql(300002356);
});
it('does not impact indices array', function () {
expect(indexArray).to.have.length(6);
});
});
describe('segment with a point coming before it, join angle > PI', function () {
beforeEach(function () {
instructions.set([2, 5, 5, 25, 5, 5, -10]);
writeLineSegmentToBuffers(
instructions,
1,
3,
5,
null,
vertexArray,
indexArray,
[],
instructionsTransform,
invertInstructionsTransform
);
});
it('generate the correct amount of vertices', () => {
expect(vertexArray).to.have.length(20);
});
it('correctly encodes the join angle', () => {
expect(vertexArray[4]).to.eql(7069);
expect(vertexArray[9]).to.eql(100007069);
expect(vertexArray[14]).to.eql(200007069);
expect(vertexArray[19]).to.eql(300007069);
});
it('does not impact indices array', function () {
expect(indexArray).to.have.length(6);
});
});
describe('segment with a point coming after it, join angle < PI', function () {
beforeEach(function () {
instructions.set([2, 5, 5, 25, 5, 5, 20]);
writeLineSegmentToBuffers(
instructions,
1,
3,
null,
5,
vertexArray,
indexArray,
[],
instructionsTransform,
invertInstructionsTransform
);
});
it('generate the correct amount of vertices', () => {
expect(vertexArray).to.have.length(20);
});
it('correctly encodes the join angle', () => {
expect(vertexArray[4]).to.eql(88870000);
expect(vertexArray[9]).to.eql(188870000);
expect(vertexArray[14]).to.eql(288870000);
expect(vertexArray[19]).to.eql(388870000);
});
it('does not impact indices array', function () {
expect(indexArray).to.have.length(6);
});
});
describe('segment with a point coming after it, join angle > PI', function () {
beforeEach(function () {
instructions.set([2, 5, 5, 25, 5, 25, -10]);
writeLineSegmentToBuffers(
instructions,
1,
3,
null,
5,
vertexArray,
indexArray,
[],
instructionsTransform,
invertInstructionsTransform
);
});
it('generate the correct amount of vertices', () => {
expect(vertexArray).to.have.length(20);
});
it('correctly encodes join angles', () => {
expect(vertexArray[4]).to.eql(23560000);
expect(vertexArray[9]).to.eql(123560000);
expect(vertexArray[14]).to.eql(223560000);
expect(vertexArray[19]).to.eql(323560000);
});
it('does not impact indices array', function () {
expect(indexArray).to.have.length(6);
});
});
});
describe('writePolygonTrianglesToBuffers', function () {
let vertexArray, indexArray, instructions, newIndex;
beforeEach(function () {
vertexArray = [];
indexArray = [];
instructions = new Float32Array(100);
});
describe('polygon with a hole', function () {
beforeEach(function () {
instructions.set([
0, 0, 0, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1,
7, 3, 5, 5, 3, 3,
]);
newIndex = writePolygonTrianglesToBuffers(
instructions,
3,
vertexArray,
indexArray,
0
);
});
it('generates triangles correctly', function () {
expect(vertexArray).to.have.length(22);
expect(vertexArray).to.eql([
0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1, 7, 3, 5, 5, 3, 3,
]);
expect(indexArray).to.have.length(24);
expect(indexArray).to.eql([
4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3,
8,
]);
});
it('correctly returns the new index', function () {
expect(newIndex).to.eql(28);
});
});
describe('polygon with a hole and custom attributes', function () {
beforeEach(function () {
instructions.set([
0, 0, 0, 1234, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3,
5, 1, 7, 3, 5, 5, 3, 3,
]);
newIndex = writePolygonTrianglesToBuffers(
instructions,
3,
vertexArray,
indexArray,
1
);
});
it('generates triangles correctly', function () {
expect(vertexArray).to.have.length(33);
expect(vertexArray).to.eql([
0, 0, 1234, 10, 0, 1234, 15, 6, 1234, 10, 12, 1234, 0, 12, 1234, 0, 0,
1234, 3, 3, 1234, 5, 1, 1234, 7, 3, 1234, 5, 5, 1234, 3, 3, 1234,
]);
expect(indexArray).to.have.length(24);
expect(indexArray).to.eql([
4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3,
8,
]);
});
it('correctly returns the new index', function () {
expect(newIndex).to.eql(29);
});
});
});
describe('getBlankImageData', function () {
it('creates a 1x1 white texture', function () {
const texture = getBlankImageData();
expect(texture.height).to.eql(1);
expect(texture.width).to.eql(1);
expect(texture.data[0]).to.eql(255);
expect(texture.data[1]).to.eql(255);
expect(texture.data[2]).to.eql(255);
expect(texture.data[3]).to.eql(255);
});
});
describe('colorEncodeId and colorDecodeId', function () {
it('correctly encodes and decodes ids', function () {
expect(colorDecodeId(colorEncodeId(0))).to.eql(0);
expect(colorDecodeId(colorEncodeId(1))).to.eql(1);
expect(colorDecodeId(colorEncodeId(123))).to.eql(123);
expect(colorDecodeId(colorEncodeId(12345))).to.eql(12345);
expect(colorDecodeId(colorEncodeId(123456))).to.eql(123456);
expect(colorDecodeId(colorEncodeId(91612))).to.eql(91612);
expect(colorDecodeId(colorEncodeId(1234567890))).to.eql(1234567890);
});
it('correctly reuses array', function () {
const arr = [];
expect(colorEncodeId(123, arr)).to.be(arr);
});
it('is compatible with Uint8Array storage', function () {
const encoded = colorEncodeId(91612);
const typed = Uint8Array.of(
encoded[0] * 255,
encoded[1] * 255,
encoded[2] * 255,
encoded[3] * 255
);
const arr = [
typed[0] / 255,
typed[1] / 255,
typed[2] / 255,
typed[3] / 255,
];
const decoded = colorDecodeId(arr);
expect(decoded).to.eql(91612);
});
});
});

View File

@@ -6,12 +6,7 @@ import TileLayer from '../../../../../../src/ol/layer/WebGLTile.js';
import VectorLayer from '../../../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../../../src/ol/source/Vector.js';
import View from '../../../../../../src/ol/View.js';
import WebGLLayerRenderer, {
colorDecodeId,
colorEncodeId,
getBlankImageData,
writePointFeatureToBuffers,
} from '../../../../../../src/ol/renderer/webgl/Layer.js';
import WebGLLayerRenderer from '../../../../../../src/ol/renderer/webgl/Layer.js';
import {getUid} from '../../../../../../src/ol/util.js';
describe('ol/renderer/webgl/Layer', function () {
@@ -36,205 +31,6 @@ describe('ol/renderer/webgl/Layer', function () {
});
});
describe('writePointFeatureToBuffers', function () {
let vertexBuffer, indexBuffer, instructions;
beforeEach(function () {
vertexBuffer = new Float32Array(100);
indexBuffer = new Uint32Array(100);
instructions = new Float32Array(100);
instructions.set([0, 0, 0, 0, 10, 11]);
});
it('writes correctly to the buffers (without custom attributes)', function () {
const stride = 3;
const positions = writePointFeatureToBuffers(
instructions,
4,
vertexBuffer,
indexBuffer,
0
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[stride + 0]).to.eql(10);
expect(vertexBuffer[stride + 1]).to.eql(11);
expect(vertexBuffer[stride + 2]).to.eql(1);
expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
expect(indexBuffer[0]).to.eql(0);
expect(indexBuffer[1]).to.eql(1);
expect(indexBuffer[2]).to.eql(3);
expect(indexBuffer[3]).to.eql(1);
expect(indexBuffer[4]).to.eql(2);
expect(indexBuffer[5]).to.eql(3);
expect(positions.indexPosition).to.eql(6);
expect(positions.vertexPosition).to.eql(stride * 4);
});
it('writes correctly to the buffers (with 2 custom attributes)', function () {
instructions.set([0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13]);
const stride = 5;
const positions = writePointFeatureToBuffers(
instructions,
8,
vertexBuffer,
indexBuffer,
2
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[3]).to.eql(12);
expect(vertexBuffer[4]).to.eql(13);
expect(vertexBuffer[stride + 0]).to.eql(10);
expect(vertexBuffer[stride + 1]).to.eql(11);
expect(vertexBuffer[stride + 2]).to.eql(1);
expect(vertexBuffer[stride + 3]).to.eql(12);
expect(vertexBuffer[stride + 4]).to.eql(13);
expect(vertexBuffer[stride * 2 + 0]).to.eql(10);
expect(vertexBuffer[stride * 2 + 1]).to.eql(11);
expect(vertexBuffer[stride * 2 + 2]).to.eql(2);
expect(vertexBuffer[stride * 2 + 3]).to.eql(12);
expect(vertexBuffer[stride * 2 + 4]).to.eql(13);
expect(vertexBuffer[stride * 3 + 0]).to.eql(10);
expect(vertexBuffer[stride * 3 + 1]).to.eql(11);
expect(vertexBuffer[stride * 3 + 2]).to.eql(3);
expect(vertexBuffer[stride * 3 + 3]).to.eql(12);
expect(vertexBuffer[stride * 3 + 4]).to.eql(13);
expect(indexBuffer[0]).to.eql(0);
expect(indexBuffer[1]).to.eql(1);
expect(indexBuffer[2]).to.eql(3);
expect(indexBuffer[3]).to.eql(1);
expect(indexBuffer[4]).to.eql(2);
expect(indexBuffer[5]).to.eql(3);
expect(positions.indexPosition).to.eql(6);
expect(positions.vertexPosition).to.eql(stride * 4);
});
it('correctly chains buffer writes', function () {
instructions.set([10, 11, 20, 21, 30, 31]);
const stride = 3;
let positions = writePointFeatureToBuffers(
instructions,
0,
vertexBuffer,
indexBuffer,
0
);
positions = writePointFeatureToBuffers(
instructions,
2,
vertexBuffer,
indexBuffer,
0,
positions
);
positions = writePointFeatureToBuffers(
instructions,
4,
vertexBuffer,
indexBuffer,
0,
positions
);
expect(vertexBuffer[0]).to.eql(10);
expect(vertexBuffer[1]).to.eql(11);
expect(vertexBuffer[2]).to.eql(0);
expect(vertexBuffer[stride * 4 + 0]).to.eql(20);
expect(vertexBuffer[stride * 4 + 1]).to.eql(21);
expect(vertexBuffer[stride * 4 + 2]).to.eql(0);
expect(vertexBuffer[stride * 8 + 0]).to.eql(30);
expect(vertexBuffer[stride * 8 + 1]).to.eql(31);
expect(vertexBuffer[stride * 8 + 2]).to.eql(0);
expect(indexBuffer[6 + 0]).to.eql(4);
expect(indexBuffer[6 + 1]).to.eql(5);
expect(indexBuffer[6 + 2]).to.eql(7);
expect(indexBuffer[6 + 3]).to.eql(5);
expect(indexBuffer[6 + 4]).to.eql(6);
expect(indexBuffer[6 + 5]).to.eql(7);
expect(indexBuffer[6 * 2 + 0]).to.eql(8);
expect(indexBuffer[6 * 2 + 1]).to.eql(9);
expect(indexBuffer[6 * 2 + 2]).to.eql(11);
expect(indexBuffer[6 * 2 + 3]).to.eql(9);
expect(indexBuffer[6 * 2 + 4]).to.eql(10);
expect(indexBuffer[6 * 2 + 5]).to.eql(11);
expect(positions.indexPosition).to.eql(6 * 3);
expect(positions.vertexPosition).to.eql(stride * 4 * 3);
});
});
describe('getBlankImageData', function () {
it('creates a 1x1 white texture', function () {
const texture = getBlankImageData();
expect(texture.height).to.eql(1);
expect(texture.width).to.eql(1);
expect(texture.data[0]).to.eql(255);
expect(texture.data[1]).to.eql(255);
expect(texture.data[2]).to.eql(255);
expect(texture.data[3]).to.eql(255);
});
});
describe('colorEncodeId and colorDecodeId', function () {
it('correctly encodes and decodes ids', function () {
expect(colorDecodeId(colorEncodeId(0))).to.eql(0);
expect(colorDecodeId(colorEncodeId(1))).to.eql(1);
expect(colorDecodeId(colorEncodeId(123))).to.eql(123);
expect(colorDecodeId(colorEncodeId(12345))).to.eql(12345);
expect(colorDecodeId(colorEncodeId(123456))).to.eql(123456);
expect(colorDecodeId(colorEncodeId(91612))).to.eql(91612);
expect(colorDecodeId(colorEncodeId(1234567890))).to.eql(1234567890);
});
it('correctly reuses array', function () {
const arr = [];
expect(colorEncodeId(123, arr)).to.be(arr);
});
it('is compatible with Uint8Array storage', function () {
const encoded = colorEncodeId(91612);
const typed = Uint8Array.of(
encoded[0] * 255,
encoded[1] * 255,
encoded[2] * 255,
encoded[3] * 255
);
const arr = [
typed[0] / 255,
typed[1] / 255,
typed[2] / 255,
typed[3] / 255,
];
const decoded = colorDecodeId(arr);
expect(decoded).to.eql(91612);
});
});
describe('context sharing', () => {
let target;
beforeEach(() => {

View File

@@ -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],
},
},
],

View File

@@ -57,6 +57,16 @@ const INVALID_FRAGMENT_SHADER = `
gl_FragColor = vec4(oops, 1.0, 1.0, 1.0);
}`;
const SAMPLE_FRAMESTATE = {
size: [100, 150],
viewState: {
rotation: 0.4,
resolution: 2,
center: [10, 20],
zoom: 3,
},
};
describe('ol/webgl/WebGLHelper', function () {
let h;
afterEach(function () {
@@ -117,7 +127,10 @@ describe('ol/webgl/WebGLHelper', function () {
u_test4: createTransform(),
},
});
h.useProgram(h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER));
h.useProgram(
h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER),
SAMPLE_FRAMESTATE
);
h.prepareDraw({
pixelRatio: 2,
size: [50, 80],
@@ -142,6 +155,16 @@ describe('ol/webgl/WebGLHelper', function () {
h.uniformLocations_[DefaultUniform.OFFSET_SCALE_MATRIX]
).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.TIME]).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.ZOOM]).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.RESOLUTION]).not.to.eql(
undefined
);
expect(h.uniformLocations_[DefaultUniform.SIZE_PX]).not.to.eql(
undefined
);
expect(h.uniformLocations_[DefaultUniform.PIXEL_RATIO]).not.to.eql(
undefined
);
});
it('has processed uniforms', function () {
@@ -164,7 +187,7 @@ describe('ol/webgl/WebGLHelper', function () {
h = new WebGLHelper();
p = h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER);
h.useProgram(p);
h.useProgram(p, SAMPLE_FRAMESTATE);
});
it('has saved the program', function () {
@@ -209,34 +232,30 @@ describe('ol/webgl/WebGLHelper', function () {
});
describe('#makeProjectionTransform', function () {
let frameState;
beforeEach(function () {
h = new WebGLHelper();
frameState = {
size: [100, 150],
viewState: {
rotation: 0.4,
resolution: 2,
center: [10, 20],
},
};
});
it('gives out the correct transform', function () {
const scaleX = 2 / frameState.size[0] / frameState.viewState.resolution;
const scaleY = 2 / frameState.size[1] / frameState.viewState.resolution;
const scaleX =
2 /
SAMPLE_FRAMESTATE.size[0] /
SAMPLE_FRAMESTATE.viewState.resolution;
const scaleY =
2 /
SAMPLE_FRAMESTATE.size[1] /
SAMPLE_FRAMESTATE.viewState.resolution;
const given = createTransform();
const expected = createTransform();
scaleTransform(expected, scaleX, scaleY);
rotateTransform(expected, -frameState.viewState.rotation);
rotateTransform(expected, -SAMPLE_FRAMESTATE.viewState.rotation);
translateTransform(
expected,
-frameState.viewState.center[0],
-frameState.viewState.center[1]
-SAMPLE_FRAMESTATE.viewState.center[0],
-SAMPLE_FRAMESTATE.viewState.center[1]
);
h.makeProjectionTransform(frameState, given);
h.makeProjectionTransform(SAMPLE_FRAMESTATE, given);
expect(given.map((val) => val.toFixed(15))).to.eql(
expected.map((val) => val.toFixed(15))
@@ -377,7 +396,8 @@ describe('ol/webgl/WebGLHelper', function () {
void main(void) {
gl_Position = vec4(u_test, attr3, 0.0, 1.0);
}`
)
),
SAMPLE_FRAMESTATE
);
});
@@ -404,4 +424,50 @@ describe('ol/webgl/WebGLHelper', function () {
expect(spy.getCall(2).args[4]).to.eql(5 * bytesPerFloat);
});
});
describe('#applyFrameState', function () {
let stubMatrix, stubFloat, stubVec2, stubTime;
beforeEach(function () {
stubTime = sinon.stub(Date, 'now');
stubTime.returns(1000);
h = new WebGLHelper();
stubMatrix = sinon.stub(h, 'setUniformMatrixValue');
stubFloat = sinon.stub(h, 'setUniformFloatValue');
stubVec2 = sinon.stub(h, 'setUniformFloatVec2');
stubTime.returns(2000);
h.applyFrameState({...SAMPLE_FRAMESTATE, pixelRatio: 2});
});
afterEach(function () {
stubTime.restore();
});
it('sets the default uniforms according the frame state', function () {
expect(stubMatrix.getCall(0).args).to.eql([
DefaultUniform.OFFSET_SCALE_MATRIX,
[
0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
],
]);
expect(stubMatrix.getCall(1).args).to.eql([
DefaultUniform.OFFSET_ROTATION_MATRIX,
[
0.9210609940028851, -0.3894183423086505, 0, 0, 0.3894183423086505,
0.9210609940028851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
],
]);
expect(stubFloat.getCall(0).args).to.eql([DefaultUniform.TIME, 1]);
expect(stubFloat.getCall(1).args).to.eql([DefaultUniform.ZOOM, 3]);
expect(stubFloat.getCall(2).args).to.eql([DefaultUniform.RESOLUTION, 2]);
expect(stubFloat.getCall(3).args).to.eql([DefaultUniform.PIXEL_RATIO, 2]);
expect(stubVec2.getCall(0).args).to.eql([
DefaultUniform.SIZE_PX,
[100, 150],
]);
});
});
});

View File

@@ -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);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -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',
});