<p>After some <a href="https://chat.stackexchange.com/rooms/154146/discussion-between-enigmatisms-and-tom-clabault">discusson</a> with Enigmatisms, it turns out that:</p>
<ol>
<li>I had an error in my renderer</li>
<li>Bad samples are expected from VNDF sampling.</li>
</ol>
<h3>The error in my renderer</h3>
<p>I'm using VNDF sampling to sample a normal about which to reflect the ray direction. If the sampled normal yields a reflected direction that is below the surface normal, I terminate the pixel.</p>
<p>The error here is that I am terminating the pixel before taking direct lighting into account (even though direct lighting yields a positive contribution). This means that I'm getting a black pixel where at least direct lighting should have contributed.</p>
<h3>Bad samples are expected</h3>
<p><a href="https://i.sstatic.net/o5ZsnoA4.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/o5ZsnoA4.png" alt="Perfectly specular VNDF valid normals" /></a>
Illustration of a perfectly specular reflection case by Enigmatisms.</p>
<p>For a given incoming direction <span class="math-container">$\omega_i$</span> and a surface normal <span class="math-container">$n$</span>, not all microfacet normals <span class="math-container">$\omega_m$</span> produced lead to a reflected direction that is above the hemisphere.</p>
<p>In the illustration, only microfacet normals <span class="math-container">$\omega_m$</span> in between the two green <span class="math-container">$\omega_m$</span> will lead to a reflected direction that is above the hemisphere.</p>
<p>Normals sampled in the red region (which can be sampled by VNDF sampling) will yield a reflection direction which is below the hemisphere.</p>
<h3>Bad samples must be discarded</h3>
<p>These samples must be discarded and rejection sampling (for example) cannot be used here, otherwise this leads to bias (derivation by Enigmatisms):</p>
<p><a href="https://i.sstatic.net/UI0xO1ED.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/UI0xO1ED.png" alt="VNDF rejection sampling bias derivation" /></a></p>

CC BY-SA 4.0
2024-07-18T09:55:10.050Z
2024-07-18T09:55:10.050Z
Tom Clabault
<p>I've noticed that my implementation of GGX VNDF sampling produces quite a lot of bad samples (below the hemisphere). When such a sample occurs, I terminate the ray, leading to an immediate black pixel.</p>
<p><a href="https://i.sstatic.net/oliatbA4.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/oliatbA4.png" alt="GGX VNDF - Lots of bad samples on the backwall" /></a></p>
<p>All the black pixels on the backwall are due solely to the metallic lobe (which uses GGX VNDF sampling) of my BRDF sampling a direction below the normal. That's maybe 1/3 or 1/4 of the samples thrown away. This seems to be quite a waste in terms of efficiency.</p>
<p>The roughness of the wall in the back is 0.9. Screenshot rendered with 0 bounce and 1 SPP.</p>
<p>Here's my implementation if needed:</p>
<pre><code>float3 GGXVNDF_sample(const float3&amp; local_view_direction, float alpha_x, float alpha_y, Xorshift32Generator&amp; random_number_generator)
{
    float r1 = random_number_generator();
    float r2 = random_number_generator();

    float3 Vh = normalize(float3{ alpha_x * local_view_direction.x, alpha_y * local_view_direction.y, local_view_direction.z });

    float lensq = Vh.x * Vh.x + Vh.y * Vh.y;
    float3 T1 = lensq &gt; 0.0f ? float3{-Vh.y, Vh.x, 0} / sqrt(lensq) : float3{ 1.0f, 0.0f, 0.0f };
    float3 T2 = cross(Vh, T1);

    float r = sqrt(r1);
    float phi = 2.0f * M_PI * r2;
    float t1 = r * cos(phi);
    float t2 = r * sin(phi);
    float s = 0.5f * (1.0f + Vh.z);
    t2 = (1.0f - s) * sqrt(1.0f - t1 * t1) + s * t2;

    float3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0f, 1.0f - t1 * t1 - t2 * t2)) * Vh;

    return normalize(float3{alpha_x * Nh.x, alpha_y * Nh.y, max(0.0f, Nh.z)});
}
</code></pre>
<p>I tried copy/pasting the implementation from PBRT or Blender Cycles to validate my implementation but they produce the same kind of results.</p>
<ol>
<li>Either this is expected: why and what be can done to be more efficient?</li>
<li>Or my issue is outside of the sampling routine: should I immediately return a black color for the current pixel if a direction is sampled below the hemisphere?</li>
</ol>

