Skip to content

Commit 91876b3

Browse files
authored
Merge pull request #1031 from OpenGeoscience/auto-point-primitive
Add auto primitiveShape to the webgl point feature.
2 parents e82bb51 + 0003a64 commit 91876b3

File tree

7 files changed

+139
-37
lines changed

7 files changed

+139
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Points with small radii or thin strokes are rendered better (#1021)
1010
- When only updating point styles, don't recompute geometry transforms (#1022)
1111
- Optimized a transform code path for pixel coordinates (#1023)
12+
- WebGL point features automatically use the most memory-efficient primitive shape for the point sizes used based on the system's graphics capabilities (#1031)
1213

1314
### Changes
1415
- Switched the default tile server to Stamen Design's toner-lite. (#1020)

examples/animation/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ $(function () {
435435
map.createLayer('osm');
436436
layer = map.createLayer('feature', layerOptions);
437437
pointFeature = layer.createFeature('point', {
438-
primitiveShape: query.primitive ? query.primitive : 'sprite'
438+
primitiveShape: query.primitive ? query.primitive : geo.pointFeature.primitiveShapes.auto
439439
})
440440
.position(function (d) {
441441
return {x: d[2], y: d[1]};

src/pointFeature.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ var feature = require('./feature');
1111
* style options.
1212
* @property {boolean|geo.pointFeature.clusteringSpec} [clustering=false]
1313
* Enable point clustering.
14-
* @property {string} [primitiveShape='sprite'] For the webgl renderer, select
15-
* the primitive shape. This is one of `'triangle'`, `'square'`, or
16-
* `'sprite'`. `sprite` uses the least memory, `triangle` is fastest if the
17-
* vertex shader is the bottleneck, and `square` is fastest if the fragment
18-
* shader is the bottleneck. `sprite` may not work for very large points.
14+
* @property {string} [primitiveShape='auto'] For the webgl renderer, select
15+
* the primitive shape. This is one of `pointFeature.primitiveShapes`:
16+
* `'auto'`, `'sprite'`, `'triangle'`, or `'square'`. `sprite` uses the
17+
* least memory but has a maximum size dependent on the GPU, `triangle` is
18+
* fastest if the vertex shader is the bottleneck, and `square` is fastest if
19+
* the fragment shader is the bottleneck. `auto` will use `sprite` unless
20+
* the largest point exceeds the size that can be rendered via GL points, and
21+
* then it will switch to `triangle`. The computation for `auto` uses some
22+
* time, so using a specific primitive could be faster.
1923
* @property {boolean} [dynamicDraw=false] For the webgl renderer, if this is
2024
* truthy, webgl source buffers can be modified and updated directly.
25+
* truthy, webgl source buffers can be modified and updated directly. This
26+
* is not strictly necessary, as it is just a recommendation for the GPU.
2127
*/
2228

2329
/**
@@ -541,5 +547,16 @@ pointFeature.capabilities = {
541547
stroke: 'point.stroke'
542548
};
543549

550+
/**
551+
* Support primitive shapes
552+
* @enum
553+
*/
554+
pointFeature.primitiveShapes = {
555+
auto: 'auto',
556+
sprite: 'sprite',
557+
triangle: 'triangle',
558+
square: 'square'
559+
};
560+
544561
inherit(pointFeature, feature);
545562
module.exports = pointFeature;

src/util/mockVGL.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ module.exports.mockWebglRenderer = function mockWebglRenderer(supported) {
8888
getExtension: incID('getExtension'),
8989
getParameter: function (key) {
9090
count('getParameter');
91-
if (key === vgl.GL.DEPTH_BITS) {
92-
return 16;
91+
switch (key) {
92+
case vgl.GL.ALIASED_POINT_SIZE_RANGE: return [1, 64];
93+
case vgl.GL.DEPTH_BITS: return 16;
94+
case vgl.GL.MAX_TEXTURE_SIZE: return 4096;
9395
}
9496
},
9597
getProgramParameter: function (id, key) {
@@ -174,6 +176,8 @@ module.exports.mockWebglRenderer = function mockWebglRenderer(supported) {
174176
webglRenderer.supported = function () {
175177
return !!supported;
176178
};
179+
webglRenderer._maxTextureSize = 4096;
180+
webglRenderer._maxPointSize = 64;
177181

178182
vgl._mocked = true;
179183
vgl.mockCounts = function () {

src/webgl/pointFeature.js

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var $ = require('jquery');
22
var inherit = require('../inherit');
33
var registerFeature = require('../registry').registerFeature;
44
var pointFeature = require('../pointFeature');
5+
var webglRenderer = require('./webglRenderer');
56

67
/**
78
* Create a new instance of webgl.pointFeature.
@@ -41,23 +42,22 @@ var webgl_pointFeature = function (arg) {
4142
m_pixelWidthUniform = null,
4243
m_aspectUniform = null,
4344
m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw,
44-
/* If you are drawing very large points, you will often get better
45-
* performance using a different primitiveShape. The 'sprite' shape uses
46-
* the least memory, but has hardware-specific limitations to its size.
47-
* 'triangle' seems to be fastest on low-powered hardware, but 'square'
48-
* visits fewer fragments. */
49-
m_primitiveShape = 'sprite', // arg can change this, below
45+
m_primitiveShapeAuto = true,
46+
m_primitiveShape = pointFeature.primitiveShapes.auto, // arg can change this, below
5047
m_modelViewUniform,
5148
m_origin,
5249
s_init = this._init,
5350
s_update = this._update,
5451
s_updateStyleFromArray = this.updateStyleFromArray;
5552

56-
if (arg.primitiveShape === 'triangle' ||
57-
arg.primitiveShape === 'square' ||
58-
arg.primitiveShape === 'sprite') {
53+
if (pointFeature.primitiveShapes[arg.primitiveShape] !== undefined) {
5954
m_primitiveShape = arg.primitiveShape;
6055
}
56+
m_primitiveShapeAuto = m_primitiveShape === pointFeature.primitiveShapes.auto;
57+
if (m_primitiveShapeAuto) {
58+
m_primitiveShape = pointFeature.primitiveShapes.sprite;
59+
m_primitiveShapeAuto = true;
60+
}
6161

6262
/**
6363
* Create the vertex shader for points.
@@ -67,7 +67,7 @@ var webgl_pointFeature = function (arg) {
6767
function createVertexShader() {
6868
var shader = new vgl.shader(vgl.GL.VERTEX_SHADER);
6969
shader.setShaderSource(
70-
m_primitiveShape === 'sprite' ? vertexShaderSprite : vertexShaderPoly);
70+
m_primitiveShape === pointFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly);
7171
return shader;
7272
}
7373

@@ -79,7 +79,7 @@ var webgl_pointFeature = function (arg) {
7979
function createFragmentShader() {
8080
var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER);
8181
shader.setShaderSource(
82-
m_primitiveShape === 'sprite' ? fragmentShaderSprite : fragmentShaderPoly);
82+
m_primitiveShape === pointFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly);
8383
return shader;
8484
}
8585

@@ -97,7 +97,7 @@ var webgl_pointFeature = function (arg) {
9797
function pointPolygon(x, y, w, h) {
9898
var verts;
9999
switch (m_primitiveShape) {
100-
case 'triangle':
100+
case pointFeature.primitiveShapes.triangle:
101101
/* Use an equilateral triangle. While this has 30% more area than a
102102
* square, the reduction in vertices should help more than the
103103
* processing the additional fragments. */
@@ -107,11 +107,7 @@ var webgl_pointFeature = function (arg) {
107107
x + w * Math.sqrt(3.0), y + h
108108
];
109109
break;
110-
case 'sprite':
111-
/* Point sprite uses only one vertex per point. */
112-
verts = [x, y];
113-
break;
114-
default: // "square"
110+
case pointFeature.primitiveShapes.square:
115111
/* Use a surrounding square split diagonally into two triangles. */
116112
verts = [
117113
x - w, y + h,
@@ -122,6 +118,10 @@ var webgl_pointFeature = function (arg) {
122118
x + w, y + h
123119
];
124120
break;
121+
default: // sprite
122+
/* Point sprite uses only one vertex per point. */
123+
verts = [x, y];
124+
break;
125125
}
126126
return verts;
127127
}
@@ -148,7 +148,7 @@ var webgl_pointFeature = function (arg) {
148148
fillColor, fillColorVal, fillColorFunc,
149149
vpf = m_this.verticesPerFeature(),
150150
data = m_this.data(),
151-
item, ivpf, ivpf3, iunit, i3,
151+
item, ivpf, ivpf3, iunit, i3, maxr = 0,
152152
geom = m_mapper.geometryData();
153153

154154
posFunc = m_this.position();
@@ -185,9 +185,7 @@ var webgl_pointFeature = function (arg) {
185185

186186
posBuf = util.getGeomBuffer(geom, 'pos', vpf * numPts * 3);
187187

188-
if (m_primitiveShape !== 'sprite') {
189-
unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2);
190-
}
188+
unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2);
191189
indices = geom.primitive(0).indices();
192190
if (!(indices instanceof Uint16Array) || indices.length !== vpf * numPts) {
193191
indices = new Uint16Array(vpf * numPts);
@@ -207,7 +205,7 @@ var webgl_pointFeature = function (arg) {
207205
for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) {
208206
item = data[i];
209207
if (!onlyStyle) {
210-
if (m_primitiveShape !== 'sprite') {
208+
if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) {
211209
for (j = 0; j < unit.length; j += 1, iunit += 1) {
212210
unitBuf[iunit] = unit[j];
213211
}
@@ -222,6 +220,11 @@ var webgl_pointFeature = function (arg) {
222220
fillVal = fillFunc(item, i) ? 1.0 : 0.0;
223221
fillOpacityVal = fillOpacityFunc(item, i);
224222
fillColorVal = fillColorFunc(item, i);
223+
if (m_primitiveShapeAuto &&
224+
((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) &&
225+
radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) {
226+
maxr = radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0);
227+
}
225228
for (j = 0; j < vpf; j += 1, ivpf += 1, ivpf3 += 3) {
226229
if (!onlyStyle) {
227230
posBuf[ivpf3] = position[i3];
@@ -243,6 +246,18 @@ var webgl_pointFeature = function (arg) {
243246
}
244247
}
245248

249+
if (m_primitiveShapeAuto &&
250+
((m_primitiveShape === pointFeature.primitiveShapes.sprite && maxr > webglRenderer._maxPointSize) ||
251+
(m_primitiveShape !== pointFeature.primitiveShapes.sprite && maxr <= webglRenderer._maxPointSize))) {
252+
// Switch primitive
253+
m_primitiveShape = maxr > webglRenderer._maxPointSize ? pointFeature.primitiveShapes.triangle : pointFeature.primitiveShapes.sprite;
254+
m_this.renderer().contextRenderer().removeActor(m_actor);
255+
m_actor = null;
256+
m_this._init(true);
257+
createGLPoints();
258+
return;
259+
}
260+
246261
if (!onlyStyle) {
247262
geom.boundsDirty(true);
248263
m_mapper.modified();
@@ -379,10 +394,47 @@ var webgl_pointFeature = function (arg) {
379394
return m_this;
380395
};
381396

397+
/**
398+
* Get or set the primitiveShape.
399+
*
400+
* @param {geo.pointFeature.primitiveShapes} [primitiveShape] If specified,
401+
* the new primitive shape.
402+
* @param {boolean} [currentShape] If truthy and getting the shape, return
403+
* the shape currently in use if the shape is set to `auto`. If falsy,
404+
* return the specifiec primitiveShape, which may be `auto`.
405+
* @returns {geo.pointFeature.primitiveShapes|this} The primitiveShape or
406+
* this instance of the feature.
407+
*/
408+
this.primitiveShape = function (primitiveShape, currentShape) {
409+
if (primitiveShape === undefined) {
410+
return currentShape || !m_primitiveShapeAuto ? m_primitiveShape : pointFeature.primitiveShapes.auto;
411+
}
412+
if (pointFeature.primitiveShapes[primitiveShape] !== undefined) {
413+
var update = false;
414+
if (primitiveShape === pointFeature.primitiveShapes.auto) {
415+
update = !m_primitiveShapeAuto;
416+
m_primitiveShapeAuto = true;
417+
} else {
418+
update = m_primitiveShapeAuto || m_primitiveShape !== primitiveShape;
419+
m_primitiveShapeAuto = false;
420+
m_primitiveShape = primitiveShape;
421+
}
422+
if (update) {
423+
m_this.renderer().contextRenderer().removeActor(m_actor);
424+
m_actor = null;
425+
m_this._init(true);
426+
m_this.modified();
427+
}
428+
}
429+
return m_this;
430+
};
431+
382432
/**
383433
* Initialize.
434+
*
435+
* @param {boolean} [reinit] If truthy, skip the parent class's init method.
384436
*/
385-
this._init = function () {
437+
this._init = function (reinit) {
386438
var prog = vgl.shaderProgram(),
387439
vertexShader = createVertexShader(),
388440
fragmentShader = createFragmentShader(),
@@ -419,23 +471,27 @@ var webgl_pointFeature = function (arg) {
419471
1, vgl.vertexAttributeKeysIndexed.Eight, {'name': 'fillOpacity'}),
420472
sourceStrokeOpacity = vgl.sourceDataAnyfv(
421473
1, vgl.vertexAttributeKeysIndexed.Nine, {'name': 'strokeOpacity'}),
422-
primitive = new vgl.triangles();
474+
primitive;
423475
m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin);
424476

425-
if (m_primitiveShape === 'sprite') {
477+
if (m_primitiveShape === pointFeature.primitiveShapes.sprite) {
426478
primitive = new vgl.points();
479+
} else {
480+
primitive = new vgl.triangles();
427481
}
428482

429483
m_pixelWidthUniform = new vgl.floatUniform(
430484
'pixelWidth', 2.0 / m_this.renderer().width());
431485
m_aspectUniform = new vgl.floatUniform(
432486
'aspect', m_this.renderer().width() / m_this.renderer().height());
433487

434-
s_init.call(m_this, arg);
488+
if (!reinit) {
489+
s_init.call(m_this, arg);
490+
}
435491
m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw});
436492

437493
prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position);
438-
if (m_primitiveShape !== 'sprite') {
494+
if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) {
439495
prog.addVertexAttribute(unitAttr, vgl.vertexAttributeKeysIndexed.One);
440496
}
441497

src/webgl/webglRenderer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ webglRenderer.supported = function () {
330330
webglRenderer._unmaskedRenderer = ctx.getParameter(ctx.getExtension(
331331
'WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL);
332332
}
333+
// store some parameters for convenience
334+
webglRenderer._maxTextureSize = ctx.getParameter(ctx.MAX_TEXTURE_SIZE);
335+
webglRenderer._maxPointSize = ctx.getParameter(ctx.ALIASED_POINT_SIZE_RANGE)[1];
333336
checkedWebGL = true;
334337
} catch (e) {
335338
console.warn('No webGL support');

tests/cases/pointFeature.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,16 @@ describe('geo.pointFeature', function () {
330330
return vgl.mockCounts().createProgram >= (glCounts.createProgram || 0) + 1;
331331
});
332332
it('other primitive shapes', function () {
333+
expect(point.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto);
334+
expect(point.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite);
333335
point2 = layer.createFeature('point', {
334-
primitiveShape: 'triangle'
336+
primitiveShape: geo.pointFeature.primitiveShapes.triangle
335337
}).data(testPoints);
338+
expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.triangle);
336339
expect(point2.verticesPerFeature()).toBe(3);
337340
layer.deleteFeature(point2);
338341
point2 = layer.createFeature('point', {
339-
primitiveShape: 'square'
342+
primitiveShape: geo.pointFeature.primitiveShapes.square
340343
}).data(testPoints);
341344
expect(point2.verticesPerFeature()).toBe(6);
342345
glCounts = $.extend({}, vgl.mockCounts());
@@ -345,6 +348,24 @@ describe('geo.pointFeature', function () {
345348
waitForIt('next render gl B', function () {
346349
return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1;
347350
});
351+
it('change primitive shapes', function () {
352+
expect(point2.primitiveShape(geo.pointFeature.primitiveShapes.auto)).toBe(point2);
353+
point2.draw();
354+
expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto);
355+
expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite);
356+
point2.style('radius', 20000);
357+
point2.draw();
358+
expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto);
359+
expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.triangle);
360+
point2.style('radius', 20);
361+
point2.draw();
362+
expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.auto);
363+
expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.sprite);
364+
expect(point2.primitiveShape(geo.pointFeature.primitiveShapes.triangle)).toBe(point2);
365+
point2.draw();
366+
expect(point2.primitiveShape()).toBe(geo.pointFeature.primitiveShapes.triangle);
367+
expect(point2.primitiveShape(undefined, true)).toBe(geo.pointFeature.primitiveShapes.triangle);
368+
});
348369
it('updateStyleFromArray single', function () {
349370
point.draw = function () {
350371
count += 1;

0 commit comments

Comments
 (0)