PAJAMA / LEARN
← Course overview

See how pixels are made / 03 OF 12

How a 3D brush lands on a 2D screen

After this lessonDerive perspective from similar triangles and see why projecting shape needs a Jacobian.

Hold a coin close to your eye, then move it away. Its size in the world stays fixed, but it covers fewer pixels. A Gaussian’s center and its footprint both need this perspective rule.

Build the idea, one step at a time

Need a math refresher?

Open only the tool you need; return here to use it in the next step.

Matrices & covarianceDerivatives as local rates

STEP 1 / 5

Move the world into the camera’s frame

Subtract the camera position, then rotate into its axes. This lesson uses +Z forward and +Y down, with a centered pinhole camera and no lens distortion. The wgpu host converts conventions for its right-handed view and clip transform. Column vectors, axis directions and matrix layout must be agreed before multiplying.

pc=Rcw(pw−c)\mathbf p_c=R_{cw}(\mathbf p_w-\mathbf c)

PUT NUMBERS IN

At the optical axis, a spherical scale of 0.2 at Z = 10 and f = 100 gives a projected standard deviation of 2 pixels before filtering. Including 0.3 pixel² gives √4.3 ≈ 2.074 pixels. Doubling depth reduces the unfiltered width to 1 pixel, not the world-space scale.

FORMULA → RUNNING CODE

Find the same operation in the code

These excerpts are read from the current repository when the site is built. Expand one to inspect it; the full snapshot opens in a separate tab.

public/learn/models.mjsL4–7

The exact arithmetic behind this lesson’s controls and worked example.

export function cameraSample(x,z,f,scale=0.2){
 const u=64+f*x/z,j=[f/z,-f*x/(z*z)];
 return {u,variance:scale**2*(j[0]**2+j[1]**2)+0.3,width:Math.sqrt(scale**2*(j[0]**2+j[1]**2)+0.3)};
}
Open the deployed source snapshot ↗
src/splat.wgslL41–49

The WGSL helper differentiates the clip-space quotient.

fn projected_axis(center_clip: vec4<f32>, axis: vec3<f32>) -> vec2<f32> {
  // Exact first-order derivative of ndc = clip.xy / clip.w along a world-space
  // covariance axis. This is the homogeneous form of the perspective Jacobian.
  let axis_clip = camera.view_proj * vec4<f32>(axis, 0.0);
  let w2 = max(center_clip.w * center_clip.w, 0.00000001);
  let ndc_derivative = (
    axis_clip.xy * center_clip.w - center_clip.xy * axis_clip.w
  ) / w2;
  return ndc_derivative * camera.viewport.xy * 0.5;
Open the deployed source snapshot ↗
src/splat.wgslL94–101

Projected axes produce the three independent entries of a 2 × 2 covariance.

  let d0 = projected_axis(center_clip, axis0);
  let d1 = projected_axis(center_clip, axis1);
  let d2 = projected_axis(center_clip, axis2);

  // Project J W Sigma W^T J^T by summing the projected covariance axes.
  let cov_a = dot(vec3<f32>(d0.x, d1.x, d2.x), vec3<f32>(d0.x, d1.x, d2.x)) + 0.3;
  let cov_b = d0.x * d0.y + d1.x * d1.y + d2.x * d2.y;
  let cov_c = dot(vec3<f32>(d0.y, d1.y, d2.y), vec3<f32>(d0.y, d1.y, d2.y)) + 0.3;
Open the deployed source snapshot ↗

CHECK YOUR UNDERSTANDING

Can you predict the result?

On the optical axis, double Z while keeping focal length fixed. The unfiltered width…

This is your own review mark. You can remove it at any time.

Go deeper with primary sources

3D Gaussian Splatting · paper & implementation ↗COLMAP · camera and pose conventions ↗Full mathematical derivation ↗