用于自定义 WebGL 图层视图的细分助手

尝试一下在线预览

重点注意:

  • 此示例显示实验功能,请在产品中使用之前仔细阅读文档。
  • 此示例面向熟悉 WebGL 和硬件加速渲染的专家开发人员。

此示例演示如何使用自定义 WebGL 图层视图呈现图形。它可以用作开发人员完全控制渲染过程的复杂可视化的起点。

此描述假定您熟悉 WebGL 和自定义 WebGL 层视图。它类似于自定义 WebGL 图层视图示例,将点三角化为四边形。此示例改为使用 BaseLayerViewGL2D 实现的新 tessellate*() 方法。这些辅助方法允许开发人员为任何几何类型创建三角形网格,包括折线

原始示例中的 updatePositions() 方法已被修改,可以将任何图形的几何图形转换为三角形网格;这些网格具有特殊的逐顶点属性,着色器程序使用这些属性来渲染任何几何类型。

侦听变化

自定义层是 GraphicsLayer 的子类。每次 graphics 发生变化时,layerview 都会重新处理所有图形并重新创建所有网格。在自定义层视图的构造函数中设置了一个侦听器,该侦听器将触发对 graphic 集合的任何更改;当检测到更改时,使用自定义方法  this.processGraphic() 重新处理所有图形;每次调用  this.processGraphic() 创建一个承诺,该承诺在创建该图形的网格时解决。完成后,所有网格都保存到 meshes 成员变量中,并且通过将  needsUpdate 设置为 true,将整个图层视图标记为脏,这意味着网格集合已更改,但尚未上传到 GPU 进行渲染。

                          
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    const CustomLayerView2D = BaseLayerViewGL2D.createSubclass({
      ...
      constructor: function() {
        ...
        const requestUpdate = function() {
          this.promises = [];
          this.layer.graphics.forEach(this.processGraphic.bind(this));
          promiseUtils.all(this.promises).then((meshes) => {
            this.meshes = meshes;
            this.needsUpdate = true;
            this.requestRender();
          });
        }.bind(this);

        this.watcher = watchUtils.on(
          this,
          "layer.graphics",
          "change",
          requestUpdate,
          requestUpdate,
          requestUpdate
        );
        ...
      }
      ...
    }

处理图形

通过调用 processGraphic() 将每个图形转换为网格。几何类型决定调用哪个tessellate*() 方法来创建网格。在下面的代码片段中,我们关注折线的情况,但其他情况类似。

图形中的属性可用于驱动选择的 tessellate*() 方法的其他参数的值。例如,图形的 width 属性可以作为 tessellatePolyline() 方法的 width 参数传递。请注意,width 参数是一个纯数字,tessellatePolyline()方法不对所使用的单位做出任何假设;从现在开始,我们将假设 width 以像素为单位,但更好的选择是使用点或其他屏幕空间长度单位。

每个图形都成为 this.promises 数组中的一个承诺;每个 promise 都解析为一个对象,该对象包含生成的网格以及原始图形的 attribute 和 symbol 对象,有效地将网格和可能决定其在屏幕上的外观的所有信息绑定在一起。

                             
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    processGraphic: function (g) {
      switch (g.geometry.type) {
        case "extent":
          // Call this.tessellateExtent()...
          break;
        case "point":
          // Call this.tessellatePoint()...
          break;
        case "multipoint":
          // Call this.tessellateMultipoint()...
          break;
        case "polyline":
          this.promises.push(
            this.tessellatePolyline(
              g.geometry,
              (g.attributes && g.attributes.width != null) ? g.attributes.width : 20
            ).then(function (mesh) {
              return {
                mesh: mesh,
                attributes: g.attributes
              };
            })
          );
          break;
        case "polygon":
          // Call this.tessellatePolygon()...
          break;
      }
    }

写入顶点和索引缓冲区

The updatePositions() 方法在每次修改 graphics 集合时或当视图在改变视点后再次变为静止时被调用;有关 updatePositions() 方法和视图更改处理的更多详细信息,请参见动画标记示例

updatePositions() 方法负责将网格数据写入顶点和数组缓冲区。七个顶点属性中的每一个都被编码为浮点值;最后一个 GPU 属性 a_upright 取自图形的 upright 属性(如果存在);对于不应随地图旋转的标记,它是 true

您的应用可能不需要我们在此处使用的某些 GPU 属性,或者它甚至可以引入其他属性并定义图形的 attributessymbol 属性之间的任意映射以及将获取的内容写入缓冲区。

                          
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    updatePositions: function(renderParameters) {
      ...
      for (let meshIndex = 0; meshIndex < this.meshes.length; ++meshIndex) {
        ...
        const upright = (item.attributes && item.attributes.upright) ? 1 : 0;
        ...
        for (let i = 0; i < mesh.vertices.length; ++i) {
          let v = mesh.vertices[i];
          vertexData[currentVertex * 8 + 0] = v.x - this.centerAtLastUpdate[0];
          vertexData[currentVertex * 8 + 1] = v.y - this.centerAtLastUpdate[1];
          vertexData[currentVertex * 8 + 2] = v.xOffset;
          vertexData[currentVertex * 8 + 3] = v.yOffset;
          vertexData[currentVertex * 8 + 4] = v.uTexcoord;
          vertexData[currentVertex * 8 + 5] = v.vTexcoord;
          vertexData[currentVertex * 8 + 6] = v.distance;
          vertexData[currentVertex * 8 + 7] = upright;
          currentVertex++;
        }
      }

      gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
      ...
    }

顶点布局规范

此示例使用的顶点规范非常简单但相当灵活,并且可以使用单个着色器程序渲染所有几何类型。有五种几何类型。

  • 是二维几何类型;它们由顶点组成,按环组织;每个顶点以地图单位表示;每个原始顶点都作为具有相同地理位置的单个顶点馈送到 GPU,存储在 a_position 属性中。
  • 范围视为面类型。
  • 折线是一维几何类型;对于每个线顶点,创建 2 个不同的 GPU 顶点;它们具有相同的 a_position 属性,但具有相反的 a_offset 向量,这些向量启用了拉伸并赋予折线其厚度。
  • 是一种0维几何类型;对于每个点,创建 4 个不同的 GPU 顶点;都共享相同的 a_position 属性,但具有不同的 a_offset 向量,可以在屏幕空间中挤压成四边形。
  • 多点视为点类型。

tessellation-helpers-extrusion

在两个属性 a_position 和 a_offset 上拆分位置信息对于实现用户期望从某些符号系统中获得的防旋转和防缩放行为至关重要。例如,线条表现出反缩放行为,因为它们不会随着用户放大而变粗;此外,标记和标签也表现出反缩放行为,因为它们的大小不会随着用户放大而改变;此外,它们还经常表现出反旋转行为:当用户旋转地图时,它们会保持直立。

tessellation-helpers-anti

有关符号拉伸以及反旋转和反缩放行为的更多信息,请参阅动画标记动画线示例。

阴影

现在让我们讨论顶点着色器。

                    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    precision highp float;

    uniform mat3 u_transform;
    uniform mat3 u_rotation;
    uniform mat3 u_display;

    attribute vec2 a_position;
    attribute vec2 a_offset;
    attribute vec2 a_texcoord;
    attribute float a_distance;
    attribute float a_upright;

    varying vec2 v_texcoord;

    void main(void) {
        vec3 transformedOffset = mix(u_rotation * vec3(a_offset, 0.0), vec3(a_offset, 0.0), a_upright);
        gl_Position.xy = (u_display * (u_transform * vec3(a_position, 1.0) + transformedOffset)).xy;
        gl_Position.zw = vec2(0.0, 1.0);
        v_texcoord = a_texcoord;
    }

顶点着色器的唯一职责是计算顶点位置;由于场景是二维的,组件zw分别设置为01xy 在概念上分 4 个步骤进行。

  • 第一步是偏移向量的旋转以考虑地图旋转,它被编码在 u_rotation 矩阵中;只有当 a_upright0 时我们才这样做。我们不使用 if (...) 语句进行分支,而是使用 a_upright 在旋转版本和固定版本之间进行 mix() 偏移向量。
  • 然后我们使用 u_transform 矩阵转换地理位置 a_position,该矩阵将地图单位转换为像素。
  • 第三步是添加(可能是旋转的)偏移向量,它已经以像素为单位,因此不需要通过 u_transform 矩阵。
  • 最后,通过与 u_display 矩阵相乘,将位置从像素转换为标准化的设备坐标。

片段着色器非常简单;它只是将插值的纹理坐标输出为颜色。当然,插值纹理坐标的一种可能用途是实际采样自定义纹理。

      
1
2
3
4
5
6
    precision highp float;
    varying vec2 v_texcoord;

    void main(void) {
        gl_FragColor = vec4(v_texcoord, 0.0, 1.0);
    }

花点时间尝试片段着色器;尝试修改颜色表达式并查看输出如何变化。

您甚至可以定义额外的属性和制服,并实现高级技术,例如动画、灯光效果和法线映射

gl_FragColor = ...Output color
vec4(1.0, 0.0, 1.0, 1.0) tessellation-helpers-magenta
vec4(v_texcoord, 1.0, 1.0) tessellation-helpers-neon
vec4(mix(vec3(0.2, 0.3, 0.5), vec3(0.4, 0.5, 0.6), length(v_texcoord) / 1.4142), 1.0) tessellation-helpers-linear
vec4(vec3(1.0, 0.0, 0.0) * (1.0 - length(v_texcoord - 0.5)), 1.0) tessellation-helpers-radial

其他可视化示例和资源

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.