4D Gaussian Splatting

1. GaussianModel:4D Gaussian 的参数

在代码里,GaussianModel 不是为每个 Gaussian 创建一个对象,而是把所有 Gaussian 的参数批量存成张量。
| 参数 | Shape | 含义 |
|---|---|---|
_xyz | [N, 3] | 空间中心 x, y, z |
_t | [N, 1] | 时间中心 t |
_scaling | [N, 3] | 空间尺度,存的是 log-scale |
_scaling_t | [N, 1] | 时间尺度,存的是 log-scale |
_rotation | [N, 4] | 4D rotation 的左四元数;在 3D 模式下是 3D rotation 四元数 |
_rotation_r | [N, 4] | 4D rotation 的右四元数,仅 rot_4d=True 时使用 |
_opacity | [N, 1] | 不透明度,存的是 inverse-sigmoid 空间的值 |
_features_dc | [N, 1, 3] | SH / 4DSH 的 DC 颜色项 |
_features_rest | [N, K-1, 3] | 其余 SH / 4DSH 系数 |
4DGS 相比 3DGS 多出来的核心参数是:
self._t
self._scaling_t
self._rotation_r
含义:
_t:这个 Gaussian 在时间轴上的中心。_scaling_t:这个 Gaussian 在时间维度上的局部尺度。_rotation_r:和_rotation一起构造完整 4D rotation,让 Gaussian 能在(x, y, z, t)空间中旋转,从而产生空间和时间的耦合。
左右四元数与 4D 协方差矩阵
3D rotation 可以用一个单位四元数表示;4D rotation 的自由度更多,代码使用一对单位四元数:
_rotation # 左四元数
_rotation_r # 右四元数
使用时会归一化为单位四元数。单位四元数表示纯旋转;如果不归一化,变换中会混入缩放,旋转矩阵就不再是正交矩阵。
需要注意:
- 一个单位四元数有 3 个自由度。
- 4D rotation 使用左右两个单位四元数,共 6 个旋转自由度。
- 4D 对称协方差矩阵本身有 10 个独立元素;代码用
scaling_xyzt + 4D rotation这种参数化方式来构造它。
4D 协方差构造链路:
scaling_xyzt = [sx, sy, sz, st]
R_4d = R(q_l, q_r)
Σ_4d = R_4d diag(sx², sy², sz², st²) R_4dᵀ
代码对应:
L = build_scaling_rotation_4d(scaling, rotation_l, rotation_r)
actual_covariance = L @ L.transpose(1, 2)
2. 4D Gaussian 如何切成当前帧 3D Gaussian
最重要的函数:
4d-gaussian-splatting/scene/gaussian_model.py
get_current_covariance_and_mean_offset()
核心变量:
dt = timestamp - self.get_t
dt 表示“当前渲染时间”和“这个 Gaussian 自己的中心时间”的差。
关键代码:
def build_covariance_from_scaling_rotation_4d(
scaling,
scaling_modifier,
rotation_l,
rotation_r,
dt=0.0,
):
L = build_scaling_rotation_4d(
scaling_modifier * scaling,
rotation_l,
rotation_r,
)
actual_covariance = L @ L.transpose(1, 2)
cov_11 = actual_covariance[:, :3, :3] # Σ_xx:空间 xyz 的 covariance
cov_12 = actual_covariance[:, 0:3, 3:4] # Σ_xt:空间和时间的耦合项
cov_t = actual_covariance[:, 3:4, 3:4] # Σ_tt:时间维度的 variance
current_covariance = cov_11 - cov_12 @ cov_12.transpose(1, 2) / cov_t
symm = strip_symmetric(current_covariance)
if dt.shape[1] > 1:
mean_offset = (cov_12.squeeze(-1) / cov_t.squeeze(-1))[:, None, :] * dt[..., None]
mean_offset = mean_offset[..., None]
else:
mean_offset = cov_12.squeeze(-1) / cov_t.squeeze(-1) * dt
return symm, mean_offset.squeeze(-1)
数学上,完整 4D Gaussian 可以分块写成:
Σ_4d =
[ Σ_xx Σ_xt ]
[ Σ_tx Σ_tt ]
给定当前时间 t 后,条件 3D Gaussian 为:
μ_x|t = μ_x + Σ_xt Σ_tt⁻¹ (t - μ_t)
Σ_x|t = Σ_xx - Σ_xt Σ_tt⁻¹ Σ_tx
代码中的:
current_covariance = Σ_x|t
mean_offset = Σ_xt Σ_tt⁻¹ (timestamp - self.get_t)
所以当前帧 3D 中心是:
当前帧 3D 中心 = _xyz + mean_offset
get_cov_t() 与时间可见性
get_cov_t() 用来计算某个 4D Gaussian 在世界时间轴上的方差:
cov_t = Var(t) = Σ_4d[3, 3]
它不是速度,也不是中心时间,而是控制这个 Gaussian 在时间维度上衰减速度的方差项。
时间可见性由 get_marginal_t() 计算:
sigma = self.get_cov_t()
torch.exp(-0.5 * (self.get_t - timestamp) ** 2 / sigma)
后续会和基础 opacity 相乘:
opacity_current = opacity_base × marginal_t(timestamp)
含义:
- 当前时间越接近 Gaussian 的中心时间
_t,该 Gaussian 越可见。 - 当前时间离
_t越远,该 Gaussian 越透明。
3. 4DSH:颜色如何引入时间
第 i 个 Gaussian 的颜色同时依赖:
d:观察方向。Δt:Gaussian 中心时间和当前渲染时间的差。
4DSH 的基函数为:
Z^m_{n,l}(t, θ, φ) = cos(2π n t / T) · Y^m_l(θ, φ)
其中:
Y^m_l(θ, φ)处理观察方向。cos(2π n t / T)处理时间变化。
因此,4DSH 相比普通 SH 的核心变化是:基函数从只依赖方向的 Y_l^m(d),扩展为依赖方向和时间的 cos(...) · Y_l^m(d)。训练时学习的仍然是这些基函数前面的颜色系数。
代码对应:
utils/sh_utils.py
eval_shfs_4d()
默认 sh_degree=3、sh_degree_t=2 时:
空间 SH basis 数 = (3 + 1)^2 = 16
时间 basis 数 = 3,即 n = 0, 1, 2
总通道数 = 16 × 3 = 48
颜色形式可以理解为:
RGB(d, Δt)
= C0(d)
+ cos(2π Δt / T) · C1(d)
+ cos(4π Δt / T) · C2(d)
4. 论文中的渲染公式
3DGS 中,一个 pixel 的最终颜色为:
C = Σ_i c_i α_i G_i^{2D}(x) Π_{j=1}^{i-1}(1 - α_j G_j^{2D}(x))
含义:
c_i:第i个 Gaussian 的颜色。α_i:第i个 Gaussian 的基础 opacity。G_i^{2D}(x):投影到屏幕后的 2D Gaussian 权重。Π(...):前面 Gaussian 透过来的剩余透射率。
4DGS 将它扩展为依赖时间的渲染:
I(u, v, t)
= Σ_i p_i(u, v, t) α_i c_i(d, t)
Π_{j=1}^{i-1}(1 - p_j(u, v, t) α_j)
其中 p_i(u, v, t) 是第 i 个 4D Gaussian 在当前像素和当前时间下的贡献。
进一步分解:
p_i(u, v, t) = p_i(t) · p_i(u, v | t)
也就是:
一个 4D Gaussian 在当前时间 t 的贡献
= 当前时间切片下的 3D Gaussian
× 这个 Gaussian 在时间 t 上的 marginal 权重
因此:
I(u, v, t)
= Σ_i p_i(t) p_i(u, v | t) α_i c_i(d, t)
Π_{j=1}^{i-1}(1 - p_j(t) p_j(u, v | t) α_j)
5. 渲染过程
代码入口:
gaussian_renderer/__init__.py
render()
关键流程:
render()
↓
拿当前 viewpoint_camera.timestamp
↓
如果 compute_cov3D_python=True:
pc.get_current_covariance_and_mean_offset()
pc.get_marginal_t()
在 Python 里得到当前 3D Gaussian
↓
否则:
把 4D 参数 ts / scales_t / rotation_r 连同 timestamp 交给 CUDA
CUDA 内部得到当前 3D Gaussian
↓
rasterizer 把当前 3D Gaussian 投影成 2D splat
↓
alpha blending 得到 rendered image
默认配置一般是:
compute_cov3D_python: False
convert_SHs_python: False
因此,默认训练时:
- 4D → 3D 的条件化主要在 CUDA 中完成。
- 4DSH → RGB 的颜色求值主要在 CUDA 中完成。
6. 训练优化
6.1 参数优化
论文中优化主要依赖渲染损失。代码里的主 loss 是:
loss = (1 - λ_dssim) · L1 + λ_dssim · (1 - SSIM)
训练时每个样本包含:
gt_imageviewpoint_camviewpoint_cam.timestamp
训练流程:
DataLoader 抽取 batch
↓
每个样本根据 timestamp 做 4D 切片
↓
render 得到预测图像
↓
和 GT image 计算 loss
↓
反向传播更新 4D Gaussian 参数
论文提到 batch sampling 可以缓解动态场景中的 temporal flickering / jitter。代码实现上,它就是一次 iteration 中采 batch_size 张训练图一起算 loss;这些样本通常对应多个相机位置和多个 timestamp。
6.2 密度控制
普通 3DGS 主要根据屏幕空间位置梯度判断哪里重建不足,需要增密。4DGS 论文中还提到引入时间位置 t 的平均梯度作为 density control 指标。
需要注意代码实现细节:
-
代码确实计算并累计了
_t.grad:_t.grad → batch_t_grad → t_gradient_accum → grads_t -
但当前
densify_and_clone()和densify_and_split()的选择条件主要仍然使用屏幕空间梯度grads,没有真正使用grads_t来决定哪些 Gaussian 被增密。
当前代码中真正体现时间维度增密的是 time split。
7. Gaussian 增长与减少逻辑
7.1 什么时候触发
训练循环中:
densify_from_iter 之后开始
每 densification_interval 个 iter 执行一次
densify_until_iter 之后停止增长
默认大致是:
500 iter 后开始
每 100 iter 执行一次
15000 iter 后停止 densification
7.2 Clone:小 Gaussian 直接复制
如果一个 Gaussian:
屏幕空间梯度大
并且自身尺度较小
代码会执行 clone:复制一个几乎相同的 Gaussian,让后续优化把它们拉开。
7.3 Split:大 Gaussian 分裂
如果一个 Gaussian:
屏幕空间梯度大
并且自身尺度较大
代码会执行 split:从该 Gaussian 的分布中采样多个新 Gaussian,并删除旧 Gaussian。
4DGS 的关键是 split 在完整 xyzt 空间中进行:
new_xyzt = R_4d @ sample_xyzt + old_xyzt
所以新 Gaussian 的中心不仅 x, y, z 会变化,t 也会变化。
这就是 time split。
7.4 Prune:删除无效 Gaussian
删除条件主要包括:
- opacity 太低。
- 屏幕空间半径太大。
- 世界空间尺度太大。
删除时,代码会同步删除对应的:
xyz / t / scaling / scaling_t / rotation / rotation_r / opacity / feature
以及 Adam optimizer 中的动量状态。
8. 评估与消融实验

论文中的主要消融项:
No-4DRot:不使用完整 4D rotation,空间和时间不充分耦合,运动表达能力下降。No-4DSH:不使用时间相关颜色建模,外观随时间变化的表达能力下降。No-Time split:split 时不在时间维度采样,时间维度上的表示密度不足。
核心结论:
4D rotation 负责让 Gaussian 表达运动;
4DSH 负责让颜色随视角和时间变化;
time split 负责让 Gaussian 在时间维度上也能增密。