VerifiedarXiv:2308.0407924 min
Graphics · Radiance fields

3D Gaussian Splatting for Real-Time Radiance Field Rendering

Store a scene as millions of tiny 3D Gaussians, and a GPU can render new views of it in real time.

NeRF made novel-view synthesis photorealistic but slow, because it queries a neural network millions of times per frame. This paper keeps the quality, throws the network out of the render loop, and hits over 100 frames per second by projecting sorted Gaussians onto the screen and blending them.

Explaining the paper3D Gaussian Splatting for Real-Time Radiance Field RenderingKerbl, Kopanas, Leimkühler, Drettakis · Inria / MPI · SIGGRAPH 2023 · arXiv:2308.04079

A high-quality neural radiance field renders one frame in about seventeen seconds. This paper renders the same scenes, at comparable quality, more than a thousand times faster, and it trains in forty minutes instead of two days.

Start with the problem the field is trying to solve, novel-view synthesis. You take a few dozen photos of a scene from different positions, and you want to produce an image from a new camera you never actually placed there, correct down to the reflections and the way a leaf occludes what is behind it. Solve it well and you can fly a virtual camera through a place that was captured with an ordinary phone.

NeRF cracked the quality by storing the scene implicitly: a small neural network that, given a 3D point and a viewing direction, returns a color and a density. To render one pixel you march a ray through the scene and query that network at dozens of points along it, then blend the results. The quality is the best in the field and the rendering is very slow. A ray per pixel, dozens of network calls per ray, millions of pixels per frame, which is why the best-quality method in this paper's comparison, Mip-NeRF360, renders a single frame in roughly seventeen seconds (0.06 frames per second) and takes 48 GPU-hours to train.

Later methods bought speed by replacing the network with a lookup grid (a voxel or hash grid you interpolate). Training drops to minutes, but rendering still marches rays through mostly-empty space, and quality slips. Before this paper, no method rendered unbounded, complete scenes at 1080p and real-time rates (30 frames per second or more) without giving up quality.

3D Gaussian Splatting closes that gap by dropping the implicit representation entirely. A scene becomes an explicit cloud of a few million little translucent 3D blobs, each a Gaussian with its own position, shape, color, and opacity. To render, you project every blob onto the screen as a fuzzy ellipse, sort the ellipses by depth, and blend them front to back. There is no network in that loop, and a GPU runs it fast: 134 frames per second at SOTA-competitive quality, trained in 41 minutes. The old choice was quality or speed. This gets both.

A handful of ideas carry the method, and none of them is heavy on its own: what a rendered pixel actually is (a front-to-back blend of translucent primitives), what the primitive is (an anisotropic 3D Gaussian) and how you keep its shape valid, how you project it to the screen, how you color it so it changes with the view, how a GPU blends millions of them fast, and how the optimizer creates and destroys Gaussians so the count fits the scene. Take them in order.

A pixel is a stack of translucent splats

Before any Gaussians, settle what a pixel's color even is when you render a scene made of translucent stuff. Look along the ray through one pixel. It passes through a sequence of primitives, each with a color ci\mathbf{c}_i and an opacity αi[0,1]\alpha_i \in [0,1] (0 is invisible, 1 is fully solid). The pixel's color is the front-to-back accumulation over that ordered list:

C=i=1NTiαici,Ti=j=1i1(1αj)C = \sum_{i=1}^{N} T_i\,\alpha_i\,\mathbf{c}_i, \qquad T_i = \prod_{j=1}^{i-1}\big(1 - \alpha_j\big)
(2)

The symbol to pin down is TiT_i, the transmittance: the fraction of light that survives all the way to primitive ii without being absorbed by anything in front of it. Each primitive jj ahead lets a fraction (1αj)(1-\alpha_j) of light through, and those survivals multiply, so TiT_i is the running product of "one minus opacity" over everything nearer the camera. Primitive ii then contributes its color weighted by how visible it is, TiαiT_i\,\alpha_i. Think of a stack of tinted, faintly glowing glass panes: each pane passes (1α)(1-\alpha) of the light and adds its own glow αc\alpha\,\mathbf{c}, and TiT_i is how much light is still travelling when it reaches pane ii. (The analogy holds for ordered, semi-transparent layers; it breaks for true scattering, where light would bounce sideways between panes rather than march straight through.)

