<p>I think, the problem lies in <strong>dividing the NEE radiance by PDF (inverse area) twice</strong>. Since NEE typically uniformly samples an emitter and then samples a point (via area product measure) on that emitter (if it is not infinitesimal), so first we will have:
<span class="math-container">$$
L_e(x_i) / \text{PDF}(x_i)
$$</span>
as the exitant radiance at sampled emitter point <span class="math-container">$x_i$</span>, and in your case, PDF will be <code>1 / area()</code> and <span class="math-container">$L_e$</span> is just a constant vector <code>this-&gt;color</code>. Since this sampling is done via area product measure, the measure conversion (to solid angle measure) should be performed, which yields:
<span class="math-container">$$
\frac{L_e(x_i)\cos(\theta_e)}{\Vert x_i - x_e \Vert^2\text{PDF}(x_i)}
$$</span>
So, <code>this-&gt;color * solidAngle</code> should be the formula above, which is correct. However, in the code that calls NEE method, you have:</p>
<pre class="lang-cpp prettyprint-override"><code> glm::dot(hr.normal, wi) * material-&gt;brdf(hr, wi) * li / pdf;
</code></pre>
<p><code>pdf</code> here is, again, <span class="math-container">$\text{PDF}(x_i)$</span>, which has already been in the denominator. The <code>pdf</code> for the denominator here, should be the pdf of sampling the emitter (say, if you have multiple glowing spheres in the scene, you want to choose one of them for this NEE).</p>
<p>I compared the rendering results in my own renderer:</p>
<div class="s-table-container"><table class="s-table">
<thead>
<tr>
<th style="text-align: center;">(incorrect) divide PDF twice</th>
<th style="text-align: center;">(correct) divide PDF once</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;"><a href="https://i.sstatic.net/JpfXZuj2.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/JpfXZuj2.png" alt="enter image description here" /></a></td>
<td style="text-align: center;"><a href="https://i.sstatic.net/itUqXDCj.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/itUqXDCj.png" alt="enter image description here" /></a></td>
</tr>
</tbody>
</table></div>
<p>Image on the left is noticably brighter.</p>

CC BY-SA 4.0
2024-04-24T20:46:27.143Z
I understand now!
Thank you so much for your explanation, it was very very very clear.
Syrinxos
CC BY-SA 4.0
2024-04-23T15:21:18.100Z
2024-04-23T15:21:18.100Z
Enigmatisms
<p>it's me again! :D</p>
<p>I have finally implemented area lights, but without modifying the emission value of the material, this is what it looks like with <a href="https://imgur.com/6qoe70a" rel="nofollow noreferrer">indirect light only</a>, this is what it looks like with <a href="https://imgur.com/S80yEaQ" rel="nofollow noreferrer">direct only</a> and this is both <a href="https://imgur.com/3KsI8Af" rel="nofollow noreferrer">direct+indirect</a>!</p>
<p>Clearly there is something wrong going on with the direct light sampling.</p>
<p>This is the function for one light:</p>
<pre><code>    float pdf, dist;
    glm::vec3 wi;
    Ray visibilityRay;
    auto li = light-&gt;li(sampler, hr, visibilityRay, wi, pdf, dist);
    if (scene-&gt;visibilityCheck(visibilityRay, EPS, dist - EPS, light))
    {
        return glm::dot(hr.normal, wi) * material-&gt;brdf(hr, wi) * li / pdf;
    }
    return BLACK;
</code></pre>
<p>In case of the area light, <em>li</em> is the following:</p>
<pre><code>    glm::vec3 samplePoint, sampleNormal;
    shape-&gt;sample(sampler, samplePoint, sampleNormal, pdf);
    wi = (samplePoint - hr.point);
    dist = glm::length(wi);
    wi = glm::normalize(wi);
    vRay.origin = hr.point + EPS * wi;
    vRay.direction = wi;
    float cosT = glm::dot(sampleNormal, -wi);
    auto solidAngle = (cosT * this-&gt;area()) / (dist * dist);
    if(cosT &gt; 0.0f) {
        return this-&gt;color * solidAngle;
    } else {
        return BLACK;
    }
</code></pre>
<p>And I think the code to sample the spheres is correct</p>
<pre><code>    glm::vec3 sampleUniformSphere(std::shared_ptr&lt;Sampler&gt; &amp;sampler)
    {
        float z = 1 - 2 * sampler-&gt;getSample();
        float r = sqrt(std::max(0.0f, 1.0f - z * z));
        float phi = 2 * PI * sampler-&gt;getSample();
        return glm::vec3(
            r * cos(phi),
            r * sin(phi),
            z);
    }

    void Sphere::sample(std::shared_ptr&lt;Sampler&gt; &amp;sampler, glm::vec3 &amp;point, glm::vec3 &amp;normal, float &amp;pdf) const
    {   
        glm::vec3 local = sampleUniformSphere(sampler);
        normal = glm::normalize(local);
        point = m_obj2World.transformPoint(radius * local);
        pdf = 1.0f / area();
    }
