<p>What coordinate system did you use for intersection.normal and scattered_ray.direction? A typical way would be sampling the scattering direction in the local space and then transforming it into the world space. Thus the cosine theta will be computed inside the sampling function. I am not sure if it is the coordinate system that caused your issue.</p>

CC BY-SA 4.0
2022-06-21T11:03:11.720Z
2022-06-21T11:03:11.720Z
wenyo
<p>Your rendering without the cosine is probably wrong since you're using uniform sphere sampling and the cosine must be the there (unless you assume your brdfs contain a term 1/cos(theta)). Also do this:</p>
<pre><code>pdf = 1.0/(2.0*PI);
return cosine_theta * attenuation / pdf * ray_trace(scattered_ray, depth - 1);
</code></pre>
<p>and also for the non-cosine part:</p>
<pre><code>pdf = 1.0/(2.0*PI);
return attenuation/pdf * ray_trace(scattered_ray, depth - 1);
</code></pre>
<p>If you want to use the non-cosine estimator, then you will have to use cosine distributed sampling for it. It will look like this:</p>
<pre><code>Vec3 Lambertian::scatter_cosine_dist(Ray ray, Intersection intersection, Ray &amp;scattered_ray)
{
    double phi = random(0.0, 2.0 * PI);
    double sin_phi = sine(phi);
    double cos_phi = cosine(phi);

    double cos_theta = random(-1.0, 1.0);
    double sin_theta = sqrt(1 - cos_theta * cos_theta);

    Vec3 scattered_direction = Vec3(sin_phi * sin_theta, cos_phi * sin_theta, cos_theta).normalize();

    scattered_ray.origin = intersection.hit_point;
    scattered_ray.direction = (intersection.normal+scattered_direction).normalize();

    return this.albedo;
}
</code></pre>
<p>Then modify the non-cosine estimator's pdf too:</p>
<pre><code>pdf_wo_cos = 1.0/PI;
return attenuation/pdf_wo_cos * ray_trace(scattered_ray, depth - 1);
</code></pre>
<p>The pdf in this is actually cos(theta)/PI, but the cosine cancels with the cosine from the rendering equation. If you use the cosine distributed sampling for the estimator without the cosine, and use the uniform sampling for the estimator with the cosine, then you should get equivalent results up to noise (the cosine estimator ought to result in less noise).</p>

