GPU Gems 1 · Chapter 8. Simulating Diffraction

从衍射光栅的波干涉出发,用切线、halfway vector 和有限彩虹样本构建实时衍射着色器。

学习目标

  • 能解释为什么普通光线反射模型会漏掉 CD 等微结构表面的彩色效果
  • 能推导 λ=|u|d/n,并说明切线方向、条纹间距与 halfway vector 如何进入 u
  • 能修改实时衍射实验的间距、视角、粗糙度和样本数,判断画质与 shader 成本的变化
  • 能回答:为什么 u=0 的精确反射需要单独加入各向异性高光?

直觉引入:表面太细时,光会“记住”波

一面普通墙壁的细节比可见光的波长大得多,我们可以把光当成一条条射线;但 CD 上的细密轨道非常小,邻近轨道反射出来的波会互相影响。观察者移动时,某些颜色突然变亮,正是不同波峰在某个方向重新对齐。

这章解决的问题是:不用完整求解电磁场,怎样让任意带有微结构方向的表面,在实时渲染中出现可控的彩色衍射。答案不是一张静态彩虹图,而是从局部几何算出哪些波长会增强,再用一个便宜的颜色近似完成着色。

先看一个波:相位决定增强还是抵消同相:波峰遇到波峰振幅更大反相:波峰遇到波谷接近抵消
衍射不是把亮度简单相加:同相增强,反相抵消,观察到的颜色来自许多波长的干涉结果。

1. 从光线模型切换到波的视角

可以看作波干涉在微结构表面的可见结果。普通表面反射模型忽略了光的波动性,这是合理的工程近似;当结构细到微米级,忽略它就会丢掉 CD、珠光包装和某些水印材料的颜色变化。

决定颜色,振幅决定强度。可见范围可以粗略取为 0.5 到 1 微米;shader 不需要模拟紫外和红外,只需把落在这段范围里的候选波长映射成 RGB。

2. 衍射光栅为什么会挑出颜色

本章把表面近似成 。相邻反射带之间的距离记为 d,入射波到达每一条带后都会产生向外传播的波。接收点看到的是这些波的叠加,而不是每条带独立贡献的亮度。

是关键:当不同路径差是波长的整数倍,波峰相遇并增强;当相位相反,波峰与波谷抵消。因为不同波长满足增强条件的方向不同,观察者移动时就会看到颜色沿表面滑动。

设入射角为 θ₁、观察方向角为 θ₂,用

u=sin(θ1)sin(θ2)u=\sin(\theta_1)-\sin(\theta_2)

这个式子在说:入射与观察方向的差异,可以先压缩成一个描述路径差的标量 u。对第 n 个干涉阶次,满足增强的波长近似为:

λn=udn,n=1,2,3,\lambda_n=\frac{|u|d}{n},\qquad n=1,2,3,\ldots

这个式子在说:条纹间距 d 越大,候选波长整体越长;阶次 n 越高,候选波长越短。最终只保留落在可见范围的 λ,并把它们的颜色累加。

3. 把物理关系放进顶点着色器

GPU Gems 的简化实现把衍射放在 vertex program 中。它并不限制网格形状,但每个顶点必须同时提供位置、法线和一条 。CD 的薄 quad 只是一种演示几何,任意带主方向的表面都可以复用这条路径。

在观察和光源方向之间构造 H=L+V,再将它投影到经过同一空间变换的切线 T 上:

u=dot(T,H)du=\operatorname{dot}(T,H)\,d

这个式子在说:切线负责告诉 shader 条纹朝向,H 负责把光源与观察者合成一个方向,d 把无量纲的方向差换成真实的周期尺度。实际代码可以把 u 取绝对值,因为正负只代表方向相反。

从光栅几何得到可计算的 udiffraction grating入射方向观察方向相邻反射带相隔 dvertex programH = L + Vu = dot(T, H) × dλ = |u|d / nC(λ) → RGBT 是顶点必须提供的切线
把微观条纹压缩成局部切线方向与间距 d;shader 只需从光线几何得到 u,就能挑出增强的波长。
vec3 P = (modelView * position).xyz;
vec3 L = normalize(lightPosition - P);
vec3 V = normalize(eyePosition - P);
vec3 H = L + V;
vec3 N = normalize((normalMatrix * normal));
vec3 T = normalize((normalMatrix * tangent));
float u = abs(dot(T, H) * spacing);
vec3 diffraction = vec3(0.0);
for (int n = 1; n < 8; n++) {
  float lambda = u / float(n);
  diffraction += rainbow(lambda);
}

这里的循环上限不是物理上的“只有七阶”,而是可实时实现的固定近似。原书指出早期 Cg 编译器会展开循环,顶点程序大小限制了动态循环;固定少量阶次使每个顶点的成本可预测,也方便在 WebGL2 中复现。

4. 用彩虹映射把波长变成颜色

真实的光谱到 RGB 需要更精确的颜色匹配函数,但实时 demo 可以使用 。原书使用三个相同形状的 bump function,分别放在蓝、绿、红区域;每个候选波长经过这个函数后,再加到 diffraction 颜色上。

若用一个形状参数 C 控制 bump 宽度,可以先把波长归一化到 [0,1],再用 max(1-x²,0) 形成平滑峰。C=4 是原实现给出的可接受起点,但它是艺术与性能之间的参数,不是新的物理常数。

