There are several solutions for this problem. Texture borders solve it elegantly, but are not available on all hardware, and only exposed through the OpenGL API (and proprietary APIs in some consoles).

When textures are static a common solution is to pre-process them in an attempt to eliminate the edge seams. In a short siggraph sketch, John Isidoro proposed averaging cube map edge texels across edges and obscuring the effect of the averaging by adjusting the intensity of the nearby texels using various methods. These methods are implemented in AMD’s CubeMapGen, whose source code is now publicly available online. While this seems like a good idea, a few minutes experimenting with CubeMapGen make it obvious that it does not always work very well!

A very simple solution that even works for dynamic cube maps is to slightly increase the FOV of the perspective projection so that the edges of adjacent faces match up exactly. Ysaneya shows that in order to achieve that, the FOV needs to be tweaked as follows:

```
```

```
fov = 2.0 * atan(n / (n - 0.5))
```

```
```

where `n`

is the resolution of the cube map.

What this is essentially doing is to scale down the face images by one texel and padding them with a border of texels that is shared between adjacent faces. Since the texels at the face edges are now identical the seams are gone.

In practice this is much trickier than it sounds. While the fragments at the adjacent face borders should sample the scene in the same direction, rasterization rules do not guarantee that in both cases the rasterized fragments will match.

However, if we take this idea to the realm of offline cube map generation, we can easily guarantee exact results. Cube maps are often used to store directional functions. Each texel has an associated uv coordinate within the cube map face, from which we derive a direction vector that is then used to sample our directional function. Examples of such functions include expensive BRDFs that we would like to precompute, or an environment map sampled using angular extent filtering.

Usually these uv coordinates are computed so that the resulting direction vectors point to the texel centers. For an integer texel coordinate `x`

in the `[0,n-1]`

range we map it to a floating point coordinate `u`

in the `[-1, 1]`

range as follows:

```
```

```
map_1(x) = (x + 0.5) * 2 / n - 1
```

```
```

We then obtain the corresponding direction vector as follows:

```
```

```
dir = normalize(faceVector + faceU * map_1(x) + faceV * map_1(y)
```

```
```

When doing that, the texels at the borders do not map to `-1`

and `1`

exactly, but to:

```
```

```
map(0) = -1 + 1/n
map(n-1) = 1 - 1/n
```

```
```

In our case we want the edges of each face to match up exactly to they result in the same direction vectors. That can be achieved with a function like this:

```
```

```
map_2(x) = 2 * x / (n - 1) - 1
```

```
```

If we use this map to sample our directional function, the resulting cube map is seamless, but the face images are scaled down uniformly. In the first case the slope of the map is:

```
```

```
map_1'(x) = 2 / n
```

```
```

but in the second case it is slightly different:

```
```

```
map_2'(x) = 2 / (n - 1)
```

```
```

This technique works very well at high resolutions. When n is sufficiently high, the change in slope between map_1 and map_2 becomes minimal. However, at low resolutions the stretching on the interior of the face can become noticeable.

A better solution is to stretch the image only in the proximity of the edges. That can be achieved warping the uv face coordinates with a cubic polynomial of this form:

```
```

```
warp3(x) = ax^3 + x
```

```
```

We can compose this function with our original mapping. The result around the origin is close to a linear identity, but we can adjust `a`

to stretch the function closer to the face edges. In our case we want the values at `1-1/n`

to produce `1`

instead, so we can easily determine the value of `a`

by solving:

```
```

```
warp3(1-1/n) = ax^3 + x = 1
```

```
```

which gives us:

```
```

```
a = n^2 / (n-1)^3
```

```
```

I implemented the linear stretch and cubic warping methods in NVTT and they often produce better results than the methods available in AMD’s CubeMapGen. However, I was not entirely satisfied. While this removed the zero-order discontinuity, it introduced a first-order discontinuity that in some cases was even more noticeable than the artifacts it was intended to remove.

