<p><a href="http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.htm" rel="nofollow noreferrer">http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.htm</a> - here is reference with different geometry shading equations</p>
<p><a href="http://jcgt.org/published/0003/02/03/paper.pdf" rel="nofollow noreferrer">http://jcgt.org/published/0003/02/03/paper.pdf</a> - paper explaining how G term works and on what assumptions it is build.</p>
<hr />
<p>Looking again at your code, I'm not sure (based on first link to Karis blog) if your G term is calculated correctly.</p>
<pre><code>  float k = pow(roughness + 1, 2) / 8.f; 
  k = pow(roughness, 4);
</code></pre>
<p>You're overwritting <strong>k</strong> with roughness^4, which doesn't fall into any Schlick equation that can be seen in Karis blog and if I recall correctly, I haven't seen that in any papers. Another note is, that you're passing <em>ω</em>o and <em>ω</em>i to your geometry function, but based on your comment that this is Schlick G and overall context, I think that you should use NoL and NoV (dot first pointToLight direction and then viewDir).</p>
<p>I was trying to do my own image, but I found [Blinn 1977] paper which sums up how Smith G model works (take a look at figure 1) - <a href="https://www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf#page=2&amp;zoom=auto,-264,459" rel="nofollow noreferrer">https://www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf#page=2&amp;zoom=auto,-264,459</a></p>
<p>Some time ago, I was playing a little with different D, F, G equations - <a href="https://github.com/komilll/dxFramework/blob/master/dxFramework/Shaders/PS_BRDF.hlsl" rel="nofollow noreferrer">https://github.com/komilll/dxFramework/blob/master/dxFramework/Shaders/PS_BRDF.hlsl</a> - take a look at <strong>PS_BRDF.hlsl</strong> and <strong>PS_BRDF_Helper.hlsl</strong> for further reference. I was basing my equations on Karis blog, some are modified base on other papers e.g. Frostbite and Disney diffuse are based on respective papers if I recall correctly.</p>
<hr />
<p>I'm not sure if that's causing a problem, but it's worth fixing for sure. Also I don't think that you're using for summing frames</p>
<pre><code>(previous_average * frame_count + current_color) / (frame_count + 1) 
</code></pre>
<p>is correct, neither for uniform or importance GGX sampling. You should derive pdf in that case, it can be found in link that I've sent you before - <a href="http://cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html" rel="nofollow noreferrer">http://cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html</a> - it's generally nice tutorial, with good step by step introduction to DXR.</p>

CC BY-SA 4.0
2020-09-02T20:44:58.300Z
Thanks, I found out the main problem is Float16 render target. The maximum Float16 value is 65504. It will have overflow and precision problem. Another problem is `wo` and `wi` should both on the same hemi sphere on normal direction, sadly most documents I read didn't mention it. So one of them should be  negative ray direction.
iaomw
CC BY-SA 4.0
2020-09-02T20:47:22.440Z
This tutor from cwyman looks good. While, I didn't see a complete repo for his code (-:
iaomw
CC BY-SA 4.0
2020-09-03T09:20:35.053Z
http://cwyman.org/code/dxrTutors/dxr_tutors.md.html - at the end of page, there is link named "code bundle". You can download .zip file with each tutorial. But keep in mind, that Chris Wyman (cwyman) is using Falcor framework, so C++ will be totally different if you're not using any framework. Also HLSL code will differ, because Falcor is providing helper methods which you're not going to have with bare code.
DirectX_Programmer
CC BY-SA 4.0
2020-09-02T17:57:42.323Z
2020-09-02T17:57:42.323Z
DirectX_Programmer
<p>Despite the discussion on <code>G</code>, there are 2 major problems. Fix them will fix the problem.</p>
<ol>
<li>Float16 is limited to 65504. When multiplied by a large frame number, it will get overflow and become a small number, then never came back.
<a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format" rel="nofollow noreferrer">https://en.wikipedia.org/wiki/Half-precision_floating-point_format</a></li>
<li>Vector <code>wo</code> and <code>wi</code> should be on the same hemisphere on normal direction. So one of them should be <code>reverse</code> ray direction. Most documents I saw simply ignored this part. While, whatsoever this chapter from PBRT-v3 helped me figuring it out.</li>
</ol>
<p><a href="http://www.pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models.html" rel="nofollow noreferrer">http://www.pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models.html</a></p>
<p><a href="https://i.sstatic.net/bdsnA.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/bdsnA.png" alt="enter image description here" /></a></p>

