Phase 04 - Lesson 22
3D Gaussian Splatting desde cero
Una escena es una nube de millones de Gaussianas 3D. Cada una tiene posición, orientación, escala, opacidad y un color que depende de la dirección de visualización. Rasterízalas, haz backprop a través de la rasterización, listo.
Tipo: Build Lenguajes: Python Prerrequisitos: Fase 4 Lección 13 (Visión 3D y NeRF), Fase 1 Lección 12 (Operaciones con Tensores), Fase 4 Lección 10 (Nociones de Diffusion, opcional) Tiempo: ~90 minutos
Objetivos de aprendizaje
- Explicar por qué el 3D Gaussian Splatting reemplazó al NeRF como el estándar de producción para la reconstrucción 3D fotorrealista en 2026
- Enunciar los seis parámetros por Gaussiana (posición, cuaternión de rotación, escala, opacidad, color por armónicos esféricos, feature opcional) y cuántos floats aporta cada uno
- Implementar desde cero un rasterizador de Gaussian splatting 2D usando composición
alpha, y luego mostrar cómo el caso 3D se proyecta al mismo bucle - Usar
nerfstudio,gsplatoSuperSplatpara reconstruir una escena a partir de 20-50 fotos y exportar a la extensión glTFKHR_gaussian_splattingo al schema OpenUSD 26.03UsdVolParticleField3DGaussianSplat
El problema
Un NeRF almacena una escena como los pesos de una MLP. Cada pixel renderizado son cientos de consultas a la MLP a lo largo de un rayo. El entrenamiento toma horas, el renderizado toma segundos, y los pesos no se pueden editar — si quieres mover una silla dentro de una escena, tienes que reentrenar.
El 3D Gaussian Splatting (Kerbl, Kopanas, Leimkühler, Drettakis, SIGGRAPH 2023) reemplazó todo eso. Una escena es un conjunto explícito de Gaussianas 3D. El renderizado es rasterización en GPU a 100+ fps. El entrenamiento toma minutos. La edición es directa: traslada un subconjunto de Gaussianas y habrás movido la silla. Para 2026 el Khronos Group ratificó una extensión glTF para Gaussian splats, OpenUSD 26.03 trae un schema de Gaussian splat, Zillow y Apartments.com renderizan bienes raíces con ellas, y la mayoría de los nuevos artículos de investigación sobre reconstrucción 3D son variantes de la idea central del 3DGS.
El modelo mental es simple, pero las matemáticas tienen suficientes partes móviles como para que la mayoría de las introducciones empiecen por la rasterización y se salten las proyecciones y los armónicos esféricos. Esta lección construye todo el asunto — una versión 2D primero, luego la extensión 3D.
El concepto
Qué lleva una Gaussiana
Una Gaussiana 3D es un blob paramétrico en el espacio con estos 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
Rotación + escala construyen una covarianza 3x3: Sigma = R S S^T R^T. Esa es la forma de la Gaussiana en 3D. Los armónicos esféricos permiten que el color cambie con la dirección de visualización — reflejos especulares, brillo sutil, glow dependiente de la vista — sin almacenar texturas por vista. Con grado 3 de SH obtienes 16 coeficientes por canal de color, 48 floats por Gaussiana solo para el color.
Una escena suele tener 1-5 millones de Gaussianas. Cada una almacena alrededor de 60 floats (3 + 4 + 3 + 1 + 48 + varios). Eso da 240 MB para una escena de cinco millones de Gaussianas — mucho más pequeña que la nube de puntos equivalente con textura por punto, y un orden de magnitud más pequeña que los pesos de la MLP de un NeRF re-renderizados a alta resolución.
Rasterización, no ray marching
flowchart LR
SCENE["Millones de Gaussianas 3D<br/>(posición, rotación, escala,<br/>opacidad, color SH)"] --> PROJ["Proyectar a 2D<br/>(extrínsecos + intrínsecos de la cámara)"]
PROJ --> TILES["Asignar a tiles<br/>(16x16 en espacio de pantalla)"]
TILES --> SORT["Ordenar por profundidad<br/>por tile"]
SORT --> ALPHA["Componer alpha<br/>de adelante hacia atrás"]
ALPHA --> PIX["Color del pixel"]
style SCENE fill:#dbeafe,stroke:#2563eb
style ALPHA fill:#fef3c7,stroke:#d97706
style PIX fill:#dcfce7,stroke:#16a34a
Cinco pasos, todos amigables para GPU. Sin consulta a la MLP por pixel. Una sola RTX 3080 Ti renderiza 6 millones de splats a 147 fps.
El paso de proyección
La Gaussiana 3D en la posición de mundo mu con covarianza 3D Sigma proyecta a una Gaussiana 2D en la posición de pantalla mu' con covarianza 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'
La huella (footprint) de la Gaussiana 2D es una elipse cuyos ejes son los autovectores de Sigma'. Cada pixel dentro de esa elipse recibe la contribución de la Gaussiana, ponderada por exp(-0.5 * (p - mu')^T Sigma'^-1 (p - mu')).
La regla de composición alpha
Para un pixel, las Gaussianas que lo cubren se ordenan de atrás hacia adelante (o equivalentemente de adelante hacia atrás con la fórmula invertida). El color se compone con la misma ecuación de todo rasterizador semitransparente desde los años 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 es la misma ecuación del renderizado volumétrico del NeRF, solo que sobre un conjunto explícito y disperso de Gaussianas en vez de muestras densas a lo largo de un rayo. Esa identidad es la razón de que la calidad renderizada iguale al NeRF — ambos están integrando la misma ecuación del campo de radiancia.
Por qué esto es diferenciable
Cada paso — proyección, asignación a tiles, composición alpha, evaluación de SH — es diferenciable con respecto a los parámetros de las Gaussianas. Dada una imagen ground-truth, calcula la loss de pixel renderizado, haz backprop a través del rasterizador, actualiza todos los (mu, q, s, alpha, c_lm) por descenso de gradiente. A lo largo de ~30.000 iteraciones las Gaussianas encuentran sus posiciones, escalas y colores correctos.
Densificación y poda
Un conjunto fijo de Gaussianas no puede cubrir una escena compleja. El entrenamiento incluye dos mecanismos adaptativos:
- Clonar una Gaussiana en su posición actual cuando la magnitud del gradiente es alta pero su escala es pequeña — la reconstrucción necesita más detalle aquí.
- Dividir una Gaussiana de gran escala en dos más pequeñas cuando su gradiente es alto — una Gaussiana grande es demasiado suave para ajustar la región.
- Podar Gaussianas cuya opacidad cae por debajo de un umbral — no están contribuyendo.
La densificación corre cada N iteraciones. Una escena suele crecer de ~100k Gaussianas iniciales (sembradas a partir de puntos de SfM) a 1-5M al final del entrenamiento.
Armónicos esféricos en un párrafo
El color dependiente de la vista es una función c(direction) en la esfera unitaria. Los armónicos esféricos son la base de Fourier de la esfera. Trunca en el grado L y obtienes (L+1)^2 funciones de base por canal. Evaluar el color para una nueva vista es un producto punto entre los coeficientes de SH aprendidos y la base evaluada en la dirección de visualización. Grado 0 = un coeficiente = color constante. Grado 3 = 16 coeficientes = suficiente para capturar sombreado Lambertiano, especular y reflexión moderada. Los artículos de SD Gaussian Splatting usan grado 3 por defecto.
El stack de producción 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 y generativas
- 4D Gaussian Splatting — las Gaussianas son funciones del tiempo; se usa para video volumétrico (Superman 2026, "Helicopter" de A$AP Rocky).
- Splats generativos — modelos text-to-splat (Marble de World Labs) que alucinan escenas enteras.
- 3D Gaussian Unscented Transform — la variante del NuRec de NVIDIA para simulación de conducción autónoma.
Constrúyelo
Paso 1: Una Gaussiana 2D
Primero construimos un rasterizador 2D. El caso 3D se reduce a él tras la proyección.
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 hace la forma cuadrática diff^T Sigma^-1 diff para cada par (Gaussiana, pixel).
Paso 2: Rasterizador de splatting 2D
Composición alpha de adelante hacia atrás. La profundidad en 2D no tiene sentido, así que usamos un escalar aprendido por Gaussiana para el orden.
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
No es rápido — una implementación real usa kernels CUDA basados en tiles — pero son exactamente las matemáticas correctas y totalmente diferenciable.
Paso 3: Una escena de splats 2D entrenable
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 y colour_logits son todos parámetros sin restricción mapeados por la activación correcta en el momento del renderizado. Ese es el patrón estándar de toda implementación de 3DGS.
Paso 4: Ajustar Gaussianas 2D a una imagen objetivo
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}")
A lo largo de 200 pasos las 64 Gaussianas se acomodan en las dos formas. Esa es toda la idea — descenso de gradiente sobre primitivas geométricas explícitas.
Paso 5: De 2D a 3D
La extensión 3D mantiene el mismo bucle. Las adiciones:
- La rotación por Gaussiana es un cuaternión en vez de un único ángulo.
- La covarianza es
R S S^T R^TconRconstruido a partir del cuaternión yS = diag(exp(log_scale)). - La proyección
(mu, Sigma) -> (mu', Sigma')usa los extrínsecos de la cámara y el Jacobiano de la proyección perspectiva enmu. - El color pasa a ser una expansión en armónicos esféricos; evalúala en la dirección de visualización.
- El ordenamiento por profundidad viene de la z real del espacio de cámara en vez de un escalar aprendido.
Toda implementación de producción (gsplat, inria/gaussian-splatting, nerfstudio) hace exactamente esto en la GPU con kernels CUDA basados en tiles.
Paso 6: Evaluación de armónicos esféricos
La base de SH hasta grado 3 tiene 16 términos por canal. Evaluación:
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
Los sh_coeffs aprendidos almacenan el "color en cada dirección" para esa Gaussiana. En el momento del renderizado evalúas con respecto a la dirección de vista actual y obtienes un RGB de 3 componentes.
Úsalo
Para trabajo real de 3DGS, usa gsplat (Meta) o nerfstudio:
pip install nerfstudio gsplat
ns-download-data example
ns-train splatfacto --data path/to/data
splatfacto es el entrenador de 3DGS de nerfstudio. La ejecución toma de 10 a 30 minutos en una RTX 4090 para una escena típica.
Opciones de exportación que importan en 2026:
.ply— nube cruda de Gaussianas (portátil, archivo más grande)..splat— formato cuantizado de PlayCanvas / SuperSplat.- glTF
KHR_gaussian_splatting— estándar Khronos, portátil entre visualizadores (RC de febrero de 2026). - OpenUSD
UsdVolParticleField3DGaussianSplat— nativo de USD, para los pipelines de NVIDIA Omniverse y Vision Pro.
Para escenas 4D / dinámicas, 4DGS y Deformable-3DGS extienden la misma maquinaria con medias y opacidades variables en el tiempo.
Entrégalo
Esta lección produce:
outputs/prompt-3dgs-capture-planner.md— un prompt que planifica una sesión de captura (número de fotos, trayectoria de cámara, iluminación) para un tipo de escena dado.outputs/skill-3dgs-export-router.md— una skill que elige el formato de exportación correcto (.ply/.splat/ glTF / USD) según el visualizador o engine de destino.
Ejercicios
- (Fácil) Ejecuta el entrenador de splats 2D de arriba en una imagen sintética diferente. Varía
num_splatsen[16, 64, 256]y grafica MSE vs paso para cada uno. Identifica el punto de rendimientos decrecientes. - (Medio) Extiende el rasterizador 2D para soportar colores RGB por Gaussiana que dependan de un "ángulo de vista" escalar a través de un armónico de grado 2. Entrena en un par de imágenes objetivo y verifica que el modelo reconstruye ambas.
- (Difícil) Clona
nerfstudioy entrenasplatfactoen una captura de 20 fotos de cualquier escena que tengas (escritorio, planta, rostro, habitación). Exporta a glTFKHR_gaussian_splattingy ábrelo en un visualizador (Three.jsGaussianSplats3D, SuperSplat, Babylon.js V9). Reporta el tiempo de entrenamiento, el número de Gaussianas y los fps renderizados.
Términos clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| 3DGS | "Gaussian splats" | Representación explícita de escena como millones de Gaussianas 3D con posición, rotación, escala, opacidad y color SH por Gaussiana |
| Covarianza | "Forma de la Gaussiana" | Sigma = R S S^T R^T; orientación y escala anisotrópica de una Gaussiana |
| Composición alpha | "Blend de atrás hacia adelante" | La misma ecuación del renderizado volumétrico del NeRF, ahora sobre un conjunto disperso explícito |
| Densificación | "Clonar y dividir" | Adición adaptativa de nuevas Gaussianas donde la reconstrucción está subajustada |
| Poda | "Borrar baja opacidad" | Eliminar Gaussianas que colapsaron a opacidad casi nula durante el entrenamiento |
| Armónicos esféricos | "Color dependiente de la vista" | Base de Fourier en la esfera; almacena el color como función de la dirección de visualización |
| Splatfacto | "3DGS de nerfstudio" | El camino más fácil para entrenar 3DGS en 2026 |
KHR_gaussian_splatting |
"Estándar glTF" | Extensión Khronos de 2026 que hace al 3DGS portátil entre visualizadores y engines |
Lecturas adicionales
- 3D Gaussian Splatting for Real-Time Radiance Field Rendering (Kerbl et al., SIGGRAPH 2023) — el artículo original
- gsplat (Meta/nerfstudio) — rasterizador CUDA de calidad de producción
- nerfstudio Splatfacto — receta de entrenamiento de referencia
- Khronos KHR_gaussian_splatting extension — el formato portátil de 2026
- OpenUSD 26.03 release notes — schema
UsdVolParticleField3DGaussianSplat - THE FUTURE 3D State of Gaussian Splatting 2026 — visión general de la industria