周末掌握3D Gaussian Splatting

Hacker News Top 工具

摘要

一篇使用C++和OpenGL从零构建简化版3D Gaussian Splatting渲染器的教程,涵盖加载和渲染Gaussian splats。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/16 21:42

# 3D高斯泼溅:周末速成 来源:https://bfeldman.me/3dgs-weekend/ ## 引言 3D高斯泼溅(3D Gaussian Splatting)是一种解决以下问题的技术:给定一个场景的照片数据集,如何将其重建为3D模型?这通过一种机器学习算法实现,该算法针对多个不同相机角度执行以下操作:渲染场景,将其与同一角度拍摄的照片进行比较,然后更新场景以减小渲染图像与真实图像之间的差异。然而,与传统3D渲染器不同,3DGS不使用三角形作为图元,而是使用称为*高斯泼溅点*的对象,这使得渲染算法在3DGS中独一无二。 在本文中,我旨在通过从零开始构建一个简化版渲染器(约1000行代码)来展示3D高斯泼溅的工作原理。主要目的是帮助建立对3DGS数学的直观理解。建议读者具备线性代数、概率论和计算机图形学的基础知识。该渲染器使用C++和OpenGL编写,所有代码均可在GitHub (https://github.com/benjamin-feldman/3dgs-weekend) 上获取,但我尽量让本教程足够通用,以便您可以使用任何图形引擎(WebGPU、Metal、DirectX等)复现。 在教程结束时,我们将能够实时渲染出这样的高斯场景: (一张西红柿的3D高斯泼溅渲染图) 我还提供了交互式WebGPU可视化,您可以使用WASD和鼠标进行导航。 本文仅涵盖渲染,不涉及训练。然而,3DGS渲染器(Kerbl et al. 2023)中的一些技术决策(如可微性、正半定协方差保持)与训练流程紧密相关,我们将会强调这些点。 ## 加载3DGS场景 首先,我们需要一个场景来渲染。在撰写本文时,好的3DGS场景仍然很难找到,但幸运的是,Supersplat (https://superspl.at/) 最近使其网站上的泼溅点可下载 (https://blog.playcanvas.com/new-in-supersplat-downloadable-splats-licenses-and-social-links/),因此我们将使用这个资源。我们将使用这个西红柿盘 (https://superspl.at/scene/0101ad57) 场景,因为它相当轻量(只有约20万泼溅点),但您也可以随意使用任何您喜欢的场景。请注意,我们构建的渲染器并未针对大型场景进行优化。 然后,我们将加载从Supersplat下载的高斯泼溅场景。这可以让我们检查场景的方向是否正确,更重要的是,让我们初步了解泼溅点究竟是什么。典型格式是`.ply`,我在`ply_loader.h`中包含了一个自定义加载器。该加载器将`.ply`文件解析为`GaussianSplat`对象数组,并包装在`Scene`中: ```cpp constexpr int SH_COUNT = 16; constexpr int SH_CHANNEL_COUNT = 3; constexpr int SH_FLOAT_COUNT = SH_COUNT * SH_CHANNEL_COUNT; struct GaussianSplat { glm::vec3 centroid = glm::vec3(0.0f); float opacity = 0.0f; std::array<float, SH_FLOAT_COUNT> sphericalHarmonics = {}; std::array<float, 3> scale = {0.0f, 0.0f, 0.0f}; std::array<float, 4> rotation = {1.0f, 0.0f, 0.0f, 0.0f}; }; struct Scene { std::vector<GaussianSplat> splats; }; ``` 我们来分解一下: - `centroid` 是泼溅点在**世界空间**坐标中的位置。这与通常的图形渲染管线(模型数据以模型空间表示)不同。因此,在3DGS管线中不会有模型矩阵,只有视图矩阵(3D世界空间 → 3D相机空间)和投影矩阵(3D相机空间 → 2D屏幕空间)。 - `scale` 和 `rotation` 描述了泼溅点的几何形状,稍后我们会详细说明。 - `opacity` 和 `sphericalHarmonics` 描述了泼溅点的可见性和颜色,稍后也会说明。 在训练好的3DGS场景中,这些值在训练期间通过从已知相机视角渲染泼溅点,将结果与训练照片进行比较,并将图像误差反向传播到每个泼溅点的中心、缩放、旋转、不透明度和颜色系数来进行优化。 作为初步验证,让我们加载3DGS场景,并使用GL_POINTS(例如,作为点云)绘制每个泼溅点的中心: ```cpp Scene scene = loadPly("scene.ply"); std::vector<glm::vec3> centroids; centroids.reserve(scene.splats.size()); for (const GaussianSplat& splat : scene.splats) { centroids.push_back(splat.centroid); } GLuint vao = 0; GLuint vbo = 0; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, centroids.size() * sizeof(glm::vec3), centroids.data(), GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), nullptr); glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(centroids.size())); ``` ## 添加颜色:球谐函数 现在我们有了点云,让我们添加颜色!3DGS看起来如此出色的原因之一是它捕获了视角依赖的颜色:闪亮的物体、高光和小反射会随着相机移动而变化。这是通过将颜色存储为球谐函数(Spherical Harmonics, SH)而不是单个RGB值来实现的。这使得每个泼溅点的颜色取决于观察方向,这对于光泽材质非常有用。 我不会深入讲解SH,但直觉上很简单:它是一种使用基函数在球面上表达任何函数的方法,有点像傅里叶级数。这使我们能够拥有一个函数 $\mathrm{rgb}(\theta, \phi)$,对于每个泼溅点,它在方向 $(\theta, \phi)$ 上输出其RGB颜色。它的定义如下: $$ \mathrm{rgb}(\theta, \phi) = \sum_{\ell=0}^{L}\sum_{m=-\ell}^{\ell} c_{\ell m}Y_{\ell m}(\theta,\phi) $$ 其中 $Y_{\ell m}$ 是SH基函数,$c_{\ell m}$ 是存储在泼溅点中的RGB系数。有关SH的更深层可视化解释,我推荐阅读《球谐函数的视觉笔记》(https://irhum.github.io/blog/spherical-harmonics/)。 为了从给定相机视角恢复RGB,我们取从相机到泼溅点中心的归一化方向,在该方向上计算SH基函数,将每个基函数值乘以存储的RGB系数,求和结果,然后应用 `+0.5` 的偏移(这样零中心的SH输出会落在中间灰度附近而不是黑色),最后裁剪到 `[0, 1]` 范围。用着色器术语来说,操作基本上是: ```glsl vec3 rgb = vec3(0.5); for (int i = 0; i < 16; ++i) { rgb += shCoefficient[i] * shBasis(i, direction); } rgb = clamp(rgb, 0.0, 1.0); ``` 我们的实际着色器将SH基函数显式写出而不是使用循环,但这太冗长无法在此展示。 现在,我们有了一个彩色点云。您已经可以看到颜色如何根据观察方向变化! ## 高斯分布 让我们离开点云,进入真正的泼溅点!但为此我们需要做一些数学运算。3D高斯泼溅的主要数学概念是……高斯分布!虽然我不打算让本文成为概率论课程,但我认为记住一些关于高斯分布的要素很重要。 您可能遇到过一维高斯分布,它是一种概率分布,由均值 $\mu\in\mathbb{R}$ 和标准差 $\sigma \geq 0$ 参数化: $$ p(x) = \frac{1}{\sigma\sqrt{2\pi}}\exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right) $$ 一维高斯分布 (一张一维高斯图,中心在均值,标准差控制钟形宽度) 一维高斯分布,均值为 $\mu$,方差为 $\sigma^2$ 这个分布也可以扩展到二维或三维。对于 $d$ 维高斯分布,概率密度函数为: $$ p(x) = \frac{1}{\sqrt{(2\pi)^d|\Sigma|}} \exp\left(-\frac{1}{2}(x-\mu)^\top\Sigma^{-1}(x-\mu)\right) $$ 在三维中,$\mu$ 现在是一个3D向量,称为**中心**。这就是我们之前加载的 `GaussianSplat.centroid`。$\mu$ 的几何解释就是泼溅点在世界空间中的位置。 我们的标准差 $\sigma$ 变成了一个 $3\times 3$ 的正半定矩阵 $\Sigma$,称为**协方差矩阵**。一般来说,如果我们想谈论一个均值为 $\mu$、协方差为 $\Sigma$ 的高斯分布,我们写作 $\mathcal{N}(\mu,\Sigma)$。这里的 $\mathcal{N}$ 代表正态分布(高斯分布的另一个名称)。 以下是它在二维和三维中的样子: (一张二维高斯分布作为轮廓椭圆) 在二维中,高斯分布的等密度轮廓是围绕均值的同心椭圆。 二维高斯分布的等密度轮廓是同心椭圆,由 $\Sigma$ 的特征向量和特征值隐含得出。 (一张三维高斯分布作为椭球体) 在三维中,相同的高斯分布变成了一个漂浮在世界空间中的椭球体。 三维高斯分布与二维情况类似:等密度面是椭球体。 我们早已可以理解3D高斯泼溅的工作原理: 1. 从三维高斯分布 $\mathcal{N}(\mu, \Sigma)$ 开始。 2. 将其投影到二维空间,变成二维高斯分布 $\mathcal{N}(\mu_{2D}, \Sigma_{2D})$。 3. 绘制由 $(\mu_{2D}, \Sigma_{2D})$ 产生的椭圆。 3DGS渲染管线 (一张流程图:3D高斯 → 投影 → 2D高斯 → 光栅化 → 像素) 泼溅点在三维中作为概率分布存在,投影到屏幕空间的二维分布,最后才变成像素。 对场景中的每个泼溅点都这样做,这就是渲染3DGS场景的方法! 然而,其中有一些棘手的部分。现阶段要理解的主要思想是,在步骤1和2中,我们处理的不是具体的几何对象(就像光栅化中的三角形),而是概率分布,只有在最后一步才变成可绘制的几何图形。这目前可能有点抽象,但我们很快就会讲到! ## 重新参数化三维高斯分布 协方差矩阵必须是对称且正半定的(PSD)。直观地说,这意味着它可以沿某些轴拉伸空间,但不能产生负方差。这一点很重要,因为协方差控制着椭球体的大小和方向。 三维高斯分布由 $\mu\in\mathbb{R}^3$ 和 $\Sigma\in S_+^3$ 参数化,其中 $S_+^3$ 是 $3\times 3$ 对称正半定矩阵的集合。$\Sigma$ 的一个重要性质是存在一个旋转矩阵 $R$ 和一个缩放矩阵 $S$,使得: $$\Sigma = RS(RS)^\top$$ 关于为什么如此,请参见附录 (https://bfeldman.me/3dgs-weekend/#covariance-factorization-with-rotation-and-scaling)。我们也可以反过来理解:对于任意旋转矩阵 $R$ 和缩放矩阵 $S$,$\Sigma = RS(RS)^\top$ 保证是一个协方差矩阵。 这种紧凑的表示是3DGS实用的部分原因:每个泼溅点的几何形状用一个中心、三个缩放和一个旋转来描述,因此训练代码每个泼溅点只需优化少数几个几何参数。还记得我们的 `GaussianSplat` 结构体吗?它正是使用了这种表示,通过 `scale` 和 `rotation` 成员,而不是完整的 $3\times 3$ 协方差矩阵! - `scale` 是三个对数空间浮点数。应用 `exp(scale)` 后,它们变成缩放矩阵 $S$ 的对角线值。 - `rotation` 是四个浮点数,编码一个四元数,这是一种紧凑存储旋转矩阵 $R$ 的方式。我不会在这里详细讲解四元数,但网上有很多好的资源 (https://eater.net/quaternions)。 为什么这很重要?记住,3DGS场景是通过训练循环创建的,每个高斯分布的参数通过反向传播特定相机角度下渲染图像与真实图像之间的误差来更新。如果在一次更新中我们直接改变 $\Sigma$ 的条目,无法保证更新后的矩阵仍然是PSD的,因此它就不再是协方差矩阵了。但是,如果我们更新 `scale` 和 `rotation` 参数,我们知道重构的 $\Sigma$ *将会* 是一个协方差矩阵。 ## 渲染一个泼溅点 正如我们刚刚看到的,我们的图形图元是一个概率分布。与点或三角形不同,我们无法直接使用标准光栅化管线绘制这种图元。一种天真的将泼溅点放到屏幕上的方法是从分布中采样点,然后绘制这些点。这样看起来就像一朵随机的点云:在中心附近密集,在边缘稀疏。这种方法的问题是效率低下且有噪声:对于每个泼溅点,我们需要生成足够多的随机样本才能使椭圆看起来平滑,而且除非使用大量样本,否则结果仍然会闪烁。 我们已经阐述了3DGS渲染的关键思想:将这个三维概率分布投影到二维屏幕空间,然后才将其绘制为近似该分布的椭圆。这就是3DGS的优美之处:直到管线的最后,我们都在处理概率分布,只有在最后一步进入二维空间后,我们才将这个分布具体化为可以实际渲染的东西。 ### 关于可微性的说明 原始的3DGS渲染器被设计为可微的,因为它在训练期间使用。本教程的渲染器仅用于显示,但我们即将使用的数学正是源于这一约束:保持泼溅点平滑,将其投影为分布,最后才变成像素。 ### 投影中心 将高斯分布的中心从世界空间投影到屏幕空间非常直接,因为它只是三维空间中的一个点。我们想要应用模型-视图-投影变换。如前所述,3DGS场景已经在世界空间中,因此模型矩阵就是单位矩阵(意味着我们可以跳过模型变换)。然后我们应用视图矩阵将其移动到相机空间,再应用投影矩阵和透视除法将其移动到屏幕上。最终我们得到 $\mu_{2D}\in\mathbb{R}^2$,即屏幕空间中的一个二维向量。 ### 投影协方差矩阵 这部分的目标是将三维世界空间中的协方差矩阵转换为二维屏幕空间中的协方差矩阵。**这是3DGS渲染的核心!** 第一步是从四元数和缩放矩阵重构三维协方差矩阵 $\Sigma$。如前所述,我们有 $\Sigma = RS(RS)^\top$。 ```cpp glm::mat3 buildCovariance(const GaussianSplat& splat) { glm::quat q(splat.rotation[0], splat.rotation[1], splat.rotation[2], splat.rotation[3]); glm::mat3 R = glm::mat3_cast(glm::normalize(q)); // 四元数 -> 3x3旋转矩阵 glm::vec3 sigma(std::exp(splat.scale[0]), std::exp(splat.scale[1]), std::exp(splat.scale[2])); glm::mat3 S = glm::mat3(sigma.x, 0.0f, 0.0f, // 对角缩放矩阵 0.0f, sigma.y, 0.0f, 0.0f, 0.0f, sigma.z); glm::mat3 RS = R * S; return RS * glm::transpose(RS); // Sigma = (RS)(RS)^T } ``` 现在进入有趣的部分:我们如何将 $\Sigma$ 投影到屏幕空间?同样,我们希望对其应用标准的MVP管线。 #### 模型变换 如前所述,高斯泼溅点已定义在世界空间中,因此模型矩阵是单位矩阵。 #### 视图变换 MVP管线的下一步是视图矩阵。标准的视图矩阵结合了从 `glm::lookAt()` 得到的旋转和平移: $$ V = \begin{pmatrix} r_x & r_y & r_z & -\mathbf{r}\cdot\mathbf{p} \\ u_x & u_y & u_z & -\mathbf{u}\cdot\mathbf{p} \\ -f_x & -f_y & -f_z & \mathbf{f}\cdot\mathbf{p} \\ 0 & 0 & 0 & 1 \end{pmatrix} $$ 其中 $\mathbf{r}$、$\mathbf{u}$ 和 $\mathbf{f}$ 是相机的右、上、前轴,$\mathbf{p}$ 是相机位置。

相似文章

GlobalSplat: 通过全局场景标记实现高效的前馈式三维高斯散射

Hugging Face Daily Papers

GlobalSplat 引入了一种高效的前馈框架,用于三维高斯散射,通过全局场景标记实现紧凑且一致的场景重建,将计算开销和推理时间降低至78毫秒以下。该方法采用从粗到细的训练策略,防止表示膨胀,同时以显著更少的高斯原语(16K)达到有竞争力的新视角合成性能,与密集基线相比更为高效。

SplatWeaver:学习分配高斯基元以实现可泛化新视角合成

Hugging Face Daily Papers

SplatWeaver 是一种前馈新视角合成框架,它根据空间复杂度动态分配 3D 高斯基元,相比固定分配方法提升了渲染质量与效率。该框架利用基数高斯专家和高频先验引导的像素级路由方案,自适应地在复杂与平滑的场景区域间分配基元。

playcanvas/supersplat

GitHub Trending (daily)

SuperSplat 是一款免费、开源的浏览器端编辑器,由 PlayCanvas 基于 Web 技术构建,用于检查、编辑、优化和发布 3D Gaussian Splat。无需安装,可直接在 superspl.at/editor 在线使用。