本系列文章旨在让3D GS的初学者或未接触过CUDA程序的读者能看懂代码内容。
3D GS论文原文链接:[2308.04079] 3D Gaussian Splatting for Real-Time Radiance Field Rendering
论文笔记链接:【论文笔记】3D Gaussian Splatting for Real-Time Radiance Field Rendering
【论文笔记】A Survey on 3D Gaussian Splatting 这篇综述的第3章也有详细介绍3D GS的方法。
官方代码链接:可微栅格化的CUDA代码(本系列文章介绍的代码均在此repo内);3D GS完整代码
本文介绍的代码若未单独说明,均位于
gaussian-splatting/submodules/diff-gaussian-rasterization/cuda_rasterizer/forward.cu
中。
第一部分 ------ 前向传播
3D高斯溅射的CUDA代码主要负责栅格化部分,其结构如下图所示。

本教程会在介绍每部分代码前,简单介绍代码涉及的基本原理和基本概念。
1. 3D协方差计算
1.1 数学基础
协方差矩阵的分解。根据
- 对称矩阵AA可通过正交对角化分解为A=RDRTA=RDRT,其中RR为正交矩阵,DD为对角矩阵,对角元素为矩阵AA的特征值。
- 协方差矩阵ΣΣ是半正定对称矩阵。
可得:协方差矩阵可分解为Σ=RSSTRTΣ=RSSTRT(其中SS为对角矩阵,且SST=S2=DSST=S2=D,因为ΣΣ的特征值均非负)。
若将RR视为旋转矩阵并转化为单位四元数表示qq,SS的对角元组成的向量ss视为尺度向量,则有R=q2r(q),S=diag(s)R=q2r(q),S=diag(s),其中q2r(⋅)q2r(⋅)表示将四元数转化为旋转矩阵。
分解原因:直接估计协方差矩阵难以满足其半正定对称性。
四元数与旋转矩阵。
- 单位四元数qq满足∥q∥2=1∥q∥2=1。非单位四元数q′q′可通过q=q′/∥q′∥2q=q′/∥q′∥2转化为单位四元数。
- 单位四元数q=(qx,qy,qz,qw)q=(qx,qy,qz,qw)转化为旋转矩阵RR的公式:
R=[1−2qy2−2qz22qxqy−2qzqw2qxqz+2qyqw2qxqy+2qzqw1−2qx2−2qz22qyqz−2qxqw2qxqz−2qyqw2qyqz+2qxqw1−2qx2−2qy2](1.1)R=1−2qy2−2qz22qxqy+2qzqw2qxqz−2qyqw2qxqy−2qzqw1−2qx2−2qz22qyqz+2qxqw2qxqz+2qyqw2qyqz−2qxqw1−2qx2−2qy2(1.1)
1.2 OpenGL/GLM中的矩阵存储方式
和python不同,OpenGL/GLM库的矩阵存储是列优先的。
例如,矩阵M=[1234]M=[1324]的存储顺序(即拉平后的数组)是[1,3,2,4][1,3,2,4]。 使用代码
glm::mat2 M = glm::mat2(1,3,2,4)
可创建上述矩阵MM。
1.3 相关代码解读
3D协方差计算代码为computeCov3D
,功能是根据尺度向量ss和单位四元数qq计算3D协方差ΣΣ。
输入 :3维尺度向量scale
ss、尺度修正系数mod
、旋转对应的单位四元数rot
qq;
输出 :协方差矩阵cov3D
ΣΣ(仅保存上三角)。
__device__ void computeCov3D(const glm::vec3 scale, float mod, const glm::vec4 rot, float* cov3D)
{
// 尺度矩阵S=diag(mod*s)
glm::mat3 S = glm::mat3(1.0f); // 初始化3阶单位阵
S[0][0] = mod * scale.x; // 缩放因数不影响本质
S[1][1] = mod * scale.y;
S[2][2] = mod * scale.z;
// rot和四元数的标准形式有着元素顺序上的差异,需进行转化
glm::vec4 q = rot;
float r = q.x;
float x = q.y;
float y = q.z;
float z = q.w;
// 旋转矩阵计算(式1.1),由于列优先存储方式,实际上是R^T。
glm::mat3 R = glm::mat3(
1.f - 2.f * (y * y + z * z), 2.f * (x * y - r * z), 2.f * (x * z + r * y),
2.f * (x * y + r * z), 1.f - 2.f * (x * x + z * z), 2.f * (y * z - r * x),
2.f * (x * z - r * y), 2.f * (y * z + r * x), 1.f - 2.f * (x * x + y * y)
);
// 协方差矩阵 Sigma = R * S * S * R'
glm::mat3 M = S * R;
glm::mat3 Sigma = glm::transpose(M) * M;
// 由于对称性,仅保存协方差矩阵的上三角(由于列优先,下列代码实际存储的是下三角),共6个元素。
cov3D[0] = Sigma[0][0];
cov3D[1] = Sigma[0][1];
cov3D[2] = Sigma[0][2];
cov3D[3] = Sigma[1][1];
cov3D[4] = Sigma[1][2];
cov3D[5] = Sigma[2][2];
}
注意由于列优先存储方式,因此代码中的R
实际上是RTRT。Sigma
的结果和前面的公式Σ=RSSTRTΣ=RSSTRT一致。
2. 2D协方差计算
2.1 基本原理
世界坐标系到摄像机坐标系的坐标变换。设点在世界坐标系下的坐标为X=[X,Y,Z]TX=[X,Y,Z]T,在摄像机坐标系下的坐标为XCXC。记摄像机外参矩阵E=[R∣t]E=[R∣t],Xˉ=[X,Y,Z,1]TXˉ=[X,Y,Z,1]T为增加一维的向量,则有关系
XC=EXˉ(2.1)XC=EXˉ(2.1)
或
XC=RX+t(2.2)XC=RX+t(2.2)
3D点X=[X,Y,Z]TX=[X,Y,Z]T在图像上的投影。设摄像机的3×43×4投影矩阵为PP,则dxˉ=PXˉdxˉ=PXˉ。其中x=(u,v)Tx=(u,v)T为投影的像素坐标,d=Zd=Z为深度。
若摄像机的3×43×4投影矩阵为P=K[I∣0],K=[fx0cx0fycy001]P=K[I∣0],K=fx000fy0cxcy1,则点XC=(XC,YC,ZC)TXC=(XC,YC,ZC)T(实际上是摄像机坐标系下的坐标)的投影坐标为
x=(u,v)T=(fxxz+cx,fyyz+cy)T(2.3)x=(u,v)T=(zfxx+cx,zfyy+cy)T(2.3)
投影的线性近似。由于涉及到射影变换,椭球(3D高斯)投影到图像上一般不是标准的椭圆(2D高斯)。因此,2D高斯只是投影的近似表达,可使用投影变换的线性近似来获得。
根据式(2.3),可求P=K[I∣0]P=K[I∣0]投影变换下的雅可比矩阵
J=[∂u/∂XC∂u/∂YC∂u/∂ZC∂v/∂XC∂v/∂YC∂v/∂ZC]=[fx/ZC0−fxXC/ZC20fy/ZC−fyYC/ZC2](2.4)J=[∂u/∂XC∂v/∂XC∂u/∂YC∂v/∂YC∂u/∂ZC∂v/∂ZC]=[fx/ZC00fy/ZC−fxXC/ZC2−fyYC/ZC2](2.4)
因此,高斯中心μμ处对应的线性近似为x=J∣XC=μCXC+bx=J∣XC=μCXC+b。
3D协方差ΣΣ在图像上的投影:设摄像机的3×43×4投影矩阵为P=K[R∣t]P=K[R∣t],则先将ΣΣ变换到摄像机坐标系下:ΣC=RΣRTΣC=RΣRT。此时,投影矩阵变为P=K[I∣0]P=K[I∣0],其线性近似为x=J∣XC=μCXC+bx=J∣XC=μCXC+b。进行这样的线性变换后,协方差变为Σ′=JΣCJT=JRΣRTJTΣ′=JΣCJT=JRΣRTJT。
2.2 相关代码解读
2D协方差计算代码为computeCov2D
,功能是获取投影后2D高斯的协方差矩阵Σ′Σ′。
输入 :高斯3D均值mean
(X,Y,Z)(X,Y,Z),相机焦距focal_x
,focal_y
,相机最大视角正切值tan_fovx
,tan_fovy
,3D协方差(上三角)cov3D
ΣΣ,以及(扩维到4×44×4并拉平的)摄像机外参矩阵viewmatrix
;
返回值:2D协方差矩阵Σ′Σ′(上三角)。
__device__ float3 computeCov2D(const float3& mean, float focal_x, float focal_y, float tan_fovx, float tan_fovy, const float* cov3D, const float* viewmatrix)
{
// 将高斯均值利用摄像机外参矩阵转换到摄像机坐标系下(式(2.1))
float3 t = transformPoint4x3(mean, viewmatrix);
// 将视锥范围外部的高斯中心限制在范围内
const float limx = 1.3f * tan_fovx; // 1.3为缩放因子,稍微扩大裁剪范围,因为中心在边界外的高斯,可能仍然能对边界像素产生影响
const float limy = 1.3f * tan_fovy;
const float txtz = t.x / t.z; // 计算像素坐标对应的正切值
const float tytz = t.y / t.z;
t.x = min(limx, max(-limx, txtz)) * t.z; // 截断:若像素正切值大于limx(小于-limx)则会使用limx(-limx)替代
t.y = min(limy, max(-limy, tytz)) * t.z;
// 雅可比矩阵的计算(式2.4),注意由于列优先存储方式,实际上为J^L
glm::mat3 J = glm::mat3(
focal_x / t.z, 0.0f, -(focal_x * t.x) / (t.z * t.z),
0.0f, focal_y / t.z, -(focal_y * t.y) / (t.z * t.z),
0, 0, 0); // 此处多一行不影响,计算结果取左上角2x2矩阵即可
// 投影矩阵左上角3x3子矩阵的转置 即 W=R^T
glm::mat3 W = glm::mat3(
viewmatrix[0], viewmatrix[4], viewmatrix[8],
viewmatrix[1], viewmatrix[5], viewmatrix[9],
viewmatrix[2], viewmatrix[6], viewmatrix[10]);
glm::mat3 T = W * J; // T = R^T * J^T
glm::mat3 Vrk = glm::mat3( // 3D协方差矩阵
cov3D[0], cov3D[1], cov3D[2],
cov3D[1], cov3D[3], cov3D[4],
cov3D[2], cov3D[4], cov3D[5]);
// 2D协方差矩阵 Cov2D = J * R * Sigma * R^T * J^T
glm::mat3 cov = glm::transpose(T) * glm::transpose(Vrk) * T;
// 由于对称性,仅返回上三角(实际上是下三角),共三个元素
return { float(cov[0][0]), float(cov[0][1]), float(cov[1][1]) };
}
注意代码中W
为RTRT,J
为JTJT。2D协方差计算公式与Σ′=JRΣRTJTΣ′=JRΣRTJT一致。
-
代码中涉及的
transformPoint4x3()
函数:__forceinline__ __device__ float3 transformPoint4x3(const float3& p, const float* matrix) { float3 transformed = { matrix[0] * p.x + matrix[4] * p.y + matrix[8] * p.z + matrix[12], matrix[1] * p.x + matrix[5] * p.y + matrix[9] * p.z + matrix[13], matrix[2] * p.x + matrix[6] * p.y + matrix[10] * p.z + matrix[14], }; return transformed; }
可以看到,
matrix
是下列4×44×4矩阵拉平后的结果(注意GLM库是列优先存储矩阵的)M=[M0M4M8M12M1M5M9M13M2M6M10M14M3M7M11M15](2.5)M=M0M1M2M3M4M5M6M7M8M9M10M11M12M13M14M15(2.5)
故该函数的输出为
p1′p2′p3′\]=\[M0M4M8M12M1M5M9M13M2M6M10M14\]\[p1p2p31\]p1′p2′p3′=M0M1M2M4M5M6M8M9M10M12M13M14p1p2p31 因此,该函数把输入矩阵MM的前3行与向量pˉ=\[pT,1\]Tpˉ=\[pT,1\]T进行矩阵相乘。 > 当MM为扩维的摄像机外参矩阵时,该函数将世界坐标系下的点转化到摄像机坐标系下。
3.1 基本原理
球面谐波函数。在函数的泰勒展开式f(x)=∑i=0∞akxkf(x)=∑i=0∞akxk或傅里叶展开式f(x)=∑i=0∞bksin(kx)f(x)=∑i=0∞bksin(kx)中,{xk}k=0∞{xk}k=0∞或{sin(kx)}k=0∞{sin(kx)}k=0∞可称为一组基函数,也即函数f(x)f(x)被表达为一组基函数的线性组合。
点击3D Gaussian Splatting部分原理介绍和CUDA代码解读(一)------3D/2D协方差和高斯颜色的计算查看全文