有限样本 → 彩虹映射 → 最终颜色n = 1 … 7λ1λ2λ3λ4λ5λ6λ7C(λ) + anis颜色总和u = 0 时叠加各向异性高光固定循环换取可展开、可实时的 shader 成本
原书把可见波长求和近似成固定 8 项以内的循环,并用三个 bump 函数拼出彩虹映射。

实践中还要限制亮度。把所有候选颜色直接相加可能超过目标格式的范围,因此可以在输出前做软压缩或让材质强度参与缩放;不要通过删掉整个颜色通道来掩盖过曝,那会让颜色随视角变化失去连续性。

5. u=0 时补上各向异性高光

u 接近 0,公式会让很多可见波长同时参与精确反射。理想情况下,这会像一条极细的白线;现实中条纹有微小不规则,亮线会扩散。原书用一个 单独表达这部分。

可以用 w=dot(N,H) 描述 halfway vector 在法线方向的分量,再用粗糙度 r 控制扩散范围。直觉上,r 越大,高光越宽但越不尖;衍射彩色部分和这个高光部分最后相加。

float w = max(dot(N, H), 0.001);
float e = roughness * u / w;
float highlight = exp(-e * e);
vec3 anis = highlightColor * highlight;
vec3 finalColor = diffraction + anis;

这段高光不是把衍射替换成普通 Phong,而是专门处理 u 接近零时的极端情况。把它放在实验开关里,读者可以看到:隐藏它,正反射方向会变得过暗或过窄;打开它,颜色亮带和主高光的过渡更完整。

6. 视线、材质与网格方向的验证

原书用薄 quad 构造 CD,并把切线沿轨道方向;同时还加了一层类似 Fresnel 的透明反射层,让掠射角更容易看到环境反射。这个 demo 也可以应用到带方向纹理的普通表面,只要每个顶点的 tangent 真正对应微结构主方向。

验证时不要只站在一个角度看。至少检查三类状态:正反射附近的高光、偏离反射方向后的彩色亮带,以及切线旋转后条纹是否随之旋转。若只有第一类状态正确,通常是 u 或 tangent 空间不一致;若只有第二类状态正确,通常是各向异性补偿没有覆盖 u≈0

猜一猜:把 d0.62 调到 0.95,候选波长会整体往长波还是短波移动?把波长样本从 6 降到 3,首先变差的是颜色连续性、亮度,还是顶点成本?

Diffraction Lab

先猜:把条纹间距调大,彩色亮带会变宽还是变窄?

可交互
微结构表面预览样本 6 项u = 0.28 · d = 0.62 · highlight = 0.84波长样本n=1n=2n=3n=4n=5n=6C(|u|d/n)

观察:固定样本数是可展开 shader 的近似;高光是 u 接近零时的另一条贡献。

实验中的“单波长误区”会把所有样本涂成同一种颜色,便于和正常的有限彩虹求和对照;它不是物理正确模式,而是一个故障注入,用来证明“多阶样本 + 彩虹映射”各自承担了什么。

7. 实时近似的边界

这套 shader 展示的是波动特征的工程近似,而不是完整的电磁求解。固定 n 的数量让成本稳定,却可能漏掉某些视角下的有效波长;简单 rainbow map 让颜色好看,却不等同于严格的光谱渲染;各向异性高光补足了 u=0 的视觉缺口,却依赖粗糙度和高光色调参数。

因此,调参顺序应当是:先确认 tangent 与空间变换正确,再确认 u 随光源和视角连续变化,之后才调整样本数、rainbow map 形状和高光粗糙度。这样每一个视觉变化都有对应的几何或近似原因,而不是在错误坐标上堆颜色。

小结:把微结构变成可控的频谱近似

  • diffraction 来自微结构反射波的 interference,不是固定彩虹贴图。
  • 光栅间距 d 与方向差 u 决定候选波长 λ=|u|d/n
  • tangent 与 halfway vector 把局部表面方向接入 vertex shader。
  • rainbow map 将有限波长样本转成颜色,固定循环换取实时成本。
  • u=0 的 anisotropic highlight 负责补足精确反射附近的亮部。

练习

问题 1|改 Demo 代码 把实验的“单波长误区”打开,再分别把样本数设为 3、6、8。描述颜色连续性和每顶点计算量的变化,并说明为什么不能无限增加样本。

问题 2|推导题 取 u=0.8d=0.5,分别计算 n=1n=4 时的候选波长比例 λ。哪一个更可能落入可见范围,为什么还需要检查可见区间而不能直接累加?

问题 3|独立实现题 为一个带主方向纹理的表面补充 tangent,并把衍射 shader 应用到它。除了最终截图,你会如何验证切线方向、u≈0 高光和样本数量都工作正常?

名词解释

本章出现的专业名词,用大白话再讲一遍。

diffraction
微小周期结构让反射波增强或抵消,从而出现随视角变化的彩色效果。
wavelength
波峰到下一个波峰的距离;可见光的不同波长对应不同颜色。
interference
多列波相遇后互相增强或抵消的现象。
diffraction grating
由平行窄反射带组成的周期表面,用间距 d 描述它的微结构。
tangent vector
沿着表面微结构主方向的向量;shader 用它判断条纹朝哪边。
halfway vector
光源方向与观察方向相加得到的中间方向,用来概括两者的相对关系。
rainbow map
把候选波长换成近似彩虹 RGB 颜色的函数或查找表。
anisotropic highlight
沿表面主方向拉开的高光,用来补足精确反射附近的亮部。

资料与写作方式声明

本章以GPU Gems 1 · Chapter 8. Simulating Diffraction公开完整正文核定章节范围、事实坐标与时代语境,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…