片元着色器是真正“绘制”高斯形状的地方。对于包围盒内的每一个像素,我们需要计算它相对于椭圆中心的马氏距离(Mahalanobis Distance),并据此输出颜色和透明度。
这里有一个非常关键的细节:我们不需要使用概率密度函数中的归一化系数
1
2
π
√
det
(
Σ
)
。在 3DGS 中,我们的目标是 Alpha 混合(Alpha Blending)而非概率估计。高斯椭圆的“浓淡”是由我们单独学习的一个参数——不透明度(Opacity,
α
) 来控制的。如果强行加入归一化系数,反而会导致颜色变得极暗,因为协方差矩阵的行列式通常很小,会把颜色“压”没。
// 片元着色器 (Fragment Shader)
in vec2 v_position; // 相对于中心的实际偏移量
uniform mat2 u_inv_covariance; // 协方差矩阵的逆
uniform vec3 u_color;
uniform float u_opacity; // 基础不透明度
out vec4 frag_color;
void main() {
// 1. 计算马氏距离的平方
// 公式: d² = (x - μ)ᵀ * Σ⁻¹ * (x - μ)
// v_position 就是 (x - μ)
float d_squared = dot(v_position, u_inv_covariance * v_position);
// 2. 计算 Alpha // 注意:这里没有归一化系数,直接由基础不透明度控制 float alpha = u_opacity * exp(-0.5 * d_squared); // 3. 裁剪:如果透明度过低,直接丢弃,提升性能 if (alpha < 0.004) { // 0.004 约等于 1/255 discard; } // 4. 输出最终颜色 frag_color = vec4(u_color, alpha);}
3. 补充说明
通过这套着色器组合,GPU 就能高效地将数百万个数学定义的高斯椭圆渲染到屏幕上。顶点着色器负责“画框”,片元着色器负责“填色”,最终呈现出平滑、连续的 3D 场景。
不过需要指出的是,上述代码主要侧重于原理的直观展示,属于一种理论化实现,旨在帮助读者理解 3DGS 的渲染逻辑。在实际的工业级工程应用中,为了追求极致的性能,实现方式可能有所不同;但其底层的数学原理——即利用二维高斯函数进行屏幕空间投影与混合——是完全一致的。
六、球谐系数
在上一节我们详细探讨了 3DGS 中每个高斯椭球的几何属性(位置、缩放、旋转),但一个逼真的 3D 场景显然不能只有形状,还必须有丰富的颜色和光照信息。回到第三章提到的 .ply 文件属性列表,你会发现除了 scale 和 rot 之外,还有一长串以 f_dc 和 f_rest 开头的属性:
property float f_dc_0
property float f_dc_1
property float f_dc_2
property float f_rest_0
…
property float f_rest_44
这些属性并非简单的 RGB 颜色值,而是球谐函数(Spherical Harmonics, SH)系数。在 3DGS 中,每个高斯椭球的颜色不是固定的,而是会随着观察角度的变化而变化,以此来模拟真实世界中光照与材质相互作用产生的复杂视觉效果(如镜面高光、菲涅尔效应等)。球谐函数正是实现这一“视角相关颜色”的核心数学工具。
- 傅里叶变换
要解释什么是球谐函数,我们先了解一下类似的傅里叶变换。在数字信号处理中,傅里叶变换的核心思想是:任何复杂的波形,都可以被拆解成无数个频率、振幅各不相同的纯正弦波的叠加。
为了理解这个过程,我们需要先认识两个核心概念:
基函数 (Basis Functions):这些“纯正弦波”就是傅里叶变换的基函数。它们是构成复杂信号的“标准波纹”。
分解与重构:傅里叶变换的过程,就是用这些标准波纹去“测量”原始信号,计算出每个频率的波纹在信号中占多少“份量”(即系数)。
在数学上,这一过程可以用以下公式严谨地表达:
f
(
x
)
∞
∑
n
0
c
n
⋅
ϕ
n
(
x
)
(23)
其中:
f
(
x
)
是我们要分析的原始复杂信号。
ϕ
n
(
x
)
就是第
n
个基函数(即我们提到的“标准正弦波纹”)。
c
n
是对应的系数,代表了该基函数在原始信号中的权重或“份量”。
简单来说,傅里叶变换就是在一条直线上,用不同频率的正弦波作为“尺子”,去度量和表达一个复杂信号。
- 球谐函数
现在,我们将傅里叶变换的思想从一维的“线”推广到三维空间中的“面”——一个单位球面。
在计算机图形学中,我们常常需要描述一个点周围的光照环境。光线可以从四面八方射来,这个光照信息是定义在一个球面上的函数,称为光照函数 (Lighting Function)。那么,如何表示这个复杂的球面函数呢?答案就是球谐函数 (Spherical Harmonics, SH)。
如果说傅里叶变换是用“平面正弦波”去拟合一条线上的信号,那么球谐变换就是用“球面波纹”去拟合一个球面上的信号。
基函数:球谐函数的基函数不再是正弦波,而是一系列定义在球面上的、具有不同频率和方向的“波纹图案”。
0 阶 SH:像一个纯色的球,代表整个球面的平均光照(环境光)。
1 阶 SH:像一个从黑到白渐变的球,能表示一个主方向的光照(比如太阳从上方照射)。
2 阶及更高:图案越来越复杂,能表示更精细的光照细节和反射。
因此,球谐函数本质上就是“球面上的傅里叶变换”。它用一组预定义的球面基函数,将复杂的环境光照压缩成一小撮系数。
在数学上,球谐函数的展开形式与傅里叶变换高度相似,其公式为:
f
(
θ
,
ϕ
)
≈
n
∑
l
0
l
∑
m
−
l
c
m
l
Y
m
l
(
θ
,
ϕ
)
(24)
其中:
f
(
θ
,
ϕ
)
是定义在球面上的原始函数(如光照分布)。
Y
m
l
(
θ
,
ϕ
)
是球谐基函数,由阶数
l
和频率
m
共同决定。
c
m
l
是对应的球谐系数,代表了该基函数在原始球面函数中的权重。
进一步地,球谐基函数
Y
m
l
(
θ
,
ϕ
)
的通用数学定义由三个部分组成:
Y
m
l
(
θ
,
ϕ
)
K
m
l
⋅
P
m
l
(
cos
θ
)
⋅
e
i
m
ϕ
(25)
K
m
l
(归一化常数):这是一个为了保证数学严谨性而存在的系数,确保基函数在球面上的积分为 1。具体计算式为:
K
m
l
√
2
l
+
1
4
π
(
l
−
m
)
!
(
l
+
m
)
!
P
m
l
(
cos
θ
)
(关联勒让德多项式):这是决定球谐函数“形状”的核心部分。不同的阶数
l
和频率
m
会对应不同的多项式形状(如常数、线性、二次曲面等)。
e
i
m
ϕ
(复指数项):这一项代表了函数在水平方向(方位角
ϕ
)上的旋转变化。
在渲染时,我们只需根据观察方向,用这些系数和基函数进行加权求和,就能快速还原出该方向上的光照强度。
- PBR中的球谐函数
在计算机图形学的基于物理的渲染(PBR)中,球谐函数扮演着至关重要的角色,主要用于高效地计算间接漫反射光照。
当一个物体(如角色、车辆)在场景中移动时,它不仅要被太阳光(直接光)照亮,还会被周围环境(如墙壁、地面)反射的光线照亮。这部分光线是柔和且无明确方向的漫反射光。为了实现这个效果,图形引擎会在场景中预先放置一些光照探针(Light Probes)。这些探针会“捕捉”周围环境的光照信息,并将其编码为一组球谐系数。当动态物体经过时,GPU 会根据其位置插值出对应的 SH 系数,并实时计算出物体表面接收到的环境光。
这样做的好处是,用极少的计算量(通常只需 2 阶 9 个系数)就能模拟出非常逼真的全局光照效果,让动态物体完美地融入静态场景。
- 3DGS中的球谐函数
在 3DGS 中,球谐函数的用途与 PBR 略有不同。它不是用来计算环境光,而是直接用来表示每个高斯椭球自身的视角相关颜色(View-dependent Color)。
3DGS 的核心思想是,一个高斯点在不同视角下看起来颜色是不同的,这模拟了现实世界中物体表面的高光(Specular Highlight)和菲涅尔效应(Fresnel Effect)。例如,一个光滑的桌面,从某个角度看会有刺眼的反光,换个角度则没有。
因此,在 3DGS 训练阶段,算法会为每个高斯点学习一组球谐系数(存储在 .ply 文件的 f_rest 字段中)。这组系数编码了该点在所有方向上的颜色变化规律。而在渲染阶段,当摄像机观察这个高斯点时,GPU 会根据观察方向(View Direction)和这组 SH 系数,实时计算出该点在当前视角下应该呈现的颜色。
为了平衡渲染质量与性能,3DGS 通常默认使用 3 阶球谐函数。这意味着公式 (24) 中的求和上限
n
取值为 3,阶数
l
的取值范围是
0
,
1
,
2
,
3
。根据球谐函数的性质,对于每一个阶数
l
,频率
m
的取值范围是
−
l
到
l
。因此,总的基函数数量(即系数个数)为 1 + 3 + 5 + 7 = 16 个。
将这 16 个基函数项代入公式 (24),我们可以将其展开为四个部分的加权和:
f
(
θ
,
ϕ
)
≈
0
∑
m
0
c
m
0
Y
m
0
0 阶: 基础漫反射
1
∑
m
−
1
c
m
1
Y
m
1
1 阶: 线性方向
2
∑
m
−
2
c
m
2
Y
m
2
2 阶: 二次曲面
3
∑
m
−
3
c
m
3
Y
m
3
3 阶: 三次细节
(26)
具体展开后的 16 项如下:
f
(
v
)
≈
c
0
0
(
0.28
)
+
c
−
1
1
(
−
0.48
y
)
+
c
0
1
(
0.48
z
)
+
c
1
1
(
−
0.48
x
)
+
c
−
2
2
(
1.09
x
y
)
+
c
−
1
2
(
−
1.09
y
z
)
+
c
0
2
(
0.31
(
3
z
2
−
1
)
)
+
c
1
2
(
−
1.09
x
z
)
+
c
2
2
(
0.54
(
x
2
−
y
2
)
)
+
c
−
3
3
(
−
0.59
(
3
x
2
−
y
2
)
y
)
+
c
−
2
3
(
2.89
x
y
z
)
+
c
−
1
3
(
−
0.45
y
(
4
z
2
−
x
2
−
y
2
)
)
+
c
0
3
(
0.39
z
(
5
z
2
−
3
)
)
+
c
1
3
(
−
0.45
x
(
4
z
2
−
x
2
−
y
2
)
)
+
c
2
3
(
1.44
z
(
x
2
−
y
2
)
)
+
c
3
3
(
−
0.59
x
(
x
2
−
3
y
2
)
)
(27)
在这个公式中:
常数项:这些数字并非随意指定,而是根据球谐基函数的通用数学定义,代入不同的阶数
l
和频率
m
后,计算出的归一化系数。它们确保了基函数在球面上的能量分布是标准的。
x
,
y
,
z
多项式:球谐基函数原本是基于球体极坐标系(
θ
,
ϕ
)定义的,包含复杂的三角函数(如
sin
θ
cos
ϕ
)。但在 3D 图形学的实际应用中,为了方便 GPU 进行高效的矩阵运算,我们通常会将这些极坐标下的三角函数,通过数学变换“翻译”成三维笛卡尔坐标系下的多项式形式。
观察方向:球谐函数本身是定义在“方向”上的,3DGS 中计算的是“当前视角”下的颜色,所以这个“方向”就是摄像机的观察方向,也就是这里的笛卡尔坐标
(
x
,
y
,
z
)
。
RGB 三通道:上述公式中的每一个系数
c
m
l
实际上都是一个包含 R、G、B 三个分量的向量。因此,对于 RGB 三个通道,总共需要存储
16
×
3
48
个浮点数。
这使得 3DGS 能够用极低的存储开销(使用 3 阶 SH,共 16 个系数,RGB 三通道共 48 个 float 值),就模拟出复杂的光照交互效果,让重建的场景看起来富有光泽和质感,而不是像塑料一样平淡。
- 3DGS 着色伪代码
结合前面的知识,我们可以写出 3DGS 中计算最终颜色的核心伪代码。这个过程发生在片元着色器(Fragment Shader)中:
// 片元着色器伪代码
// 输入
vec3 view_direction; // 观察方向
float sh_coeffs[48]; // 完整的球谐系数 (f_dc_0~2 + f_rest_0~44)
float opacity; // 基础不透明度
// 1. 计算 16 个球谐基函数值 (基于观察方向)
float sh_basis[16];
sh_basis[0] = 0.282095; // 0 阶 (对应 f_dc)
sh_basis[1] = -0.488603 * view_direction.y; // 1 阶 (对应 f_rest 前3个)
sh_basis[2] = 0.488603 * view_direction.z;
sh_basis[3] = -0.488603 * view_direction.x;
// … 计算 2 阶和 3 阶的基函数 (对应 f_rest 后42个)
// 2. 统一计算最终颜色
// 将 48 个系数与 16 个基函数进行加权求和
vec3 final_color = vec3(0.0);
for (int i = 0; i < 16; i++) {
// RGB 三个通道分别计算
final_color.r += sh_coeffs[i] * sh_basis[i];
final_color.g += sh_coeffs[i + 16] * sh_basis[i];
final_color.b += sh_coeffs[i + 32] * sh_basis[i];
}
// 3. 计算最终 Alpha
float alpha = opacity * exp(-0.5 * d_squared);
// 输出
frag_color = vec4(final_color, alpha);
通过这套机制,3DGS 巧妙地将复杂的、视角相关的光照效果“烘焙”进了一小组球谐系数中,实现了在极低计算成本下的高质量实时渲染。