deck.gl ScreenGridLayer 完全指南:屏幕空间网格聚合图层的原理、参数与实战
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
ScreenGridLayer 是 deck.gl aggregation-layers 模块提供的屏幕空间聚合图层:它将输入数据点投影到屏幕坐标系后,按固定像素尺寸划分直方图网格(bin),对落入每个网格的数据对象权重执行求和、均值等聚合运算,并以彩色网格叠加在地图上。本文以官方 API 文档 docs/api-reference/aggregation-layers/screen-grid-layer.md 为主体骨架,结合仓库源码与测试,完整讲解其安装、全部配置参数、拾取交互以及 CPU/GPU 聚合的底层实现,读完后你可以在自己的地图应用中直接落地一个高性能的密度/强度热力网格图层。
什么是 ScreenGridLayer
ScreenGridLayer的核心思路是把"地理聚合"转化为"屏幕聚合":
- 数据在屏幕空间(而非经纬度空间)被聚合为直方图分箱(histogram bins);
- 每个分箱即一个固定像素尺寸的网格单元(cell),单元宽度/高度由
cellSizePixels决定; - 所有落入同一网格的数据对象的权重按
aggregation指定的运算(SUM / MEAN / MIN / MAX / COUNT)汇总为单元值; - 单元值再通过
colorDomain+colorRange+colorScaleType映射为颜色,最终以叠加的彩色网格渲染出来。
由于聚合发生在屏幕空间,官方文档明确给出一个重要提示:只要地图发生缩放或平移,层就必须重新聚合数据(详见 docs/api-reference/aggregation-layers/screen-grid-layer.md)。这意味着该图层最适合中小规模数据集——配合合适的数据与配色,其视觉效果可以非常出色,常用于展示停车位分布、交通站点密度、事件热点等场景。
安装与引入
使用 npm 安装聚合图层依赖:
npm install deck.gl # 或按需拆分安装 npm install @deck.gl/core @deck.gl/layers @deck.gl/aggregation-layersTypeScript 下同时引入类型定义:
import {ScreenGridLayer} from '@deck.gl/aggregation-layers'; import type {ScreenGridLayerProps, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; new ScreenGridLayer<DataT>(...props: ScreenGridLayerProps<DataT>[]);使用预打包脚本(pre-bundled scripts)时:
<script src="https://unpkg.com/deck.gl@^9.0.0/dist.min.js"></script> <!-- 或 --> <script src="https://unpkg.com/@deck.gl/core@^9.0.0/dist.min.js"></script> <script src="https://unpkg.com/@deck.gl/layers@^9.0.0/dist.min.js"></script> <script src="https://unpkg.com/@deck.gl/aggregation-layers@^9.0.0/dist.min.js"></script>new deck.ScreenGridLayer({});仓库中该图层的类定义位于 modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.ts,static layerName = 'ScreenGridLayer',可直接在 JSON 配置(如 playground)中以"@@type": "ScreenGridLayer"引用。
快速上手:JavaScript / TypeScript / React
官方文档提供了三套等价的最小可运行示例(数据为旧金山自行车停车位数据)。JavaScript 版本:
import {Deck} from '@deck.gl/core'; import {ScreenGridLayer} from '@deck.gl/aggregation-layers'; const layer = new ScreenGridLayer({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: d => d.COORDINATES, getWeight: d => d.SPACES, opacity: 0.8 }); new Deck({ initialViewState: { longitude: -122.4, latitude: 37.74, zoom: 11 }, controller: true, getTooltip: ({object}) => object && `Count: ${object.value}`, layers: [layer] });TypeScript 版本(带数据对象类型与拾取信息类型):
import {Deck} from '@deck.gl/core'; import {ScreenGridLayer, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; type BikeRack = { ADDRESS: string; SPACES: number; COORDINATES: [longitude: number, latitude: number]; }; const layer = new ScreenGridLayer<BikeRack>({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: (d: BikeRack) => d.COORDINATES, getWeight: (d: BikeRack) => d.SPACES, opacity: 0.8 }); new Deck({ initialViewState: {longitude: -122.4, latitude: 37.74, zoom: 11}, controller: true, getTooltip: ({object}: ScreenGridLayerPickingInfo<BikeRack>) => object && `Count: ${object.value}`, layers: [layer] });React 版本(基于@deck.gl/react的DeckGL组件):
import React from 'react'; import {DeckGL} from '@deck.gl/react'; import {ScreenGridLayer, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; type BikeRack = { ADDRESS: string; SPACES: number; COORDINATES: [longitude: number, latitude: number]; }; function App() { const layer = new ScreenGridLayer<BikeRack>({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: (d: BikeRack) => d.COORDINATES, getWeight: (d: BikeRack) => d.SPACES, opacity: 0.8 }); return <DeckGL initialViewState={{longitude: -122.4, latitude: 37.74, zoom: 11}} controller getTooltip={({object}: ScreenGridLayerPickingInfo<BikeRack>) => object && `Count: ${object.value}`} layers={[layer]} />; }ScreenGridLayer继承自聚合图层基类AggregationLayer(继承链为ScreenGridLayer → AggregationLayer → CompositeLayer → Layer),因此继承所有 Base Layer 属性(id、data、opacity、visible、pickable、updateTriggers等),详见 modules/aggregation-layers/src/common/aggregation-layer.ts。
聚合选项(Aggregation Options)
gpuAggregation(boolean,可选)
- 默认值:
true
当设为true且浏览器支持时,聚合在 GPU 上执行(通过WebGLAggregator)。源码 screen-grid-layer.ts 中的getAggregatorType()展示了真实的决策逻辑:
getAggregatorType(): string { return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) ? 'gpu' : 'cpu'; }即在gpuAggregation: true且当前设备支持 WebGL 聚合时走 GPU 路径;否则自动回退到CPUAggregator。在合适的场景下开启 GPU 聚合能显著加速应用,但它对输入数据性质和所需功能各有取舍,详见下文"CPU 与 GPU 聚合的取舍"以及 overview.md 的 CPU vs GPU Aggregation 章节。
cellSizePixels(number,可选,支持过渡动画)
- 默认值:
100
网格单元(bin)的像素宽/高。源码中该属性的类型定义为{type: 'number', value: 100, min: 1},即最小值被限制为 1 像素。设置过小会导致网格数量爆炸,GPU 聚合时尤其明显(见下文 binIdRange 推导);设置过大则会丢失空间细节。
aggregation(string,可选)
- 默认值:
'SUM'
定义把落入同一网格的所有数据对象权重汇总为单元值的运算。合法取值:
'SUM':单元格内所有点权重的总和;'MEAN':单元格内所有点权重的均值;'MIN':单元格内所有点权重的最小值;'MAX':单元格内所有点权重的最大值;'COUNT':落入单元格的点数量。
getWeight与aggregation共同决定每个网格单元的值(也即后续渲染的高度/强度依据)。源码中该值直接作为operations传给聚合器:aggregator.setProps({operations: [aggregation], ...}),聚合类型AggregationOperation定义于 modules/aggregation-layers/src/common/aggregator/index.ts。
渲染选项(Render Options)
cellMarginPixels(number,可选,支持过渡动画)
- 默认值:
2,会被钳制在[0, 5]区间
网格单元之间的间距(像素)。注意:设置该属性不会影响数据如何分箱——它只影响渲染时每个单元格的实际绘制尺寸。源码 screen-grid-cell-layer.ts 中体现为:
const cellSize = Math.max(gridSize - cellMarginPixels, 0);即最终绘制单元尺寸 =cellSizePixels - cellMarginPixels(下限 0),再换算为裁剪空间尺寸cellSizeClipspace传给着色器。因此cellMarginPixels越大,网格之间缝隙越明显,视觉上越像"砖格"。
colorScaleType(string,可选)
- 默认值:
'linear'
颜色比例尺负责把连续的数值区间(colorDomain)映射为一组离散颜色(colorRange)。值为colorDomain[0]的单元格渲染为colorRange[0]的颜色,值为colorDomain[1]的单元格渲染为colorRange最后一个颜色。支持的取值:
'linear':按值在colorDomain中的位置对colorRange做线性插值;'quantize':把colorDomain等分为colorRange.length段,每段映射到colorRange中的一个离散颜色。
实现层面,源码把colorRange与colorScaleType烘焙进一张 1D 纹理(createColorRangeTexture),并在顶点着色器里用texture(range, vec2(r, 0.5))采样取色;linear对应线性过滤、quantize对应最近邻过滤,参见 modules/aggregation-layers/src/common/utils/color-utils.ts 与 screen-grid-layer-vertex.glsl.ts。
colorDomain(number[2],可选)
- 默认值:
null(自动)
若未提供,图层在运行时把colorDomain设为所有网格单元的实际最小值与最大值(源码中通过aggregator.getResultDomain(0)在draw()时刻求值:colorDomain: () => this.props.colorDomain || aggregator.getResultDomain(0))。
显式提供colorDomain可以控制数值到颜色的映射关系,适合希望用同一套配色渲染不同数据输入以便相互比较的场景。例如固定[0, 1000]后,两个数据集的相同数值会呈现相同颜色。
colorRange(Color[6],可选)
- 默认值:colorbrewer 6 级顺序色板
YlOrRd(黄-橙-红)
由 6 个颜色组成的数组[color1, ..., color6]。每个颜色是 3 或 4 个值的数组[R, G, B]或[R, G, B, A],分别表示红、绿、蓝和透明度通道强度,取值范围0~255;省略 Alpha 时视为255。源码中默认色板定义于 color-utils.ts:
export const defaultColorRange: Color[] = [ [255, 255, 178], [254, 217, 118], [254, 178, 76], [253, 141, 60], [240, 59, 32], [189, 0, 38] ];展平时(colorRangeToFlatArray),缺失的 Alpha 通道统一补255。上面的示例代码使用了绿色系色板(深绿 → 亮绿)来呈现停车位数量,与本图层的密度主题非常契合。
数据访问器(Data Accessors)
访问器的通用约定见 开发者指南:Accessors。
getPosition(Accessor<Position>,可选)
- 默认值:
object => object.position
用于取回每个数据对象位置的函数,返回经纬度坐标。源码中该属性被注册为positions属性:size: 3、type: 'float64',并在启用 64 位精度时使用fp64补偿,见 screen-grid-layer.ts 的initializeState()。
getWeight(Accessor<number>,可选)
- 默认值:
1
每个数据对象的权重:
- 传入数字时,该数字作为所有对象的统一权重;
- 传入函数时,函数在每个对象上被调用以取回其权重。
该属性在 GPU 路径下注册为counts属性(size: 1),其值作为顶点着色器中的in float counts参与聚合。
拾取(Picking)与交互
ScreenGridLayer的 hover/click 事件返回的 PickingInfo.object 代表一个被聚合的网格单元(而非单个数据点)。官方文档与源码中的ScreenGridLayerPickingInfo<DataT>类型(screen-grid-layer.ts)定义了以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
col | number | 被拾取单元的列索引,从视口最左侧的 0 开始 |
row | number | 被拾取单元的行索引,从视口最顶部的 0 开始 |
value | number | 聚合值,由getWeight与aggregation共同决定 |
count | number | 落入该单元的数据点数量 |
pointIndices | number[] | 落入该单元的数据对象索引数组,仅 CPU 聚合时可用 |
points | object[] | 落入该单元的数据对象数组,仅 CPU 聚合且 layer data 为数组时可用 |
源码getPickingInfo()的实现(screen-grid-layer.ts)展示了字段来源:col = bin.id[0]、row = bin.id[1]、value = bin.value[0]、count = bin.count;而pointIndices/points只有在bin.pointIndices存在(即 CPU 聚合路径)时才填充。前端示例中常见的getTooltip: ({object}) => object && \Count: ${object.value}`正是利用value` 字段。
底层原理:CPU 与 GPU 两条聚合路径
从源码结构看,ScreenGridLayer是一个复合图层:它先用聚合器(Aggregator)把原始数据折叠为"网格单元 + 单元权重"两个属性(getBin、getWeight),再交给子图层ScreenGridCellLayer(screen-grid-cell-layer.ts)以 instanced 方式绘制(几何体为 triangle-strip 单位四边形)。
CPU 路径
createAggregator()中 CPU 分支使用CPUAggregator(维度 2),其分箱逻辑清晰展示了"屏幕空间"的含义:
const viewport = this.context.viewport; const p = viewport.project(positions); const cellSizePixels: number = opts.cellSizePixels; if (p[0] < 0 || p[0] >= viewport.width || p[1] < 0 || p[1] >= viewport.height) { // Not on screen return null; } return [Math.floor(p[0] / cellSizePixels), Math.floor(p[1] / cellSizePixels)];即:先把经纬度经viewport.project投影为屏幕像素坐标,越界点被丢弃,然后按floor(像素坐标 / cellSizePixels)取整得到网格行列号。这也从实现层面印证了文档的提示——每次视口变化都必须重新聚合,源码在updateState()中检测到changeFlags.viewportChanged时会调用this.state.aggregator.setNeedsUpdate()强制重跑聚合。
GPU 路径
GPU 分支使用WebGLAggregator,配合project32与binOptionsUniforms两个 shader 模块,顶点着色器内计算 bin:
void getBin(out ivec2 binId) { vec4 pos = project_position_to_clipspace(positions, positions64Low, vec3(0.0)); vec2 screenCoords = vec2(pos.x / pos.w + 1.0, 1.0 - pos.y / pos.w) / 2.0 * project.viewportSize.xy / project.devicePixelRatio; vec2 gridCoords = floor(screenCoords / binOptions.cellSizePixels); binId = ivec2(gridCoords); }GPU 聚合需要预先声明网格总范围binIdRange,源码依据当前视口尺寸推导:
binIdRange: [ [0, Math.ceil(width / cellSizePixels)], [0, Math.ceil(height / cellSizePixels)] ]注意这会覆盖所有可能存在的网格(包括空网格),其内存占用与"最大网格数量"成正比,而非与"有点的网格数量"成正比——这是判断 GPU 聚合是否划算的关键因素。
子图层渲染时,每个单元格被展开为单位四边形,顶点着色器(screen-grid-layer-vertex.glsl.ts)根据instanceWeights、screenGrid.colorDomain与colorRange纹理插值出颜色,并对NaN权重(无数据网格)直接丢弃绘制;片段着色器(screen-grid-layer-fragment.glsl.ts)输出颜色并预留DECKGL_FILTER_COLOR钩子供扩展注入。
CPU 与 GPU 聚合的取舍(重要)
官方文档在 overview.md 中给出了深入分析,要点如下:
- 兼容性:GPU 聚合所需的客户端特性已被主流常青浏览器普遍支持(覆盖全球 95%+ 市场),但个别设备/芯片的驱动差异可能影响结果;
- 数据规模:CPU 聚合耗时与输入数据量大致呈线性关系;GPU 聚合有搭建 shader 与上传缓冲区的前置开销,但处理更多数据的边际成本很小。大数据集(>100K)GPU 明显更快,小数据集 GPU 可能反而更慢;
- 数据分布:CPU 聚合内存与"含点网格数"成正比,GPU 聚合内存与"全部可能网格数"(含空网格)成正比。数据密集集中时 GPU 表现更好,稀疏分散时 GPU 优势减弱;
- 过滤扩展:基于 GPU 的扩展如 DataFilterExtension、MaskExtension 仅与 GPU 聚合协同工作;
- 精度:GPU shader 仅支持 32 位浮点。虽然本图层实现了缓解精度损失的补偿措施,但 GPU 聚合结果与 CPU 未必完全一致,仓库测试保证二者具有可接受的近似一致性;
- 访问分箱内的点:GPU 聚合不暴露"某个单元包含哪些数据点"。若你需要(例如点击网格后列出位置清单),要么使用 CPU 聚合,要么自行对数据做即时过滤。
overview.md 中给出了基于随机数据的实测性能对比(2016 款 15 英寸 MacBook Pro,CPU 2.8 GHz Intel Core i7,GPU AMD Radeon R9 M370X 2 GB):
| 对象数量 | CPU(迭代/秒) | GPU(迭代/秒) | 备注 |
|---|---|---|---|
| 25K | 535 | 359 | GPU 慢约 33% |
| 100K | 119 | 437 | GPU 快约 267% |
| 1M | 12.7 | 158 | GPU 快约 1144% |
JSON 配置用法(Playground 示例)
仓库的 playground JSON 示例 examples/playground/json-examples/screen-grid.json 展示了以声明式 JSON 使用该图层的方式(数据为加州公交站点,cellSizePixels: 20、opacity: 0.8、6 级 YlOrRd 色板):
{ "layers": [ { "@@type": "ScreenGridLayer", "id": "grid", "data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/screen-grid/ca-transit-stops.json", "opacity": 0.8, "cellSizePixels": 20, "colorRange": [ [255, 255, 178, 25], [254, 217, 118, 85], [254, 178, 76, 127], [253, 141, 60, 170], [240, 59, 32, 212], [189, 0, 38, 255] ], "gpuAggregation": true } ] }注意此例的colorRange每个颜色都带显式 Alpha 通道,实现从低密度到高密度"由淡到浓"的叠加效果。完整的交互式示例还可在 examples/website/screen-grid 中找到(React 实现 + 旧金山自行车数据)。
测试验证
仓库使用 Vitest 对该图层做了系统测试,见 test/modules/aggregation-layers/screen-grid-layer.spec.ts:
import {ScreenGridLayer, WebGLAggregator, CPUAggregator} from '@deck.gl/aggregation-layers'; import {testLayer, generateLayerTests} from '@deck.gl/test-utils/vitest'; test('ScreenGridLayer', () => { const testCases = generateLayerTests({ Layer: ScreenGridLayer, sampleProps: {data: FIXTURES.points.slice(0, 3), getPosition}, assert: (cond, msg) => expect(cond, msg).toBeTruthy(), onBeforeUpdate: ({testCase}) => console.log(testCase.title) }); testLayer({Layer: ScreenGridLayer, testCases, onError: err => expect(err).toBeFalsy()}); });该测试通过generateLayerTests自动生成覆盖各属性组合的用例,再经testLayer逐条渲染验证,涵盖 WebGL 与 CPU 两条聚合路径,是排查自定义配置时的重要参考。
小结
ScreenGridLayer是 deck.gl 中"屏幕空间聚合"的代表实现,核心要点可归纳为:
- 空间语义:分箱在屏幕像素空间完成,视口变化必然触发重新聚合,适合中小规模数据;
- 三条控制链:
getPosition+cellSizePixels决定"怎么分箱",getWeight+aggregation决定"每个格子值多少",colorDomain+colorRange+colorScaleType决定"值映射成什么颜色"; - 双聚合引擎:
gpuAggregation默认开启,但会在设备不支持时自动回退 CPU;大数据集、数据集中、需要 GPU 扩展(DataFilter/Mask)时选 GPU,需要访问格内点列表(pointIndices/points)或追求小数据量性能时选 CPU; - 拾取即单元:交互回调拿到的对象是聚合后的网格单元(
col/row/value/count),而非原始数据点。
如需进一步深入,可继续阅读聚合图层家族的其他成员 ContourLayer、GridLayer、HeatmapLayer、HexagonLayer,以及高级用法中的 AggregationLayer、Aggregator 接口(由 CPUAggregator 与 WebGLAggregator 实现)。
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考