☰
Cesium czm 内置变量与函数实战:着色器材质开发指南
2026/10/28 11:15:35 网站建设 项目流程

1. 先把 czm 这层"外壳"看透

写过 Cesium 自定义材质的人大概都有过这么一段经历:打开官方示例,把czm_getMaterial那段代码复制下来,改两个数字,画面动了,效果出来了,但心里其实是虚的。czm_frameNumber是什么?materialInput.st的 st 到底指什么?czm_viewerPositionWC能不能在顶点着色器里用?下一次想改个更复杂的算法,就完全不知道该从哪儿下手了。

这篇内容想解决的就是这个问题。我把czm 内置变量和czm 内置函数按用途重新梳理了一遍,不按字母顺序罗列,而是按"你什么时候会用到它"来组织,中间穿插实战代码和踩过的坑。适合已经能跑起一个 Cesium 场景、想往着色器方向深入的开发者,也适合被某个编译报错卡了半天、想搞清楚原因的人。全文基于 1.9x 到 1.10x 这个区间的版本实践,老版本(1.6x 到 1.8x)的差异我会单独标出来。

先说清楚 czm 到底是个什么东西。可以把 Cesium 的着色器体系理解成两个层次:下面一层是用 GLSL 写的渲染内核,上面一层是给你留的"插槽"。czm_开头的这些标识符,就是两层之间的接口契约——Cesium 在编译你的着色器代码时,会把一大堆公共的 GLSL 文件(相机矩阵、光照结构、噪声函数、椭球体常量等)拼接到你写的代码前面,拼接完成后czm_前缀的变量和函数才能被解析。所以你在源码里搜czm_viewMatrix,在你自己写的字符串里是搜不到的,它来自引擎内部的AutomaticUniforms和BuiltinFunctions这一类注册表。

1.1 拼接机制决定了你能用什么

理解拼接机制的实际价值在于:你的着色器不是运行在一个干净的环境里,而是运行在一堆前置代码之后。这带来几个直接后果,很多人第一次遇到会觉得莫名其妙。

第一,你不能自己写#version指令。引擎已经写死了,你再写一个会直接报重复声明或版本冲突。第二,你不能定义main函数(在材质体系里),因为真正被调用的是czm_getMaterial,main由引擎提供。第三,你声明的变量名如果和内置的重名,会撞车,报错信息往往指向引擎那一侧的行号,看起来跟你毫无关系。

提示:当编译报错的行号出现在引擎的 builtin 文件里,第一反应应该是"我是不是定义了某个和内置同名的变量",而不是去怀疑引擎有 bug。

还有一点很关键:前缀代码是分块的。顶点着色器和片元着色器加载的公共块不完全一样,有些czm_变量只在片元阶段可用,有些只在顶点阶段可用,有些两边都有但语义不同。比如相机位置这类信息,在不同阶段拿到的精度和形式可能是有区别的。如果你把一个片元专用的变量写进顶点着色器,得到的不是"值为 0",而是直接编译失败。

1.2 三条路径,选错了会白干

在 Cesium 里往着色器方向走,实际上有三条路,选哪条决定了你后面要用到的czm_能力范围。

路径入口能改什么适用场景主要限制
Fabric 材质new Cesium.Material({fabric})只改片元,且必须返回czm_material表面纹理、扫描、流光、渐变动不了几何,顶点阶段几乎无自由
自定义 PrimitiveCesium.Primitive+ 自定义Appearance顶点和片元都能改粒子、拖尾、特殊几何体需要自己管几何数据、包围球、着色器属性
CustomShadernew Cesium.CustomShader()给已有模型注入顶点/片元逻辑3D Tiles、glTF 模型的动态效果受模型自身属性约束,需按规范命名

三条路径的选择逻辑其实很朴素:效果只跟表面颜色有关,就走材质;需要改顶点位置或者新造几何,就走自定义 Primitive;效果要贴到已有 3D Tiles 或 glTF 模型上,就走 CustomShader。我见过不少人明明只需要一个扫描线,非要去写 Primitive,结果在包围球计算和批次合并上耗掉两天,完全没有必要。