CC BY-SA 4.0
2021-01-10T21:56:52.433Z
Just a small side note, the outgoing direction of the ray should be on the same hemisphere IF the surface material is not transmissive, so overlooking that might be intentional depending on the context
Kaan E.
CC BY-SA 4.0
2020-09-02T21:06:17.673Z
2020-09-02T21:06:17.673Z
iaomw
<p>I am trying to add GGX and PBR textures to my GPU ray tracer. The result is very strange, it's getting darker for each frame.</p>
<p><a href="https://i.sstatic.net/1tA6Y.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/1tA6Y.jpg" alt="enter image description here" /></a>
<a href="https://i.sstatic.net/eJZmo.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/eJZmo.jpg" alt="enter image description here" /></a></p>
<pre><code>        float ao = texAO.sample(textureSampler, hitRecord.uv).r;
        
        float3 albedo = texAlbedo.sample(textureSampler, hitRecord.uv).rgb;
        albedo = pow(albedo, 2.2); //albedo = float3(1.0);
        
        float metallic = texMetallic.sample(textureSampler, hitRecord.uv).r;
        float3 tNormal = texNormal.sample(textureSampler, hitRecord.uv).xyz;
        tNormal = normalize(tNormal * 2 - 1);
        
        float roughness = texRoughness.sample(textureSampler, hitRecord.uv).r;
        roughness = max(roughness, 0.001);
        
        float3 nx, ny;
        CoordinateSystem(normal, nx, ny);
        float3x3 stw = { nx, ny, normal };
        
        normal = stw * tNormal;
        CoordinateSystem(normal, nx, ny);
        stw = { nx, ny, normal };
        
        float3 wi, wh;
        float3 wo = transpose(stw) * (ray.direction);
    
        auto pSpecular = 1/(2-metallic);
        
        auto r0 = randomF(seed);
        auto r1 = randomF(seed);
        
        if (randomF(seed) &gt; pSpecular) {
            
            float sinTheta = sqrt(r0);
            float cosTheta = sqrt(1-r0);
            
            float phi = 2.0 * M_PI_F * r1;
            
            float x = sinTheta * cos(phi);
            float y = sinTheta * sin(phi);
            float z = cosTheta;
            
            wi = { x, y, z };
            wh = normalize(wo + wi); // not specular
            
        } else {
        
            auto a1 = roughness;
            auto a2 = a1 * a1;
            
            auto theta = acos(sqrt((1-r0) / ((a2-1)*r0+1)));
            auto phi = 2 * M_PI_F * r1;
            
            auto x = sin(theta) * cos(phi);
            auto y = sin(theta) * sin(phi);
            auto z = cos(theta) * 1;
            
            //wo = transpose(stw) * (ray.direction);
            wh = normalize( {x, y, z} );
            wi = reflect(wo, wh);
        }
        
        if (wi.z &lt;= 0 || dot(wh, wi) &lt;= 0) { return false; }
    
        auto D = NDF(wh, roughness);
        auto G = GX(wo, wi, roughness);
        auto F = FX(wi, wh, albedo, metallic);
        
        float denominator = 4.0f * abs(wo.z * wi.z);
        
        auto specular = D * G * F / max(FLT_MIN, denominator);
        
        auto diffuse = albedo / M_PI_F;
        
        auto kS = F;
        auto kD = (1 - metallic) * (1.0f - kS);

        ray = Ray(hitRecord.p, stw * wi);
        scatRecord.attenuation = (kD * diffuse + specular) * ao;
        
        //auto pdf = PDF(wh, roughness);
        //scatRecord.attenuation /= abs(pdf);
        
        return true;
</code></pre>
<pre><code>// Normal Distribution Function
float NDF(const thread float3&amp; h, float roughness) {
  //  GGX / Trowbridge-Reitz
  
  float a1 = roughness;
  float a2 = a1 * a1;
  float NoH = abs(h.z);
  
  float d = (NoH * a2 - NoH) * NoH + 1;
  return a2 / (d * d * M_PI_F);
}