You can hover the cursor over the following image to show how the warp edge fixup method eliminates the discontinuities, but sometimes still results in visible artifacts:

Any edge fixup method is going to force the slope of the color gradient across the edge to be zero, because it needs to duplicate the border texels. The eye seems to be very sensible to this form of discontinuity and it’s questionable whether this is better than the original artifact. Maybe other warp functions would make the discontinuity less obvious, or maybe it could be smoothed like Isidoro’s method do. At the time I implemented this I thought the remaining artifacts did not deserve more attention and moved on to other tasks.

However, a few days ago Sebastien Lagarde integrated these methods in AMD’s CubeMapGen. See this post for more results and comparisons against other methods. That got me thinking again about this and then I realized that the only thing that needs to be done to avoid the seams is to modify the texture coordinates at runtime the same way we modify them during the offline cube map evaluation. At first I thought that would be impractical, because it would require projecting the texture coordinates onto the cube map faces, but turns out that the resulting math is very simple. In the case of the uniform stretch that I first suggested, the transform required at runtime is just a conditional per-component multiplication:

```
```

```
float3 fix_cube_lookup(float3 v) {
float M = max(max(abs(v.x), abs(v.y)), abs(v.z));
float scale = (cube_size - 1) / cube_size;
if (abs(v.x) != M) v.x *= scale;
if (abs(v.y) != M) v.y *= scale;
if (abs(v.z) != M) v.z *= scale;
return v;
}
```

```
```

One problem is that we need to know the size of the cube map face in advance, but every mipmap has a different size and we may not know what mipmap is going to be sampled in advance. So, this method only works when explicit LOD is used.

Another issue is that with trilinear filtering enabled, the hardware samples from two contiguous mipmap levels. Ideally we would have to use a different scale factor for each mipmap level. That could be achieved sampling them separately and combining the result manually, but in practice, using the same scale for both levels seems to produce fairly good results. We can easily find a scale factor that works well for fractional LODs as a function of the LOD value and the size of the top level mipmap:

```
```

```
float scale = 1 - exp2(lod) / cube_size;
if (abs(v.x) != M) v.x *= scale;
if (abs(v.y) != M) v.y *= scale;
if (abs(v.z) != M) v.z *= scale;
```

```
```

If you are using cube maps to store prefiltered environment maps, chances are you are computing the cube map LOD from the specular power using `log2(specular_power)`

. If that’s the case, the two transcendental instructions cancel out and the scale becomes a linear function of the specular power.

The images below show the results using the warp filtering method (these were chosen to highlight the artifacts of the warp method). Hover the cursor over the images to visualize the results of the new approach:

I’d like to thank Sebastien Lagarde for his valuable feedback while testing these ideas and for providing the nice images accompanying this article.

*Note: This article is also published at The Witness blog.*

With AltDevConf around the corner, we are all busy with preparations to make sure it becomes a success. In the mean time, we still have a few more talks to announce properly, next up in the pipeline is John McCutchan‘s talk titled “Control, Configure, Monitor and View Your Game Engine From the Web”.

Today I had the opportunity to catch up with John and ask him a few questions about his talk and his background. Instead of writing a formal introduction, we thought it would be a good idea to simply transcribe his answers:

*Ignacio Castaño:* I’ve seen your name in the linux kernel and gnome mailing lists, can you explain what was your involvement in those projects?

John McCutchan:I got my start with Unix at a young age- when I was in elementary school I was given a book on Unix that came with 5 inch floppy disks. A couple years later my friend introduced me to Debian Linux and I started to get into C and Free Software. Many years later, after Gnome 2.0 came out, I started to use Gnome as my desktop environment. At the time if you had a CD-ROM the file manager would block it from being unmounted because it was using something called dnotify to monitor file systems for changes. I wanted to fix it so I wrote inotify which allow for more efficient file system monitoring without blocking unmounts. In order to get inotify in the kernel I ported a bunch of user space code to use it (FAM, gnome-vfs, etc).

