{"id":349,"date":"2024-06-27T16:20:51","date_gmt":"2024-06-27T16:20:51","guid":{"rendered":"https:\/\/www.photometric.io\/blog\/?p=349"},"modified":"2025-02-17T11:34:41","modified_gmt":"2025-02-17T11:34:41","slug":"improving-schlicks-approximation","status":"publish","type":"post","link":"https:\/\/www.photometric.io\/blog\/improving-schlicks-approximation\/","title":{"rendered":"Finding Alternatives to Schlick&#8217;s Approximation of the Fresnel Equations"},"content":{"rendered":"\n<p>Lately I&#8217;ve been spending some time developing a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Symbolic_regression\" data-type=\"link\" data-id=\"https:\/\/en.wikipedia.org\/wiki\/Schlick%27s_approximation\" target=\"_blank\" rel=\"noopener\">symbolic regression<\/a> library which mostly runs on the GPU. Symbolic regression is about finding equations that best fit a given dataset. It can also be used to find a simpler approximation of a function by generating a synthetic dataset using this function. <\/p>\n\n\n\n<p>As a first test the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Fresnel_equations\" target=\"_blank\" rel=\"noopener\">Fresnel equations<\/a> seemed like a good fit. These describe the reflection and transmission of light and are therefore very relevant for computer graphics. For real time rendering the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Schlick%27s_approximation\" target=\"_blank\" rel=\"noopener\">Schlick&#8217;s approximation<\/a> is normally used instead of the original equations because it&#8217;s a lot cheaper to compute.<\/p>\n\n\n\n<p>For generating realistic data I&#8217;ve used the following scheme: <\/p>\n\n\n\n<ul>\n<li>4096 datapoints.<\/li>\n\n\n\n<li>Uniformly distributed $\\theta$ from $0$ to $\\pi$.<\/li>\n\n\n\n<li>IOR of the material is 1.5 in 40% of cases, randomly distributed between 1.34 and 1.7 in another 40% and the rest is a random choice of a few metals (0.3+3i, 0.15+4i, 0.3+3.35i, 1.4+7i, 1.84+3.73i, 2.1+4i, 2.9+3.1i)<\/li>\n<\/ul>\n\n\n\n<p>The symbolic regression runs about 28ms until it adds the following equation to its results (with one parameter being $R_0$ and the other being $1-cos\\theta$):<br>$$R(\\theta) = R_0+(1-cos\\theta-R_0)(1-cos\\theta)^4$$<\/p>\n\n\n\n<p>This looks very similar to Schlick&#8217;s approximation:<br>$$R(\\theta) = R_0+(1-R_0)(1-cos\\theta)^5$$<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Quality Comparison<\/h2>\n\n\n\n<p>The interesting part about this equation is that it has a 34% less mean squared error (MSE) when compared to the Schlick approximation. For dielectrics the approximations essentially match in shape, while for metals it almost has a 50% improvement in MSE using the few example IORs I&#8217;ve used, the result will look different for a different dataset.<\/p>\n\n\n\n<p>Here is one example of the reflectance of aluminium:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img decoding=\"async\" width=\"899\" height=\"558\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/06\/image-1.png\" alt=\"\" class=\"wp-image-372\" style=\"width:800px;height:auto\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/06\/image-1.png 899w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/06\/image-1-300x186.png 300w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/06\/image-1-768x477.png 768w\" sizes=\"(max-width: 899px) 100vw, 899px\" \/><\/figure>\n\n\n\n<p>This is what the function looks like for different IORs:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"857\" height=\"515\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-4.png\" alt=\"\" class=\"wp-image-411\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-4.png 857w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-4-300x180.png 300w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-4-768x462.png 768w\" sizes=\"(max-width: 857px) 100vw, 857px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading has-large-font-size\">Performance Comparison<\/h2>\n\n\n\n<p>But how does it compare to the Schlick approximation in terms of performance? Here is what both approximations compile to when implemented in CUDA:<\/p>\n\n\n\n<p><strong>Schlick Approximation: <\/strong><br>$R_0+(1-R_0)(1-cos\\theta)^5$<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FADD.FTZ R7, -R4, 1       ; R7 = 1 - cos_theta\nFADD.FTZ R9, -R0, 1       ; R9 = 1 - R_0\nFMUL.FTZ R6, R7, R7       ; R6 = (1 - cos_theta)^2\nFMUL.FTZ R9, R6, R9       ; r9 = R9<em> * R6<\/em>\nFMUL.FTZ R9, R6, R9       ; r9 = R9 * R6\nFFMA.FTZ R9, R7, R9, R0   ; r9 = R_0 + R9*R7<\/code><\/pre>\n\n\n\n<p><strong>New Approximation: <\/strong><br>$R_0+(1-cos\\theta-R_0)(1-cos\\theta)^4$<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FADD.FTZ R7, -R4, 1       ; R7 = 1 - cos_theta\nFMUL.FTZ R6, R7, R7       ; R6 = (1 - cos_theta)^2\nFADD.FTZ R8, -R0, R7      ; R8 = 1 - cos_theta - R_0\nFMUL.FTZ R7, R6, R6       ; R7 = (1 - cos_theta)^4\nFFMA.FTZ R7, R7, R8, R0   ; R7 = R_0 + R7*R8<\/code><\/pre>\n\n\n\n<p>The new approximation needs 5 instructions while the Schlick approximation needs 6.<\/p>\n\n\n\n<h2 class=\"wp-block-heading has-x-large-font-size\">Controlling the Shape<\/h2>\n\n\n\n<p>In addition the function can be modified rather easily for controlling the shape. The main idea is that Schlick&#8217;s approximation and the new one behave differently for materials with a high $R_0$ and the better match essentially depends on the complex IOR of the material rather than the calculated $R_0$. This is because the shape of the Fresnel equations can vary for equal $R_0$. Therefore we can linearly interpolate between Schlick&#8217;s approximation and the new approximation like this:<br>$$R_{Schlick}(\\theta) = R_0+(1-R_0)(1-cos\\theta)^5$$ $$R_{ours}(\\theta) = R_0+(1-cos\\theta-R_0)(1-cos\\theta)^4$$ $$R_{interp}(\\theta) = R_{sch}(\\theta) &#8211; s*(R_{interp}(\\theta)-R_{sch}(\\theta))$$<br>The parameter $s$ controls the shape. The computational cost of this can be further improved by setting the exponent in Schlick&#8217;s approximation to 4, this will impact its match for dielectric materials, but we can choose to set $s=1$ to only use our approximation in this case which is close to equal for low $R_0$ anyways. This change also allows for major simplification yielding the following end result:<br>$$R_0 + (1 &#8211; R_0 &#8211; s * cos\\theta)*(1 &#8211; cos\\theta)^4$$<br>This compiles to 6 instructions in total as listed below, same as Schlick&#8217;s approximation, but now with an additional parameter controlling the shape. Another variation is to set the exponent in the approximation to 5, this adds an instruction, but often allows for a better match of the Fresnel equations (in this case set $s=0$ for dielectrics).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FADD.FTZ R8, -R7, 1 ;        R8 = 1 - cos_theta\nFADD.FTZ R9, -R0, 1 ;        R9 = 1 - f0\nFMUL.FTZ R8, R8, R8 ;        R8 = (1 - cos_theta)^2\nFFMA.FTZ R10, -R4, R7, R9 ;  R10 = 1 - f0 - s*cos_theta\nFMUL.FTZ R9, R8, R8 ;        R9 = (1-cos_theta)^4\nFFMA.FTZ R9, R9, R10, R0 ;   R9 = f0 + R10*R9<\/code><\/pre>\n\n\n\n<p>The shape parameter can be varied to better match the reflectance of metals where both approximations fail, or where one does better than the other. Here is one example:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"948\" height=\"572\" src=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-6.png\" alt=\"\" class=\"wp-image-424\" srcset=\"https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-6.png 948w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-6-300x181.png 300w, https:\/\/www.photometric.io\/blog\/wp-content\/uploads\/2024\/07\/image-6-768x463.png 768w\" sizes=\"(max-width: 948px) 100vw, 948px\" \/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Lately I&#8217;ve been spending some time developing a symbolic regression library which mostly runs on the GPU. Symbolic regression is about finding equations that best fit a given dataset. It can also be used to find a simpler approximation of a function by generating a synthetic dataset using this function. As a first test the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[21],"tags":[23,24,22],"_links":{"self":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/349"}],"collection":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/comments?post=349"}],"version-history":[{"count":60,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/349\/revisions"}],"predecessor-version":[{"id":557,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/posts\/349\/revisions\/557"}],"wp:attachment":[{"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/media?parent=349"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/categories?post=349"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.photometric.io\/blog\/wp-json\/wp\/v2\/tags?post=349"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}