From c3113a093df1c951e60ad96a4086df861eb91204 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 19 Sep 2019 09:16:34 -0400 Subject: [PATCH] Add auto primitiveShape to the webgl point feature. The new default primitiveShape, `auto`, uses a sprite (a webgl point) if points are smaller than the GPU's maximum point size and a triangle if any are larger. Specific primitive shapes can still be specified, as there are performance reasons for each of them and auto detection has some time cost. --- CHANGELOG.md | 1 + examples/animation/main.js | 2 +- src/pointFeature.js | 27 +++++++-- src/util/mockVGL.js | 8 ++- src/webgl/pointFeature.js | 108 +++++++++++++++++++++++++++--------- src/webgl/webglRenderer.js | 3 + tests/cases/pointFeature.js | 25 ++++++++- 7 files changed, 137 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f0c1bb9e..99eca53417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Improvements - Points with small radii or thin strokes are rendered better (#1021) - When only updating point styles, don't recompute geometry transforms (#1022) +- WebGL point features automatically use the most memory-efficient primitive shape for the point sizes used based on the system's graphics capabilities (#1031) ### Changes - Switched the default tile server to Stamen Design's toner-lite. (#1020) diff --git a/examples/animation/main.js b/examples/animation/main.js index 292f41fb2f..4d3234653b 100644 --- a/examples/animation/main.js +++ b/examples/animation/main.js @@ -435,7 +435,7 @@ $(function () { map.createLayer('osm'); layer = map.createLayer('feature', layerOptions); pointFeature = layer.createFeature('point', { - primitiveShape: query.primitive ? query.primitive : 'sprite' + primitiveShape: query.primitive ? query.primitive : geo.pointFeature.primitiveShapes.auto }) .position(function (d) { return {x: d[2], y: d[1]}; diff --git a/src/pointFeature.js b/src/pointFeature.js index 1d952d6f61..8287521421 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -11,13 +11,19 @@ var feature = require('./feature'); * style options. * @property {boolean|geo.pointFeature.clusteringSpec} [clustering=false] * Enable point clustering. - * @property {string} [primitiveShape='sprite'] For the webgl renderer, select - * the primitive shape. This is one of `'triangle'`, `'square'`, or - * `'sprite'`. `sprite` uses the least memory, `triangle` is fastest if the - * vertex shader is the bottleneck, and `square` is fastest if the fragment - * shader is the bottleneck. `sprite` may not work for very large points. + * @property {string} [primitiveShape='auto'] For the webgl renderer, select + * the primitive shape. This is one of `pointFeature.primitiveShapes`: + * `'auto'`, `'sprite'`, `'triangle'`, or `'square'`. `sprite` uses the + * least memory but has a maximum size dependent on the GPU, `triangle` is + * fastest if the vertex shader is the bottleneck, and `square` is fastest if + * the fragment shader is the bottleneck. `auto` will use `sprite` unless + * the largest point exceeds the size that can be rendered via GL points, and + * then it will switch to `triangle`. The computation for `auto` uses some + * time, so using a specific primitive could be faster. * @property {boolean} [dynamicDraw=false] For the webgl renderer, if this is * truthy, webgl source buffers can be modified and updated directly. + * truthy, webgl source buffers can be modified and updated directly. This + * is not strictly necessary, as it is just a recommendation for the GPU. */ /** @@ -541,5 +547,16 @@ pointFeature.capabilities = { stroke: 'point.stroke' }; +/** + * Support primitive shapes + * @enum + */ +pointFeature.primitiveShapes = { + auto: 'auto', + sprite: 'sprite', + triangle: 'triangle', + square: 'square' +}; + inherit(pointFeature, feature); module.exports = pointFeature; diff --git a/src/util/mockVGL.js b/src/util/mockVGL.js index 262885aab2..809ff801f6 100644 --- a/src/util/mockVGL.js +++ b/src/util/mockVGL.js @@ -88,8 +88,10 @@ module.exports.mockWebglRenderer = function mockWebglRenderer(supported) { getExtension: incID('getExtension'), getParameter: function (key) { count('getParameter'); - if (key === vgl.GL.DEPTH_BITS) { - return 16; + switch (key) { + case vgl.GL.ALIASED_POINT_SIZE_RANGE: return [1, 64]; + case vgl.GL.DEPTH_BITS: return 16; + case vgl.GL.MAX_TEXTURE_SIZE: return 4096; } }, getProgramParameter: function (id, key) { @@ -174,6 +176,8 @@ module.exports.mockWebglRenderer = function mockWebglRenderer(supported) { webglRenderer.supported = function () { return !!supported; }; + webglRenderer._maxTextureSize = 4096; + webglRenderer._maxPointSize = 64; vgl._mocked = true; vgl.mockCounts = function () { diff --git a/src/webgl/pointFeature.js b/src/webgl/pointFeature.js index 972802b4ab..8672c1123d 100644 --- a/src/webgl/pointFeature.js +++ b/src/webgl/pointFeature.js @@ -2,6 +2,7 @@ var $ = require('jquery'); var inherit = require('../inherit'); var registerFeature = require('../registry').registerFeature; var pointFeature = require('../pointFeature'); +var webglRenderer = require('./webglRenderer'); /** * Create a new instance of webgl.pointFeature. @@ -41,23 +42,22 @@ var webgl_pointFeature = function (arg) { m_pixelWidthUniform = null, m_aspectUniform = null, m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - /* If you are drawing very large points, you will often get better - * performance using a different primitiveShape. The 'sprite' shape uses - * the least memory, but has hardware-specific limitations to its size. - * 'triangle' seems to be fastest on low-powered hardware, but 'square' - * visits fewer fragments. */ - m_primitiveShape = 'sprite', // arg can change this, below + m_primitiveShapeAuto = true, + m_primitiveShape = pointFeature.primitiveShapes.auto, // arg can change this, below m_modelViewUniform, m_origin, s_init = this._init, s_update = this._update, s_updateStyleFromArray = this.updateStyleFromArray; - if (arg.primitiveShape === 'triangle' || - arg.primitiveShape === 'square' || - arg.primitiveShape === 'sprite') { + if (pointFeature.primitiveShapes[arg.primitiveShape] !== undefined) { m_primitiveShape = arg.primitiveShape; } + m_primitiveShapeAuto = m_primitiveShape === pointFeature.primitiveShapes.auto; + if (m_primitiveShapeAuto) { + m_primitiveShape = pointFeature.primitiveShapes.sprite; + m_primitiveShapeAuto = true; + } /** * Create the vertex shader for points. @@ -67,7 +67,7 @@ var webgl_pointFeature = function (arg) { function createVertexShader() { var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); shader.setShaderSource( - m_primitiveShape === 'sprite' ? vertexShaderSprite : vertexShaderPoly); + m_primitiveShape === pointFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly); return shader; } @@ -79,7 +79,7 @@ var webgl_pointFeature = function (arg) { function createFragmentShader() { var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); shader.setShaderSource( - m_primitiveShape === 'sprite' ? fragmentShaderSprite : fragmentShaderPoly); + m_primitiveShape === pointFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly); return shader; } @@ -97,7 +97,7 @@ var webgl_pointFeature = function (arg) { function pointPolygon(x, y, w, h) { var verts; switch (m_primitiveShape) { - case 'triangle': + case pointFeature.primitiveShapes.triangle: /* Use an equilateral triangle. While this has 30% more area than a * square, the reduction in vertices should help more than the * processing the additional fragments. */ @@ -107,11 +107,7 @@ var webgl_pointFeature = function (arg) { x + w * Math.sqrt(3.0), y + h ]; break; - case 'sprite': - /* Point sprite uses only one vertex per point. */ - verts = [x, y]; - break; - default: // "square" + case pointFeature.primitiveShapes.square: /* Use a surrounding square split diagonally into two triangles. */ verts = [ x - w, y + h, @@ -122,6 +118,10 @@ var webgl_pointFeature = function (arg) { x + w, y + h ]; break; + default: // sprite + /* Point sprite uses only one vertex per point. */ + verts = [x, y]; + break; } return verts; } @@ -148,7 +148,7 @@ var webgl_pointFeature = function (arg) { fillColor, fillColorVal, fillColorFunc, vpf = m_this.verticesPerFeature(), data = m_this.data(), - item, ivpf, ivpf3, iunit, i3, + item, ivpf, ivpf3, iunit, i3, maxr = 0, geom = m_mapper.geometryData(); posFunc = m_this.position(); @@ -185,9 +185,7 @@ var webgl_pointFeature = function (arg) { posBuf = util.getGeomBuffer(geom, 'pos', vpf * numPts * 3); - if (m_primitiveShape !== 'sprite') { - unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2); - } + unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2); indices = geom.primitive(0).indices(); if (!(indices instanceof Uint16Array) || indices.length !== vpf * numPts) { indices = new Uint16Array(vpf * numPts); @@ -207,7 +205,7 @@ var webgl_pointFeature = function (arg) { for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) { item = data[i]; if (!onlyStyle) { - if (m_primitiveShape !== 'sprite') { + if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) { for (j = 0; j < unit.length; j += 1, iunit += 1) { unitBuf[iunit] = unit[j]; } @@ -222,6 +220,9 @@ var webgl_pointFeature = function (arg) { fillVal = fillFunc(item, i) ? 1.0 : 0.0; fillOpacityVal = fillOpacityFunc(item, i); fillColorVal = fillColorFunc(item, i); + if (m_primitiveShapeAuto && ((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) && radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) { + maxr = radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0); + } for (j = 0; j < vpf; j += 1, ivpf += 1, ivpf3 += 3) { if (!onlyStyle) { posBuf[ivpf3] = position[i3]; @@ -243,6 +244,18 @@ var webgl_pointFeature = function (arg) { } } + if (m_primitiveShapeAuto && + ((m_primitiveShape === pointFeature.primitiveShapes.sprite && maxr > webglRenderer._maxPointSize) || + (m_primitiveShape !== pointFeature.primitiveShapes.sprite && maxr <= webglRenderer._maxPointSize))) { + // Switch primitive + m_primitiveShape = maxr > webglRenderer._maxPointSize ? pointFeature.primitiveShapes.triangle : pointFeature.primitiveShapes.sprite; + m_this.renderer().contextRenderer().removeActor(m_actor); + m_actor = null; + m_this._init(true); + createGLPoints(); + return; + } + if (!onlyStyle) { geom.boundsDirty(true); m_mapper.modified(); @@ -379,10 +392,47 @@ var webgl_pointFeature = function (arg) { return m_this; }; + /** + * Get or set the primitiveShape. + * + * @param {geo.pointFeature.primitiveShapes} [primitiveShape] If specified, + * the new primitive shape. + * @param {boolean} [currentShape] If truthy and getting the shape, return + * the shape currently in use if the shape is set to `auto`. If falsy, + * return the specifiec primitiveShape, which may be `auto`. + * @returns {geo.pointFeature.primitiveShapes|this} The primitiveShape or + * this instance of the feature. + */ + this.primitiveShape = function (primitiveShape, currentShape) { + if (primitiveShape === undefined) { + return currentShape || !m_primitiveShapeAuto ? m_primitiveShape : pointFeature.primitiveShapes.auto; + } + if (pointFeature.primitiveShapes[primitiveShape] !== undefined) { + var update = false; + if (primitiveShape === pointFeature.primitiveShapes.auto) { + update = !m_primitiveShapeAuto; + m_primitiveShapeAuto = true; + } else { + update = m_primitiveShapeAuto || m_primitiveShape !== primitiveShape; + m_primitiveShapeAuto = false; + m_primitiveShape = primitiveShape; + } + if (update) { + m_this.renderer().contextRenderer().removeActor(m_actor); + m_actor = null; + m_this._init(true); + m_this.modified(); + } + } + return m_this; + }; + /** * Initialize. + * + * @param {boolean} [reinit] If truthy, skip the parent class's init method. */ - this._init = function () { + this._init = function (reinit) { var prog = vgl.shaderProgram(), vertexShader = createVertexShader(), fragmentShader = createFragmentShader(), @@ -419,11 +469,13 @@ var webgl_pointFeature = function (arg) { 1, vgl.vertexAttributeKeysIndexed.Eight, {'name': 'fillOpacity'}), sourceStrokeOpacity = vgl.sourceDataAnyfv( 1, vgl.vertexAttributeKeysIndexed.Nine, {'name': 'strokeOpacity'}), - primitive = new vgl.triangles(); + primitive; m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin); - if (m_primitiveShape === 'sprite') { + if (m_primitiveShape === pointFeature.primitiveShapes.sprite) { primitive = new vgl.points(); + } else { + primitive = new vgl.triangles(); } m_pixelWidthUniform = new vgl.floatUniform( @@ -431,11 +483,13 @@ var webgl_pointFeature = function (arg) { m_aspectUniform = new vgl.floatUniform( 'aspect', m_this.renderer().width() / m_this.renderer().height()); - s_init.call(m_this, arg); + if (!reinit) { + s_init.call(m_this, arg); + } m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); - if (m_primitiveShape !== 'sprite') { + if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) { prog.addVertexAttribute(unitAttr, vgl.vertexAttributeKeysIndexed.One); } diff --git a/src/webgl/webglRenderer.js b/src/webgl/webglRenderer.js index 7ba97439d0..b700bd3816 100644 --- a/src/webgl/webglRenderer.js +++ b/src/webgl/webglRenderer.js @@ -330,6 +330,9 @@ webglRenderer.supported = function () { webglRenderer._unmaskedRenderer = ctx.getParameter(ctx.getExtension( 'WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL); } + // store some parameters for convenience + webglRenderer._maxTextureSize = ctx.getParameter(ctx.MAX_TEXTURE_SIZE); + webglRenderer._maxPointSize = ctx.getParameter(ctx.ALIASED_POINT_SIZE_RANGE)[1]; checkedWebGL = true; } catch (e) { console.warn('No webGL support'); diff --git a/tests/cases/pointFeature.js b/tests/cases/pointFeature.js index 9e18484dc8..e4bd2836c0 100644 --- a/tests/cases/pointFeature.js +++ b/tests/cases/pointFeature.js @@ -330,13 +330,16 @@ describe('geo.pointFeature', function () { return vgl.mockCounts().createProgram >= (glCounts.createProgram || 0) + 1; }); it('other primitive shapes', function () { + expect(point.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto); + expect(point.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite); point2 = layer.createFeature('point', { - primitiveShape: 'triangle' + primitiveShape: geo.pointFeature.primitiveShapes.triangle }).data(testPoints); + expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.triangle); expect(point2.verticesPerFeature()).toBe(3); layer.deleteFeature(point2); point2 = layer.createFeature('point', { - primitiveShape: 'square' + primitiveShape: geo.pointFeature.primitiveShapes.square }).data(testPoints); expect(point2.verticesPerFeature()).toBe(6); glCounts = $.extend({}, vgl.mockCounts()); @@ -345,6 +348,24 @@ describe('geo.pointFeature', function () { waitForIt('next render gl B', function () { return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1; }); + it('change primitive shapes', function () { + expect(point2.primitiveShape(geo.pointFeature.primitiveShapes.auto)).toBe(point2); + point2.draw(); + expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto); + expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite); + point2.style('radius', 20000); + point2.draw(); + expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto); + expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.triangle); + point2.style('radius', 20); + point2.draw(); + expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto); + expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite); + expect(point2.primitiveShape(geo.pointFeature.primitiveShapes.triangle)).toBe(point2); + point2.draw(); + expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.triangle); + expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.triangle); + }); it('updateStyleFromArray single', function () { point.draw = function () { count += 1;