Light Propagation Volumes
Light Propagation Volumes is a technique for approximating the first bounce of diffuse global illumination. I present a finite element approximation, using Spherical Harmonics(SH) and Radiance Volumes(RV). This solution is fully dynamic and the additional steps can be easily implemented into pre-existing complex rendering pipelines.

download build: lpv_build.7z
download source: lpv_source.7z

Injection

After generating a set of secondary light sources, using Reflective Shadow Mapping, that will emit indirect light into the scene, I use an intensity-based downsampling technique to reduce the size of our Virtual Point Light(VPL) data set. This will make the injection stage a lot lighter. The primary idea of this pass is to transform the VPL cloud into an initial radiance distribution. I use SH coefficients to achieve this. Storing 4 coefficients per color channel in a set of volumetric textures. Do note that this is an approximation, but due to the scattered and diffuse nature of indirect lighting, I feel that this is sufficient. The input for this stage will be our Reflective Shadow Map (RSM). I evaluate all VPLs in an area and use an intensity-based evaluation to determine what VPLs will most likely contribute. Selected VPLs are added together and normalized. I generate a 1024x1024 RSM and down-sample it to 512x512. I use a vertex shader for the downsampling, a geometry shader to output the down-sampled VPL into the correct slice of our 3D texture, and a pixel shader for calculating the SH coefficients and writing the final result. Geometry shaders in DirectX12 have access to the system value semantic SV_RenderTargetArrayIndex. This semantic can be used to tell the Output Merger in what depth slice of a 3D texture to render the result of the pixel shader. Although this is an easy way of rendering our radiance volume, I propose an optimization using a gathering algorithm in a compute shader.

Spherical Harmonics

SH can be used for approximating a signal integrated over a sphere. This is very useful when trying to simulate scattering phenomena. Various previous uses in computer graphics include precomputed radiance transfer and radiance probes.

Real-valued SH are defined as: \begin{equation} Y_{l}^{m}(\theta, \phi) = \Phi^{m}(\phi) N_{l}^{|m|} P_{l}^{|m|}(cos \theta) \end{equation} Where: \begin{equation} \Phi^{m}(\phi)=\begin{cases} \sqrt{2}cos(mx), & \text{m $>$ 0}.\\ 1, & \text{m = 0}. \\ \sqrt{2}sin(|m|x) & \text{m $<$ 0} \end{cases} \end{equation} \begin{equation} N_{l}^{|m|} = \sqrt{\frac{2l + 1}{4\pi} \frac{(l-m)!}{(l + m)!}} \end{equation} And \(P_{l}^{|m|}\) are the associated Legendre polynomials: \begin{align*} P_{0}^{0} & = 1\\ P_{1}^{0} & = x\\ P_{1}^{1} & = -\sqrt{1 - x^2}\\ P_{0}^{2} & = \frac{1}{2}(3x^2-1)\\ P_{1}^{2} & = -3x\sqrt{1 - x^2}\\ P_{2}^{2} & = 3(1 - x^2)\\ ... \end{align*} I will assume points on a unit sphere are defined in cartesian coordinates as: \begin{align*} x & = sin(\theta) cos(\phi)\\ y & = sin(\theta) sin(\phi)\\ z & = cos(theta)\\ \end{align*} solving these equations for the first two bands gives: \begin{align*} Y_{0}^{0}(\theta, \phi) & = \sqrt{\frac{1}{4\pi}} \\ Y_{-1}^{1}(\theta, \phi) & = -\sqrt{\frac{3}{4\pi}} y \\ Y_{0}^{1}(\theta, \phi) & = \sqrt{\frac{3}{4\pi}} z \\ Y_{1}^{1}(\theta, \phi) & = -\sqrt{\frac{3}{4\pi}} x \\ \end{align*}

Our constants now represent radiance, which is not what we want, Spherical harmonics are surprisingly bad at approximating radiance, they are however extremely good at approximating irradiance. Thus, we want our constants to represent Irradiance. We can convert them by multiplying with normalization constant \(Â_{l}\), which is defined as: \begin{align*} \hat{A}_{0} & = \pi \\ \hat{A}_{1} & = 2\pi / 3 \\ \end{align*} I precompute these values and use them to represent a normal by a vector of SH coefficients. I renormalize them right away to form a hemispherical lobe. Finally, we must not forget to scale the resulting vector with the weight of the surfel.

Propagation

Now we have a 3D texture filled with SH coefficients. The next step is propagation. Propagation is a sequential iterative process where each iteration represents one discrete step of light propagation in the radiance volume. The propagation can be visualized as every grid cell sending its directional energy to neighbors through the face that they share. However, in practice, this will be a race condition prone approach. Multiple threads will be reading and writing from/to the same cell in an unspecified order. This will cause race conditions and, therefore, flickering. I suggest a gathering approach, where threads sample the data from neighboring cells. We must not forget that we are propagating light through a cubical grid. If we do not project our propagated light correctly there will be artifacts in the form of unlit spots. this is done by re-projecting our cosine lobes in the direction of the four side faces and just using the current lobe for the front facing cube. All the resulting SH coefficients are added together and stored in their cell. Note that in the discussed implementation I have separate injection textures and accumulation buffers. My Geforce GTX660m only allows 32bit typed loads from RWTextures, I worked around this issue by using a RWStructuredBuffer<float4> to store the accumulated SH coefficients.

Sampling

Lastly, we have to sample our accumulation buffers, luckily this is a fairly straightforward process. We take the world space normal of the fragment we are shading and project it into SH basis. Then we use the world space position of the fragment to do linear sampling on our accumulation buffers. Finally, we do dot products and a multiplication with PI to find the final flux.

Result

Figure1: Sponza with Light Propagation

Source


download build: lpv_build.7z
download source: lpv_source.7z