{"id":1218,"date":"2010-09-09T00:11:00","date_gmt":"2010-09-09T08:11:00","guid":{"rendered":"http:\/\/www.ludicon.com\/castano\/blog\/?page_id=1218"},"modified":"2025-01-07T09:52:30","modified_gmt":"2025-01-07T17:52:30","slug":"computing-alpha-mipmaps","status":"publish","type":"page","link":"http:\/\/www.ludicon.com\/castano\/blog\/articles\/computing-alpha-mipmaps\/","title":{"rendered":"Computing Alpha Mipmaps"},"content":{"rendered":"\n<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>\n\n\n\n<p>Here&#8217;s an example. The following tree looked OK close to the camera:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"438\" src=\"https:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage-700x438.png\" alt=\"\" class=\"wp-image-1451\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage-700x438.png 700w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage-267x167.png 267w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage-768x480.png 768w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage-800x500.png 800w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/near_nocoverage.png 1024w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/figure>\n<\/div>\n\n\n<p><a href=\"http:\/\/the-witness.net\/news\/wp-content\/uploads\/2010\/09\/near_nocoverage.png\"><\/a>but as we moved away, the leafs started to fade and thin out:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"438\" src=\"https:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage-700x438.png\" alt=\"\" class=\"wp-image-1452\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage-700x438.png 700w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage-267x167.png 267w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage-768x480.png 768w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage-800x500.png 800w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_nocoverage.png 1024w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/figure>\n<\/div>\n\n\n<p><a href=\"http:\/\/the-witness.net\/news\/wp-content\/uploads\/2010\/09\/mid_nocoverage.png\"><\/a>until they almost disappeared:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"438\" src=\"https:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage-700x438.png\" alt=\"\" class=\"wp-image-1454\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage-700x438.png 700w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage-267x167.png 267w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage-768x480.png 768w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage-800x500.png 800w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_nocoverage.png 1024w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/figure>\n<\/div>\n\n\n<p><a href=\"http:\/\/the-witness.net\/news\/wp-content\/uploads\/2010\/09\/far_nocoverage.png\"><\/a>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>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Manually adjusting contrast and sharpening the alpha channel of each mipmap in Photoshop.<\/li>\n\n\n\n<li>Scaling the alpha in the shader based on distance or on an LOD factor estimated using texture gradients.<\/li>\n\n\n\n<li>Limiting the number of mipmaps, so that the lowest ones aren&#8217;t used by the game.<\/li>\n<\/ul>\n\n\n\n<p> 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>\n\n\n\n<p>In order to address the problem it&#8217;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>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">coverage = Sum(a_i &gt; A_r) \/ N<\/pre>\n\n\n\n<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>\n\n\n\n<pre class=\"wp-block-preformatted\">Sum(scale * a_i &gt; A_r) \/ N == coverage<\/pre>\n\n\n\n<p>However, finding this <code>scale<\/code> directly is tricky because it&#8217;s a discrete problem, in general, there&#8217;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>\n\n\n\n<pre class=\"wp-block-preformatted\">Sum(a_i &gt; a_r) \/ N = coverage<\/pre>\n\n\n\n<p>This is much easier to solve, because <code>a_r<\/code> is bounded between 0 and 1. So, it&#8217;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>\n\n\n\n<pre class=\"wp-block-preformatted\">scale = A_r \/ a_r<\/pre>\n\n\n\n<p>An implementation of this algorithm is publicly available in <a href=\"https:\/\/github.com\/castano\/nvidia-texture-tools\/blob\/master\/src\/nvimage\/FloatImage.cpp#L1379\">NVTT<\/a>. The relevant code can be found in the following methods of the FloatImage class:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">float FloatImage::alphaTestCoverage(float alphaRef, int alphaChannel) const; <br>void FloatImage::scaleAlphaToCoverage(float desiredCoverage, float alphaRef, int alphaChannel); <\/pre>\n\n\n\n<p>And here&#8217;s a simple example of how this feature can be used through NVTT&#8217;s public API:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ Output first mipmap.<br>context.compress(image, compressionOptions, outputOptions);<br>\u200b<br>\/\/ Estimate original coverage.<br>const float coverage = image.alphaTestCoverage(A_r);<br>\u200b<br>\/\/ Build mipmaps and scale alpha to preserve original coverage.<br>while (image.buildNextMipmap(nvtt::MipmapFilter_Kaiser))<br>{<br> &nbsp; &nbsp;image.scaleAlphaToCoverage(coverage, A_r);<br> &nbsp; &nbsp;context.compress(tmpImage, compressionOptions, outputOptions);<br>}<\/pre>\n\n\n\n<p>As seen in the following screenshot, this solves the problem entirely:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"438\" src=\"https:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage-700x438.png\" alt=\"\" class=\"wp-image-1455\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage-700x438.png 700w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage-267x167.png 267w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage-768x480.png 768w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage-800x500.png 800w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/mid_coverage.png 1024w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/figure>\n<\/div>\n\n\n<p><a href=\"http:\/\/the-witness.net\/news\/wp-content\/uploads\/2010\/09\/far_coverage.png\"><\/a>even when the trees are far away from the camera:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"438\" src=\"https:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage-700x438.png\" alt=\"\" class=\"wp-image-1456\" srcset=\"http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage-700x438.png 700w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage-267x167.png 267w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage-768x480.png 768w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage-800x500.png 800w, http:\/\/www.ludicon.com\/castano\/blog\/wp-content\/uploads\/2025\/01\/far_coverage.png 1024w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/figure>\n<\/div>\n\n\n<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&#8217;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>\n\n\n\n<p><em>This article was first published on <a href=\"http:\/\/the-witness.net\/news\/2010\/09\/computing-alpha-mipmaps\/\">The Witness website<\/a>.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<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. Here&#8217;s an example. The following tree looked OK close to&#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-1218","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1218","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=1218"}],"version-history":[{"count":4,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1218\/revisions"}],"predecessor-version":[{"id":1457,"href":"http:\/\/www.ludicon.com\/castano\/blog\/wp-json\/wp\/v2\/pages\/1218\/revisions\/1457"}],"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=1218"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}