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 enter image description here

        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);
  • 1
    $\begingroup$ 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 $\endgroup$ Commented Aug 31, 2020 at 23:12
  • $\begingroup$ 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 $\endgroup$
    – iaomw
    Commented Sep 1, 2020 at 0:59
  • $\begingroup$ I can't see code responsible for accumulating color. To correctly accumulate non-uniform sampling, you need to use appropriate pdf ( cwyman.org/code/dxrTutors/tutors/Tutor14/tutorial14.md.html ). I didn't check all of your DFG, 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. $\endgroup$ Commented Sep 1, 2020 at 13:21
  • $\begingroup$ 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 ? $\endgroup$
    – PaulHK
    Commented Sep 1, 2020 at 15:39
  • $\begingroup$ (previous_average * frame_count + current_color) / (frame_count + 1) @PaulHK $\endgroup$
    – iaomw
    Commented Sep 2, 2020 at 14:07

2 Answers 2


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.

  • $\begingroup$ 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. $\endgroup$
    – iaomw
    Commented Sep 2, 2020 at 20:44
  • $\begingroup$ This tutor from cwyman looks good. While, I didn't see a complete repo for his code (-: $\endgroup$
    – iaomw
    Commented Sep 2, 2020 at 20:47
  • $\begingroup$ 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. $\endgroup$ Commented Sep 3, 2020 at 9:20

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.


enter image description here

  • $\begingroup$ 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 $\endgroup$
    – Kaan E.
    Commented Jan 10, 2021 at 21:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.