案例 5.7粗糙漫反射:Oren-Nayar 与 Minnaert进阶
目标:理解为什么月亮、陶土、粗布在正面光照下“不够立体”,实现 Oren-Nayar 定性模型(并推导出一个不含三角函数的版本),以及更简单的 Minnaert 模型。
原理
满月看起来像一个亮度均匀的圆盘,边缘并没有像 Lambert 球那样变暗。原因在于月壤非常粗糙:表面由无数小坑和小斜面组成。在 Oren 和 Nayar 的模型里,粗糙表面被看成大量 V 形凹槽,每个小斜面本身是 Lambert 的。当光源和观察者在同一方向时,我们看到的恰好是那些朝向光源、被照亮的小斜面,而背光的斜面被挡在后面——即使宏观上表面已经倾斜到球的边缘,看到的仍然是亮面。于是粗糙表面呈现出“逆反射”(Retro-reflection)的特性:光从哪来,就更多地往哪里反射回去。
完整的 Oren-Nayar 模型包含遮挡、阴影和凹槽之间的相互反射,很复杂。它的论文同时给出了一个常用的“定性模型”(Qualitative Model):
θi、θr 是入射与出射方向的极角,φi − φr 是它们在切平面上投影的夹角,σ 是微斜面坡度的标准差(弧度),表示粗糙程度。σ = 0 时 A = 1、B = 0,退化成 Lambert。
去掉三角函数
直接照公式写,需要两次 acos、一次 sin、一次 tan,还要把 L、V 投影到切平面上求夹角,在 Shader 里很贵。其实可以完全用点积表达。记 s = L·V − (N·L)(N·V),把 L、V 用极坐标展开:
而 sin α · tan β = sin α · sin β / cos β = sin θi sin θr / max(cos θi, cos θr)(β 是较小的角,余弦较大)。两式相除:
这是一个精确的恒等变换,不是近似,结果只需要三次点积和一次除法。
// Oren-Nayar 定性模型,返回值已乘 N·L(URP 约定,不含 1/π)。 // 原式 A + B·max(0, cos(φi-φr))·sinα·tanβ 里的三角函数可以全部消掉: // s = L·V - (N·L)(N·V) = sinθi·sinθr·cos(φi-φr) // sinα·tanβ = sinθi·sinθr / max(cosθi, cosθr) // 于是 cos(φi-φr)·sinα·tanβ = s / max(N·L, N·V) float OrenNayar(float3 N, float3 L, float3 V, float sigma) { float NdotL = saturate(dot(N, L)); float NdotV = saturate(dot(N, V)); float s2 = sigma * sigma; float A = 1.0 - 0.5 * s2 / (s2 + 0.33); float B = 0.45 * s2 / (s2 + 0.09); float s = dot(L, V) - NdotL * NdotV; float t = max(max(NdotL, NdotV), 1e-4); return NdotL * (A + B * max(0.0, s) / t); }
// Oren-Nayar 定性模型,返回值已乘 N·L(URP 约定,不含 1/π)。 // 原式 A + B·max(0, cos(φi-φr))·sinα·tanβ 里的三角函数可以全部消掉: // s = L·V - (N·L)(N·V) = sinθi·sinθr·cos(φi-φr) // sinα·tanβ = sinθi·sinθr / max(cosθi, cosθr) // 于是 cos(φi-φr)·sinα·tanβ = s / max(N·L, N·V) float OrenNayar(vec3 N, vec3 L, vec3 V, float sigma) { float NdotL = saturate(dot(N, L)); float NdotV = saturate(dot(N, V)); float s2 = sigma * sigma; float A = 1.0 - 0.5 * s2 / (s2 + 0.33); float B = 0.45 * s2 / (s2 + 0.09); float s = dot(L, V) - NdotL * NdotV; float t = max(max(NdotL, NdotV), 1e-4); return NdotL * (A + B * max(0.0, s) / t); }
Minnaert 模型更早,也更简单,最初用来描述月面的亮度分布。它只有一个参数 k:
// Minnaert:k = 1 时就是 Lambert;k < 1 边缘变亮(天鹅绒、月面感),k > 1 边缘更暗 float Minnaert(float3 N, float3 L, float3 V, float k) { float NdotL = saturate(dot(N, L)); float NdotV = max(saturate(dot(N, V)), 1e-3); return pow(NdotL, k) * pow(NdotV, k - 1.0); }
k = 1 时 N·V 的指数为 0,就是 Lambert;k < 1 时 (N·V)k−1 在边缘(N·V → 0)变大,轮廓变亮,适合天鹅绒、桃子绒毛一类材质;k > 1 则让边缘更暗。由于 k < 1 时边缘处会趋于无穷,代码里给 N·V 设了一个下限。


从图 5-17 还能看出一个问题:σ 越大,Oren-Nayar 球整体越暗。这是因为定性模型丢掉了凹槽之间的相互反射,能量并不守恒。对于需要精确能量的场合,可以考虑后来的改进模型;在游戏里,更常见的选择是第 9 章的 Disney(Burley)漫反射,它用很小的代价同时考虑了粗糙度对边缘亮度的影响。