Two consequences follow, and both matter later. Once enough opaque stuff has accumulated, TiT_i drops to nearly zero and everything behind it contributes nothing, so a renderer can stop early the moment a pixel saturates. And because each layer only multiplies and adds, the sum is differentiable in every αi\alpha_i and ci\mathbf{c}_i, which lets you optimize the scene by gradient descent.

This is not a new model, and the reuse is deliberate. It is exactly the image-formation model NeRF uses, which itself discretizes the classical emission-absorption integral from volume rendering. NeRF gathers the list by marching a ray and asking its network for a density at each step; 3D Gaussian Splatting gathers the list by projecting its Gaussians and sorting them. Same blend, different way of collecting the layers. In the figure below, a single ray runs through a teal front surface and an amber one behind it. Drag the front surface's density and watch two things: how far down the ray the transmittance T survives, and where along the ray the pixel's color actually comes from.

Figure 1 · the image-formation model
0.82
A camera ray through a front and a back surface. The teal curve is transmittance T falling from 1 as light is absorbed; the violet area is each point's weight TiαiT_i\alpha_i in the final pixel. Make the front opaque and the weight collapses onto it, occluding the back; make it thin and the weight splits. The same blend NeRF uses.

One thing the paper leaves as a trap for the careful reader: where does αi\alpha_i come from? In NeRF it is αi=1eσiδi\alpha_i = 1 - e^{-\sigma_i\delta_i}, built from a density σ\sigma and a ray step δ\delta. 3D Gaussian Splatting has no σ\sigma and no ray steps. Its αi\alpha_i is a learned opacity times the primitive's 2D Gaussian falloff evaluated at the pixel, which the next two sections build. The blending algebra above is shared; the source of α\alpha is not. Keep the two apart and the rest of the method reads cleanly.

The primitive: an anisotropic 3D Gaussian

Now the primitive itself. Each one is a 3D Gaussian: a smooth blob centered at a point μ\boldsymbol\mu in space, fading out with distance from that center. Written out, its value at a point x\mathbf{x} is

G(x)=exp ⁣(12(xμ)Σ1(xμ))G(\mathbf{x}) = \exp\!\Big(-\tfrac{1}{2}(\mathbf{x}-\boldsymbol\mu)^\top \Sigma^{-1} (\mathbf{x}-\boldsymbol\mu)\Big)
(4)

The 3×33\times 3 matrix Σ\Sigma, the covariance, is the blob's shape: it says how far the Gaussian reaches along each direction and how those directions tilt, which geometrically is an ellipsoid. Notice what is not here: the usual (2π)3/2Σ1/2(2\pi)^{-3/2}|\Sigma|^{-1/2} normalizing constant that would make GG integrate to one. That omission is intentional. This Gaussian is a spatial weight that peaks at 1, not a probability density; how solid the blob is comes from a separately learned opacity, so pinning the peak to a fixed height and letting opacity set the strength keeps the two jobs from fighting.

Crucially, the ellipsoid is anisotropic: it can be stretched and rotated, long in one direction and thin in another, rather than forced to a uniform sphere, and that fit to real geometry is why a Gaussian is the right primitive. A thin structure, the edge of a table or a railing or a blade of grass, is covered by a single long, flat splat aligned to it, where a pile of round blobs would either miss the ends or spill off the sides and blur it. The ablation is blunt about it: force every Gaussian to be an isotropic sphere and quality drops (average PSNR on the ablation scenes falls from 26.05 to 25.23, with visibly smeared fine detail) at the same Gaussian count. Below, shape one splat over a thin diagonal strip of geometry: stretch and rotate it, then flip between anisotropic and isotropic and watch the coverage and waste readouts respond.

Figure 2 · anisotropic covers thin structure
4.6×
34°
A thin strip of geometry. Shape one Gaussian splat by its rotation and its two axis scales (that is Σ=RSSR\Sigma = RSS^\top R^\top), keeping area fixed. Aligned and stretched, one anisotropic splat covers the strip with little waste; forced to a circle it cannot, and it takes many round splats to tile the same edge. Same number of Gaussians, better fit.

So the optimizer needs to move Σ\Sigma around. One constraint shapes the design here: a covariance matrix must be positive semi-definite (its ellipsoid must have non-negative widths in every direction, or it is not a real shape at all). If you handed all six independent entries of Σ\Sigma to gradient descent, a single step would happily produce an invalid matrix with a negative variance, and the blob would become nonsense. The obvious fix, projecting back to the valid set after each step, is exactly the kind of constraint plain SGD handles badly.

