附录 A:Shader 常见错误与排查顺序

本附录是第 1~23 章的随手排查表。先确认错误发生在编译、几何、坐标、函数、法线、颜色中的哪一层,再改动对应代码;同一时间只改一个变量,观察画面是否按预期变化。示例使用 Three.js r174 的 ShaderMaterial,完整运行方式见教程首页。

先按现象缩小范围

  1. **完全没有画面:**先看浏览器控制台中的 JavaScript、GLSL 编译和 WebGL 错误;确认静态服务器、模块路径、scene/camera/renderer、gl_Position 与片元输出。不要先调颜色参数。
  2. **画面有,但形状不对:**暂时输出 UV、局部坐标或距离遮罩,确认是取样位置还是函数本身。再检查比例、中心、旋转顺序和参数范围。
  3. **形状对,明暗不对:**输出法线 RGB,检查法线、光线和视线属于同一坐标空间;位移后确认法线也重新计算。
  4. **只有细节、轮廓不变:**确认效果运行在顶点还是片元阶段,以及几何体分段是否足够。
  5. **静态正常,动画或设备上异常:**检查时间单位、采样频率、精度、循环次数与色彩空间,再分辨视觉差异是否来自性能或算法。

1. 编译与 Three.js 输入

现象 常见原因 第一项检查 对应章节
GLSL 报类型不匹配 把 1 当 float 使用,或 vec2/vec3 分量数不对 用 1.0、vec2(1.0) 等明确类型;检查函数参数签名 2
编译通过但位置消失 顶点未正确写 gl_Position,或模型在相机可视范围外 暂用最小顶点变换与纯色片元输出 1、3
uv 在片元中不可用 uv 是顶点属性,未通过自定义 varying 传递 顶点写 vUv=uv,两阶段声明匹配 1
自定义几何上图案或光照异常 几何体缺 uv 或 normal 属性 检查 geometry.attributes,不要假设每个几何体都有这些属性 1
改 JS 滑块无效 shader 与 JavaScript 的 uniform 名称或类型不一致,或忘了更新值 同时检查声明、uniforms 对象和事件处理 1
RawShaderMaterial 一换就报未声明变量 原本依赖 ShaderMaterial 的内置声明 明确自行声明需要的属性与矩阵,核对实际 GLSL 版本 1

本教程的普通 ShaderMaterial 示例源码使用 attribute、varying 和 gl_FragColor 的入门写法;Three.js r174 在编译时处理兼容定义。直接把片段搬进别的材质或独立 GLSL 环境,必须重新核对版本、输入输出声明及内置变量。控制台给出的错误行号可能指向 Three.js 展开后的 shader,而不一定与示例源码一一对应。

排查坐标时可暂时把片元结果改为下面的颜色。它只验证 vUv,不验证你的纹理、距离场或光照:

// 片元阶段;vUv 来自顶点阶段的 UV 插值。
gl_FragColor = vec4( vUv, 0.0, 1.0 );
#include <colorspace_fragment>

若看到左下暗、右上偏黄的渐变,UV 通路基本成立;若是纯色或全黑,先检查几何属性、varying 与实际被渲染的材质。

2. 坐标、比例与基础函数

现象 常见原因 第一项检查 对应章节
圆变成椭圆 在非正方形平面或画布上直接用未修正的 UV 距离 分清画布、平面、图片三个比例;看距离在哪套单位中计算 7
图片被拉伸 纹理本身的比例与显示框不同 分别检查图片尺寸、目标平面尺寸与裁剪/适配规则 7、11
step 边界反了 参数顺序弄反,或把“内侧为 1”当成“外侧为 1” 把关键输入在阈值两侧各代入一个数 4
smoothstep 出现意外过渡 两个边界顺序错误或相等;过渡宽度和当前坐标单位不匹配 保证 edge0<edge1,先用常数测试遮罩 5
重复图案边缘断裂 在单格边界直接对 fract 后坐标求导,或格子宽高不是同一物理长度 先输出 fract(uv*tiles),再单独检查抗锯齿与格数 6、23
负坐标处重复错位 误把 fract(x) 当成“保留整数后的余数”,或混用 mod 的参数/符号规则 用 x=-0.2 手算:fract(x)=0.8 6
旋转中心跑偏 先旋转全局坐标再减中心,或混淆矩阵列向量约定 先减目标中心,再乘逆向旋转矩阵 9
波浪方向或速度与预想相反 sin(kx-ωt) 与 sin(kx+ωt) 的传播方向不同;角度未转弧度 暂时固定空间位置看时间变化,再固定时间看空间变化 10