CC BY-SA 4.0
2024-07-15T11:22:01.863Z
Is this image rendered with only 1 spp?
Enigmatisms
CC BY-SA 4.0
2024-07-15T13:19:40.613Z
Yes @Enigmatisms.
Tom Clabault
CC BY-SA 4.0
2024-07-16T06:18:14.607Z
I wonder why you will have any black pixel, since you are rendering the image with 0 bounces so basically it should just compute the direct illumination. Unless your code solely depends on BSDF sampling, you should never get black pixels since emitter sampling does not require you to sample the half vector (it is deterministic, given the emitter point and your incident direction from camera). Yet you are using MIS, is that correct? So once there is a valid emitter sample, this pixel should not be black.
Enigmatisms
CC BY-SA 4.0
2024-07-16T09:00:09.007Z
@Enigmatisms My direct illumination is correct and uses MIS, yes. The issue arises when I sample the bounce direction from the BSDF. This sampled bounce direction is invalid and so I terminate the pixel. In this case where there's no bounce, I could just ignore it and don't terminate the pixel. But what if I want to add 1 more bounce?
Tom Clabault
CC BY-SA 4.0
2024-07-16T09:36:03.013Z
Actually, I guess that if there's one more bounce, I should still have my direct lighting contributing. It's just that because the bounce direction is invalid, I won't have indirect lighting at that point.

Now the question is: is it expected to have that many bad samples from the VNDF sampling?
Tom Clabault
CC BY-SA 4.0
2024-07-16T11:05:40.147Z
I mean, if you actually use VNDF sampling, it will be expected to have bad samples. This can be understood easily: draw a 2D ray reflection illustration, and it's easy to see that the valid range of sampled normal is just the half vector of the incident direction and the surfaces (two tangential vectors). An extreme case is that if the incident direction is nearly perpendicular to the geometric normal, you will only have half of the hemisphere valid for sampled normals.
Enigmatisms
CC BY-SA 4.0
2024-07-16T11:06:18.727Z
Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/154146/discussion-between-enigmatisms-and-tom-clabault).
Enigmatisms
<pathtracing><sampling><importance-sampling><microfacet><ggx>
Lots of bad samples (below the hemisphere) when sampling the GGX VNDF
Tom Clabault
# Lots of bad samples (below the hemisphere) when sampling the GGX VNDF

(Asked by: username_0 on 2024-07-15)

I've noticed that my implementation of GGX VNDF sampling produces quite a lot of bad samples (below the hemisphere). When such a sample occurs, I terminate the ray, leading to an immediate black pixel.

