Cranberry Lair

Walking the programming parameter space

Where is the cosine? Lambertian Reflectors and Rendering — March 14, 2021

Where is the cosine? Lambertian Reflectors and Rendering

For quite some time, I’ve been plagued by a question about Lambertian reflectors and sampling them in a renderer. The issue arises from the definition of a lambertian reflector. You might be familiar with the formulation of the Lambertian BRDF of \frac{Albedo}{pi}, this formulation is simple and constant across viewing direction and illumination direction. However, there is actually an implicit cosine factor based on viewing direction involved.

Lambertian surfaces emit less light as the viewing angle from the surface’s normal increases. The factor is actually cos(\theta_o) where \theta_o is the angle from our viewing direction to our normal. [1]

My question was, why don’t we apply this cosine factor when sampling our surfaces?

After all, we’re asking for how much light is being emitted from a surface at a given point.

If we wanted this answer, shouldn’t we also multiply it by the cosine of our viewing angle?

Shouldn’t it be \frac{Albedo}{\pi} * cos(\theta_o) * L_i * cos(\theta_i) instead of \frac{Albedo}{\pi} * L_i * cos(\theta_i)?

The problem lies in the mental model I’ve presented.

When rendering, we’re not simply asking how much light is being emitted at a given point.

We’re asking how much light is being emitted from a given area. This area, is the projection of our pixel onto our surface A_p.

If we imagine our rendering problem as a 2 dimensional problem – where our image plane is represented by a line segment and a pixel is represented as a segment of our line.

What we’re asking when rendering our surface, is not the light being emitted from a single sample point. But actually the light being emitted from the surface area of A_p.

EmittedLight = L_o * A_p Where L_o represents the amount of light being emitted in a particular direction per unit area.

As a result, if we do a bit of trigonometry, we can see that A_p = \frac{A}{cos(\theta_o)}.

And we also know that our Lambertian reflector emits light as L_o = \frac{Albedo}{\pi} * cos(\theta_o) * L_i * cos(\theta_i)

If we place these values in our equation for emitted light we get:

EmittedLight = L_o * A_p

EmittedLight = \frac{Albedo}{\pi} * cos(\theta_o) * L_i * cos(\theta_i) * \frac{A}{cos(\theta_o)}

EmittedLight = \frac{Albedo}{\pi} * L_i * cos(\theta_i) * A

Which cancels out our cosine terms! (A is implicit when rendering a pixel grid)

This led me to some interesting insights/questions.

  • The value represented by a pixel can be seen as radiant intensity.
  • The value calculated by our shader can be seen as radiant intensity. (If it was radiance, we would need to introduce the cosine term again since we would not be multiplying by projected area)
  • We can think of the pixel grid as a big integration of our image plane and thinking of the pixels as little squares can be a valuable tool.
  • Do all BRDFs have an implicit cosine term? This leads me to think that is the case.

That’s it for today! Thanks for popping by, hopefully you found this as insightful as I have.

[1] https://en.wikipedia.org/wiki/Lambert%27s_cosine_law

Derivation – Importance Sampling The Cosine Lobe — June 7, 2020

Derivation – Importance Sampling The Cosine Lobe

Introduction

I’ve recently been diving into the world of importance sampling and I decided to share my derivation for importance sampling the cosine lobe.

Shade

When we’re shading a point in path tracing, we typically shoot a ray from our surface in a uniformity random direction contained on a hemisphere centered about our normal. This has the downside of introducing quite a bit of variance into our renders.

Imagine that we have a very bright light that only occupies a very small projected area from our shaded point.

IMG_2799

We would be very likely to miss this light with most of our samples and our render could turn out much darker than we would expect.

This is where importance sampling comes in.

If you imagine that your illumination is a polar function

IMG_2800

If we were to sample a random variable with a distribution that matches this function, we would be much more likely to hit the important points of our function. (Hence importance sampling)

I won’t dive deeply into this topic, as there are a variety of excellent resources detailing this topic. [1]

The essence of it however, is that you want to find a Probability Density Function (PDF) that matches the shape of your illumination function. Once you’ve define this PDF, you can sample it using the Cumulative Density Function (CDF).

Derivation

Since our cosine lobe illumination will look like this:

IMG_2801

We will use this as the basis to derive our distribution since we’re most likely to get the most light from directions arriving parallel to our normal.

Thankfully, our cosine lobe has an analytical formula that we can use as our PDF.

PDF(\omega) = C*cos(\theta) (1)

Our PDF must integrate to 1, we integrate the PDF across our hemisphere

\int_{\Omega}PDF(\omega)d\omega

