diff --git a/doc/errors/index.md b/doc/errors/index.md index 687d9efbf1..79ac5dac7c 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -228,3 +228,11 @@ Missing or invalid `size`. ### 61 Cannot determine IIIF Image API version from provided image information JSON. + +### 62 + +A `WebGLArrayBuffer` must either be of type `ELEMENT_ARRAY_BUFFER` or `ARRAY_BUFFER`. + +### 63 + +Support for the `OES_element_index_uint` WebGL extension is mandatory for WebGL layers. diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html index 7d7220d17b..f465510d4c 100644 --- a/examples/icon-sprite-webgl.html +++ b/examples/icon-sprite-webgl.html @@ -13,5 +13,8 @@ docs: > The dataset contains around 80k points and can be found here: https://www.kaggle.com/NUFORC/ufo-sightings tags: "webgl, icon, sprite, point, ufo" +cloak: + - key: pk.eyJ1IjoidHNjaGF1YiIsImEiOiJjaW5zYW5lNHkxMTNmdWttM3JyOHZtMmNtIn0.CDIBD8H-G2Gf-cPkIuWtRg + value: Your Mapbox access token from https://mapbox.com/ here ---
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js index 4b8be0cb98..7c0e1da6bd 100644 --- a/examples/icon-sprite-webgl.js +++ b/examples/icon-sprite-webgl.js @@ -10,6 +10,8 @@ import {fromLonLat} from '../src/ol/proj.js'; import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer.js'; import {lerp} from '../src/ol/math.js'; +const key = 'pk.eyJ1IjoidHNjaGF1YiIsImEiOiJjaW5zYW5lNHkxMTNmdWttM3JyOHZtMmNtIn0.CDIBD8H-G2Gf-cPkIuWtRg'; + const vectorSource = new Vector({ features: [], attributions: 'National UFO Reporting Center' @@ -105,7 +107,7 @@ new Map({ layers: [ new TileLayer({ source: new TileJSON({ - url: 'https://api.tiles.mapbox.com/v3/mapbox.world-dark.json?secure', + url: 'https://api.tiles.mapbox.com/v4/mapbox.world-dark.json?access_token=' + key, crossOrigin: 'anonymous' }) }), diff --git a/rendering/webpack.config.js b/rendering/webpack.config.js index 948ce5c151..64fbb63861 100644 --- a/rendering/webpack.config.js +++ b/rendering/webpack.config.js @@ -22,5 +22,16 @@ module.exports = { context: __dirname, target: 'web', entry: entry, - devtool: 'source-map' + devtool: 'source-map', + module: { + rules: [{ + test: /\.js$/, + use: { + loader: path.join(__dirname, '../examples/webpack/worker-loader.js') + }, + include: [ + path.join(__dirname, '../src/ol/worker') + ] + }] + } }; diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js index 6e2b87d9f9..b50b54d1b2 100644 --- a/src/ol/renderer/webgl/Layer.js +++ b/src/ol/renderer/webgl/Layer.js @@ -5,6 +5,26 @@ import LayerRenderer from '../Layer.js'; import WebGLHelper from '../../webgl/Helper.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 @@ -36,7 +56,11 @@ class WebGLLayerRenderer extends LayerRenderer { const options = opt_options || {}; - this.helper_ = new WebGLHelper({ + /** + * @type {WebGLHelper} + * @protected + */ + this.helper = new WebGLHelper({ postProcesses: options.postProcesses, uniforms: options.uniforms }); @@ -55,96 +79,151 @@ class WebGLLayerRenderer extends LayerRenderer { * @api */ getShaderCompileErrors() { - return this.helper_.getShaderCompileErrors(); + return this.helper.getShaderCompileErrors(); } } /** - * Pushes vertices and indices to the given buffers using the geometry coordinates and the following properties - * from the feature: - * - `color` - * - `opacity` - * - `size` (for points) - * - `u0`, `v0`, `u1`, `v1` (for points) - * - `rotateWithView` (for points) - * - `width` (for lines) - * Custom attributes can be designated using the `opt_attributes` argument, otherwise other properties on the - * feature will be ignored. - * @param {import("../../webgl/Buffer").default} vertexBuffer WebGL buffer in which new vertices will be pushed. - * @param {import("../../webgl/Buffer").default} indexBuffer WebGL buffer in which new indices will be pushed. - * @param {import("../../format/GeoJSON").GeoJSONFeature} geojsonFeature Feature in geojson format, coordinates - * expressed in EPSG:4326. - * @param {Array} [opt_attributes] Custom attributes. An array of properties which will be read from the - * feature and pushed in the buffer in the given order. Note: attributes can only be numerical! Any other type or - * NaN will result in `0` being pushed in the buffer. + * @param {Float32Array} instructions Instructons array in which to write. + * @param {number} elementIndex Index from which render instructions will be written. + * @param {number} x Point center X coordinate + * @param {number} y Point center Y coordinate + * @param {number} u0 Left texture coordinate + * @param {number} v0 Bottom texture coordinate + * @param {number} u1 Right texture coordinate + * @param {number} v1 Top texture coordinate + * @param {number} size Radius of the point + * @param {number} opacity Opacity + * @param {boolean} rotateWithView If true, the point will stay aligned with the view + * @param {Array} color Array holding red, green, blue, alpha values + * @return {number} Index from which the next element should be written + * @private */ -export function pushFeatureToBuffer(vertexBuffer, indexBuffer, geojsonFeature, opt_attributes) { - if (!geojsonFeature.geometry) { - return; - } - switch (geojsonFeature.geometry.type) { - case 'Point': - pushPointFeatureToBuffer_(vertexBuffer, indexBuffer, geojsonFeature, opt_attributes); - return; - default: - return; - } +export function writePointFeatureInstructions(instructions, elementIndex, x, y, u0, v0, u1, v1, size, opacity, rotateWithView, color) { + let i = elementIndex; + instructions[i++] = x; + instructions[i++] = y; + instructions[i++] = u0; + instructions[i++] = v0; + instructions[i++] = u1; + instructions[i++] = v1; + instructions[i++] = size; + instructions[i++] = opacity; + instructions[i++] = rotateWithView ? 1 : 0; + instructions[i++] = color[0]; + instructions[i++] = color[1]; + instructions[i++] = color[2]; + instructions[i++] = color[3]; + return i; } const tmpArray_ = []; +const bufferPositions_ = {vertexPosition: 0, indexPosition: 0}; + +export const POINT_INSTRUCTIONS_COUNT = 13; +export const POINT_VERTEX_STRIDE = 12; + +function writePointVertex(buffer, pos, x, y, offsetX, offsetY, u, v, opacity, rotateWithView, red, green, blue, alpha) { + buffer[pos + 0] = x; + buffer[pos + 1] = y; + buffer[pos + 2] = offsetX; + buffer[pos + 3] = offsetY; + buffer[pos + 4] = u; + buffer[pos + 5] = v; + buffer[pos + 6] = opacity; + buffer[pos + 7] = rotateWithView; + buffer[pos + 8] = red; + buffer[pos + 9] = green; + buffer[pos + 10] = blue; + buffer[pos + 11] = alpha; +} + +function writeCustomAttrs(buffer, pos, customAttrs) { + if (customAttrs.length) { + buffer.set(customAttrs, pos); + } +} + +/** + * 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 {import("../../webgl/Buffer").default} vertexBuffer WebGL buffer - * @param {import("../../webgl/Buffer").default} indexBuffer WebGL buffer - * @param {import("../../format/GeoJSON").GeoJSONFeature} geojsonFeature Feature - * @param {Array} [opt_attributes] Custom attributes + * @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 {BufferPositions} [bufferPositions] Buffer write positions; if not specified, positions will be set at 0. + * @param {number} [count] Amount of render instructions that will be read. Default value is POINT_INSTRUCTIONS_COUNT + * but a higher value can be provided; all values beyond the default count will be put in the vertices buffer as + * is, thus allowing specifying custom attributes. Please note: this value should not vary inside the same buffer or + * rendering will break. + * @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 */ -function pushPointFeatureToBuffer_(vertexBuffer, indexBuffer, geojsonFeature, opt_attributes) { - const stride = 12 + (opt_attributes !== undefined ? opt_attributes.length : 0); +export function writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer, bufferPositions, count) { + const count_ = count > POINT_INSTRUCTIONS_COUNT ? count : POINT_INSTRUCTIONS_COUNT; - const x = geojsonFeature.geometry.coordinates[0]; - const y = geojsonFeature.geometry.coordinates[1]; - const u0 = geojsonFeature.properties.u0; - const v0 = geojsonFeature.properties.v0; - const u1 = geojsonFeature.properties.u1; - const v1 = geojsonFeature.properties.v1; - const size = geojsonFeature.properties.size; - const opacity = geojsonFeature.properties.opacity; - const rotateWithView = geojsonFeature.properties.rotateWithView; - const color = geojsonFeature.properties.color; - const red = color[0]; - const green = color[1]; - const blue = color[2]; - const alpha = color[3]; - const baseIndex = vertexBuffer.getArray().length / stride; + const x = instructions[elementIndex + 0]; + const y = instructions[elementIndex + 1]; + const u0 = instructions[elementIndex + 2]; + const v0 = instructions[elementIndex + 3]; + const u1 = instructions[elementIndex + 4]; + const v1 = instructions[elementIndex + 5]; + const size = instructions[elementIndex + 6]; + const opacity = instructions[elementIndex + 7]; + const rotateWithView = instructions[elementIndex + 8]; + const red = instructions[elementIndex + 9]; + const green = instructions[elementIndex + 10]; + const blue = instructions[elementIndex + 11]; + const alpha = instructions[elementIndex + 12]; + + // the default vertex buffer stride is 12, plus additional custom values if any + const baseStride = POINT_VERTEX_STRIDE; + const stride = baseStride + count_ - POINT_INSTRUCTIONS_COUNT; // read custom numerical attributes on the feature - const customAttributeValues = tmpArray_; - customAttributeValues.length = opt_attributes ? opt_attributes.length : 0; - for (let i = 0; i < customAttributeValues.length; i++) { - customAttributeValues[i] = parseFloat(geojsonFeature.properties[opt_attributes[i]]) || 0; + const customAttrs = tmpArray_; + customAttrs.length = count_ - POINT_INSTRUCTIONS_COUNT; + for (let i = 0; i < customAttrs.length; i++) { + customAttrs[i] = instructions[elementIndex + POINT_INSTRUCTIONS_COUNT + 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) - vertexBuffer.getArray().push(x, y, -size / 2, -size / 2, u0, v0, opacity, rotateWithView, red, green, blue, alpha); - Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + writePointVertex(vertexBuffer, vPos, x, y, -size / 2, -size / 2, u0, v0, opacity, rotateWithView, red, green, blue, alpha); + writeCustomAttrs(vertexBuffer, vPos + baseStride, customAttrs); + vPos += stride; - vertexBuffer.getArray().push(x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView, red, green, blue, alpha); - Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + writePointVertex(vertexBuffer, vPos, x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView, red, green, blue, alpha); + writeCustomAttrs(vertexBuffer, vPos + baseStride, customAttrs); + vPos += stride; - vertexBuffer.getArray().push(x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView, red, green, blue, alpha); - Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + writePointVertex(vertexBuffer, vPos, x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView, red, green, blue, alpha); + writeCustomAttrs(vertexBuffer, vPos + baseStride, customAttrs); + vPos += stride; - vertexBuffer.getArray().push(x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView, red, green, blue, alpha); - Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + writePointVertex(vertexBuffer, vPos, x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView, red, green, blue, alpha); + writeCustomAttrs(vertexBuffer, vPos + baseStride, customAttrs); + vPos += stride; - indexBuffer.getArray().push( - baseIndex, baseIndex + 1, baseIndex + 3, - baseIndex + 1, baseIndex + 2, baseIndex + 3 - ); + 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_; } /** diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 65a25a8a9c..25eccac815 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -5,9 +5,11 @@ import WebGLArrayBuffer from '../../webgl/Buffer.js'; import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../webgl.js'; import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js'; import GeometryType from '../../geom/GeometryType.js'; -import WebGLLayerRenderer, {getBlankTexture, pushFeatureToBuffer} from './Layer.js'; -import GeoJSON from '../../format/GeoJSON.js'; -import {getUid} from '../../util.js'; +import WebGLLayerRenderer, { + getBlankTexture, + POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType, + writePointFeatureInstructions +} from './Layer.js'; import ViewHint from '../../ViewHint.js'; import {createEmpty, equals} from '../../extent.js'; import { @@ -16,6 +18,7 @@ import { multiply as multiplyTransform, apply as applyTransform } from '../../transform.js'; +import {create as createWebGLWorker} from '../../worker/webgl.js'; const VERTEX_SHADER = ` precision mediump float; @@ -208,15 +211,15 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { this.sourceRevision_ = -1; - this.verticesBuffer_ = new WebGLArrayBuffer([], DYNAMIC_DRAW); - this.indicesBuffer_ = new WebGLArrayBuffer([], DYNAMIC_DRAW); + this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW); + this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW); - this.program_ = this.helper_.getProgram( + this.program_ = this.helper.getProgram( options.fragmentShader || FRAGMENT_SHADER, options.vertexShader || VERTEX_SHADER ); - this.helper_.useProgram(this.program_); + this.helper.useProgram(this.program_); this.sizeCallback_ = options.sizeCallback || function() { return 1; @@ -241,14 +244,6 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { return false; }; - this.geojsonFormat_ = new GeoJSON(); - - /** - * @type {Object} - * @private - */ - this.geojsonFeatureCache_ = {}; - this.previousExtent_ = createEmpty(); /** @@ -272,6 +267,30 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { * @private */ this.invertRenderTransform_ = createTransform(); + + /** + * @type {Float32Array} + * @private + */ + this.renderInstructions_ = new Float32Array(0); + + this.worker_ = createWebGLWorker(); + this.worker_.addEventListener('message', function(event) { + const received = event.data; + if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) { + const projectionTransform = received.projectionTransform; + this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer); + this.indicesBuffer_.fromArrayBuffer(received.indexBuffer); + this.helper.flushBufferData(this.verticesBuffer_); + this.helper.flushBufferData(this.indicesBuffer_); + + // saves the projection transform for the current frame state + this.renderTransform_ = projectionTransform; + makeInverseTransform(this.invertRenderTransform_, this.renderTransform_); + + this.renderInstructions_ = new Float32Array(event.data.renderInstructions); + } + }.bind(this)); } /** @@ -285,11 +304,12 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { * @inheritDoc */ renderFrame(frameState) { - const layerState = frameState.layerStatesArray[frameState.layerIndex]; - this.helper_.drawElements(0, this.indicesBuffer_.getArray().length); - this.helper_.finalizeDraw(frameState); - const canvas = this.helper_.getCanvas(); + const renderCount = this.indicesBuffer_.getArray() ? this.indicesBuffer_.getArray().length : 0; + this.helper.drawElements(0, renderCount); + 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 = opacity; @@ -306,8 +326,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { const vectorSource = vectorLayer.getSource(); const viewState = frameState.viewState; - // TODO: get this from somewhere... - const stride = 12; + const stride = POINT_VERTEX_STRIDE; // the source has changed: clear the feature cache & reload features const sourceChanged = this.sourceRevision_ < vectorSource.getRevision(); @@ -328,22 +347,22 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { } // apply the current projection transform with the invert of the one used to fill buffers - this.helper_.makeProjectionTransform(frameState, this.currentTransform_); + this.helper.makeProjectionTransform(frameState, this.currentTransform_); multiplyTransform(this.currentTransform_, this.invertRenderTransform_); - this.helper_.prepareDraw(frameState); + this.helper.prepareDraw(frameState); // write new data - this.helper_.bindBuffer(ARRAY_BUFFER, this.verticesBuffer_); - this.helper_.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + this.helper.bindBuffer(this.verticesBuffer_); + this.helper.bindBuffer(this.indicesBuffer_); const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT; - this.helper_.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * stride, 0); - this.helper_.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 2); - this.helper_.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 4); - this.helper_.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 6); - this.helper_.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 7); - this.helper_.enableAttributeArray(DefaultAttrib.COLOR, 4, FLOAT, bytesPerFloat * stride, bytesPerFloat * 8); + this.helper.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * stride, 0); + this.helper.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 2); + this.helper.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 4); + this.helper.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 6); + this.helper.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 7); + this.helper.enableAttributeArray(DefaultAttrib.COLOR, 4, FLOAT, bytesPerFloat * stride, bytesPerFloat * 8); return true; } @@ -357,47 +376,59 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { const vectorLayer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); const vectorSource = vectorLayer.getSource(); - this.verticesBuffer_.getArray().length = 0; - this.indicesBuffer_.getArray().length = 0; - // saves the projection transform for the current frame state - this.helper_.makeProjectionTransform(frameState, this.renderTransform_); - makeInverseTransform(this.invertRenderTransform_, this.renderTransform_); + const projectionTransform = createTransform(); + this.helper.makeProjectionTransform(frameState, projectionTransform); + + const features = vectorSource.getFeatures(); + const totalInstructionsCount = POINT_INSTRUCTIONS_COUNT * features.length; + if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) { + this.renderInstructions_ = new Float32Array(totalInstructionsCount); + } // loop on features to fill the buffer - const features = vectorSource.getFeatures(); let feature; + const tmpCoords = []; + let elementIndex = 0; for (let i = 0; i < features.length; i++) { feature = features[i]; if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) { continue; } - let geojsonFeature = this.geojsonFeatureCache_[getUid(feature)]; - if (!geojsonFeature) { - geojsonFeature = this.geojsonFormat_.writeFeatureObject(feature); - this.geojsonFeatureCache_[getUid(feature)] = geojsonFeature; - } + tmpCoords[0] = this.coordCallback_(feature, 0); + tmpCoords[1] = this.coordCallback_(feature, 1); + applyTransform(projectionTransform, tmpCoords); - geojsonFeature.geometry.coordinates[0] = this.coordCallback_(feature, 0); - geojsonFeature.geometry.coordinates[1] = this.coordCallback_(feature, 1); - applyTransform(this.renderTransform_, geojsonFeature.geometry.coordinates); - geojsonFeature.properties = geojsonFeature.properties || {}; - geojsonFeature.properties.color = this.colorCallback_(feature, this.colorArray_); - geojsonFeature.properties.u0 = this.texCoordCallback_(feature, 0); - geojsonFeature.properties.v0 = this.texCoordCallback_(feature, 1); - geojsonFeature.properties.u1 = this.texCoordCallback_(feature, 2); - geojsonFeature.properties.v1 = this.texCoordCallback_(feature, 3); - geojsonFeature.properties.size = this.sizeCallback_(feature); - geojsonFeature.properties.opacity = this.opacityCallback_(feature); - geojsonFeature.properties.rotateWithView = this.rotateWithViewCallback_(feature) ? 1 : 0; - - pushFeatureToBuffer(this.verticesBuffer_, this.indicesBuffer_, geojsonFeature); + elementIndex = writePointFeatureInstructions( + this.renderInstructions_, + elementIndex, + tmpCoords[0], + tmpCoords[1], + this.texCoordCallback_(feature, 0), + this.texCoordCallback_(feature, 1), + this.texCoordCallback_(feature, 2), + this.texCoordCallback_(feature, 3), + this.sizeCallback_(feature), + this.opacityCallback_(feature), + this.rotateWithViewCallback_(feature), + this.colorCallback_(feature, this.colorArray_) + ); } - this.helper_.flushBufferData(ARRAY_BUFFER, this.verticesBuffer_); - this.helper_.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + /** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */ + const message = { + type: WebGLWorkerMessageType.GENERATE_BUFFERS, + renderInstructions: this.renderInstructions_.buffer + }; + // additional properties will be sent back as-is by the worker + message['projectionTransform'] = projectionTransform; + + this.worker_.postMessage(message, [this.renderInstructions_.buffer]); + this.renderInstructions_ = null; } + + } export default WebGLPointsLayerRenderer; diff --git a/src/ol/webgl/Buffer.js b/src/ol/webgl/Buffer.js index 21e5b25c8c..735a08ffd9 100644 --- a/src/ol/webgl/Buffer.js +++ b/src/ol/webgl/Buffer.js @@ -2,6 +2,8 @@ * @module ol/webgl/Buffer */ import {STATIC_DRAW, STREAM_DRAW, DYNAMIC_DRAW} from '../webgl.js'; +import {ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER} from '../webgl.js'; +import {assert} from '../asserts.js'; /** * Used to describe the intended usage for the data: `STATIC_DRAW`, `STREAM_DRAW` @@ -17,43 +19,109 @@ export const BufferUsage = { /** * @classdesc * Object used to store an array of data as well as usage information for that data. - * See the documentation of [WebGLRenderingContext.bufferData](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferData) for more info. + * Stores typed arrays internally, either Float32Array or Uint16/32Array depending on + * the buffer type (ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER) and available extensions. + * + * To populate the array, you can either use: + * * A size using `#ofSize(buffer)` + * * An `ArrayBuffer` object using `#fromArrayBuffer(buffer)` + * * A plain array using `#fromArray(array)` + * + * Note: + * See the documentation of [WebGLRenderingContext.bufferData](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferData) + * for more info on buffer usage. * @api */ class WebGLArrayBuffer { /** - * @param {Array=} opt_arr Array. - * @param {number=} opt_usage Usage, either `STATIC_DRAW`, `STREAM_DRAW` or `DYNAMIC_DRAW`. Default is `DYNAMIC_DRAW`. + * @param {number} type Buffer type, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER. + * @param {number=} opt_usage Intended usage, either `STATIC_DRAW`, `STREAM_DRAW` or `DYNAMIC_DRAW`. + * Default is `DYNAMIC_DRAW`. */ - constructor(opt_arr, opt_usage) { + constructor(type, opt_usage) { /** * @private - * @type {Array} + * @type {Float32Array|Uint32Array} */ - this.arr_ = opt_arr !== undefined ? opt_arr : []; + this.array = null; /** * @private * @type {number} */ - this.usage_ = opt_usage !== undefined ? opt_usage : BufferUsage.STATIC_DRAW; + this.type = type; + assert(type === ARRAY_BUFFER || type === ELEMENT_ARRAY_BUFFER, 62); + + /** + * @private + * @type {number} + */ + this.usage = opt_usage !== undefined ? opt_usage : BufferUsage.STATIC_DRAW; } /** - * @return {Array} Array. + * Populates the buffer with an array of the given size (all values will be zeroes). + * @param {number} size Array size + */ + ofSize(size) { + this.array = new (getArrayClassForType(this.type))(size); + } + + /** + * Populates the buffer with an array of the given size (all values will be zeroes). + * @param {Array} array Numerical array + */ + fromArray(array) { + this.array = (getArrayClassForType(this.type)).from(array); + } + + /** + * Populates the buffer with a raw binary array buffer. + * @param {ArrayBuffer} buffer Raw binary buffer to populate the array with. Note that this buffer must have been + * initialized for the same typed array class. + */ + fromArrayBuffer(buffer) { + this.array = new (getArrayClassForType(this.type))(buffer); + } + + /** + * @return {number} Buffer type. + */ + getType() { + return this.type; + } + + /** + * @return {Float32Array|Uint32Array} Array. */ getArray() { - return this.arr_; + return this.array; } /** * @return {number} Usage. */ getUsage() { - return this.usage_; + return this.usage; + } +} + +/** + * Returns a typed array constructor based on the given buffer type + * @param {number} type Buffer type, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER. + * @returns {Float32ArrayConstructor|Uint32ArrayConstructor} The typed array class to use for this buffer. + */ +export function getArrayClassForType(type) { + switch (type) { + case ARRAY_BUFFER: + return Float32Array; + case ELEMENT_ARRAY_BUFFER: + return Uint32Array; + default: + return Float32Array; } } diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index d9c2c8d638..b6b80865bd 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -2,12 +2,10 @@ * @module ol/webgl/Helper */ import {getUid} from '../util.js'; -import {EXTENSIONS as WEBGL_EXTENSIONS} from '../webgl.js'; import Disposable from '../Disposable.js'; -import {includes} from '../array.js'; import {listen, unlistenAll} from '../events.js'; import {clear} from '../obj.js'; -import {ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, TEXTURE_2D, TEXTURE_WRAP_S, TEXTURE_WRAP_T} from '../webgl.js'; +import {TEXTURE_2D, TEXTURE_WRAP_S, TEXTURE_WRAP_T, EXTENSIONS as WEBGL_EXTENSIONS} from '../webgl.js'; import ContextEventType from '../webgl/ContextEventType.js'; import { compose as composeTransform, @@ -19,6 +17,8 @@ import { import {create, fromTransform} from '../vec/mat4.js'; import WebGLPostProcessingPass from './PostProcessingPass.js'; import {getContext} from '../webgl.js'; +import {includes} from '../array.js'; +import {assert} from '../asserts.js'; /** @@ -258,15 +258,8 @@ class WebGLHelper extends Disposable { */ this.currentProgram_ = null; - /** - * @type {boolean} - */ - this.hasOESElementIndexUint = includes(WEBGL_EXTENSIONS, 'OES_element_index_uint'); - - // use the OES_element_index_uint extension if available - if (this.hasOESElementIndexUint) { - gl.getExtension('OES_element_index_uint'); - } + assert(includes(WEBGL_EXTENSIONS, 'OES_element_index_uint'), 63); + gl.getExtension('OES_element_index_uint'); listen(this.canvas_, ContextEventType.LOST, this.handleWebGLContextLost, this); @@ -347,11 +340,10 @@ class WebGLHelper extends Disposable { * Just bind the buffer if it's in the cache. Otherwise create * the WebGL buffer, bind it, populate it, and add an entry to * the cache. - * @param {number} target Target, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER. * @param {import("./Buffer").default} buffer Buffer. * @api */ - bindBuffer(target, buffer) { + bindBuffer(buffer) { const gl = this.getGL(); const bufferKey = getUid(buffer); let bufferCache = this.bufferCache_[bufferKey]; @@ -362,28 +354,19 @@ class WebGLHelper extends Disposable { webGlBuffer: webGlBuffer }; } - gl.bindBuffer(target, bufferCache.webGlBuffer); + gl.bindBuffer(buffer.getType(), bufferCache.webGlBuffer); } /** * Update the data contained in the buffer array; this is required for the * new data to be rendered - * @param {number} target Target, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER. * @param {import("./Buffer").default} buffer Buffer. * @api */ - flushBufferData(target, buffer) { + flushBufferData(buffer) { const gl = this.getGL(); - const arr = buffer.getArray(); - this.bindBuffer(target, buffer); - let /** @type {ArrayBufferView} */ arrayBuffer; - if (target == ARRAY_BUFFER) { - arrayBuffer = new Float32Array(arr); - } else if (target == ELEMENT_ARRAY_BUFFER) { - arrayBuffer = this.hasOESElementIndexUint ? - new Uint32Array(arr) : new Uint16Array(arr); - } - gl.bufferData(target, arrayBuffer, buffer.getUsage()); + this.bindBuffer(buffer); + gl.bufferData(buffer.getType(), buffer.getArray(), buffer.getUsage()); } /** @@ -462,9 +445,8 @@ class WebGLHelper extends Disposable { */ drawElements(start, end) { const gl = this.getGL(); - const elementType = this.hasOESElementIndexUint ? - gl.UNSIGNED_INT : gl.UNSIGNED_SHORT; - const elementSize = this.hasOESElementIndexUint ? 4 : 2; + const elementType = gl.UNSIGNED_INT; + const elementSize = 4; const numItems = end - start; const offsetInBytes = start * elementSize; diff --git a/src/ol/worker/webgl.js b/src/ol/worker/webgl.js new file mode 100644 index 0000000000..d1542aad4e --- /dev/null +++ b/src/ol/worker/webgl.js @@ -0,0 +1,48 @@ +/** + * A worker that does cpu-heavy tasks related to webgl rendering. + * @module ol/worker/webgl + */ +import { + POINT_INSTRUCTIONS_COUNT, + POINT_VERTEX_STRIDE, + WebGLWorkerMessageType, + writePointFeatureToBuffers +} from '../renderer/webgl/Layer.js'; +import {assign} from '../obj.js'; + +onmessage = event => { + const received = event.data; + if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) { + const renderInstructions = new Float32Array(received.renderInstructions); + const customAttributesCount = received.customAttributesCount || 0; + const instructionsCount = POINT_INSTRUCTIONS_COUNT + customAttributesCount; + + const elementsCount = renderInstructions.length / instructionsCount; + const indicesCount = elementsCount * 6; + const verticesCount = elementsCount * 4 * (POINT_VERTEX_STRIDE + customAttributesCount); + const indexBuffer = new Uint32Array(indicesCount); + const vertexBuffer = new Float32Array(verticesCount); + + let bufferPositions = null; + for (let i = 0; i < renderInstructions.length; i += instructionsCount) { + bufferPositions = writePointFeatureToBuffers( + renderInstructions, + i, + vertexBuffer, + indexBuffer, + bufferPositions, + instructionsCount); + } + + /** @type {import('../renderer/webgl/Layer').WebGLWorkerGenerateBuffersMessage} */ + const message = assign({ + vertexBuffer: vertexBuffer.buffer, + indexBuffer: indexBuffer.buffer, + renderInstructions: renderInstructions.buffer + }, received); + + postMessage(message, [vertexBuffer.buffer, indexBuffer.buffer, renderInstructions.buffer]); + } +}; + +export let create; diff --git a/tasks/serialize-workers.js b/tasks/serialize-workers.js index 0e2b14182f..cbf8929f34 100644 --- a/tasks/serialize-workers.js +++ b/tasks/serialize-workers.js @@ -20,6 +20,7 @@ async function build(input, {minify = true} = {}) { common(), resolve(), babel({ + 'externalHelpers': true, 'presets': [ [ '@babel/preset-env', diff --git a/test/spec/ol/renderer/webgl/layer.test.js b/test/spec/ol/renderer/webgl/layer.test.js index 4d48e2df0c..cb9f30f3c1 100644 --- a/test/spec/ol/renderer/webgl/layer.test.js +++ b/test/spec/ol/renderer/webgl/layer.test.js @@ -1,5 +1,7 @@ -import WebGLLayerRenderer, {getBlankTexture, pushFeatureToBuffer} from '../../../../../src/ol/renderer/webgl/Layer.js'; -import WebGLArrayBuffer from '../../../../../src/ol/webgl/Buffer.js'; +import WebGLLayerRenderer, { + getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, + writePointFeatureInstructions, writePointFeatureToBuffers +} from '../../../../../src/ol/renderer/webgl/Layer.js'; import Layer from '../../../../../src/ol/layer/Layer.js'; @@ -28,111 +30,211 @@ describe('ol.renderer.webgl.Layer', function() { }); - describe('pushFeatureToBuffer', function() { - let vertexBuffer, indexBuffer; + describe('writePointFeatureInstructions', function() { + let instructions; beforeEach(function() { - vertexBuffer = new WebGLArrayBuffer(); - indexBuffer = new WebGLArrayBuffer(); + instructions = new Float32Array(100); }); - it('does nothing if the feature has no geometry', function() { - const feature = { - type: 'Feature', - id: 'AFG', - properties: { - color: [0.5, 1, 0.2, 0.7], - size: 3 - }, - geometry: null - }; - pushFeatureToBuffer(vertexBuffer, indexBuffer, feature); - expect(vertexBuffer.getArray().length).to.eql(0); - expect(indexBuffer.getArray().length).to.eql(0); + it('writes instructions corresponding to the given parameters', function() { + const baseIndex = 17; + writePointFeatureInstructions(instructions, baseIndex, + 1, 2, 3, 4, 5, 6, + 7, 8, true, [10, 11, 12, 13]); + expect(instructions[baseIndex + 0]).to.eql(1); + expect(instructions[baseIndex + 1]).to.eql(2); + expect(instructions[baseIndex + 2]).to.eql(3); + expect(instructions[baseIndex + 3]).to.eql(4); + expect(instructions[baseIndex + 4]).to.eql(5); + expect(instructions[baseIndex + 5]).to.eql(6); + expect(instructions[baseIndex + 6]).to.eql(7); + expect(instructions[baseIndex + 7]).to.eql(8); + expect(instructions[baseIndex + 8]).to.eql(1); + expect(instructions[baseIndex + 9]).to.eql(10); + expect(instructions[baseIndex + 10]).to.eql(11); + expect(instructions[baseIndex + 11]).to.eql(12); + expect(instructions[baseIndex + 12]).to.eql(13); }); - it('adds two triangles with the correct attributes for a point geometry', function() { - const feature = { - type: 'Feature', - id: 'AFG', - properties: { - color: [0.5, 1, 0.2, 0.7], - size: 3 - }, - geometry: { - type: 'Point', - coordinates: [-75, 47] - } - }; - const attributePerVertex = 12; - pushFeatureToBuffer(vertexBuffer, indexBuffer, feature); - expect(vertexBuffer.getArray().length).to.eql(attributePerVertex * 4); - expect(indexBuffer.getArray().length).to.eql(6); + it('correctly chains writes', function() { + let baseIndex = 0; + baseIndex = writePointFeatureInstructions(instructions, baseIndex, + 1, 2, 3, 4, 5, 6, + 7, 8, true, [10, 11, 12, 13]); + baseIndex = writePointFeatureInstructions(instructions, baseIndex, + 1, 2, 3, 4, 5, 6, + 7, 8, true, [10, 11, 12, 13]); + writePointFeatureInstructions(instructions, baseIndex, + 1, 2, 3, 4, 5, 6, + 7, 8, true, [10, 11, 12, 13]); + expect(instructions[baseIndex + 0]).to.eql(1); + expect(instructions[baseIndex + 1]).to.eql(2); + expect(instructions[baseIndex + 2]).to.eql(3); + expect(instructions[baseIndex + 3]).to.eql(4); + expect(instructions[baseIndex + 4]).to.eql(5); + expect(instructions[baseIndex + 5]).to.eql(6); + expect(instructions[baseIndex + 6]).to.eql(7); + expect(instructions[baseIndex + 7]).to.eql(8); + expect(instructions[baseIndex + 8]).to.eql(1); + expect(instructions[baseIndex + 9]).to.eql(10); + expect(instructions[baseIndex + 10]).to.eql(11); + expect(instructions[baseIndex + 11]).to.eql(12); + expect(instructions[baseIndex + 12]).to.eql(13); + }); + }); + + describe('writePointFeatureToBuffers', function() { + let vertexBuffer, indexBuffer, instructions, elementIndex; + + beforeEach(function() { + vertexBuffer = new Float32Array(100); + indexBuffer = new Uint32Array(100); + instructions = new Float32Array(100); + elementIndex = 3; + + writePointFeatureInstructions(instructions, elementIndex, + 1, 2, 3, 4, 5, 6, + 7, 8, true, [10, 11, 12, 13]); }); - it('correctly sets indices & coordinates for several features', function() { - const feature = { - type: 'Feature', - id: 'AFG', - properties: { - color: [0.5, 1, 0.2, 0.7], - size: 3 - }, - geometry: { - type: 'Point', - coordinates: [-75, 47] - } - }; - const attributePerVertex = 12; - pushFeatureToBuffer(vertexBuffer, indexBuffer, feature); - pushFeatureToBuffer(vertexBuffer, indexBuffer, feature); - expect(vertexBuffer.getArray()[0]).to.eql(-75); - expect(vertexBuffer.getArray()[1]).to.eql(47); - expect(vertexBuffer.getArray()[0 + attributePerVertex]).to.eql(-75); - expect(vertexBuffer.getArray()[1 + attributePerVertex]).to.eql(47); + it('writes correctly to the buffers (without custom attributes)', function() { + const stride = POINT_VERTEX_STRIDE; + const positions = writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer); - // first point - expect(indexBuffer.getArray()[0]).to.eql(0); - expect(indexBuffer.getArray()[1]).to.eql(1); - expect(indexBuffer.getArray()[2]).to.eql(3); - expect(indexBuffer.getArray()[3]).to.eql(1); - expect(indexBuffer.getArray()[4]).to.eql(2); - expect(indexBuffer.getArray()[5]).to.eql(3); + expect(vertexBuffer[0]).to.eql(1); + expect(vertexBuffer[1]).to.eql(2); + expect(vertexBuffer[2]).to.eql(-3.5); + expect(vertexBuffer[3]).to.eql(-3.5); + expect(vertexBuffer[4]).to.eql(3); + expect(vertexBuffer[5]).to.eql(4); + expect(vertexBuffer[6]).to.eql(8); + expect(vertexBuffer[7]).to.eql(1); + expect(vertexBuffer[8]).to.eql(10); + expect(vertexBuffer[9]).to.eql(11); + expect(vertexBuffer[10]).to.eql(12); + expect(vertexBuffer[11]).to.eql(13); - // second point - expect(indexBuffer.getArray()[6]).to.eql(4); - expect(indexBuffer.getArray()[7]).to.eql(5); - expect(indexBuffer.getArray()[8]).to.eql(7); - expect(indexBuffer.getArray()[9]).to.eql(5); - expect(indexBuffer.getArray()[10]).to.eql(6); - expect(indexBuffer.getArray()[11]).to.eql(7); + expect(vertexBuffer[stride + 0]).to.eql(1); + expect(vertexBuffer[stride + 1]).to.eql(2); + expect(vertexBuffer[stride + 2]).to.eql(+3.5); + expect(vertexBuffer[stride + 3]).to.eql(-3.5); + expect(vertexBuffer[stride + 4]).to.eql(5); + expect(vertexBuffer[stride + 5]).to.eql(4); + + expect(vertexBuffer[stride * 2 + 0]).to.eql(1); + expect(vertexBuffer[stride * 2 + 1]).to.eql(2); + expect(vertexBuffer[stride * 2 + 2]).to.eql(+3.5); + expect(vertexBuffer[stride * 2 + 3]).to.eql(+3.5); + expect(vertexBuffer[stride * 2 + 4]).to.eql(5); + expect(vertexBuffer[stride * 2 + 5]).to.eql(6); + + expect(vertexBuffer[stride * 3 + 0]).to.eql(1); + expect(vertexBuffer[stride * 3 + 1]).to.eql(2); + expect(vertexBuffer[stride * 3 + 2]).to.eql(-3.5); + expect(vertexBuffer[stride * 3 + 3]).to.eql(+3.5); + expect(vertexBuffer[stride * 3 + 4]).to.eql(3); + expect(vertexBuffer[stride * 3 + 5]).to.eql(6); + + 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 adds custom attributes', function() { - const feature = { - type: 'Feature', - id: 'AFG', - properties: { - color: [0.5, 1, 0.2, 0.7], - custom: 4, - customString: '5', - custom2: 12.4, - customString2: 'abc' - }, - geometry: { - type: 'Point', - coordinates: [-75, 47] - } - }; - const attributePerVertex = 16; - pushFeatureToBuffer(vertexBuffer, indexBuffer, feature, ['custom', 'custom2', 'customString', 'customString2']); - expect(vertexBuffer.getArray().length).to.eql(attributePerVertex * 4); - expect(indexBuffer.getArray().length).to.eql(6); - expect(vertexBuffer.getArray()[12]).to.eql(4); - expect(vertexBuffer.getArray()[13]).to.eql(12.4); - expect(vertexBuffer.getArray()[14]).to.eql(5); - expect(vertexBuffer.getArray()[15]).to.eql(0); + it('writes correctly to the buffers (with custom attributes)', function() { + instructions[elementIndex + POINT_INSTRUCTIONS_COUNT] = 101; + instructions[elementIndex + POINT_INSTRUCTIONS_COUNT + 1] = 102; + instructions[elementIndex + POINT_INSTRUCTIONS_COUNT + 2] = 103; + + const stride = POINT_VERTEX_STRIDE + 3; + const positions = writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer, + undefined, POINT_INSTRUCTIONS_COUNT + 3); + + expect(vertexBuffer[0]).to.eql(1); + expect(vertexBuffer[1]).to.eql(2); + expect(vertexBuffer[2]).to.eql(-3.5); + expect(vertexBuffer[3]).to.eql(-3.5); + expect(vertexBuffer[4]).to.eql(3); + expect(vertexBuffer[5]).to.eql(4); + expect(vertexBuffer[6]).to.eql(8); + expect(vertexBuffer[7]).to.eql(1); + expect(vertexBuffer[8]).to.eql(10); + expect(vertexBuffer[9]).to.eql(11); + expect(vertexBuffer[10]).to.eql(12); + expect(vertexBuffer[11]).to.eql(13); + + expect(vertexBuffer[12]).to.eql(101); + expect(vertexBuffer[13]).to.eql(102); + expect(vertexBuffer[14]).to.eql(103); + + expect(vertexBuffer[stride + 12]).to.eql(101); + expect(vertexBuffer[stride + 13]).to.eql(102); + expect(vertexBuffer[stride + 14]).to.eql(103); + + expect(vertexBuffer[stride * 2 + 12]).to.eql(101); + expect(vertexBuffer[stride * 2 + 13]).to.eql(102); + expect(vertexBuffer[stride * 2 + 14]).to.eql(103); + + expect(vertexBuffer[stride * 3 + 12]).to.eql(101); + expect(vertexBuffer[stride * 3 + 13]).to.eql(102); + expect(vertexBuffer[stride * 3 + 14]).to.eql(103); + + 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() { + const stride = POINT_VERTEX_STRIDE; + let positions = writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer); + positions = writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer, positions); + positions = writePointFeatureToBuffers(instructions, elementIndex, vertexBuffer, indexBuffer, positions); + + expect(vertexBuffer[0]).to.eql(1); + expect(vertexBuffer[1]).to.eql(2); + expect(vertexBuffer[2]).to.eql(-3.5); + expect(vertexBuffer[3]).to.eql(-3.5); + + expect(vertexBuffer[stride * 4 + 0]).to.eql(1); + expect(vertexBuffer[stride * 4 + 1]).to.eql(2); + expect(vertexBuffer[stride * 4 + 2]).to.eql(-3.5); + expect(vertexBuffer[stride * 4 + 3]).to.eql(-3.5); + + expect(vertexBuffer[stride * 8 + 0]).to.eql(1); + expect(vertexBuffer[stride * 8 + 1]).to.eql(2); + expect(vertexBuffer[stride * 8 + 2]).to.eql(-3.5); + expect(vertexBuffer[stride * 8 + 3]).to.eql(-3.5); + + 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('getBlankTexture', function() { diff --git a/test/spec/ol/renderer/webgl/pointslayer.test.js b/test/spec/ol/renderer/webgl/pointslayer.test.js index f0d104a70c..e08f32ed8f 100644 --- a/test/spec/ol/renderer/webgl/pointslayer.test.js +++ b/test/spec/ol/renderer/webgl/pointslayer.test.js @@ -1,12 +1,11 @@ import Feature from '../../../../../src/ol/Feature.js'; import Point from '../../../../../src/ol/geom/Point.js'; -import LineString from '../../../../../src/ol/geom/LineString.js'; import VectorLayer from '../../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../../src/ol/source/Vector.js'; import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/PointsLayer.js'; import {get as getProjection} from '../../../../../src/ol/proj.js'; -import Polygon from '../../../../../src/ol/geom/Polygon.js'; import ViewHint from '../../../../../src/ol/ViewHint.js'; +import {POINT_VERTEX_STRIDE, WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js'; describe('ol.renderer.webgl.PointsLayer', function() { @@ -52,44 +51,46 @@ describe('ol.renderer.webgl.PointsLayer', function() { projection: projection, resolution: 1, rotation: 0, - center: [10, 10] + center: [0, 0] }, - size: [256, 256], + size: [2, 2], extent: [-100, -100, 100, 100] }; }); it('calls WebGlHelper#prepareDraw', function() { - const spy = sinon.spy(renderer.helper_, 'prepareDraw'); + const spy = sinon.spy(renderer.helper, 'prepareDraw'); renderer.prepareFrame(frameState); expect(spy.called).to.be(true); }); - it('fills up a buffer with 2 triangles per point', function() { + it('fills up a buffer with 2 triangles per point', function(done) { layer.getSource().addFeature(new Feature({ geometry: new Point([10, 20]) })); - renderer.prepareFrame(frameState); - - const attributePerVertex = 12; - expect(renderer.verticesBuffer_.getArray().length).to.eql(4 * attributePerVertex); - expect(renderer.indicesBuffer_.getArray().length).to.eql(6); - }); - - it('ignores geometries other than points', function() { layer.getSource().addFeature(new Feature({ - geometry: new LineString([[10, 20], [30, 20]]) - })); - layer.getSource().addFeature(new Feature({ - geometry: new Polygon([[10, 20], [30, 20], [30, 10], [10, 20]]) + geometry: new Point([30, 40]) })); renderer.prepareFrame(frameState); - expect(renderer.verticesBuffer_.getArray().length).to.eql(0); - expect(renderer.indicesBuffer_.getArray().length).to.eql(0); + const attributePerVertex = POINT_VERTEX_STRIDE; + + renderer.worker_.addEventListener('message', function(event) { + if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) { + return; + } + expect(renderer.verticesBuffer_.getArray().length).to.eql(2 * 4 * attributePerVertex); + expect(renderer.indicesBuffer_.getArray().length).to.eql(2 * 6); + + expect(renderer.verticesBuffer_.getArray()[0]).to.eql(10); + expect(renderer.verticesBuffer_.getArray()[1]).to.eql(20); + expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 0]).to.eql(30); + expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 1]).to.eql(40); + done(); + }); }); - it('clears the buffers when the features are gone', function() { + it('clears the buffers when the features are gone', function(done) { const source = layer.getSource(); source.addFeature(new Feature({ geometry: new Point([10, 20]) @@ -100,9 +101,15 @@ describe('ol.renderer.webgl.PointsLayer', function() { })); renderer.prepareFrame(frameState); - const attributePerVertex = 12; - expect(renderer.verticesBuffer_.getArray().length).to.eql(4 * attributePerVertex); - expect(renderer.indicesBuffer_.getArray().length).to.eql(6); + renderer.worker_.addEventListener('message', function(event) { + if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) { + return; + } + const attributePerVertex = 12; + expect(renderer.verticesBuffer_.getArray().length).to.eql(4 * attributePerVertex); + expect(renderer.indicesBuffer_.getArray().length).to.eql(6); + done(); + }); }); it('rebuilds the buffers only when not interacting or animating', function() { diff --git a/test/spec/ol/webgl/buffer.test.js b/test/spec/ol/webgl/buffer.test.js index af590da0e4..09c6daf694 100644 --- a/test/spec/ol/webgl/buffer.test.js +++ b/test/spec/ol/webgl/buffer.test.js @@ -1,51 +1,76 @@ -import WebGLArrayBuffer from '../../../../src/ol/webgl/Buffer.js'; +import WebGLArrayBuffer, {getArrayClassForType} from '../../../../src/ol/webgl/Buffer.js'; +import { + ARRAY_BUFFER, + ELEMENT_ARRAY_BUFFER, + STATIC_DRAW, + STREAM_DRAW +} from '../../../../src/ol/webgl.js'; describe('ol.webgl.Buffer', function() { describe('constructor', function() { - describe('without an argument', function() { - - let b; - beforeEach(function() { - b = new WebGLArrayBuffer(); - }); - - it('constructs an empty instance', function() { - expect(b.getArray()).to.be.empty(); - }); - + it('sets the default usage when not specified', function() { + const b = new WebGLArrayBuffer(ARRAY_BUFFER); + expect(b.getUsage()).to.be(STATIC_DRAW); }); - describe('with a single array argument', function() { - - let b; - beforeEach(function() { - b = new WebGLArrayBuffer([0, 1, 2, 3]); - }); - - it('constructs a populated instance', function() { - expect(b.getArray()).to.eql([0, 1, 2, 3]); - }); - + it('sets the given usage when specified', function() { + const b = new WebGLArrayBuffer(ARRAY_BUFFER, STREAM_DRAW); + expect(b.getUsage()).to.be(STREAM_DRAW); }); + it('raises an error if an incorrect type is used', function(done) { + try { + new WebGLArrayBuffer(1234); + } catch (e) { + done(); + } + done(true); + }); }); - describe('with an empty instance', function() { + describe('#getArrayClassForType', function() { + it('returns the correct typed array constructor', function() { + expect(getArrayClassForType(ARRAY_BUFFER)).to.be(Float32Array); + expect(getArrayClassForType(ELEMENT_ARRAY_BUFFER)).to.be(Uint32Array); + }); + }); + + describe('populate methods', function() { let b; beforeEach(function() { - b = new WebGLArrayBuffer(); + b = new WebGLArrayBuffer(ARRAY_BUFFER); }); - describe('getArray', function() { + it('initializes the array using a size', function() { + b.ofSize(12); + expect(b.getArray().length).to.be(12); + expect(b.getArray()[0]).to.be(0); + expect(b.getArray()[11]).to.be(0); + }); - it('returns an empty array', function() { - expect(b.getArray()).to.be.empty(); - }); + it('initializes the array using an array', function() { + b.fromArray([1, 2, 3, 4, 5]); + expect(b.getArray().length).to.be(5); + expect(b.getArray()[0]).to.be(1); + expect(b.getArray()[1]).to.be(2); + expect(b.getArray()[2]).to.be(3); + expect(b.getArray()[3]).to.be(4); + expect(b.getArray()[4]).to.be(5); + }); + it('initializes the array using a size', function() { + const a = Float32Array.of(1, 2, 3, 4, 5); + b.fromArrayBuffer(a.buffer); + expect(b.getArray().length).to.be(5); + expect(b.getArray()[0]).to.be(1); + expect(b.getArray()[1]).to.be(2); + expect(b.getArray()[2]).to.be(3); + expect(b.getArray()[3]).to.be(4); + expect(b.getArray()[4]).to.be(5); }); }); diff --git a/test/spec/ol/worker/webgl.test.js b/test/spec/ol/worker/webgl.test.js new file mode 100644 index 0000000000..b15fd5c829 --- /dev/null +++ b/test/spec/ol/worker/webgl.test.js @@ -0,0 +1,51 @@ +import {create} from '../../../../src/ol/worker/webgl.js'; +import { + POINT_INSTRUCTIONS_COUNT, + WebGLWorkerMessageType +} from '../../../../src/ol/renderer/webgl/Layer.js'; + + +describe('ol/worker/webgl', function() { + + let worker; + beforeEach(function() { + worker = create(); + }); + + afterEach(function() { + if (worker) { + worker.terminate(); + } + worker = null; + }); + + describe('messaging', function() { + describe('GENERATE_BUFFERS', function() { + it('responds with buffer data', function(done) { + worker.addEventListener('error', done); + + 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(POINT_INSTRUCTIONS_COUNT); + + const message = { + type: WebGLWorkerMessageType.GENERATE_BUFFERS, + renderInstructions: instructions, + testInt: 101, + testString: 'abcd' + }; + + worker.postMessage(message); + }); + }); + }); + +});