<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Ignacio Castaño &#8211; The Witness</title>
	<atom:link href="https://the-witness.net/news/author/castano/feed/" rel="self" type="application/rss+xml" />
	<link>https://the-witness.net/news</link>
	<description>Explore an abandoned island.</description>
	<lastBuildDate>Wed, 11 Jan 2017 18:38:22 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.9</generator>
	<item>
		<title>HLSLParser</title>
		<link>https://the-witness.net/news/2014/08/hlslparser/</link>
					<comments>https://the-witness.net/news/2014/08/hlslparser/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Fri, 29 Aug 2014 08:21:58 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=2663</guid>

					<description><![CDATA[We just finished porting The Witness to D3D11. It took a little longer than I was hoping and one of the unexpected difficulties was getting our D3D9 HLSL shaders to work in D3D11. The D3D shader compiler provides a backward compatibility flag (D3DCOMPILE_ENABLE_BACKWARDS_COMPATIBILITY) that I was hoping would allow us …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2014/08/hlslparser/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>We just finished porting The Witness to D3D11. It took a little longer than I was hoping and one of the unexpected difficulties was getting our D3D9 HLSL shaders to work in D3D11. </p>
<p>The D3D shader compiler provides a backward compatibility flag (D3DCOMPILE_ENABLE_BACKWARDS_COMPATIBILITY) that I was hoping would allow us to use our shaders 'as is'. Unfortunately, it seems the backward compatibility mode has not received much love and things were not that simple. I run into several compiler internal errors, and even after finding workarounds most of our shaders did not actually compile, but would have required significant modifications, the resulting vertex and pixel shaders would often not link at runtime, and the use of register annotations on samplers only allowed us to control the layout of the samplers, but not of the associated textures.</p>
<p>For other targets (Cg &amp; PSSL) we had been able to get away with some preprocessor macros and simple text transformations, but it seemed that was not going to be an option in this case. Not only that, having to write and adapt our shader code for the idiosyncrasies of each compiler was starting to become a challenge.</p>
<p>Instead, I thought it would be much cleaner and much more powerful to standardize on a fixed subset of HLSL and transform it to the shading language of the target by building a syntax tree, applying syntactic transformations, and generating shader code by traversing the resulting syntax tree.</p>
<p>This is essentially what we already do in the OpenGL backend to transform GLSL into HLSL. For that we use <a href="https://github.com/aras-p/hlsl2glslfork">HLSL2GLSL</a> and <a href="https://github.com/aras-p/glsl-optimizer">GLSLOptimizer</a>. I considered using these same projects, either modifying HLSL2GLSL to output HLSL1x code instead of GLSL, or modifying GLSLOptimizer to accept and produce HLSL code. I avoided the former, because HLSL2GLSL's code was a big mess and it was not very attractive to work with it. The latter was more interesting, and I believe it's the approach taken by the UE4 engine. However, while much cleaner, GLSLOptimizer is still a fairly large project and I did not care too much about the optimization aspects that it offered.</p>
<p>Another option that I had just come across is Max McGuire's <a href="https://github.com/unknownworlds/hlslparser">HLSLParser</a>. HLSLParser was a lot smaller and cleaner than either of those code bases. It was pretty much the code I would have written if I were to do this from scratch, so it seemed like the best possible starting point.</p>
<p>One of the main annoyances when working in the OpenGL backend is that GLSL errors are extremely hard to track down. They can be reported by either HLSL2GLSL, GLSLOptimizer, or the driver's GLSL compiler. Each of the inputs to these compilers has different file and line information and none of them matches the original source code. Whenever an error occurs we have to dump the intermediate code and figure out from there what's going on. It's usually a waste of time and if D3D11 is to become our primary backend I did not want to have to deal with this issue.</p>
<p>I was happy to see that HLSLParser handled this correctly associating file and line info to the tree nodes and producing correct #file preprocessor statements in the generated output.</p>
<p>In order to parse our shaders with HLSLParser we had to extend it in several ways, here's a partial list of the features I added:</p>
<ul>
<li>Support for 3D, 2DMS and shadow samplers, and most of the associated intrinsics.</li>
<li>Added support for many missing intrinsics.</li>
<li>out argument modifier.</li>
<li>Default argument values.</li>
<li>Block statements.</li>
<li>Attribute annotations.</li>
<li>Static, inline, uniform and other type modifiers.</li>
<li>Bitwise operators.</li>
<li>Hex literals.</li>
<li>Full declaration syntax, including multiple declarations in the statement.</li>
<li>Add support for full declarations in buffer fields.</li>
<li>Effect syntax (techniques, passes, sampler states, state assignments).</li>
</ul>
<p>I'm very thankful for the work Max McGuire did on HLSLParser and the least I can do is to release these our improvements to the public as well. You can find our fork of the project in our github repository:</p>
<p><a href="https://github.com/Thekla/hlslparser">https://github.com/Thekla/hlslparser</a></p>
<p>Once we have the syntax tree in memory we can do many cool code transformations. In particular, we do:</p>
<ul>
<li>Dead code elimination.</li>
<li>Determine used parameters and resources.</li>
<li>Assign registers/offsets explicitly to control layout of parameters and resources.</li>
<li>Sort resource registers and group parameters in buffers based on frequency of change.</li>
<li>Rename semantics.</li>
<li>Reorder pixel shader arguments so that vertex shader outputs match pixel shader inputs and avoid linkage errors.</li>
</ul>
<p>The shaders that result from these transformations are minimal, that is, they only contain the definitions and declarations that are necessary for a specific entry point. We have complete control of the layout of resources and parameters and extract all the necessary information about the shader from the tree instead of relying on the platform-specific reflection APIs. The shader transformation is very fast and the resulting shaders compile faster than the original ones. Our new D3D11 shader processor and compiler is about 4 times faster than its D3D9 counterpart.</p>
<p>While it currently accepts all of our shaders, HLSLParser still has some limitations. Here are a few that come to mind:</p>
<ul>
<li>No type inference for sampler types. It assumes sampler == sampler2D.</li>
<li>No support for {} initializers. Instead of float2 p = {0, 0} you have to write float2 p = float2(0, 0). This particular case is not complicated, but it does get more hairy when initializing structures. For now, I've simply modified our shaders to avoid this.</li>
<li>No support for rectangular matrices. We did not support them in the GLSL translators either, so we avoided their use in our shaders. It would be nice to add this eventually.</li>
<li>No support for do, while or switch statements. These should be easy to add, but we just didn't need them!</li>
<li>No proper error recovery. We stop reporting errors after first one. This would be nice, but is not critical.</li>
<li>Semantic analysis is incomplete. The parser does some, but not a complete semantic check. The generated shader is not guaranteed to be correct in the absence of errors, but since we have proper file/line annotations, this usually doesn't really matter.</li>
</ul>
<p>Looking forward, I expect we will add code generators to target other platforms. It should be really easy to bring the existing GLSLGenerator up to date, or add new generators for PSSL and Metal.</p>
<p>I'm not particularly fond of our shader code. On one side, we abuse the preprocessor to generate shader variations and the result is often a mess that is hard to read and modify. On the other, our code is often highly repetitive, but there doesn't seem to be a clean way to share it across different materials or structure it in a way that's more compact. I'm particularly interested in experimenting with higher level code transformations, or coming up with new constructs to facilitate writing and sharing shader code.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2014/08/hlslparser/feed/</wfw:commentRss>
			<slash:comments>17</slash:comments>
		
		
			</item>
		<item>
		<title>Shadow Mapping Summary &#8211; Part 1</title>
		<link>https://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/</link>
					<comments>https://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Tue, 24 Sep 2013 01:23:04 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=2281</guid>

					<description><![CDATA[During the last several weeks I've been polishing and optimizing the shadowing system and I think that at this point it won't undergo any more significant changes. In the process I've been reading a bit and looking up references and I'm appalled by how inaccessible is all the shadow mapping …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>During the last several weeks I've been polishing and optimizing the shadowing system and I think that at this point it won't undergo any more significant changes. In the process I've been reading a bit and looking up references and I'm appalled by how inaccessible is all the shadow mapping knowledge. For the most part it is scattered in obscure power point presentations, random some blog posts, SDK examples <a href="http://social.msdn.microsoft.com/Search/en-US/windows/desktop?query=shadow%20maps">from Microsoft</a> and hardware vendors (<a href="https://developer.nvidia.com/search/google_cse_adv/shadow">NVIDIA</a>, <a href="http://developer.amd.com/tools-and-sdks/graphics-development/amd-radeon-sdk/">AMD</a>, <a href="http://software.intel.com/en-us/search/site/language/en?query=shadow">Intel</a>), and numerous <a href="http://www.introgamedev.com/graphics_shadows.html">ShaderX / GPU Gems / GPU Pro books</a>.</p>
<p><img src="https://the-witness.net/news/wp-content/uploads/2013/09/shot_2013.09.18__time_14_11_n09.png" alt="shot_2013.09.18__time_14_11_n09" width="600" height="337" class="alignnone size-full wp-image-2295" /><br />
I'm not going to try to address that here, but just as I was writting this <a href="http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/">Matt Pettineo released a good overview</a> of some of the different techniques available that is a good starting point. Instead, I'll describe some of the problems that I faced when implementing and optimizing our shadow mapping implementation, some of the techniques that I tried and the solutions that worked best for us in the end.</p>
<p>While there are many techniques that I haven't tried and I'm sure our solution could be improved further, for this game I'm pretty comfortable with our implementation in terms of quality and performance.</p>
<p><span id="more-2281"></span></p>
<p>Jon wrote our initial shadow mapping system a few years ago and with the exception of some small improvements I made early on, it had remained pretty much unchanged until very recently, when it became obvious that shadow map rendering and sampling was too slow and needed to be improved drastically.</p>
<p>The posts where Jon describes our old shadow mapping system are <a href="https://the-witness.net/news/2010/03/graphics-tech-shadow-maps-part-1/">here</a> and <a href="https://the-witness.net/news/2010/04/graphics-tech-shadow-maps-part-2-save-25-texture-memory-and-possibly-much-more/">here</a>, but to summarize, we were using stable cascade shadow maps, with 4 partitions, distance based selection, and some innovative tricks to reduce unused shadow map space. For filtering we used randomized PCF sampling and a combination of gradient, slope and constant biases to prevent acne artifacts.</p>
<h2>Filtering</h2>
<p>One of the first thing I changed in the original implementation was the filtering method. Initially we used a Poisson distribution of PCF samples that was rotated randomly, similar to what is used in the Crysis engines. This approach has many advantages: it scales well reducing the number of samples, the kernel radius can be adjusted easily, but it often resulted in noise artifacts. </p>
<p>In many games, these artifacts would not be very noticeable in the presence of detailed textures and normal maps, but in The Witness the artifacts really stood off in contrast with our stylized geometry and smooth precomputed lighting.</p>
<p>We experimented with higher number of samples, but in our case the number of samples required to obtain smooth results defeated the purpose of using a sparse distribution. We were able to obtain much higher quality results with a compact box filter such as the one described by Bunnell and Pellacini in <a href="http://http.developer.nvidia.com/GPUGems/gpugems_ch11.html">Shadow Map Antialiasing</a>. The box filter produced smooth results, but didn't do a good job smoothing out the stair stepping artifacts resulting from the limited resolution of the shadow maps. A straightforward extension of their method that looked considerably better is to weight each of the samples using a Gaussian curve.</p>
<p>The naive implementation of this filter uses (N-1)x(N-1) PCF texture samples. In "Fast Conventional Shadow Filtering", Holger Grün shows how to reduce the box filter to (N/2)x(N/2) samples, but argues that separable filters require (N-1)x(N/2), and proposes to use Gather4 in order to reduce the number of samples down to (N/2)x(N/2). I think Grün was trying too hard to sell the Gather4 feature and didn't fully explore the bilinear PCF sampling approach. His approach to bilinear PCF sampling is too complicated and does not result in the lowest possible number of samples. Here I'll show how to compute the filter weights in a more efficient and much more simple way.</p>
<p>Let's first imagine a symmetric horizontal kernel q with 5 taps:</p>
<p><code></p>
<pre>
  q = {0, a, b, c, b, a, 0}
</pre>
<p></code></p>
<p>Each of these kernel weights corresponds to a bilinear PCF sample, that is, the actual footprint of the kernel is 6 shadow map texels and each of the kernel weights is a linear combination of our original kernel:</p>
<p><code></p>
<pre>
  k[x_] := s*q[[x]] + (1 - s)*q[[x + 1]]
</pre>
<p></code></p>
<p>where x is the integer texel coordinate and s is the fractional part.</p>
<p>The traditional way to apply this kernel is by simply taking 5 bilinear samples and weighting each one by the weights in q. However, you could achieve the same result taking 6 point samples and weighting each of them by the factors in k.</p>
<p>The important observation is that we can reduce the number of samples to a half (just 3 in this case) by spacing out the PCF samples so that their footprints do not overlap and adjusting the subtexel location of the samples and the corresponding weighting factors. That is, for every sample we want to find a new subpixel coordinate 'u' and a new weight 'w' that meets these constrains:</p>
<p><code></p>
<pre>
  eq[1, x_] := (k[x] == (1 - u) w)
  eq[2, x_] := (k[x + 1] == u*w)
</pre>
<p></code></p>
<p>Solving this equation system in Mathematica gives us the following result for each of the 3 samples:</p>
<p><code></p>
<pre>
  Simplify[Solve[{eq[1, 1], eq[2, 1]}, {u, w}]]

    u -&gt; (b + a s - b s)/(a + b - b s)
    w -&gt; a + b - b s

  Simplify[Solve[{eq[1, 3], eq[2, 3]}, {u, w}]]

    u -&gt; (b - b s + c s)/(b + c)
    w -&gt; b + c

  Simplify[Solve[{eq[1, 5], eq[2, 5]}, {u, w}]]

    u -&gt; (a s)/(a + b s)
    w -&gt; a + b s
</pre>
<p></code></p>
<p>This may be equivalent to what Grün does to half the number of samples in each of the rows in the kernel, but contrary to his method, this can be extended to separable 2D kernels.</p>
<p>Let's say we have a separable 5x5 filter like this:</p>
<p><code></p>
<pre>
  q = {{0, 0, 0, 0, 0, 0, 0},
    {0, a*a, a*b, a*c, a*b, a*a, 0},
    {0, b*a, b*b, b*c, b*b, b*a, 0},
    {0, c*a, c*b, c*c, c*b, c*a, 0},
    {0, b*a, b*b, b*c, b*b, b*a, 0},
    {0, a*a, a*b, a*c, a*b, a*a, 0},
    {0, 0, 0, 0, 0, 0, 0}}
</pre>
<p></code></p>
<p>In this case, the filter touches a 6x6 range of texels and each of the weights is a bilinear combination of the 4 nearest factors:</p>
<p><code></p>
<pre>
  k[x_, y_] := s*t*q[[x, y]] +
    (1 - s)*t*q[[x + 1, y]] +
    s*(1 - t)*q[[x, y + 1]] +
    (1 - s)*(1 - t)*q[[x + 1, y + 1]]
</pre>
<p></code></p>
<p>To find the new subpixel coordinates 'u' and 'v' and the corresponding weight 'w' we have the following equations:</p>
<p><code></p>
<pre>
  eq[1, x_, y_] := (k[x, y] == (1 - u) (1 - v) w)
  eq[2, x_, y_] := (k[x + 1, y] == u (1 - v)*w)
  eq[3, x_, y_] := (k[x, y + 1] == (1 - u) v*w)
  eq[4, x_, y_] := (k[x + 1, y + 1] == u*v*w)
</pre>
<p></code></p>
<p>Solving this equation system for the left-top sample gives us the following result:</p>
<p><code></p>
<pre>
Solve[{eq[1, 1, 1], eq[2, 1, 1], eq[3, 1, 1], eq[4, 1, 1]}, {u, v, w}]

    u -&gt; (b + a s - b s)/(a + b - b s) 
    v -&gt; (b + a t - b t)/(a + b - b t) 
    w -&gt; (a + b - b s) (a + b - b t)
</pre>
<p></code></p>
<p>We can repeat this for the remaining 8 PCF samples to compute the corresponding uv coordinates and weights. Looking at the results we can notice some common terms that appear due to the symmetries in the kernel. Once we factor the common sub-expressions and replace the a,b,c constants with scalars we can simplify the formulas and end up with fairly simple expressions. For example, in our case, in the 5x5 kernel we set the constants to 1, 3 and 4 to achieve a crude approximation to a Gaussian filter and this is the resulting code:</p>
<p><code></p>
<pre>
    float uw0 = (4 - 3 * s);
    float uw1 = 7;
    float uw2 = (1 + 3 * s);

    float u0 = (3 - 2 * s) / uw0 - 2;
    float u1 = (3 + s) / uw1;
    float u2 = s / uw2 + 2;

    float vw0 = (4 - 3 * t);
    float vw1 = 7;
    float vw2 = (1 + 3 * t);

    float v0 = (3 - 2 * t) / vw0 - 2;
    float v1 = (3 + t) / vw1;
    float v2 = t / vw2 + 2;

    float sum = 0;

    sum += uw0 * vw0 * do_pcf_sample(base_uv, u0, v0, z, dz_duv);
    sum += uw1 * vw0 * do_pcf_sample(base_uv, u1, v0, z, dz_duv);
    sum += uw2 * vw0 * do_pcf_sample(base_uv, u2, v0, z, dz_duv);

    sum += uw0 * vw1 * do_pcf_sample(base_uv, u0, v1, z, dz_duv);
    sum += uw1 * vw1 * do_pcf_sample(base_uv, u1, v1, z, dz_duv);
    sum += uw2 * vw1 * do_pcf_sample(base_uv, u2, v1, z, dz_duv);

    sum += uw0 * vw2 * do_pcf_sample(base_uv, u0, v2, z, dz_duv);
    sum += uw1 * vw2 * do_pcf_sample(base_uv, u1, v2, z, dz_duv);
    sum += uw2 * vw2 * do_pcf_sample(base_uv, u2, v2, z, dz_duv);

    sum *= 1.0f / 144;
</pre>
<p></code></p>
<p>The main advantage of the PCF separable sampling approach over Gather4 is not just that it works in a wider range of hardware, but that is also more efficient. With PCF sampling we need to compute the new sampling coordinates and weights, while with Gather4 we have to perform depth comparisons and filtering in the shader. The latter uses a lower number of instructions, but consumes more bandwidth between the ALUs and texture units and more importantly, it requires more registers to hold the results of the texture samples. That said, despite the lower performance, the use of Gather4 has some advantages that we will discuss in the next section.</p>
<p>For those that simply want to use our shadow sampling code without deriving formulas for their own weights, you can find our implementation integrated in <a href="http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/">Matt's shadow mapping example</a> under the "OptimizedPCF" option. Running his demo in the two GPUs that I have available, a NVIDIA GTX 480 and an Intel HD 4000, I've measured that our technique is between 10 and 20% faster than the Gather4 method.</p>
<h2>Acne</h2>
<p>Shadow acne is one of the major issues of most shadow mapping implementations. Eliminating shadow acne is such a pain, that I wonder why it's not received much more attention.</p>
<p>In our case I ended up employing what I believe is the most common solution: A combination of constant, slope-scale, <a href="http://c0de517e.blogspot.com/2011/05/shadowmap-bias-notes.html">normal</a> and receiver plane depth biases. Finding the right magnitudes for each of these offsets is complicated. If the offsets are too big, that results in <em>peter panning</em> or light leaking artifacts, if the offsets are too small, they don't eliminate acne entirely.</p>
<p>In <a href="http://www.shaderx4.com/TOC.html">Eliminating Surface Acne with Gradient Shadow Mapping</a> Christian Schuler proposes the use of linearly filtered depth values and fuzzy depth comparisons to mitigate the acne artifacts. However, these techniques are not possible when using PCF sampling.</p>
<p>Another alternative is to render backfaces to the shadow maps, but this approach is more prone to light leaking and does not really solve the acne problems on thin walls. Dual or midpoint shadow maps are interesting, but it seems like they would be more expensive, do not entirely solve the problem near silhouettes or with very thin objects, and impose additional modeling constrains.</p>
<p>The magnitude of the offsets is proportional to the size of the texels and the filtering radius, so a simple way of reducing them is using higher resolution shadows, but larger shadow maps also reduce performance. In the end we just try to find the best compromise between the two forms of artifacts and as a last resort constrain the modelling to prevent the most egregious artifacts.</p>
<p>One of the unfortunate disadvantages of our shadow filtering method is that we rely on the hardware texture units to sample 4 shadow texels simultaneously and perform the depth comparison. When using receiver plane depth bias we would like to use a different bias for each of the texels, but there's no way we can provide a depth gradient to the texture unit. As a result, the receiver plane depth bias is not very effective with our method. In fact, it's so ineffective, that we chose to disable it entirely and compensate with a larger normal and slope-scale bias.</p>
<p>For these biases to work well it's important to scale their magnitude correctly based on the surface normal and light direction. In particular, the slope-scale bias should be proportional to the tangent of the angle between the normal (N) and the light direction (L), and the normal bias should be proportional to the sine:</p>
<p><img src="https://the-witness.net/news/wp-content/uploads/2013/09/shadow_offsets.png" alt="shadow_offsets" width="512" height="201" class="aligncenter size-full wp-image-2330" /></p>
<p><code></p>
<pre>
float2 get_shadow_offsets(float3 N, float3 L) {
    float cos_alpha = saturate(dot(N, L));
    float offset_scale_N = sqrt(1 - cos_alpha*cos_alpha); // sin(acos(L·N))
    float offset_scale_L = offset_scale_N / cos_alpha;    // tan(acos(L·N))
    return float2(offset_scale_N, min(2, offset_scale_L));
}
</pre>
<p></code></p>
<p>Finally, one of the disadvantages of the normal bias is that it does not only bias the depth, it also offsets the shadow. On its own this offset is not very noticeable, but the offset amount is different for each of the cascades and that results in an obvious shift between cascades. Blending the cascades helps, but to reduce the problem further we also interpolate the magnitude of the bias from one cascade to the next, so that the shift is barely noticeable.</p>
<h2>Coming Next</h2>
<p>In part 2, I'll describe our cascade placement algorithm and finish with the optimizations that we use in order to speed up shadow map rendering.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/feed/</wfw:commentRss>
			<slash:comments>13</slash:comments>
		
		
			</item>
		<item>
		<title>scope(exit) in C++11</title>
		<link>https://the-witness.net/news/2012/11/scopeexit-in-c11/</link>
					<comments>https://the-witness.net/news/2012/11/scopeexit-in-c11/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Tue, 20 Nov 2012 01:38:01 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=1865</guid>

					<description><![CDATA[I really like the scope(exit) statements in D. They allow you to keep the cleanup code next to the initialization code making it much easier to maintain and understand. I wish we had them in C++. Turns out this can be implemented easily using some of the new C++ features. …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2012/11/scopeexit-in-c11/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>I really like the <a href="http://dlang.org/statement.html#ScopeGuardStatement">scope(exit) statements in D</a>. They allow you to keep the cleanup code next to the initialization code making it much easier to maintain and understand. I wish we had them in C++.</p>
<p>Turns out this can be implemented easily using some of the new C++ features. I'm not a big fan of C++11; for the most part I think it adds even more complexity to an already complex language. However, I have to admit that some of the features can be useful, when used with care and moderation.</p>
<p>I think this is one of such cases. If you can get past the weird syntax, lambdas can be very powerful. What I find most useful is to be able to define local functions (closures) that can access the local variables of the enclosing scope.</p>
<p>In our case, what we want to do is to define some code that is executed at the end of the scope to cleanup objects allocated in the current scope. This is easily achieved allocating an object on the stack that invokes a lambda function that wraps our custom cleanup code:</p>
<pre>#include &lt;functional&gt;

struct ScopeExit {
    ScopeExit(std::function f) : f(f) {}
    ~ScopeExit() { f(); }
    std::function f;
};

#define STRING_JOIN2(arg1, arg2) DO_STRING_JOIN2(arg1, arg2)
#define DO_STRING_JOIN2(arg1, arg2) arg1 ## arg2
#define SCOPE_EXIT(code) \
    ScopeExit STRING_JOIN2(scope_exit_, __LINE__)([=](){code;})
</pre>
<p>Then you can write code like:</p>
<pre>int * ip = new int[16];
SCOPE_EXIT(delete [] ip);

FILE * fp = fopen("test.out", "wb");
SCOPE_EXIT(fclose(fp));
</pre>
<p>I know you can go to greater lengths to mimic the D syntax, and that Boost also provides similar functionality, but this does all we need in less than 10 lines of code.</p>
<p>Sadly we cannot really use this on The Witness, because I'm not sure we are yet ready to drop support for platforms and compilers that do not implement the required C++11 functionality.</p>
<p><em><strong>Update:</strong></em></p>
<p>As mentioned by Daniel in the comments, you don't really want to use std::function in this case. A quick look at the assembly output confirms that the generated code is horrible. It makes a lot more sense to wrap the closure explicitly and rely on type inference to allocate the wrapper object on the stack:</p>
<pre>template &lt;typename F&gt;
struct ScopeExit {
    ScopeExit(F f) : f(f) {}
    ~ScopeExit() { f(); }
    F f;
};

template &lt;typename F&gt;
ScopeExit&lt;F&gt; MakeScopeExit(F f) {
    return ScopeExit&lt;F&gt;(f);
};

#define SCOPE_EXIT(code) \
    auto STRING_JOIN2(scope_exit_, __LINE__) = MakeScopeExit([=](){code;})
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2012/11/scopeexit-in-c11/feed/</wfw:commentRss>
			<slash:comments>61</slash:comments>
		
		
			</item>
		<item>
		<title>Seamless Cube Map Filtering</title>
		<link>https://the-witness.net/news/2012/02/seamless-cube-map-filtering/</link>
					<comments>https://the-witness.net/news/2012/02/seamless-cube-map-filtering/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Mon, 27 Feb 2012 18:36:38 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=1502</guid>

					<description><![CDATA[Modern GPUs filter seamlessly across cube map faces. This feature is enabled automatically when using Direct3D 10 and 11 and in OpenGL when using the ARB_seamless_cube_map extension. However, it's not exposed through Direct3D 9 and it's just not available in any of the current generation consoles. There are several solutions …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2012/02/seamless-cube-map-filtering/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>Modern GPUs filter seamlessly across cube map faces. This feature is enabled automatically when using Direct3D 10 and 11 and in OpenGL when using the <a href="http://www.opengl.org/registry/specs/ARB/seamless_cube_map.txt">ARB_seamless_cube_map</a> extension. However, it's not exposed through Direct3D 9 and it's just not available in any of the current generation consoles.<br />
<span id="more-1502"></span><br />
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).</p>
<p>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 <a href="http://developer.amd.com/media/gpu_assets/Isidoro-CubeMapFiltering-Sketch-SIG05.pdf">proposed averaging cube map edge texels</a> 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 <a href="http://developer.amd.com/archive/gpu/cubemapgen/pages/default.aspx">AMD's CubeMapGen</a>, whose source code is now <a href="http://code.google.com/p/cubemapgen/">publicly available online</a>. While this seems like a good idea, a few minutes experimenting with CubeMapGen make it obvious that it does not always work very well!</p>
<h2>Embedded Texture Borders</h2>
<p>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. <a href="http://www.gamedev.net/blog/73/entry-2005516-seamless-filtering-across-faces-of-dynamic-cube-map/">Ysaneya shows</a> that in order to achieve that, the FOV needs to be tweaked as follows:</p>
<p><code>fov = 2.0 * atan(n / (n - 0.5))</code></p>
<p>where <code>n</code> is the resolution of the cube map.</p>
<p>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. </p>
<p>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.</p>
<p>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.</p>
<p>Usually these uv coordinates are computed so that the resulting direction vectors point to the texel centers. For an integer texel coordinate <code>x</code> in the <code>[0,n-1]</code> range we map it to a floating point coordinate <code>u</code> in the <code>[-1, 1]</code> range as follows:</p>
<p><code>map_1(x) = (x + 0.5) * 2 / n - 1</code></p>
<p>We then obtain the corresponding direction vector as follows:</p>
<p><code>dir = normalize(faceVector + faceU * map_1(x) + faceV * map_1(y)</code></p>
<p>When doing that, the texels at the borders do not map to <code>-1</code> and <code>1</code> exactly, but to:</p>
<p><code></p>
<pre>
map(0) = -1 + 1/n
map(n-1) = 1 - 1/n
</pre>
<p></code></p>
<p>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:</p>
<p><code>map_2(x) = 2 * x / (n - 1) - 1</code></p>
<p>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:</p>
<p><code>map_1'(x) = 2 / n</code></p>
<p>but in the second case it is slightly different:</p>
<p><code>map_2'(x) = 2 / (n - 1)</code></p>
<p>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.</p>
<p>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:</p>
<p><img src="https://the-witness.net/news/wp-content/uploads/2012/02/warp.png" alt="" width="360" height="222" class="alignright size-full wp-image-1545" /><code>warp3(x) = ax^3 + x</code></p>
<p>We can compose this function with our original mapping. The result around the origin is close to a linear identity, but we can adjust <code>a</code> to stretch the function closer to the face edges. In our case we want the values at <code>1-1/n</code> to produce <code>1</code> instead, so we can easily determine the value of <code>a</code> by solving:</p>
<p><code>warp3(1-1/n) = ax^3 + x = 1</code></p>
<p>which gives us:</p>
<p><code>a = n^2 / (n-1)^3</code></p>
<p>I implemented the linear stretch and cubic warping methods in <a href="http://code.google.com/p/nvidia-texture-tools/">NVTT</a> 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.</p>
<p>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:</p>
<p><a><img src="https://the-witness.net/news/wp-content/uploads/2012/02/seamless_warp.png" alt="" width="560" height="273" class="hover alignnone size-full wp-image-1546" /><img src="https://the-witness.net/news/wp-content/uploads/2012/02/seams.png" alt="" width="560" height="273" class="nohover alignnone size-full wp-image-1547" /></a></p>
<p>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.</p>
<h2>Modifed Texture Lookup</h2>
<p>However, a few days ago <a href="https://twitter.com/#!/SebLagarde">Sebastien Lagarde</a> integrated these methods in AMD's CubeMapGen. See <a href="http://seblagarde.wordpress.com/2012/02/26/amd-cubemapgen-for-physically-based-rendering/">this post</a> 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:</p>
<p><code></p>
<pre>
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;
}
</pre>
<p></code></p>
<p>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.</p>
<p>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:</p>
<p><code></p>
<pre>
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;
</pre>
<p></code></p>
<p>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 <code>log2(specular_power)</code>. If that's the case, the two transcendental instructions cancel out and the scale becomes a linear function of the specular power.</p>
<p>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:</p>
<p><a><img src="https://the-witness.net/news/wp-content/uploads/2012/02/examples_warp.png" alt="" width="566" height="582" class="nohover alignnone size-full wp-image-1549" /><img src="https://the-witness.net/news/wp-content/uploads/2012/02/examples_seamless.png" alt="" width="566" height="582" class="hover alignnone size-full wp-image-1548" /></a></p>
<p>I'd like to thank Sebastien Lagarde for his valuable feedback while testing these ideas and for providing the nice images accompanying this article.</p>
<p><em>Note: This article is also published at <a href="http://altdevblogaday.com/2012/02/27/seamless-cube-map-filtering/">AltDevBlogADay</a>.</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2012/02/seamless-cube-map-filtering/feed/</wfw:commentRss>
			<slash:comments>23</slash:comments>
		
		
			</item>
		<item>
		<title>Irradiance Caching &#8211; Part 1</title>
		<link>https://the-witness.net/news/2011/07/irradiance-caching-part-1/</link>
					<comments>https://the-witness.net/news/2011/07/irradiance-caching-part-1/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Thu, 14 Jul 2011 08:21:15 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=245</guid>

					<description><![CDATA[This is the first article I wanted to publish when I started writing about the technology behind The Witness. I had just spent a week or so implementing irradiance caching with rotation and translation gradients. Extending the equations of the gradient estimation to the hemicube sample distribution had proven to …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2011/07/irradiance-caching-part-1/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>This is the first article I wanted to publish when I started writing about the technology behind <a href="https://the-witness.net">The Witness</a>. I had just spent a week or so implementing irradiance caching with rotation and translation gradients. Extending the equations of the gradient estimation to the hemicube sample distribution had proven to be tricky and had taken me a considerable amount of effort. So, I thought it would be cool to document my derivation, so that others would not have to do the same. However, for that article to make sense I first had to provide some background, hence the previous two articles:</p>
<ul>
<li><a href="https://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/">Lightmap Parameterization</a></li>
<li><a href="https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/">Hemicube Rendering and Integration</a></li>
</ul>
<p>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.</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2011/06/irradiance_gradients_0.jpg"><img class="aligncenter size-large wp-image-1133" src="https://the-witness.net/news/wp-content/uploads/2011/06/irradiance_gradients_0-512x288.jpg" alt="" width="512" height="288" /></a><span id="more-245"></span></p>
<p>In <a href="https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/">my previous post</a> 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 <em>Irradiance Caching</em> solves all these problems. However, at the time I didn't know that, and my initial attempts at a solution were misguided.</p>
<h2>Texture Space Approaches</h2>
<p>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. <a href="http://freespace.virgin.net/hugo.elias/radiosity/radiosity.htm">Hugo Elias' popular article</a> also proposes a texture space hierarchical sampling method, and that persuaded me that working in texture space would be a good idea.</p>
<p>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.</p>
<p>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.</p>
<h2>Irradiance Caching</h2>
<p>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 <a href="http://radsite.lbl.gov/radiance/papers/sg88/paper.html">paper that introduced the technique for the first time</a> Greg Ward referred to it as <em>lazy irradiance evaluation</em>, which seems to me a much more appropriate name.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>For more details, the <a href="http://cgg.mff.cuni.cz/~jaroslav/papers/2008-irradiance_caching_class/index.htm">Siggraph '08 course</a> 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.</p>
<p>The <a href="http://www.pbrt.org/"><em>Physically Based Rendering book</em> by Matt Pharr &amp; Greg Humphreys</a> is an excellent resource and the <a href="https://github.com/mmp/pbrt-v2">accompanying source code</a> contains a good, albeit basic implementation.</p>
<p>Finally, Rory Driscoll provides a good introduction from a programmer's point of view: <a href="http://www.rorydriscoll.com/2009/01/18/irradiance-caching-part-1/">part 1</a>, <a href="http://www.rorydriscoll.com/2009/01/24/irradiance-caching-part-2/">part 2</a>.</p>
<p>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.</p>
<h2>Irradiance Gradients</h2>
<p>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.</p>
<p>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.</p>
<p>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 <img src='https://the-witness.net/news/wp-content/static-export/latex/27cde89021aa063e1ac9ec9a29656758882c42ee.png' alt='L_i' title='L_i' class='latex' />, the solid angle of the texel <img src='https://the-witness.net/news/wp-content/static-export/latex/f80e5a7455933dfa9ca9dbecec1e7dbb5bac7bf8.png' alt='A_i' title='A_i' class='latex' />, and the cosine term <img src='https://the-witness.net/news/wp-content/static-export/latex/13d49e15fe50523794cdd3e0550b0b38cc4e3d44.png' alt='\cos_{\theta_i}' title='\cos_{\theta_i}' class='latex' />. That is, the irradiance <img src='https://the-witness.net/news/wp-content/static-export/latex/79152869bb1d5290560705e4aaab440e69401218.png' alt='E(\mathbf{x})' title='E(\mathbf{x})' class='latex' /> at a point <img src='https://the-witness.net/news/wp-content/static-export/latex/4082975efe6765896ad20baa7cd64b8507d6328d.png' alt='\mathbf{x}' title='\mathbf{x}' class='latex' /> is approximated as:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/f7b28038b95677acd7b15c1b51c7f455f8686de7.png' alt='E(\mathbf{x}) \simeq \sum_{i=0}^N A_i L_i \cos\theta_i' title='E(\mathbf{x}) \simeq \sum_{i=0}^N A_i L_i \cos\theta_i' class='latex' /></p>
<p>The irradiance gradients consider how each these terms change under infinitesimal rotations and translations and can be obtained by differentiating <img src='https://the-witness.net/news/wp-content/static-export/latex/79152869bb1d5290560705e4aaab440e69401218.png' alt='E(\mathbf{x})' title='E(\mathbf{x})' class='latex' />.</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/2619e668407498964d313a6f0e0cff450f940449.png' alt='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N \nabla A_i L_i \cos\theta_i' title='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N \nabla A_i L_i \cos\theta_i' class='latex' /></p>
<p>The radiance term is considered to be constant, so the equation reduces to:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/850b19d222497037293fab8bedc5253ae3edaa8a.png' alt='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla A_i \cos\theta_i' title='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla A_i \cos\theta_i' class='latex' /></p>
<p>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.</p>
<h3>Rotation Gradient</h3>
<p>The rotation gradient expresses how the irradiance changes as the hemicube is rotated in any direction:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/6d8fdaed64003d6b61c45151f8f8d32d0b4f450e.png' alt='\nabla_r E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla_r A_i \cos\theta_i' title='\nabla_r E(\mathbf{x}) \simeq \sum_{i=0}^N L_i \nabla_r A_i \cos\theta_i' class='latex' /></p>
<p>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:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/14f07e173c6797a87d60955471ac829726c04d86.png' alt='\nabla_r A_i \cos\theta_i = A_i \nabla_r \cos\theta_i' title='\nabla_r A_i \cos\theta_i = A_i \nabla_r \cos\theta_i' class='latex' /></p>
<p>With this in mind, the rotation gradient is simply the rotation axis scaled by the rate of change, that is, the angle gradient:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/933eab5d03c2f797793ad0d2023c398eeb34b90f.png' alt='\frac{\partial }{\partial \theta_i} \cos\theta_i = -\sin\theta_i' title='\frac{\partial }{\partial \theta_i} \cos\theta_i = -\sin\theta_i' class='latex' /></p>
<p>And the rotation axis is given by the cross product of the z axis and the texel direction <img src='https://the-witness.net/news/wp-content/static-export/latex/e512180fe21b1959a65698123b7236e73c85ad4a.png' alt='\mathbf{d}_i' title='\mathbf{d}_i' class='latex' />:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/44638cff0688887d8004ce44f5c251373d00bf88.png' alt='\mathbf{v}_i = \left| \mathbf{d}_i \times (0, 0, 1) \right|' title='\mathbf{v}_i = \left| \mathbf{d}_i \times (0, 0, 1) \right|' class='latex' /></p>
<p>Therefore:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/8f072554de8c43061694329d9cfbebb887663045.png' alt='\nabla_r \cos\theta_i = -\mathbf{v}_i \sin\theta_i' title='\nabla_r \cos\theta_i = -\mathbf{v}_i \sin\theta_i' class='latex' /></p>
<p>This can be simplified further by noting that <img src='https://the-witness.net/news/wp-content/static-export/latex/080c2d9b6e3cf8bbd9732b672aeb00ae2a31b3f3.png' alt='\sin\theta_i' title='\sin\theta_i' class='latex' /> is the length of <img src='https://the-witness.net/news/wp-content/static-export/latex/47601a9bb91bfdf292a941e1fc121b5b27350f46.png' alt='\mathbf{v}_i' title='\mathbf{v}_i' class='latex' /> before normalization and that results in a very simple expression:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/6b7c0a301f63edcce7fb1933470019c5c208c353.png' alt='\nabla_r \cos\theta_i = (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)' title='\nabla_r \cos\theta_i = (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)' class='latex' /></p>
<p>Finally, the rotation gradient for the hemisphere is the sum of the gradients corresponding to each texel.</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/818ea7f7b21d320408e01f62d63e0adc14adede6.png' alt='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i A_i (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)' title='\nabla E(\mathbf{x}) \simeq \sum_{i=0}^N L_i A_i (\mathbf{d}_{yi}, -\mathbf{d}_{xi}, 0)' class='latex' /></p>
<p>The resulting code is trivial:</p>
<p>&nbsp;</p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;">foreach texel, color in hemicube <span style="color: #008000;">&#123;</span>
    Vector2 v <span style="color: #000080;">=</span> texel.<span style="color: #007788;">solid_angle</span> <span style="color: #000040;">*</span> Vector2<span style="color: #008000;">&#40;</span>texel.<span style="color: #007788;">dir</span>.<span style="color: #007788;">y</span>, <span style="color: #000040;">-</span>texel.<span style="color: #007788;">dir</span>.<span style="color: #007788;">x</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    rotation_gradient<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">0</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> v <span style="color: #000040;">*</span> color.<span style="color: #007788;">x</span><span style="color: #008080;">;</span>
    rotation_gradient<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">1</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> v <span style="color: #000040;">*</span> color.<span style="color: #007788;">y</span><span style="color: #008080;">;</span>
    rotation_gradient<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">2</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> v <span style="color: #000040;">*</span> color.<span style="color: #007788;">z</span><span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p>&nbsp;</p>
<h3>Translation Gradients</h3>
<p>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.</p>
<p>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.</p>
<p>In general, the differential solid angle in a given direction <img src='https://the-witness.net/news/wp-content/static-export/latex/16ed0d4fe3706c474d81ee1ed2c9ffac844b8397.png' alt='\Theta' title='\Theta' class='latex' /> is given by:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/a6cf47821dc8aa65f361e3550e0bd896f30ccce9.png' alt='d\omega_\Theta = \frac{\mathbf{n}\cdot\Theta}{r^2} da' title='d\omega_\Theta = \frac{\mathbf{n}\cdot\Theta}{r^2} da' class='latex' /></p>
<p>For the particular case of the texels of the top face of the hemicube, <strong>n</strong> is equal to the z axis. So, the differential solid angle is:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/ec429d53c8dbe751e6189c60ad04971b313ea1f7.png' alt='d\omega_\Theta = \frac{\cos\theta}{r^2} da' title='d\omega_\Theta = \frac{\cos\theta}{r^2} da' class='latex' /></p>
<p>We also know that:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/0ddb8335aa4c0b025a555b3c2508375399985dfc.png' alt='\cos\theta = 1/r' title='\cos\theta = 1/r' class='latex' /></p>
<p>which simplifies the expression further:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/5366fdeb1552bd5093cd6f285d977804040a9cb2.png' alt='d\omega_\Theta = (\cos\theta)^3 da' title='d\omega_\Theta = (\cos\theta)^3 da' class='latex' /></p>
<p>Using this result and factoring the area of the texels out of the sum we obtain the following expression for the gradient:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/fc8a2ba78447401d68b4791c6fc937b6be11ec69.png' alt='\nabla_t E_z(\mathbf{x}) \simeq 4 \sum_{i=0}^N L_i \nabla_t (\cos\theta_i)^4' title='\nabla_t E_z(\mathbf{x}) \simeq 4 \sum_{i=0}^N L_i \nabla_t (\cos\theta_i)^4' class='latex' /></p>
<p>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.</p>
<p>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.</p>
<p><div id="attachment_727" style="width: 610px" class="wp-caption aligncenter"><img aria-describedby="caption-attachment-727" class="size-full wp-image-727" src="https://the-witness.net/news/wp-content/uploads/2011/01/translation_gradient_comparison_a.png" alt="" width="600" height="157" /><p id="caption-attachment-727" class="wp-caption-text">Left: Reference. Middle: Irradiance caching without gradients. Right: Irradiance caching with flawed gradients</p></div></p>
<p>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.</p>
<p>This is exactly the same flaw of the gradients that Krivanek et al proposed in <a href="http://cgg.mff.cuni.cz/~jaroslav/papers/tvcg2005/krivanek05radiance_caching.pdf">Radiance Caching for Efficient Global Illumination Computation</a>, but was later corrected in the followup paper <a href="http://cgg.mff.cuni.cz/~jaroslav/papers/sccg2005/sccg2005-krivanek.pdf">Improved Radiance Gradient Computation</a> by approaching the problem the same way Greg Ward did in the original <a href="http://radsite.lbl.gov/radiance/papers/erw92/paper.pdf">original <em>Irradiance Gradients</em> paper</a>.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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 <img src='https://the-witness.net/news/wp-content/static-export/latex/a9a14e60a6178c5c6e4c2dc27547118d449de17e.png' alt='\mathbf{d}_0, \mathbf{d}_1' title='\mathbf{d}_0, \mathbf{d}_1' class='latex' />, the length of their hemispherical projection is:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/5aac7a14f6ff61952d5c2acb473d9dc3020ba370.png' alt='arclength(\mathbf{d}_0, \mathbf{d}_1) = \arccos{\mathbf{d}_0 \cdot \mathbf{d}_1}' title='arclength(\mathbf{d}_0, \mathbf{d}_1) = \arccos{\mathbf{d}_0 \cdot \mathbf{d}_1}' class='latex' /></p>
<p>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.</p>
<p>The key observation is that we can classify all the hemicube edges in wo categories: Edges with constant longitude (<img src='https://the-witness.net/news/wp-content/static-export/latex/7c7f2dca53b671b80ef9a9d8de499f0026b78004.png' alt='\theta' title='\theta' class='latex' /> is invariant), and edges with constant latitude (<img src='https://the-witness.net/news/wp-content/static-export/latex/558446561c6788d797cec821bc6dc4e6346dc1d7.png' alt='\phi' title='\phi' class='latex' /> is invariant).</p>
<p>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 <a href="http://mathworld.wolfram.com/SphericalCoordinates.html">canonical hemispherical parametrization</a>:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/ba505c00ec0cd1ccbc338a3a45e98ab414c5e18b.png' alt='r = \sqrt{x^2+y^2+z^2}' title='r = \sqrt{x^2+y^2+z^2}' class='latex' /><br />
<img src='https://the-witness.net/news/wp-content/static-export/latex/e6823ce594fdeba557a5b4c2002eb49b84f99c1b.png' alt='\phi = \arctan\frac{y}{x}' title='\phi = \arctan\frac{y}{x}' class='latex' /><br />
<img src='https://the-witness.net/news/wp-content/static-export/latex/9f34f7ad892d55886b3f838414a076eda66b68dd.png' alt='\theta = \arccos\frac{z}{r}' title='\theta = \arccos\frac{z}{r}' class='latex' /></p>
<p>For longitudinal edges we can consider how <img src='https://the-witness.net/news/wp-content/static-export/latex/7c7f2dca53b671b80ef9a9d8de499f0026b78004.png' alt='\theta' title='\theta' class='latex' /> changes with respect to motion along the x axis:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/9cc56ce54c0c16007ae2017bf7dda65a2f9f98a9.png' alt='\frac{\partial\theta}{\partial x} = \frac{-\cos\theta}{r}' title='\frac{\partial\theta}{\partial x} = \frac{-\cos\theta}{r}' class='latex' /></p>
<p>and for latitudinal edges we can consider how <img src='https://the-witness.net/news/wp-content/static-export/latex/558446561c6788d797cec821bc6dc4e6346dc1d7.png' alt='\phi' title='\phi' class='latex' /> changes with respect to motion along the y axis:</p>
<p><img src='https://the-witness.net/news/wp-content/static-export/latex/b1de83215c7293e59839d7e9ef2f49fb4d10d8b5.png' alt='\frac{\partial\phi}{\partial y} = \frac{-1}{r \sin\theta}' title='\frac{\partial\phi}{\partial y} = \frac{-1}{r \sin\theta}' class='latex' /></p>
<p>As we noted before, the rate of the motion is dominated by the nearest edge, so <img src='https://the-witness.net/news/wp-content/static-export/latex/cd8c8e41e6057ef859c6ea3c1b21f3e4b5df0dcf.png' alt='r' title='r' class='latex' /> takes the value of the minimum depth of the two samples adjacent to the edge.</p>
<p>A detailed derivation for these formulas can be found in <a href="http://zurich.disneyresearch.com/~wjarosz/publications/dissertation/">Wojciech Jarosz's dissertation</a>, 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.</p>
<p>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.</p>
<p>As can be seen 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.</p>
<p><div id="attachment_1048" style="width: 610px" class="wp-caption aligncenter"><img aria-describedby="caption-attachment-1048" class="size-full wp-image-1048" src="https://the-witness.net/news/wp-content/uploads/2011/06/translation_gradient_comparison.png" alt="" width="600" /><p id="caption-attachment-1048" class="wp-caption-text">Left: No gradients. Middle: Flawed translation gradients. Right: Improved translation gradients.</p></div></p>
<p>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:</p>
<p>&nbsp;</p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;"><span style="color: #666666;">// Compute the length of the edge walls multiplied by the rate of motion</span>
<span style="color: #666666;">// with respect to motion in the normal direction</span>
foreach edge in hemicube <span style="color: #008000;">&#123;</span>
    <span style="color: #0000ff;">float</span> wall_length <span style="color: #000080;">=</span> <span style="color: #0000dd;">acos</span><span style="color: #008000;">&#40;</span>dot<span style="color: #008000;">&#40;</span>d0, d1<span style="color: #008000;">&#41;</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    Vector2 projected_wall_normal <span style="color: #000080;">=</span> normalize<span style="color: #008000;">&#40;</span>cross<span style="color: #008000;">&#40;</span>d0, d1<span style="color: #008000;">&#41;</span>.<span style="color: #007788;">xy</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    <span style="color: #0000ff;">float</span> motion_rate <span style="color: #000080;">=</span> <span style="color: #008000;">&#40;</span>constant_altitude<span style="color: #008000;">&#40;</span>d0,d1<span style="color: #008000;">&#41;</span> <span style="color: #008080;">?</span>
        <span style="color: #000040;">-</span>cos_theta<span style="color: #008000;">&#40;</span>d0,d1<span style="color: #008000;">&#41;</span> <span style="color: #008080;">:</span>      <span style="color: #666666;">// d(theta)/dx = -cos(theta) / depth</span>
        <span style="color: #000040;">-</span><span style="color: #0000dd;">1</span> <span style="color: #000040;">/</span> sin_theta<span style="color: #008000;">&#40;</span>d0,d1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>   <span style="color: #666666;">// d(phi)/dy = -1 / (sin(theta) * depth)</span>
&nbsp;
    edge.<span style="color: #007788;">translation_gradient</span> <span style="color: #000080;">=</span> projected_wall_normal <span style="color: #000040;">*</span> wall_length <span style="color: #000040;">*</span> motion_rate<span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p>&nbsp;</p>
<p>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:</p>
<p>&nbsp;</p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;">foreach edge in hemicube <span style="color: #008000;">&#123;</span>
    <span style="color: #0000ff;">float</span> depth0 <span style="color: #000080;">=</span> depth<span style="color: #008000;">&#40;</span>edge.<span style="color: #007788;">x0</span>, edge.<span style="color: #007788;">y0</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    <span style="color: #0000ff;">float</span> depth1 <span style="color: #000080;">=</span> depth<span style="color: #008000;">&#40;</span>edge.<span style="color: #007788;">x1</span>, edge.<span style="color: #007788;">y1</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    <span style="color: #0000ff;">float</span> min_depth <span style="color: #000080;">=</span> min<span style="color: #008000;">&#40;</span>depth0, depth1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
&nbsp;
    texel<span style="color: #008000;">&#91;</span>edge.<span style="color: #007788;">x0</span>, edge.<span style="color: #007788;">y0</span><span style="color: #008000;">&#93;</span>.<span style="color: #007788;">translation_gradient</span> <span style="color: #000040;">-</span><span style="color: #000080;">=</span> edge.<span style="color: #007788;">gradient</span> <span style="color: #000040;">/</span> min_depth<span style="color: #008080;">;</span>
    texel<span style="color: #008000;">&#91;</span>edge.<span style="color: #007788;">x1</span>, edge.<span style="color: #007788;">y1</span><span style="color: #008000;">&#93;</span>.<span style="color: #007788;">translation_gradient</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> edge.<span style="color: #007788;">gradient</span> <span style="color: #000040;">/</span> min_depth<span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p>&nbsp;</p>
<p>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:</p>
<p>&nbsp;</p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;">foreach texel, color in hemicube <span style="color: #008000;">&#123;</span>
    Vector2 translation_gradient <span style="color: #000080;">=</span> texel.<span style="color: #007788;">translation_gradient</span> <span style="color: #000040;">*</span> texel.<span style="color: #007788;">clamped_cosine</span><span style="color: #008080;">;</span>
    translation_gradient_sum<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">0</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> translation_gradient <span style="color: #000040;">*</span> color.<span style="color: #007788;">x</span><span style="color: #008080;">;</span>
    translation_gradient_sum<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">1</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> translation_gradient <span style="color: #000040;">*</span> color.<span style="color: #007788;">y</span><span style="color: #008080;">;</span>
    translation_gradient_sum<span style="color: #008000;">&#91;</span><span style="color: #0000dd;">2</span><span style="color: #008000;">&#93;</span> <span style="color: #000040;">+</span><span style="color: #000080;">=</span> translation_gradient <span style="color: #000040;">*</span> color.<span style="color: #007788;">z</span><span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p>&nbsp;</p>
<h2>Conclusions</h2>
<p>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.</p>
<p>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.</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2011/06/ic_comparison_1.jpg"><img class="aligncenter size-large wp-image-1139" src="https://the-witness.net/news/wp-content/uploads/2011/06/ic_comparison_1-351x384.jpg" alt="" width="351" height="384" /></a></p>
<p>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.</p>
<p><em>Note: This article is also published on <a href="http://altdevblogaday.com/2011/07/14/irradiance-caching-part-1/">#AltDevBlogADay</a>.</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2011/07/irradiance-caching-part-1/feed/</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
		<item>
		<title>Hemicube Rendering and Integration</title>
		<link>https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/</link>
					<comments>https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Thu, 30 Sep 2010 00:26:54 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=244</guid>

					<description><![CDATA[One of our aims with our global illumination implementation was to keep it as simple as possible, while still obtaining good performance, and we hoped to achieve that by using the GPU. While the are several approaches to compute global illumination in the GPU, the most simple one that we …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>One of our aims with our global illumination implementation was to keep it as simple as possible, while still obtaining good performance, and we hoped to achieve that by using the GPU. While the are several approaches to compute global illumination in the GPU, the most simple one that we could come up with was to use our existing rendering engine to estimate global illumination using a fixed sequence of final gathering steps. That is, rendering the scene from every lightmap texel to gather all the incoming irradiance at that point, integrating it to compute the output radiance, and repeating these steps multiple times until the lightmaps converge to an approximate solution. This is basically the same <a href="http://freespace.virgin.net/hugo.elias/radiosity/radiosity.htm">procedure described by Hugo Elias</a>.</p>
<p><img class="aligncenter size-full wp-image-588" src="https://the-witness.net/news/wp-content/uploads/2010/09/pretty_shot.png" alt="" width="512" height="288" /></p>
<p>Using the same engine makes it very easy to keep the global illumination in sync with the rest of the game. If we add new shaders or materials, change the representation of the geometry or alter the world, keeping the global illumination up to date requires little o no effort. Another interesting benefit is that optimizations to the rendering engine also benefit the global illumination pre-computation. However, as we will see later, in practice each application stresses the system in different ways and end up limited by different bottlenecks, what improves the rendering time does not necessary improve the pre-computation and vice-versa.</p>
<p><span id="more-244"></span></p>
<h3>Projection Methods</h3>
<p>I tried several different approaches to render the scene and estimate the incoming radiance. The most common approach is to render hemicubes; that requires rendering the scene to 5 different viewports. Several sources suggested the use of spherical or parabolic projections, but in my experience these approaches produced terrible results due to the non linear projection. Without very high tessellation rates the scene would become unacceptably distorted, creating gaps between objects or making parallel surfaces self-intersect.</p>
<p><img class="aligncenter size-full wp-image-628" src="https://the-witness.net/news/wp-content/uploads/2010/09/spherical_paraboloid.png" alt="" width="520" height="278" /></p>
<p>In <a href="http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter39.html">GPU Gems 2, Coombe et al.</a> suggested to project the z value independently of (x,y) coordinates using an orthographic projection in order to maintain a correct depth ordering. As seen in the next picture, that removed the most obvious artifacts; note how the staircase inside the tower is not visible anymore:</p>
<p><img class="aligncenter size-full wp-image-622" src="https://the-witness.net/news/wp-content/uploads/2010/09/paraboloid_orthographic.png" alt="" width="256" height="256" /></p>
<p>However, while this works OK from some points of view, the distortion becomes unacceptable when the camera is close to a surface, which is always our case. So, in practice these approaches are not very useful. Note how the outdoors are visible when sampling the lighting from the interior of the tower:</p>
<p><img class="aligncenter size-full wp-image-624" src="https://the-witness.net/news/wp-content/uploads/2010/09/paraboloid_artifacts.png" alt="" width="256" height="256" /></p>
<p>Other implementations use a single plane projection. This kind of projection does not capture the whole hemisphere, but by using a wide field of view it's possible to capture about 80% of it. I haven't actually tried this approach, but to get an idea of what the results would look like, I modified the hemicube integrator to ignore 20% of the samples near the horizon and found out that this introduced artifacts that were unacceptable for high-quality results.</p>
<p><a href="http://www.graphics.cornell.edu/~jaroslav/papers/2008-irradiance_caching_class/08-pascal-gpu-notes.pdf">Pascal Gautron</a> estimates that on typical scenes this approach introduced a 18% RMS error, but suggests various tricks to reduce it by re-balancing the solid angle weights, or extrapolating the border samples and filling the holes. The latter method reduces the error down to a mere 6%.</p>
<p>However, these improvements do not eliminate banding artifacts caused when an intense light source visible from one location is suddenly not visible anymore in the next when it falls out of the projection frustum. To make things worse, the projection lacks rotational invariance, which enhances the problem when hemicubes are rendered with random rotations. So, while in many cases the results look alright, the few cases that do not render the method impractical.</p>
<p>Another problem is that the wide frustum is 4 times larger than the equivalent hemicube and easily intersects with the surrounding geometry; as I'll discuss next, this is one of the main problems that I had to deal with.</p>
<p>In the end I simply stayed with the traditional hemicubes for simplicity. I think it would have been possible to do somewhat better using a fancy tetrahedral or pyramidal projection with 3 or 4 planes, but that didn't seem worth the trouble.</p>
<h3>Hemicube Overlaps</h3>
<p>As I just mentioned, one of the main problems that I had to solve was to handle overlaps between the hemicubes and the geometry. These overlaps are unavoidable unless you impose some severe modeling restrictions that are not desirable for production.</p>
<p>When the geometry is well formed and does not have self-intersections, these overlaps can sometimes be avoided with centroid sampling. Without centroid sampling the location where the hemicube is rendered from can easily end up being outside of the polygon that contains the sample and thus inside another volume.</p>
<p><img class="aligncenter white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/centroid21.png" border="0" alt="centroid sampling" width="490" height="231" /></p>
<p>With centroid sampling the origin of the hemicube is now always inside the surface. However, it can still be very close to the surface boundary and have overlaps with nearby surfaces. A simple solution is to move the near plane closer to the camera until the overlap dissapears, but how close? The distance to the nearest boundary can be determined analytically, but there's a limit in how much the near plane distance can be reduced without introducing depth sampling artifacts. An interesting solution for this problem is to render multiple hemicubes at the same location, each hemicube enclosing the previous one with its far plane at the near plane of the next.</p>
<p><img class="aligncenter white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/hemicubes1.png" border="0" alt="hemicubes" width="235" height="170" /></p>
<p>That would solve most the problems when the geometry is watertight and centroid sampling is used, but in many cases our assets are not always well conditioned. So, I had to find a more general solution.</p>
<p>I tried all sort of heuristics, to render backfaces in black, to fill them with the average color, to extrapolate the nearest colors, etc. None of these solutions worked well in all cases. In the end, something that produced satisfactory results was to render the backfaces with a special tag to distinguish them from front facing faces when integrating the hemicubes. Whenever a hemicube had too many tagged texels it would be discarded entirely, and instead its output radiance would be estimated at that location interpolating or extrapolating it from the nearest valid samples.</p>
<p>The extrapolation is not always correct, and the detection of invalid hemicubes is not 100% accurate, but so far this is the method that I'm happiest with. In the next article I'll explain how this extrapolation is done in more detail.</p>
<h3>Shadow Rendering</h3>
<p>While the goal was to use the same rendering engine, using its shadow system 'as is' would have been overkill. We are using <a href="https://the-witness.net/news/2010/03/graphics-tech-shadow-maps-part-1/">cascade shadow maps</a>, which are updated on every frame, and to maximize the shadow map resolution the shadow maps are tightly aligned to the view frustum slices. When rendering the hemicubes, shadow map resolution is not that important, and re-rendering the shadows for each hemicube face is unnecessarily expensive. To keep things simple I still used cascade shadow maps, but instead of rendering them for each view, I only rendered them once for each object positioning the cascades around the center of the object.</p>
<h3>Rasterization</h3>
<p>Now that I've explained how to capture the irradiance of the scene at any point, it's necessary to sample it for every lightmap texel. To do that I simply rasterize the geometry in the lightmap UV space using a conservative rasterizer and render one hemicube at every fragment whose coverage is above certain threshold.</p>
<p>One nice thing of using our own rasterizer is that I can evaluate the exact coverage analytically. Since the parameterization does not have overlaps, the coverage is just the area of the intersection of the texel square with the polygon being rasterized. Similarly, the centroid is the center of mass of the clipped polygon.</p>
<p>This approach gives us a position and a direction to render the hemicube from, but I still need to determine the orientation of the hemicube. Initially, I simply chose an arbitrary orientation using <a href="http://sourceforge.net/mailarchive/message.php?msg_name=3.0.6.32.20001111143758.010fcdf8%40208.55.130.3">the traditional/standard method</a>, but that resulted in banding artifacts:</p>
<p><img class="aligncenter size-full wp-image-607 white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/banding_small_00.png" alt="" width="320" height="180" /></p>
<p>The source of the problem is just the limited hemicube resolution, but it's surprisingly hard to get rid of it entirely by only increasing the hemicube size. A better solution is to simply use random orientations:</p>
<p><img class="aligncenter size-full wp-image-608 white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/noise_small_00.png" alt="" width="320" height="180" /></p>
<p>Note that I am trading banding artifacts by noise, but the latter is much easier to get rid of by using some smoothing.</p>
<h3>Integration</h3>
<p>Once the hemicubes have been rendered it's necessary to integrate the sampled irradiance to compute the output radiance. The integral of the irradiance hemicube is simply a weighted sum of the samples, where the weights are the cosine-weighted areas of the hemicube texels projected on the hemisphere, that is, the cosine-weighted solid angles of the texels. These weights are constant, so they can be precomputed in advance.</p>
<p>In the <a href="http://freespace.virgin.net/hugo.elias/radiosity/radiosity.htm">article by Hugo Helias</a> that I referenced in the introduction the same approach is used and this weight table is called the "The Multiplier Map". Beware of that part of the article, though, because there's a bug in the formulas. The subtended solid angle of the texel is not "the cosine of the angle between the direction the camera is facing in, and the line from the camera to the pixel":</p>
<p><img class="white-background" style="float: right;" src="https://the-witness.net/news/wp-content/uploads/2010/09/global_illumiantion_compendium.png" border="0" alt="global_illumiantion_compendium.png" width="275" height="141" /><img class="white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/texel_solid_angle_bad.gif" border="0" alt="texel_solid_angle_bad.gif" width="129" height="20" /></p>
<p>That term needs to be divided by the squared distance between the location of the camera <code>x</code> and the center of the texel <code>y</code>:</p>
<p><img class="white-background" src="https://the-witness.net/news/wp-content/uploads/2010/09/texel_solid_angle_good.gif" border="0" alt="texel_solid_angle_good.gif" width="133" height="46" /></p>
<p>See formula 25 in the <a href="http://people.cs.kuleuven.be/~philip.dutre/GI/">Global Illumination Compendium</a> for more details.</p>
<p>Also, since this is a precomputation it made sense to evaluate the solid angle exactly. That can be derived using Girard's formula or directly using the handy formula in <a href="http://www.fizzmoll11.com/thesis/">Manne Ohrstrom's thesis</a>. Doing so improved the invariance of the integral with respect to rotations of the hemicube and that reduced noise significantly.</p>
<p>In the end, the integration is a fairly simple operation. It could be done very efficiently using compute shaders and I might still do that in the future, but for prototyping purposes it was much easier to perform the integration in the CPU. Unfortunately, that required transferring the data across the bus and, if not done properly, it severely reduced performance.</p>
<h3>Asynchronous Memory Transfers</h3>
<p>Contrary to popular belief, Direct3D 9 provides a mechanism to perform memory copies asynchronously. In Direct3D 10 and OpenGL this is done through staging resources or pixel buffer objects; in Direct3D 9 something similar can be achieved with offscreen plain surfaces. The problem is that this is not very well documented, it's very easy to fall out of the fast path, and the conditions in which that happens are slightly different for each IHV.</p>
<p>In the first place, you have to create two render targets and offscreen plain surfaces:</p>
<p><code></p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;">dev<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>CreateRenderTarget<span style="color: #008000;">&#40;</span>..., <span style="color: #000040;">&amp;</span>render_target_0<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
dev<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>CreateRenderTarget<span style="color: #008000;">&#40;</span>..., <span style="color: #000040;">&amp;</span>render_target_1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
&nbsp;
dev<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>CreateOffscreenPlainSurface<span style="color: #008000;">&#40;</span>..., <span style="color: #000040;">&amp;</span>offscreen_surface_0<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
dev<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>CreateOffscreenPlainSurface<span style="color: #008000;">&#40;</span>..., <span style="color: #000040;">&amp;</span>offscreen_surface_1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span></pre></td></tr></table></div>

<p></code></p>
<p>If you have more than two targets the copies become synchronous in some systems, but two is sufficient to generate enough work to do in parallel with the memory transfers.</p>
<p>The most important observation is that the only device method that appears to be asynchronous is <code>GetRenderTargetData</code>. After calling this method, any other device method blocks until the copy is complete. So, instead of overlapping the memory transfer with the rendering code, you have to find some CPU work to do simultaneously. In my case, I just do the integration of one of the render targets while copying the other:</p>
<p><code></p>

<div class="wp_syntax"><table><tr><td class="code"><pre class="cpp" style="font-family:monospace;"><span style="color: #0000ff;">while</span> hemicubes left <span style="color: #008000;">&#123;</span>
&nbsp;
    <span style="color: #008000;">&#91;</span>...<span style="color: #008000;">&#93;</span> <span style="color: #666666;">// Render hemicube to render_target_0</span>
&nbsp;
    <span style="color: #666666;">// Initiate asynchronous copy to system memory.</span>
    dev<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>GetRenderTargetData<span style="color: #008000;">&#40;</span>render_target_0, offscreen_surface_0<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
&nbsp;
    <span style="color: #666666;">// Lock offscreen_surface_1</span>
    <span style="color: #666666;">// at this point the corresponding async copy has finished already.</span>
    offscreen_surface_1<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>LockRect<span style="color: #008000;">&#40;</span>..., D3DLOCK_READONLY<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
&nbsp;
    <span style="color: #008000;">&#91;</span>...<span style="color: #008000;">&#93;</span> <span style="color: #666666;">// Integrate hemicube.</span>
&nbsp;
    offscreen_surface_1<span style="color: #000040;">-</span><span style="color: #000080;">&gt;</span>UnlockRect<span style="color: #008000;">&#40;</span><span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
&nbsp;
    swap<span style="color: #008000;">&#40;</span>render_target_0, render_target_1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
    swap<span style="color: #008000;">&#40;</span>offscreen_surface_0, offscreen_surface_1<span style="color: #008000;">&#41;</span><span style="color: #008080;">;</span>
<span style="color: #008000;">&#125;</span></pre></td></tr></table></div>

<p></code></p>
<p>Another problem is that the buffer needs to be sufficiently big to reach transfer rates that saturate the available bandwidth. In order to achieve that I batch multiple hemicubes in a single texture atlas as seen in the following picture:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/04/hemicube_atlas.png"><img class="aligncenter size-medium wp-image-318" src="https://the-witness.net/news/wp-content/uploads/2010/04/hemicube_atlas-300x300.png" alt="" width="300" height="300" /></a></p>
<p>In practice I do not render the hemicubes using the depicted cross layout, but instead arrange the faces tightly in a 3x1 rectangle that does not waste any texture space and maximizes bandwidth. However, the cross layout was useful to visualize the output for debugging purposes.</p>
<h3>Performance and Conclusions</h3>
<p>The performance of our global illumination solution is somewhat disappointing. While we do most of the work on the GPU we are not really making a very effective use of it. The main GPU bottleneck is in the fixed function geometry pipeline; we are basically rendering a large number of tiny triangles on a small render target.</p>
<p>But that's not the main problem. Traversing the scene in the CPU and submitting commands to render it actually takes more time than it takes the GPU to process the commands and render the scene, so the GPU is actually underutilized.</p>
<p>Our engine was designed for quick prototyping, to make it very easy to draw stuff with different materials, but not to do so in the most efficient way possible. These inefficiencies became a lot more pronounced in the lightmap baker.</p>
<p>On the bright side, as we optimize the CPU side of the engine we expect performance to improve. Modern GPUs also have functionality that make it possible to render to multiple hemicube faces at once. We expect that will also reduce the CPU work significantly.</p>
<p>While it was easy to get our global illumination solution up and running by reusing our rendering engine, bringing it to a level of sufficient quality and performance took a considerable amount of time. In the next installment I'll describe some of the techniques and optimizations that I implemented to fix some of the remaining artifacts and improve performance.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2010/09/hemicube-rendering-and-integration/feed/</wfw:commentRss>
			<slash:comments>16</slash:comments>
		
		
			</item>
		<item>
		<title>Computing Alpha Mipmaps</title>
		<link>https://the-witness.net/news/2010/09/computing-alpha-mipmaps/</link>
					<comments>https://the-witness.net/news/2010/09/computing-alpha-mipmaps/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Fri, 10 Sep 2010 00:59:02 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=484</guid>

					<description><![CDATA[A little problem that we had when we started to create trees and vegetation is that the standard mipmap generation algorithms produced surprisingly bad results on alpha tested textures. As the trees moved farther away from the camera, the leafs faded out becoming almost transparent. Here's an example. The following …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2010/09/computing-alpha-mipmaps/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>A little problem that we had when we started to create trees and vegetation is that the standard mipmap generation algorithms produced surprisingly bad results on alpha tested textures. As the trees moved farther away from the camera, the leafs faded out becoming almost transparent.</p>
<p>Here's an example. The following tree looked OK close to the camera:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/09/near_nocoverage.png"><img class="aligncenter size-large wp-image-495" src="https://the-witness.net/news/wp-content/uploads/2010/09/near_nocoverage-512x320.png" alt="" width="512" height="320" /></a></p>
<p><span id="more-484"></span></p>
<p>but as we moved away, the leafs started to fade and thin out:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/09/mid_nocoverage.png"><img class="aligncenter size-large wp-image-493" src="https://the-witness.net/news/wp-content/uploads/2010/09/mid_nocoverage-512x320.png" alt="" width="512" height="320" /></a></p>
<p>until they almost disappeared:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/09/far_nocoverage.png"><img class="aligncenter size-large wp-image-491" src="https://the-witness.net/news/wp-content/uploads/2010/09/far_nocoverage-512x320.png" alt="" width="512" height="320" /></a></p>
<p>I had never encountered this problem before, neither had I heard much about it, but after a few google searches I found out that artists are often frustrated by it and usually work around the issue using <a href="http://www.polycount.com/forum/showthread.php?t=65261">various</a> <a href="http://www.gamedev.net/community/forums/topic.asp?topic_id=540726">hacks</a>. These are some of the proposed solutions that are usually suggested:</p>
<ul>
<li>Manually adjusting contrast and sharpening the alpha channel of each mipmap in Photoshop.</li>
<li>Scaling the alpha in the shader based on distance or on an lod factor estimated using texture gradients.</li>
<li>Limiting the number of mipmaps, so that the lowest ones aren't used by the game.</li>
</ul>
<p><P><br />
These solutions may work around the problem in one way or another, but none of them is entirely correct and in some cases add a significant overhead.</p>
<p>In order to address the problem it's important to understand why the geometry fades out in the distance. That is because when computing alpha mipmaps using the standard algorithms, each mipmap has a different alpha test coverage. That is, the proportion of pixels that pass the alpha test changes, in most cases going down and causing the texture to become more transparent.</p>
<p>A simple solution to the problem is to find a scale factor that preserves the original alpha test coverage as best as possible. We define the coverage of the first mipmap as follows:</p>
<p><code>coverage = Sum(a_i &gt; A_r) / N</code></p>
<p>where <code>A_r</code> is the alpha test value used in your application, <code>a_i</code> are the alpha values of the mipmap, and <code>N</code> is the number of texels in the mipmap. Then, for the following mipmaps you want to find a scale factor that causes the resulting coverage to stay the same:</p>
<p><code>Sum(scale * a_i &gt; A_r) / N == coverage</code></p>
<p>However, finding this <code>scale</code> directly is tricky because it's a discrete problem, in general, there's no exact solution, and the range of <code>scale</code> is unbounded. Instead, what you can do is to find a new alpha reference value <code>a_r</code> that produces the desired coverage:</p>
<p><code>Sum(a_i &gt; a_r) / N = coverage</code></p>
<p>This is much easier to solve, because <code>a_r</code> is bounded between 0 and 1. So, it's possible to use a simple bisection search to find the best solution. Once you know <code>a_r</code> the scale is simply:</p>
<p><code>scale = A_r / a_r</code></p>
<p>An implementation of this algorithm is publicly available in <a href="http://code.google.com/p/nvidia-texture-tools/">NVTT</a>. The relevant code can be found in the following methods of the FloatImage class:</p>
<p><code>float FloatImage::<a href="http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvimage/FloatImage.cpp#985">alphaTestCoverage</a>(float alphaRef, int alphaChannel) const;<br />
void FloatImage::<a href="http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvimage/FloatImage.cpp#1002">scaleAlphaToCoverage</a>(float desiredCoverage, float alphaRef, int alphaChannel);<br />
</code></p>
<p>And here's a simple example of how this feature can be used through NVTT's public API:</p>
<pre>
// Output first mipmap.
context.compress(image, compressionOptions, outputOptions);

// Estimate original coverage.
const float coverage = image.alphaTestCoverage(A_r);

// Build mipmaps and scale alpha to preserve original coverage.
while (image.buildNextMipmap(nvtt::MipmapFilter_Kaiser))
{
    image.scaleAlphaToCoverage(coverage, A_r);
    context.compress(tmpImage, compressionOptions, outputOptions);
}
</pre>
<p>As seen in the following screenshot, this solves the problem entirely:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/09/mid_coverage.png"><img class="aligncenter size-large wp-image-492" src="https://the-witness.net/news/wp-content/uploads/2010/09/mid_coverage-512x320.png" alt="" width="512" height="320" /></a></p>
<p>even when the trees are far away from the camera:</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/09/far_coverage.png"><img class="aligncenter size-large wp-image-490" src="https://the-witness.net/news/wp-content/uploads/2010/09/far_coverage-512x320.png" alt="" width="512" height="320" /></a></p>
<p>Note that this problem does not only show up when using alpha testing, but also when using alpha to coverage (as in these screenshots) or alpha blending in general. In those cases you don't have a specific alpha reference value, but this algorithm still works fine if you choose a value that is close to 1, since essentially what you want is to preserve the percentage of texels that are nearly opaque.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2010/09/computing-alpha-mipmaps/feed/</wfw:commentRss>
			<slash:comments>32</slash:comments>
		
		
			</item>
		<item>
		<title>Lightmap Parameterization</title>
		<link>https://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/</link>
					<comments>https://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/#comments</comments>
		
		<dc:creator><![CDATA[Ignacio Castaño]]></dc:creator>
		<pubDate>Tue, 30 Mar 2010 08:05:57 +0000</pubDate>
				<category><![CDATA[Engine Tech]]></category>
		<guid isPermaLink="false">https://the-witness.net/news/?p=120</guid>

					<description><![CDATA[As explained by Jon in his previous post, we are using precomputed global illumination in The Witness, and one of my first tasks was to work on that system. I have developed some interesting technology to compute automatic paramaterizations, to formulate the lighting computations in a way that can be …<p class="continue-reading-button"> <a class="continue-reading-link" href="https://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/">Continue reading<i class="crycon-right-dir"></i></a></p>]]></description>
										<content:encoded><![CDATA[<p>As explained by Jon <a href="https://the-witness.net/news/2010/03/graphics-tech-precomputed-lighting/">in his previous post</a>, we are using precomputed global illumination in <em>The Witness</em>, and one of my first tasks was to work on that system.</p>
<p>I have developed some interesting technology to compute automatic paramaterizations, to formulate the lighting computations in a way that can be easily mapped to the GPU, and to speed them up performing them at a lower frequency by using irradiance caching. None of these things are very cutting edge, but I run into some interesting problems that are usually overlooked in the literature and came up with some creative solutions that I think would be interesting to share. However, in retrospect, I think I also made some mistakes, but I hope that being honest about them will help me avoid them in the future.</p>
<p>In this first post I will describe the lightmap parameterization algorithm that I have implemented and in the next few posts I'll cover other details of our lighting system such as our approach to perform final gathering using hemicubes and our implementation of irradiance caching.</p>
<p><span id="more-120"></span></p>
<p>In order to use precomputed lighting we need a way of mapping these lighting computations to the surfaces. One possibility is to store them per vertex, but in many cases that does not provide enough resolution and the interpolation of the lighting makes the triangulation of the mesh fairly obvious. Many games get away with that by compensating the lack of detail in the lighting with high texture and normal map detail, but that does not go along well with the graphical style that Jon wants to achieve. So, instead of that we decided to create full parameterizations of our objects in order to store the precomputed lighting into lightmap textures.</p>
<p>Creating these parameterizations by hand is possible, but it is time consuming and I'd rather have our artists work on something more enjoyable. So, we decided to create the parameterizations automatically. However, the greatest mistake I've made so far is to not look more carefully at the available solutions to accomplish that. I had some pieces of the implementation already available, and I overestimated how much effort it would take to put them together and produce robust results. While talking about this to some friends at GDC one of them asked why I wasn't using <a href="http://msdn.microsoft.com/en-us/library/ee422528(VS.85).aspx">Microsoft's UVAtlas</a> tool, and to my dismay and embarrassment my only answer was that it had not occurred to me. I think that goes to prove how valuable it is to be open about our development in a small team like ours.</p>
<p>Now that I got that out of the way, let's talk about the tech. We only use lightmaps to represent indirect illumination and the direct illumination of static area lights. So, our expectation was that lightmaps would be fairly smooth and that seams would not be a huge problem. For this reason we decided not to invest in a solution that guaranteed seamless parameterizations and instead used a fairly traditional approach: We first divide the input geometry into charts with disk topology, parameterize them individually, and finally pack them into a rectangular texture.</p>
<h3>Segmentation</h3>
<p>Dividing the mesh into charts is by far the most complex step of the process. In the past I had tried using hierarchical clustering without much success, so this time around I decided to use an iterative clustering algorithm similar to Lloyd's algorithm, in the same vein as the algorithm used in the <a href="http://research.microsoft.com/en-us/um/people/hoppe/proj/mcgim/">Multi-chart geometry images (MCGI)</a> paper.</p>
<p>Like Lloyd's clustering algorithm, the MCGI algorithm iterates between chart growing and reseeding stages. The growth of the charts is guided by a metric based on the normal deviation and the distance between the face centroids. After each chart growing iteration a new seed is selected taking the most central face of the chart, and the process is repeated until the seed location does not change anymore.</p>
<p>My implementation of the MCGI algorithm worked moderately well, but it didn't always produce good results. It was possible to tweak the number and location of the seeds to obtain the desired output, but that required a certain amount of user intervention that was not desired.</p>
<p>Part of the problem is that the meaning of "good" in this setting is very subjective. It usually means nicely shaped charts, that fit well to the surface, produce parameterizations with little distortion, have fairly straight and convex boundaries and are more or less equaly sized.</p>
<p>In order to adapt the algorithm to our purposes I did two things, modified the bootstraping method so that the initial number of seeds is chosen automatically and used a combination of metrics to obtain better control over the results.</p>
<p>In order to select the initial number of seeds, I simply insert the seeds one by one, and grow the respective charts until a threshold is met. At that point I pick an arbitrary face that is not assigned to any chart yet and repeat the process. When no faces are left, the growing process is restarted from all seeds in parallel.</p>
<p>A common strategy to remove the sensitivity to the initial conditions is to use the farthest point heuristic. However, instead of doing that I found that an approach that works well is to pick the worst candidate face of the already chosen charts as the next seed.</p>
<p>The second important change to the algorithm was to use a different set of metrics. The MCGI paper proposes a metric that combines normal deviation with distance between face centroids. I found that using absolute distances made the algorithm very dependent on the tessellation of the mesh and that measuring the distances from the centroids favored triangles with low aspect ratios.</p>
<p>Instead of using distance between faces I considered only the normal deviation and combined it with other metrics similar to the ones proposed in <a href="http://www.cs.ubc.ca/labs/imager/tr/2005/Vlad_DCharts/">D-Charts: Quasi-Developable Mesh Segmentation</a> and that take roundness and straightness into account.</p>
<p>These metrics also had their own problems and I had to tweak them in various ways. The most significant issue is that they constrained the chart growth in undesired ways. For example, to encourage chart convexity you would associate a high factor to the corresponding metric, but that would result in a large number of almost convex charts, as soon as a convex configuration was found no additional faces could be added to the chart. So, instead of using these metrics to constrain the growth I only use them to favor convex charts or straight boundaries; that allowed the charts to grow freely, but still retain the most convex and most straight shape possible.</p>
<p>A trick that helped in many cases was to incorporate some input from the artist, but without imposing any additional burden to them. A simple way of doing that is taking into account the existing parameterization of the mesh. Our objects already have a parameterization that is used for texturing and the artists tend to place texture seams at locations that are not easily visible. Snapping the boundaries of our charts to the existing seams did not only reduce the overall number of seams, but also caused the charts to evolve in a way that matches better what the artist would have done manually.</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/03/charts02.png"><img class="aligncenter size-medium wp-image-221" src="https://the-witness.net/news/wp-content/uploads/2010/03/charts02-300x168.png" alt="" width="300" height="168" /></a></p>
<p>I considered texture and normal seams independently. Since our parameterization is used for lighting I follow normal seams more strictly and proportionally to their angle, not doing so would cause the corners to appear less defined or more blurry. That also helps the process to converge much more quickly in architectural meshes that have many hard edges.</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/03/charts00.png"><img class="aligncenter size-medium wp-image-178" src="https://the-witness.net/news/wp-content/uploads/2010/03/charts00-300x168.png" alt="normal seam fitting" width="300" height="168" /></a></p>
<p>Finally, I don't impose any chart topology constrains. I specially wanted charts go grow around holes in the mesh as long as the metric was below the desired threshold. I was tempted to use a better proxy fitting metric instead of the normal deviation metric, but that would have generated cylindrical or conical charts that would have to be cut, and I did not want to deal with that. Closing holes on the other side is fairly easy, it's just matter of detecting the boundaries and closing all, but the longest one.</p>
<p><a href="https://the-witness.net/news/wp-content/uploads/2010/03/charts01.png"><img class="aligncenter size-medium wp-image-179" src="https://the-witness.net/news/wp-content/uploads/2010/03/charts01-300x168.png" alt="charts with holes" width="300" height="168" /></a></p>
<h3>Parameterization</h3>
<p>For the chart parameterization algorithm we simply use <a href="http://alice.loria.fr/index.php/publications.html?Paper=lscm@2002">Least Squares Conformal Maps</a> since I had an implementation already available. My implementation is based on the description of the algorithm in the <a href="http://www.inf.usi.ch/hormann/parameterization/CourseNotes.pdf">notes</a> of the <a href="http://www.inf.usi.ch/hormann/parameterization/index.html">SIGGRAPH 2007 Mesh Parameterization course</a>, which I found much more accessible than the one in the original paper. Particularly useful is the chapter about numerical optimization, although if I had to write a sparse matrix solver again I would probably use <a href="http://alice.loria.fr/index.php/software/4-library/23-opennl.html">OpenNL</a>, since it has now been released under a liberal BSD license.</p>
<p>LSCM is not the best parameterization algorithm out there. It worked pretty well in most cases, although in some rare situations it resulted in small self-overlaps. That happened in two cases:</p>
<ul style="margin-bottom: 1em">
<li>Zero area triangles in the input sometimes flipped in the parameterization.</li>
<li>Charts with complex boundaries that self-intersect when parameterized.</li>
</ul>
<p>The first case turned out that was caused by T-junctions in the input mesh that were detected as holes and filled with a degenerate triangle. In practice the small overlaps in the parameterization were not really an issue so instead of fixing the input we simply plan to report these problems to the artist in the exporter and have him fix the them if it turns out it's causing artifacts.</p>
<p>In the second case, increasing the weight of the convexity metric used during the clustering process allowed us to prevent these errors. So, I didn't really do anything to handle this problem, but so far it has not been an issue.</p>
<p>Even after that we had some problematic cases remaining. I noticed that the implementation in Blender was not having this same issues and was handling these cases more gracefully. So, I asked Brecht Van Lommel, who suggested the use of alternate matrix weights based on the ABF formulation of LSCM, and that seemed to eliminate the remaining issues.</p>
<p>While LSCM is very easy to implement, it's definitely not the best parameterization method out there. If I were to implement something better I would probably look at <a href="http://alice.loria.fr/index.php/publications.html?Paper=LinABF@2007">LinABF</a> or <a href="http://mgarland.org/files/papers/param.pdf">Fitting to Guidance Gradient Fields</a>. However, since parameterization of the individual charts is currently the fastest step it may make sense to also look at more expensive deformation analysis methods.</p>
<h3>Packing</h3>
<p>Our packing algorithm is fairly simple. Like many other implementations we first sort the charts by parametric area, rotate them to align the charts with their best fit rectangle and greedily introduce them in the atlas one by one. Ideally I should test all 4 possible orientations, but currently I simply pick one of the orientations randomly.</p>
<p>Instead of using a tetris-like scheme to find the best location for each chart, I use a more brute force approach in which all the possible locations are considered. I achieve that using a conservative rasterizer that tags all the texels touched by a chart, plus an extra texel for padding. I progressively increase the texture extents as new charts are inserted. If the a location within the current extents is found, the chart is automatically accepted, otherwise a metric is evaluated to determine the location that least increases the extents of the atlas. Since I use a greedy algorithm, minimizing area resulted in atlases with very high aspect ratios. So, instead I minimize a metric that combines extent and perimeter and results in mostly square atlases.</p>
<p>Here are some examples of the results:</p>
<p><img class="aligncenter size-full wp-image-145" style="border: 1px solid black" src="https://the-witness.net/news/wp-content/uploads/2010/03/packing_cottage00.png" alt="" width="448" height="452" /></p>
<p><img class="aligncenter size-full wp-image-153" style="border: 1px solid black" src="https://the-witness.net/news/wp-content/uploads/2010/03/packing_tower00.png" alt="" width="435" height="421" /></p>
<p>Once we use lightmap compression it will probably make sense to align the charts to DXT block boundaries in order to prevent bleeding artifacts.</p>
<h3>Future Improvements</h3>
<p>This is one of the areas where I think there's a lot of room for improvement, but at the same time, there's not much !/$ in spending more time improving the results. Even though it has not been tested in production, I think that our current solution works fairly well. It would be nice to have larger charts with less distortion or a lower number of seams, and I would certainly have fun working on that. However, that's not going to make the game run significantly faster or look dramatically better. We plan to release the source code of these algorithms at some point so my hope is that the implementation will progressively get better as other people adopt it and improve it to fit their needs.</p>
<p>That said, if there's one area where I think it's worth investing, that is in the use of signal specialized parameterizations. The research papers on the subject (<a href="http://research.microsoft.com/en-us/um/people/hoppe/proj/ssp/">Signal-specialized parametrization</a> and <a href="http://research.microsoft.com/en-us/um/people/hoppe/proj/ssplinear/">Signal-specialized parameterization for piecewise linear reconstruction</a>) are probably overkill in our setting, and I'm not sure they would work very well with our low resolution meshes.</p>
<p>Half-Life 2 achieved that that by using artist specified sampling rates for each surface, but I think it's worth doing that automatically using an approach like <a href="http://www.bungie.net/images/Inside/publications/presentations/Lightmap_Compression_2008_02_22.pptx">the one proposed by Bungie</a>, which seems fairly simple and provides most of the gains the more complex approaches would give us.</p>
<p>My expectation is that the current implementation won't suffer significant changes, other than small tweaks and slight adjustments to deal with problematic cases that may show up in the future. If someone has any experience with these kind of algorithms, I would love to hear about it. In particular I'd like to know if there's anything you would have done differently, or if there's any optimization or improvement that I may have overlooked and you think is worth considering.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://the-witness.net/news/2010/03/graphics-tech-texture-parameterization/feed/</wfw:commentRss>
			<slash:comments>13</slash:comments>
		
		
			</item>
	</channel>
</rss>
