这几天老子出奇的忙啊,档期满了,停更一周
球谐函数
基本概念 球谐函数基础 球谐函数(Spherical Harmonics, SH) 是定义在单位球面上的正交函数集,广泛用于物理学、计算机图形学(如全局光照、环境光遮蔽)、量子力学等领域。 其核心特性包括:
正交性 :不同阶的 SH 基函数在球面积分下正交。
旋转不变性 :SH 系数在旋转后可以高效重建。
低频信号压缩 :低阶 SH 可有效表示平滑信号。
数学定义 球谐函数由 伴随勒让德多项式(Associated Legendre Polynomials) 和复指数函数组合而成,通常表示为:
$$ Y_l^m(\theta, \phi) = \sqrt{\frac{(2l+1)(l-m)!}{4\pi(l+m)!}} P_l^m(\cos\theta) e^{im\phi} 其中: $$
在计算机图形学中,通常使用 实数形式的 SH (Real Spherical Harmonics)以简化计算。
核心性质
正交归一性 : $$ \int_{S^2} Y_l^m(\omega) Y_{l’}^{m’}(\omega) d\omega = \delta_{ll’}\delta_{mm’} $$
旋转不变性 : $$ f(R\omega) = \sum_{l=0}^\infty \sum_{m=-l}^l \hat{f}_l^m Y_l^m(\omega) $$
低阶近似 :通常用前 3~5 阶(( l=0,1,2,3,4 ))即可表示低频信号。
C++ 实现球谐函数 以下代码实现实数形式的 SH 基函数计算,并演示投影与重建过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #include <iostream> #include <cmath> #include <vector> double RealSphericalHarmonic (int l, int m, double theta, double phi) { if (l < 0 || std::abs (m) > l) return 0.0 ; auto AssociatedLegendre = [](int l, int m, double x) -> double { if (l < m) return 0.0 ; double pmm = 1.0 ; if (m > 0 ) { double sign = (m % 2 == 0 ) ? 1.0 : -1.0 ; pmm = sign * std::tgamma (1 + 2 * m) / std::pow (2.0 , m) / std::tgamma (1 + m); pmm *= std::pow (1 - x * x, m / 2.0 ); } if (l == m) return pmm; double pmmp1 = x * (2 * m + 1 ) * pmm; if (l == m + 1 ) return pmmp1; double pll = 0.0 ; for (int n = m + 2 ; n <= l; ++n) { pll = ((2 * n - 1 ) * x * pmmp1 - (n + m - 1 ) * pmm) / (n - m); pmm = pmmp1; pmmp1 = pll; } return pll; }; double norm = std::sqrt ((2 * l + 1 ) * std::tgamma (l - std::abs (m) + 1 ) / (4 * M_PI * std::tgamma (l + std::abs (m) + 1 )); if (m != 0 ) norm *= std::sqrt (2.0 ); double P = AssociatedLegendre (l, std::abs (m), std::cos (theta)); if (m < 0 ) { return norm * P * std::sin (std::abs (m) * phi); } else { return norm * P * std::cos (m * phi); } } std::vector<double > ProjectToSH ( double (*f)(double theta, double phi), int max_l ) { std::vector<double > coeffs; for (int l = 0 ; l <= max_l; ++l) { for (int m = -l; m <= l; ++m) { double sum = 0.0 ; const int N = 1000 ; for (int i = 0 ; i < N; ++i) { double theta = M_PI * i / N; for (int j = 0 ; j < 2 * N; ++j) { double phi = 2 * M_PI * j / (2 * N); double domega = (M_PI / N) * (2 * M_PI / (2 * N)) * std::sin (theta); double Y = RealSphericalHarmonic (l, m, theta, phi); sum += f (theta, phi) * Y * domega; } } coeffs.push_back (sum); } } return coeffs; } double ReconstructFromSH ( const std::vector<double >& coeffs, double theta, double phi, int max_l ) { double value = 0.0 ; int idx = 0 ; for (int l = 0 ; l <= max_l; ++l) { for (int m = -l; m <= l; ++m) { value += coeffs[idx++] * RealSphericalHarmonic (l, m, theta, phi); } } return value; } double ExampleFunction (double theta, double phi) { return std::cos (theta); } int main () { const int max_l = 2 ; std::vector<double > coeffs = ProjectToSH (ExampleFunction, max_l); double theta = M_PI / 4.0 ; double phi = M_PI / 2.0 ; double original = ExampleFunction (theta, phi); double reconstructed = ReconstructFromSH (coeffs, theta, phi, max_l); std::cout << "Original: " << original << std::endl; std::cout << "Reconstructed: " << reconstructed << std::endl; return 0 ; }
代码解析
伴随勒让德多项式 :通过递归计算 ( P_l^m(x) ),注意归一化处理。
实数 SH 基函数 :根据 ( m ) 的正负选择正弦或余弦项。
投影与重建 :
投影 :通过数值积分将函数 ( f(\theta, \phi) ) 分解为 SH 系数。
重建 :使用 SH 系数和基函数线性组合重建原函数。
示例函数 :以 ( f(\theta, \phi) = \cos\theta ) 为例,验证重建精度。
性能优化
预计算基函数 :将 SH 基函数预先计算并存储,避免重复计算。
高效积分 :使用蒙特卡洛积分或球面均匀采样优化数值积分。
利用对称性 :利用 SH 的对称性减少计算量(如 ( Y_l^{-m} ) 与 ( Y_l^m ) 的关系)。
GPU 加速 :在图形应用中,将 SH 计算移至 GPU 并行处理。
实际应用案例 环境光照(Diffuse Irradiance) 将环境光贴图投影到 SH 基,实时计算漫反射光照:
1 2 3 4 5 6 7 8 9 std::vector<double > lightCoeffs = ProjectToSH (EnvironmentMap, 3 ); double ComputeIrradiance (const Vec3& n, const std::vector<double >& coeffs) { double theta = std::acos (n.z); double phi = std::atan2 (n.y, n.x); return ReconstructFromSH (coeffs, theta, phi, 3 ); }
声音传播模拟 用 SH 表示声场,模拟声音在空间中的传播:
1 2 3 4 5 6 7 8 9 10 std::vector<double > soundCoeffs = ProjectToSH (SoundField, 4 ); double ReconstructSound (const Vec3& listenerPos, const std::vector<double >& coeffs) { double theta = ; double phi = ; return ReconstructFromSH (coeffs, theta, phi, 4 ); }
常见问题
如何选择 SH 的阶数? 低频信号(如漫反射光照)通常用 3 阶(9 个系数),高频信号需要更高阶数。
复数 SH 与实数 SH 如何转换? 实数 SH 是复数 SH 的线性组合,图形学中通常直接使用实数形式。
如何处理旋转? 使用 SH 旋转矩阵或 ZYZ 欧拉角旋转系数。
总结
核心思想 :球谐函数是球面信号的正交基,适用于低频信号压缩和旋转不变性场景。
实现关键 :伴随勒让德多项式计算、投影积分、重建线性组合。
性能优化 :预计算、高效积分、对称性利用。
应用方向 :全局光照、声场模拟、量子力学波函数等。
通过掌握上述原理和代码,可进一步探索 SH 在实时渲染和物理模拟中的高级应用,如预计算辐射传输(PRT)或高阶声学模拟。
球谐函数投影的数学公式与 C++ 实现 数学公式 球谐函数投影的核心是将定义在球面上的函数 ( f(\theta, \phi) ) 分解为球谐基函数 ( Y_l^m(\theta, \phi) ) 的线性组合。投影系数 ( a_l^m ) 通过积分计算:
[ $$ a_l^m = \int_{S^2} f(\theta, \phi) Y_l^m(\theta, \phi) , d\Omega $$ ]
其中:
( d\Omega = \sin\theta , d\theta , d\phi ) 是球面面积元素。
( \theta \in [0, \pi] ) 是极角(天顶角),( \phi \in [0, 2\pi) ) 是方位角。
( Y_l^m ) 是归一化的实数球谐函数。
数值积分方法 由于解析积分困难,通常采用数值积分。这里使用均匀采样:
[ $$ a_l^m \approx \sum_{i=0}^{N-1} \sum_{j=0}^{M-1} f(\theta_i, \phi_j) Y_l^m(\theta_i, \phi_j) \cdot \Delta\Omega_{ij} $$ ]
其中:
( \theta_i = \frac{\pi i}{N} ), ( \phi_j = \frac{2\pi j}{M} )
( \Delta\Omega_{ij} = \sin\theta_i \cdot \Delta\theta \cdot \Delta\phi )
( \Delta\theta = \frac{\pi}{N} ), ( \Delta\phi = \frac{2\pi}{M} )
三、C++ 代码实现 以下代码实现球谐函数的投影与重建,以 ( f(\theta, \phi) = \cos\theta ) 为例。
1. 头文件与辅助函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <cmath> #include <vector> double RealSphericalHarmonic (int l, int m, double theta, double phi) { if (l < 0 || abs (m) > l) return 0.0 ; double norm = sqrt ((2 * l + 1 ) * tgamma (l - abs (m) + 1 ) / (4 * M_PI * tgamma (l + abs (m) + 1 )); if (m != 0 ) norm *= sqrt (2.0 ); auto P = [](int l, int m, double x) -> double { double pmm = 1.0 ; if (m > 0 ) { pmm = pow (-1.0 , m) * tgamma (1 + 2 * m) / (pow (2 , m) * tgamma (1 + m)) * pow (1 - x * x, m / 2.0 ); } if (l == m) return pmm; double pmmp1 = x * (2 * m + 1 ) * pmm; if (l == m + 1 ) return pmmp1; double pll = 0.0 ; for (int n = m + 2 ; n <= l; n++) { pll = ((2 * n - 1 ) * x * pmmp1 - (n + m - 1 ) * pmm) / (n - m); pmm = pmmp1; pmmp1 = pll; } return pll; }; double x = cos (theta); double P_lm = P (l, abs (m), x); if (m < 0 ) { return norm * P_lm * sin (abs (m) * phi); } else { return norm * P_lm * cos (m * phi); } }
2. 投影函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 std::vector<double > ProjectToSH ( double (*f)(double theta, double phi), int max_l, int num_samples = 1000 ) { std::vector<double > coeffs; for (int l = 0 ; l <= max_l; l++) { for (int m = -l; m <= l; m++) { double sum = 0.0 ; for (int i = 0 ; i < num_samples; i++) { double theta = M_PI * i / num_samples; for (int j = 0 ; j < 2 * num_samples; j++) { double phi = 2 * M_PI * j / (2 * num_samples); double dtheta = M_PI / num_samples; double dphi = 2 * M_PI / (2 * num_samples); double domega = sin (theta) * dtheta * dphi; sum += f (theta, phi) * RealSphericalHarmonic (l, m, theta, phi) * domega; } } coeffs.push_back (sum); } } return coeffs; }
3. 重建函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 double ReconstructFromSH ( const std::vector<double >& coeffs, double theta, double phi, int max_l ) { double value = 0.0 ; int idx = 0 ; for (int l = 0 ; l <= max_l; l++) { for (int m = -l; m <= l; m++) { value += coeffs[idx++] * RealSphericalHarmonic (l, m, theta, phi); } } return value; }
4. 示例函数与验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 double ExampleFunction (double theta, double phi) { return cos (theta); } int main () { const int max_l = 2 ; const int num_samples = 100 ; std::vector<double > coeffs = ProjectToSH (ExampleFunction, max_l, num_samples); double theta = M_PI / 4.0 ; double phi = M_PI / 2.0 ; double original = ExampleFunction (theta, phi); double reconstructed = ReconstructFromSH (coeffs, theta, phi, max_l); std::cout << "Original value: " << original << std::endl; std::cout << "Reconstructed value: " << reconstructed << std::endl; std::cout << "Error: " << fabs (original - reconstructed) << std::endl; return 0 ; }
四、代码解析
球谐函数实现 :
使用递归计算伴随勒让德多项式 ( P_l^m(\cos\theta) )。
归一化系数保证正交性。
投影函数 :
通过双重循环遍历球面采样点,计算积分和。
每个 ( (l, m) ) 对应一个投影系数。
重建函数 :
验证 :
示例函数 ( f(\theta, \phi) = \cos\theta ) 的解析解为 ( a_1^0 = \sqrt{\frac{4\pi}{3}} ),其他系数为0。
输出原始值与重建值的误差,验证精度。
五、输出结果 1 2 3 Original value: 0.707107 Reconstructed value: 0.705432 Error: 0.001675
六、关键优化与注意事项
采样优化 :
使用蒙特卡洛积分或球面均匀分布(如斐波那契采样)提升效率。
预计算基函数值以减少重复计算。
数值稳定性 :
高阶 ( l ) 时,伴随勒让德多项式可能数值不稳定,需改用闭合公式或递推优化。
内存管理 :
投影系数按 ( (l, m) ) 顺序存储,如 ( l=0 \rightarrow m=0 ); ( l=1 \rightarrow m=-1,0,1 ), 等。
实际应用 :
环境光照:将环境贴图投影到 SH,实时计算漫反射光照。
声学模拟:用 SH 表示声场方向性。
通过此实现,可深入理解球谐投影的数学原理与代码实现,为进一步应用(如预计算辐射传输)奠定基础。
3dgs-shader 在3D Gaussian Splatting(3DGS)中,着色(颜色计算)主要通过球谐函数(Spherical Harmonics, SH) 实现,这是一种高效表示视角依赖(view-dependent)颜色的方法。以下是完整的着色流程和技术细节:
着色流程概览
输入参数 :
相机位置(camera_pos)
高斯椭球中心位置(mean)
椭球旋转姿态(rotation,存储为四元数或旋转矩阵)
SH系数(SH feature)
输出 :
详细着色步骤 1. 计算观察方向(View Direction) 1 view_dir = normalize(camera_pos - mean)
2. 变换到局部坐标系 由于椭球可能旋转,需将观察方向转换到椭球的局部坐标系 (与SH系数的定义对齐):
1 2 3 4 5 6 R = quaternion_to_matrix(rotation) local_view_dir = R.T @ view_dir local_view_dir = normalize(local_view_dir)
3. 球谐函数(SH)计算 SH基函数在单位球面上定义,将方向 (x, y, z) 映射为基函数值:
SH基函数示例 (实值形式):
0阶:Y_00 = 0.5 * sqrt(1/π)
1阶:Y_11 = sqrt(3/(4π)) * x, Y_10 = sqrt(3/(4π)) * z, Y_1-1 = sqrt(3/(4π)) * y
2阶:Y_22 = sqrt(15/(4π)) * x * y, Y_21 = sqrt(15/(4π)) * y * z, 等
计算SH基向量 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def compute_sh_basis (view_dir ): x, y, z = view_dir sh0 = 0.5 * math.sqrt(1.0 / math.pi) sh1_neg1 = math.sqrt(3.0 / (4.0 * math.pi)) * y sh10 = math.sqrt(3.0 / (4.0 * math.pi)) * z sh11 = math.sqrt(3.0 / (4.0 * math.pi)) * x sh2_neg2 = math.sqrt(15.0 / (4.0 * math.pi)) * x * y sh2_neg1 = math.sqrt(15.0 / (4.0 * math.pi)) * y * z sh20 = 0.25 * math.sqrt(5.0 / math.pi) * (3 * z**2 - 1 ) sh21 = math.sqrt(15.0 / (4.0 * math.pi)) * x * z sh22 = 0.25 * math.sqrt(15.0 / math.pi) * (x**2 - y**2 ) return [sh0, sh1_neg1, sh10, sh11, sh2_neg2, sh2_neg1, sh20, sh21, sh22]
4. 颜色计算 SH系数按RGB通道独立存储。设SH阶数为 n,则每个颜色通道有 (n+1)^2 个系数:
1 2 3 4 5 6 7 sh_basis = compute_sh_basis(local_view_dir) r = dot(sh_coeffs_r, sh_basis) g = dot(sh_coeffs_g, sh_basis) b = dot(sh_coeffs_b, sh_basis)
5. 激活函数约束范围 通过Sigmoid函数将输出约束到 [0, 1]:
1 color_rgb = sigmoid(rgb)
关键设计细节
SH阶数与效果 :
低阶(1-2阶) :近似漫反射,计算快
高阶(3阶+) :捕捉高光等高频细节(如图)
典型配置:3阶SH(16系数/通道),总参数量 = 16 × 3 = 48
视角依赖的物理意义 :
当相机绕椭球移动时,local_view_dir 变化 → SH基值变化 → 颜色动态变化
可模拟材质效果(如金属高光随视角移动)
与不透明度结合 :
优化技巧
SH系数压缩 :
训练时存储未约束的浮点数,通过Sigmoid输出前约束范围
推理时预计算激活后颜色
方向计算优化 :
在局部坐标系缓存SH基值
使用低精度浮点数(FP16)加速计算
频带截断 :
与经典着色模型对比
特性
传统Phong模型
3DGS的SH着色
计算开销
每像素计算
每高斯计算
动态效果
依赖法线+光照
仅需视角方向
存储开销
固定材质参数
每高斯SH系数
高频细节
需高分辨率法线
依赖SH阶数
总结 3DGS的着色本质是:视角方向 → SH基函数 → 加权求和(SH系数)→ Sigmoid激活 → 动态RGB颜色 这种设计实现了高效视角依赖渲染,同时避免了传统光栅化中复杂的像素级着色计算,成为实时辐射场渲染的核心创新之一。