Fabric 材质的一个隐性约束值得单独说:它的source里你只能定义一个czm_material czm_getMaterial(czm_materialInput materialInput)函数。这个签名是硬性的,返回的czm_material结构体包含diffuse、specular、shininess、normal、emission、alpha这几个字段。也就是说材质路径的输出带宽就这么宽,你想在片元里做深度写入、做多目标渲染,这条路根本走不通。

1.3 版本漂移才是真正的长期成本

这一点在很多教程里几乎不提,但它是我实际项目里最痛的部分。Cesium 的czm_命名空间在历史上调整过几次,主要是光照模型重构那一轮,把一批跟太阳、环境光相关的变量换了名字,从"跟具体光源绑定"改成"通用的光照方向 + 颜色"。你从网上抄一段 2019 年的代码,很可能出现的情况是:不报错,但效果全黑或者亮度完全不对。

我一般的处理原则是:新项目只使用当前版本源码里确实存在的变量,把每个用到的czm_名字在本机源码里搜一遍确认,而不是凭记忆写。源码路径相对固定,在引擎包的 Shaders 目录下,按名字搜一下两秒钟的事,比调试半天强。

注意:升级 Cesium 版本时,如果项目里写了自定义着色器,一定要把着色器相关的部分单独回归一遍。渲染管线的破坏性变更通常不会在普通 API 层体现,只在着色器层面暴露。

2. 内置变量:哪些能直接用,哪些要看上下文

这一节按用途把常用的czm_变量分组。我不追求穷举,而是把"实际会用到、用错了会出问题"的那批挑出来讲。每个变量我会说清楚三件事:它是什么类型、它的数值语义、以及它在什么阶段可用。

2.1 时间与帧驱动变量

动画类效果绕不开时间。Cesium 在着色器层面提供的"时间"其实很有限,主要就是czm_frameNumber,类型是float,语义是当前渲染帧的序号,从场景创建开始每帧加一。它不是一个归一化的时间,也不是秒数,所以任何基于它的动画都必须自己乘系数。

这里有个真实的精度坑:GLSL 的float是 32 位,尾数 23 位,能精确表示连续整数的上限是 2 的 24 次方,也就是 16777216。按 60 帧每秒算,大约 77 小时之后帧号就失去整数精度了。对于长时间挂着不关的大屏项目,这是个必须处理的问题。我一般的写法是先取模再运算:

float t = mod(czm_frameNumber, 100000.0) * 0.001;

这样周期被限制在可控范围内,而且帧号回绕的瞬间因为取了模,肉眼基本看不出来。

另一个问题是czm_frameNumber只能驱动"跟帧率相关"的动画,不同机器帧率不一样,效果速度就不一样。如果你的效果需要跟真实时间对齐(比如跟时间轴联动的水位上涨),正确做法是自己传 uniform。Fabric 材质的 uniforms 支持函数回调,引擎会在每次渲染前求值:

