返回知识库

GIS

矢量与实体

矢量与实体 封面
GISengine-webgpu矢量Entity

前情回顾06 相机与拾取 讲清了 RTE 高精度相机、Picking 颜色编码和球面插值飞行。当用户点击地图上的点、线、面时,这些矢量数据是怎么被 GPU 高效渲染的?本篇回答:GIS 矢量数据在 GPU 上的表达与渲染。

直觉问题

打开一个支持标绘的 Web 地图,新建一个场景:

  • 在地图上点一个红色标记——这个标记本质上是什么几何体?是一个二维图片贴到三维球面上?还是一个三维模型?
  • 沿着航线画一条绿色折线——折线是由无数小线段拼接而成的。如果航线从纽约到东京(跨越半个地球),折线需要多少个点?如果把每个点都传给 GPU 单独渲染,性能会怎样?
  • 标记了一个危险区域(半透明红多边形)——这个多边形在球面上不是平的。它的几何定义和平面几何有什么不同?
  • 场景中有 10 万个标记点——如果每个标记都是一个独立网格传给 GPU,GPU 开销会很大。有没有一种方法让 GPU 批量处理这些点?

读完本篇,你能回答:GPU 实例化为什么能大幅改善大量同类型实体的渲染性能?SDF 字体渲染和传统字体纹理有什么区别?Billboard 为什么叫永远面向相机?

核心概念白话讲

GIS 矢量数据模型:点、线、面

GIS 中所有的矢量数据都可以归结为三种几何基元:

类型数学定义用途
(Point)一个 (x, y, z) 或 (lon, lat) 坐标标记位置、POI、图标
线 (LineString)有序点序列 [P_0, P_1, …, P_n]航线、轨迹、边界线
(Polygon)闭合点序列 + 可选内点 (holes)区域、多边形面、行政区划

NOTE

关键认知:矢量用坐标点序列定义形状,栅格用像素网格定义图像。矢量放大不失真,栅格放大会模糊。

ECS 架构:把实体拆成零件

传统面向对象(OOP):每个地图上的标记是一个完整对象——位置、颜色、大小、旋转全部封装在一起。当需要更新 10 万个标记的颜色时,CPU 要逐个对象访问,数据在内存中分散,cache 命中率低。

ECS(Entity-Component-System)把对象拆开:

  • Entity:一个 ID(空壳,不携带数据)
  • Component:纯数据(位置、颜色、大小),同类型连续存储
  • System:纯逻辑,批量处理同类型 Component
ECS vs OOP 数据布局对比

为什么 ECS 适合大规模场景?

维度OOPECS
数据布局分散(每个对象独立 malloc)连续(SoA,Structure of Arrays)
CPU Cache命中率高高(数组连续访问)
批量更新逐个遍历System 一次读取整个数组
GPU 友好需手动整理可直接生成 instance buffer
代码耦合高(继承链深)低(组合优于继承)

NOTE

SoA 为什么快? CPU 从内存读取数据时,会把目标地址附近 64 字节全部拉入 L1 Cache(Cache Line)。如果 Position 数组是连续的,CPU 一次 cache miss 后,后续 16 个 Position 都在 cache 里。而 OOP 的对象成员分散在不同内存页,每个对象都要重新寻址。

GPU 实例化:一个模板画 N 次

传统方式:10,000 次 draw call,每次传一次几何数据——CPU 大部分时间花在了跟 GPU 通信上。

实例化方式:1 次 draw call,传一个位置数组告诉 GPU 在这个位置画 10,000 次。

传统渲染 vs GPU 实例化管线对比

更加重要的是:每个实例可以有不同的属性

实例化不是”完全相同的 10,000 个克隆”。通过 Instance Attributes,每个实例可以有自己独立的数据:

模板几何:一个 Billboard 四边形(4 个顶点,2 个三角形)

