/** * @module ol/render/webgl/VectorStyleRenderer */ import { create as createTransform, makeInverse as makeInverseTransform, } from '../../transform.js'; import WebGLArrayBuffer from '../../webgl/Buffer.js'; import {AttributeType} from '../../webgl/Helper.js'; import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js'; import {create as createWebGLWorker} from '../../worker/webgl.js'; import {WebGLWorkerMessageType} from './constants.js'; import {colorEncodeIdAndPack} from './encodeUtil.js'; import { generateLineStringRenderInstructions, generatePointRenderInstructions, generatePolygonRenderInstructions, getCustomAttributesSize, } from './renderinstructions.js'; import {parseLiteralStyle} from './style.js'; const tmpColor = []; /** @type {Worker|undefined} */ let WEBGL_WORKER; function getWebGLWorker() { if (!WEBGL_WORKER) { WEBGL_WORKER = createWebGLWorker(); } return WEBGL_WORKER; } let workerMessageCounter = 0; /** * 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', LOCAL_POSITION: 'a_localPosition', SEGMENT_START: 'a_segmentStart', SEGMENT_END: 'a_segmentEnd', MEASURE_START: 'a_measureStart', MEASURE_END: 'a_measureEnd', ANGLE_TANGENT_SUM: 'a_angleTangentSum', JOIN_ANGLES: 'a_joinAngles', DISTANCE_LOW: 'a_distanceLow', DISTANCE_HIGH: 'a_distanceHigh', }; /** * @typedef {Object} AttributeDefinition A description of a custom attribute to be passed on to the GPU, with a value different * for each feature. * @property {number} [size] Amount of numerical values composing the attribute, either 1, 2, 3 or 4; in case size is > 1, the return value * of the callback should be an array; if unspecified, assumed to be a single float value * @property {function(this:import("./MixedGeometryBatch.js").GeometryBatchItem, import("../../Feature").FeatureLike):number|Array} callback This callback computes the numerical value of the * attribute for a given feature. */ /** * @typedef {Object} AttributeDefinitions * @typedef {Object} UniformDefinitions */ /** * @typedef {Array} WebGLArrayBufferSet Buffers organized like so: [indicesBuffer, vertexAttributesBuffer, instanceAttributesBuffer] */ /** * @typedef {Object} WebGLBuffers * @property {WebGLArrayBufferSet} polygonBuffers Array containing indices and vertices buffers for polygons * @property {WebGLArrayBufferSet} lineStringBuffers Array containing indices and vertices buffers for line strings * @property {WebGLArrayBufferSet} pointBuffers Array containing indices and vertices buffers for points * @property {import("../../transform.js").Transform} invertVerticesTransform Inverse of the transform applied when generating buffers */ /** * @typedef {Object} RenderInstructions * @property {Float32Array|null} polygonInstructions Polygon instructions; null if nothing to render * @property {Float32Array|null} lineStringInstructions LineString instructions; null if nothing to render * @property {Float32Array|null} pointInstructions Point instructions; null if nothing to render */ /** * @typedef {Object} ShaderProgram An object containing both shaders (vertex and fragment) * @property {string} vertex Vertex shader source * @property {string} fragment Fragment shader source */ /** * @typedef {import('./style.js').StyleParseResult} StyleShaders */ /** * @typedef {import('../../style/flat.js').FlatStyleLike} FlatStyleLike */ /** * @typedef {import('../../style/flat.js').FlatStyle} FlatStyle */ /** * @typedef {import('../../style/flat.js').Rule} FlatStyleRule */ /** * @typedef {Object} SubRenderPass * @property {string} vertexShader Vertex shader * @property {string} fragmentShader Fragment shader * @property {Array} attributesDesc Attributes description, defined for each primitive vertex * @property {Array} instancedAttributesDesc Attributes description, defined once per primitive * @property {number} instancePrimitiveVertexCount Number of vertices per instance primitive in this render pass * @property {WebGLProgram} [program] Program; this has to be recreated if the helper is lost/changed */ /** * @typedef {Object} RenderPass * @property {SubRenderPass} [fillRenderPass] Fill render pass; undefined if no fill in pass * @property {SubRenderPass} [strokeRenderPass] Stroke render pass; undefined if no stroke in pass * @property {SubRenderPass} [symbolRenderPass] Symbol render pass; undefined if no symbol in pass */ /** * @classdesc This class is responsible for: * 1. generating WebGL buffers according to a provided style, using a MixedGeometryBatch as input * 2. rendering geometries contained in said buffers * * A VectorStyleRenderer instance can be created either from a literal style or from shaders. * The shaders should not be provided explicitly but instead as a preconfigured ShaderBuilder instance. * * The `generateBuffers` method returns a promise resolving to WebGL buffers that are intended to be rendered by the * same renderer. */ class VectorStyleRenderer { /** * @param {FlatStyleLike|StyleShaders|Array} styles Vector styles expressed as flat styles, flat style rules or style shaders * @param {import('../../style/flat.js').StyleVariables} variables Style variables * @param {import('../../webgl/Helper.js').default} helper Helper * @param {boolean} [enableHitDetection] Whether to enable the hit detection (needs compatible shader) */ constructor(styles, variables, helper, enableHitDetection) { /** * @private * @type {import('../../webgl/Helper.js').default} */ this.helper_; /** * @private */ this.hitDetectionEnabled_ = !!enableHitDetection; /** * @type {Array} * @private */ this.styleShaders = convertStyleToShaders(styles, variables); /** * @type {AttributeDefinitions} * @private */ this.customAttributes_ = {}; /** @type {UniformDefinitions} * @private */ this.uniforms_ = {}; // add hit detection attribute if enabled if (this.hitDetectionEnabled_) { this.customAttributes_['hitColor'] = { callback() { return colorEncodeIdAndPack(this.ref, tmpColor); }, size: 2, }; } // add attributes & uniforms coming from all shaders for (const styleShader of this.styleShaders) { for (const attributeName in styleShader.attributes) { if (attributeName in this.customAttributes_) { // already defined: skip continue; } this.customAttributes_[attributeName] = styleShader.attributes[attributeName]; } for (const uniformName in styleShader.uniforms) { if (uniformName in this.uniforms_) { // already defined: skip continue; } this.uniforms_[uniformName] = styleShader.uniforms[uniformName]; } } // create a render pass for each shader /** * @type {Array} * @private */ this.renderPasses_ = this.styleShaders.map((styleShader) => { /** @type {RenderPass} */ const renderPass = {}; const customAttributesDesc = Object.entries(this.customAttributes_).map( ([name, value]) => { const isUsed = name in styleShader.attributes || name === 'hitColor'; return { name: isUsed ? `a_${name}` : null, // giving a null name means this is only used for "spacing" in between attributes size: value.size || 1, type: AttributeType.FLOAT, }; }, ); // set up each subpass if (styleShader.builder.getFillVertexShader()) { renderPass.fillRenderPass = { vertexShader: styleShader.builder.getFillVertexShader(), fragmentShader: styleShader.builder.getFillFragmentShader(), attributesDesc: [ { name: Attributes.POSITION, size: 2, type: AttributeType.FLOAT, }, ...customAttributesDesc, ], instancedAttributesDesc: [], // no instanced rendering for polygons instancePrimitiveVertexCount: 3, }; } if (styleShader.builder.getStrokeVertexShader()) { renderPass.strokeRenderPass = { vertexShader: styleShader.builder.getStrokeVertexShader(), fragmentShader: styleShader.builder.getStrokeFragmentShader(), attributesDesc: [ { name: Attributes.LOCAL_POSITION, size: 2, type: AttributeType.FLOAT, }, ], instancedAttributesDesc: [ { name: Attributes.SEGMENT_START, size: 2, type: AttributeType.FLOAT, }, { name: Attributes.MEASURE_START, size: 1, type: AttributeType.FLOAT, }, { name: Attributes.SEGMENT_END, size: 2, type: AttributeType.FLOAT, }, { name: Attributes.MEASURE_END, size: 1, type: AttributeType.FLOAT, }, { name: Attributes.JOIN_ANGLES, size: 2, type: AttributeType.FLOAT, }, { name: Attributes.DISTANCE_LOW, size: 1, type: AttributeType.FLOAT, }, { name: Attributes.DISTANCE_HIGH, size: 1, type: AttributeType.FLOAT, }, { name: Attributes.ANGLE_TANGENT_SUM, size: 1, type: AttributeType.FLOAT, }, ...customAttributesDesc, ], instancePrimitiveVertexCount: 6, }; } if (styleShader.builder.getSymbolVertexShader()) { renderPass.symbolRenderPass = { vertexShader: styleShader.builder.getSymbolVertexShader(), fragmentShader: styleShader.builder.getSymbolFragmentShader(), attributesDesc: [ { name: Attributes.LOCAL_POSITION, size: 2, type: AttributeType.FLOAT, }, ], instancedAttributesDesc: [ { name: Attributes.POSITION, size: 2, type: AttributeType.FLOAT, }, ...customAttributesDesc, ], instancePrimitiveVertexCount: 6, }; } return renderPass; }); this.hasFill_ = this.renderPasses_.some((pass) => pass.fillRenderPass); this.hasStroke_ = this.renderPasses_.some((pass) => pass.strokeRenderPass); this.hasSymbol_ = this.renderPasses_.some((pass) => pass.symbolRenderPass); // this will initialize render passes with the given helper this.setHelper(helper); } /** * @param {import('./MixedGeometryBatch.js').default} geometryBatch Geometry batch * @param {import("../../transform.js").Transform} transform Transform to apply to coordinates * @return {Promise} A promise resolving to WebGL buffers; returns null if buffers are empty */ async generateBuffers(geometryBatch, transform) { if (geometryBatch.isEmpty()) { return null; } const renderInstructions = this.generateRenderInstructions_( geometryBatch, transform, ); const [polygonBuffers, lineStringBuffers, pointBuffers] = await Promise.all( [ this.generateBuffersForType_( renderInstructions.polygonInstructions, 'Polygon', transform, ), this.generateBuffersForType_( renderInstructions.lineStringInstructions, 'LineString', transform, ), this.generateBuffersForType_( renderInstructions.pointInstructions, 'Point', transform, ), ], ); // also return the inverse of the transform that was applied when generating buffers const invertVerticesTransform = makeInverseTransform( createTransform(), transform, ); return { polygonBuffers: polygonBuffers, lineStringBuffers: lineStringBuffers, pointBuffers: pointBuffers, invertVerticesTransform: invertVerticesTransform, }; } /** * @param {import('./MixedGeometryBatch.js').default} geometryBatch Geometry batch * @param {import("../../transform.js").Transform} transform Transform to apply to coordinates * @return {RenderInstructions} Render instructions * @private */ generateRenderInstructions_(geometryBatch, transform) { const polygonInstructions = this.hasFill_ ? generatePolygonRenderInstructions( geometryBatch.polygonBatch, new Float32Array(0), this.customAttributes_, transform, ) : null; const lineStringInstructions = this.hasStroke_ ? generateLineStringRenderInstructions( geometryBatch.lineStringBatch, new Float32Array(0), this.customAttributes_, transform, ) : null; const pointInstructions = this.hasSymbol_ ? generatePointRenderInstructions( geometryBatch.pointBatch, new Float32Array(0), this.customAttributes_, transform, ) : null; return { polygonInstructions, lineStringInstructions, pointInstructions, }; } /** * @param {Float32Array|null} renderInstructions Render instructions * @param {import("../../geom/Geometry.js").Type} geometryType Geometry type * @param {import("../../transform.js").Transform} transform Transform to apply to coordinates * @return {Promise|null} Indices buffer and vertices buffer; null if nothing to render * @private */ generateBuffersForType_(renderInstructions, geometryType, transform) { if (renderInstructions === null) { return null; } const messageId = workerMessageCounter++; let messageType; switch (geometryType) { case 'Polygon': messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS; break; case 'LineString': messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS; break; case 'Point': messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS; break; default: // pass } /** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */ const message = { id: messageId, type: messageType, renderInstructions: renderInstructions.buffer, renderInstructionsTransform: transform, customAttributesSize: getCustomAttributesSize(this.customAttributes_), }; const WEBGL_WORKER = getWebGLWorker(); WEBGL_WORKER.postMessage(message, [renderInstructions.buffer]); // leave ownership of render instructions renderInstructions = null; return new Promise((resolve) => { /** * @param {{data: import('./constants.js').WebGLWorkerGenerateBuffersMessage}} event Event. */ const handleMessage = (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 WEBGL_WORKER.removeEventListener('message', handleMessage); // the helper has disposed in the meantime; the promise will not be resolved if (!this.helper_.getGL()) { return; } // copy & flush received buffers to GPU const indicesBuffer = new WebGLArrayBuffer( ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW, ).fromArrayBuffer(received.indicesBuffer); const vertexAttributesBuffer = new WebGLArrayBuffer( ARRAY_BUFFER, DYNAMIC_DRAW, ).fromArrayBuffer(received.vertexAttributesBuffer); const instanceAttributesBuffer = new WebGLArrayBuffer( ARRAY_BUFFER, DYNAMIC_DRAW, ).fromArrayBuffer(received.instanceAttributesBuffer); this.helper_.flushBufferData(indicesBuffer); this.helper_.flushBufferData(vertexAttributesBuffer); this.helper_.flushBufferData(instanceAttributesBuffer); resolve([ indicesBuffer, vertexAttributesBuffer, instanceAttributesBuffer, ]); }; WEBGL_WORKER.addEventListener('message', handleMessage); }); } /** * Render the geometries in the given buffers. * @param {WebGLBuffers} buffers WebGL Buffers to draw * @param {import("../../Map.js").FrameState} frameState Frame state * @param {function(): void} preRenderCallback This callback will be called right before drawing, and can be used to set uniforms */ render(buffers, frameState, preRenderCallback) { for (const renderPass of this.renderPasses_) { renderPass.fillRenderPass && this.renderInternal_( buffers.polygonBuffers[0], buffers.polygonBuffers[1], buffers.polygonBuffers[2], renderPass.fillRenderPass, frameState, preRenderCallback, ); renderPass.strokeRenderPass && this.renderInternal_( buffers.lineStringBuffers[0], buffers.lineStringBuffers[1], buffers.lineStringBuffers[2], renderPass.strokeRenderPass, frameState, preRenderCallback, ); renderPass.symbolRenderPass && this.renderInternal_( buffers.pointBuffers[0], buffers.pointBuffers[1], buffers.pointBuffers[2], renderPass.symbolRenderPass, frameState, preRenderCallback, ); } } /** * @param {WebGLArrayBuffer} indicesBuffer Indices buffer * @param {WebGLArrayBuffer} vertexAttributesBuffer Vertex attributes buffer * @param {WebGLArrayBuffer} instanceAttributesBuffer Instance attributes buffer * @param {SubRenderPass} subRenderPass Render pass (program, attributes, etc.) specific to one geometry type * @param {import("../../Map.js").FrameState} frameState Frame state. * @param {function(): void} preRenderCallback This callback will be called right before drawing, and can be used to set uniforms * @private */ renderInternal_( indicesBuffer, vertexAttributesBuffer, instanceAttributesBuffer, subRenderPass, frameState, preRenderCallback, ) { const renderCount = indicesBuffer.getSize(); if (renderCount === 0) { return; } const usesInstancedRendering = subRenderPass.instancedAttributesDesc.length; this.helper_.useProgram(subRenderPass.program, frameState); this.helper_.bindBuffer(vertexAttributesBuffer); this.helper_.bindBuffer(indicesBuffer); this.helper_.enableAttributes(subRenderPass.attributesDesc); this.helper_.bindBuffer(instanceAttributesBuffer); this.helper_.enableAttributesInstanced( subRenderPass.instancedAttributesDesc, ); preRenderCallback(); if (usesInstancedRendering) { const instanceAttributesStride = subRenderPass.instancedAttributesDesc.reduce( (prev, curr) => prev + (curr.size || 1), 0, ); const instanceCount = instanceAttributesBuffer.getSize() / instanceAttributesStride; this.helper_.drawElementsInstanced(0, renderCount, instanceCount); } else { this.helper_.drawElements(0, renderCount); } } /** * @param {import('../../webgl/Helper.js').default} helper Helper * @param {WebGLBuffers} buffers WebGL Buffers to reload if any */ setHelper(helper, buffers = null) { this.helper_ = helper; for (const renderPass of this.renderPasses_) { if (renderPass.fillRenderPass) { renderPass.fillRenderPass.program = this.helper_.getProgram( renderPass.fillRenderPass.fragmentShader, renderPass.fillRenderPass.vertexShader, ); } if (renderPass.strokeRenderPass) { renderPass.strokeRenderPass.program = this.helper_.getProgram( renderPass.strokeRenderPass.fragmentShader, renderPass.strokeRenderPass.vertexShader, ); } if (renderPass.symbolRenderPass) { renderPass.symbolRenderPass.program = this.helper_.getProgram( renderPass.symbolRenderPass.fragmentShader, renderPass.symbolRenderPass.vertexShader, ); } } this.helper_.addUniforms(this.uniforms_); if (buffers) { if (buffers.polygonBuffers) { this.helper_.flushBufferData(buffers.polygonBuffers[0]); this.helper_.flushBufferData(buffers.polygonBuffers[1]); this.helper_.flushBufferData(buffers.polygonBuffers[2]); } if (buffers.lineStringBuffers) { this.helper_.flushBufferData(buffers.lineStringBuffers[0]); this.helper_.flushBufferData(buffers.lineStringBuffers[1]); this.helper_.flushBufferData(buffers.lineStringBuffers[2]); } if (buffers.pointBuffers) { this.helper_.flushBufferData(buffers.pointBuffers[0]); this.helper_.flushBufferData(buffers.pointBuffers[1]); this.helper_.flushBufferData(buffers.pointBuffers[2]); } } } } export default VectorStyleRenderer; /** * Breaks down a vector style into an array of prebuilt shader builders with attributes and uniforms * @param {FlatStyleLike|StyleShaders|Array} style Vector style * @param {import('../../style/flat.js').StyleVariables} variables Style variables * @return {Array} Array of style shaders */ export function convertStyleToShaders(style, variables) { // possible cases: // - single shader // - multiple shaders // - single style // - multiple styles // - multiple rules const asArray = Array.isArray(style) ? style : [style]; // if array of rules: break rules into separate styles, compute "else" filters if ('style' in asArray[0]) { /** @type {Array} */ const shaders = []; const rules = /** @type {Array} */ (asArray); const previousFilters = []; for (const rule of rules) { /** @type {Array} */ const ruleStyles = Array.isArray(rule.style) ? rule.style : [rule.style]; /** @type {import("../../expr/expression.js").EncodedExpression} */ let currentFilter = rule.filter; if (rule.else && previousFilters.length) { currentFilter = [ 'all', ...previousFilters.map((filter) => ['!', filter]), ]; if (rule.filter) { currentFilter.push(rule.filter); } if (currentFilter.length < 3) { currentFilter = currentFilter[1]; } } if (rule.filter) { previousFilters.push(rule.filter); } // parse each style and convert to shader const styleShaders = ruleStyles.map((style) => parseLiteralStyle(style, variables, currentFilter), ); shaders.push(...styleShaders); } return shaders; } // if array of shaders: return as is if ('builder' in asArray[0]) { return /** @type {Array} */ (asArray); } // array of flat styles: simply convert to shaders return /** @type {Array} */ (asArray).map((style) => parseLiteralStyle(style, variables, null), ); }