[![GGX VNDF - Lots of bad samples on the backwall](https://i.sstatic.net/oliatbA4.png)](https://i.sstatic.net/oliatbA4.png)

All the black pixels on the backwall are due solely to the metallic lobe (which uses GGX VNDF sampling) of my BRDF sampling a direction below the normal. That's maybe 1/3 or 1/4 of the samples thrown away. This seems to be quite a waste in terms of efficiency.

The roughness of the wall in the back is 0.9. Screenshot rendered with 0 bounce and 1 SPP.

Here's my implementation if needed:

```
float3 GGXVNDF_sample(const float3& local_view_direction, float alpha_x, float alpha_y, Xorshift32Generator& random_number_generator)
{
    float r1 = random_number_generator();
    float r2 = random_number_generator();

    float3 Vh = normalize(float3{ alpha_x * local_view_direction.x, alpha_y * local_view_direction.y, local_view_direction.z });

    float lensq = Vh.x * Vh.x + Vh.y * Vh.y;
    float3 T1 = lensq > 0.0f ? float3{-Vh.y, Vh.x, 0} / sqrt(lensq) : float3{ 1.0f, 0.0f, 0.0f };
    float3 T2 = cross(Vh, T1);

    float r = sqrt(r1);
    float phi = 2.0f * M_PI * r2;
    float t1 = r * cos(phi);
    float t2 = r * sin(phi);
    float s = 0.5f * (1.0f + Vh.z);
    t2 = (1.0f - s) * sqrt(1.0f - t1 * t1) + s * t2;

    float3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0f, 1.0f - t1 * t1 - t2 * t2)) * Vh;

    return normalize(float3{alpha_x * Nh.x, alpha_y * Nh.y, max(0.0f, Nh.z)});
}

```

I tried copy/pasting the implementation from PBRT or Blender Cycles to validate my implementation but they produce the same kind of results.

1. Either this is expected: why and what be can done to be more efficient?
2. Or my issue is outside of the sampling routine: should I immediately return a black color for the current pixel if a direction is sampled below the hemisphere?

**Tags:** pathtracing, sampling, importance-sampling, microfacet, ggx

> **Comments:**
> **username_1**: Is this image rendered with only 1 spp?  
> **username_0**: Yes @username_1.  
> **username_1**: I wonder why you will have any black pixel, since you are rendering the image with 0 bounces so basically it should just compute the direct illumination. Unless your code solely depends on BSDF sampling, you should never get black pixels since emitter sampling does not require you to sample the half vector (it is deterministic, given the emitter point and your incident direction from camera). Yet you are using MIS, is that correct? So once there is a valid emitter sample, this pixel should not be black.  
> **username_0**: @username_1 My direct illumination is correct and uses MIS, yes. The issue arises when I sample the bounce direction from the BSDF. This sampled bounce direction is invalid and so I terminate the pixel. In this case where there's no bounce, I could just ignore it and don't terminate the pixel. But what if I want to add 1 more bounce?  
> **username_0**: Actually, I guess that if there's one more bounce, I should still have my direct lighting contributing. It's just that because the bounce direction is invalid, I won't have indirect lighting at that point.  Now the question is: is it expected to have that many bad samples from the VNDF sampling?  
> **username_1**: I mean, if you actually use VNDF sampling, it will be expected to have bad samples. This can be understood easily: draw a 2D ray reflection illustration, and it's easy to see that the valid range of sampled normal is just the half vector of the incident direction and the surfaces (two tangential vectors). An extreme case is that if the incident direction is nearly perpendicular to the geometric normal, you will only have half of the hemisphere valid for sampled normals.  
> **username_1**: Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/154146/discussion-between-username_1-and-username_0).  

---

## 1 answers

### Answer by username_0 on 2024-07-18. Score: 3 | Accepted answer

After some [discusson](https://chat.stackexchange.com/rooms/154146/discussion-between-username_1-and-username_0) with username_1, it turns out that:

1. I had an error in my renderer
2. Bad samples are expected from VNDF sampling.

### The error in my renderer

I'm using VNDF sampling to sample a normal about which to reflect the ray direction. If the sampled normal yields a reflected direction that is below the surface normal, I terminate the pixel.

The error here is that I am terminating the pixel before taking direct lighting into account (even though direct lighting yields a positive contribution). This means that I'm getting a black pixel where at least direct lighting should have contributed.

### Bad samples are expected

[![Perfectly specular VNDF valid normals](https://i.sstatic.net/o5ZsnoA4.png)](https://i.sstatic.net/o5ZsnoA4.png)
Illustration of a perfectly specular reflection case by username_1.

For a given incoming direction $\omega\_i$ and a surface normal $n$, not all microfacet normals $\omega\_m$ produced lead to a reflected direction that is above the hemisphere.

In the illustration, only microfacet normals $\omega\_m$ in between the two green $\omega\_m$ will lead to a reflected direction that is above the hemisphere.

Normals sampled in the red region (which can be sampled by VNDF sampling) will yield a reflection direction which is below the hemisphere.

### Bad samples must be discarded

These samples must be discarded and rejection sampling (for example) cannot be used here, otherwise this leads to bias (derivation by username_1):

[![VNDF rejection sampling bias derivation](https://i.sstatic.net/UI0xO1ED.png)](https://i.sstatic.net/UI0xO1ED.png)


---