// Geometry Function
float GX(const thread float3&amp; wo, const thread float3&amp; wi, float roughness) {
  // Schlick, remap roughness and k

  if (wo.z &gt;= 0 || wi.z &lt;= 0)
      return 0;

  float k = pow(roughness + 1, 2) / 8.f;
  
  k = pow(roughness, 4);
  
  float G1_wo = wo.z / (wo.z*(1 - k) + k);
  float G1_wi = wi.z / (wi.z*(1 - k) + k);
  return G1_wo * G1_wi;
}

// Fresnel
float3 FX(const thread float3&amp; wi, const thread float3&amp; h, const thread float3 albedo, float metallic) {
  // Schlick’s approximation
  // use a Spherical Gaussian approximation to replace the power.
  // slightly more efficient to calculate and the difference is imperceptible
  
  float3 F0 = mix(float3(0.04f), albedo, metallic);
  float HoWi = dot(h, wi);
  return F0 + (float3(1.0f) - F0) * pow(2.0f, (-5.55473f * HoWi - 6.98316f) * HoWi);
}
</code></pre>

CC BY-SA 4.0
2020-08-31T23:12:53.813Z
Getting darker from one frame to the next must mean that some input value is changing, or something about the scene is changing. The same math won't produce different results when evaluated with the same inputs. Whatever the problem is, I don't think it lies in the code you've posted here
Nathan Reed
CC BY-SA 4.0
2020-09-01T00:59:22.510Z
It's progressive ray tracing. Simply saving the average color of previous frames and current frame, then use it for next frame. I guess the difference came from randomF and floating point issue. But FLT_MAX is a very large number, there shouldn't be overflow issue. @NathanReed
iaomw
CC BY-SA 4.0
2020-09-01T13:21:08.443Z
I can't see code responsible for accumulating color. To correctly accumulate non-uniform sampling, you need to use appropriate pdf ( http://cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html ). I didn't check all of your D*F*G, but if there are no mistakes regarding missing terms or lacking of some PI, then my best guess is not using appropriate pdf. It might cause non-important frames to contribute too much, which is introducing darkening over time.
DirectX_Programmer
CC BY-SA 4.0
2020-09-01T15:39:22.297Z
At a glance it looks like an gamma/linear conversion that shouldn't be there or maybe precision loss. How are you summing the output into the final image ?
PaulHK
CC BY-SA 4.0
2020-09-02T14:07:42.583Z
(previous_average * frame_count + current_color) / (frame_count + 1) @PaulHK
iaomw
CC BY-SA 4.0
2020-09-02T14:55:52.213Z
I am a bit confused, when calculating G, should I use ray_driection or negative_ray_direction. If we didn't consider subsurface, the input ray direction will always have negative direction on normal direction.  @DirectX_Programmer
iaomw
CC BY-SA 4.0
2020-09-03T07:02:10.030Z
What format is the texture these colours are stored in ? I'm suspecting a floating point truncation issue is to blame..
PaulHK
CC BY-SA 4.0
2020-09-03T07:30:54.820Z
It's using RGBAFloat16, then changing to RGBAFloat32 could fix that problem. @PaulHK
iaomw
<raytracing><pbr><metal><ggx>
Ray Tracing, why does the GGX make my rendering result darker and darker for each frame?
iaomw
# Ray Tracing, why does the GGX make my rendering result darker and darker for each frame?

(Asked by: username_0 on 2020-08-31)

I am trying to add GGX and PBR textures to my GPU ray tracer. The result is very strange, it's getting darker for each frame.