CC BY-SA 4.0
2022-05-19T11:19:37.727Z
@Rahn I don't think the bug is in the code that you posted, or at the very least I cannot find it there, it may be some part that you have not posted. What does your dot function look like? You also should implement what I suggested regarding the without cosine part.
lightxbulb
CC BY-SA 4.0
2022-05-19T12:58:45.040Z
update again. Code added (it's in Rust, but not so different from C++).
Rahn
CC BY-SA 4.0
2022-05-19T08:09:43.263Z
2022-05-19T08:09:43.263Z
lightxbulb
<p>I'm following <a href="https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html" rel="nofollow noreferrer">Ray Tracing The Rest of Your Life</a> to implement a ray tracer, but the explains on math (mainly pdf part) got me confused so I followed Rendering Equation to understand the math behind. As a result, I sample the scene with plain Monte Carlo method (derived from the Equation) and got some seemly correct result after removing the <code>cos(θ)</code> in Rendering Equation, but got an incorrectly dark image with <code>cos(θ)</code>. And worse, the rendering doesn't converge: as number of samples increase, it goes darker and darker.</p>
<p>from left to right: 500 samples without <code>cos(θ)</code>, 500 samples with <code>cos(θ)</code>, 100 samples with <code>cos(θ)</code>:
<a href="https://i.sstatic.net/v0OVp.jpg" rel="nofollow noreferrer"><img src="https://i.sstatic.net/v0OVp.jpg" alt="enter image description here" /></a></p>
<p>The code is not complicated. For every sampling, the camera send a ray into function <code>ray_trace(Ray ray, int depth)</code>, the function terminates when the ray hit a light or couldn't find one after bouncing enough time (<code>depth &lt;= 0</code>). When it hits something in the scene (the only material in the scene other than light is Lambertian material), a scattered ray is generated from material's <code>scatter(Ray ray, Intersection intersection, Ray &amp;scattered_ray)</code> function. In Lambertian's <code>scatter()</code> function, the scattered ray is generated by uniformly sampling around hemisphere. Then this scattered ray becomes the new ray in function <code>ray_trace</code>, thus <code>depth = depth-1</code>, and  start another round of ray tracing.</p>
<pre><code>{
    // initially, this ray is generated from a camera
    if (depth &lt;= 0)
    {
        // return black when no light found
        return Vec3(0.0, 0.0, 0.0);
    }

    auto intersection = scene.intersect(ray);

    if (!intersection.intersected)
    {
        // return black when hits nothing
        return Vec3(0.0, 0.0, 0.0);
    }

    if (intersection.hit_a_light)
    {
        // found a light
        return intersection.material.emit(...);
    }

    Ray scattered_ray;
    Vec3 attenuation = intersection.material.scatter(ray, intersection, &amp;scattered_ray);

    if (with_cosine_theta)
    {
        Vec3 cosine_theta = dot(intersection.normal.normalize(), scattered_ray.direction.normalize());
        return cosine_theta * attenuation * ray_trace(scattered_ray, depth - 1);
    }
    else
    {
        // without cosine(theta)
        return attenuation * ray_trace(scattered_ray, depth - 1);
    }
}

Vec3 Lambertian::scatter(Ray ray, Intersection intersection, Ray &amp;scattered_ray)
{
    double phi = random(0.0, 2.0 * PI);
    double sin_phi = sine(phi);
    double cos_phi = cosine(phi);

    double cos_theta = random(-1.0, 1.0);
    double sin_theta = sqrt(1 - cos_theta * cos_theta);

    Vec3 scattered_direction = Vec3(sin_phi * sin_theta, cos_phi * sin_theta, cos_theta);

    // generate random direction in a hemisphere
    if (dot(scattered_direction, intersection.normal) &lt; 0)
    {
        scattered_direction = -scattered_direction;
    }

    scattered_ray.origin = intersection.hit_point;
    scattered_ray.direction = scattered_direction;

    return this.albedo;
}
</code></pre>
<p>Can someone point out what makes my rendering wrong?</p>
<hr />
<p>update:</p>
<p>Following lightxbulb's idea, I tried both approaches (code at <a href="https://codeshare.io/bvR8ev" rel="nofollow noreferrer">https://codeshare.io/bvR8ev</a>):</p>
<ul>
<li>keep <code>cos(θ)</code> in <code>ray_trace()</code> and uniformly sample hemisphere in <code>scatter()</code> (<code>WITH_COSINE=true</code>, <code>UNIFORM_SAMPLE=true</code>)</li>
<li>remove <code>cos(θ)</code> in <code>ray_trace()</code> and sample hemisphere with <code>cos(θ)</code> importance in <code>scatter()</code> (<code>WITH_COSINE=false</code>, <code>UNIFORM_SAMPLE=false</code>)</li>
</ul>
<p>for the 1st approach I have (100/200/500 samples):
<a href="https://i.sstatic.net/iPG68.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/iPG68.png" alt="enter image description here" /></a></p>
<p>for the 2nd approach I have (100/200/500 samples):
<a href="https://i.sstatic.net/bOh3O.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/bOh3O.png" alt="enter image description here" /></a></p>
<p>left to right: 100 samples, 200 samples, 500 samples</p>

CC BY-SA 4.0
2022-05-19T04:16:14.307Z
`dot(intersection.normal, scattered_ray).normalize()` - dot should return a scalar and the scalar should not be normalized.
lightxbulb
CC BY-SA 4.0
2022-05-19T16:27:15.393Z
What is this: `if attenuation.r <= 0.0 && attenuation.g <= 0.0 && attenuation.b <= 0.0 {return attenuation;}`? Also I don't get why you're getting different results, the two estimators ought to converge to the same thing. What are the albedos of your surfaces? Are they in [0,1/PI]^3? If they aren't, then the scene is not energy conserving, which may explain the second case, but it doesn't explain the first.
lightxbulb
CC BY-SA 4.0
2022-05-19T17:06:39.973Z
If you don't want to change all your albedos you can just drop the PI in both pdfs.
lightxbulb
<raytracing><rendering><global-illumination>
Adding cos(θ) from rendering equation ruins the renderings
Rahn
# Adding cos(θ) from rendering equation ruins the renderings

(Asked by: username_0 on 2022-05-19)

I'm following [Ray Tracing The Rest of Your Life](https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html) to implement a ray tracer, but the explains on math (mainly pdf part) got me confused so I followed Rendering Equation to understand the math behind. As a result, I sample the scene with plain Monte Carlo method (derived from the Equation) and got some seemly correct result after removing the `cos(θ)` in Rendering Equation, but got an incorrectly dark image with `cos(θ)`. And worse, the rendering doesn't converge: as number of samples increase, it goes darker and darker.

from left to right: 500 samples without `cos(θ)`, 500 samples with `cos(θ)`, 100 samples with `cos(θ)`:
[![enter image description here](https://i.sstatic.net/v0OVp.jpg)](https://i.sstatic.net/v0OVp.jpg)

The code is not complicated. For every sampling, the camera send a ray into function `ray_trace(Ray ray, int depth)`, the function terminates when the ray hit a light or couldn't find one after bouncing enough time (`depth <= 0`). When it hits something in the scene (the only material in the scene other than light is Lambertian material), a scattered ray is generated from material's `scatter(Ray ray, Intersection intersection, Ray &scattered_ray)` function. In Lambertian's `scatter()` function, the scattered ray is generated by uniformly sampling around hemisphere. Then this scattered ray becomes the new ray in function `ray_trace`, thus `depth = depth-1`, and start another round of ray tracing.

```
{
    // initially, this ray is generated from a camera
    if (depth <= 0)
    {
        // return black when no light found
        return Vec3(0.0, 0.0, 0.0);
    }

    auto intersection = scene.intersect(ray);

    if (!intersection.intersected)
    {
        // return black when hits nothing
        return Vec3(0.0, 0.0, 0.0);
    }

    if (intersection.hit_a_light)
    {
        // found a light
        return intersection.material.emit(...);
    }

    Ray scattered_ray;
    Vec3 attenuation = intersection.material.scatter(ray, intersection, &scattered_ray);

    if (with_cosine_theta)
    {
        Vec3 cosine_theta = dot(intersection.normal.normalize(), scattered_ray.direction.normalize());
        return cosine_theta * attenuation * ray_trace(scattered_ray, depth - 1);
    }
    else
    {
        // without cosine(theta)
        return attenuation * ray_trace(scattered_ray, depth - 1);
    }
}

Vec3 Lambertian::scatter(Ray ray, Intersection intersection, Ray &scattered_ray)
{
    double phi = random(0.0, 2.0 * PI);
    double sin_phi = sine(phi);
    double cos_phi = cosine(phi);

    double cos_theta = random(-1.0, 1.0);
    double sin_theta = sqrt(1 - cos_theta * cos_theta);

    Vec3 scattered_direction = Vec3(sin_phi * sin_theta, cos_phi * sin_theta, cos_theta);

    // generate random direction in a hemisphere
    if (dot(scattered_direction, intersection.normal) < 0)
    {
        scattered_direction = -scattered_direction;
    }

    scattered_ray.origin = intersection.hit_point;
    scattered_ray.direction = scattered_direction;

    return this.albedo;
}

```

Can someone point out what makes my rendering wrong?

---

update:

Following username_1's idea, I tried both approaches (code at <https://codeshare.io/bvR8ev>):

* keep `cos(θ)` in `ray_trace()` and uniformly sample hemisphere in `scatter()` (`WITH_COSINE=true`, `UNIFORM_SAMPLE=true`)
* remove `cos(θ)` in `ray_trace()` and sample hemisphere with `cos(θ)` importance in `scatter()` (`WITH_COSINE=false`, `UNIFORM_SAMPLE=false`)

for the 1st approach I have (100/200/500 samples):
[![enter image description here](https://i.sstatic.net/iPG68.png)](https://i.sstatic.net/iPG68.png)

for the 2nd approach I have (100/200/500 samples):
[![enter image description here](https://i.sstatic.net/bOh3O.png)](https://i.sstatic.net/bOh3O.png)

left to right: 100 samples, 200 samples, 500 samples

**Tags:** raytracing, rendering, global-illumination

> **Comments:**
> **username_1**: `dot(intersection.normal, scattered_ray).normalize()` - dot should return a scalar and the scalar should not be normalized.  
> **username_1**: What is this: `if attenuation.r <= 0.0 && attenuation.g <= 0.0 && attenuation.b <= 0.0 {return attenuation;}`? Also I don't get why you're getting different results, the two estimators ought to converge to the same thing. What are the albedos of your surfaces? Are they in [0,1/PI]^3? If they aren't, then the scene is not energy conserving, which may explain the second case, but it doesn't explain the first.  
> **username_1**: If you don't want to change all your albedos you can just drop the PI in both pdfs.  

---

## 2 answers

### Answer by username_2 on 2022-06-21. Score: 1

What coordinate system did you use for intersection.normal and scattered\_ray.direction? A typical way would be sampling the scattering direction in the local space and then transforming it into the world space. Thus the cosine theta will be computed inside the sampling function. I am not sure if it is the coordinate system that caused your issue.


---

### Answer by username_1 on 2022-05-19. Score: 1

Your rendering without the cosine is probably wrong since you're using uniform sphere sampling and the cosine must be the there (unless you assume your brdfs contain a term 1/cos(theta)). Also do this:

```
pdf = 1.0/(2.0*PI);
return cosine_theta * attenuation / pdf * ray_trace(scattered_ray, depth - 1);

```

and also for the non-cosine part:

```
pdf = 1.0/(2.0*PI);
return attenuation/pdf * ray_trace(scattered_ray, depth - 1);

```

If you want to use the non-cosine estimator, then you will have to use cosine distributed sampling for it. It will look like this:

```
Vec3 Lambertian::scatter_cosine_dist(Ray ray, Intersection intersection, Ray &scattered_ray)
{
    double phi = random(0.0, 2.0 * PI);
    double sin_phi = sine(phi);
    double cos_phi = cosine(phi);

    double cos_theta = random(-1.0, 1.0);
    double sin_theta = sqrt(1 - cos_theta * cos_theta);

    Vec3 scattered_direction = Vec3(sin_phi * sin_theta, cos_phi * sin_theta, cos_theta).normalize();

    scattered_ray.origin = intersection.hit_point;
    scattered_ray.direction = (intersection.normal+scattered_direction).normalize();

    return this.albedo;
}

```

Then modify the non-cosine estimator's pdf too:

```
pdf_wo_cos = 1.0/PI;
return attenuation/pdf_wo_cos * ray_trace(scattered_ray, depth - 1);

```

The pdf in this is actually cos(theta)/PI, but the cosine cancels with the cosine from the rendering equation. If you use the cosine distributed sampling for the estimator without the cosine, and use the uniform sampling for the estimator with the cosine, then you should get equivalent results up to noise (the cosine estimator ought to result in less noise).

> **Comments:**
> **username_1**: @username_0 I don't think the bug is in the code that you posted, or at the very least I cannot find it there, it may be some part that you have not posted. What does your dot function look like? You also should implement what I suggested regarding the without cosine part.  
> **username_0**: update again. Code added (it's in Rust, but not so different from C++).  

---