比例问题不能只靠“看起来差不多”解决。先标记取样坐标的单位:UV 无单位;(uv-0.5)*vec2(1.5,1.0) 以 3:2 平面的高度为长度单位;纹理采样坐标还要考虑图片自身宽高。改变 CSS 的画布宽度,并不自动改变平面几何或图片比例。

3. Noise、分段与动画

现象 常见原因 第一项检查 对应章节
顶点 noise 位移没有形状 几何分段太少,只有几个顶点被取样 查看 PlaneGeometry 的 widthSegments/heightSegments 15、23
多层 noise 整体越来越亮 累加层数后忘记除以权重和 单独输出每层和归一化后的值 14
高频 noise 闪烁或格子状 频率超过顶点或像素采样能力,或差分间距与频率不匹配 降低最高频/层数,再提高必要的几何或像素采样密度 14、17
不同设备的 noise 细节不同 浮点精度、三角函数 hash 或高频累积误差 确认输入/输出范围与精度,避免以逐像素完全一致作为正确标准 13、17
暂停后画面还动 JS 仍在增加 uTime,或另一个公式直接使用真实时钟 统一通过一个模拟时间更新所有动画 uniform 10、23

片元图案只决定颜色时,低分段平面也可以有细线;顶点位移要靠足够密的网格才能改变轮廓。把分段调高会增加顶点成本,却不会自动让片元 noise 的跨设备精度变好。性能问题先确认瓶颈在顶点数量、每片元的 noise 层数,还是连续重绘。

4. 法线、光照与色彩

现象 常见原因 第一项检查 对应章节
表面起伏了,明暗仍像平面 位移后继续使用原 normal 输出修正法线 RGB,与原法线对照 15
法线方向随相机或模型转动错乱 局部、观察、世界空间混用 标注每个向量的空间;用 normalMatrix 或对应变换统一空间 15、16
细节明暗有,轮廓仍平 只在片元中扰动法线,没有顶点位移 看网格轮廓和深度,决定是否需要真实几何 16
自定义颜色偏灰或过亮 线性 RGB 与 sRGB 输出/纹理设置不一致 检查 renderer.outputColorSpace、颜色纹理设置和最终颜色转换 3、11
高光位置不跟波峰走 高度函数与法线斜率公式不是同一表面,或法线插值过粗 用同一个高度函数求斜率,再查看法线 RGB 15、23

normalMatrix 把局部法线转换到观察空间。如果你的光方向在世界空间,先把它也转换到观察空间,或者统一把法线转到世界空间;不能只把数值代入 dot 就期待方向正确。法线 RGB 是方向调试图,不是物体表面真实颜色。

5. SDF、光晕与 Ray Marching

现象 常见原因 第一项检查 对应章节
SDF 内外反了 混淆负内正外约定,或差集少了负号 在形状中心、边界、外部各代入一点 18、20
矩形角外距离太短 把两个正分量只取 max,未算角点的欧氏距离 看角外等距线是否为圆弧 19
并集填充对、等距线怪 硬组合在隐藏边界附近未保持精确内部距离 同时画原形状和组合零边界,不把 min/max 当处处精确距离 20
缩放后光晕宽度异常 查询点除以比例后,距离没乘回正的等比系数 用等距线比较缩放前后距离单位 20、23
阴影反向移动或盖住本体 查询点偏移号反了,或合成顺序不对 查 p-offset 和“背景→阴影→本体”的顺序 21
Ray Marching 缺面或提前命中 场值高估安全距离、步数不足、命中阈值过大 从单个精确球开始,分别看步数与阈值 22
虚拟 SDF 物体不能遮挡 Three.js 网格 全屏片元颜色没有自动写入虚拟表面深度 区分平面网格的深度与 ray march 命中距离 22

fwidth(d) 只根据邻近片元估计屏幕上一像素内的距离变化,用它画平滑边缘不会修复错误的 SDF 数值。SDF 的符号、零边界、距离幅值应分别验证。Ray Marching 还必须确认距离可以安全作为步长;一个只保证零边界正确的函数不一定适合直接向前跳同样长度。

最后核对输出与资源