The paper sidesteps it by never optimizing Σ\Sigma directly. It factors the shape the way you would describe an ellipsoid to someone: a rotation and a set of axis lengths.

Σ=RSSR,S=diag(s1,s2,s3),R=R(q)\Sigma = R\,S\,S^\top R^\top, \qquad S = \operatorname{diag}(s_1, s_2, s_3), \quad R = R(q)
(6)

It stores a scale vector ss (three positive stretches, one per axis) and a unit quaternion qq (a rotation), and rebuilds Σ\Sigma from them each step. This form is positive semi-definite for any ss and qq you throw at it, because vΣv=SRv20\mathbf{v}^\top\Sigma\mathbf{v} = \lVert S^\top R^\top \mathbf{v}\rVert^2 \ge 0, so the optimizer is fenced inside the legal region without a projection step. It costs nothing in expressiveness: the eigenvalues of Σ\Sigma come out as the squared scales si2s_i^2 and its axes as the columns of RR, so (s,q)(s, q) spans exactly the same six degrees of freedom as a general symmetric 3×33\times 3. Store three stretch dials and one rotation dial, and you cannot dial in an impossible shape.

Where do the first Gaussians come from? Calibrating the cameras is itself done by Structure-from-Motion (COLMAP), which matches features across the photos and solves for the camera poses; as a by-product it drops a sparse point cloud roughly where the real surfaces are. 3D Gaussian Splatting seeds one small isotropic Gaussian at each of those points (its initial size set by the distance to its nearest neighbors), colored to match. That cloud is a scaffold, not the answer. The optimizer then stretches, recolors, splits, and prunes it into the final scene.

Projecting a Gaussian to the screen

To blend a Gaussian into a pixel you need its footprint on the screen, a 2D ellipse, not the 3D ellipsoid. This projection is where "splatting" gets its name: each blob is splatted onto the image as a soft elliptical stamp.

A Gaussian has a convenient property: push it through any linear map and it stays a Gaussian, with its covariance transformed as AΣAA\Sigma A^\top. The world-to-camera transform is linear, so that part is clean. Perspective projection is not linear, though (it divides by depth), and a perspective image of a Gaussian is not exactly a Gaussian. The fix, from EWA splatting, is to approximate the projection near each blob's center by its local linear part, the Jacobian JJ. Chaining the viewing transform WW and that Jacobian gives the screen-space covariance:

Σ=JWΣWJ\Sigma' = J\,W\,\Sigma\,W^\top J^\top
(5)

That gives a 3×33\times 3 covariance in camera space; drop its third row and column and you have the 2×22\times 2 covariance of the ellipse on the image plane. That last step is exact given the linearization; the only approximation in the projection is JJ, the local affine stand-in for perspective, and it is a good one because each splat is small, so perspective barely bends across it. Painting an ellipse on a rubber sheet and stretching the sheet is a fair picture: the ellipse's spread matrix transforms as A()AA(\cdot)A^\top, and here the stretch is A=JWA = JW.

With the 2D ellipse in hand, the opacity from the last section is finally concrete. At a pixel offset d\mathbf{d} from the splat's projected center, the contribution is the learned opacity oio_i times the 2D Gaussian evaluated there:

αi=oiexp ⁣(12dΣ1d)\alpha_i = o_i \cdot \exp\!\Big(-\tfrac{1}{2}\,\mathbf{d}^\top {\Sigma'}^{-1}\mathbf{d}\Big)

That αi\alpha_i feeds the blend of Figure 1. The splat is brightest at its center and fades with the elliptical falloff of Σ\Sigma', and oio_i (squashed through a sigmoid, so it always lands in (0,1)(0,1)) scales how solid the splat is.

Color that depends on the view

A surface does not look the same from every angle. A waxed floor throws a highlight that slides as you move; a gilded frame flips from dark to bright. So each Gaussian's color has to be a function of the viewing direction, not one fixed RGB. NeRF handles this by feeding the direction into its network. 3D Gaussian Splatting will not put a network in the render loop, so it needs a cheaper trick, and it borrows the one Plenoxels and Instant-NGP used: spherical harmonics.

Spherical harmonics are a Fourier series for directions: a fixed set of basis functions on the sphere, ordered from smooth to wiggly, so that a few coefficients times those bases reconstruct a smooth function of direction. Give each Gaussian a handful of coefficients per color channel and evaluate them at the view direction, a plain dot product, and you get the RGB it shows from that angle:

c(d)==03m=kmYm(d)\mathbf{c}(\mathbf{d}) = \sum_{\ell=0}^{3}\sum_{m=-\ell}^{\ell} \mathbf{k}_\ell^{\,m}\, Y_\ell^{\,m}(\mathbf{d})

The first basis, band 0, is a constant, so its coefficient is the flat base color the Gaussian shows from everywhere, its diffuse appearance. Each higher band adds gentler directional variation, a lobe that brightens as your view lines up with it, which is how a specular glint appears. The paper uses 4 bands (degrees 0 through 3), which is 16 coefficients per channel, 48 numbers in all, and it adds them one band at a time (a new band every 1000 iterations) so the base color is settled before the higher bands add shine. Below, orbit the view and raise the SH degree from 0 to 3 to watch the flat base color grow a view-dependent highlight.

Figure 3 · spherical-harmonic color
ℓ = 3
320°
One Gaussian's color as a function of view direction, built from spherical harmonics. At degree 0 the ring is a constant circle, diffuse and the same from every angle. Raise the degree and a lobe grows, so the color becomes view-dependent and a near-white specular highlight flares as your view lines up with it. All evaluated as a dot product, no network.

Four bands buy the smooth, low-frequency view-dependence that most surfaces need, at almost no cost. What they cannot buy is a sharp mirror: a crisp reflection is high-frequency in direction and would need far more bands than this, so highly reflective surfaces stay a known weak spot. Keeping color a cheap dot product instead of a network call is what wins the speed and gives up the sharp mirror.

The fast tile rasterizer

Now assemble it: a few million splats, each with a screen ellipse, an opacity, and a color, blended front to back per Figure 1. Done naively, that is where the speed dies. Sorting every pixel's overlapping splats by depth, independently, is far too much work millions of times over. The rasterizer is the engineering that makes it real-time, and it turns on one decision: sort once, for the entire image, not per pixel.

Split the screen into 16×1616\times 16-pixel tiles. Duplicate each splat into every tile its ellipse touches, and give each copy a single 64-bit key with the tile ID in the high bits and the depth in the low bits. One GPU radix sort over those keys buckets everything by tile and, within each tile, orders it front to back in the same pass, like sorting mail by a combined ZIP-plus-house-number label. Each tile then walks its own short, already-sorted list, accumulating color and multiplying down the transmittance, and a pixel's thread quits the instant TT falls below 10410^{-4}, so the splats hidden behind a wall cost nothing. Pick a tile below and walk its depth-sorted list to see where the tile's color comes from.

Figure 4 · one global sort, then blend per tile
·
The screen is cut into tiles; every splat is sorted once by (tile, depth). Tap a tile to see its depth-sorted splats, then play or scrub the front-to-back blend: transmittance T falls as each splat is composited, and once it saturates the occluded splats behind it are skipped. The sort runs once per image, never per pixel.

Sharing one depth order across a whole tile makes the blend approximate, since two pixels in the same tile might technically want slightly different orders. It stops mattering once splats shrink toward pixel size, which the optimizer drives them to, and the paper reports no visible artifacts from it in converged scenes. That single approximation trades a small, invisible error for real-time speed.

Training needs the backward pass, and here the paper makes a pointed choice against the grain of earlier work. Pulsar and its kin cap how many front splats per pixel receive gradients (their default is a handful); the ablation shows that a cap of ten collapses quality by 11 dB, because a pixel deep in foliage needs gradients flowing to far more than ten overlapping blobs. 3D Gaussian Splatting puts no cap on it. To do that without storing a per-pixel list, it re-walks each tile's sorted splats back to front in the backward pass and recovers the transmittance in front of each splat from a single stored number: it keeps the final transmittance Tfinal=i(1αi)T_\text{final} = \prod_i (1-\alpha_i) and divides back out one factor of (1αi)(1-\alpha_i) per step as it walks. One number per pixel, gradients for every splat.

Training: render, compare, correct

Every step so far is differentiable, so training is the obvious loop: render an image the scene predicts for a training camera, compare it to the real photo, and let the gradient nudge every Gaussian's position, scale, rotation, opacity, and color. The comparison is two graders blended:

L=(1λ)L1+λLD-SSIM,λ=0.2\mathcal{L} = (1-\lambda)\,\mathcal{L}_1 + \lambda\,\mathcal{L}_{\text{D-SSIM}}, \qquad \lambda = 0.2
(7)

L1\mathcal{L}_1 is the plain mean absolute error, pixel by pixel: is each dot roughly the right color? LD-SSIM=1SSIM\mathcal{L}_{\text{D-SSIM}} = 1 - \text{SSIM} scores the structural match over little 11×1111\times 11 windows: do the edges, contrast, and texture look right? The 0.20.2 weight leans mostly on L1\mathcal{L}_1 and adds a fifth of structural pressure, which is enough to sharpen edges without letting the structural term dominate. (One number to keep straight: the D-SSIM term here is 1SSIM1-\text{SSIM}, not the (1SSIM)/2(1-\text{SSIM})/2 you will see elsewhere; the released code uses the full difference.)

The activations are chosen so the optimizer stays in valid territory without constraints. Opacity passes through a sigmoid, so it is always in (0,1)(0,1); each scale passes through an exponential, so it is always positive and its gradient is smooth across orders of magnitude; the quaternion is renormalized to unit length after each step. Positions get a learning rate that decays over training (start coarse, settle fine); the other parameters use fixed rates. Every Gaussian carries roughly 59 numbers, its position (3), scale (3), rotation (4), opacity (1), and SH color (48), and a few million of them make up a scene.

# one optimization step (Adam), interleaved with density control
image = rasterize(gaussians, camera)     # project, sort once, blend
loss  = 0.8*l1(image, gt) + 0.2*(1 - ssim(image, gt))   # Eq (7)
loss.backward()                          # grads to every Gaussian
opt.step()                               # pos, scale, quat, opacity, SH

if step % 100 == 0 and 500 <= step <= 15000:  # density control
    g = mean_screen_space_grad(gaussians)     # per-Gaussian, in pixels
    clone(gaussians[(g > 2e-4) & small])      # under-reconstruction
    split(gaussians[(g > 2e-4) & large])      # over-reconstruction, s /= 1.6
    prune(opacity < 0.005 or footprint_too_big)
if step % 3000 == 0:
    opacity = min(opacity, 0.01)              # reset: re-earn visibility

Once trained, rendering a new view runs the top loop again, forward: project, sort once, blend. That inner loop has no network in it, which is the entire reason it runs at over 100 frames per second.

# render one frame: there is no neural network in this loop
for g in gaussians:
    mu2d, cov2d = project(g, camera)     # Eq (5): Sigma' = J W Sigma W^T J^T
    color       = eval_sh(g.sh, view_dir)   # view-dependent color, a dot product
    for tile in tiles_covered(mu2d, cov2d):    # 16x16 px tiles
        emit(key = (tile << 32) | depth_bits, g)   # one 64-bit key per splat

radix_sort(keys)                         # ONE global sort: by tile, then depth

for tile in screen:                      # each tile blends front to back
    T = 1
    for g in tile.sorted_splats:
        a  = g.opacity * gaussian_2d(pixel, mu2d, cov2d)   # per-pixel alpha
        C += T * a * color;  T *= (1 - a)
        if T < 1e-4: break               # saturated: skip the occluded rest

Creating and destroying Gaussians

Gradient descent can slide, stretch, and fade the Gaussians it has, but it cannot change how many there are. Start from a sparse SfM cloud and no amount of nudging will grow the splats needed to resolve a thin railing or fill a blank wall. So the paper interleaves adaptive density control: every 100 iterations (over iterations 500 to 15000) it adds and removes Gaussians based on which ones are poorly fit.

The tell that a region is under-reconstructed is a large gradient on a splat's screen-space position: the gradient keeps pushing the blob toward geometry it cannot cover, and that shows up as a big average positional gradient. Any Gaussian whose average exceeds τpos=0.0002\tau_{\text{pos}} = 0.0002 gets densified, and its size decides how. A small splat in an under-covered spot means missing geometry, so clone it: make a copy. (The paper's text says to offset the copy along the positional gradient; the released code places the clone at the exact same position and lets the subsequent optimization pull the two apart, which matters if you read the code expecting the offset.) A large splat sitting on top of fine detail means one blob is smeared across structure it should not own, so split it into two, each with its scale divided by ϕ=1.6\phi = 1.6 and its center drawn from the parent's own Gaussian. Below, pick a case and apply it to see the clone or split happen.

Figure 5 · clone and split
Adaptive density control, driven by the screen-space position gradient. Under-reconstruction: a small splat cannot cover a thin bar, so it is cloned. Over-reconstruction: one big splat is smeared across a textured patch, so it is split into two with their scale divided by 1.6. Press densify to apply the move and watch the count go up.

Splitting is described in the paper as conserving total volume, but two children at 1/1.61/1.6 the scale hold only about 2/1.630.492/1.6^3 \approx 0.49 of the parent's volume, so a split actually shrinks total volume, which is what you want when you are trying to resolve finer detail rather than fatten up. The factor 1.61.6 is 0.80.8 times the split count of 2 and was tuned by hand, not derived.

Left alone, this would only ever add Gaussians. Two culls keep the count in check. Any Gaussian whose opacity falls below 0.0050.005 is pruned, along with any that grows too large in world or screen space. And every 3000 iterations the optimizer runs a kind of amnesty: it forces every opacity down near zero (to min(α,0.01)\min(\alpha, 0.01)) and makes each Gaussian earn its visibility back from the images over the next steps. The blobs that are actually supporting the reconstruction climb back up; floaters and stray splats near the cameras, which nothing is asking for, never recover and get swept away. Between growth and these culls, the count breathes to wherever the scene needs it, 1 to 5 million Gaussians for the real scenes tested.

Real-time, and where it breaks

The headline is the top-right corner of a plot everyone in this field knows. Quality against render speed has always been a trade-off: Mip-NeRF360 sits top-left, high quality but 0.06 frames per second (one frame every seventeen seconds); the fast grid methods sit left of the real-time line and lower in quality. 3D Gaussian Splatting lands in the corner that was empty, SOTA-competitive quality and 134 frames per second, trained in 41 minutes instead of 48 hours. Drag the slider below to step through the methods and compare.

Figure 6 · quality vs render speed
3DGS-30K
Render speed (FPS, log scale) against quality (SSIM) on the Mip-NeRF360 dataset, from Table 1. Mip-NeRF360 is high but renders at 0.06 FPS; the fast NeRF grids sit left of the real-time band and lower. Only the two 3D Gaussian Splatting points are both high in quality and inside real time. Tap a point for its full numbers.

Be exact about "SOTA-competitive," because the paper is, and the popular retelling is not. On the Mip-NeRF360 dataset, 3D Gaussian Splatting at 30K iterations beats Mip-NeRF360 on SSIM (0.815 vs 0.792) and on LPIPS (0.214 vs 0.237, lower is better) but trails it on PSNR (27.21 vs 27.69). On the Tanks&Temples and Deep Blending datasets it matches or beats Mip-NeRF360 on all three metrics. So the precise claim is "matches or beats the best quality, and renders it a thousand-plus times faster," not "wins every metric everywhere."

The cost is hiding on a third axis nobody usually plots: storage. An explicit cloud of a few million Gaussians, each carrying 59 numbers, is big. That same Mip-NeRF360-dataset model is 734 MB on disk against 8.6 MB for Mip-NeRF360 and tens of megabytes for the Instant-NGP hash grids (Plenoxels, a dense voxel grid, is bigger still at 2.1 GB), and peak GPU memory during training can top 20 GB. So the method is dominant on the two axes people usually care about, quality and speed, and clearly loses on the one they usually ignore. It has other rough edges the paper owns: in regions the cameras barely saw it produces elongated or splotchy artifacts, and it can "pop" when large Gaussians suddenly switch depth order, both of which trace back to having no explicit smoothness or regularization on the cloud.

What is left is a change of representation, not a new kind of network. A scene is a pile of oriented, colored, translucent blobs; rendering it is a sort and a blend a GPU finishes in milliseconds; and the blobs are trained by comparing each render to a photo and adjusting them to fit. The neural network that NeRF put at the center of radiance fields is optional. Take it out of the render loop, put a rasterizer there, and the field has its real-time renderer.

Provenance Verified against primary literature
Kerbl et al. (2023)The method: anisotropic 3D Gaussians, adaptive density control, and the tile-based differentiable rasterizer (SIGGRAPH 2023).
Max (1995) / NeRF (2020)The emission-absorption volume-rendering model Eqs (1)-(3) discretize; the same image formation 3DGS inherits.
Zwicker et al., EWA (2001)Projecting a 3D covariance to a 2D screen splat, Σ' = J W Σ Wᵀ Jᵀ, via the local-affine Jacobian.
Plenoxels / Instant-NGP (2022)Representing per-Gaussian color with spherical harmonics, keeping the render loop network-free.
COLMAP (Schönberger & Frahm, 2016)Structure-from-Motion: the camera poses and the sparse point cloud that seed the Gaussians.
Official code (graphdeco-inria)Ground-truth defaults: clone in place, split scale ÷1.6, opacity reset min(α,0.01), the 64-bit sort key, backward T/(1−α).
correctionThe paper's Eq. (2) prints the transmittance as Tᵢ = ∏(1 − αᵢ), but the product runs over j, so the factor must be (1 − αⱼ). We teach the correct form; the paper's own Eq. (1) and Eq. (3) already use the j index, which confirms it is a typo.

Questions you might still have

?

Why is there no neural network in the render loop?
Because the scene is stored explicitly, as a few million Gaussians with their own positions, shapes, colors, and opacities. Rendering is projecting them to the screen, sorting them by depth, and alpha-blending, all fixed arithmetic. NeRF, by contrast, has to query a network at dozens of points along every ray, which is what makes it slow.

?

Where does color come from without an MLP?
Each Gaussian stores spherical-harmonic coefficients per color channel. Evaluating them at the viewing direction is a dot product with fixed basis functions and returns the RGB seen from that angle. Band 0 is the flat base color; higher bands add smooth view-dependent shine. Four bands (16 coefficients per channel) capture most surfaces but not sharp mirror reflections.

?

Why optimize scale and rotation instead of the covariance directly?
A covariance must be positive semi-definite, or the ellipsoid has a negative width and is not a real shape. Gradient descent on the six entries of the matrix would drift out of that valid set. Factoring Σ = R S Sᵀ Rᵀ from a scale vector and a unit quaternion is positive semi-definite for any values, so the optimizer stays valid with no projection step, at no loss of expressiveness.

?

Is the front-to-back blending exact?
Not quite. All pixels in a 16×16 tile share one depth order, chosen by a single global sort rather than a per-pixel sort. Sorting once instead of once per pixel is what makes it real-time, and the error it introduces vanishes as splats shrink toward pixel size, which the optimizer drives them to. The paper reports no visible artifacts from it in converged scenes.

?

What is the catch, given it beats NeRF on quality and speed?
Storage. An explicit cloud of a few million Gaussians is 734 MB for the Mip-NeRF360-dataset model, against 8.6 MB for Mip-NeRF360 itself, and training can peak above 20 GB of GPU memory. It also produces artifacts in regions the cameras barely observed. It wins the two axes people plot, quality and speed, and loses the one they usually ignore.

Footnotes & further reading

  1. The paper: Kerbl, Kopanas, Leimkühler, Drettakis, 3D Gaussian Splatting for Real-Time Radiance Field Rendering (Inria / MPI Informatik / Université Côte d'Azur, SIGGRAPH 2023). Code.
  2. The image-formation model is the classical emission-absorption integral of Max, Optical Models for Direct Volume Rendering (1995), the same one NeRF (Mildenhall et al., 2020) discretizes.
  3. Projecting a 3D Gaussian to a 2D screen splat, Σ=JWΣWJ\Sigma' = JW\Sigma W^\top J^\top, is EWA splatting: Zwicker, Pfister, van Baar, Gross, EWA Volume Splatting (IEEE Vis 2001).
  4. Network-free view-dependent color via spherical harmonics follows Plenoxels (Fridovich-Keil and Yu et al., 2022) and Instant-NGP (Müller et al., 2022). The SH evaluation constants trace to PlenOctrees (Yu et al., 2021).
  5. Camera poses and the sparse initialization cloud come from Structure-from-Motion: Schönberger & Frahm, Structure-from-Motion Revisited (COLMAP, CVPR 2016). The structural loss term is SSIM: Wang et al., 2004.
  6. The unbounded-gradient rasterizer contrasts with Pulsar (Lassner & Zollhöfer, 2021), which caps how many front primitives per pixel receive gradients; the ablation shows a cap of ten costs 11 dB of PSNR. Implementation defaults (clone in place, split scale ÷1.6, opacity reset, sort key, backward transmittance recovery) are from the official code.