From 143c19ca0391bcd27ceee5dfa8bef523a586a57b Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 16 Mar 2022 18:09:44 +0100 Subject: [PATCH] Add utils for generating webgl buffers from lines and polygons Uses @mapbox/earcut for polygon (what else), and a home made logic for lines --- package-lock.json | 11 + package.json | 1 + src/ol/render/webgl/constants.js | 27 ++ src/ol/render/webgl/utils.js | 221 +++++++++++++- .../spec/ol/render/webgl/utils.test.js | 270 ++++++++++++++++++ 5 files changed, 523 insertions(+), 7 deletions(-) create mode 100644 src/ol/render/webgl/constants.js diff --git a/package-lock.json b/package-lock.json index cf9997f4d3..046ec1b7a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "6.14.2-dev", "license": "BSD-2-Clause", "dependencies": { + "earcut": "^2.2.3", "geotiff": "2.0.4", "ol-mapbox-style": "^8.0.5", "pbf": "3.2.1", @@ -3828,6 +3829,11 @@ "void-elements": "^2.0.0" } }, + "node_modules/earcut": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz", + "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -13170,6 +13176,11 @@ "void-elements": "^2.0.0" } }, + "earcut": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.3.tgz", + "integrity": "sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index c698db44d3..d80f281685 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "url": "https://opencollective.com/openlayers" }, "dependencies": { + "earcut": "^2.2.3", "geotiff": "2.0.4", "ol-mapbox-style": "^8.0.5", "pbf": "3.2.1", diff --git a/src/ol/render/webgl/constants.js b/src/ol/render/webgl/constants.js new file mode 100644 index 0000000000..edd36810e2 --- /dev/null +++ b/src/ol/render/webgl/constants.js @@ -0,0 +1,27 @@ +/** + * @module ol/render/webgl/constants + */ + +/** + * @enum {string} + */ +export const WebGLWorkerMessageType = { + GENERATE_POLYGON_BUFFERS: 'GENERATE_POLYGON_BUFFERS', + GENERATE_POINT_BUFFERS: 'GENERATE_POINT_BUFFERS', + GENERATE_LINE_STRING_BUFFERS: 'GENERATE_LINE_STRING_BUFFERS', +}; + +/** + * @typedef {Object} WebGLWorkerGenerateBuffersMessage + * This message will trigger the generation of a vertex and an index buffer based on the given render instructions. + * When the buffers are generated, the worked will send a message of the same type to the main thread, with + * the generated buffers in it. + * Note that any addition properties present in the message *will* be sent back to the main thread. + * @property {number} id Message id; will be used both in request and response as a means of identification + * @property {WebGLWorkerMessageType} type Message type + * @property {ArrayBuffer} renderInstructions Polygon render instructions raw binary buffer. + * @property {number} [customAttributesCount] Amount of custom attributes count in the polygon render instructions. + * @property {ArrayBuffer} [vertexBuffer] Vertices array raw binary buffer (sent by the worker). + * @property {ArrayBuffer} [indexBuffer] Indices array raw binary buffer (sent by the worker). + * @property {import("../../transform").Transform} [renderInstructionsTransform] Transformation matrix used to project the instructions coordinates + */ diff --git a/src/ol/render/webgl/utils.js b/src/ol/render/webgl/utils.js index 3ddd7ebac6..9bfa7895c8 100644 --- a/src/ol/render/webgl/utils.js +++ b/src/ol/render/webgl/utils.js @@ -1,14 +1,11 @@ /** * @module ol/render/webgl/utils */ -const tmpArray_ = []; -const bufferPositions_ = {vertexPosition: 0, indexPosition: 0}; +import {apply as applyTransform} from '../../transform.js'; +import {clamp} from '../../math.js'; +import earcut from 'earcut'; -function writePointVertex(buffer, pos, x, y, index) { - buffer[pos + 0] = x; - buffer[pos + 1] = y; - buffer[pos + 2] = index; -} +const tmpArray_ = []; /** * An object holding positions both in an index and a vertex buffer. @@ -16,6 +13,13 @@ function writePointVertex(buffer, pos, x, y, index) { * @property {number} vertexPosition Position in the vertex buffer * @property {number} indexPosition Position in the index buffer */ +const bufferPositions_ = {vertexPosition: 0, indexPosition: 0}; + +function writePointVertex(buffer, pos, x, y, index) { + buffer[pos + 0] = x; + buffer[pos + 1] = y; + buffer[pos + 2] = index; +} /** * Pushes a quad (two triangles) based on a point geometry @@ -91,6 +95,209 @@ export function writePointFeatureToBuffers( return bufferPositions_; } +/** + * Pushes a single quad to form a line segment; also includes a computation for the join angles with previous and next + * segment, in order to be able to offset the vertices correctly in the shader + * @param {Float32Array} instructions Array of render instructions for lines. + * @param {number} segmentStartIndex Index of the segment start point from which render instructions will be read. + * @param {number} segmentEndIndex Index of the segment start point from which render instructions will be read. + * @param {number|null} beforeSegmentIndex Index of the point right before the segment (null if none, e.g this is a line start) + * @param {number|null} afterSegmentIndex Index of the point right after the segment (null if none, e.g this is a line end) + * @param {number[]} vertexArray Array containing vertices. + * @param {number[]} indexArray Array containing indices. + * @param {number[]} customAttributes Array of custom attributes value + * @param {import('../../transform.js').Transform} instructionsTransform Transform matrix used to project coordinates in instructions + * @param {import('../../transform.js').Transform} invertInstructionsTransform Transform matrix used to project coordinates in instructions + * @private + */ +export function writeLineSegmentToBuffers( + instructions, + segmentStartIndex, + segmentEndIndex, + beforeSegmentIndex, + afterSegmentIndex, + vertexArray, + indexArray, + customAttributes, + instructionsTransform, + invertInstructionsTransform +) { + // compute the stride to determine how many vertices were already pushed + const baseVertexAttrsCount = 5; // base attributes: x0, y0, x1, y1, params (vertex number [0-3], join angle 1, join angle 2) + const stride = baseVertexAttrsCount + customAttributes.length; + let baseIndex = vertexArray.length / stride; + + // The segment is composed of two positions called P0[x0, y0] and P1[x1, y1] + // Depending on whether there are points before and after the segment, its final shape + // will be different + const p0 = [ + instructions[segmentStartIndex + 0], + instructions[segmentStartIndex + 1], + ]; + const p1 = [instructions[segmentEndIndex], instructions[segmentEndIndex + 1]]; + + // to compute offsets from the line center we need to reproject + // coordinates back in world units and compute the length of the segment + const p0world = applyTransform(invertInstructionsTransform, [...p0]); + const p1world = applyTransform(invertInstructionsTransform, [...p1]); + + function computeVertexParameters(vertexNumber, joinAngle1, joinAngle2) { + const shift = 10000; + const anglePrecision = 1500; + return ( + Math.round(joinAngle1 * anglePrecision) + + Math.round(joinAngle2 * anglePrecision) * shift + + vertexNumber * shift * shift + ); + } + + // compute the angle between p0pA and p0pB + // returns a value in [0, 2PI] + function angleBetween(p0, pA, pB) { + const lenA = Math.sqrt( + (pA[0] - p0[0]) * (pA[0] - p0[0]) + (pA[1] - p0[1]) * (pA[1] - p0[1]) + ); + const tangentA = [(pA[0] - p0[0]) / lenA, (pA[1] - p0[1]) / lenA]; + const orthoA = [-tangentA[1], tangentA[0]]; + const lenB = Math.sqrt( + (pB[0] - p0[0]) * (pB[0] - p0[0]) + (pB[1] - p0[1]) * (pB[1] - p0[1]) + ); + const tangentB = [(pB[0] - p0[0]) / lenB, (pB[1] - p0[1]) / lenB]; + + // this angle can be clockwise or anticlockwise; hence the computation afterwards + const angle = + lenA === 0 || lenB === 0 + ? 0 + : Math.acos( + clamp(tangentB[0] * tangentA[0] + tangentB[1] * tangentA[1], -1, 1) + ); + const isClockwise = tangentB[0] * orthoA[0] + tangentB[1] * orthoA[1] > 0; + return !isClockwise ? Math.PI * 2 - angle : angle; + } + + const joinBefore = beforeSegmentIndex !== null; + const joinAfter = afterSegmentIndex !== null; + + let angle0 = 0; + let angle1 = 0; + + // add vertices and adapt offsets for P0 in case of join + if (joinBefore) { + // B for before + const pB = [ + instructions[beforeSegmentIndex], + instructions[beforeSegmentIndex + 1], + ]; + const pBworld = applyTransform(invertInstructionsTransform, [...pB]); + angle0 = angleBetween(p0world, p1world, pBworld); + } + // adapt offsets for P1 in case of join + if (joinAfter) { + // A for after + const pA = [ + instructions[afterSegmentIndex], + instructions[afterSegmentIndex + 1], + ]; + const pAworld = applyTransform(invertInstructionsTransform, [...pA]); + angle1 = angleBetween(p1world, p0world, pAworld); + } + + // add main segment triangles + vertexArray.push( + p0[0], + p0[1], + p1[0], + p1[1], + computeVertexParameters(0, angle0, angle1) + ); + vertexArray.push(...customAttributes); + + vertexArray.push( + p0[0], + p0[1], + p1[0], + p1[1], + computeVertexParameters(1, angle0, angle1) + ); + vertexArray.push(...customAttributes); + + vertexArray.push( + p0[0], + p0[1], + p1[0], + p1[1], + computeVertexParameters(2, angle0, angle1) + ); + vertexArray.push(...customAttributes); + + vertexArray.push( + p0[0], + p0[1], + p1[0], + p1[1], + computeVertexParameters(3, angle0, angle1) + ); + vertexArray.push(...customAttributes); + + indexArray.push( + baseIndex, + baseIndex + 1, + baseIndex + 2, + baseIndex + 1, + baseIndex + 3, + baseIndex + 2 + ); +} + +/** + * Pushes several triangles to form a polygon, including holes + * @param {Float32Array} instructions Array of render instructions for lines. + * @param {number} polygonStartIndex Index of the polygon start point from which render instructions will be read. + * @param {number[]} vertexArray Array containing vertices. + * @param {number[]} indexArray Array containing indices. + * @param {number} customAttributesCount Amount of custom attributes for each element. + * @return {number} Next polygon instructions index + * @private + */ +export function writePolygonTrianglesToBuffers( + instructions, + polygonStartIndex, + vertexArray, + indexArray, + customAttributesCount +) { + const instructionsPerVertex = 2; // x, y + const attributesPerVertex = 2 + customAttributesCount; + let instructionsIndex = polygonStartIndex; + const customAttributes = instructions.slice( + instructionsIndex, + instructionsIndex + customAttributesCount + ); + instructionsIndex += customAttributesCount; + const ringsCount = instructions[instructionsIndex++]; + let verticesCount = 0; + const holes = new Array(ringsCount - 1); + for (let i = 0; i < ringsCount; i++) { + verticesCount += instructions[instructionsIndex++]; + if (i < ringsCount - 1) holes[i] = verticesCount; + } + const flatCoords = instructions.slice( + instructionsIndex, + instructionsIndex + verticesCount * instructionsPerVertex + ); + + // pushing to vertices and indices!! this is where the magic happens + const result = earcut(flatCoords, holes, instructionsPerVertex); + for (let i = 0; i < result.length; i++) { + indexArray.push(result[i] + vertexArray.length / attributesPerVertex); + } + for (let i = 0; i < flatCoords.length; i += 2) { + vertexArray.push(flatCoords[i], flatCoords[i + 1], ...customAttributes); + } + + return instructionsIndex + verticesCount * instructionsPerVertex; +} + /** * Returns a texture of 1x1 pixel, white * @private diff --git a/test/browser/spec/ol/render/webgl/utils.test.js b/test/browser/spec/ol/render/webgl/utils.test.js index 9adac57c38..8e94ef3684 100644 --- a/test/browser/spec/ol/render/webgl/utils.test.js +++ b/test/browser/spec/ol/render/webgl/utils.test.js @@ -2,8 +2,15 @@ import { colorDecodeId, colorEncodeId, getBlankImageData, + writeLineSegmentToBuffers, writePointFeatureToBuffers, + writePolygonTrianglesToBuffers, } from '../../../../../../src/ol/render/webgl/utils.js'; +import { + compose as composeTransform, + create as createTransform, + makeInverse as makeInverseTransform, +} from '../../../../../../src/ol/transform.js'; describe('webgl render utils', function () { describe('writePointFeatureToBuffers', function () { @@ -158,6 +165,269 @@ describe('webgl render utils', function () { }); }); + describe('writeLineSegmentToBuffers', function () { + let vertexArray, indexArray, instructions; + let instructionsTransform, invertInstructionsTransform; + + beforeEach(function () { + vertexArray = []; + indexArray = []; + + instructions = new Float32Array(100); + + instructionsTransform = createTransform(); + invertInstructionsTransform = createTransform(); + composeTransform(instructionsTransform, 0, 0, 10, 20, 0, -50, 200); + makeInverseTransform(invertInstructionsTransform, instructionsTransform); + }); + + describe('isolated segment', function () { + beforeEach(function () { + instructions.set([0, 0, 0, 2, 5, 5, 25, 5]); + writeLineSegmentToBuffers( + instructions, + 4, + 6, + null, + null, + vertexArray, + indexArray, + [], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('generates a quad for the segment', function () { + expect(vertexArray).to.have.length(20); + expect(vertexArray).to.eql([ + 5, 5, 25, 5, 0, 5, 5, 25, 5, 100000000, 5, 5, 25, 5, 200000000, 5, 5, + 25, 5, 300000000, + ]); + expect(indexArray).to.have.length(6); + expect(indexArray).to.eql([0, 1, 2, 1, 3, 2]); + }); + }); + + describe('isolated segment with custom attributes', function () { + beforeEach(function () { + instructions.set([888, 999, 2, 5, 5, 25, 5]); + writeLineSegmentToBuffers( + instructions, + 3, + 5, + null, + null, + vertexArray, + indexArray, + [888, 999], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('adds custom attributes in the vertices buffer', function () { + expect(vertexArray).to.have.length(28); + expect(vertexArray).to.eql([ + 5, 5, 25, 5, 0, 888, 999, 5, 5, 25, 5, 100000000, 888, 999, 5, 5, 25, + 5, 200000000, 888, 999, 5, 5, 25, 5, 300000000, 888, 999, + ]); + }); + it('does not impact indices array', function () { + expect(indexArray).to.have.length(6); + }); + }); + + describe('segment with a point coming before it, join angle < PI', function () { + beforeEach(function () { + instructions.set([2, 5, 5, 25, 5, 5, 20]); + writeLineSegmentToBuffers( + instructions, + 1, + 3, + 5, + null, + vertexArray, + indexArray, + [], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('generate the correct amount of vertices', () => { + expect(vertexArray).to.have.length(20); + }); + it('correctly encodes the join angle', () => { + expect(vertexArray[4]).to.eql(2356); + expect(vertexArray[9]).to.eql(100002356); + expect(vertexArray[14]).to.eql(200002356); + expect(vertexArray[19]).to.eql(300002356); + }); + it('does not impact indices array', function () { + expect(indexArray).to.have.length(6); + }); + }); + + describe('segment with a point coming before it, join angle > PI', function () { + beforeEach(function () { + instructions.set([2, 5, 5, 25, 5, 5, -10]); + writeLineSegmentToBuffers( + instructions, + 1, + 3, + 5, + null, + vertexArray, + indexArray, + [], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('generate the correct amount of vertices', () => { + expect(vertexArray).to.have.length(20); + }); + it('correctly encodes the join angle', () => { + expect(vertexArray[4]).to.eql(7069); + expect(vertexArray[9]).to.eql(100007069); + expect(vertexArray[14]).to.eql(200007069); + expect(vertexArray[19]).to.eql(300007069); + }); + it('does not impact indices array', function () { + expect(indexArray).to.have.length(6); + }); + }); + + describe('segment with a point coming after it, join angle < PI', function () { + beforeEach(function () { + instructions.set([2, 5, 5, 25, 5, 5, 20]); + writeLineSegmentToBuffers( + instructions, + 1, + 3, + null, + 5, + vertexArray, + indexArray, + [], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('generate the correct amount of vertices', () => { + expect(vertexArray).to.have.length(20); + }); + it('correctly encodes the join angle', () => { + expect(vertexArray[4]).to.eql(88870000); + expect(vertexArray[9]).to.eql(188870000); + expect(vertexArray[14]).to.eql(288870000); + expect(vertexArray[19]).to.eql(388870000); + }); + it('does not impact indices array', function () { + expect(indexArray).to.have.length(6); + }); + }); + + describe('segment with a point coming after it, join angle > PI', function () { + beforeEach(function () { + instructions.set([2, 5, 5, 25, 5, 25, -10]); + writeLineSegmentToBuffers( + instructions, + 1, + 3, + null, + 5, + vertexArray, + indexArray, + [], + instructionsTransform, + invertInstructionsTransform + ); + }); + it('generate the correct amount of vertices', () => { + expect(vertexArray).to.have.length(20); + }); + it('correctly encodes join angles', () => { + expect(vertexArray[4]).to.eql(23560000); + expect(vertexArray[9]).to.eql(123560000); + expect(vertexArray[14]).to.eql(223560000); + expect(vertexArray[19]).to.eql(323560000); + }); + it('does not impact indices array', function () { + expect(indexArray).to.have.length(6); + }); + }); + }); + + describe('writePolygonTrianglesToBuffers', function () { + let vertexArray, indexArray, instructions, newIndex; + + beforeEach(function () { + vertexArray = []; + indexArray = []; + instructions = new Float32Array(100); + }); + + describe('polygon with a hole', function () { + beforeEach(function () { + instructions.set([ + 0, 0, 0, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1, + 7, 3, 5, 5, 3, 3, + ]); + newIndex = writePolygonTrianglesToBuffers( + instructions, + 3, + vertexArray, + indexArray, + 0 + ); + }); + it('generates triangles correctly', function () { + expect(vertexArray).to.have.length(22); + expect(vertexArray).to.eql([ + 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, 5, 1, 7, 3, 5, 5, 3, 3, + ]); + expect(indexArray).to.have.length(24); + expect(indexArray).to.eql([ + 4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3, + 8, + ]); + }); + it('correctly returns the new index', function () { + expect(newIndex).to.eql(28); + }); + }); + + describe('polygon with a hole and custom attributes', function () { + beforeEach(function () { + instructions.set([ + 0, 0, 0, 1234, 2, 6, 5, 0, 0, 10, 0, 15, 6, 10, 12, 0, 12, 0, 0, 3, 3, + 5, 1, 7, 3, 5, 5, 3, 3, + ]); + newIndex = writePolygonTrianglesToBuffers( + instructions, + 3, + vertexArray, + indexArray, + 1 + ); + }); + it('generates triangles correctly', function () { + expect(vertexArray).to.have.length(33); + expect(vertexArray).to.eql([ + 0, 0, 1234, 10, 0, 1234, 15, 6, 1234, 10, 12, 1234, 0, 12, 1234, 0, 0, + 1234, 3, 3, 1234, 5, 1, 1234, 7, 3, 1234, 5, 5, 1234, 3, 3, 1234, + ]); + expect(indexArray).to.have.length(24); + expect(indexArray).to.eql([ + 4, 0, 9, 7, 10, 0, 1, 2, 3, 3, 4, 9, 7, 0, 1, 3, 9, 8, 8, 7, 1, 1, 3, + 8, + ]); + }); + it('correctly returns the new index', function () { + expect(newIndex).to.eql(29); + }); + }); + }); + describe('getBlankImageData', function () { it('creates a 1x1 white texture', function () { const texture = getBlankImageData();