Deck.gl自定义图层难吗?Shader代码怎么写?
引言:很多做 WebGIS 可视化的同学都会问:Deck.gl自定义图层难吗?Shader代码怎么写? 如果只是调用 ScatterplotLayer、GeoJsonLayer 这类内置图层,deck.gl 上手并不难;真正卡住人的,通常是业务需要“内置图层做不到”的效果,比如按像元值渲染网格、做动态流线、显示自定义符号、在地图上叠加特殊热力效果。这时就会接触到 deck.gl 自定义图层和 Shader 代码。
本文以 GIS 场景为主,讲清楚 deck.gl 自定义图层的基本思路、Shader 代码应该写在哪里、如何把地理坐标传到 GPU,以及初学者最容易踩的坑。你不需要先成为图形学专家,但需要理解 WebGL 渲染管线里几个核心概念。

背景:为什么 GIS 项目会需要 deck.gl 自定义图层
deck.gl 是 Uber 开源的 WebGL 可视化框架,常用于海量点、线、面、轨迹、网格和三维地理数据渲染。对于一般 GIS 项目,内置图层已经能解决很多问题,例如:
- 用
GeoJsonLayer渲染行政区、道路、地块。 - 用
ScatterplotLayer渲染 POI、监测站、车辆位置。 - 用
PathLayer渲染轨迹、管线、河流。 - 用
HeatmapLayer做点密度热力图。 - 用
TileLayer加载切片服务或栅格瓦片。
但当需求变得更具体时,内置图层可能不够用。例如:
- 要根据风速风向绘制动态流场,而不是普通线段。
- 要让每个地块边界出现自定义闪烁扫描效果。
- 要在地图上渲染自定义网格,每个网格颜色由业务公式实时计算。
- 要实现特定行业符号,比如雷达覆盖扇区、通信基站方向瓣、污染扩散范围。
- 要把外部模型、纹理、数据字段和地图坐标结合起来渲染。
这些需求通常就需要 deck.gl 自定义图层。自定义图层的本质,是你自己定义数据如何变成 GPU 可以绘制的几何体,并用 Shader 控制最终显示效果。
原理:deck.gl 自定义图层和 Shader 到底是什么关系
理解 deck.gl 自定义图层,先记住一句话:Layer 管数据和生命周期,Shader 管 GPU 上怎么画。
在 deck.gl 中,一个图层通常包含以下几部分:
- props:图层参数,例如数据源、颜色字段、大小字段、透明度等。
- state:图层内部状态,例如 WebGL 模型、属性管理器、纹理等。
- attributes:传给 GPU 的顶点属性,例如位置、颜色、半径、索引等。
- vertex shader:顶点着色器,主要决定每个顶点在屏幕上的位置。
- fragment shader:片元着色器,主要决定每个像素最终显示什么颜色。
如果用 GIS 语言来理解,deck.gl 自定义图层做了三件事:
- 把地理数据,例如经纬度、线坐标、面坐标,整理成 GPU 能读的数组。
- 通过 deck.gl 提供的投影函数,把经纬度转换到屏幕坐标或裁剪空间。
- 通过 Shader 控制图形的位置、颜色、透明度、纹理和动态效果。
初学者最容易误解的一点是:Shader 不是用来读取 GeoJSON 的。GeoJSON、接口数据、业务字段应该在 JavaScript 层解析,Shader 只处理已经传入 GPU 的数值属性。
步骤:从一个最小 deck.gl 自定义图层开始
下面用一个简化示例说明如何写 deck.gl 自定义图层。这个图层的目标很简单:传入一组经纬度点,在地图上绘制固定大小的彩色点。实际项目中,你可以在这个基础上扩展为网格、符号、流场或行业专题图层。
1. 准备数据结构
假设你的 GIS 点数据已经转换为 JavaScript 数组:
const points = [
{position: [116.391, 39.907], value: 10},
{position: [121.473, 31.230], value: 30},
{position: [113.264, 23.129], value: 60}
];
这里的 position 是经纬度坐标,value 是业务字段。真实项目中,它可以来自 GeoJSON、PostGIS 接口、矢量瓦片解析结果或实时数据流。
2. 定义自定义 Layer 类
deck.gl 自定义图层通常继承 Layer。下面是一个最小结构:
import {Layer} from '@deck.gl/core';
import {Model, Geometry} from '@luma.gl/engine';
const vertexShader = `
#define SHADER_NAME custom-point-layer-vertex-shader
attribute vec2 positions;
attribute float values;
uniform float pointSize;
varying float vValue;
void main(void) {
vec3 positionCommon = project_position(vec3(positions, 0.0));
vec4 positionClipspace = project_common_position_to_clipspace(positionCommon);
gl_Position = positionClipspace;
gl_PointSize = pointSize;
vValue = values;
}
`;
const fragmentShader = `
#define SHADER_NAME custom-point-layer-fragment-shader
precision highp float;
varying float vValue;
void main(void) {
float t = clamp(vValue / 100.0, 0.0, 1.0);
vec3 color = mix(vec3(0.0, 0.4, 1.0), vec3(1.0, 0.1, 0.0), t);
float dist = distance(gl_PointCoord, vec2(0.5));
if (dist > 0.5) {
discard;
}
gl_FragColor = vec4(color, 0.85);
}
`;
export default class CustomPointLayer extends Layer {
initializeState() {
const attributeManager = this.getAttributeManager();
attributeManager.add({
positions: {
size: 2,
accessor: 'getPosition'
},
values: {
size: 1,
accessor: 'getValue'
}
});
this.setState({model: this._getModel()});
}
_getModel() {
const gl = this.context.gl;
return new Model(gl, {
id: this.props.id,
vs: vertexShader,
fs: fragmentShader,
geometry: new Geometry({
drawMode: gl.POINTS,
vertexCount: this.props.data.length
}),
modules: ['project32']
});
}
draw({uniforms}) {
const {model} = this.state;
const {pointSize = 12} = this.props;
model.setUniforms({
...uniforms,
pointSize
});
model.draw();
}
}
CustomPointLayer.layerName = 'CustomPointLayer';
CustomPointLayer.defaultProps = {
getPosition: {type: 'accessor', value: d => d.position},
getValue: {type: 'accessor', value: d => d.value},
pointSize: 12
};
这段代码展示了 deck.gl 自定义图层最核心的结构:initializeState 里创建属性和模型,draw 里把参数传给 Shader 并执行绘制。
3. 在 Deck 中使用自定义图层
写好自定义图层后,使用方式和内置图层类似:
import {Deck} from '@deck.gl/core';
import {MapView} from '@deck.gl/core';
import CustomPointLayer from './CustomPointLayer';
const deck = new Deck({
initialViewState: {
longitude: 116.391,
latitude: 39.907,
zoom: 4,
pitch: 0,
bearing: 0
},
controller: true,
views: new MapView(),
layers: [
new CustomPointLayer({
id: 'custom-point-layer',
data: points,
getPosition: d => d.position,
getValue: d => d.value,
pointSize: 16
})
]
});
如果你已经在项目中使用 Mapbox、MapLibre、React 或 Vue,思路是一样的:把自定义 Layer 放到 layers 数组里,让 deck.gl 负责统一管理视图状态和渲染。
步骤:Shader 代码怎么写才不容易出错
1. vertex shader 负责坐标位置
在 GIS 场景中,vertex shader 最关键的问题是坐标转换。经纬度不能直接当成屏幕像素使用,必须经过 deck.gl 的投影模块处理。
常见写法是:
vec3 positionCommon = project_position(vec3(positions, 0.0));
vec4 positionClipspace = project_common_position_to_clipspace(positionCommon);
gl_Position = positionClipspace;
这里的 project_position 和 project_common_position_to_clipspace 来自 deck.gl 的投影模块。它们帮助你把地理坐标转换到 WebGL 裁剪空间。否则图形可能完全不显示,或者显示在错误位置。
2. fragment shader 负责颜色和像素效果
fragment shader 控制每个像素的颜色。比如根据业务值生成蓝到红的渐变:
float t = clamp(vValue / 100.0, 0.0, 1.0);
vec3 color = mix(vec3(0.0, 0.4, 1.0), vec3(1.0, 0.1, 0.0), t);
gl_FragColor = vec4(color, 0.85);
如果绘制点符号,还可以利用 gl_PointCoord 把默认方形点裁剪成圆形:
float dist = distance(gl_PointCoord, vec2(0.5));
if (dist > 0.5) {
discard;
}
discard 表示丢弃当前像素。这个技巧常用于圆点、光晕、雷达圈、渐变符号等效果。
3. attribute、uniform、varying 分别做什么
| 类型 | 作用 | GIS 示例 |
|---|---|---|
attribute |
每个要素或顶点不同的数据 | 坐标、数值、分类编码、半径 |
uniform |
整次绘制共享的参数 | 透明度、统一缩放系数、时间变量、色带范围 |
varying |
从 vertex shader 传到 fragment shader 的中间变量 | 把业务值传给片元着色器用于配色 |
一个实用判断方法是:如果每个点的值不同,用 attribute;如果整个图层共用一个值,用 uniform;如果 vertex shader 算出来的东西还要给 fragment shader 用,用 varying。
常见坑:deck.gl 自定义图层为什么不显示
1. 坐标顺序写反
WebGIS 中最常见的错误是把 [longitude, latitude] 写成 [latitude, longitude]。deck.gl 默认地理坐标一般使用经纬度顺序,也就是:
[116.391, 39.907]
不是:
[39.907, 116.391]
如果坐标顺序反了,图层可能跑到海外、极区,或者完全看不到。
2. 没有使用 deck.gl 投影模块
如果 vertex shader 直接写:
gl_Position = vec4(positions, 0.0, 1.0);
这通常是错误的。经纬度不是 WebGL 裁剪空间坐标。GIS 自定义图层应使用 deck.gl 提供的投影函数,并确保模型加载了对应模块。
3. attribute size 和数据不匹配
如果坐标是二维经纬度,positions 的 size 通常是 2;如果带高程,可能是 3。size 写错会导致 GPU 读取数据错位,表现为图形位置异常、颜色异常或直接不显示。
4. drawMode 和几何结构不匹配
如果你要画点,用 gl.POINTS;要画线,可能需要 gl.LINES 或把线转换成三角形带;要画面,通常需要三角剖分后用 gl.TRIANGLES。
很多 GIS 面数据不能直接丢给 Shader 画。Polygon 通常需要先转成三角网格,尤其是带洞、多部件、多环的面。
5. 透明度和深度测试导致结果被遮挡
三维场景或多图层叠加时,如果打开深度测试,后绘制的图层不一定覆盖前面的图层。透明对象也可能出现排序问题。遇到图层忽隐忽现,可以检查:
- 是否需要关闭 depth test。
- 图层绘制顺序是否正确。
- 透明度是否过低。
- 是否被底图或三维对象遮挡。
方法比较:内置图层、CompositeLayer 和自定义 Shader 怎么选
| 方法 | 适合场景 | 优点 | 限制 |
|---|---|---|---|
| 内置图层 | 常规点线面、热力、路径、瓦片 | 开发快、稳定、文档多 | 样式和渲染逻辑受限制 |
CompositeLayer |
组合多个现有图层形成业务图层 | 不必直接写 Shader,适合封装业务 | 底层效果仍受内置图层限制 |
继承 Layer 写自定义 Shader |
特殊符号、动态效果、自定义网格、行业可视化 | 自由度最高,性能潜力大 | 需要理解 WebGL、Shader 和 deck.gl 生命周期 |
| 外部 WebGL 引擎混合 | 复杂三维模型、粒子系统、仿真动画 | 能力更强 | 和地图相机、坐标系统集成更复杂 |
实际项目中,不建议一开始就写自定义 Shader。更稳妥的路线是:
- 先确认内置图层能否满足 80% 需求。
- 如果只是组合多个图层,优先写
CompositeLayer。 - 如果必须控制 GPU 渲染细节,再继承
Layer写 Shader。 - 如果涉及大量粒子、三维模型或仿真,再考虑更底层的 WebGL 或 three.js 集成。
检查清单:写 deck.gl 自定义图层前先确认这些问题
- 数据类型:是点、线、面、网格、瓦片,还是实时流数据?
- 坐标系统:输入数据是否为 WGS84 经纬度?是否需要从其他坐标系转换?
- 几何结构:GPU 最终绘制的是点、线,还是三角形?
- 字段映射:哪些字段需要作为 attribute?哪些参数适合作为 uniform?
- 投影处理:vertex shader 是否使用了 deck.gl 的投影函数?
- 更新频率:数据是静态、定时刷新,还是每帧变化?
- 性能瓶颈:瓶颈在数据解析、网络传输、CPU 计算,还是 GPU 绘制?
- 调试方式:是否准备了最小数据集,用 3 到 10 个要素先验证显示结果?
- 降级方案:低端显卡或移动端是否需要关闭部分特效?
FAQ:关于 deck.gl 自定义图层和 Shader 的常见问题
Q1:deck.gl 自定义图层难吗?
如果目标只是封装现有图层,不难,可以用 CompositeLayer。如果要自己写 Shader,难点主要在 WebGL 基础、坐标投影、GPU 数据组织和调试方式。对 GIS 开发者来说,最重要的是先弄清楚数据如何从经纬度变成屏幕上的图形。
Q2:Shader 代码必须从零写吗?
不一定。很多情况下可以参考 deck.gl 官方图层源码,复用已有模块和写法。建议从最简单的点图层开始,只写位置和颜色,然后逐步增加大小、透明度、纹理、动画时间等参数。
Q3:GeoJSON 能不能直接传进 Shader?
不能直接传。Shader 处理的是数值数组、纹理和 uniform 参数。GeoJSON 应该先在 JavaScript 层解析为坐标、索引、属性数组,再通过 deck.gl 的 attribute 机制传给 GPU。
Q4:为什么我的自定义图层在地图缩放后位置不对?
通常是坐标投影处理不正确,或者没有使用 deck.gl 的投影函数。GIS 图层必须随着视图状态变化重新计算屏幕位置,不能把经纬度当作固定像素坐标使用。
Q5:自定义图层能渲染 PostGIS 查询结果吗?
可以。PostGIS 查询结果通常通过接口返回 GeoJSON、MVT 或自定义 JSON。前端拿到数据后,需要转换成自定义图层需要的数组结构,例如坐标数组、属性数组和索引数组。Shader 不关心数据来自 PostGIS、文件还是实时接口,只关心传入 GPU 的数值。
Q6:deck.gl 自定义图层适合做大数据量 GIS 可视化吗?
适合,但前提是数据组织合理。大数据量场景要重点考虑切片、分层加载、二进制数据传输、属性压缩和视野裁剪。不要把所有 GeoJSON 一次性加载到浏览器后再指望 Shader 解决全部性能问题。
结论:先用内置图层验证,再用 Shader 解决特殊渲染问题
Deck.gl自定义图层难吗?Shader代码怎么写? 这个问题的答案是:入门不算难,但不能跳过基本原理。对于 GIS 读者来说,关键不是背 WebGL 语法,而是理解数据、坐标、属性和 Shader 之间的关系。
如果你的需求只是普通点线面渲染,优先使用 deck.gl 内置图层;如果是多个图层组合,优先使用 CompositeLayer;只有当业务效果确实需要控制 GPU 渲染细节时,再写真正的 deck.gl 自定义图层。
实践建议是:先用 3 个点写通最小图层,再加字段配色,再加动态参数,最后再处理真实 GIS 数据。这样排错成本最低,也更容易把 Shader 代码写得稳定、可维护。