Merge pull request #13461 from jahow/webgl-shape-renderer
WebGL vector renderer for polygons, lines and points
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user