<p><strong>To follow up my question and provide partial answer:</strong>
First of all, I'd like to thank <a href="https://computergraphics.stackexchange.com/users/10096/lightxbulb">lightxbulb</a> for providing comments which helped me to find solution.</p>
<p>So here is a basic image, 4 light bounces with 1024 spp:</p>
<p><a href="https://i.sstatic.net/5qtrm.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/5qtrm.jpg" alt="enter image description here" /></a></p>
<p>Problem here is that everything is rather light. Why? Because I've based this part of my HLSL code on <strong>MJP's path tracer</strong> ( <a href="https://github.com/TheRealMJP/DXRPathTracer" rel="nofollow noreferrer">https://github.com/TheRealMJP/DXRPathTracer</a> ) which <strong>assumes that if path allows both diffuse and specular then</strong> throughput *= 2.0. However, specular term is allowed only for first bounce with 50% chance probability. I decided that this multiplying was based on testing in his engine and it doesn't have physical basis so I got rid of it and got better results. Specular/metal parts are basically the same, but you can see darkenings in the background which grounds objects in their place.</p>
<p><a href="https://i.sstatic.net/4EVAD.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/4EVAD.jpg" alt="enter image description here" /></a></p>
<p>However I noticed that throughput is not mutiplied. MJP's algorithm is that throughput = 0, then throughput += diffuse/specular BRDF. He doesn't multiply BRDF results in the fashion presented in PBRT book ( <a href="http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html" rel="nofollow noreferrer">http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html</a> ). So I've added throughput to the payload. <strong>It starts with value (1.0, 1.0, 1.0) and is multiplied by BRDF at each point in the path resulting in below image:</strong></p>
<p><a href="https://i.sstatic.net/kxWAH.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/kxWAH.jpg" alt="enter image description here" /></a></p>
<p><strong>Based on my knowledge, last image should be closest to ground truth in physics terms.</strong> Also, I think that it's closest to my reference that I've created in MJP's engine and I find it most pleasing. I am aware of the flaws. One is that for many samples (3000-5000 spp), diffuse textures starts having weird contrast and I don't know the reason yet.</p>
<p>Also russian roulette turned out to be workin poorly in my progressive path tracing. <a href="http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/Russian_Roulette_and_Splitting.html" rel="nofollow noreferrer">http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/Russian_Roulette_and_Splitting.html</a> - maybe after trying to optimize it to reduce variance it would be worth but I'm afraid that it'll still introduce too much noise with very little speedup. It might be worth giving it a try after adding denoiser I think, but for now, I don't think it's a good idea.</p>

CC BY-SA 4.0
2020-10-15T08:31:11.677Z
2020-10-15T08:31:11.677Z
DirectX_Programmer
<p>I'm creating DXR PathTracer highly influence by Matt Pettineo's one - <a href="https://github.com/TheRealMJP/DXRPathTracer" rel="nofollow noreferrer">https://github.com/TheRealMJP/DXRPathTracer</a> ; Relevant HLSL code below:</p>
<pre><code>[shader(&quot;raygeneration&quot;)]
void RayGen()
{
    // Accumulate for limited amount of frames
    if (g_giCB.maxFrames &gt; 0 &amp;&amp; g_giCB.accFrames &gt;= g_giCB.maxFrames)
    {
        return;
    }
    uint2 LaunchIndex = DispatchRaysIndex().xy;
    uint2 LaunchDimensions = DispatchRaysDimensions().xy;
    
    if (g_giCB.samplingType == SAMPLE_MJ)
    {
        const uint pixelIdx = LaunchIndex.y * LaunchDimensions.x + LaunchIndex.x;
        uint sampleSetIdx = 0;
        offset = SamplePoint(pixelIdx, sampleSetIdx);
        seed = pixelIdx;
        seed2 = sampleSetIdx;
    }
    
    float3 primaryRayOrigin = g_sceneCB.cameraPosition.xyz;
    float3 primaryRayDirection;
    GenerateCameraRay(LaunchIndex, LaunchDimensions, g_sceneCB.projectionToWorld, primaryRayOrigin, primaryRayDirection, offset);
    
    // Prepare payload
    PayloadIndirect indirectPayload;
    indirectPayload.color = float3(0, 0, 0);
    indirectPayload.roughness = 0;
    indirectPayload.rndSeed = seed;
    indirectPayload.rndSeed2 = seed2;
    indirectPayload.pathLength = 0;
    indirectPayload.diffusePath = false;
        
    // Calculate pixel color in current pass and merge with previous frames
    float4 finalColor = float4(shootIndirectRay(primaryRayOrigin, primaryRayDirection, 1e-3f, indirectPayload), 1.0f);
    bool colorsNan = any(isnan(finalColor));
    if (colorsNan)
    {
        finalColor = float4(0, 0, 0, 1);
    }
    
    const float FP16Max = 65000.0f;
    finalColor = clamp(finalColor, 0.0f, FP16Max);
    
    if (g_giCB.accFrames &gt; 0)
    {
        float4 prevScene = RTOutput[LaunchIndex];
        finalColor = ((float) g_giCB.accFrames * prevScene + finalColor) / ((float) g_giCB.accFrames + 1.0f);
    }
    RTOutput[LaunchIndex] = finalColor;
}

[shader(&quot;miss&quot;)]
void Miss(inout RayPayload payload : SV_RayPayload)
{
    payload.vis = 1.0f;
}

[shader(&quot;closesthit&quot;)]
void ClosestHit(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{

}

[shader(&quot;miss&quot;)]
void MissIndirect(inout PayloadIndirect payload : SV_RayPayload)
{
    // Use skybox as contribution if ray failed to hit geometry (right now, disabled for debug purposes)
    float3 rayDir = WorldRayDirection();
    rayDir.z = -rayDir.z;
    if (g_giCB.useSkybox)
    {
        payload.color += skyboxTexture.SampleLevel(g_sampler, rayDir, 0).rgb;
    }
}
   
float3 CalcLighting(in float3 normal, in float3 lightDir, in float3 peakIrradiance,
                    in float3 diffuseAlbedo, in float3 specularAlbedo, in float roughness,
                    in float3 positionWS, in float3 cameraPosWS, in float3 msEnergyCompensation)
{
    float3 lighting = diffuseAlbedo * INV_PI;

    float3 view = normalize(cameraPosWS - positionWS);
    const float NoL = saturate(dot(normal, lightDir));
    if (NoL &gt; 0.0f)
    {
        const float NoV = saturate(dot(normal, view));
        float3 h = normalize(view + lightDir);

        float3 fresnel = Fresnel(specularAlbedo, h, lightDir);

        float specular = GGXSpecular(roughness, normal, h, view, lightDir);
        lighting += specular * fresnel * msEnergyCompensation;
    }

    return lighting * NoL * peakIrradiance;
}
    
float3 CalculateRadiance(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{
    /* Prepare input data */
    float3 hitPos = WorldRayOrigin() + WorldRayDirection() * RayTCurrent();
    float3 triangleNormal, triangleTangent, triangleBitangent;
    loadHitData(triangleNormal, triangleTangent, triangleBitangent, attribs);

    float4 albedo = albedoTexture.Load(int3(DispatchRaysIndex().xy, 0));

    float roughness = specularTexture.Load(int3(DispatchRaysIndex().xy, 0)).g;
    roughness = max(roughness, payload.roughness);
    float metallic = specularTexture.Load(int3(DispatchRaysIndex().xy, 0)).b;
    float3 normalTextureData = NormalTextureInput.Load(int3(DispatchRaysIndex().xy, 0)).rgb;
    float3x3 tangentToWorld = float3x3(triangleTangent, triangleBitangent, triangleNormal);
    float3 normalWS = triangleNormal;

    bool enableDiffuse = metallic &lt; 1.0f;
    bool enableSpecular = !payload.diffusePath;
    
    if (enableDiffuse == false &amp;&amp; enableSpecular == false)
        return float3(0, 0, 0);
    
    const float3 diffuseAlbedo = lerp(albedo.rgb, 0.0f, metallic) * (enableDiffuse ? 1.0f : 0.0f);
    const float3 specularAlbedo = lerp(0.03f, albedo.rgb, metallic) * (enableSpecular ? 1.0f : 0.0f);

    float3 msEnergyCompensation = float3(1, 1, 1);
    float2 DFG = dfgTexture.SampleLevel(g_sampler, float2(saturate(dot(triangleNormal, -WorldRayDirection())), roughness), 0.0f).xy;
    float Ess = DFG.x;
    msEnergyCompensation = float3(1, 1, 1) + specularAlbedo * (1.0f / Ess - 1.0f);
    float3 worldOrigin = WorldRayOrigin();  
    /*********************************************/
    
    // Calculate directional light
    {
        float3 V = g_sceneCB.cameraPosition.xyz;
        float3 N = triangleNormal.xyz;
        
        float3 L = normalize(g_giCB.sunDirection);
        float distToLight = 1e+38f;
        
        // Check visibility
        float NoL = saturate(dot(N, L));
        float visibility = shadowRayVisibility(hitPos, L, 1e-3f, distToLight);
        
        // Calculate light contribution to point in world (diffuse lambertian term)
        payload.color += CalcLighting(triangleNormal, L, 1.0f, diffuseAlbedo, specularAlbedo,
                    roughness, hitPos, worldOrigin, msEnergyCompensation) * visibility;
    }

    float2 brdfSample = SamplePoint(payload.rndSeed, payload.rndSeed2);
                
    float selector = brdfSample.x;
    if (enableSpecular == false)
        selector = 0.0f;
    else if (enableDiffuse == false)
        selector = 1.0f;
        
    if (g_giCB.useIndirect == 1)
    {
        float3 throughput = float3(0, 0, 0);
                    
        // Find next direction
        float3 rayDirWS = float3(0, 0, 0);
        if (g_giCB.samplingType == SAMPLE_MJ)
        {
            if (selector &lt; 0.5f)
            {
                if (enableSpecular)
                    brdfSample.x *= 2.0f;
                float3 rayDirTS = SampleDirectionCosineHemisphere(brdfSample.x, brdfSample.y);
                rayDirWS = normalize(mul(rayDirTS, tangentToWorld));
                throughput = diffuseAlbedo;
            }
            else
            {
                if (enableDiffuse)
                    brdfSample.x = (brdfSample.x - 0.5f) * 2.0f;

                float3 incomingRayDirWS = WorldRayDirection();
                float3 incomingRayDirTS = normalize(mul(incomingRayDirWS, transpose(tangentToWorld)));

                float3 wo = incomingRayDirTS;
                float3 wm = SampleGGXVisibleNormal(-wo, roughness, roughness, brdfSample.x, brdfSample.y);
                float3 wi = reflect(wo, wm);

                float3 normalTS = float3(0.0f, 0.0f, 1.0f);

                float3 F = Fresnel(specularAlbedo.rgb, wm, wi);
                float G1 = SmithGGXMasking(normalTS, wi, -wo, roughness * roughness);
                float G2 = SmithGGXMaskingShadowing(normalTS, wi, -wo, roughness * roughness);

                throughput = (F * (G2 / G1));
                float3 rayDirTS = wi;
                rayDirWS = normalize(mul(rayDirTS, tangentToWorld));
                    
#if 1
                DFG = dfgTexture.SampleLevel(g_sampler, float2(saturate(dot(triangleNormal, -WorldRayDirection())), roughness), 0.0f).xy;
                Ess = DFG.x;
                throughput *= float3(1, 1, 1) + specularAlbedo * (1.0f / Ess - 1.0f);
#endif
            }
        }
            
        if (enableDiffuse &amp;&amp; enableSpecular)
            throughput *= 2.0f;
            
        if (payload.pathLength &lt; g_giCB.bounceCount)
        {
            // Prepare payload
            PayloadIndirect newPayload;
            newPayload.pathLength = payload.pathLength + 1;
            newPayload.rndSeed = payload.rndSeed;
            newPayload.rndSeed2 = payload.rndSeed2;
            newPayload.color = float3(0, 0, 0);
            newPayload.diffusePath = (selector &lt; 0.5f);
            newPayload.roughness = roughness;
            
            // Calculate next ray bounce color contribution
            float3 bounceColor = shootIndirectRay(hitPos, rayDirWS, 1e-3f, newPayload);
            
            // Check to make sure our randomly selected, normal mapped diffuse ray didn't go below the surface.
            if (dot(normalWS, rayDirWS) &lt;= 0.0f)
                bounceColor = float3(0, 0, 0);
            
            payload.color += bounceColor * throughput; //* albedo.rgb;
        }
    }
    
    return payload.color;
}

[shader(&quot;closesthit&quot;)]
void ClosestHitIndirect(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{
    payload.color = CalculateRadiance(payload, attribs);
}
</code></pre>
<p>Problem with my code occurs when using path of length more than 1 (indirect light bounces more than once). Shadows are starting to disappear and objects doesn't look grounded anymore. Series of images will occur now. Open full-size for better comparison. Points of interest marked in red in first image. Information about path length can be seen in GUI on the left. All images are generated with 1024 spp.</p>
<p>Here is my render (1-4-8 indirect bounces in that order):
<a href="https://i.sstatic.net/b21tq.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/b21tq.jpg" alt="enter image description here" /></a></p>
<p><a href="https://i.sstatic.net/Urvp5.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/Urvp5.jpg" alt="enter image description here" /></a></p>
<p><a href="https://i.sstatic.net/r2Y4V.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/r2Y4V.jpg" alt="enter image description here" /></a></p>
<p>As you can see, information about shadows are lost in the process of increasing path length. Now, I tried to skip skybox, so any miss shader will just return (0, 0, 0) instead of skybox values. Results are slightly better (4, 8 indirect bounces):</p>
<p><a href="https://i.sstatic.net/3UQYK.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/3UQYK.jpg" alt="enter image description here" /></a></p>
<p><a href="https://i.sstatic.net/31t8j.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/31t8j.jpg" alt="enter image description here" /></a></p>
<p>Here is reference from MJP's Path Tracer mentioned above (3-6 path length, but it's calculated differently. In my terms in 1-4 path length actually). No specular paths. Take a look at points of interest. Shadows are well grounded and you can see that creases are not getting many light:</p>
<p><a href="https://i.sstatic.net/yEPdB.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/yEPdB.jpg" alt="enter image description here" /></a></p>
<p><a href="https://i.sstatic.net/cmP1v.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/cmP1v.jpg" alt="enter image description here" /></a></p>
<p>And here is ground truth reference created in MJP's Path Tracer with 5000 spp, 8 (in my terms - 6) path length:</p>
<p><a href="https://i.sstatic.net/qGbFG.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/qGbFG.jpg" alt="enter image description here" /></a></p>

CC BY-SA 4.0
2020-10-12T19:55:51.520Z
Yes indirect illumination can bring extra light. By cutting off the bounces at some length you are cutting off part of the energy.
lightxbulb
CC BY-SA 4.0
2020-10-12T20:28:16.783Z
@lightxbulb You're right and I'm aware of that. However, shouldn't some corners, creases, holes etc. still be dark as presented in ground truth reference (last image)? I feel like my image is failing to present any contrast between objects because everything becomes too bright. In advance - I am using exposure slider, aces filmic tone mapping and convertion from linear to sRGB space.
DirectX_Programmer
CC BY-SA 4.0
2020-10-13T08:35:42.117Z
Considering you're using exposure slider, tone mapping, etc, and that similarly the "reference" may use a 100 different things, it's impossible to tell anything. It could be that your code or the code used to generate the reference has a GI bug, or both. Or it could be that the two don't agree on some parameters/tonemapping.
lightxbulb
CC BY-SA 4.0
2020-10-13T08:48:42.143Z
@lightxbulb I read MJP's code multiple times but you're right that our postprocesses are a bit different and it may be causing difference in final image. You're right that there is some bug probably but I cannot find it. Maybe taking a look at postprocess will be helpful as you've said. I don't know if reference is wrong. I'm not experienced in production quality raytracing so I'd appreciate your opinion. My personal guess that my code is wrong is based on a fact that there are no darkenings in holes/corners which should naturally happen for RTAO and even more for RTGI/Path Tracing.
DirectX_Programmer
CC BY-SA 4.0
2020-10-13T09:51:24.557Z
Actually with more bounces, corners/holes may become brighter due to indirect illumination. The other way to keep it darker is to: decrease the light intensity or decrease the materials reflectivity. In either case the image will look darker overall however.
lightxbulb
CC BY-SA 4.0
2020-10-13T10:32:30.783Z
You could also make only the corners & holes darker by modifying only their material. So if you use a texture, just darken the material near corners/holes. To keep it realistic you can make it due to mold/wear and tear.
lightxbulb
CC BY-SA 4.0
2020-10-13T13:26:13.067Z
Changing MJP's postprocess to ones that I've created makes no noticable difference. Roughness and metalness of materials is basically same. BRDF is also the same. Thanks for your advice on modifing texture/material but what I'm trying to achieve ground truth reference. I'm not planning on making any artistic driven adjustments.
DirectX_Programmer
CC BY-SA 4.0
2020-10-13T15:49:01.960Z
Disclaimer: I do not plan to wade through MJP or your code to search for a bug that may not be there. After all very minor differences (that are not necessarily bugs) can result in the differences you mention. What you could do however, is to pick a very simple scene: cornell, some spheres, or w/e. Then remove all postprocess and check that the results are not too different between the two renderers. The usual suspects for bugs would be: random number generation, ray scattering and its associated pdf, the estimator that you are using. I could not find where you acc attenuation either.
lightxbulb
CC BY-SA 4.0
2020-10-13T18:06:06.560Z
I understand that. Thank you for your help. I've started changing my code a little and through experimentation I'm starting to get better results. I'll be modifying throughput settings a little, reducing specular path length and adding russian roulette. Thank you for your help. I'll make sure to post my solution and reason why previous results weren't satisfying as soon as I finish my work.
DirectX_Programmer
<raytracing><pathtracing><brdf><hlsl>
Pathtracing - many bounces reduce shadows
DirectX_Programmer
# Pathtracing - many bounces reduce shadows

(Asked by: username_0 on 2020-10-12)

I'm creating DXR PathTracer highly influence by Matt Pettineo's one - <https://github.com/TheRealMJP/DXRPathTracer> ; Relevant HLSL code below:

```
[shader("raygeneration")]
void RayGen()
{
    // Accumulate for limited amount of frames
    if (g_giCB.maxFrames > 0 && g_giCB.accFrames >= g_giCB.maxFrames)
    {
        return;
    }
    uint2 LaunchIndex = DispatchRaysIndex().xy;
    uint2 LaunchDimensions = DispatchRaysDimensions().xy;
    
    if (g_giCB.samplingType == SAMPLE_MJ)
    {
        const uint pixelIdx = LaunchIndex.y * LaunchDimensions.x + LaunchIndex.x;
        uint sampleSetIdx = 0;
        offset = SamplePoint(pixelIdx, sampleSetIdx);
        seed = pixelIdx;
        seed2 = sampleSetIdx;
    }
    
    float3 primaryRayOrigin = g_sceneCB.cameraPosition.xyz;
    float3 primaryRayDirection;
    GenerateCameraRay(LaunchIndex, LaunchDimensions, g_sceneCB.projectionToWorld, primaryRayOrigin, primaryRayDirection, offset);
    
    // Prepare payload
    PayloadIndirect indirectPayload;
    indirectPayload.color = float3(0, 0, 0);
    indirectPayload.roughness = 0;
    indirectPayload.rndSeed = seed;
    indirectPayload.rndSeed2 = seed2;
    indirectPayload.pathLength = 0;
    indirectPayload.diffusePath = false;
        
    // Calculate pixel color in current pass and merge with previous frames
    float4 finalColor = float4(shootIndirectRay(primaryRayOrigin, primaryRayDirection, 1e-3f, indirectPayload), 1.0f);
    bool colorsNan = any(isnan(finalColor));
    if (colorsNan)
    {
        finalColor = float4(0, 0, 0, 1);
    }
    
    const float FP16Max = 65000.0f;
    finalColor = clamp(finalColor, 0.0f, FP16Max);
    
    if (g_giCB.accFrames > 0)
    {
        float4 prevScene = RTOutput[LaunchIndex];
        finalColor = ((float) g_giCB.accFrames * prevScene + finalColor) / ((float) g_giCB.accFrames + 1.0f);
    }
    RTOutput[LaunchIndex] = finalColor;
}

[shader("miss")]
void Miss(inout RayPayload payload : SV_RayPayload)
{
    payload.vis = 1.0f;
}

[shader("closesthit")]
void ClosestHit(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{

}

[shader("miss")]
void MissIndirect(inout PayloadIndirect payload : SV_RayPayload)
{
    // Use skybox as contribution if ray failed to hit geometry (right now, disabled for debug purposes)
    float3 rayDir = WorldRayDirection();
    rayDir.z = -rayDir.z;
    if (g_giCB.useSkybox)
    {
        payload.color += skyboxTexture.SampleLevel(g_sampler, rayDir, 0).rgb;
    }
}
   
float3 CalcLighting(in float3 normal, in float3 lightDir, in float3 peakIrradiance,
                    in float3 diffuseAlbedo, in float3 specularAlbedo, in float roughness,
                    in float3 positionWS, in float3 cameraPosWS, in float3 msEnergyCompensation)
{
    float3 lighting = diffuseAlbedo * INV_PI;

    float3 view = normalize(cameraPosWS - positionWS);
    const float NoL = saturate(dot(normal, lightDir));
    if (NoL > 0.0f)
    {
        const float NoV = saturate(dot(normal, view));
        float3 h = normalize(view + lightDir);

        float3 fresnel = Fresnel(specularAlbedo, h, lightDir);

        float specular = GGXSpecular(roughness, normal, h, view, lightDir);
        lighting += specular * fresnel * msEnergyCompensation;
    }

    return lighting * NoL * peakIrradiance;
}
    
float3 CalculateRadiance(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{
    /* Prepare input data */
    float3 hitPos = WorldRayOrigin() + WorldRayDirection() * RayTCurrent();
    float3 triangleNormal, triangleTangent, triangleBitangent;
    loadHitData(triangleNormal, triangleTangent, triangleBitangent, attribs);

    float4 albedo = albedoTexture.Load(int3(DispatchRaysIndex().xy, 0));

    float roughness = specularTexture.Load(int3(DispatchRaysIndex().xy, 0)).g;
    roughness = max(roughness, payload.roughness);
    float metallic = specularTexture.Load(int3(DispatchRaysIndex().xy, 0)).b;
    float3 normalTextureData = NormalTextureInput.Load(int3(DispatchRaysIndex().xy, 0)).rgb;
    float3x3 tangentToWorld = float3x3(triangleTangent, triangleBitangent, triangleNormal);
    float3 normalWS = triangleNormal;

    bool enableDiffuse = metallic < 1.0f;
    bool enableSpecular = !payload.diffusePath;
    
    if (enableDiffuse == false && enableSpecular == false)
        return float3(0, 0, 0);
    
    const float3 diffuseAlbedo = lerp(albedo.rgb, 0.0f, metallic) * (enableDiffuse ? 1.0f : 0.0f);
    const float3 specularAlbedo = lerp(0.03f, albedo.rgb, metallic) * (enableSpecular ? 1.0f : 0.0f);

    float3 msEnergyCompensation = float3(1, 1, 1);
    float2 DFG = dfgTexture.SampleLevel(g_sampler, float2(saturate(dot(triangleNormal, -WorldRayDirection())), roughness), 0.0f).xy;
    float Ess = DFG.x;
    msEnergyCompensation = float3(1, 1, 1) + specularAlbedo * (1.0f / Ess - 1.0f);
    float3 worldOrigin = WorldRayOrigin();  
    /*********************************************/
    
    // Calculate directional light
    {
        float3 V = g_sceneCB.cameraPosition.xyz;
        float3 N = triangleNormal.xyz;
        
        float3 L = normalize(g_giCB.sunDirection);
        float distToLight = 1e+38f;
        
        // Check visibility
        float NoL = saturate(dot(N, L));
        float visibility = shadowRayVisibility(hitPos, L, 1e-3f, distToLight);
        
        // Calculate light contribution to point in world (diffuse lambertian term)
        payload.color += CalcLighting(triangleNormal, L, 1.0f, diffuseAlbedo, specularAlbedo,
                    roughness, hitPos, worldOrigin, msEnergyCompensation) * visibility;
    }

    float2 brdfSample = SamplePoint(payload.rndSeed, payload.rndSeed2);
                
    float selector = brdfSample.x;
    if (enableSpecular == false)
        selector = 0.0f;
    else if (enableDiffuse == false)
        selector = 1.0f;
        
    if (g_giCB.useIndirect == 1)
    {
        float3 throughput = float3(0, 0, 0);
                    
        // Find next direction
        float3 rayDirWS = float3(0, 0, 0);
        if (g_giCB.samplingType == SAMPLE_MJ)
        {
            if (selector < 0.5f)
            {
                if (enableSpecular)
                    brdfSample.x *= 2.0f;
                float3 rayDirTS = SampleDirectionCosineHemisphere(brdfSample.x, brdfSample.y);
                rayDirWS = normalize(mul(rayDirTS, tangentToWorld));
                throughput = diffuseAlbedo;
            }
            else
            {
                if (enableDiffuse)
                    brdfSample.x = (brdfSample.x - 0.5f) * 2.0f;

                float3 incomingRayDirWS = WorldRayDirection();
                float3 incomingRayDirTS = normalize(mul(incomingRayDirWS, transpose(tangentToWorld)));

                float3 wo = incomingRayDirTS;
                float3 wm = SampleGGXVisibleNormal(-wo, roughness, roughness, brdfSample.x, brdfSample.y);
                float3 wi = reflect(wo, wm);

                float3 normalTS = float3(0.0f, 0.0f, 1.0f);

                float3 F = Fresnel(specularAlbedo.rgb, wm, wi);
                float G1 = SmithGGXMasking(normalTS, wi, -wo, roughness * roughness);
                float G2 = SmithGGXMaskingShadowing(normalTS, wi, -wo, roughness * roughness);

                throughput = (F * (G2 / G1));
                float3 rayDirTS = wi;
                rayDirWS = normalize(mul(rayDirTS, tangentToWorld));
                    
#if 1
                DFG = dfgTexture.SampleLevel(g_sampler, float2(saturate(dot(triangleNormal, -WorldRayDirection())), roughness), 0.0f).xy;
                Ess = DFG.x;
                throughput *= float3(1, 1, 1) + specularAlbedo * (1.0f / Ess - 1.0f);
#endif
            }
        }
            
        if (enableDiffuse && enableSpecular)
            throughput *= 2.0f;
            
        if (payload.pathLength < g_giCB.bounceCount)
        {
            // Prepare payload
            PayloadIndirect newPayload;
            newPayload.pathLength = payload.pathLength + 1;
            newPayload.rndSeed = payload.rndSeed;
            newPayload.rndSeed2 = payload.rndSeed2;
            newPayload.color = float3(0, 0, 0);
            newPayload.diffusePath = (selector < 0.5f);
            newPayload.roughness = roughness;
            
            // Calculate next ray bounce color contribution
            float3 bounceColor = shootIndirectRay(hitPos, rayDirWS, 1e-3f, newPayload);
            
            // Check to make sure our randomly selected, normal mapped diffuse ray didn't go below the surface.
            if (dot(normalWS, rayDirWS) <= 0.0f)
                bounceColor = float3(0, 0, 0);
            
            payload.color += bounceColor * throughput; //* albedo.rgb;
        }
    }
    
    return payload.color;
}

[shader("closesthit")]
void ClosestHitIndirect(inout PayloadIndirect payload, in BuiltInTriangleIntersectionAttributes attribs)
{
    payload.color = CalculateRadiance(payload, attribs);
}

```

Problem with my code occurs when using path of length more than 1 (indirect light bounces more than once). Shadows are starting to disappear and objects doesn't look grounded anymore. Series of images will occur now. Open full-size for better comparison. Points of interest marked in red in first image. Information about path length can be seen in GUI on the left. All images are generated with 1024 spp.

Here is my render (1-4-8 indirect bounces in that order):
[![enter image description here](https://i.sstatic.net/b21tq.jpg)](https://i.sstatic.net/b21tq.jpg)

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

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

As you can see, information about shadows are lost in the process of increasing path length. Now, I tried to skip skybox, so any miss shader will just return (0, 0, 0) instead of skybox values. Results are slightly better (4, 8 indirect bounces):

[![enter image description here](https://i.sstatic.net/3UQYK.jpg)](https://i.sstatic.net/3UQYK.jpg)

[![enter image description here](https://i.sstatic.net/31t8j.jpg)](https://i.sstatic.net/31t8j.jpg)

Here is reference from MJP's Path Tracer mentioned above (3-6 path length, but it's calculated differently. In my terms in 1-4 path length actually). No specular paths. Take a look at points of interest. Shadows are well grounded and you can see that creases are not getting many light:

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

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

And here is ground truth reference created in MJP's Path Tracer with 5000 spp, 8 (in my terms - 6) path length:

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

**Tags:** raytracing, pathtracing, brdf, hlsl

> **Comments:**
> **username_1**: Yes indirect illumination can bring extra light. By cutting off the bounces at some length you are cutting off part of the energy.  
> **username_0**: @username_1 You're right and I'm aware of that. However, shouldn't some corners, creases, holes etc. still be dark as presented in ground truth reference (last image)? I feel like my image is failing to present any contrast between objects because everything becomes too bright. In advance - I am using exposure slider, aces filmic tone mapping and convertion from linear to sRGB space.  
> **username_1**: Considering you're using exposure slider, tone mapping, etc, and that similarly the "reference" may use a 100 different things, it's impossible to tell anything. It could be that your code or the code used to generate the reference has a GI bug, or both. Or it could be that the two don't agree on some parameters/tonemapping.  
> **username_0**: @username_1 I read MJP's code multiple times but you're right that our postprocesses are a bit different and it may be causing difference in final image. You're right that there is some bug probably but I cannot find it. Maybe taking a look at postprocess will be helpful as you've said. I don't know if reference is wrong. I'm not experienced in production quality raytracing so I'd appreciate your opinion. My personal guess that my code is wrong is based on a fact that there are no darkenings in holes/corners which should naturally happen for RTAO and even more for RTGI/Path Tracing.  
> **username_1**: Actually with more bounces, corners/holes may become brighter due to indirect illumination. The other way to keep it darker is to: decrease the light intensity or decrease the materials reflectivity. In either case the image will look darker overall however.  
> **username_1**: You could also make only the corners & holes darker by modifying only their material. So if you use a texture, just darken the material near corners/holes. To keep it realistic you can make it due to mold/wear and tear.  
> **username_0**: Changing MJP's postprocess to ones that I've created makes no noticable difference. Roughness and metalness of materials is basically same. BRDF is also the same. Thanks for your advice on modifing texture/material but what I'm trying to achieve ground truth reference. I'm not planning on making any artistic driven adjustments.  
> **username_1**: Disclaimer: I do not plan to wade through MJP or your code to search for a bug that may not be there. After all very minor differences (that are not necessarily bugs) can result in the differences you mention. What you could do however, is to pick a very simple scene: cornell, some spheres, or w/e. Then remove all postprocess and check that the results are not too different between the two renderers. The usual suspects for bugs would be: random number generation, ray scattering and its associated pdf, the estimator that you are using. I could not find where you acc attenuation either.  
> **username_0**: I understand that. Thank you for your help. I've started changing my code a little and through experimentation I'm starting to get better results. I'll be modifying throughput settings a little, reducing specular path length and adding russian roulette. Thank you for your help. I'll make sure to post my solution and reason why previous results weren't satisfying as soon as I finish my work.  

---

## 1 answers

### Answer by username_0 on 2020-10-15. Score: 1 | Accepted answer

**To follow up my question and provide partial answer:**
First of all, I'd like to thank [username_1](https://computergraphics.stackexchange.com/users/10096/username_1) for providing comments which helped me to find solution.

So here is a basic image, 4 light bounces with 1024 spp:

[![enter image description here](https://i.sstatic.net/5qtrm.jpg)](https://i.sstatic.net/5qtrm.jpg)

Problem here is that everything is rather light. Why? Because I've based this part of my HLSL code on **MJP's path tracer** ( <https://github.com/TheRealMJP/DXRPathTracer> ) which **assumes that if path allows both diffuse and specular then** throughput \*= 2.0. However, specular term is allowed only for first bounce with 50% chance probability. I decided that this multiplying was based on testing in his engine and it doesn't have physical basis so I got rid of it and got better results. Specular/metal parts are basically the same, but you can see darkenings in the background which grounds objects in their place.

[![enter image description here](https://i.sstatic.net/4EVAD.jpg)](https://i.sstatic.net/4EVAD.jpg)

However I noticed that throughput is not mutiplied. MJP's algorithm is that throughput = 0, then throughput += diffuse/specular BRDF. He doesn't multiply BRDF results in the fashion presented in PBRT book ( <http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html> ). So I've added throughput to the payload. **It starts with value (1.0, 1.0, 1.0) and is multiplied by BRDF at each point in the path resulting in below image:**

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

**Based on my knowledge, last image should be closest to ground truth in physics terms.** Also, I think that it's closest to my reference that I've created in MJP's engine and I find it most pleasing. I am aware of the flaws. One is that for many samples (3000-5000 spp), diffuse textures starts having weird contrast and I don't know the reason yet.

Also russian roulette turned out to be workin poorly in my progressive path tracing. <http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/Russian_Roulette_and_Splitting.html> - maybe after trying to optimize it to reduce variance it would be worth but I'm afraid that it'll still introduce too much noise with very little speedup. It might be worth giving it a try after adding denoiser I think, but for now, I don't think it's a good idea.


---