[![enter image description here](https://i.sstatic.net/1tA6Y.jpg)](https://i.sstatic.net/1tA6Y.jpg)
[![enter image description here](https://i.sstatic.net/eJZmo.jpg)](https://i.sstatic.net/eJZmo.jpg)

```
        float ao = texAO.sample(textureSampler, hitRecord.uv).r;
        
        float3 albedo = texAlbedo.sample(textureSampler, hitRecord.uv).rgb;
        albedo = pow(albedo, 2.2); //albedo = float3(1.0);
        
        float metallic = texMetallic.sample(textureSampler, hitRecord.uv).r;
        float3 tNormal = texNormal.sample(textureSampler, hitRecord.uv).xyz;
        tNormal = normalize(tNormal * 2 - 1);
        
        float roughness = texRoughness.sample(textureSampler, hitRecord.uv).r;
        roughness = max(roughness, 0.001);
        
        float3 nx, ny;
        CoordinateSystem(normal, nx, ny);
        float3x3 stw = { nx, ny, normal };
        
        normal = stw * tNormal;
        CoordinateSystem(normal, nx, ny);
        stw = { nx, ny, normal };
        
        float3 wi, wh;
        float3 wo = transpose(stw) * (ray.direction);
    
        auto pSpecular = 1/(2-metallic);
        
        auto r0 = randomF(seed);
        auto r1 = randomF(seed);
        
        if (randomF(seed) > pSpecular) {
            
            float sinTheta = sqrt(r0);
            float cosTheta = sqrt(1-r0);
            
            float phi = 2.0 * M_PI_F * r1;
            
            float x = sinTheta * cos(phi);
            float y = sinTheta * sin(phi);
            float z = cosTheta;
            
            wi = { x, y, z };
            wh = normalize(wo + wi); // not specular
            
        } else {
        
            auto a1 = roughness;
            auto a2 = a1 * a1;
            
            auto theta = acos(sqrt((1-r0) / ((a2-1)*r0+1)));
            auto phi = 2 * M_PI_F * r1;
            
            auto x = sin(theta) * cos(phi);
            auto y = sin(theta) * sin(phi);
            auto z = cos(theta) * 1;
            
            //wo = transpose(stw) * (ray.direction);
            wh = normalize( {x, y, z} );
            wi = reflect(wo, wh);
        }
        
        if (wi.z <= 0 || dot(wh, wi) <= 0) { return false; }
    
        auto D = NDF(wh, roughness);
        auto G = GX(wo, wi, roughness);
        auto F = FX(wi, wh, albedo, metallic);
        
        float denominator = 4.0f * abs(wo.z * wi.z);
        
        auto specular = D * G * F / max(FLT_MIN, denominator);
        
        auto diffuse = albedo / M_PI_F;
        
        auto kS = F;
        auto kD = (1 - metallic) * (1.0f - kS);

        ray = Ray(hitRecord.p, stw * wi);
        scatRecord.attenuation = (kD * diffuse + specular) * ao;
        
        //auto pdf = PDF(wh, roughness);
        //scatRecord.attenuation /= abs(pdf);
        
        return true;

```

```
// Normal Distribution Function
float NDF(const thread float3& h, float roughness) {
  //  GGX / Trowbridge-Reitz
  
  float a1 = roughness;
  float a2 = a1 * a1;
  float NoH = abs(h.z);
  
  float d = (NoH * a2 - NoH) * NoH + 1;
  return a2 / (d * d * M_PI_F);
}

// Geometry Function
float GX(const thread float3& wo, const thread float3& wi, float roughness) {
  // Schlick, remap roughness and k

  if (wo.z >= 0 || wi.z <= 0)
      return 0;

  float k = pow(roughness + 1, 2) / 8.f;
  
  k = pow(roughness, 4);
  
  float G1_wo = wo.z / (wo.z*(1 - k) + k);
  float G1_wi = wi.z / (wi.z*(1 - k) + k);
  return G1_wo * G1_wi;
}

// Fresnel
float3 FX(const thread float3& wi, const thread float3& h, const thread float3 albedo, float metallic) {
  // Schlick’s approximation
  // use a Spherical Gaussian approximation to replace the power.
  // slightly more efficient to calculate and the difference is imperceptible
  
  float3 F0 = mix(float3(0.04f), albedo, metallic);
  float HoWi = dot(h, wi);
  return F0 + (float3(1.0f) - F0) * pow(2.0f, (-5.55473f * HoWi - 6.98316f) * HoWi);
}

```

**Tags:** raytracing, pbr, metal, ggx

> **Comments:**
> **username_1**: Getting darker from one frame to the next must mean that some input value is changing, or something about the scene is changing. The same math won't produce different results when evaluated with the same inputs. Whatever the problem is, I don't think it lies in the code you've posted here  
> **username_0**: It's progressive ray tracing. Simply saving the average color of previous frames and current frame, then use it for next frame. I guess the difference came from randomF and floating point issue. But FLT_MAX is a very large number, there shouldn't be overflow issue. @username_1  
> **username_2**: I can't see code responsible for accumulating color. To correctly accumulate non-uniform sampling, you need to use appropriate pdf ( http://cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html ). I didn't check all of your D*F*G, but if there are no mistakes regarding missing terms or lacking of some PI, then my best guess is not using appropriate pdf. It might cause non-important frames to contribute too much, which is introducing darkening over time.  
> **username_3**: At a glance it looks like an gamma/linear conversion that shouldn't be there or maybe precision loss. How are you summing the output into the final image ?  
> **username_0**: (previous_average * frame_count + current_color) / (frame_count + 1) @username_3  
> **username_0**: I am a bit confused, when calculating G, should I use ray_driection or negative_ray_direction. If we didn't consider subsurface, the input ray direction will always have negative direction on normal direction.  @username_2  
> **username_3**: What format is the texture these colours are stored in ? I'm suspecting a floating point truncation issue is to blame..  
> **username_0**: It's using RGBAFloat16, then changing to RGBAFloat32 could fix that problem. @username_3  

---

## 2 answers

### Answer by username_2 on 2020-09-02. Score: 1

<http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.htm> - here is reference with different geometry shading equations

<http://jcgt.org/published/0003/02/03/paper.pdf> - paper explaining how G term works and on what assumptions it is build.

---

Looking again at your code, I'm not sure (based on first link to Karis blog) if your G term is calculated correctly.

```
  float k = pow(roughness + 1, 2) / 8.f; 
  k = pow(roughness, 4);

```

You're overwritting **k** with roughness^4, which doesn't fall into any Schlick equation that can be seen in Karis blog and if I recall correctly, I haven't seen that in any papers. Another note is, that you're passing *ω*o and *ω*i to your geometry function, but based on your comment that this is Schlick G and overall context, I think that you should use NoL and NoV (dot first pointToLight direction and then viewDir).

I was trying to do my own image, but I found [Blinn 1977] paper which sums up how Smith G model works (take a look at figure 1) - <https://www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf#page=2&zoom=auto,-264,459>

Some time ago, I was playing a little with different D, F, G equations - <https://github.com/komilll/dxFramework/blob/master/dxFramework/Shaders/PS_BRDF.hlsl> - take a look at **PS\_BRDF.hlsl** and **PS\_BRDF\_Helper.hlsl** for further reference. I was basing my equations on Karis blog, some are modified base on other papers e.g. Frostbite and Disney diffuse are based on respective papers if I recall correctly.

---

I'm not sure if that's causing a problem, but it's worth fixing for sure. Also I don't think that you're using for summing frames

```
(previous_average * frame_count + current_color) / (frame_count + 1) 

```

is correct, neither for uniform or importance GGX sampling. You should derive pdf in that case, it can be found in link that I've sent you before - <http://cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html> - it's generally nice tutorial, with good step by step introduction to DXR.

> **Comments:**
> **username_0**: Thanks, I found out the main problem is Float16 render target. The maximum Float16 value is 65504. It will have overflow and precision problem. Another problem is `wo` and `wi` should both on the same hemi sphere on normal direction, sadly most documents I read didn't mention it. So one of them should be  negative ray direction.  
> **username_0**: This tutor from cwyman looks good. While, I didn't see a complete repo for his code (-:  
> **username_2**: http://cwyman.org/code/dxrTutors/dxr_tutors.md.html - at the end of page, there is link named "code bundle". You can download .zip file with each tutorial. But keep in mind, that Chris Wyman (cwyman) is using Falcor framework, so C++ will be totally different if you're not using any framework. Also HLSL code will differ, because Falcor is providing helper methods which you're not going to have with bare code.  

---

### Answer by username_0 on 2020-09-02. Score: 0

Despite the discussion on `G`, there are 2 major problems. Fix them will fix the problem.

1. Float16 is limited to 65504. When multiplied by a large frame number, it will get overflow and become a small number, then never came back.
   <https://en.wikipedia.org/wiki/Half-precision_floating-point_format>
2. Vector `wo` and `wi` should be on the same hemisphere on normal direction. So one of them should be `reverse` ray direction. Most documents I saw simply ignored this part. While, whatsoever this chapter from PBRT-v3 helped me figuring it out.

<http://www.pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models.html>

[![enter image description here](https://i.sstatic.net/bdsnA.png)](https://i.sstatic.net/bdsnA.png)

> **Comments:**
> **username_4**: Just a small side note, the outgoing direction of the ray should be on the same hemisphere IF the surface material is not transmissive, so overlooking that might be intentional depending on the context  

---

