Skip to content

Commit

Permalink
Add auto primitiveShape to the webgl point feature.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
manthey committed Sep 19, 2019
1 parent 3d6472f commit c3113a0
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion examples/animation/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]};
Expand Down
27 changes: 22 additions & 5 deletions src/pointFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

/**
Expand Down Expand Up @@ -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;
8 changes: 6 additions & 2 deletions src/util/mockVGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () {
Expand Down
108 changes: 81 additions & 27 deletions src/webgl/pointFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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. */
Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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];
}
Expand All @@ -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];
Expand All @@ -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();
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -419,23 +469,27 @@ 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(
'pixelWidth', 2.0 / m_this.renderer().width());
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);
}

Expand Down
3 changes: 3 additions & 0 deletions src/webgl/webglRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
25 changes: 23 additions & 2 deletions tests/cases/pointFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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;
Expand Down

0 comments on commit c3113a0

Please sign in to comment.