Assignment 1: Basic Direct Illumination

Due September 27th, 2017 at 11:59pm on myCourses
Worth 10%


Overview

In this assignment, you'll implement a simple rendering algorithm in order to get a better hang of the rendering architecture of the base code. You'll be implementing a simple lighting "integrator" but, in the examples here, no actual integration is taking place; in the future, more complex light transport algorithms will actually solve integrals (numerically), and so the integrator nomenclature becomes more appropriate.

Your shading algorithm will generate images with direct illumination from simple, idealized emitters. As such, parts of a scene that are not directly lit by the light sources (emitters) should appear completely dark. Moreover, since the emitters we ask you to implement in this assigment are based on simple, idealized models (as opposed to physically-realizable emitters), the shadows they will generate will be "hard": they will not have any penumbra. In other words, every point in the scene is either completely lit or completely shadowed.

For now, we will assume a scene comprising only perfectly white diffuse objects, i.e., $\rho = 1$.

Start by copying the files in your solution to Assignment 0. Overwrite those files provided to you in this assignment's code handout. Make sure to preserve the existing folder hierarchy and to use the new CMakeLists.txt.

You'll implement two new emitter types in Task 1, before moving on to implementing your shading algorithm (which will use these emitters) in Task 2.


Task 1: Delta lights (50 points)

Before we can write a shading algorithm (in Task 2) that actually computes any outgoing radiance values (i.e., pixel colors), we'll need some scaffolding in order to support scenes with different types of light sources. The emitter abstraction allows us to define different light sources.

You'll implement two such emitters, point and directional lights, before moving on to coding shading algorithms that use these emitters to generate images.

Point lights (25 points)

The scene file scenes/hw1/Tests/sphere-point.xml instantiates a (currently nonexistent) integrator/shading algorithm named simple. In Task 2, you'll implement its logic. This scene file also relies on a (currently incomplete implementation of an) emitter of type point.

The first part of this task requires that you implement a new class PointLight, derived from Emitter, to simulate an isotropic point light source's emission: one that emits the same radiance in all directions.

This new emitter has two class fields: its 3D position $\mathbf{p}$ (m_position in the PointLight class) and its radiant intensity $I$ (m_radiance in the Emitter class), as shown in the code snippet below. Both properties are retrieved directly in the class constructor via PropertyList.

<!-- Use a basic direct illumination integrator -->
<integrator type="simple"/>

<!-- Illuminate using a point light -->
<emitter type="point">
        <point name="position" value="0.2, 0.2, 0"/>
        <color name="radiance" value="0.1, 0.1, 0.1"/>
    </emitter>

Once you have established the basic structure for your emitter, implement PointLight::eval() which takes an EmitterQueryRecord &eqr, sets its fields (e.g., the outgoing ray direction $\omega_i$ from the light to the shading point), and returns the incident radiance from the light to the shading point $\mathbf{x}$. These radiance values depend on both the radiant intensity $I$ and the squared distance between $\mathbf{x}$ and the light's position $\mathbf{p}$ as

$$L_o(\mathbf{p}, \mathbf{p} \rightarrow \mathbf{x}) = L_o(\mathbf{p}, -\omega_i) = \frac{I}{\|\mathbf{x} - \mathbf{p}\|^2}~.$$

Emitter::eval() returns incident illumination from the light to a shading point. Keep in mind, however, that outgoing radiance from the light is equal to incident radiance at the shade point: $$L_o(\mathbf{p}, -\omega_i) = L_i(\mathbf{x}, \omega_i)~.$$ For consistency across different emitter types, the PointLight class' m_radiance field will exceptionally represent a point light's radiant intensity (and not its uniform outgoing radiance). Do not forget properly convert the radiant intensity to outgoing radiance in PointLight::eval(), as detailed above.

Directional lights (25 points)

The goal of this second task is to implement a distant directional light source. This type of light can be interpreted as a point light “at infinity” since, as a point light moves further and further away from a shading point, the effect of the direction towards the light dominates over its actual position (if we ignore the reciprocal distance factor). For instance, shading due to the sun (at any point on Earth) can be approximated using a directional light source.

Implement a DirectionalLight class derived from Emitter to simulate this delta emitter. The outgoing radiance from the light is constant and depends only on the direction towards the light. In fact, we no longer specify an actual position for the light, as it lies at infinity: we use a direction $\omega$ (m_direction in the DirectionalLight class) and a radiance $L_o(\omega)$ exitant from this direction, to fully describe the light source. Have a look at the sphere-directional.xml XML file for an example scene with a directional emitter definition:

<!-- Illuminate using a directional/distant light -->
<emitter type="directional">
    <vector name="direction" value="1.0,1.0,0"/>
    <color name="radiance" value="2,2,2"/>
</emitter>

The radiance emitted by a directional source is non-zero only in the direction towards (or from, depending on your point of reference) the source.


Task 2: Basic direct illumination integrator (50 points)

To test your new lights, you will implement two simple shading algorithms, one that computes unshadowed shading and another that computes images with (hard) shadows; in both cases, you will support shading from both point and directional light sources, but using only diffuse BRDFs.

The integrator abstraction encapsulates shading algorithms: the complex shading algorithms we'll see later on the course will solve integral equations, such as the reflection equation (a.k.a. the direct illumination equation) below, which motivates the naming of this abstraction layer:

$$L_r(\mathbf{x},\omega_r) = \int_{\mathcal{H}^2} L_i(\mathbf{x}, \omega_i) \, f_r(\mathbf{x}, \omega_i, \omega_r) \, \cos \theta_i \, \mathrm{d}\omega_i.$$

The shading algorithms you'll implement below, whose logic will be contained primarily in the SimpleIntegrator::Li() routine of the SimpleIntegrator class, will compute a much simpler reflection equation that doesn't actually require any explicit integration: when we substitute the delta lighting models into the $L_i$ term in the reflection equation, and when we assume that all our objects have diffuse BRDFs, the shading equations simplify significantly (as detailed below).

Note that this integrator share a similar structure as the normal integrator from Assignment 0.

No shadows (20 points)

Start by implementing a direct illumination integrator that does not include any occlusion queries (i.e., for shadows) between the shading point and the light. As with your normal integrator, begin by intersecting your scene with the camera ray provided (by the rendering system) to your Li routine. If you find an intersection $\mathbf{x}$, retrieve the intersected shape's BRDF $f_r$ at the hit/shading point, evaluate the incident radiance $L_i$ at that point, and multiply by the cosine term to compute the outgoing radiance contribution: $$L_r(\mathbf{x},\omega_r) = L_i(\mathbf{x},\omega_l) \, f_r(\mathbf{x},\omega_r,\omega_l) V(\mathbf{x}, \omega_l) |\cos \theta_l|,$$

where $omega_l$ is the direction from the shading point towards the light. Here, the incident radiance $L_i$ will depend on the underlying light model that's called using Emitter::eval(), but should be transparent to the implementation of the integrator (as it'll be defined in the scene file and instantiated in C++ accordingly).

Implement the simple integrator according to this specification and render the scenes scenes/hw1/Tests/sphere-point.xml and scenes/hw1/Tests/sphere-directional.xml. Below are reference images for point and directional lights.

Point light without shadows
Directional light without shadows

Hard shadows (30 points)

To compute hard shadows, modify your integrator so that it computes the visibility function $V(\mathbf{x}, \omega_l) = V(\mathbf{x} \leftrightarrow \mathbf{p})$ defined as $$V(\mathbf{x} \leftrightarrow \mathbf{p}) := \begin{cases} 1, &\text{$\mathbf{x}$ and $\mathbf{p}$ are mutually visible} \\ 0, &\text{otherwise} \end{cases}$$

which can be implemented using a shadow ray query. As an example, for point lights and diffuse BRDFs, the outgoing radiance is computed as

$$L_r(\mathbf{x},\omega_r) = I \, \frac{\rho_x}{\pi}\, V(\mathbf{x} \leftrightarrow \mathbf{p}) \frac{|\cos \theta_l|}{\|\mathbf{x} - \mathbf{p}\|^2}$$

and you will have to derive a similar equation for a directional light source. Note that, when implementing your visibility logic, you need to design your approach to be robust to both point and directional sources.

Intersecting a shadow ray against the scene is generally cheaper than tracing an arbitrary ray, since it suffices to check whether any intersection exists (rather than having to find the closest one). Make sure to compare your results with the reference images below.

Point light with hard shadows
Directional light with hard shadows

What to submit

Finished? Submit your final renders scenes/hw1/Final/finalhw1-point.xml and scenes/hw1/Final/finalhw1-directional.xml (in that order) bundled in a single HTML file generated using the submission script. Also submit your new emitters src/emitters/directional.cpp and src/emitters/point.cpp, as well as your direct illumination integrator src/integrators/simple.cpp. Please tar/zip all these files together.