*IC:* What brought you to the game industry?

JM:During graduate school I started to become interested in real time rigid body simulations and collision detection in particular. While working on my thesis I spent my spare time writing a 3D collision detection engine and rigid body physics engine. I never released my engine to the public, but along the way I met Erwin Coumans and he hired me to work on Bullet at SCEA.

*IC:* Are you still involved in any other open source project?

JM:No, I’ve drifted away from working on open source projects. Mostly due to limited spare time.

*IC:* You are becoming a regular author at AltDevBlogADay. What motivates you to be part of this community?

JM:I really enjoyed many of the articles on the site and after reading an article about something I had just done, I decided that I should share some of my ideas and quit working in a vacuum. I always enjoyed working on open source projects because of all the interesting people and ideas constantly being shared. AltDevBlog has captured some of that.

*IC:* In a few words, can you tell our readers what your talk is about?

JM:Creating an interface to your game engine that allows web applications to control, configure, monitor, and view your game over the network.

*IC:* John, it was great talking to you, thanks for you time and for contributing to AltDevConf!

To learn more about John’s work, check out his recent AltDevBlogADay articles or come to his talk this Saturday at 2:00 PM (PST). Check out our schedule or register online now!

]]>Only 2 days to go before AltDevConf! With the conference around the corner it’s time to finish unveiling all the details of the program we have put together. Today I’m pleased to present the next three speakers in our lineup for the Programming track: Matthieu Laban, Philippe Rollin and Miguel de Icaza. Together they will present a joint talk titled “Cross Platform Development in C#: From Windows Phone to iOS with MonoTouch”.

Matthieu and Philippe are the founders of Flying Development Studio, an independent studio focused on the development of flight simulation products for mobile platforms. Their first product, Infinite Flight gained critical acclaim and commercial success on the Windows Phone store and is about to be released for iOS devices. Matthieu is a licensed pilot and can often be found flying around the bay area. Philippe is passionate about computer graphics and a flight simulation enthusiast. Both share a meticulous attention to detail and a relentless desire to create great products.

Miguel needs little to no presentation. He currently directs the Mono project, an open source implementation of the .NET framework running on Mac, Linux, mobile devices and embedded systems. He is currently CTO for Xamarin, a startup focused on bringing .NET to mobile devices and previously worked at Novell and Ximian, where he created both the Mono project and worked on the Linux desktop.

Miguel will start the session providing an overview of the mono ecosystem as it relates to game developers. He will describe some of the reasons behind mono’s popularity among game developers, and show different ways in which developers are using the mono runtime in games. He will focus on mono features that were specifically added for game development and address common fears and concerns that game developers usually have.

Matthieu and Philippe’s success porting Infinite Flight from Windows Phone to iOS would have not been possible without MonoTouch, but that doesn’t mean it came without challenges. In the second part of the session they will share their experiences with the Mono runtime on iOS and the challenges they faced in order to bring the simulator to market on both platforms.

I think that combining their different perspectives over the mono runtime will give us a unique opportunity to learn from their experiences.

Their talk will air next Saturday at 12:00 PM (PST), check it out in our schedule and register now!

]]>Alex is a veteran of the industry. I’ve known about him since the late 90s, when he used to write some regular columns at flipcode that I still have fond memories of. Later, as flipcode closed his doors he found his passion in game AI and continued writing and sharing his work at the ai-depot.

Today, Alex has a unique perspective of recent game AI developments that draws from both his professional and academic experience. He has worked at leading game studios and continues consulting with them regularly. He has contributed to many games including Killzone 3, Killzone 2, Max Payne 3, and Rockstar Table Tennis.

On the academic front, he’s the editor-in-chief of AiGameDev.com, author of the AI Game Development book, and frequent speaker at the Paris Game AI Conference, which he co-organizes with his wife Petra.

Alex has become one of the main authorities for the study and advocation of Behavior Trees in games. In his AltDevConf talk he will introduce this technique to newcomers, discuss modern trends in current games, and predict what to expect from future implementations. Here’s a description of the talk in his own words:

This presentation will provide the most comprehensive overview of the design and implementation of behavior trees to date. Since their incremental evolution into the games industry ~8 years ago, a lot has changed and there are clear trends in their uses and applications in commercial games. Drawing from AiGameDev.com‘s extensive experience prototyping, teaching, consulting about behavior trees, you’ll learn the key principles behind first- and second-generation behavior trees as they are currently used in games — and what challenges and opportunities this presents for your game’s AI and scripting system.

Alex is just one of the speakers we’ll be introducing during the next few days leading to the conference. You can read more about the program we are putting together at AltDevConf.org and remember to clear your schedule February 11th/12th to come join us!

]]>Unfortunately, by the time I was done with these, the work I had done on the gradients was not so fresh anymore, which is why it has taken me so long to complete this article.

In my previous post I mentioned the need to find an algorithm that would allow us to extrapolate the irradiance to fill lightmap texels with invalid samples, to smooth the resulting lightmaps to reduce noise, and to sample the irradiance at a lower frequency to improve performance. As some avid readers suggested *Irradiance Caching* solves all these problems. However, at the time I didn’t know that, and my initial attempts at a solution were misguided.

I had some experience writing baker tools to capture normal and displacement maps from high resolution meshes. In that setting it is common to use various image filters to fill holes caused by sampling artifacts and extend sampled attributes beyond the boundaries of the mesh parameterization. Hugo Elias’ popular article also proposes a texture space hierarchical sampling method, and that persuaded me that working in texture space would be a good idea.

However, in practice texture space methods had many problems. Extrapolation filters only provide accurate estimates close to chart boundaries, and even then, they only use information from one side of the boundary, which usually results in seams. Also, the effectiveness of irregular sampling and interpolation approaches in texture space is greatly reduced by texture discontinuities and did not reduce the number of samples as much as desired.

After trying various approaches, it became clear that the solution was to work in object space instead. I was about to implement my own solution, when I learned that this is what irradiance caching was about.

This technique has such a terrible name that I would have never learned about it if it wasn’t due to a friend’s suggestion. I think that a much more appropriate name would be adaptive irradiance sampling or irradiance interpolation. In fact, in the paper that introduced the technique for the first time Greg Ward referred to it as *lazy irradiance evaluation*, which seems to me a much more appropriate name.

There’s plenty of literature on the subject and I don’t want to repeat what you can learn elsewhere. So, in this article I’m only going to provide a brief overview, focus on the unique aspects of our implementation, and point you to other sources for further reference.

The basic idea behind irradiance caching is to sample the irradiance adaptively based on a metric that predicts changes in the irradiance due to proximity to occluders and reflectors. This metric is based on the split sphere model, which basically models the worst possible illumination conditions in order to predict the necessary distance between irradiance samples based on their distance to the surrounding geometry.

Interpolation between these samples is then performed using radial basis functions, the radius associated to each sample is also based on this distance. Whenever the contribution of the nearby samples falls below a certain threshold a new irradiance record needs to be generated, in our case this is done by rendering an hemicube at that location. In order to perform interpolation efficiently the irradiance records are inserted in an octree, which allows us to efficiently query the nearby records at any given point.

For more details, the Siggraph ’08 course on the topic is probably the most comprehensive reference. There’s also a book based on the materials of the course; it’s slightly augmented, but in my opinion it does not add much value to the freely available material.

The *Physically Based Rendering book* by Matt Pharr & Greg Humphreys is an excellent resource and the accompanying source code contains a good, albeit basic implementation.

Finally, Rory Driscoll provides a good introduction from a programmer’s point of view: part 1, part 2.

Our implementation is fairly standard, there are however a two main differences: First, we are sampling the irradiance using hemicubes instead of a stratified Montecarlo distribution. As a result, the way the we compute the irradiance gradients for interpolation is somewhat different. Second, we use a record placement strategy more suited to our setting. In part 1 I’ll focus on the estimation of the irradiance gradients, while in part 2 I’ll write about our record placement strategies.