</code></pre>
<p>It looks like either the solid angle or the distance attenuation aren't working correctly. <a href="https://imgur.com/a/sWQTszs" rel="nofollow noreferrer">This is a Mitsuba3 render</a> with roughly the same values.</p>
<p>Thanks!</p>

<raytracing><pathtracing><geometry><importance-sampling><light>
Direct Light Sampling produces way too bright images compared to naive diffuse bounces only
Syrinxos
# Direct Light Sampling produces way too bright images compared to naive diffuse bounces only

(Asked by: username_0 on 2024-04-22)

it's me again! :D

I have finally implemented area lights, but without modifying the emission value of the material, this is what it looks like with [indirect light only](https://imgur.com/6qoe70a), this is what it looks like with [direct only](https://imgur.com/S80yEaQ) and this is both [direct+indirect](https://imgur.com/3KsI8Af)!

Clearly there is something wrong going on with the direct light sampling.

This is the function for one light:

```
    float pdf, dist;
    glm::vec3 wi;
    Ray visibilityRay;
    auto li = light->li(sampler, hr, visibilityRay, wi, pdf, dist);
    if (scene->visibilityCheck(visibilityRay, EPS, dist - EPS, light))
    {
        return glm::dot(hr.normal, wi) * material->brdf(hr, wi) * li / pdf;
    }
    return BLACK;

```

In case of the area light, *li* is the following:

```
    glm::vec3 samplePoint, sampleNormal;
    shape->sample(sampler, samplePoint, sampleNormal, pdf);
    wi = (samplePoint - hr.point);
    dist = glm::length(wi);
    wi = glm::normalize(wi);
    vRay.origin = hr.point + EPS * wi;
    vRay.direction = wi;
    float cosT = glm::dot(sampleNormal, -wi);
    auto solidAngle = (cosT * this->area()) / (dist * dist);
    if(cosT > 0.0f) {
        return this->color * solidAngle;
    } else {
        return BLACK;
    }

```

And I think the code to sample the spheres is correct

```
    glm::vec3 sampleUniformSphere(std::shared_ptr<Sampler> &sampler)
    {
        float z = 1 - 2 * sampler->getSample();
        float r = sqrt(std::max(0.0f, 1.0f - z * z));
        float phi = 2 * PI * sampler->getSample();
        return glm::vec3(
            r * cos(phi),
            r * sin(phi),
            z);
    }

    void Sphere::sample(std::shared_ptr<Sampler> &sampler, glm::vec3 &point, glm::vec3 &normal, float &pdf) const
    {   
        glm::vec3 local = sampleUniformSphere(sampler);
        normal = glm::normalize(local);
        point = m_obj2World.transformPoint(radius * local);
        pdf = 1.0f / area();
    }

```

It looks like either the solid angle or the distance attenuation aren't working correctly. [This is a Mitsuba3 render](https://imgur.com/a/sWQTszs) with roughly the same values.

Thanks!

**Tags:** raytracing, pathtracing, geometry, importance-sampling, light


---

## 1 answers

### Answer by username_1 on 2024-04-23. Score: 3 | Accepted answer

I think, the problem lies in **dividing the NEE radiance by PDF (inverse area) twice**. Since NEE typically uniformly samples an emitter and then samples a point (via area product measure) on that emitter (if it is not infinitesimal), so first we will have:
$$
L\_e(x\_i) / \text{PDF}(x\_i)
$$
as the exitant radiance at sampled emitter point $x\_i$, and in your case, PDF will be `1 / area()` and $L\_e$ is just a constant vector `this->color`. Since this sampling is done via area product measure, the measure conversion (to solid angle measure) should be performed, which yields:
$$
\frac{L\_e(x\_i)\cos(\theta\_e)}{\Vert x\_i - x\_e \Vert^2\text{PDF}(x\_i)}
$$
So, `this->color * solidAngle` should be the formula above, which is correct. However, in the code that calls NEE method, you have:

```cpp
 glm::dot(hr.normal, wi) * material->brdf(hr, wi) * li / pdf;

```

`pdf` here is, again, $\text{PDF}(x\_i)$, which has already been in the denominator. The `pdf` for the denominator here, should be the pdf of sampling the emitter (say, if you have multiple glowing spheres in the scene, you want to choose one of them for this NEE).

I compared the rendering results in my own renderer:

| (incorrect) divide PDF twice | (correct) divide PDF once |
| --- | --- |
| [enter image description here](https://i.sstatic.net/JpfXZuj2.png) | [enter image description here](https://i.sstatic.net/itUqXDCj.png) |

Image on the left is noticably brighter.

> **Comments:**
> **username_0**: I understand now! Thank you so much for your explanation, it was very very very clear.  

---

