Rama physically-based lighting
Indirect lighting on specular surfaces

1 Introduction

In this section we want to compute the light which goes from the Rama light sources into the camera via exactly two bounces, first on a diffuse surface or in the atmosphere, and then on a specular surface. This is equal to the light that bounces off from the specular surface, times the transmittance of the atmosphere between this surface and the camera. This transmittance has been computed in a previous section so we only compute the first term here, i.e. how much light from the light sources is reflected after the bounce on the specular surface.

2 Model

From the BRDF definition, and using the isotropic Ward BRDF, the light reflected in direction $\bv$ after the second bounce, supposed here to happen on a perfectly specular surface patch centered at $\bp$ and of normal $\bn$ is equal to \begin{equation} L_{L(D|V)S}(\bp,\bn,\bv)=\int_{\Omega^+}\color{red}{L_i}\color{green}{\mathrm{f_r}}\color{blue}{\cos\theta_i}\diff\omega_i=\int_{\Omega^+}\color{red}{L_{L(D|V)E}(\bp,\bw)}\color{green}{\frac{F(\bv\cdot\bh(x))\exp\left(-\frac{\tan^2\alpha(x)}{\sigma^2}\right)}{4\pi\sigma^2\sqrt{(\bl(x)\cdot\bn)(\bv\cdot\bn)}}}\color{blue}{\bn\cdot\bw}\,\diff \omega \end{equation} where $L_{L(D|V)E}(\bp,\bw)$ is the direct light arriving at $\bp$ from direction $\bw$ after exactly one bounce on a diffuse surface or on the atmosphere. Noting $\br(\bp,\bw)$ the point on Rama's surface which is visible from $\bp$ in direction $\bw$ (i.e. the nearest intersection of the ray of origin $\bp$ and direction $\bw$ with the surface), $L_{L(D|V)E}(\bp,\bw)$ can be rewritten as: \begin{equation} L_{L(D|V)E}(\bp,\bw)=\mathfrak{t}(\bp,\br(\bp,\bw))L_{LD}(\br(\bp,\bw))+L_{LVE}(\bp,\br(\bp,\bw)) \end{equation} where $L_{LD}$ and $L_{LVE}$ are the functions that we computed in the Diffuse surfaces and Atmospheric scattering sections, respectively. Thus, we finally get: \begin{equation} \begin{split} L_{L(D|V)S}(\bp,\bn,\bv)=\int_{\Omega^+}&\left[\mathfrak{t}(\bp,\br(\bp,\bw))L_{LD}(\br(\bp,\bw))+L_{LVE}(\bp,\br(\bp,\bw))\right]\\ &\frac{F(\bv\cdot\bh(x))\exp\left(-\frac{\tan^2\alpha(x)}{\sigma^2}\right)}{4\pi\sigma^2\sqrt{(\bl(x)\cdot\bn)(\bv\cdot\bn)}}\bn\cdot\bw\,\diff \omega \end{split}\label{eq:final} \end{equation}

From the definitions of $L_{LD}$ and $L_{LVE}$, it is easy to see that this expression involves double and triple integrals, as well as ray tracing functions to compute $\br(\bp,\bw)$. It would thus be really hard to compute this in real time. Hopefully the light sources in Rama are static so in theory we can precompute $L_{L(D|V)S}$ in a lightmap. Note however that this quantity depends on $\bp$, $\bn$ and $\bv$, i.e. 6 parameters. In other words a precomputed lightmap would require a 6D texture! To reduce this to a 4D texture "only", we use the following approximation: we precompute only $L_{L(D|V)S}(\bp,\bu_z,\bv)$ for a single normal $\bu_z$ (equal to the vertical vector at $\bp$), and then approximate \begin{equation} L_{L(D|V)S}(\bp,\bn,\bv)\approx L_{L(D|V)S}(\bp,\bu_z,\mathrm{reflect}(\mathrm{reflect}(\bv,\bn),\bu_z))) \end{equation} where $\mathrm{reflect}(\bv,\bn)=2(\bv\cdot\bn)\bn-\bv$ is the reflected view vector on a specular surface of normal $\bn$. In other words we sample the precomputed lightmap with a modified view vector $\bv'$ such that the reflected view vectors are equal in both cases, i.e. $\mathrm{reflect}(\bv',\bu_z)=\mathrm{reflect}(\bv,\bn)$ (which would give the exact result for a perfect mirror, $\sigma=0$ and no Fresnel coefficient).

