{"id":1205,"date":"2012-02-27T10:20:00","date_gmt":"2012-02-27T18:20:00","guid":{"rendered":"http:\/\/www.ludicon.com\/castano\/blog\/?page_id=1205"},"modified":"2020-05-20T00:00:17","modified_gmt":"2020-05-20T08:00:17","slug":"seamless-cube-map-filtering","status":"publish","type":"page","link":"http:\/\/www.ludicon.com\/castano\/blog\/articles\/seamless-cube-map-filtering\/","title":{"rendered":"Seamless Cube Map Filtering"},"content":{"rendered":"\n<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&#8217;s not exposed through Direct3D 9 and it&#8217;s just not available in any of the current generation consoles.<\/p>\n\n\n\n<p>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>\n\n\n\n<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=\"https:\/\/gpuopen.com\/archived\/cubemapgen\/\">AMD&#8217;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>\n\n\n\n<h2 class=\"wp-block-heading\">Embedded Texture Borders<\/h2>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">fov = 2.0 * atan(n \/ (n - 0.5))<\/pre>\n\n\n\n<p>where <code>n<\/code> is the resolution of the cube map.<\/p>\n\n\n\n<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>\n\n\n\n<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>\n\n\n\n<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>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">map_1(x) = (x + 0.5) * 2 \/ n - 1<\/pre>\n\n\n\n<p>We then obtain the corresponding direction vector as follows:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">dir = normalize(faceVector + faceU * map_1(x) + faceV * map_1(y)<\/pre>\n\n\n\n<p>When doing that, the texels at the borders do not map to <code>-1<\/code> and <code>1<\/code> exactly, but to:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> map(0) = -1 + 1\/n map(n-1) = 1 - 1\/n <\/pre>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">map_2(x) = 2 * x \/ (n - 1) - 1<\/pre>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">map_1'(x) = 2 \/ n<\/pre>\n\n\n\n<p>but in the second case it is slightly different:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">map_2'(x) = 2 \/ (n - 1)<\/pre>\n\n\n\n<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>\n\n\n\n<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>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"alignright size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"360\" height=\"222\" src=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/warp.png\" alt=\"img\" class=\"wp-image-1213\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/warp.png 360w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/warp-267x165.png 267w\" sizes=\"auto, (max-width: 360px) 100vw, 360px\" \/><\/figure><\/div>\n\n\n\n<pre class=\"wp-block-preformatted\">warp3(x) = ax^3 + x<\/pre>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">warp3(1-1\/n) = ax^3 + x = 1<\/pre>\n\n\n\n<p>which gives us:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">a = n^2 \/ (n-1)^3<\/pre>\n\n\n\n<p>I implemented the linear stretch and cubic warping methods in <a href=\"https:\/\/github.com\/castano\/nvidia-texture-tools\">NVTT<\/a> and they often produce better results than the methods available in  AMD&#8217;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>\n\n\n\n<p>The following images show how the  warp edge fixup method eliminates the discontinuities, but sometimes  still results in visible artifacts:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"560\" height=\"273\" src=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seams.png\" alt=\"\" class=\"wp-image-1207\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seams.png 560w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seams-267x130.png 267w\" sizes=\"auto, (max-width: 560px) 100vw, 560px\" \/><figcaption>Original seam artifacts<\/figcaption><\/figure><\/div>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"560\" height=\"273\" src=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seamless_warp.png\" alt=\"\" class=\"wp-image-1206\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seamless_warp.png 560w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/seamless_warp-267x130.png 267w\" sizes=\"auto, (max-width: 560px) 100vw, 560px\" \/><figcaption>Seam artifacts after warping<\/figcaption><\/figure><\/div>\n\n\n\n<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 sensitive to this form of  discontinuity and it&#8217;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&#8217;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>\n\n\n\n<h2 class=\"wp-block-heading\">Modifed Texture Lookup<\/h2>\n\n\n\n<p>However, a few days ago <a href=\"https:\/\/twitter.com\/#!\/SebLagarde\">Sebastien Lagarde<\/a> integrated these methods in AMD&#8217;s CubeMapGen. See <a href=\"https:\/\/seblagarde.wordpress.com\/2012\/06\/10\/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>\n\n\n\n<pre class=\"wp-block-preformatted\">float3 fix_cube_lookup(float3 v) {<br> &nbsp; &nbsp;float M = max(max(abs(v.x), abs(v.y)), abs(v.z));<br> &nbsp; &nbsp;float scale = (cube_size - 1) \/ cube_size;<br> &nbsp; &nbsp;if (abs(v.x) != M) v.x *= scale;<br> &nbsp; &nbsp;if (abs(v.y) != M) v.y *= scale;<br> &nbsp; &nbsp;if (abs(v.z) != M) v.z *= scale;<br> &nbsp; &nbsp;return v;<br>} <\/pre>\n\n\n\n<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>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">float scale = 1 - exp2(lod) \/ cube_size;<br>if (abs(v.x) != M) v.x *= scale;<br>if (abs(v.y) != M) v.y *= scale;<br>if (abs(v.z) != M) v.z *= scale; <\/pre>\n\n\n\n<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&#8217;s the case, the two  transcendental instructions cancel out and the scale becomes a linear  function of the specular power.<\/p>\n\n\n\n<p>The images below show the results using the warp filtering method (these were chosen to highlight the artifacts of the warp method) compared with the new  approach:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"566\" height=\"582\" src=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_warp.png\" alt=\"\" class=\"wp-image-1210\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_warp.png 566w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_warp-267x275.png 267w\" sizes=\"auto, (max-width: 566px) 100vw, 566px\" \/><figcaption>Artifacts after warp filtering.<\/figcaption><\/figure><\/div>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"566\" height=\"582\" src=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_seamless.png\" alt=\"\" class=\"wp-image-1211\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_seamless.png 566w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2020\/05\/examples_seamless-267x275.png 267w\" sizes=\"auto, (max-width: 566px) 100vw, 566px\" \/><figcaption>Results with modified texture sampling.<\/figcaption><\/figure><\/div>\n\n\n\n<p>I&#8217;d like to thank Sebastien Lagarde for his valuable feedback while testing these ideas and for providing the nice images accompanying this  article.<\/p>\n","protected":false},"excerpt":{"rendered":"<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 ARB_seamless_cube_map extension. However, it&#8217;s not exposed through Direct3D 9 and it&#8217;s just not available in any of the current generation consoles. There are several solutions for this problem. Texture borders&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":17,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1205","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1205","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/comments?post=1205"}],"version-history":[{"count":6,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1205\/revisions"}],"predecessor-version":[{"id":1216,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1205\/revisions\/1216"}],"up":[{"embeddable":true,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/17"}],"wp:attachment":[{"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/media?parent=1205"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}