\int_0^{2\pi}\int_0^{\frac{\pi}{2}}PDF(\omega)sin\theta d\theta d\phi

Plug in (1)

\int_0^{2\pi}\int_0^{\frac{\pi}{2}}C*cos\theta sin\theta d\theta d\phi

C*\int_0^{2\pi}\int_0^{\frac{\pi}{2}}cos\theta sin\theta d\theta d\phi

\int cos\theta sin\theta d\theta = -\frac{1}{4}cos2\theta

\int_0^{\frac{\pi}{2}}cos\theta sin\theta d\theta

-\frac{1}{4}cos\pi+ \frac{1}{4}cos0

\frac{1}{4}+\frac{1}{4}

\int_0^{\frac{\pi}{2}}cos\theta sin\theta d\theta=\frac{1}{2} (2)

Plug in (2)

C*\int_0^{2\pi}\frac{1}{2} d\phi

C*\frac{1}{2}*2\pi

C*\int_0^{2\pi}\int_0^{\frac{\pi}{2}}cos\theta sin\theta d\theta d\phi=C*\pi (3)

Since our PDF has to integrate to 1,

\int_0^{2\pi}\int_0^{\frac{\pi}{2}}PDF(\omega)sin\theta d\theta d\phi = 1

Plug in (3),

C*\pi=1

C=\frac{1}{pi} (4)

Finally, plug in (4) into our PDF,

PDF(\omega) = \frac{cos(\theta)}{\pi} (5)

Now that we have our PDF, we can calculate our PDF in terms of \theta and \phi.

PDF(\theta,\phi)d\theta d\phi = PDF(\omega)d\omega

PDF(\theta,\phi)d\theta d\phi = PDF(\omega)sin\theta d\theta d\phi

PDF(\theta,\phi)=PDF(\omega)sin\theta

PDF(\theta,\phi)=\frac{cos\theta sin\theta}{\pi} (6)

Now we integrate with respect to \phi to get PDF(\theta)

\int_0^{2\pi}\frac{cos\theta sin\theta}{\pi}d\phi = 2cos\theta sin\theta

PDF(\theta)=2cos\theta sin\theta

And then to get PDF(\phi),

\frac{PDF(\theta,\phi)}{PDF(\theta)}=PDF(\phi)

\frac{cos\theta sin\theta}{2cos\theta sin\theta \pi}=\frac{1}{2\pi}

PDF(\phi)=\frac{1}{2\phi}

Now we want to calculate the CDF of each function,

CDF(\theta)=\int_0^\theta PDF(\theta) d\theta

CDF(\theta)=\int_0^\theta 2cos(\theta)sin(\theta) d\theta

CDF(\theta)=\int_0^\theta sin(2\theta) d\theta

CDF(\theta)=\frac{1}{2}-\frac{cos(2\theta)}{2}

CDF(\phi)=\int_0^\phi PDF(\phi) d\phi

CDF(\phi)=\int_0^\phi\frac{1}{2\pi} d\phi

CDF(\phi)=\frac{\phi}{2\pi}

Now we want to invert our CDF to sample it using our random variable y,

y=CDF(\theta)

y=\frac{1}{2}-\frac{cos(2\theta)}{2}

\frac{1}{2}-y=\frac{cos(2\theta)}{2}

1-2y=cos(2\theta)

\frac{cos^{-1}(1-2y)}{2}=\theta (7)

For \phi,

y=CDF(\phi)

y=\frac{\phi}{2\pi}

y*2\pi = \phi (8)

Now we have our CDFs and our PDFs, we can finally calculate our direction.

In pseudo code you can simply do:

\theta=\frac{cos^{-1}(1-2rand01())}{2}

\phi=rand01()*2\pi

With these directions, you can now sample your scene:

\frac{SampleScene(SphericalTo3D(\theta, \phi))}{PDF(\omega)}

Plug in (5)

\frac{SampleScene(SphericalTo3D(\theta, \phi))\pi}{cos\theta}

Conclusion

That’s it! These formulas will sample the hemisphere where it is receiving more light defined by the cosine lobe. The results are pretty awesome.

Bibliography

[1] https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/global-illumination-path-tracing-practical-implementation

Attempting Triangular Area Lights — May 16, 2020
A Quest Towards Intuition – Why is depth interpolated as 1/z? — August 27, 2019

A Quest Towards Intuition – Why is depth interpolated as 1/z?

Premise

I have recently been attempting to improve my understanding of perspective projection. This included a variety of topics such as deriving a perspective projection matrix and understanding interpolation of vertex shader outputs in perspective. However, one topic that evaded me, was the surprising result that depth is interpolated as 1/z instead of z.

Continue reading