Phase 04 - Lesson 22
3D Gaussian Splatting do zero
Uma cena é uma nuvem de milhões de Gaussianas 3D. Cada uma tem posição, orientação, escala, opacidade e uma cor que depende da direção de visualização. Rasterize-as, faça backprop através da rasterização, pronto.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 4 Lição 13 (Visão 3D e NeRF), Fase 1 Lição 12 (Operações com Tensores), Fase 4 Lição 10 (Noções de Diffusion, opcional) Tempo: ~90 minutos
Objetivos de aprendizado
- Explicar por que o 3D Gaussian Splatting substituiu o NeRF como padrão de produção para reconstrução 3D fotorrealista em 2026
- Enunciar os seis parâmetros por Gaussiana (posição, quaternion de rotação, escala, opacidade, cor por harmônicos esféricos, feature opcional) e quantos floats cada um contribui
- Implementar do zero um rasterizador de Gaussian splatting 2D usando composição
alpha, e então mostrar como o caso 3D se projeta para o mesmo loop - Usar
nerfstudio,gsplatouSuperSplatpara reconstruir uma cena a partir de 20-50 fotos e exportar para a extensão glTFKHR_gaussian_splattingou o schema OpenUSD 26.03UsdVolParticleField3DGaussianSplat
O problema
Um NeRF armazena uma cena como os pesos de uma MLP. Cada pixel renderizado são centenas de consultas à MLP ao longo de um raio. O treinamento leva horas, a renderização leva segundos, e os pesos não podem ser editados — se você quiser mover uma cadeira dentro de uma cena, precisa retreinar.
O 3D Gaussian Splatting (Kerbl, Kopanas, Leimkühler, Drettakis, SIGGRAPH 2023) substituiu tudo isso. Uma cena é um conjunto explícito de Gaussianas 3D. A renderização é rasterização na GPU a 100+ fps. O treinamento leva minutos. A edição é direta: translade um subconjunto de Gaussianas e você moveu a cadeira. Até 2026 o Khronos Group ratificou uma extensão glTF para Gaussian splats, o OpenUSD 26.03 traz um schema de Gaussian splat, a Zillow e a Apartments.com renderizam imóveis com elas, e a maioria dos novos artigos de pesquisa sobre reconstrução 3D são variantes da ideia central do 3DGS.
O modelo mental é simples, mas a matemática tem partes móveis suficientes para que a maioria das introduções comece pela rasterização e pule as projeções e os harmônicos esféricos. Esta lição constrói a coisa toda — uma versão 2D primeiro, depois a extensão 3D.
O conceito
O que uma Gaussiana carrega
Uma Gaussiana 3D é um blob paramétrico no espaço com estes atributos:
position mu (3,) centre in world coordinates
rotation q (4,) unit quaternion encoding orientation
scale s (3,) log-scales per axis (exponentiated at render time)
opacity alpha (1,) post-sigmoid opacity [0, 1]
SH coefficients c_lm (3 * (L+1)^2,) view-dependent colour
Rotação + escala constroem uma covariância 3x3: Sigma = R S S^T R^T. Essa é a forma da Gaussiana em 3D. Os harmônicos esféricos permitem que a cor mude com a direção de visualização — reflexos especulares, brilho sutil, glow dependente da vista — sem armazenar texturas por vista. Com grau 3 de SH você obtém 16 coeficientes por canal de cor, 48 floats por Gaussiana só para a cor.
Uma cena tipicamente tem 1-5 milhões de Gaussianas. Cada uma armazena cerca de 60 floats (3 + 4 + 3 + 1 + 48 + diversos). Isso dá 240 MB para uma cena de cinco milhões de Gaussianas — muito menor que a nuvem de pontos equivalente com textura por ponto, e uma ordem de magnitude menor que os pesos da MLP de um NeRF re-renderizados em alta resolução.
Rasterização, não ray marching
flowchart LR
SCENE["Milhões de Gaussianas 3D<br/>(posição, rotação, escala,<br/>opacidade, cor SH)"] --> PROJ["Projetar para 2D<br/>(extrínsecos + intrínsecos da câmera)"]
PROJ --> TILES["Atribuir a tiles<br/>(16x16 no espaço de tela)"]
TILES --> SORT["Ordenar por profundidade<br/>por tile"]
SORT --> ALPHA["Compor alpha<br/>de frente para trás"]
ALPHA --> PIX["Cor do pixel"]
style SCENE fill:#dbeafe,stroke:#2563eb
style ALPHA fill:#fef3c7,stroke:#d97706
style PIX fill:#dcfce7,stroke:#16a34a
Cinco passos, todos amigáveis para GPU. Sem consulta à MLP por pixel. Uma única RTX 3080 Ti renderiza 6 milhões de splats a 147 fps.
O passo de projeção
A Gaussiana 3D na posição de mundo mu com covariância 3D Sigma projeta para uma Gaussiana 2D na posição de tela mu' com covariância 2D Sigma':
mu' = project(mu)
Sigma' = J W Sigma W^T J^T (2 x 2)
W = viewing transform (rotation + translation of camera)
J = Jacobian of the perspective projection at mu'
A pegada (footprint) da Gaussiana 2D é uma elipse cujos eixos são os autovetores de Sigma'. Todo pixel dentro dessa elipse recebe a contribuição da Gaussiana, ponderada por exp(-0.5 * (p - mu')^T Sigma'^-1 (p - mu')).
A regra de composição alpha
Para um pixel, as Gaussianas que o cobrem são ordenadas de trás para frente (ou equivalentemente de frente para trás com a fórmula invertida). A cor é composta com a mesma equação de todo rasterizador semitransparente desde os anos 1980:
C_pixel = sum_i alpha_i * T_i * c_i
T_i = prod_{j < i} (1 - alpha_j) transmittance up to i
alpha_i = opacity_i * exp(-0.5 * d^T Sigma'^-1 d) local contribution
c_i = eval_SH(SH_i, view_direction) view-dependent colour
Esta é a mesma equação da renderização volumétrica do NeRF, apenas sobre um conjunto explícito e esparso de Gaussianas em vez de amostras densas ao longo de um raio. Essa identidade é o motivo de a qualidade renderizada igualar o NeRF — ambos estão integrando a mesma equação do campo de radiância.
Por que isso é diferenciável
Cada passo — projeção, atribuição a tiles, composição alpha, avaliação de SH — é diferenciável em relação aos parâmetros das Gaussianas. Dada uma imagem ground-truth, calcule a loss de pixel renderizado, faça backprop através do rasterizador, atualize todos os (mu, q, s, alpha, c_lm) por gradiente descendente. Ao longo de ~30.000 iterações as Gaussianas encontram suas posições, escalas e cores corretas.
Densificação e poda
Um conjunto fixo de Gaussianas não consegue cobrir uma cena complexa. O treinamento inclui dois mecanismos adaptativos:
- Clonar uma Gaussiana em sua posição atual quando a magnitude do gradiente é alta mas sua escala é pequena — a reconstrução precisa de mais detalhe aqui.
- Dividir uma Gaussiana de grande escala em duas menores quando o gradiente é alto — uma Gaussiana grande é suave demais para ajustar a região.
- Podar Gaussianas cuja opacidade cai abaixo de um limiar — elas não estão contribuindo.
A densificação roda a cada N iterações. Uma cena tipicamente cresce de ~100k Gaussianas iniciais (semeadas a partir de pontos de SfM) para 1-5M ao final do treinamento.
Harmônicos esféricos em um parágrafo
A cor dependente da vista é uma função c(direction) na esfera unitária. Os harmônicos esféricos são a base de Fourier da esfera. Trunque no grau L e você obtém (L+1)^2 funções de base por canal. Avaliar a cor para uma nova vista é um produto escalar entre os coeficientes de SH aprendidos e a base avaliada na direção de visualização. Grau 0 = um coeficiente = cor constante. Grau 3 = 16 coeficientes = suficiente para capturar sombreamento Lambertiano, especular e reflexão moderada. Os artigos de SD Gaussian Splatting usam grau 3 por padrão.
A stack de produção de 2026
1. Capture smartphone / DJI drone / handheld scanner
2. SfM / MVS COLMAP or GLOMAP derives camera poses + sparse points
3. Train 3DGS nerfstudio / gsplat / inria official / PostShot (~10-30 min on RTX 4090)
4. Edit SuperSplat / SplatForge (clean floaters, segment)
5. Export .ply -> glTF KHR_gaussian_splatting or .usd (OpenUSD 26.03)
6. View Cesium / Unreal / Babylon.js / Three.js / Vision Pro
Variantes 4D e generativas
- 4D Gaussian Splatting — as Gaussianas são funções do tempo; usado para vídeo volumétrico (Superman 2026, "Helicopter" do A$AP Rocky).
- Splats generativos — modelos text-to-splat (Marble da World Labs) que alucinam cenas inteiras.
- 3D Gaussian Unscented Transform — a variante do NuRec da NVIDIA para simulação de direção autônoma.
Construa
Passo 1: Uma Gaussiana 2D
Primeiro construímos um rasterizador 2D. O caso 3D se reduz a ele após a projeção.
import torch
import torch.nn as nn
import torch.nn.functional as F
def eval_2d_gaussian(means, covs, points):
"""
means: (G, 2) centres
covs: (G, 2, 2) covariance matrices
points: (H, W, 2) pixel coordinates
returns: (G, H, W) density at every pixel for every Gaussian
"""
G = means.size(0)
H, W, _ = points.shape
flat = points.view(-1, 2)
inv = torch.linalg.inv(covs)
diff = flat[None, :, :] - means[:, None, :]
d = torch.einsum("gpi,gij,gpj->gp", diff, inv, diff)
density = torch.exp(-0.5 * d)
return density.view(G, H, W)
einsum faz a forma quadrática diff^T Sigma^-1 diff para cada par (Gaussiana, pixel).
Passo 2: Rasterizador de splatting 2D
Composição alpha de frente para trás. A profundidade em 2D não tem sentido, então usamos um escalar aprendido por Gaussiana para a ordem.
def rasterise_2d(means, covs, colours, opacities, depths, image_size):
"""
means: (G, 2)
covs: (G, 2, 2)
colours: (G, 3)
opacities: (G,) in [0, 1]
depths: (G,) per-Gaussian scalar used for ordering
image_size: (H, W)
returns: (H, W, 3) rendered image
"""
H, W = image_size
yy, xx = torch.meshgrid(
torch.arange(H, dtype=torch.float32, device=means.device),
torch.arange(W, dtype=torch.float32, device=means.device),
indexing="ij",
)
points = torch.stack([xx, yy], dim=-1)
densities = eval_2d_gaussian(means, covs, points)
alphas = opacities[:, None, None] * densities
alphas = alphas.clamp(0.0, 0.99)
order = torch.argsort(depths)
alphas = alphas[order]
colours_sorted = colours[order]
T = torch.ones(H, W, device=means.device)
out = torch.zeros(H, W, 3, device=means.device)
for i in range(means.size(0)):
a = alphas[i]
out += (T * a)[..., None] * colours_sorted[i][None, None, :]
T = T * (1.0 - a)
return out
Não é rápido — uma implementação real usa kernels CUDA baseados em tiles — mas é exatamente a matemática correta e totalmente diferenciável.
Passo 3: Uma cena de splats 2D treinável
class Splats2D(nn.Module):
def __init__(self, num_splats=128, image_size=64, seed=0):
super().__init__()
g = torch.Generator().manual_seed(seed)
H, W = image_size, image_size
self.means = nn.Parameter(torch.rand(num_splats, 2, generator=g) * torch.tensor([W, H]))
self.log_scale = nn.Parameter(torch.ones(num_splats, 2) * math.log(2.0))
self.rot = nn.Parameter(torch.zeros(num_splats)) # single angle in 2D
self.colour_logits = nn.Parameter(torch.randn(num_splats, 3, generator=g) * 0.5)
self.opacity_logit = nn.Parameter(torch.zeros(num_splats))
self.depth = nn.Parameter(torch.rand(num_splats, generator=g))
def covs(self):
s = torch.exp(self.log_scale)
c, si = torch.cos(self.rot), torch.sin(self.rot)
R = torch.stack([
torch.stack([c, -si], dim=-1),
torch.stack([si, c], dim=-1),
], dim=-2)
S = torch.diag_embed(s ** 2)
return R @ S @ R.transpose(-1, -2)
def forward(self, image_size):
covs = self.covs()
colours = torch.sigmoid(self.colour_logits)
opacities = torch.sigmoid(self.opacity_logit)
return rasterise_2d(self.means, covs, colours, opacities, self.depth, image_size)
log_scale, opacity_logit e colour_logits são todos parâmetros irrestritos mapeados pela ativação correta no momento da renderização. Esse é o padrão padrão de toda implementação de 3DGS.
Passo 4: Ajustar Gaussianas 2D a uma imagem-alvo
import math
import numpy as np
def make_target(size=64):
yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
img = np.zeros((size, size, 3), dtype=np.float32)
# Red circle
mask = (xx - 20) ** 2 + (yy - 20) ** 2 < 10 ** 2
img[mask] = [1.0, 0.2, 0.2]
# Blue square
mask = (np.abs(xx - 45) < 8) & (np.abs(yy - 40) < 8)
img[mask] = [0.2, 0.3, 1.0]
return torch.from_numpy(img)
target = make_target(64)
model = Splats2D(num_splats=64, image_size=64)
opt = torch.optim.Adam(model.parameters(), lr=0.05)
for step in range(200):
pred = model((64, 64))
loss = F.mse_loss(pred, target)
opt.zero_grad(); loss.backward(); opt.step()
if step % 40 == 0:
print(f"step {step:3d} mse {loss.item():.4f}")
Ao longo de 200 passos as 64 Gaussianas se acomodam nas duas formas. Essa é a ideia inteira — gradiente descendente sobre primitivas geométricas explícitas.
Passo 5: De 2D para 3D
A extensão 3D mantém o mesmo loop. As adições:
- A rotação por Gaussiana é um quaternion em vez de um único ângulo.
- A covariância é
R S S^T R^TcomRconstruído a partir do quaternion eS = diag(exp(log_scale)). - A projeção
(mu, Sigma) -> (mu', Sigma')usa os extrínsecos da câmera e o Jacobiano da projeção perspectiva emmu. - A cor passa a ser uma expansão em harmônicos esféricos; avalie-a na direção de visualização.
- A ordenação por profundidade vem do z real do espaço de câmera em vez de um escalar aprendido.
Toda implementação de produção (gsplat, inria/gaussian-splatting, nerfstudio) faz exatamente isso na GPU com kernels CUDA baseados em tiles.
Passo 6: Avaliação de harmônicos esféricos
A base de SH até grau 3 tem 16 termos por canal. Avaliação:
def eval_sh_degree_3(sh_coeffs, dirs):
"""
sh_coeffs: (..., 16, 3) last dim is RGB channels
dirs: (..., 3) unit vectors
returns: (..., 3)
"""
C0 = 0.282094791773878
C1 = 0.488602511902920
C2 = [1.092548430592079, 1.092548430592079,
0.315391565252520, 1.092548430592079,
0.546274215296039]
x, y, z = dirs[..., 0], dirs[..., 1], dirs[..., 2]
x2, y2, z2 = x * x, y * y, z * z
xy, yz, xz = x * y, y * z, x * z
result = C0 * sh_coeffs[..., 0, :]
result = result - C1 * y[..., None] * sh_coeffs[..., 1, :]
result = result + C1 * z[..., None] * sh_coeffs[..., 2, :]
result = result - C1 * x[..., None] * sh_coeffs[..., 3, :]
result = result + C2[0] * xy[..., None] * sh_coeffs[..., 4, :]
result = result + C2[1] * yz[..., None] * sh_coeffs[..., 5, :]
result = result + C2[2] * (2.0 * z2 - x2 - y2)[..., None] * sh_coeffs[..., 6, :]
result = result + C2[3] * xz[..., None] * sh_coeffs[..., 7, :]
result = result + C2[4] * (x2 - y2)[..., None] * sh_coeffs[..., 8, :]
# degree 3 terms omitted here for brevity; full 16-coefficient version in the code file
return result
Os sh_coeffs aprendidos armazenam a "cor em cada direção" para aquela Gaussiana. No momento da renderização você avalia em relação à direção de vista atual e obtém um RGB de 3 componentes.
Use
Para trabalho real de 3DGS, use gsplat (Meta) ou nerfstudio:
pip install nerfstudio gsplat
ns-download-data example
ns-train splatfacto --data path/to/data
splatfacto é o treinador de 3DGS do nerfstudio. A execução leva de 10 a 30 minutos em uma RTX 4090 para uma cena típica.
Opções de exportação que importam em 2026:
.ply— nuvem bruta de Gaussianas (portátil, maior arquivo)..splat— formato quantizado do PlayCanvas / SuperSplat.- glTF
KHR_gaussian_splatting— padrão Khronos, portátil entre visualizadores (RC de fevereiro de 2026). - OpenUSD
UsdVolParticleField3DGaussianSplat— nativo de USD, para os pipelines do NVIDIA Omniverse e do Vision Pro.
Para cenas 4D / dinâmicas, 4DGS e Deformable-3DGS estendem a mesma maquinaria com médias e opacidades variáveis no tempo.
Entregue
Esta lição produz:
outputs/prompt-3dgs-capture-planner.md— um prompt que planeja uma sessão de captura (número de fotos, trajetória da câmera, iluminação) para um dado tipo de cena.outputs/skill-3dgs-export-router.md— uma skill que escolhe o formato de exportação correto (.ply/.splat/ glTF / USD) dado o visualizador ou engine de destino.
Exercícios
- (Fácil) Rode o treinador de splats 2D acima em uma imagem sintética diferente. Varie
num_splatsem[16, 64, 256]e plote MSE vs passo para cada um. Identifique o ponto de retornos decrescentes. - (Médio) Estenda o rasterizador 2D para suportar cores RGB por Gaussiana que dependem de um "ângulo de vista" escalar através de um harmônico de grau 2. Treine em um par de imagens-alvo e verifique que o modelo reconstrói ambas.
- (Difícil) Clone o
nerfstudioe treine osplatfactoem uma captura de 20 fotos de qualquer cena que você tenha (mesa, planta, rosto, sala). Exporte para glTFKHR_gaussian_splattinge abra em um visualizador (Three.jsGaussianSplats3D, SuperSplat, Babylon.js V9). Relate o tempo de treinamento, o número de Gaussianas e os fps renderizados.
Termos-chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| 3DGS | "Gaussian splats" | Representação explícita de cena como milhões de Gaussianas 3D com posição, rotação, escala, opacidade e cor SH por Gaussiana |
| Covariância | "Forma da Gaussiana" | Sigma = R S S^T R^T; orientação e escala anisotrópica de uma Gaussiana |
| Composição alpha | "Blend de trás para frente" | A mesma equação da renderização volumétrica do NeRF, agora sobre um conjunto esparso explícito |
| Densificação | "Clonar e dividir" | Adição adaptativa de novas Gaussianas onde a reconstrução está subajustada |
| Poda | "Deletar baixa opacidade" | Remover Gaussianas que colapsaram para opacidade quase nula durante o treinamento |
| Harmônicos esféricos | "Cor dependente da vista" | Base de Fourier na esfera; armazena a cor como função da direção de visualização |
| Splatfacto | "3DGS do nerfstudio" | O caminho mais fácil para treinar 3DGS em 2026 |
KHR_gaussian_splatting |
"Padrão glTF" | Extensão Khronos de 2026 que torna o 3DGS portátil entre visualizadores e engines |
Leitura adicional
- 3D Gaussian Splatting for Real-Time Radiance Field Rendering (Kerbl et al., SIGGRAPH 2023) — o artigo original
- gsplat (Meta/nerfstudio) — rasterizador CUDA de qualidade de produção
- nerfstudio Splatfacto — receita de treinamento de referência
- Khronos KHR_gaussian_splatting extension — o formato portátil de 2026
- OpenUSD 26.03 release notes — schema
UsdVolParticleField3DGaussianSplat - THE FUTURE 3D State of Gaussian Splatting 2026 — visão geral da indústria