Per-Instance Attributes(每个实例各不同):
  - instance_position : vec3f  -> 世界坐标
  - instance_color    : vec4f  -> 颜色(RGBA)
  - instance_scale    : vec2f  -> 宽高缩放
  - instance_rotation : f32    -> 绕法线旋转

GPU 执行时(伪代码):
  for each instance in instances:
      for each vertex in template:
          transform = instance.TRS * vertex.local
          输出 vertex_color = instance.color

TIP

点题:实例化不是克隆 10,000 个”完全相同”的物体,而是让 GPU 用同一套几何模板,在每个实例自己的位置上画一次。每个实例的 instance_colorinstance_scale 等属性不同,所以屏幕上的标记看起来颜色、大小各不相同——但底层只发起 1 次 draw call。

SDF/MSDF:字体的数学等高线图

传统字体渲染:把每个字符画成小图片,放大就模糊。

SDF (Signed Distance Field):存每个像素到字形边界的距离。距离 = 0 在边界,> 0 在内部,< 0 在外部。渲染时看距离值就知道像素颜色,放大也不模糊。

MSDF (Multi-channel SDF):用 R、G、B 三个通道分别存距离,解决 SDF 在锐角处的模糊问题。

Billboard:永远面向相机的纸片

Billboard 是一个始终面向相机的平面。标记点和文字标签用 Billboard,保证用户看到的是正面。球面 Billboard 所有轴自由旋转;圆柱面 Billboard 只能绕 Y 轴旋转(保持直立)。

原理与数学/机制

1. GPU 实例化数学

1.1 实例化矩阵:TRS

每个实例有变换矩阵把模板几何变换到世界位置:

Mworld=TRSM_{world} = T \cdot R \cdot S

Translation 矩阵 T:

T=(100tx010ty001tz0001)T = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}

Rotation 矩阵 R_y(alpha)(绕 Y 轴):

Ry(α)=(cosα0sinα00100sinα0cosα00001)R_y(\alpha) = \begin{pmatrix} \cos\alpha & 0 & \sin\alpha & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\alpha & 0 & \cos\alpha & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

Scale 矩阵 S:

S=(sx0000sy0000sz00001)S = \begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

1.2 实例化 vs 非实例化:性能对比

场景实体数量非实例化 Draw Calls实例化 Draw Calls性能提升
机场标记点10,00010,000110000x
城市名称5,0005,00015000x
航线(200条x50点)2002001200x
行政区划面3003001300x

IMPORTANT

核心洞见:实例化的瓶颈不在 GPU 计算能力,而在 CPU-GPU 通信开销。每次 draw call 都有固定 CPU 开销。实例化把 N 次 draw call 压缩到 1 次,消除了这个瓶颈。

2. SDF 字体渲染的数学原理

2.1 有向距离场的定义

对于字形图像,定义距离场 phi(p):

