什么是 Shader
Shader 是运行在 GPU 上的小程序,用来控制图形最终如何显示。
在 Cesium 里,平时看到的地球影像、3D Tiles、Primitive、Entity 的面线模型、自定义材质、后处理效果,最终都会进入 WebGL 渲染流程,而 WebGL 渲染时就会用到 Shader。
JavaScript = 负责组织数据、控制逻辑
Shader = 负责在 GPU 上计算顶点位置和像素颜色
Shader 在渲染流程中的位置
Geometry / Model / Tile
↓
顶点数据进入 GPU
↓
Vertex Shader ← 处理每个顶点位置
↓
图元装配 / 光栅化(三角形插值生成片元)
↓
Fragment Shader ← 处理每个片元颜色
↓
深度测试 / 混合
↓
写入帧缓冲 → 显示到屏幕
| 类型 | 作用 |
|---|---|
| Vertex Shader | 决定形状在哪里(顶点位置) |
| Fragment Shader | 决定表面长什么样(像素颜色) |
Vertex Shader 与 Fragment Shader
Vertex Shader
处理每一个顶点,主要决定:
- 顶点最终出现在屏幕哪里
- 顶点法线如何变换
- 顶点相关数据如何传给片元阶段(varying)
极简示例:
attribute vec3 position;
void main()
{
gl_Position = vec4(position, 1.0);
}
Fragment Shader
处理每一个片元(接近屏幕像素),主要决定:颜色、透明度、纹理、渐变、发光、扫描、流动效果。
极简示例(输出不透明红色):
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
在 Cesium 实际开发中,通常不直接写完整的 WebGL Shader,而是通过
Material.source、CustomShader、PostProcessStage等入口写 Shader 片段,由 Cesium 拼接成完整 Shader。
GLSL 基础
WebGL Shader 使用 GLSL(OpenGL Shading Language)语言,专门为 GPU 设计,与 JavaScript 差异较大。
数据类型
| 类型 | 含义 |
|---|---|
float | 浮点数(必须带小数点:1.0,不能写 1) |
int | 整数 |
bool | 布尔值 |
vec2 | 二维浮点向量 |
vec3 | 三维浮点向量,常用于 RGB |
vec4 | 四维浮点向量,常用于 RGBA |
mat3 | 3×3 矩阵 |
mat4 | 4×4 矩阵 |
sampler2D | 二维纹理 |
float alpha = 0.5;
vec2 uv = vec2(0.0, 1.0);
vec3 color = vec3(1.0, 0.0, 0.0);
vec4 rgba = vec4(color, alpha); // 用 vec3 构造 vec4
向量与 Swizzling
构造向量
vec2 st = vec2(0.5, 0.5);
vec3 rgb = vec3(1.0, 0.0, 0.0);
vec4 color = vec4(rgb, 1.0); // 从 vec3 + float 构造
vec4 white = vec4(1.0); // 所有分量都是 1.0
分量访问
GLSL 支持两套等价的分量名称:
| 用途 | 分量名 |
|---|---|
| 颜色 | .r .g .b .a |
| 坐标 | .x .y .z .w |
| 纹理 | .s .t .p .q |
vec4 c = vec4(1.0, 0.5, 0.2, 0.8);
c.r // 1.0
c.gb // vec2(0.5, 0.2)
c.a // 0.8
c.xyz // vec3(1.0, 0.5, 0.2)
Swizzling(重排 / 复制分量)
GLSL 可以任意组合分量,顺序和重复都允许:
vec4 c = vec4(1.0, 0.5, 0.2, 0.8);
c.bgr // vec3(0.2, 0.5, 1.0) — 重新排列
c.rrr // vec3(1.0, 1.0, 1.0) — 重复 r 分量
c.aaaa // vec4(0.8, 0.8, 0.8, 0.8)
// 赋值
c.rgb = vec3(0.0, 1.0, 0.0);
c.a = 0.5;
Swizzling 在 Shader 里非常常用,可以减少很多中间变量。
uniform / attribute / varying(in / out)
uniform
从 JavaScript 传入 Shader 的全局变量,同一次绘制中对所有顶点/片元都相同:
uniform vec4 color;
uniform float time;
uniform sampler2D image;
attribute(WebGL1)/ in(WebGL2)
每个顶点自己的数据(顶点位置、法线、纹理坐标等),只在 Vertex Shader 中可用:
// WebGL1
attribute vec3 position;
attribute vec2 st;
// WebGL2 / GLSL ES 3.0
in vec3 position;
in vec2 st;
varying(WebGL1)/ in/out(WebGL2)
把 Vertex Shader 的数据插值传给 Fragment Shader:
// Vertex Shader
varying vec2 v_st; // 输出
// Fragment Shader
varying vec2 v_st; // 接收(WebGL1 两边同名)
// WebGL2 Vertex Shader
out vec2 v_st;
// WebGL2 Fragment Shader
in vec2 v_st;
attribute / in → Vertex Shader 的每顶点数据
varying / in/out → Vertex → Fragment 的插值传递
uniform → 两端都可以读取的全局数据
精度修饰符
GLSL 支持三种精度,影响计算精度和性能:
| 修饰符 | 精度 | 常用场景 |
|---|---|---|
highp | 高精度 | 位置坐标、精确计算 |
mediump | 中精度 | 颜色、纹理坐标 |
lowp | 低精度 | 简单颜色 |
Fragment Shader 中浮点数没有默认精度,需要声明:
precision mediump float; // 通常在片元着色器顶部声明
Cesium 的内部 Shader 和材质系统已经处理好精度声明,在 Material.source 里通常不需要手动写。
常用 GLSL 函数
| 函数 | 作用 | 示例 |
|---|---|---|
mix(a, b, t) | 线性插值:a*(1-t) + b*t | 颜色渐变 |
clamp(x, min, max) | 限制范围 | 防止越界 |
fract(x) | 取小数部分 | 循环动画 |
mod(x, y) | 取模(类似 %) | 周期图案 |
step(edge, x) | x < edge 返回 0,否则 1 | 硬边界 |
smoothstep(a, b, x) | 平滑过渡(三次曲线) | 软边界、发光边缘 |
sin(x) / cos(x) | 三角函数 | 周期动画 |
abs(x) | 绝对值 | 镜像效果 |
floor(x) / ceil(x) | 向下/向上取整 | 网格图案 |
pow(x, y) | 幂运算 | Fresnel 发光 |
sqrt(x) | 平方根 | 距离计算 |
length(v) | 向量长度 | 圆形判断 |
distance(a, b) | 两点距离 | 扫描圆 |
normalize(v) | 归一化为单位向量 | 法线处理 |
dot(a, b) | 点积 | 光照、Fresnel |
cross(a, b) | 叉积 | 法线计算 |
texture(sampler, uv) | 纹理采样(WebGL2) | 贴图 |
texture2D(sampler, uv) | 纹理采样(WebGL1) | 贴图(旧写法) |
常用组合示例
// 透明度呼吸变化
float alpha = abs(sin(time * 2.0));
// 在 st.s = 0.5 附近生成一条软边线
float line = smoothstep(0.48, 0.5, st.s) - smoothstep(0.5, 0.52, st.s);
// 棋盘格图案
float checker = mod(floor(st.s * 10.0) + floor(st.t * 10.0), 2.0);
// 圆形 mask(圆内为 1,圆外为 0)
float circle = 1.0 - step(0.5, distance(st, vec2(0.5)));
Cesium czm_ 内置变量与常量
Cesium 在 Shader 中提供了一批 czm_ 前缀的内置变量、常量和函数。
数学常量
| 常量 | 值 |
|---|---|
czm_pi | π ≈ 3.14159 |
czm_twoPi | 2π |
czm_piOverTwo | π/2 |
czm_piOverFour | π/4 |
czm_radiansPerDegree | π/180,角度转弧度 |
czm_degreesPerRadian | 180/π,弧度转角度 |
czm_infinity | 极大值 |
场景变量
| 变量 | 类型 | 说明 |
|---|---|---|
czm_frameNumber | float | 当前帧编号,可用于动画 |
czm_viewerPositionWC | vec3 | 相机世界坐标 |
czm_sunPositionWC | vec3 | 太阳世界坐标 |
czm_lightDirectionEC | vec3 | 光照方向(眼空间) |
czm_lightDirectionWC | vec3 | 光照方向(世界空间) |
czm_morphTime | float | 场景切换进度(2D/3D) |
变换矩阵
| 变量 | 说明 |
|---|---|
czm_projection | 投影矩阵 |
czm_view | 视图矩阵 |
czm_inverseView | 视图逆矩阵 |
czm_modelView | 模型视图矩阵 |
czm_modelViewProjection | MVP 矩阵 |
czm_inverseViewProjection | 视图投影逆矩阵 |
常用函数
| 函数 | 说明 |
|---|---|
czm_getDefaultMaterial(materialInput) | 返回默认材质结构 |
czm_readDepth(depthTexture, uv) | 读取深度纹理(后处理用) |
czm_windowToEyeCoordinates(fragCoord) | 窗口坐标转眼空间 |
用帧号代替 time uniform
// 不需要从 JS 传 time,直接用帧号模拟时间(假设 60fps)
float time = fract(czm_frameNumber / 120.0);
Cesium 材质 Shader(Fabric source)
在 Cesium 自定义材质里,Shader 入口是 czm_getMaterial:
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
// ... 修改 material 字段
return material;
}
这不是完整的 WebGL Shader,而是 Cesium Material 系统提供的材质函数片段,Cesium 会将它拼接到内部 Fragment Shader 中。
czm_material —— 输出结构
struct czm_material {
vec3 diffuse; // 漫反射颜色(主体颜色)
float alpha; // 透明度(0~1)
vec3 emission; // 自发光颜色(不受光照影响)
float specular; // 高光强度(0~1,默认 0)
float shininess; // 高光集中程度(默认 1)
vec3 normal; // 表面法线(默认使用几何法线)
};
| 字段 | 常用场景 |
|---|---|
diffuse | 主体颜色,最常用 |
alpha | 透明度 |
emission | 发光体、霓虹灯,不受光照影响 |
specular / shininess | 金属感、反光面 |
normal | 法线贴图效果 |
czm_materialInput —— 输入结构
struct czm_materialInput {
float s; // 一维纹理坐标(0~1)
vec2 st; // 二维纹理坐标(UV)
vec3 str; // 三维纹理坐标
vec3 normalEC; // 法线(眼空间),用于光照计算
mat3 tangentToEyeMatrix; // TBN 矩阵,法线贴图时使用
vec3 positionToEyeEC; // 从当前点指向相机的向量(眼空间)
};
常用字段:
| 字段 | 用途 |
|---|---|
st | 纹理坐标,渐变/贴图/流动效果的基础 |
normalEC | 法线,做边缘发光、光照效果 |
positionToEyeEC | 视角相关效果(Fresnel) |
materialInput.st 纹理坐标
st.s = 横向坐标,通常 0 → 1
st.t = 纵向坐标,通常 0 → 1
vec2 st = materialInput.st;
// 横向渐变
vec3 color = mix(color1.rgb, color2.rgb, st.s);
// 纵向透明(底部透明 → 顶部不透明)
material.alpha = st.t;
// 流动纹理(沿 s 轴向右流动)
vec4 c = texture(image, vec2(fract(st.s - time), st.t));
// 以中心为圆心的距离
float dist = distance(st, vec2(0.5)) * 2.0;
// 重复纹理(平铺 10 次)
vec2 repeatSt = fract(st * 10.0);
常用 Shader 效果
渐变
使用 mix() 做颜色插值:
const material = new Cesium.Material({
fabric: {
type: 'GradientMaterial',
uniforms: {
color1: Cesium.Color.BLUE,
color2: Cesium.Color.CYAN
},
source: `
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
vec2 st = materialInput.st;
material.diffuse = mix(color1.rgb, color2.rgb, st.t);
material.alpha = mix(color1.a, color2.a, st.t);
return material;
}
`
}
})
扫描圆
根据距离中心点的远近计算透明度,形成扩散扫描环:
const scanMaterial = new Cesium.Material({
fabric: {
type: 'ScanCircle',
uniforms: {
color: Cesium.Color.CYAN,
speed: 1.5,
time: 0.0
},
source: `
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
vec2 st = materialInput.st;
float dist = distance(st, vec2(0.5)) * 2.0;
// 扫描环:随时间向外推进的圆弧,dist < 1 才在圆内
float ring = fract(dist - time * speed);
float alpha = (1.0 - ring) * step(dist, 1.0);
material.diffuse = color.rgb;
material.alpha = alpha * color.a;
return material;
}
`
}
})
viewer.scene.preRender.addEventListener(() => {
scanMaterial.uniforms.time += 0.01
})
如果直接修改 uniforms.radius 而不用 time,可以做点击扩散效果(一次性从小到大):
// 配合 tween 或逐帧增大 radius
scanMaterial.uniforms.radius += 0.005
流动纹理
让纹理沿 s 轴随时间流动:
const flowMaterial = new Cesium.Material({
fabric: {
type: 'FlowMaterial',
uniforms: {
image: '/textures/flow.png',
speed: 1.0,
time: 0.0
},
source: `
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
vec2 st = materialInput.st;
vec4 c = texture(image, vec2(fract(st.s - time * speed), st.t));
material.diffuse = c.rgb;
material.alpha = c.a;
return material;
}
`
}
})
viewer.scene.preRender.addEventListener(() => {
flowMaterial.uniforms.time += 0.005
})
边缘发光(Fresnel)
Fresnel 效果:视角与法线夹角越大(越接近边缘),发光越强。
const edgeGlowMaterial = new Cesium.Material({
fabric: {
type: 'EdgeGlow',
uniforms: {
color: Cesium.Color.CYAN,
power: 3.0
},
source: `
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
vec3 normal = normalize(materialInput.normalEC);
vec3 toEye = normalize(materialInput.positionToEyeEC);
// dot 越小(视角越偏),fresnel 越大(边缘越亮)
float fresnel = pow(1.0 - abs(dot(normal, toEye)), power);
material.diffuse = color.rgb;
material.emission = color.rgb * fresnel; // 边缘自发光
material.alpha = fresnel * color.a;
return material;
}
`
}
})
emission不受场景光照影响,边缘发光通常用emission而不是diffuse,这样在暗处也能看到发光效果。
CustomShader
CustomShader 是 Cesium 提供的更接近真正 Shader 的扩展入口,用于 Model 和 3D Tiles 的自定义着色,比材质系统更灵活。
基本用法
const customShader = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.UNLIT,
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
material.diffuse = vec3(1.0, 0.0, 0.0);
material.alpha = 0.8;
}
`
})
// 应用到 3D Tiles
tileset.customShader = customShader
// 应用到 Model
model.customShader = customShader
LightingModel
LightingModel 决定模型采用哪种光照模式:
| 值 | 说明 |
|---|---|
Cesium.LightingModel.UNLIT | 无光照,颜色直接输出(diffuse 即最终颜色) |
Cesium.LightingModel.PBR | 物理渲染(默认),diffuse 会受光照、阴影影响 |
大屏可视化通常用 UNLIT,颜色更鲜艳且不受光照干扰;真实场景模型用 PBR。
czm_modelMaterial —— CustomShader 输出
CustomShader 中修改的是 czm_modelMaterial,与材质系统的 czm_material 略有不同:
struct czm_modelMaterial {
vec3 diffuse; // 漫反射颜色
float alpha; // 透明度
vec3 specular; // 镜面反射颜色
float roughness; // 粗糙度(PBR)
float metallic; // 金属度(PBR)
float occlusion; // 环境光遮蔽
vec3 emissive; // 自发光颜色
vec3 normalEC; // 表面法线(眼空间)
};
常用字段:diffuse、alpha、emissive(UNLIT 模式下 diffuse 直接决定颜色)。
FragmentInput —— 片元输入
FragmentInput 包含当前片元的所有可用属性:
struct FragmentInput {
Attributes attributes;
};
// Attributes 常见字段(具体取决于模型数据)
// positionMC — 模型空间坐标
// positionWC — 世界坐标
// positionEC — 眼空间坐标
// normalMC — 模型空间法线
// normalEC — 眼空间法线
// tangentMC — 切线
// texCoord_0 — 第一套 UV(vec2)
// texCoord_1 — 第二套 UV
// color_0 — 顶点颜色(vec4)
使用示例:
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
// 根据模型空间高度着色
float height = fsInput.attributes.positionMC.z;
material.diffuse = mix(
vec3(0.0, 0.5, 1.0),
vec3(1.0, 0.0, 0.0),
clamp(height / 100.0, 0.0, 1.0)
);
// 使用 UV 坐标
vec2 uv = fsInput.attributes.texCoord_0;
material.diffuse = vec3(uv, 0.0);
}
vertexShaderText —— 顶点扩展
CustomShader 也支持扩展顶点阶段,用于修改顶点位置、做形变动画等:
const customShader = new Cesium.CustomShader({
uniforms: {
time: {
type: Cesium.UniformType.FLOAT,
value: 0.0
}
},
vertexShaderText: `
void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput)
{
// 让模型顶点随时间上下波动
vsOutput.positionMC.z += sin(time + vsInput.attributes.positionMC.x) * 10.0;
}
`,
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
material.diffuse = vec3(0.2, 0.6, 1.0);
}
`
})
// 每帧更新 uniform
viewer.scene.preRender.addEventListener(() => {
customShader.setUniform('time', performance.now() / 1000)
})
CustomShader的 uniform 需要用Cesium.UniformType声明类型,这与 Fabric 材质的uniforms写法不同。
根据属性或高度着色
const customShader = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.UNLIT,
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float height = fsInput.attributes.positionMC.z;
if (height > 50.0) {
material.diffuse = vec3(1.0, 0.2, 0.0); // 高层红色
} else if (height > 20.0) {
material.diffuse = vec3(1.0, 0.8, 0.0); // 中层黄色
} else {
material.diffuse = vec3(0.0, 0.5, 1.0); // 低层蓝色
}
}
`
})
tileset.customShader = customShader
模型扫描效果
沿模型高度方向做扫描线:
const customShader = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.UNLIT,
uniforms: {
scanTime: { type: Cesium.UniformType.FLOAT, value: 0.0 },
scanColor: { type: Cesium.UniformType.VEC4, value: new Cesium.Cartesian4(0.0, 1.0, 1.0, 1.0) }
},
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float height = fsInput.attributes.positionMC.z;
float line = abs(sin(height * 0.05 - scanTime * 2.0));
float glow = pow(1.0 - line, 6.0);
material.diffuse = mix(material.diffuse, scanColor.rgb, glow * scanColor.a);
}
`
})
viewer.scene.preRender.addEventListener(() => {
customShader.setUniform('scanTime', performance.now() / 1000)
})
tileset.customShader = customShader
后处理 Shader(PostProcessStage)
后处理不修改某个对象的材质,而是对已经渲染好的屏幕图像再处理一遍。
场景先正常渲染
↓
得到一张屏幕颜色纹理(colorTexture)
↓
PostProcessStage Fragment Shader 处理
↓
输出最终画面
基本用法
后处理 Shader 的结构与材质 Shader 不同,是标准 Fragment Shader:
const stage = new Cesium.PostProcessStage({
fragmentShader: `
uniform sampler2D colorTexture;
in vec2 v_textureCoordinates;
void main()
{
vec4 color = texture(colorTexture, v_textureCoordinates);
// 灰度处理
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
out_FragColor = vec4(vec3(gray), color.a);
}
`
})
viewer.scene.postProcessStages.add(stage)
传入 uniforms
后处理 Shader 也可以传入外部变量:
const brightnessStage = new Cesium.PostProcessStage({
fragmentShader: `
uniform sampler2D colorTexture;
uniform float brightness;
in vec2 v_textureCoordinates;
void main()
{
vec4 color = texture(colorTexture, v_textureCoordinates);
out_FragColor = vec4(color.rgb * brightness, color.a);
}
`,
uniforms: {
brightness: 1.5
}
})
viewer.scene.postProcessStages.add(brightnessStage)
// 动态修改
brightnessStage.uniforms.brightness = 2.0
使用深度纹理
后处理阶段可以读取深度缓冲,实现轮廓描边、景深、SSAO 等效果:
const outlineStage = new Cesium.PostProcessStage({
fragmentShader: `
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
in vec2 v_textureCoordinates;
void main()
{
vec4 color = texture(colorTexture, v_textureCoordinates);
float depth = czm_readDepth(depthTexture, v_textureCoordinates);
vec2 texelSize = vec2(1.0 / czm_viewport.z, 1.0 / czm_viewport.w);
// 采样周围 4 个像素的深度,差异大的地方就是边缘
float d1 = czm_readDepth(depthTexture, v_textureCoordinates + vec2( texelSize.x, 0.0));
float d2 = czm_readDepth(depthTexture, v_textureCoordinates + vec2(-texelSize.x, 0.0));
float d3 = czm_readDepth(depthTexture, v_textureCoordinates + vec2(0.0, texelSize.y));
float d4 = czm_readDepth(depthTexture, v_textureCoordinates + vec2(0.0, -texelSize.y));
float edge = abs(d1 - depth) + abs(d2 - depth) + abs(d3 - depth) + abs(d4 - depth);
edge = step(0.0001, edge);
out_FragColor = mix(color, vec4(0.0, 1.0, 1.0, 1.0), edge);
}
`
})
常见后处理效果
| 效果 | 核心思路 |
|---|---|
| 灰度 | dot(color.rgb, vec3(0.299, 0.587, 0.114)) |
| 亮度/对比度 | color.rgb * brightness / (color.rgb - 0.5) * contrast + 0.5 |
| 泛光 Bloom | 先提取高亮区域,高斯模糊后叠加 |
| 夜视 | 绿色通道放大 + 噪点 |
| 颜色校正 | 调整 RGB 曲线 |
| 轮廓描边 | 深度/法线差异检测边缘 |
| 屏幕扫描线 | sin(v_textureCoordinates.y * freq - time) |
Cesium 内置了常见后处理效果集合:
// 内置泛光
viewer.scene.postProcessStages.bloom.enabled = true
viewer.scene.postProcessStages.bloom.uniforms.contrast = 128
viewer.scene.postProcessStages.bloom.uniforms.brightness = -0.3
// 内置环境光遮蔽 SSAO(需要在 Viewer 中开启)
viewer.scene.postProcessStages.ambientOcclusion.enabled = true
// 内置景深 FXAA
viewer.scene.postProcessStages.fxaa.enabled = true
Shader 与 Material 的关系
Shader = GPU 程序(底层)
Material = Cesium 对常见表面效果的封装
Material.source(Fabric)
↓
Cesium 把它拼进内部 Fragment Shader
↓
GPU 执行完整 Shader
↓
得到最终颜色
所以写自定义材质时,虽然只写了 czm_getMaterial 函数片段,本质上还是在写 Fragment Shader 的一部分。
各入口对比:
| 入口 | 适用对象 | 灵活度 | 学习成本 |
|---|---|---|---|
Material.source(Fabric) | Primitive、Wall、Polyline | 中 | 低 |
CustomShader.fragmentShaderText | Model、3D Tiles | 高 | 中 |
CustomShader.vertexShaderText | Model、3D Tiles | 高 | 高 |
PostProcessStage | 全屏 | 最高 | 中 |
Shader 调试技巧
Shader 的错误比 JavaScript 更难调试,以下方法可以提高效率。
1. 把中间值可视化为颜色
// 把 UV 坐标显示出来(验证 UV 是否正确)
material.diffuse = vec3(st.s, st.t, 0.0);
// 把距离显示出来
material.diffuse = vec3(distance(st, vec2(0.5)) * 2.0);
// 把法线显示出来(-1~1 映射到 0~1)
material.diffuse = normalize(materialInput.normalEC) * 0.5 + 0.5;
// 显示单通道值(看 time 是否在变化)
material.diffuse = vec3(fract(time));
2. 先关闭透明排除干扰
调试时先设 material.alpha = 1.0,确认颜色逻辑正确后再调透明度。
3. 看浏览器控制台的 WebGL 编译错误
Shader 编译失败通常会在控制台报 WebGL: INVALID_OPERATION: linkProgram: program failed to link,展开可以看到具体哪行出错。
4. 常见编译错误
| 错误 | 原因 |
|---|---|
undeclared identifier | 变量名写错,或 uniform 名和 Fabric 声明不一致 |
no matching overloaded function | 类型不匹配(如把 int 传给需要 float 的函数) |
type mismatch in initialization | 赋值类型不对 |
| 整数字面量报错 | GLSL 浮点数必须写 1.0,不能写 1 |
5. 用 step / smoothstep 代替 if
GPU 不擅长分支,用数学函数代替 if 通常性能更好:
// 慢
if (dist < 0.5) {
material.diffuse = vec3(1.0, 0.0, 0.0);
} else {
material.diffuse = vec3(0.0, 0.0, 1.0);
}
// 快
float t = step(0.5, dist); // dist < 0.5 → 0,否则 → 1
material.diffuse = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), t);
常见问题 FAQ
1. Shader 报错但页面没明显提示
打开浏览器控制台查看 WebGL Shader 编译错误,通常会提示具体哪行语法错误、变量不存在或类型不匹配。
2. texture() 报错
WebGL1 的写法是 texture2D(sampler, uv),WebGL2 / Cesium 新版本用 texture(sampler, uv)。Cesium 1.x 较新版本统一使用 texture()。
3. 透明度不生效
检查三处:material.alpha、Appearance.translucent = true、材质注册时 translucent: () => true。
4. 渐变方向不对
检查使用的是 st.s(横向)还是 st.t(纵向)。方向由几何体的 UV 展开决定。
5. 动画太快或太慢
用 time uniform 时,调整每帧增加的步长;用 czm_frameNumber 时,调整除数(/ 60.0 大致对应 1 秒)。
6. Shader 里不能直接用 JavaScript 变量
JavaScript 变量必须通过 uniforms 传入 Shader,Shader 里只能用 uniform 声明的变量。
7. Entity 的自定义材质不刷新
检查 MaterialProperty.isConstant 是否为 false,以及 getValue() 返回的 uniform 是否在变化。
8. CustomShader 在不同数据上效果不一样
3D Tiles 和 Model 的 FragmentInput.attributes 取决于模型数据,不是所有属性都有。调试时先输出 fsInput.attributes.positionMC 验证坐标范围。
9. CustomShader 的 uniform 更新方式与 Material 不同
// Material uniform 直接赋值
material.uniforms.time = 1.0
// CustomShader uniform 用 setUniform
customShader.setUniform('time', 1.0)
小结
Vertex Shader = 算位置(顶点在哪里)
Fragment Shader = 算颜色(像素长什么样)
Material.source = Cesium 封装的表面 Shader 片段
CustomShader = 用来改 Model / 3D Tiles 的 Shader
PostProcessStage = 用来改最终屏幕画面的 Shader
实际开发学习路径:
| 阶段 | 学什么 |
|---|---|
| 入门 | Color、内置线材质、ImageMaterialProperty |
| 进阶 | Fabric Material.source、st、mix / fract / sin |
| 动态效果 | uniform time、扫描圆、流动线、Fresnel 边缘发光 |
| 模型 / 3D Tiles | CustomShader、FragmentInput、LightingModel |
| 全屏效果 | PostProcessStage、colorTexture、depthTexture |
一句话总结:
材质决定对象表面效果,Shader 决定这些效果背后的 GPU 计算逻辑。
写 Shader = 用数学函数把坐标/时间/颜色算成你想要的画面。