1. 球谐光照:给游戏世界打上“柔光滤镜”
大家好,我是老张,一个在图形学里摸爬滚打了十来年的老程序员。今天想和大家聊聊一个听起来很“数学”、很“吓人”,但实际上非常酷且实用的技术——球谐光照。你可以把它想象成给3D游戏世界打上一层高级的“柔光滤镜”。过去,我们想模拟一个物体被整个天空的光线柔和照亮的效果,比如一个角色站在户外的树荫下,那种光线从四面八方漫射过来的感觉,往往需要预计算一张叫做“辐照度图”的贴图,然后实时去采样。这个方法效果不错,但有个大问题:太占内存了!每个不同的环境光就得存一张图,场景复杂点,内存就吃不消了。
球谐光照就是为了解决这个问题而生的。它的核心思想特别聪明:用一串数字(系数)来“描述”整个环境光,而不是存一整张贴图。这就像你用“温暖、明亮、偏黄”这几个词来形容午后的阳光,而不是拍一张360度的全景照片。在渲染时,我们只需要把这串数字和当前像素点的法线方向代入一个公式,就能立刻算出它应该被照得多亮、是什么颜色。这个计算量极小,速度快得飞起,而且占用的内存仅仅是几个浮点数而已。
我第一次在项目里尝试用球谐光照替换传统的环境光贴图时,效果简直惊艳。原本因为内存限制只能使用低分辨率环境光的移动端场景,在应用了三阶球谐(9个系数)后,不仅帧率稳住了,那种柔和的漫反射光影质感也立刻上了一个档次,物体看起来像是自然地“融”入了环境光里,再也没有了生硬的明暗分界。这让我深刻体会到,好的技术不一定是复杂的,而是能用最优雅的方式解决最棘手的问题。接下来,我就带你从最根本的数学概念开始,一步步拆解,直到用DirectX 12把它实现出来。放心,我会尽量避开那些让人头大的纯理论推导,聚焦在“怎么用代码把它搞出来”上。
2. 球谐函数:用“乐高积木”拼出任意形状的光
要理解球谐光照,首先得弄明白什么是球谐函数。别被名字吓到,我们可以用一个非常生活化的类比来理解它:乐高积木。
想象一下,你想用乐高积木拼出一个复杂的球形雕塑。你手头有各种形状的基础积木块:正方形、长方形、楔形、弧形等等。这些基础积木块,就是基函数。球谐函数,就是一套定义在球面这个“圆形底盘”上的特殊形状的积木块。你的目标雕塑——比如一个复杂的环境光照分布——就是你想拼出来的最终形状。
2.1 基函数:你的光影“积木箱”
在数学上,任何定义在球面上的连续函数(比如环境光的亮度分布),都可以用无限多个这种“球谐基函数”像搭积木一样线性组合出来。公式看起来是这样的:
f(n) ≈ Σ (系数 * 基函数(n))
这里的 f(n) 就是我们想描述的环境光函数,n 是球面上的一个方向(可以理解为法线方向)。系数 决定了每块“积木”要用多少,基函数(n) 就是那块特定形状的积木在方向 n 上的“高度”或“值”。
在实际应用中,我们当然不可能用无限多块积木。通常,我们只取前几阶的基函数,就能很好地近似低频的光照信息(也就是变化缓慢、没有锐利边缘的光影)。最常用的是三阶球谐,它需要9个基函数(对应9个系数)。这9个基函数长什么样呢?我直接给出它们在代码里常用的形式,你可以把它们想象成9种不同的、固定在球面上的波形图案:
// 三阶球谐基函数,输入是单位化的方向向量 n
float3 SHBasis(float3 n)
{
float x = n.x, y = n.y, z = n.z;
float3 basis;
// 第0阶 (l=0): 1个基函数,是个常数
basis[0] = 0.282095f; // Y00: 0.5 * sqrt(1/PI)
// 第1阶 (l=1): 3个基函数,和坐标线性相关
basis[1] = 0.488603f * y; // Y1-1
basis[2] = 0.488603f * z; // Y10
basis[3] = 0.488603f * x; // Y11
// 第2阶 (l=2): 5个基函数,和坐标的二次项相关
basis[4] = 1.092548f * x * y; // Y2-2
basis[5] = 1.092548f * y * z; // Y2-1
basis[6] = 0.315392f * (3.0f * z * z - 1.0f); // Y20
basis[7] = 1.092548f * x * z; // Y21
basis[8] = 0.546274f * (x * x - y * y); // Y22
return basis;
}
看到这些公式是不是亲切多了?它们就是一些常数和方向向量 (x, y, z) 的乘法和加减组合。在Shader里计算起来成本极低。这9个基函数的值,构成了我们描述光照的“9种基本波形”。
2.2 投影与重建:如何“拍照”和“显影”
现在我们有了“积木”(基函数),但怎么知道拼某个特定的环境光(比如一张HDR天空盒)需要每块积木用多少呢?这个过程叫做 投影。简单说,就是拿你的目标环境光函数,和每一个基函数做“比较”(数学上是求内积,即在整个球面上积分),算出一个相关系数。这个系数越大,说明这个基函数的“形状”和目标光照的“形状”越像,在组合中就越重要。
实际操作中,我们无法对连续的球面积分,所以采用


445

被折叠的 条评论
为什么被折叠?



