From e11e3b6131908a15c88b51519cbce7a67d861ddc Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 11 Jun 2019 12:02:34 -0400 Subject: [PATCH] Improve webgl coordinate precision. For line, point, polygon, and contour features, this uses a localized origin for the view matrix to reduce jitter at high zoom levels. Using global coordinates runs into the limitations of float precision, which manes that small movements are functionally rounded by that precision. When panning at high zoom levels this manifests as jitter where the features jump between discrete locations instead of panning smoothly. The default is to use the first coordinate of the data of the feature for the origin. For features that span a large area, this will not help the jitter far from that coordinate (but won't be any worse that before this PR). The origin function is exposed on the style object, so if a large area feature is needed, the application can bind geo.event.pan to recompute the origin upon significant changes in the map center at high zoom values. Resolves #831. --- CHANGELOG.md | 3 +++ src/contourFeature.js | 8 +++++++- src/lineFeature.js | 8 +++++++- src/pointFeature.js | 8 +++++++- src/polygonFeature.js | 15 ++++++++++++++- src/quadFeature.js | 2 +- src/webgl/contourFeature.js | 16 ++++++++++++++-- src/webgl/lineFeature.js | 16 ++++++++++++++-- src/webgl/pointFeature.js | 16 +++++++++++++--- src/webgl/polygonFeature.js | 14 +++++++++----- tests/external-data/base-images.tgz.sha512 | 2 +- 11 files changed, 90 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c6504f9b..8dafc64a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ ### Improvements - More response zooming via mouse wheel (#993) - Explicitly exit retired renderers when autosharing renderers (#1007) +- If a point has no stroke or fill, don't return it from pointSearch (#1003) +- WebGL point, line, polygon, and contour features use a localized origin for improved precision at high zoom levels. This reduces panning jitter in zoom levels 19 and up (#1005) +- When doing a point search on a line feature, report which line segment is found (#1008) ### Changes - Idle handlers no longer defer to scene-graph parents. Parents still wait for all children to be idle (#1001) diff --git a/src/contourFeature.js b/src/contourFeature.js index f72989c2f9..9ccefc2432 100644 --- a/src/contourFeature.js +++ b/src/contourFeature.js @@ -28,6 +28,11 @@ var meshFeature = require('./meshFeature'); * that point won't be included in the results. * @property {number|function} [opacity=1] The opacity for the whole feature on * a scale of 0 to 1. + * @property {number[]|function} [origin] Origin in map gcs coordinates used + * for to ensure high precision drawing in this location. When called as a + * function, this is passed the vertex positions as a single continuous array + * in map gcs coordinates. It defaults to the first vertex used in the + * contour. */ /** @@ -214,7 +219,8 @@ var contourFeature = function (arg) { opacity: 1.0, value: function (d, i) { return m_this.position()(d, i).z; - } + }, + origin: (p) => (p.length >= 3 ? [p[0], p[1], 0] : [0, 0, 0]) }, arg.style === undefined ? {} : arg.style ); diff --git a/src/lineFeature.js b/src/lineFeature.js index a3012b0d4d..079251214a 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -50,6 +50,11 @@ var util = require('./util'); * visible gradient. This is a single value that applies to all lines. * @property {string|function} [debug] If 'debug', render lines in debug mode. * This is a single value that applies to all lines. + * @property {number[]|function} [origin] Origin in map gcs coordinates used + * for to ensure high precision drawing in this location. When called as a + * function, this is passed the vertex positions as a single continuous array + * in map gcs coordinates. It defaults to the first line's first vertex's + * position. */ /** @@ -353,7 +358,8 @@ var lineFeature = function (arg) { antialiasing: 2.0, closed: false, line: function (d) { return d; }, - position: function (d) { return d; } + position: function (d) { return d; }, + origin: (p) => (p.length >= 3 ? p.slice(0, 3) : [0, 0, 0]) }, arg.style === undefined ? {} : arg.style ); diff --git a/src/pointFeature.js b/src/pointFeature.js index ffe577eaa8..e602d98e2a 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -37,6 +37,10 @@ var feature = require('./feature'); * @property {geo.geoColor|function} [fillColor] Color to fill each point. * @property {number|function} [fillOpacity=1] Opacity for each point. Opacity * is on a [0-1] scale. + * @property {number[]|function} [origin] Origin in map gcs coordinates used + * for to ensure high precision drawing in this location. When called as a + * function, this is passed the point positions as a single continuous array + * in map gcs coordinates. It defaults to the first point's position. */ /** @@ -312,6 +316,7 @@ var pointFeature = function (arg) { // Find points inside the bounding box idx = m_rangeTree.range(min.x, min.y, max.x, max.y); + idx.sort((a, b) => a - b); // Filter by circular region idx.forEach(function (i) { var d = data[i], @@ -411,7 +416,8 @@ var pointFeature = function (arg) { fillColor: { r: 1.0, g: 0.839, b: 0.439 }, fill: true, fillOpacity: 0.8, - position: function (d) { return d; } + position: function (d) { return d; }, + origin: (p) => (p.length >= 3 ? p.slice(0, 3) : [0, 0, 0]) }, arg.style === undefined ? {} : arg.style ); diff --git a/src/polygonFeature.js b/src/polygonFeature.js index db0cf07eff..33bc9f1ad9 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -30,6 +30,11 @@ var transform = require('./transform'); * each polygon has a uniform style (uniform fill color, fill opacity, stroke * color, and stroke opacity). Can vary by polygon. * @property {boolean|function} [closed=true] Ignored. Always `true`. + * @property {number[]|function} [origin] Origin in map gcs coordinates used + * for to ensure high precision drawing in this location. When called as a + * function, this is passed an array of items, each of which has a vertices + * property that is a single continuous array in map gcs coordinates. It + * defaults to the first polygon's first vertex's position. */ /** @@ -583,7 +588,15 @@ var polygonFeature = function (arg) { strokeColor: {r: 0.0, g: 1.0, b: 1.0}, strokeOpacity: 1.0, polygon: function (d) { return d; }, - position: function (d) { return d; } + position: function (d) { return d; }, + origin: (items) => { + for (let i = 0; i < items.length; i += 1) { + if (items[i].vertices.length >= 3) { + return items[i].vertices.slice(0, 3); + } + } + return [0, 0, 0]; + } }, arg.style === undefined ? {} : arg.style ); diff --git a/src/quadFeature.js b/src/quadFeature.js index 774c306b7b..dc6cd029a3 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -583,7 +583,7 @@ var quadFeature = function (arg) { clrQuads: clrQuads, imgQuads: imgQuads, vidQuads: vidQuads, - origin: origin + origin: new Float32Array(origin) }; return m_quads; }; diff --git a/src/webgl/contourFeature.js b/src/webgl/contourFeature.js index d058ebd659..a5a0d55618 100644 --- a/src/webgl/contourFeature.js +++ b/src/webgl/contourFeature.js @@ -44,6 +44,8 @@ var webgl_contourFeature = function (arg) { m_stepsUniform = null, m_steppedUniform = null, m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + m_origin, + m_modelViewUniform, s_init = this._init, s_update = this._update; @@ -95,6 +97,16 @@ var webgl_contourFeature = function (arg) { m_texture.setColorTable(colorTable); contour.pos = transform.transformCoordinates( m_this.gcs(), m_this.layer().map().gcs(), contour.pos, 3); + m_origin = new Float32Array(m_this.style.get('origin')(contour.pos)); + if (m_origin[0] || m_origin[1] || m_origin[2]) { + for (i = 0; i < contour.pos.length; i += 3) { + contour.pos[i] -= m_origin[0]; + contour.pos[i + 1] -= m_origin[1]; + contour.pos[i + 2] -= m_origin[2]; + } + } + m_modelViewUniform.setOrigin(m_origin); + posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3); opacityBuf = util.getGeomBuffer(geom, 'opacity', numPts); valueBuf = util.getGeomBuffer(geom, 'value', numPts); @@ -128,7 +140,6 @@ var webgl_contourFeature = function (arg) { mat = vgl.material(), tex = vgl.lookupTable(), geom = vgl.geometryData(), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), projectionUniform = new vgl.projectionUniform('projectionMatrix'), samplerUniform = new vgl.uniform(vgl.GL.INT, 'sampler2d'), vertexShader = createVertexShader(), @@ -142,6 +153,7 @@ var webgl_contourFeature = function (arg) { sourceOpacity = vgl.sourceDataAnyfv( 1, vgl.vertexAttributeKeysIndexed.Two, {'name': 'opacity'}), primitive = new vgl.triangles(); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix'); s_init.call(m_this, arg); m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); @@ -150,7 +162,7 @@ var webgl_contourFeature = function (arg) { prog.addVertexAttribute(valueAttr, vgl.vertexAttributeKeysIndexed.One); prog.addVertexAttribute(opacityAttr, vgl.vertexAttributeKeysIndexed.Two); - prog.addUniform(modelViewUniform); + prog.addUniform(m_modelViewUniform); prog.addUniform(projectionUniform); m_minColorUniform = new vgl.uniform(vgl.GL.FLOAT_VEC4, 'minColor'); prog.addUniform(m_minColorUniform); diff --git a/src/webgl/lineFeature.js b/src/webgl/lineFeature.js index 241fe2271b..40d7003732 100644 --- a/src/webgl/lineFeature.js +++ b/src/webgl/lineFeature.js @@ -75,6 +75,8 @@ var webgl_lineFeature = function (arg) { m_flagsUniform, m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, m_geometry, + m_origin, + m_modelViewUniform, s_init = this._init, s_update = this._update; @@ -186,6 +188,16 @@ var webgl_lineFeature = function (arg) { position = transform.transformCoordinates( m_this.gcs(), m_this.layer().map().gcs(), position, 3); + m_origin = new Float32Array(m_this.style.get('origin')(position)); + if (m_origin[0] || m_origin[1] || m_origin[2]) { + for (i = 0; i < position.length; i += 3) { + position[i] -= m_origin[0]; + position[i + 1] -= m_origin[1]; + position[i + 2] -= m_origin[2]; + } + } + m_modelViewUniform.setOrigin(m_origin); + len = numSegments * orderLen; posBuf = util.getGeomBuffer(geom, 'pos', len * 3); prevBuf = util.getGeomBuffer(geom, 'prev', len * 3); @@ -381,7 +393,6 @@ var webgl_lineFeature = function (arg) { strkColorAttr = vgl.vertexAttribute('strokeColor'), strkOpacityAttr = vgl.vertexAttribute('strokeOpacity'), // Shader uniforms - mviUnif = new vgl.modelViewUniform('modelViewMatrix'), prjUnif = new vgl.projectionUniform('projectionMatrix'), geom = vgl.geometryData(), // Sources @@ -402,6 +413,7 @@ var webgl_lineFeature = function (arg) { 1, vgl.vertexAttributeKeysIndexed.Three, {name: 'strokeOpacity'}), // Primitive indices triangles = vgl.triangles(); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix'); m_pixelWidthUnif = new vgl.floatUniform( 'pixelWidth', 1.0 / m_this.renderer().width()); @@ -424,7 +436,7 @@ var webgl_lineFeature = function (arg) { prog.addVertexAttribute(farAttr, vgl.vertexAttributeKeysIndexed.Six); prog.addVertexAttribute(flagsAttr, vgl.vertexAttributeKeysIndexed.Seven); - prog.addUniform(mviUnif); + prog.addUniform(m_modelViewUniform); prog.addUniform(prjUnif); prog.addUniform(m_pixelWidthUnif); prog.addUniform(m_aspectUniform); diff --git a/src/webgl/pointFeature.js b/src/webgl/pointFeature.js index 07de3cb3d4..0933f6e167 100644 --- a/src/webgl/pointFeature.js +++ b/src/webgl/pointFeature.js @@ -47,6 +47,8 @@ var webgl_pointFeature = function (arg) { * 'triangle' seems to be fastest on low-powered hardware, but 'square' * visits fewer fragments. */ m_primitiveShape = 'sprite', // arg can change this, below + m_modelViewUniform, + m_origin, s_init = this._init, s_update = this._update, s_updateStyleFromArray = this.updateStyleFromArray; @@ -167,6 +169,15 @@ var webgl_pointFeature = function (arg) { } position = transform.transformCoordinates( m_this.gcs(), m_this.layer().map().gcs(), position, 3); + m_origin = new Float32Array(m_this.style.get('origin')(position)); + if (m_origin[0] || m_origin[1] || m_origin[2]) { + for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { + position[i3] -= m_origin[0]; + position[i3 + 1] -= m_origin[1]; + position[i3 + 2] -= m_origin[2]; + } + } + m_modelViewUniform.setOrigin(m_origin); posBuf = util.getGeomBuffer(geom, 'pos', vpf * numPts * 3); @@ -363,7 +374,6 @@ var webgl_pointFeature = function (arg) { strokeAttr = vgl.vertexAttribute('stroke'), fillOpacityAttr = vgl.vertexAttribute('fillOpacity'), strokeOpacityAttr = vgl.vertexAttribute('strokeOpacity'), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), projectionUniform = new vgl.projectionUniform('projectionMatrix'), mat = vgl.material(), blend = vgl.blend(), @@ -388,6 +398,7 @@ var webgl_pointFeature = function (arg) { sourceStrokeOpacity = vgl.sourceDataAnyfv( 1, vgl.vertexAttributeKeysIndexed.Nine, {'name': 'strokeOpacity'}), primitive = new vgl.triangles(); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin); if (m_primitiveShape === 'sprite') { primitive = new vgl.points(); @@ -401,7 +412,6 @@ var webgl_pointFeature = function (arg) { s_init.call(m_this, arg); m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); - // TODO: Right now this is ugly but we will fix it. prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); if (m_primitiveShape !== 'sprite') { prog.addVertexAttribute(unitAttr, vgl.vertexAttributeKeysIndexed.One); @@ -418,7 +428,7 @@ var webgl_pointFeature = function (arg) { prog.addUniform(m_pixelWidthUniform); prog.addUniform(m_aspectUniform); - prog.addUniform(modelViewUniform); + prog.addUniform(m_modelViewUniform); prog.addUniform(projectionUniform); prog.addShader(fragmentShader); diff --git a/src/webgl/polygonFeature.js b/src/webgl/polygonFeature.js index 220cf358b8..adfdcdd6a8 100644 --- a/src/webgl/polygonFeature.js +++ b/src/webgl/polygonFeature.js @@ -38,6 +38,8 @@ var webgl_polygonFeature = function (arg) { m_mapper = vgl.mapper(), m_material = vgl.material(), m_geometry, + m_origin, + m_modelViewUniform, s_init = this._init, s_update = this._update, m_builtOnce, @@ -167,6 +169,8 @@ var webgl_polygonFeature = function (arg) { geom.primitive(0).setIndices(indices); } m_geometry = {items: items, numPts: numPts}; + m_origin = new Float32Array(m_this.style.get('origin')(items)); + m_modelViewUniform.setOrigin(m_origin); } else { items = m_geometry.items; numPts = m_geometry.numPts; @@ -214,9 +218,9 @@ var webgl_polygonFeature = function (arg) { } else { j = items[k].triangles[i] * 3; if (!onlyStyle) { - posBuf[d3] = vertices[j]; - posBuf[d3 + 1] = vertices[j + 1]; - posBuf[d3 + 2] = vertices[j + 2]; + posBuf[d3] = vertices[j] - m_origin[0]; + posBuf[d3 + 1] = vertices[j + 1] - m_origin[1]; + posBuf[d3 + 2] = vertices[j + 2] - m_origin[2]; indices[d] = i; } if (!uniform && fillColorVal === undefined) { @@ -255,7 +259,6 @@ var webgl_polygonFeature = function (arg) { posAttr = vgl.vertexAttribute('pos'), fillColorAttr = vgl.vertexAttribute('fillColor'), fillOpacityAttr = vgl.vertexAttribute('fillOpacity'), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), projectionUniform = new vgl.projectionUniform('projectionMatrix'), vertexShader = createVertexShader(), fragmentShader = createFragmentShader(), @@ -267,12 +270,13 @@ var webgl_polygonFeature = function (arg) { sourceFillOpacity = vgl.sourceDataAnyfv( 1, vgl.vertexAttributeKeysIndexed.Three, {'name': 'fillOpacity'}), trianglePrimitive = vgl.triangles(); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix'); prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); prog.addVertexAttribute(fillColorAttr, vgl.vertexAttributeKeysIndexed.Two); prog.addVertexAttribute(fillOpacityAttr, vgl.vertexAttributeKeysIndexed.Three); - prog.addUniform(modelViewUniform); + prog.addUniform(m_modelViewUniform); prog.addUniform(projectionUniform); prog.addShader(fragmentShader); diff --git a/tests/external-data/base-images.tgz.sha512 b/tests/external-data/base-images.tgz.sha512 index 5d43a1957e..8600d9df70 100644 --- a/tests/external-data/base-images.tgz.sha512 +++ b/tests/external-data/base-images.tgz.sha512 @@ -1 +1 @@ -3b3eb2d6d4bf78cd9ff98319a7745ca7a3c115ec8d47a706165e5bdb224e8e49ba122acacb647ae4edb994fe3e3ab7e1456bfa1577c7dd0fe6e1b524f37d186d \ No newline at end of file +28a26c252b7e013ad542fb3be0286f22b153b6bb8a791e3aef118c54944094807f041e1b4073a02c7a14f45848740dd63e5c6f20141e8801db8a0594ffb8efc5 \ No newline at end of file