cesium学习(七)-Shader

0 阅读14分钟

什么是 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.sourceCustomShaderPostProcessStage 等入口写 Shader 片段,由 Cesium 拼接成完整 Shader。


GLSL 基础

WebGL Shader 使用 GLSL(OpenGL Shading Language)语言,专门为 GPU 设计,与 JavaScript 差异较大。

数据类型

类型含义
float浮点数(必须带小数点:1.0,不能写 1
int整数
bool布尔值
vec2二维浮点向量
vec3三维浮点向量,常用于 RGB
vec4四维浮点向量,常用于 RGBA
mat33×3 矩阵
mat44×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
czm_piOverTwoπ/2
czm_piOverFourπ/4
czm_radiansPerDegreeπ/180,角度转弧度
czm_degreesPerRadian180/π,弧度转角度
czm_infinity极大值

场景变量

变量类型说明
czm_frameNumberfloat当前帧编号,可用于动画
czm_viewerPositionWCvec3相机世界坐标
czm_sunPositionWCvec3太阳世界坐标
czm_lightDirectionECvec3光照方向(眼空间)
czm_lightDirectionWCvec3光照方向(世界空间)
czm_morphTimefloat场景切换进度(2D/3D)

变换矩阵

变量说明
czm_projection投影矩阵
czm_view视图矩阵
czm_inverseView视图逆矩阵
czm_modelView模型视图矩阵
czm_modelViewProjectionMVP 矩阵
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 的扩展入口,用于 Model3D 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;   // 表面法线(眼空间)
};

常用字段:diffusealphaemissiveUNLIT 模式下 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.fragmentShaderTextModel、3D Tiles
CustomShader.vertexShaderTextModel、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.alphaAppearance.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.sourcestmix / fract / sin
动态效果uniform time、扫描圆、流动线、Fresnel 边缘发光
模型 / 3D TilesCustomShaderFragmentInputLightingModel
全屏效果PostProcessStagecolorTexturedepthTexture

一句话总结:

材质决定对象表面效果,Shader 决定这些效果背后的 GPU 计算逻辑。
写 Shader = 用数学函数把坐标/时间/颜色算成你想要的画面。