I’m currently working at university on implementing Light Propagation Volumes. The paper makes extensive use of spherical harmonics while the implementation uses the first two bands.

Below is a visualization of the first 4 bands of the SH basis functions (created using Mayavi):

The first 4 bands of the spherical harmonic basis functions

As you can see the first two bands are 4 functions, so 4 coefficients to store which conveniently fits into one RGBA texture.

One of the main transformations that is performed in the LPV paper is the rotation of the spherical harmonics representation of a clamped cosine lobe (that represents surface lighting) onto a normal vector direction. It took me a while to figure out, but actually it’s quite easy, which is why I write about it :-)

The analytical presentation of the first four base functions is simple:

\[S_0 \left( x, y, z \right ) = \frac{1}{2 \sqrt{\pi}}\] \[S_1 \left( x, y, z \right ) = - \frac{\sqrt{3}}{2 \sqrt{\pi}} y\] \[S_2 \left( x, y, z \right ) = \frac{\sqrt{3}}{2 \sqrt{\pi}} z \] \[S_3 \left( x, y, z \right ) = - \frac{\sqrt{3}}{2 \sqrt{\pi}} x\]

To evaluate lighting with SH for some direction \(v\), you first determine the coefficients/weights of the SH basis functions and then sum them up.

\[ L = \sum_i s_i \, S_i \left( v \right ) \]

Let’s assume we know the coefficients \(s^z_0, s^z_1, ...\) of the clamped cosine lobe around the z axis, then we can determine the lighting in direction v for the cosine lobe around the normal n by transforming it into the space where the normal coincides with the z axis (ie rotate n onto the z axis):

\[ L = \sum_i s^z_i \, S_i \left( R_{n \to z} \, v \right ) \]

where \(R_{n \to z}\) is a rotation matrix that rotates n onto z.

The idea is to expand \(S_i \left( R_{n \to z} \, v \right )\) and rewrite it in terms of \(S_i \left ( v \right )\).

Before doing this, let’s first take a look at the coefficients of the clamped cosine lobe:

\[\begin{align*} s^z_0 &=\frac{ \sqrt{ \pi } }{ 2 }\\ s^z_1 &= 0\\ s^z_2 &= \sqrt\frac{ \pi }{3}\\ s^z_3 &= 0\\ \end{align*}\]

The y and x direction are 0 because the cosine lobe is centered isotropic around the z axis:

So let’s look at the expanded version of this formula if \(r_1^T\), \(r_2^T\), \(r_3^T\) are the row vectors of the matrix, \(v=\bigl(\begin{smallmatrix} x\\ y\\ z \end{smallmatrix}\bigr)\) and \(R_{n \to z}=\left(\begin{smallmatrix} r_1^T\\ r_2^T\\ r_3^T \end{smallmatrix}\right )\), then:

\[ L = \sum_i s^z_i \, S_i \left( R_{n \to z} \, v \right ) = \sum_i s^z_i \, S_i \left( \left(\begin{smallmatrix} r_1^T \, v\\ r_2^T \, v\\ r_3^T \, v\end{smallmatrix}\right ) \right ) \] \[\begin{align*} L &= s^z_0 \, c_0\\ &+ s^z_1 \, (-c_1) \, r_2^T \, v \\ &+ s^z_2 \, c_1 \, r_3^T \, v\\ &+ s^z_3 \, (-c_1) \, r_1^T \, v \end{align*} \]

Since \(s^z_1 = 0\) and \(s^z_3 = 0\):

\[ L = s^z_0 \, c_0 + s^z_2 \, c_1 \, r_3^T \, v = s^z_0 \, c_0 + s^z_2 \, c_1 \, r_{31} \, x + s^z_2 \, c_1 \, r_{32} \, y + s^z_2 \, c_1 \, r_{33} \, z \]

\[ L = s^z_0 \, S_0 \left ( v \right ) - s^z_2 \, r_{32} \, S_1 \left ( v \right )+ s^z_2 \, r_{33} \, S_2 \left ( v \right ) - s^z_2 \, r_{31} \, S_3 \left ( v \right ) \]

Now the question is: what is the third row of \(R_{n \to z}\)? If we look at the inverse matrix instead: \(R_{z \to n}\), we can immediately see that its third column has to be n, because \[ R_{z \to n} \, \bigl(\begin{smallmatrix} 0\\ 0\\ 1 \end{smallmatrix}\bigr) = n \] by construction. Since rotations are orthogonal matrices, the inverse is the same as the transposed, so we can deduce that the third row of \(R_{n \to z}\) is the same as the third column of \(R_{z \to n}\), that is: n. Thus with \(n = \bigl(\begin{smallmatrix} n_x\\ n_y\\ n_z \end{smallmatrix}\bigr)\) we get:

\[ L = s^z_0 \, S_0 \left ( v \right ) - s^z_2 \, n_y \, S_1 \left ( v \right )+ s^z_2 \, n_z \, S_2 \left ( v \right ) - s^z_2 \, n_x \, S_3 \left ( v \right ) \]

So the SH coefficients of the clamped cosine lobe along n are:

\[ s^n_0 = s^z_0 = \frac{ \sqrt{ \pi } }{ 2 } \\ s^n_1 = - s^z_2 \, n_y = -\sqrt{ \frac{ \pi }{3} } \, n_y \\ s^n_2 = s^z_2 \, n_z = \sqrt{\frac{ \pi }{3} } \, n_z \\ s^n_1 = - s^z_2 \, n_x = - \sqrt{\frac{ \pi }{3}} \, n_x \]

This is it :-)

Cheers,
 Andreas

PS: a few screenshots from the LPV project:

GPUPropCopy
noLPV_2
noLPV
LPV32P128C_2
LPV32P128C