const scanMaterial = new Cesium.Material({ fabric: { type: 'ScanLine', uniforms: { color: new Cesium.Color(0.3, 0.85, 1.0, 1.0), speed: 0.6, // 每次渲染前求值,拿到的是真实时间 time: function () { return Cesium.JulianDate.secondsDifference( viewer.clock.currentTime, viewer.clock.startTime ); } }, source: `...` } });

time这个 uniform 名字在 GLSL 里直接当float用就行。这个技巧比czm_frameNumber稳得多,代价是每帧多一次 JS 调用,不过实测下来这点开销可以忽略。

除了帧号,还有一个跟视图模式过渡相关的变量czm_morphTime,float,范围 0 到 1,表示 2D/3D/哥伦布视图之间过渡的完成度。如果你写了一个只在 3D 下成立的效果(比如贴地的大气散射),过渡期间的表现会很难看,用这个值做个衰减能平滑掉。

2.2 相机、矩阵与坐标链路变量

坐标相关的变量是整个 czm 体系里最容易用错的一类,因为 Cesium 同时维护着好几套坐标系:世界坐标(WC)、相机坐标/视空间(EC)、模型坐标(MC)、屏幕坐标、NDC,以及针对大坐标做了精度补偿的"相对视点的模型坐标"。

矩阵类变量里,最常用的是czm_viewMatrix、czm_inverseViewMatrix、czm_projectionMatrix、czm_modelViewMatrix、czm_modelViewProjectionMatrix,以及带RelativeToEye后缀的变体。带这个后缀的矩阵是 Cesium 处理大坐标精度问题的核心手段:它把顶点做相对视点的平移,然后再投影,避免在上万公里的坐标上直接做矩阵乘法导致的抖动。

提示:自己写完整 Primitive 时,如果你发现远处物体在相机移动时出现明显的顶点抖动,八成是用了czm_modelViewProjection而不是czm_modelViewProjectionRelativeToEye,同时又自己传了原始世界坐标。这不是精度不够,是用错了链路。

相机位置相关的变量里,czm_viewerPositionWC(vec3)是最直观的一个,世界坐标系下的相机位置。还有一组名字带Encoded和MC的变量,是把相机位置拆成高、低两部分存储,专门给相对视点的计算用的,正常情况下不需要手动碰。

相机朝向相关的,可以用czm_viewRotation(mat3)以及它的逆czm_inverseViewRotation,还有一个转置版本czm_viewRotationTranspose(转置矩阵在渲染里很好用,因为正交矩阵的逆等于转置,省一次求逆)。把世界方向转到视空间,用czm_viewRotation乘就行;反过来用czm_inverseViewRotation。

视口相关的czm_viewport,类型是vec4,四个分量依次是 x、y、width、height,单位是像素。做全屏后处理或者屏幕空间算法(比如基于屏幕坐标的雨滴、扫描)时用得上,把 NDC 转到像素坐标再采样。

椭球体常量这一组经常被忽略,但做地形相关效果时很省事:czm_ellipsoidRadii(vec3)、czm_ellipsoidInverseRadii、czm_ellipsoidRadiiSquared、czm_ellipsoidCenter。这几个都是vec3类型(中心点除外),直接拿来做椭球表面的法线计算或者经纬度反算。

2.3 光照、大气与雾效相关变量

这一组是版本漂移最严重的地方。我按"当前版本"和"历史版本"两栏对照着列,看到老代码对不上号时,可以直接查表。

用途当前版本常用名历史版本名说明
光源方向(视空间)czm_lightDirectionECczm_sunDirectionEC重构后改为通用光源
光源方向(世界系)czm_lightDirectionWCczm_sunDirectionWC同上
光源颜色czm_lightColorczm_sunColor含强度,可直接乘
环境光czm_ambientLightColor同名偏暗,需配合 gamma 使用
太阳位置czm_sunPositionWC同名用于天空、光晕类效果
月光方向czm_moonDirectionWC同名夜间场景
雾密度czm_fogDensity同名全局配置驱动

czm_lightDirectionEC这个名字里的 EC 是 Eye Coordinates,也就是视空间。它的含义是"从物体指向光源的方向",注意不是光照方向的反向,这个约定跟很多渲染教程里的lightDir是反的,我因此写出过一张全黑的图。

颜色的色彩空间也要留意。渲染管线内部可能是线性空间,而你在 JS 里设置的Cesium.Color默认感知上是 sRGB 的。如果直接把颜色扔进着色器乘光照,结果会偏亮或者偏灰。Cesium 提供了czm_gammaCorrect和一对czm_srgbToLinear/czm_linearToSrgb来做转换,在需要精确还原颜色时该用就用,别省这一步。

2.4 一张变量可用性对照表

把"能不能用"这件事表格化,比记在脑子里可靠。下面这张表是我自己项目里积累的,标了顶点、片元两个阶段的可用性,以及材质的source里能不能直接引用。

变量类型顶点片元材质 source
czm_frameNumberfloat是是是
czm_morphTimefloat是是是
czm_viewMatrixmat4是是是
czm_inverseViewMatrixmat4是是是
czm_projectionMatrixmat4是是是
czm_modelViewMatrixmat4是是是
czm_viewRotationmat3是是是
czm_viewerPositionWCvec3是是是
czm_viewportvec4是是是
czm_lightDirectionECvec3否是是
czm_lightColorvec3否是是
czm_sunPositionWCvec3是是是
czm_fogDensityfloat否是是
czm_ellipsoidRadiivec3是是是

表里的"否"要当真。往顶点着色器里写czm_lightDirectionEC得到的不是零值,是编译错误,而且报错位置在拼接后的公共代码里,指向一个你没写过的文件,第一次遇到会很懵。我当时排查了快一个小时,最后靠二分法把代码切掉一半一半地试才定位到。

3. 内置函数:按用途分类拆解

函数这一块比变量复杂,因为数量多、签名长、用途交叉。我按"你写效果时的心智模型"分成四组,每组只挑高频的几个详细讲,剩下的给个速查思路。

3.1 坐标与空间转换类

先明确一点:Cesium 里几乎所有涉及坐标的函数名都带空间后缀,WC(世界)、EC(视空间)、MC(模型)、Window(屏幕像素)、NDC,看懂后缀基本就看懂了函数用途。

czm_translateRelativeToEye,签名大致是(vec3 positionHigh, vec3 positionLow)返回vec4。这个函数是把一个用高低位表示的世界坐标转换成"相对相机的视空间坐标",是解决大坐标抖动的最底层手段。你自己写 Primitive 传顶点属性时,如果顶点坐标量级在万米以上,就应该把坐标拆成高低位两个vec3属性,在顶点着色器里用这个函数合并。

czm_windowToEyeCoordinates,输入是屏幕像素坐标和深度,输出是视空间下的vec4。做"从屏幕点反推世界射线"这类拾取、投影、贴花效果时是主力函数。它的兄弟函数是czm_eyeToWindowCoordinates,方向反过来。

czm_modelToWindowCoordinates是一步到位的封装,直接给模型坐标,出屏幕坐标,做标注跟随、屏幕空间 UI 对齐时很省事,副作用是内部要做完整变换,密集调用会有点开销。

还有一个容易被忽视的czm_geodeticSurfaceNormal,输入椭球上的位置,输出该点的椭球面法线。给地形或地表贴效果、算坡度、算太阳入射角都靠它,比自己写公式稳。

3.2 程序化纹理与噪声类

czm_noise是我用得最多的一个函数,签名是float czm_noise(vec2),输入二维坐标返回 0 到 1 之间的伪随机值。它的实现读的是一张内置噪声纹理,所以你需要保证传进去的 UV 是归一化或者有明确周期的,直接传世界坐标那种成千上万的数值进去,采样会破坏掉纹理的环绕,得到一片纯色。

我一般的写法是先缩放再取其小数部分:

vec2 uv = fract(st * 8.0); float n = czm_noise(uv);

fract这一步看起来多余,实际很关键。不过它的副作用是接缝:取小数部分之后,8.0 和 0.0 是同一个点但采样纹理时的插值不连续,会出现明显的网格状接缝。要消掉接缝,可以把纹理设置为重复采样再用,或者改用多层不同频率叠加并把可见的接缝藏在其他效果里。

做体积感、云雾、腐蚀效果时,单层噪声不够,一般叠三到四层不同频率:

float fbm(vec2 p) { float v = 0.0; float a = 0.5; for (int i = 0; i < 4; i++) { v += a * czm_noise(p); p *= 2.0; a *= 0.5; } return v; }

层数不设太高,是因为每次czm_noise都是一次纹理采样,四层就意味着四次采样,在移动端上已经很可观了。我在一个中端安卓机上测过,片元里叠到六层,帧率直接掉三分之一。

注意:程序化纹理的着色器里不要写for循环的边界是变量,GLSL ES 对这种写法支持不好,可能导致循环无法展开而编译失败。固定次数的循环记得写成常量。

3.3 光照计算与色彩空间类

czm_phong是一个封装好的光照函数,输入大概包括视线方向、czm_material结构和光照方向,返回计算后的光照颜色。用它之前必须先调用czm_getDefaultMaterial拿到基础材质,再改字段,否则结构体里是未初始化的垃圾值。

自己写光照而不是用czm_phong,好处是可控,坏处是要自己处理色彩空间。这里我踩过一个典型的坑:给地形叠加自发光效果,直接用czm_lightColor乘上颜色,结果亮部过曝、暗部死黑。原因是czm_lightColor的量级可能带着强度,直接乘相当于把光照强度平方了一次。正确做法是先归一化方向、取值,或者用czm_lightColor做一次clamp后再插值。

色彩相关的还有一组色相、饱和度调节函数,czm_hue和czm_saturation,签名大致是接收vec3颜色和调整量,返回调整后的颜色。做"被选中高亮""夜间模式"这类整体调色时很好用,比自己写 HSL 转换省事。

czm_luminance输入vec3返回亮度值,做灰度化或者阈值判断时用。czm_gammaCorrect输入输出都是颜色,做线性到显示空间的转换。

3.4 深度、阴影与几何工具类

这一组偏底层,日常不太碰,但做后处理或者阴影相关效果时绕不开。

深度打包解包这一对czm_packDepth/czm_unpackDepth值得记住。WebGL1 时代,深度精度只有 8 位,直接写会分档,所以要把浮点深度压到 RGBA 四个通道里。现在 WebGL2 普及了,很多地方可以直接用浮点纹理,但 Cesium 内部仍然有依赖这对函数的路径,看到代码里有它们不要觉得奇怪。

射线与几何体求交的工具函数在处理拾取时很有用,czm_ray结构和相关的求交函数配合,可以算出拾取射线和椭球的交点。不过实际项目里我更倾向于在 JS 侧用 Cesium 的高层 API 做拾取,因为着色器里的求交拿不到地形高度,精度不够。

阴影相关有一组函数涉及级联阴影贴图的采样,输入是阴影坐标和权重。这块我建议直接复用官方的阴影材质,自己写采样很容易在级联切换的边界上出现条纹,排查成本很高。

4. 实战:写一个动态扫描光环材质

前面铺垫了这么多,落到代码上才有意义。这一节完整写一个效果:一个贴在地表的光环,从中心向外扩散,扩散过程中亮度衰减,同时叠加一层轻微的噪声扰动让边缘不那么规则。选这个效果是因为它把时间、噪声、颜色、st坐标四类知识都用上了。

4.1 需求拆解与参数设计

先把需求翻译成参数。光环需要一个扩散周期,我设成 2 秒一轮,参数名speed,含义是"每秒完成多少个周期"。需要一个最大半径,参数名maxRadius,取值范围 0 到 0.5,因为st坐标是 0 到 1 的归一化空间,0.5 就是整个面的半径。需要一个基础颜色,参数名color,vec4。需要一个噪声强度,参数名noiseStrength,范围 0 到 0.2。

这里有个设计决定需要解释:为什么不直接用czm_frameNumber驱动,而用外部传入的timeuniform?因为周期是 2 秒,如果用帧号驱动,写出来大概是mod(czm_frameNumber, 120.0) / 120.0,这个 120 是假定 60 帧每秒算出来的。在 30 帧的机器上,周期变成 4 秒;在 144 帧的机器上,周期变成 0.83 秒。同一个效果在不同设备上速度不一致,这在做交付项目时是不能接受的。所以用真实时间,把不确定性从渲染端拿掉。

st坐标的含义也要说清楚。czm_materialInput结构体里的st是纹理坐标,二维的,取值范围 0 到 1,原点在左下角。而str是加了旋转的版本(第三个分量是旋转后的分量)。我用st减0.5把它变成以中心为原点的坐标,再用length求距离,得到的就是"到中心的归一化距离",最大约 0.707(对角线)。所以maxRadius设 0.5 的时候,光环正好在到达边的中点时消失。

4.2 完整代码与逐段说明

先给完整的 Fabric 定义,然后拆开讲。

const rippleMaterial = new Cesium.Material({ fabric: { type: 'RippleRing', uniforms: { color: new Cesium.Color(0.25, 0.85, 1.0, 1.0), speed: 0.5, maxRadius: 0.5, noiseStrength: 0.06, time: function () { return Cesium.JulianDate.secondsDifference( viewer.clock.currentTime, viewer.clock.startTime ); } }, source: ` uniform vec4 color; uniform float speed; uniform float maxRadius; uniform float noiseStrength; uniform float time; float fbm(vec2 p) { float v = 0.0; float a = 0.5; for (int i = 0; i < 3; i++) { v += a * czm_noise(p); p *= 2.0; a *= 0.5; } return v; } czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; vec2 centered = st - vec2(0.5); float dist = length(centered); // 周期推进,0 -> 1 循环 float phase = fract(time * speed); // 当前扩散到的半径 float ringRadius = phase * maxRadius; // 环带宽度随扩散变宽,模拟扩散损耗 float ringWidth = mix(0.012, 0.06, phase); // 平滑的环状衰减 float ring = smoothstep( ringWidth, 0.0, abs(dist - ringRadius) ); // 噪声扰动边缘 float n = fbm(centered * 6.0); float disturbed = smoothstep( ringWidth, 0.0, abs(dist - ringRadius - (n - 0.5) * noiseStrength) ); // 整体亮度随扩散衰减 float fade = 1.0 - phase; float alpha = disturbed * fade * color.a; material.diffuse = color.rgb; material.emission = color.rgb * disturbed * 0.8; material.alpha = alpha; return material; } ` } });

czm_getDefaultMaterial(materialInput)这一步不能省。它会把materialInput里携带的法线、光照方向等信息填进默认材质结构,你不改的字段保持合理默认值。很多人直接czm_material material;声明一个然后只填diffuse,能跑,但在有光照的场景里表现会怪。

fract(time * speed)是周期推进的核心。fract取小数部分,所以time * speed从 0 累加到 1 时,phase从 0 走到 1,然后瞬间回到 0,形成循环。周期长度等于1.0 / speed秒。speed设 0.5,就是两秒一轮。

ringWidth用mix做插值,从 0.012 线性变到 0.06,意思是环刚开始很细很锐,扩散到边缘时变得很粗很虚。这个细节不加也能看,但加上之后"扩散"的物理感会强很多,因为它模拟了能量在扩散中逐步耗散的过程。

smoothstep(ringWidth, 0.0, abs(...))这个写法要注意参数顺序。smoothstep(edge0, edge1, x)在edge0 < edge1时是标准的"从 0 平滑升到 1"。我这里故意把两个边界反过来写成(ringWidth, 0.0, ...),得到的是"距离差越接近 0 值越大",正好是环状的峰值。这个技巧在写环形效果时很顺手,省掉一次1.0 -。

噪声扰动那一段,fbm的输入用centered * 6.0而不是st * 6.0,是为了让噪声图案跟着中心走,光环的呼吸看起来是整体的而不是斜着漂。(n - 0.5)把噪声拉到 -0.5 到 0.5 的对称区间,再乘强度,这样扰动是有正有负的,边缘会既往外凸也往里凹。

material.emission是自发光,给它加了 0.8 的系数而不是直接用diffuse,是因为在白天光照较强的场景里,纯靠diffuse的环会被环境光压住看不清,加一点自发光能保证可见性。系数不给到 1.0,是为了避免在夜间场景里过曝。

4.3 参数调优与性能取舍

拿到代码之后,最花时间的其实是调参。我把自己常用的取值范围整理成表,可以直接抄。

参数稳妥范围调大了会怎样调小了会怎样
speed0.2 ~ 1.5闪烁感强,像故障太慢,容易被当成静止
maxRadius0.35 ~ 0.6超出对角线的部分永远看不到环在小范围内挤成一团
noiseStrength0.02 ~ 0.12边缘碎成噪点,失去环形完全看不出噪声,白加
ringWidth起始0.008 ~ 0.02起步就是一团雾太细,远处看不清
fbm层数2 ~ 4移动端掉帧明显噪声太平,没层次

性能上的取舍逻辑:fbm每层是一次纹理采样,三层就是三次。这个材质如果贴在一个很大的多边形上(比如整个城市范围),片元数量是屏幕像素级别的,采样次数直接决定填充率。我做过一个对比,在一个覆盖屏幕三分之一的多边形上,三层fbm在集成显卡上的单帧渲染耗时约为单层的 2.2 倍。

如果要覆盖大面积,我的建议是分层处理:把光环用两层材质做,一层是纯数学的环(没有噪声,开销极低),一层是只有小范围才显现的噪声层。视觉上差别不大,开销能降一半以上。

5. 常见问题与排查实录

这一节全是踩过的坑,按"什么时候遇到"组织,遇到对应现象直接查表。

5.1 编译期报错速查

编译失败是最容易定位的,因为一定有报错信息,麻烦的是报错位置经常指向引擎那一侧。

现象大概率原因排查动作
报ERROR: 'xxx' : undeclared identifier,行号在 builtin 里用了当前版本不存在的czm_变量在引擎 Shaders 目录搜索该名字
报重复定义自己定义了和内置同名的变量或函数把自定义名字都加前缀,如my_
报#version相关错误自己写了版本指令删掉,材质里不允许
报main重复定义材质 source 里写了main改成czm_getMaterial
顶点着色器用光照变量报错该变量只在片元阶段可用换到片元里用,或用 uniform 传进来
报循环相关错误for循环边界是变量改成常量边界,用if做提前跳出

提示:排查编译错误时,"二分注释法"是最快的。把czm_getMaterial里的一半代码注释掉,跑一次,看还报不报错,十分钟内基本能定位到行。比盯着报错信息猜快得多。

5.2 运行期表现异常排查

能编译但效果不对,这类问题最耗时,因为没有报错。

现象:材质全黑。排查顺序是:先看alpha是不是 0(很多人忘了给material.alpha赋值,默认可能是 0 或者未定义),再看diffuse有没有赋值,最后看颜色空间。如果场景里有光照,还要确认diffuse是不是被环境光压到了接近 0。

现象:材质全白或过曝。典型原因是颜色乘了两次光照强度。czm_lightColor本身就带强度,如果你的diffuse又做了一次光照叠加,结果就是平方。把自发光部分单独拎出来设置,是最快的验证方式。

现象:动画速度在不同机器上不一致。百分之百是用了czm_frameNumber。改成外部 uniform 传真实时间。

现象:远处物体抖动。用了非 RelativeToEye 的矩阵链路,或者顶点坐标没做高低位拆分。这个问题在几百米范围内看不出来,飞到几公里外就开始抖。

现象:czm_noise采样出一片纯色。UV 超出合理范围,纹理采样被钳制了。加fract或者缩放到 0 到 1 区间。

现象:光环在球面上看起来是椭圆。因为st坐标在多边形上是线性插值的,在大范围球面上会被拉伸。这不是着色器的问题,是几何细分不够。解决办法是给多边形加细分,或者在着色器里用世界坐标算距离而不是用st。

5.3 兼容性与性能避坑清单

最后这部分是我这几年攒下来的硬经验,写成清单供自查。

移动端 GLSL 精度声明。片元着色器默认精度在移动端可能是mediump,只有 10 位尾数。如果你的算法里出现了大数值坐标运算(比如用世界坐标算噪声),会直接精度崩坏,表现为画面出现块状色带。稳妥做法是涉及坐标的变量显式声明highp,或者干脆全部用归一化坐标。

别在片元里做能移到顶点的计算。片元执行次数是像素级别的,顶点是几何顶点级别的,差好几个数量级。任何只跟顶点有关的东西(比如基于世界位置的整体偏移),都应该放顶点阶段。

纹理采样次数是硬瓶颈。尤其在移动端,一次采样在低端机上可能就要好几个时钟周期。三层噪声是很多效果的性价比上限,超过这个数,宁可牺牲一点视觉复杂度。

CustomShader 的 uniform 没有回调机制。材质支持函数回调自动求值,CustomShader 不支持,必须在preUpdate里手动setUniform。这个差异我第一次用的时候不知道,写了个time: function(){}进去,结果拿到的是undefined,排查了一阵。

升级版本时单独回归着色器。建立一条规矩:每次升级,把所有自定义着色器相关的场景飞一遍,尤其是光照和雾效。这两块是历史上变动最多的地方。

把czm_名字写进项目文档。我现在每个用自定义着色器的项目都会维护一个小文件,记录用了哪些czm_变量和函数、对应的版本号、用途一句话。看起来是额外工作,但下次升级时能省掉大量比对时间,这是真金白银的经验。

我在实际项目里最深的体会是:czm 这套内置能力本质上是把引擎内部的一块"半开放接口"暴露给了业务代码,它能让你做出很漂亮的效果,但它的契约并不像公开 API 那样稳定。所以用它的正确姿势是——每次使用都去源码里核对一遍,把依赖点显式记录下来,并且把着色器代码和引擎版本绑定在一起管理。做到这三点,这套东西用起来就很顺;做不到,每隔半年就会有一次"上次还好好的,怎么现在不对了"的时刻。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询