When interpolating our discrete irradiance samples using radial basis functions the resulting images usually have spotty or smudgy appearance. This can be mitigated by increasing the number of samples and increasing the interpolation threshold, which effectively makes them overlap and smoothes out the result. However, this also increases the number of samples significantly, which defeats the purpose of using irradiance caching.

One of the most effective approaches to improve the interpolation is estimating the irradiance gradients at the sampling points, which basically tell how the irradiance changes when the position and orientation of the surface changes. We cannot evaluate the irradiance gradients exactly, but we can find reasonable approximations with the information that we already have from rendering an hemicube.

In my previous article I explained that the irradiance integral was just a weighted sum over the radiance samples, where the contribution of each sample is the product of the radiance through each of the hemicube texels [latex]L_i[/latex], the solid angle of the texel [latex]A_i[/latex], and the cosine term [latex]\cos_{\theta_i}[/latex]. That is, the irradiance [latex]E(\mathbf{x})[/latex] at a point [latex]\mathbf{x}[/latex] is approximated as:

[latex]E(\mathbf{x}) \simeq \sum_{i=0}^N A_i L_i \cos\theta_i[/latex]

The irradiance gradients consider how each these terms change under infinitesimal rotations and translations and can be obtained by differentiating [latex]E(\mathbf{x})[/latex].

[latex]\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N \nabla A_i L_i \cos\theta_i[/latex]

The radiance term is considered to be constant, so the equation reduces to:

[latex]\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla A_i \cos\theta_i[/latex]

In reality, the radiance is not really constant, but the goal is to estimate the gradients without using any information in addition to what we have already obtained by rendering the scene on a hemicube. So that we can improve the quality of the interpolation by only doing some additional computations while integrating the hemicube.

The rotation gradient expresses how the irradiance changes as the hemicube is rotated in any direction:

[latex]\nabla_r E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla_r A_i \cos\theta_i[/latex]

The most important observation is that as the hemicube rotates, the solid angle of the texels with respect to the origin remains constant. So, the only factor that influences the gradient is the cosine term:

[latex]\nabla_r A_i \cos\theta_i = A_i \nabla_r \cos\theta_i[/latex]

With this in mind, the rotation gradient is simply the rotation axis scaled by the rate of change, that is, the angle gradient:

[latex]\frac{\partial }{\partial \theta_i} \cos\theta_i = -\sin\theta_i[/latex]

And the rotation axis is given by the cross product of the z axis and the texel direction [latex]\mathbf{d}_i[/latex]:

[latex]\mathbf{v}_i = \left| \mathbf{d}_i \times (0, 0, 1) \right|[/latex]

Therefore:

[latex]\nabla_r \cos\theta_i = -\mathbf{v}_i \sin\theta_i[/latex]

This can be simplified further by noting that [latex]\sin\theta_i[/latex] is the length of [latex]\mathbf{v}_i[/latex] before normalization and that results in a very simple expression:

[latex]\nabla_r \cos\theta_i = (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)[/latex]

Finally, the rotation gradient for the hemisphere is the sum of the gradients corresponding to each texel.

[latex]\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i A_i (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)[/latex]

The resulting code is trivial:

foreach texel, color in hemicube { Vector2 v = texel.solid_angle * Vector2(texel.dir.y, -texel.dir.x); rotation_gradient[0] += v * color.x; rotation_gradient[1] += v * color.y; rotation_gradient[2] += v * color.z; } |

The derivation of the translation gradients is a bit more complicated than the rotation gradients because the solid angle term is not invariant under translations anymore.

My first approach was to simplify the expression of the solid angle term by using an approximation that is easier to differentiate. Instead of using the exact texel solid angle like I had been doing so far, I approximated it by the differential solid angle scaled by the constant area of the texel, which is a reasonable approximation if the texels are sufficiently small.

