diff --git a/examples/filter-points-webgl.js b/examples/filter-points-webgl.js index 75bfe406d1..76e2deab0a 100644 --- a/examples/filter-points-webgl.js +++ b/examples/filter-points-webgl.js @@ -11,7 +11,6 @@ import {clamp, lerp} from '../src/ol/math'; import Stamen from '../src/ol/source/Stamen'; const vectorSource = new Vector({ - features: [], attributions: 'NASA' }); diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js new file mode 100644 index 0000000000..c17dfbb56d --- /dev/null +++ b/src/ol/renderer/webgl/Layer.js @@ -0,0 +1,162 @@ +/** + * @module ol/renderer/webgl/Layer + */ +import LayerRenderer from '../Layer.js'; +import WebGLHelper from '../../webgl/Helper'; + + +/** + * @typedef {Object} PostProcessesOptions + * @property {number} [scaleRatio] Scale ratio; if < 1, the post process will render to a texture smaller than + * the main canvas that will then be sampled up (useful for saving resource on blur steps). + * @property {string} [vertexShader] Vertex shader source + * @property {string} [fragmentShader] Fragment shader source + * @property {Object.} [uniforms] Uniform definitions for the post process step + */ + +/** + * @typedef {Object} Options + * @property {Object.} [uniforms] Uniform definitions for the post process steps + * @property {Array} [postProcesses] Post-processes definitions + */ + +/** + * @classdesc + * Base WebGL renderer class. + * Holds all logic related to data manipulation & some common rendering logic + */ +class WebGLLayerRenderer extends LayerRenderer { + + /** + * @param {import("../../layer/Layer.js").default} layer Layer. + * @param {Options=} [opt_options] Options. + */ + constructor(layer, opt_options) { + super(layer); + + const options = opt_options || {}; + + this.helper_ = new WebGLHelper({ + postProcesses: options.postProcesses, + uniforms: options.uniforms + }); + } + + /** + * @inheritDoc + */ + disposeInternal() { + super.disposeInternal(); + } + + /** + * Will return the last shader compilation errors. If no error happened, will return null; + * @return {string|null} Errors, or null if last compilation was successful + * @api + */ + 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. + */ +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; + } +} + +const tmpArray_ = []; + +/** + * 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 + * @private + */ +function pushPointFeatureToBuffer_(vertexBuffer, indexBuffer, geojsonFeature, opt_attributes) { + const stride = 12 + (opt_attributes !== undefined ? opt_attributes.length : 0); + + 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; + + // 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; + } + + // 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); + + vertexBuffer.getArray().push(x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView, red, green, blue, alpha); + Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + + vertexBuffer.getArray().push(x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView, red, green, blue, alpha); + Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + + vertexBuffer.getArray().push(x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView, red, green, blue, alpha); + Array.prototype.push.apply(vertexBuffer.getArray(), customAttributeValues); + + indexBuffer.getArray().push( + baseIndex, baseIndex + 1, baseIndex + 3, + baseIndex + 1, baseIndex + 2, baseIndex + 3 + ); +} + +/** + * Returns a texture of 1x1 pixel, white + * @private + * @return {ImageData} Image data. + */ +export function getBlankTexture() { + const canvas = document.createElement('canvas'); + const image = canvas.getContext('2d').createImageData(1, 1); + image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255; + return image; +} + +export default WebGLLayerRenderer; diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index 37dbc27830..77b4e5f623 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -1,11 +1,21 @@ /** * @module ol/renderer/webgl/PointsLayer */ -import LayerRenderer from '../Layer'; import WebGLArrayBuffer from '../../webgl/Buffer'; import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../webgl'; -import WebGLHelper, {DefaultAttrib} from '../../webgl/Helper'; +import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper'; import GeometryType from '../../geom/GeometryType'; +import WebGLLayerRenderer, {getBlankTexture, pushFeatureToBuffer} from './Layer'; +import GeoJSON from '../../format/GeoJSON'; +import {getUid} from '../../util'; +import ViewHint from '../../ViewHint'; +import {createEmpty, equals} from '../../extent'; +import { + create as createTransform, + makeInverse as makeInverseTransform, + multiply as multiplyTransform, + apply as applyTransform +} from '../../transform'; const VERTEX_SHADER = ` precision mediump float; @@ -55,15 +65,6 @@ const FRAGMENT_SHADER = ` gl_FragColor.rgb *= gl_FragColor.a; }`; -/** - * @typedef {Object} PostProcessesOptions - * @property {number} [scaleRatio] Scale ratio; if < 1, the post process will render to a texture smaller than - * the main canvas that will then be sampled up (useful for saving resource on blur steps). - * @property {string} [vertexShader] Vertex shader source - * @property {string} [fragmentShader] Fragment shader source - * @property {Object.} [uniforms] Uniform definitions for the post process step - */ - /** * @typedef {Object} Options * @property {function(import("../../Feature").default):number} [sizeCallback] Will be called on every feature in the @@ -91,7 +92,7 @@ const FRAGMENT_SHADER = ` * @property {string} [fragmentShader] Fragment shader source * @property {Object.} [uniforms] Uniform definitions for the post process steps * Please note that `u_texture` is reserved for the main texture slot. - * @property {Array} [postProcesses] Post-processes definitions + * @property {Array} [postProcesses] Post-processes definitions */ /** @@ -186,22 +187,23 @@ const FRAGMENT_SHADER = ` * * @api */ -class WebGLPointsLayerRenderer extends LayerRenderer { +class WebGLPointsLayerRenderer extends WebGLLayerRenderer { /** * @param {import("../../layer/Vector.js").default} vectorLayer Vector layer. * @param {Options=} [opt_options] Options. */ constructor(vectorLayer, opt_options) { - super(vectorLayer); - const options = opt_options || {}; const uniforms = options.uniforms || {}; - uniforms.u_texture = options.texture || this.getDefaultTexture(); - this.helper_ = new WebGLHelper({ - postProcesses: options.postProcesses, - uniforms: uniforms + uniforms.u_texture = options.texture || getBlankTexture(); + const projectionMatrixTransform = createTransform(); + uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform; + + super(vectorLayer, { + uniforms: uniforms, + postProcesses: options.postProcesses }); this.sourceRevision_ = -1; @@ -238,6 +240,38 @@ class WebGLPointsLayerRenderer extends LayerRenderer { this.rotateWithViewCallback_ = options.rotateWithViewCallback || function() { return false; }; + + this.geojsonFormat_ = new GeoJSON(); + + /** + * @type {Object} + * @private + */ + this.geojsonFeatureCache_ = {}; + + this.previousExtent_ = createEmpty(); + + /** + * This transform is updated on every frame and is the composition of: + * - invert of the world->screen transform that was used when rebuilding buffers (see `this.renderTransform_`) + * - current world->screen transform + * @type {import("../../transform.js").Transform} + * @private + */ + this.currentTransform_ = projectionMatrixTransform; + + /** + * This transform is updated when buffers are rebuilt and converts world space coordinates to screen space + * @type {import("../../transform.js").Transform} + * @private + */ + this.renderTransform_ = createTransform(); + + /** + * @type {import("../../transform.js").Transform} + * @private + */ + this.invertRenderTransform_ = createTransform(); } /** @@ -269,58 +303,35 @@ class WebGLPointsLayerRenderer extends LayerRenderer { prepareFrame(frameState) { const vectorLayer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()); const vectorSource = vectorLayer.getSource(); + const viewState = frameState.viewState; + // TODO: get this from somewhere... const stride = 12; - this.helper_.prepareDraw(frameState); - - if (this.sourceRevision_ < vectorSource.getRevision()) { + // the source has changed: clear the feature cache & reload features + const sourceChanged = this.sourceRevision_ < vectorSource.getRevision(); + if (sourceChanged) { this.sourceRevision_ = vectorSource.getRevision(); - this.verticesBuffer_.getArray().length = 0; - this.indicesBuffer_.getArray().length = 0; + this.geojsonFeatureCache_ = {}; - const viewState = frameState.viewState; const projection = viewState.projection; const resolution = viewState.resolution; - - // loop on features to fill the buffer vectorSource.loadFeatures([-Infinity, -Infinity, Infinity, Infinity], resolution, projection); - vectorSource.forEachFeature((feature) => { - if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) { - return; - } - const x = this.coordCallback_(feature, 0); - const y = this.coordCallback_(feature, 1); - const u0 = this.texCoordCallback_(feature, 0); - const v0 = this.texCoordCallback_(feature, 1); - const u1 = this.texCoordCallback_(feature, 2); - const v1 = this.texCoordCallback_(feature, 3); - const size = this.sizeCallback_(feature); - const opacity = this.opacityCallback_(feature); - const rotateWithView = this.rotateWithViewCallback_(feature) ? 1 : 0; - const color = this.colorCallback_(feature, this.colorArray_); - const red = color[0]; - const green = color[1]; - const blue = color[2]; - const alpha = color[3]; - const baseIndex = this.verticesBuffer_.getArray().length / stride; - - this.verticesBuffer_.getArray().push( - x, y, -size / 2, -size / 2, u0, v0, opacity, rotateWithView, red, green, blue, alpha, - x, y, +size / 2, -size / 2, u1, v0, opacity, rotateWithView, red, green, blue, alpha, - x, y, +size / 2, +size / 2, u1, v1, opacity, rotateWithView, red, green, blue, alpha, - x, y, -size / 2, +size / 2, u0, v1, opacity, rotateWithView, red, green, blue, alpha - ); - this.indicesBuffer_.getArray().push( - baseIndex, baseIndex + 1, baseIndex + 3, - baseIndex + 1, baseIndex + 2, baseIndex + 3 - ); - }); - - this.helper_.flushBufferData(ARRAY_BUFFER, this.verticesBuffer_); - this.helper_.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); } + const viewNotMoving = !frameState.viewHints[ViewHint.ANIMATING] && !frameState.viewHints[ViewHint.INTERACTING]; + const extentChanged = !equals(this.previousExtent_, frameState.extent); + if ((sourceChanged || extentChanged) && viewNotMoving) { + this.rebuildBuffers_(frameState); + this.previousExtent_ = frameState.extent.slice(); + } + + // apply the current projection transform with the invert of the one used to fill buffers + this.helper_.makeProjectionTransform(frameState, this.currentTransform_); + multiplyTransform(this.currentTransform_, this.invertRenderTransform_); + + this.helper_.prepareDraw(frameState); + // write new data this.helper_.bindBuffer(ARRAY_BUFFER, this.verticesBuffer_); this.helper_.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); @@ -337,24 +348,54 @@ class WebGLPointsLayerRenderer extends LayerRenderer { } /** - * Will return the last shader compilation errors. If no error happened, will return null; - * @return {string|null} Errors, or null if last compilation was successful - * @api - */ - getShaderCompileErrors() { - return this.helper_.getShaderCompileErrors(); - } - - /** - * Returns a texture of 1x1 pixel, white + * Rebuild internal webgl buffers based on current view extent; costly, should not be called too much + * @param {import("../../PluggableMap").FrameState} frameState Frame state. * @private - * @return {ImageData} Image data. */ - getDefaultTexture() { - const canvas = document.createElement('canvas'); - const image = canvas.getContext('2d').createImageData(1, 1); - image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255; - return image; + rebuildBuffers_(frameState) { + 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_); + + // loop on features to fill the buffer + const features = vectorSource.getFeatures(); + let feature; + 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; + } + + 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); + } + + this.helper_.flushBufferData(ARRAY_BUFFER, this.verticesBuffer_); + this.helper_.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); } } diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 180654c25f..b5ec037408 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -62,7 +62,7 @@ export const DefaultAttrib = { }; /** - * @typedef {number|Array|HTMLCanvasElement|HTMLImageElement|ImageData} UniformLiteralValue + * @typedef {number|Array|HTMLCanvasElement|HTMLImageElement|ImageData|import("../transform").Transform} UniformLiteralValue */ /** @@ -273,12 +273,6 @@ class WebGLHelper extends Disposable { listen(this.canvas_, ContextEventType.RESTORED, this.handleWebGLContextRestored, this); - /** - * @private - * @type {import("../transform.js").Transform} - */ - this.projectionMatrix_ = createTransform(); - /** * @private * @type {import("../transform.js").Transform} @@ -514,14 +508,6 @@ class WebGLHelper extends Disposable { applyFrameState(frameState) { const size = frameState.size; const rotation = frameState.viewState.rotation; - const resolution = frameState.viewState.resolution; - const center = frameState.viewState.center; - - // set the "uniform" values (coordinates 0,0 are the center of the view) - const projectionMatrix = resetTransform(this.projectionMatrix_); - scaleTransform(projectionMatrix, 2 / (resolution * size[0]), 2 / (resolution * size[1])); - rotateTransform(projectionMatrix, -rotation); - translateTransform(projectionMatrix, -center[0], -center[1]); const offsetScaleMatrix = resetTransform(this.offsetScaleMatrix_); scaleTransform(offsetScaleMatrix, 2 / size[0], 2 / size[1]); @@ -531,7 +517,6 @@ class WebGLHelper extends Disposable { rotateTransform(offsetRotateMatrix, -rotation); } - this.setUniformMatrixValue(DefaultUniform.PROJECTION_MATRIX, fromTransform(this.tmpMat4_, projectionMatrix)); this.setUniformMatrixValue(DefaultUniform.OFFSET_SCALE_MATRIX, fromTransform(this.tmpMat4_, offsetScaleMatrix)); this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix)); } @@ -565,7 +550,9 @@ class WebGLHelper extends Disposable { // fill texture slots by increasing index gl.uniform1i(this.getUniformLocation(uniform.name), textureSlot++); - } else if (Array.isArray(value)) { + } else if (Array.isArray(value) && value.length === 6) { + this.setUniformMatrixValue(uniform.name, fromTransform(this.tmpMat4_, value)); + } else if (Array.isArray(value) && value.length <= 4) { switch (value.length) { case 2: gl.uniform2f(this.getUniformLocation(uniform.name), value[0], value[1]); @@ -689,6 +676,28 @@ class WebGLHelper extends Disposable { return this.attribLocations_[name]; } + /** + * Modifies the given transform to apply the rotation/translation/scaling of the given frame state. + * The resulting transform can be used to convert world space coordinates to view coordinates. + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../transform").Transform} transform Transform to update. + * @return {import("../transform").Transform} The updated transform object. + * @api + */ + makeProjectionTransform(frameState, transform) { + const size = frameState.size; + const rotation = frameState.viewState.rotation; + const resolution = frameState.viewState.resolution; + const center = frameState.viewState.center; + + resetTransform(transform); + scaleTransform(transform, 2 / (resolution * size[0]), 2 / (resolution * size[1])); + rotateTransform(transform, -rotation); + translateTransform(transform, -center[0], -center[1]); + + return transform; + } + /** * Give a value for a standard float uniform * @param {string} uniform Uniform name diff --git a/test/spec/ol/renderer/webgl/layer.test.js b/test/spec/ol/renderer/webgl/layer.test.js new file mode 100644 index 0000000000..83c24a6704 --- /dev/null +++ b/test/spec/ol/renderer/webgl/layer.test.js @@ -0,0 +1,150 @@ +import WebGLLayerRenderer, {getBlankTexture, pushFeatureToBuffer} from '../../../../../src/ol/renderer/webgl/Layer'; +import WebGLArrayBuffer from '../../../../../src/ol/webgl/Buffer'; +import Layer from '../../../../../src/ol/layer/Layer'; + + +describe('ol.renderer.webgl.Layer', function() { + + describe('constructor', function() { + + let target; + + beforeEach(function() { + target = document.createElement('div'); + target.style.width = '256px'; + target.style.height = '256px'; + document.body.appendChild(target); + }); + + afterEach(function() { + document.body.removeChild(target); + }); + + it('creates a new instance', function() { + const layer = new Layer({}); + const renderer = new WebGLLayerRenderer(layer); + expect(renderer).to.be.a(WebGLLayerRenderer); + }); + + }); + + describe('pushFeatureToBuffer', function() { + let vertexBuffer, indexBuffer; + + beforeEach(function() { + vertexBuffer = new WebGLArrayBuffer(); + indexBuffer = new WebGLArrayBuffer(); + }); + + 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('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 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); + + // 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); + + // 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); + }); + + 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); + }); + }); + + describe('getBlankTexture', function() { + it('creates a 1x1 white texture', function() { + const texture = getBlankTexture(); + expect(texture.height).to.eql(1); + expect(texture.width).to.eql(1); + expect(texture.data[0]).to.eql(255); + expect(texture.data[1]).to.eql(255); + expect(texture.data[2]).to.eql(255); + expect(texture.data[3]).to.eql(255); + }); + }); + +}); diff --git a/test/spec/ol/renderer/webgl/pointslayer.test.js b/test/spec/ol/renderer/webgl/pointslayer.test.js new file mode 100644 index 0000000000..b85048b789 --- /dev/null +++ b/test/spec/ol/renderer/webgl/pointslayer.test.js @@ -0,0 +1,142 @@ +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'; +import {get as getProjection} from '../../../../../src/ol/proj'; +import Polygon from '../../../../../src/ol/geom/Polygon'; +import ViewHint from '../../../../../src/ol/ViewHint'; + + +describe('ol.renderer.webgl.PointsLayer', function() { + + describe('constructor', function() { + + let target; + + beforeEach(function() { + target = document.createElement('div'); + target.style.width = '256px'; + target.style.height = '256px'; + document.body.appendChild(target); + }); + + afterEach(function() { + document.body.removeChild(target); + }); + + it('creates a new instance', function() { + const layer = new VectorLayer({ + source: new VectorSource() + }); + const renderer = new WebGLPointsLayerRenderer(layer); + expect(renderer).to.be.a(WebGLPointsLayerRenderer); + }); + + }); + + describe('#prepareFrame', function() { + let layer, renderer, frameState; + + beforeEach(function() { + layer = new VectorLayer({ + source: new VectorSource() + }); + renderer = new WebGLPointsLayerRenderer(layer); + const projection = getProjection('EPSG:3857'); + frameState = { + skippedFeatureUids: {}, + viewHints: [], + viewState: { + projection: projection, + resolution: 1, + rotation: 0, + center: [10, 10] + }, + size: [256, 256], + extent: [-100, -100, 100, 100] + }; + }); + + it('calls WebGlHelper#prepareDraw', function() { + 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() { + 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]]) + })); + renderer.prepareFrame(frameState); + + expect(renderer.verticesBuffer_.getArray().length).to.eql(0); + expect(renderer.indicesBuffer_.getArray().length).to.eql(0); + }); + + it('clears the buffers when the features are gone', function() { + const source = layer.getSource(); + source.addFeature(new Feature({ + geometry: new Point([10, 20]) + })); + source.removeFeature(source.getFeatures()[0]); + source.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('rebuilds the buffers only when not interacting or animating', function() { + const spy = sinon.spy(renderer, 'rebuildBuffers_'); + + frameState.viewHints[ViewHint.INTERACTING] = 1; + frameState.viewHints[ViewHint.ANIMATING] = 0; + renderer.prepareFrame(frameState); + expect(spy.called).to.be(false); + + frameState.viewHints[ViewHint.INTERACTING] = 0; + frameState.viewHints[ViewHint.ANIMATING] = 1; + renderer.prepareFrame(frameState); + expect(spy.called).to.be(false); + + frameState.viewHints[ViewHint.INTERACTING] = 0; + frameState.viewHints[ViewHint.ANIMATING] = 0; + renderer.prepareFrame(frameState); + expect(spy.called).to.be(true); + }); + + it('rebuilds the buffers only when the frame extent changed', function() { + const spy = sinon.spy(renderer, 'rebuildBuffers_'); + + renderer.prepareFrame(frameState); + expect(spy.callCount).to.be(1); + + renderer.prepareFrame(frameState); + expect(spy.callCount).to.be(1); + + frameState.extent = [10, 20, 30, 40]; + renderer.prepareFrame(frameState); + expect(spy.callCount).to.be(2); + }); + }); + +}); diff --git a/test/spec/ol/webgl/helper.test.js b/test/spec/ol/webgl/helper.test.js index 0a48a337d6..b6a75761bd 100644 --- a/test/spec/ol/webgl/helper.test.js +++ b/test/spec/ol/webgl/helper.test.js @@ -1,4 +1,9 @@ import WebGLHelper from '../../../../src/ol/webgl/Helper'; +import { + create as createTransform, + rotate as rotateTransform, + scale as scaleTransform, translate as translateTransform +} from '../../../../src/ol/transform'; const VERTEX_SHADER = ` @@ -95,7 +100,8 @@ describe('ol.webgl.WebGLHelper', function() { uniforms: { u_test1: 42, u_test2: [1, 3], - u_test3: document.createElement('canvas') + u_test3: document.createElement('canvas'), + u_test4: createTransform() } }); h.useProgram(h.getProgram(FRAGMENT_SHADER, VERTEX_SHADER)); @@ -116,13 +122,15 @@ describe('ol.webgl.WebGLHelper', function() { }); it('has processed uniforms', function() { - expect(h.uniforms_.length).to.eql(3); + expect(h.uniforms_.length).to.eql(4); expect(h.uniforms_[0].name).to.eql('u_test1'); expect(h.uniforms_[1].name).to.eql('u_test2'); expect(h.uniforms_[2].name).to.eql('u_test3'); + expect(h.uniforms_[3].name).to.eql('u_test4'); expect(h.uniforms_[0].location).to.not.eql(-1); expect(h.uniforms_[1].location).to.not.eql(-1); expect(h.uniforms_[2].location).to.not.eql(-1); + expect(h.uniforms_[3].location).to.not.eql(-1); expect(h.uniforms_[2].texture).to.not.eql(undefined); }); }); @@ -181,5 +189,33 @@ describe('ol.webgl.WebGLHelper', function() { }); }); + describe('#makeProjectionTransform', function() { + let h; + let frameState; + beforeEach(function() { + h = new WebGLHelper(); + + frameState = { + size: [100, 150], + viewState: { + rotation: 0.4, + resolution: 2, + center: [10, 20] + } + }; + }); + + it('gives out the correct transform', function() { + const scaleX = 2 / frameState.size[0] / frameState.viewState.resolution; + const scaleY = 2 / frameState.size[1] / frameState.viewState.resolution; + const given = createTransform(); + const expected = createTransform(); + scaleTransform(expected, scaleX, scaleY); + rotateTransform(expected, -frameState.viewState.rotation); + translateTransform(expected, -frameState.viewState.center[0], -frameState.viewState.center[1]); + expect(h.makeProjectionTransform(frameState, given)).to.eql(expected); + }); + }); + }); });