3 Implementation

In our implementation we precompute $L_{L(D|V)S}(\bp,\bu_z,\bv)$ for $16\times 72$ points $\bp$ regularly sampled on the cylindrical sea, and for each point $65\times 16$ directions $\bv$ (using spherical coordinates, i.e. $65$ samples for the azimuth $\phi$ and $16$ samples for the cosine of the zenith angle $\theta$). Packing this in a 2D texture yields a $1040\times 1152$ texture. For each texel we compute the above integral, using the precomputed lighmap $L_{LD}$ and the precomputed volumetric lightmap $J_{LV}$ to compute $L_{LVE}$ with a single inner integral.

To precompute this $1040\times 1152$ lightmap we loop over each sample point $\bp$. For each point we first generate a temporary $512\times 128$ environment map using ray tracing, and containing the incident radiance term $\mathfrak{t}(\bp,\br(\bp,\bw))L_{LD}(\br(\bp,\bw))+L_{LVE}(\bp,\br(\bp,\bw))$. Then for each direction $\bv$ we compute the integral in Eq. \eqref{eq:final} using this environment map to speed up computations.

At runtime, since there is no builtin support for 4D textures, we have to simulate 4D bilinear interpolation using a manual bilinear interpolation for $\bp$ (and relying on the GPU hardware for the bilinear interpolation in $\bv$):

// Compute the indirect light reflected by the cylindrical sea at p, with
// normal n, reflected in direction v after a previous bounce on a diffuse
// surface or in the atmosphere. p must be given in Rama coordinates (Rama axis
// equal to the x axis, units in kilometers, with the cylindrical sea between
// x = -5 and x = +5).
vec3 indirectSpecular(vec3 p, vec3 n, vec3 v) {
  vec3 ux = vec3(1.0, 0.0, 0.0);
  vec3 uz = -normalize(vec3(0.0, p.yz));
  vec3 uy = cross(uz, ux);
  v = reflect(reflect(v, n), uz);
  float phi = atan(dot(v, uy), dot(v, ux)) / (2.0 * PI) + 0.5;
  vec2 uv = vec2(phi, max(dot(v, uz), 0.0));
  uv = vec2(0.5 / 65.0, 0.5 / 16.0) + uv * vec2(64.0 / 65.0, 15.0 / 16.0);

  float xi = (p.x + 5.0) / 10.0 * 15.0;
  float alphai = (atan(p.z, p.y) / (2.0 * PI) + 0.5) * 72.0;
  float floor_xi = floor(xi);
  float floor_alphai = floor(alphai);
  float dxi = xi - floor_xi;
  float dalphai = alphai - floor_alphai;

  vec2 uv00 = uv + vec2(floor_xi, floor_alphai);
  vec2 uv01 = uv + vec2(floor_xi, floor_alphai + 1.0);
  vec2 uv10 = uv + vec2(floor_xi + 1.0, floor_alphai);
  vec2 uv11 = uv + vec2(floor_xi + 1.0, floor_alphai + 1.0);

  vec3 c00 = texture2D(specularSampler, uv00 / vec2(16.0, 72.0)).rgb;
  vec3 c01 = texture2D(specularSampler, uv01 / vec2(16.0, 72.0)).rgb;
  vec3 c10 = texture2D(specularSampler, uv10 / vec2(16.0, 72.0)).rgb;
  vec3 c11 = texture2D(specularSampler, uv11 / vec2(16.0, 72.0)).rgb;

  return (c00 * (1.0 - dalphai) + c01 * dalphai) * (1.0 - dxi) +
      (c10 * (1.0 - dalphai) + c11 * dalphai) * dxi;
}

4 Results

Here are the results with a single linear light source:


Figure 1: Indirect lighting on specular surfaces, for a single light source.

With the 6 linear light sources (and the same exposure) we get the following:



Figure 2: Indirect lighting on specular surfaces.