In general, the differential solid angle in a given direction [latex]\Theta[/latex] is given by:

[latex]d\omega_\Theta = \frac{\mathbf{n}\cdot\Theta}{r^2} da[/latex]

For the particular case of the texels of the top face of the hemicube, **n** is equal to the z axis. So, the differential solid angle is:

[latex]d\omega_\Theta = \frac{\cos\theta}{r^2} da[/latex]

We also know that:

[latex]\cos\theta = 1/r[/latex]

which simplifies the expression further:

[latex]d\omega_\Theta = (\cos\theta)^3 da[/latex]

Using this result and factoring the area of the texels out of the sum we obtain the following expression for the gradient:

[latex]\nabla_t E_z(\mathbf{x}) \simeq 4 \sum_{i=0}^N L_i \nabla_t (\cos\theta_i)^4[/latex]

The remaining faces have similar expressions, slightly more complex, but still easy to differentiate along the x and y directions. We could continue along this path and we would obtain closed formulas that can be used to estimate the translation gradients. However, this simple approach did not produce satisfactory results. The problem is that these gradients assume that the only thing that changes under translation is the projected solid angle of the texels, but that ignores the parallax effect and the occlusion between neighboring texels. That is, samples that correspond to objects that are close to the origin of the hemicube should move faster than those that that are farther, and as they move they may occlude nearby samples.

As you can see in the following lightmap, the use of these gradients resulted in smoother results in areas that are away from occluders, but produced incorrect results whenever the occluders are nearby.

The hexagonal shape corresponds to the ceiling of a room that has walls along its sides and a window through which the light enters. The image on the left is a reference lightmap with one hemicube sample per pixel. The image on the middle is the same lightmap computed using irradiance caching taking only 2% of the samples. Note that the number of samples is artificially low to emphasize the artifacts. On the right side the same lightmap is computed using the proposed gradients. If you look closely you can notice that on the interior the lightmap looks smoother, but close to the walls the artifacts remain.

This is exactly the same flaw of the gradients that Krivanek et al proposed in Radiance Caching for Efficient Global Illumination Computation, but was later corrected in the followup paper Improved Radiance Gradient Computation by approaching the problem the same way Greg Ward did in the original original *Irradiance Gradients* paper.

I then tried follow the same approach. The basic idea is to consider the cosine term invariant under translation so that only the area term needs to be differentiated, and to express the gradient of the cell area in terms of the marginal area changes between adjacent cells. The most important observation is that the motion of the boundary between cells is always determined by the closest sample, so this approach takes occlusion into account.

Unfortunately we cannot directly use Greg Ward’s gradients, because they are closely tied to the stratified Montecarlo distribution and we are using a hemicube sampling distribution. However, we can still apply the same methodology to arrive to a formulation of the gradients that we can use in our setting.

In our case, the cells are the hemicube texels projected onto the hemisphere and the cell area is the texel solid angle. To determine the translation gradients of the texel’s solid angle is equivalent to computing the sum of the marginal gradients corresponding to each of the texel walls.

These marginal gradients are the wall normals projected onto the translation plane, scaled by the length of the wall, and multiplied by the rate of motion of the wall in that direction.

Since the hemicube uses a gnomonic projection, where lines in the plane map to great circles in the sphere, the texel boundaries of the hemicubes map to great arcs in the hemisphere. So, the length of these arcs is very easy to compute. Given the direction of the two texel corners [latex]\mathbf{d}_0, \mathbf{d}_1[/latex], the length of their hemispherical projection is:

[latex]arclength(\mathbf{d}_0, \mathbf{d}_1) = \arccos{\mathbf{d}_0 \cdot \mathbf{d}_1}[/latex]

The remaining problem is to compute the rate of motion of the wall, which as Greg Ward noted has to be proportional to the minimum distance of the two samples.

The key observation is that we can classify all the hemicube edges in wo categories: Edges with constant longitude ([latex]\theta[/latex] is invariant), and edges with constant latitude ([latex]\phi[/latex] is invariant).

