PAJAMA / LEARN
← Course overview

Add appearance and time / 05 OF 12

One direction, three independent color sums

After this lessonRead SH as weighted directional patterns and explain why separate channels produce a colored sphere.

A shiny surface looks different as you move around it. A single RGB triple cannot express that change. Instead, store a small recipe that takes a viewing direction and returns RGB.

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.

Vectors & weighted sumsIntegrals & accumulation

STEP 1 / 5

Use a direction, not a position

Subtract camera position from the Gaussian center and divide by the vector length. This produces a unit vector v: three numbers whose squared values sum to one. Our SH convention uses camera → Gaussian. Reversing it flips odd-degree SH terms, so sign conventions must match the exporter.

v=μ−c∥μ−c∥,vx2+vy2+vz2=1\mathbf v=\frac{\boldsymbol\mu-\mathbf c}{\|\boldsymbol\mu-\mathbf c\|},\qquad v_x^2+v_y^2+v_z^2=1

PUT NUMBERS IN

Take baseline RGB = (0.5, 0.5, 0.5) and only a₂R = 0.4. At +Z, Y₂ ≈ 0.4886, so RGB ≈ (0.6954, 0.5, 0.5). At −Z, it is (0.3046, 0.5, 0.5). Green and blue never change.

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/math/sh.mjsL19–36

These are the actual 16 basis polynomials used by the SH lab.

export function shBasis(direction, degree = 3) {
  if (!Number.isInteger(degree) || degree < 0 || degree > 3) throw new RangeError('Degree must be 0..3');
  const [x, y, z] = unitDirection(direction);
  const pi = Math.PI, xx = x*x, yy = y*y, zz = z*z;
  const a = Math.sqrt(3 / (4*pi)), b = Math.sqrt(15 / (4*pi));
  const c = Math.sqrt(35 / (32*pi)), d = Math.sqrt(21 / (32*pi));
  const values = [
    C0,
    -a*y, a*z, -a*x,
    b*x*y, -b*y*z, Math.sqrt(5/(16*pi))*(2*zz-xx-yy),
    -b*x*z, Math.sqrt(15/(16*pi))*(xx-yy),
    -c*y*(3*xx-yy), Math.sqrt(105/(4*pi))*x*y*z,
    -d*y*(4*zz-xx-yy), Math.sqrt(7/(16*pi))*z*(2*zz-3*xx-3*yy),
    -d*x*(4*zz-xx-yy), Math.sqrt(105/(16*pi))*z*(xx-yy), -c*x*(xx-3*yy),
  ];
  return values.slice(0, (degree+1)**2);
}
Open the deployed source snapshot ↗
public/math/sh.mjsL37–50

Each RGB component accumulates its own coefficient products.

export function sampleSH(coefficients, direction, degree = 3) {
  const basis = shBasis(direction, degree);
  if (coefficients.length < basis.length || coefficients.slice(0,basis.length).some(
    rgb => rgb.length !== 3 || !rgb.every(Number.isFinite))) throw new RangeError('Missing finite RGB coefficients');
  const contributions = basis.map((value,k) => coefficients[k].map(a => a*value));
  const raw = [0,1,2].map(ch => contributions.reduce((sum,rgb) => sum+rgb[ch],0));
  const shifted = raw.map(value => value+0.5);
  // Graphdeco's forward.cu clamps below zero; it does NOT clamp above one here.
  const shaderColor = shifted.map(value => Math.max(0,value));
  // A separate SDR display clipping step for this canvas demo (no tone mapping).
  const display = shaderColor.map(value => Math.min(1,value));
  return { basis, contributions, raw, shifted, shaderColor, display };
}
Open the deployed source snapshot ↗

CHECK YOUR UNDERSTANDING

Can you predict the result?

Changing only a red SH coefficient directly changes…

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

Go deeper with primary sources

3D Gaussian Splatting · paper & implementation ↗Spacetime Gaussian Feature Splatting · paper ↗Full mathematical derivation ↗