ϕ(p)={+d(p,Ω)pΩd(p,Ω)pΩ\phi(p) = \begin{cases} +d(p, \partial\Omega) & p \in \Omega \\ -d(p, \partial\Omega) & p \notin \Omega \end{cases}

其中 p 是像素位置,Omega 是字形区域,partial Omega 是字形边界,d 是欧氏距离。

2.2 SDF 抗锯齿渲染

渲染时根据距离场判断颜色:

color(p)={fgϕ(p)>0.5bgϕ(p)<0.5mix(fg,bg,(ϕ(p)+0.5))otherwise\text{color}(p) = \begin{cases} \text{fg} & \phi(p) > 0.5 \\ \text{bg} & \phi(p) < -0.5 \\ \text{mix}(\text{fg}, \text{bg}, (\phi(p) + 0.5)) & \text{otherwise} \end{cases}

边缘自动线性插值,抗锯齿,放大不模糊。

2.3 MSDF:解决锐角问题的多通道方案

方案通道优点缺点
传统纹理1 (灰度)简单、兼容性好放大模糊
SDF1 (距离)连续、缩小好锐角处失真
MSDF3 (R/G/B)锐角清晰、连续生成复杂、需预处理

3. Billboard 几何变换

3.1 Billboard 变换矩阵

设 C 为相机世界位置,B 为 Billboard 世界位置,U 为世界 up 向量 (0, 1, 0)。

Billboard 的 local-to-world 变换:

z=normalize(CB)z' = \text{normalize}(C - B) x=normalize(U×z)x' = \text{normalize}(U \times z') y=z×xy' = z' \times x'

Billboard 的旋转矩阵就是新基底 [x’, y’, z’]:

Rbillboard=(xxyxzx0xyyyzy0xzyzzz00001)R_{billboard} = \begin{pmatrix} x'_x & y'_x & z'_x & 0 \\ x'_y & y'_y & z'_y & 0 \\ x'_z & y'_z & z'_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

NOTE

直观理解:想象你拿着一张纸,无论如何转身,这张纸都正对着你的眼睛。

3.2 球面 Billboard vs 圆柱面 Billboard

类型旋转约束适用场景
球面 Billboard所有轴自由旋转点状图标、标签
圆柱面 Billboard只能绕世界 Y 轴旋转人物、树木(保持直立)

4. 矢量线在 GPU 上的渲染

矢量线不是简单地把相邻点连起来——那样线宽只有 1 像素,在远距离或斜视角下会消失。GPU 上的矢量线渲染需要解决两个核心问题:

4.1 线宽展开:从线段到多边形

将一条线段 (P_0, P_1) 展宽成四边形或更复杂的多边形mesh:

P0 ------------ P1          P0' -------- P1'
  | 线宽=w                  | 线宽=w 展开成四边形
  n 法线方向                P0'' -------- P1''

对于连接处(Join)还需要处理:

Join 类型描述视觉效果
Miter延长边到交点锐角处可能无限长
Bevel直接截断稳定、不突兀
Round圆弧过渡最自然、最贵

4.2 Line SDF:GPU 上的抗锯齿线

更现代的做法是在 GPU 中用 SDF 画线:

  • 将线段的每一点传入 GPU 作为实例化数据
  • Fragment Shader 中计算当前像素到线段的距离
  • 根据距离 smoothstep 得到半透明边缘

这种方案的优点:线宽是数学精确的,抗锯齿免费,拐角可以自动圆角化。

5. 球面多边形的渲染挑战

还记得直觉问题中的疑问吗——“多边形在球面上不是平的,它的几何定义和平面几何有什么不同?” 这里直接回答:

5.1 平面多边形 vs 球面多边形

特性平面多边形球面多边形
所在空间欧几里得平面球面表面
直线段大圆弧(测地线)
内角和= (n - 2) * 180 deg> (n - 2) * 180 deg
GPU 渲染直接 triangulate需细分成小三角形贴合球面

NOTE

为什么球面多边形的内角和更大? 想象在地球表面画一个三角形:从北极出发,沿经线到赤道,再沿赤道到另一条经线,最后回到北极。三条边都是大圆弧,但三个角都是 90 deg,内角和 = 270 deg > 180 deg。这就是球面几何与平面几何的本质区别。

5.2 GPU 上的处理策略

球面多边形不能直接作为一个平面多边形渲染,否则会出现以下问题:

  • 边不贴球面:平面多边形的边是直线,而球面上最短路径是大圆弧
  • 深度冲突:平面多边形的一部分可能切入球体内部,导致 z-fighting

解决方案——曲面细分(Tessellation)

  1. 将多边形的每条边沿大圆弧细分为若干短边
  2. 将细分后的多边形 triangulate 成小三角形
  3. 每个小三角形的顶点投影到球面上
  4. GPU 渲染这些小三角形,近似球面多边形
球面多边形渲染流程

可视化对比与动手实验

实验 1:实例化 vs 非实例化性能

方案CPU 时间GPU 时间Draw Calls帧率 (60Hz)
非实例化~16ms~2ms10,000掉帧 (约 15fps)
实例化~1ms~2ms1流畅 (60fps)

实验 2:SDF 字体放大对比

渲染方案放大 2 倍放大 5 倍放大 10 倍
传统纹理图轻微模糊明显锯齿像素化严重
SDF清晰清晰轻微模糊
MSDF清晰清晰清晰

实验 3:Billboard 视角对比

场景无 Billboard有 Billboard
俯瞰正常正常
侧视图标倾斜正对视线
仰视图标倒转正对视线

实验 4:矢量数据格式对比

格式几何表达属性支持坐标系优劣势
GeoJSONJSON完整 (properties)WGS84 默认可读、Web 友好、无拓扑
Shapefile二进制 + dBASE完整需指定 EPSG工业标准、多文件
WKT/WKB文本/二进制无(纯几何)需结合 SRID轻依附标准、无属性
KMLXML完整WGS84Google Earth 原生、XML 臃肿
TopoJSON拓扑编码完整WGS84共享边界、文件小、需解码

常见误区

WARNING

误区 1:实例化只能用于完全相同的实体 实例化支持每个实例有不同的变换(位置、旋转、缩放),甚至可以通过 instance attributes 传入每实例的颜色、大小等数据。关键是几何形状相同。

WARNING

误区 2:SDF 字体没有锯齿 SDF 的不模糊是指距离场本身是数学连续的。但渲染时采样分辨率有限,字体太小(< 8px)时依然会有锯齿。

WARNING

误区 3:Billboard 是最省性能的方案 Billboard 比 3D 模型简单,但每个 Billboard 依然需要一次 draw call(除非实例化)。10,000 个 Billboard 不实例化照样卡。

WARNING

误区 4:矢量线就是一系列点连起来 矢量线在 GPU 上不是简单连接相邻点,而是展宽成多边形带状区域或用 SDF 计算。否则线只有 1 像素宽,在不同视角下会消失。

延伸阅读与自测

延伸阅读

  1. OGC Simple Feature Access Standardhttps://www.ogc.org/standards/sfa — GIS 矢量数据的权威标准
  2. GPU Instancinghttps://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/drawArraysInstanced — WebGL2 实例化 API
  3. Signed Distance Fieldshttps://css-tricks.com/the-different-ways-to-generate-a-sdf/ — SDF 生成方法
  4. MSDF Font Renderinghttps://github.com/Chlumsky/msdfgen — Viktor Chlumsky 的 MSDF 工具
  5. Line Rendering in WebGLhttps://mattdesl.svbtle.com/drawing-lines-is-hard — WebGL 矢量线渲染的技术挑战

自测题

  1. 计算题:一个有 10,000 个三角形组成的 3D 模型,实例化 1000 次,GPU 实际处理的三角形数是多少?draw call 次数是多少?

  2. 概念辨析:为什么说实例化渲染的瓶颈是 CPU-GPU 通信而不是 GPU 计算能力?

  3. 数学推导:给定点 PworldP_{world} 和世界 up 向量 U = (0, 1, 0),推导球面 Billboard 的 [x’, y’, z’] 基底。如果相机在 (0, 0, R),Billboard 在 (R, 0, 0),直接写出变换矩阵。

  4. 方案对比:在需要显示 10 万个动态文字标签的 3D 地图中(大小 12px~48px,UTF-8 中英文混合),你会选择传统纹理图集、SDF 还是 MSDF?为什么?

  5. 设计题:设计一个矢量标绘系统,支持点、线、面三种基元,支持 100,000 个点的实时渲染,点在球面上 Billboard 渲染,线宽在屏幕空间固定。写出 GPU 侧的伪代码和数据结构设计。


下一篇预告08 渲染管线 将深入 GPU 渲染管线的核心——从 8 步帧循环到 Tone Mapping 色彩管理,再到 Rayleigh/Mie 大气散射的物理原理,带你理解”一张漂亮的 3D 地图是如何从一堆shader和纹理变成屏幕上的像素的”。