In each of these cases we can estimate the rate of change of edges with respect to the normal direction by considering the analogous problem in the canonical hemispherical parametrization:

[latex]r = \sqrt{x^2+y^2+z^2}[/latex]

[latex]\phi = \arctan\frac{y}{x}[/latex]

[latex]\theta = \arccos\frac{z}{r}[/latex]

For longitudinal edges we can consider how [latex]\theta[/latex] changes with respect to motion along the x axis:

[latex]\frac{\partial\theta}{\partial x} = \frac{-\cos\theta}{r}[/latex]

and for latitudinal edges we can consider how [latex]\phi[/latex] changes with respect to motion along the y axis:

[latex]\frac{\partial\phi}{\partial y} = \frac{-1}{r \sin\theta}[/latex]

As we noted before, the rate of the motion is dominated by the nearest edge, so [latex]r[/latex] takes the value of the minimum depth of the two samples adjacent to the edge.

A detailed derivation for these formulas can be found in Wojciech Jarosz’s dissertation, which is now publicly available online. His detailed explanation is very didactic and I highly recommend reading it to get a better understanding of how these formulas are obtained.

Note that these are just the same formulas used by Greg Ward and others. In practice, the only thing that changes is the layout of the cells and the length of the walls between them.

As can be seeing in the following picture, the new gradients produce much better results. Note that the appearance on the interior of the lightmap is practically the same, but closer to the walls the results are now much smoother. Keep in mind that artifacts are still present, because the number of samples is artificially low.

In order to estimate the translation gradients efficiently we precompute the direction and magnitude of the marginal gradients corresponding to each texel edge without taking the distance to the edge into account:

// Compute the length of the edge walls multiplied by the rate of motion // with respect to motion in the normal direction foreach edge in hemicube { float wall_length = acos(dot(d0, d1)); Vector2 projected_wall_normal = normalize(cross(d0, d1).xy); float motion_rate = (constant_altitude(d0,d1) ? -cos_theta(d0,d1) : // d(theta)/dx = -cos(theta) / depth -1 / sin_theta(d0,d1); // d(phi)/dy = -1 / (sin(theta) * depth) edge.translation_gradient = projected_wall_normal * wall_length * motion_rate; } |

Then, during integration we split the computations in two steps. First, we accumulate the marginal gradients of each of the edges scaled by the minimum edge distance:

foreach edge in hemicube { float depth0 = depth(edge.x0, edge.y0); float depth1 = depth(edge.x1, edge.y1); float min_depth = min(depth0, depth1); texel[edge.x0, edge.y0].translation_gradient -= edge.gradient / min_depth; texel[edge.x1, edge.y1].translation_gradient += edge.gradient / min_depth; } |

Once we have the per texel gradient, we compute the final irradiance gradient as the sum of the texel gradients weighted by the texel colors:

foreach texel, color in hemicube { Vector2 translation_gradient = texel.translation_gradient * texel.clamped_cosine; translation_gradient_sum[0] += translation_gradient * color.x; translation_gradient_sum[1] += translation_gradient * color.y; translation_gradient_sum[2] += translation_gradient * color.z; } |

Irradiance Caching provides a very significant speedup to our lightmap baker. On typical meshes we only need to render about 10 to 20% of the samples to approximate the irradiance at very high quality, and for lower quality results or fast previews we can get away with as few as 3% of the samples.

The use of irradiance gradients for interpolation allows reducing the number of samples significantly, or for the same number of samples produces much higher quality results. The following picture compares the results without irradiance gradients and with them, in both cases the number of samples and their location is the same, note how the picture at the bottom has much smoother lightmaps.

I think that without irradiance caching the hemicube rendering approach to global illumination would have not been practical. That said, implementing irradiance caching robustly is tricky and a lot of tuning and tweaking is required in order to obtain good results.

*Note: This article is also published at The Witness blog.*