Preliminaries

Downloading, Compiling, and Running Wakame

Part 1: Cloning using the command line

The source code of Wakame is available on Bitbucket. To check out a copy of the source code, enter

$ git clone https://bitbucket.org/smarschner/cs6630-pas
on the terminal. We'll likely publish extensions and bug-fixes to this repository as the semester progresses. To get them, navigate into the wakame directory and run the command
$ git pull
If there were any concurrent modifications to the same file, git may ask you to resolve merge conflicts. We refer you to the learning resources for information about resolving conflicts in general.

Part 2: Setting up a development environment

Wakame is written using Java version 8 and uses some of its newest features such as lambda expressions, and the new parallel processing library. As a result, to compile and run Wakame, JDK 8 is needed, and it can be downloaded from this web site. Please download and install the JDK and make sure that you can run the java and javac command from the command line.

The wakame root directory (the pa1 directory in the repository) has been set up as a project for two Java IDEs: Eclipse and IntelliJ IDEA. Please open the project with the IDE that you prefer and try to compile the project.

We will be rendering images info PFM files. The Wakame main class (to be discussed later) can open and display them, but it might be more convenient to use an external viewer that is integrated with the shell. For this, we recommend the PCG HDRITools by Edgar Velázquez-Armendáriz.

Part 3: A high-level overview

The Wakame repository consists of a number of directories, which are explained in the table below.

Directory Description
src Contains the Java source code.
lib Contains libraries in the form of JAR files that Wakame depends on
data/scenes Contains example scenes and test datasets to validate your implementation
docs Contains documentations in the form of web pages, including this very web page.

vecmath

Wakame depends on a number of external libraries. All of them has already been included in the lib directory. Most of the time, you will be dealing with the vecmath library, which is responsible for basic linear algebra types, such as vectors, points, and linear transformations. You should already be familiar with this library if you have taken some Cornell's introductory graphics courses such as CS 4620 and CS 5625. Otherwise, please take some time to look at the Javadoc of the class Tuple3d, Point3d, VEctor3d, and Matrix4d. (Important: You should know what the scaleAdd method does to the receiver and the arguments!)

As you might have been able to tell from the last paragraph, Wakame uses double precisition floating point numbers (double in Java) to store most graphics related data. As such, scalars are stored as doubles, and double-precision linear algebra types such as Point3d and Vector3d are used instead of the single-precision varieties such as Point3f and Vector3f.

Source code organization

Take a moment to browse through the source files in the src directory, which is arranged through subdirectories into the following Java packages:

Package Description
wakame Contains the base object class WakameObject and other classes which are crutial to Wakame's operations but otherwise hard to categorized into subpackages.
wakame.app Contains the main Wakame application.
wakame.block Contains classes that implements image blocks, which are areas of the output image that are rendered in parallel.
wakame.bsdf Contains classes that implement BSDF models.
wakame.camera Contains classes that implement camera models.
wakame.emitter Contains classes that implement light sources.
wakame.integrator Contains classes that implement rendering techniques.
wakame.mesh Contains classes that implement triangle meshes, which are used to represent all surfaces in Wakame.
wakame.rfilter Contains classes that implement image reconstruction filters.
wakame.sampler Contains classes that implement randon number generators.
wakame.struct Contains classes that implement basic ray-tracing-related data structures such as rays, coordinate frames, colors, and axis-aligned bounding boxes.
wakame.util Contains classes that implement ray-tracing-related utility functions.
wakame.accel Contains classes that implement ray tracing acceleration structures.
yondoko Contains utility classes that are not directly related to ray tracing.
yumyai An OpenGL library classes that uses JOGL to interface with OpenGL.

You will find that most classes have documentation markup in place. The most important class in Wakame is called WakameObject. It is the base class of everything that can be constructed using the XML scene description language. Other interfaces (e.g. Camera) derive from this class and expose additional more specific functionality (e.g. to generate an outgoing ray from a camera).

Hypothesis test support

With each programming assignment, we will provide statistical hypothesis tests that you can use to verify that your algorithms are implemented correctly. You can think of them as unit tests with a little extra twist: suppose that the correct result of a certain computation in a is given by a constant \(c\). A normal unit test would check that the actual computed \(c'\) satisfies \(|c-c'|<\varepsilon\) for some small constant \(\varepsilon\) to allow for rounding errors etc. However, rendering algorithms usually employ randomness (they are Monte Carlo algorithms), and in practice the computed answer \(c'\) can be quite different from \(c\), which makes it tricky to choose a suitable constant \(\varepsilon\).

A statistical hypothesis test, on the other hand, analyzes the computed value and an estimate of its variance and tries to assess how likely it is that the difference \(|c-c'|\) is due to random noise or an actual implementation bug. When it is extremely unlikely (usually \(p<0.001\)) that the error could be attributed to noise, the test reports a failure.

Part 4: Scene file format and parsing

Wakame uses a very simple XML-based scene description language, which can be interpreted as a kind of building plan: the parser creates the scene step by step as it reads the scene file from top to bottom. The XML tags in this document are interpreted as requests to construct certain Java objects including information on how to put them together.

Each XML tag is either an object or a property. Objects correspond to Java instances. Properties are small bits of information that are passed to an object at the time of its instantiation. For instance, the following snippet creates red diffuse BSDF:

<bsdf type="diffuse">
    <color name="albedo" value="0.5, 0, 0"/>
</bsdf>

Here, the <bsdf> tag will cause the creation of an object of type Bsdf, and the type attribute specifies what specific subclass of Bsdf should be used. The <color> tag creates a property of name albedo that will be passed to its constructor. If you open up the Java source file src/wakame/bsdf/Diffuse.java, you will see that there is a method called setProperties, which looks for this specific property:

public class Diffuse implements Bsdf {
    // Irrelevant code elided

    @Override
    protected void setProperties(HashMap<String, Object> properties) {
        albedo.set(PropertiesUtil.getColor(properties, "albedo", new Color3d(0.5, 0.5, 0.5)));
    }
}

The piece of code that associates the "diffuse" XML identifier with the Diffuse class in the Java code can be found in the static initializer of the wakame.WakameObject class:

public abstract class WakameObject {
    // Irrelevant code elided.    

    static {
        // Irrelevant code elided.            

        // BSDF
        WakameObject.registerBuilder("diffuse", Diffuse.Builder.class);
    }
}

You might be wondering why we register the "builder" Diffuse.Builder class instead of the Diffuse class itself. We will talk more about this in details in the next section.

Certain objects can be nested hierarchically. For example, the following XML snippet creates a mesh that loads its contents from an external OBJ file and assigns a red diffuse BRDF to it.

<mesh type="obj">
    <string type="filename" value="bunny.obj"/>

    <bsdf type="diffuse">
        <color name="albedo" value="0.5, 0, 0"/>
    </bsdf>
</mesh>

Implementation-wise, this kind of nesting will cause a method named addChild() to be invoked within the parent object. In this specific example, this means that Mesh.addChild() is called, which roughly looks as follows:

public class Mesh extends WakameObject {
    // Irrelevant coded elided.

    public void addChild(WakameObject obj) {
        if (obj instanceof Bsdf) {
            if (bsdf != null) {
                throw new RuntimeException("Mesh.addChild(): Tried to register multiple Bsdf instances.");
            }
            bsdf = (Bsdf)obj;
        }
        // ..(omitted)..
    }
}

This function verifies that the nested object is a BSDF, and that no BSDF was specified before; otherwise, it throws an exception.

The following different types of properties can currently be passed to objects within the XML description language:

<!-- Basic parameter types -->
<string name="property name" value="arbitrary string"/>
<boolean name="property name" value="true/false"/>
<float name="property name" value="float value"/>
<integer name="property name" value="integer value"/>
<vector name="property name" value="x, y, z"/>
<point name="property name" value="x, y, z"/>
<color name="property name" value="r, g, b"/>
<!-- Linear transformations use a different syntax -->
<transform name="property name">
    <!-- Any sequence of the following operations: -->
    <translate value="x, y, z"/>
    <scale value="x, y, z"/>
    <rotate axis="x, y, z" angle="deg."/>
    <!-- Useful for cameras and spot lights: -->
    <lookat origin="x,y,z" target="x,y,z" up="x,y,z"/>
</transform>

The top-level element of any scene file is usually a <scene> tag, but this is not always the case. For instance, some of the programming assignments will ask you to run statistical tests on BRDF models or rendering algorithms, and these tests are also specified using the XML scene description language, like so:

<?xml version="1.0"?>

<test type="chi2test">
    <!-- Run a χ2 test on the microfacet BRDF model (@ 0.01 significance level) -->
    <float name="significanceLevel" value="0.01"/>

    <bsdf type="microfacet">
        <float name="alpha" value="0.1"/>
    </bsdf>
</test>

Part 5: Creating your first Wakame class

In Wakame, rendering algorithms are referred to as integrators because they generally solve a numerical integration problem. The remainder of this section explains how to create your first (dummy) integrator which visualizes the surface normals of objects.

We begin by creating a new WakameObject subclass wakame.integrator.NormalIntegrator with the following content:

package wakame.integrator;

import wakame.Scene;
import wakame.WakameObject;
import wakame.sampler.Sampler;
import wakame.struct.Color3d;
import wakame.struct.Ray;
import wakame.util.PropertiesUtil;

import java.util.HashMap;

public class NormalIntegrator extends Integrator {
    private String myProperty;

    private NormalIntegrator() {
        // NO-OP
    }

    @Override
    public void Li(Scene scene, Sampler sampler, Ray ray, Color3d output) {
        output.set(0, 1, 0);
    }

    @Override
    protected void activate() {
        // NO-OP
    }

    @Override
    protected void setProperties(HashMap<String, Object> properties) {
        myProperty = PropertiesUtil.getString(properties, "myProperty", "nothing");
        System.out.println("Parameter value was : " + myProperty);
    }

    public String toString() {
        return String.format("NormalIntegrato[\n"
                + "  myProperty = \"%s\""
                + "]", myProperty);
    }

    public static class Builder extends WakameObject.Builder {
        @Override
        protected WakameObject createInstance() {
            return new NormalIntegrator();
        }
    }
}

Looking at the code snippet above, you can notice that an instance of NormalIntegrator cannot be created directly by the new keyword because the default constructor has been made private. Instead, to create an instance, we must create an instance the "builder" class NormalIntegrator.Builder and then call the method build() on it to get an instance, like so:

NormalIntegrator integrator = (NormalIntegrator)new NormalIntegrator.Builder().build();

Alternatively, once the builder is created, it can be used to specified the integrator's property as follows:

NormalIntegrator.Builder builder = new NormalIntegrator.Builder();
builder.setProperty("myProperty", "hello");
NormalIntegrator integrator = (NormalIntegrator)builder.build();

This rather convoluted way of creating an object comes from the application of the builder pattern, which provides a "builder" object that are used to specify parameters of the real object before its eventual construction. The builder pattern is used when there is a lot of work to be done before one arrives an object with a consistent internal state. This is especially true for any WakameObject because, before an object can be used, a number of steps has to be taken:

  1. Properties of the object must be specified, for example, through XML tags.
  2. Children must be added to the object, possibly through XML specification.
  3. After all the properties and children are specified, the object must be initialized to take into account all the specified information. (This is done through the activate() method, which all subclasses of WakameObject must implement.
The builder was created so that the user can set the properties and add children before the actual object is created. By finally calling build(), the builder creates a blank instance (whose internal state is inconsistent), sets the properties, adds all children, and initializes the instance for returning it. The returned object has consistent internal state and can be used without fear of errors.

An alternative to the builder pattern would be to allow the user to create a blank instance of the object by himself and entrust him with the responsibility of initializing it afterwards. However, yours truly tried this approach and found that it was very easy to forget to call activate() after object creation. The approach gave rise to three bugs, so your truly think the builder patttern would be better because it always gives objects that are internally consistent.

We have learned about Wakame's object creation convention. Next, to use the above integrator, we must be able to specify it in an XML scene file and have the system create an instance from such a file. To this end, we pick the name "normal" for this integrator. To let the system knows that the name "normal" with this particular class, we register the "builder" of the class in the static initializer of the WakameObject class by adding the following new lines at the end.

public class WakameObject {
    // Irrelevant code elided.

    static {
        // Irrelevant code elided.
        
        // Mesh                                                                    // Old line
        WakameObject.registerBuilder("obj", WavefrontOBJ.Builder.class);           // Old line

        // BSDF                                                                    // Old line
        WakameObject.registerBuilder("diffuse", Diffuse.Builder.class);            // Old line

        // Integrators                                                             // New line
        WakameObject.registerBuilder("normals", NormalIntegrator.Builder.class);   // New line
    }
}

Notice that we register the "builder" inner-class of the NormalIntegrator class instead of the class itself. To test the integrator, we create a test scene with the following content and save ti as test.xml:

<?xml version="1.0"?>

<scene>
    <integrator type="normals">
        <string name="myProperty" value="Hello!"/>
    </integrator>

    <camera type="perspective"/>
</scene>

This file instantiates our integrator and creates the default camera setup. Running wakame.app.Main with test.xml as its argument causes two things to happen.

First, some text output should be visible on the console:

Parameter value was : Hello!


Configuration: Scene[
  integrator = NormalIntegrato[
    myProperty = "Hello!"],
  sampler = Independent[sampleCount=1]
  camera = PerspectiveCamera[
    cameraToWorld = 
      1.0, 0.0, 0.0, 0.0
      0.0, 1.0, 0.0, 0.0
      0.0, 0.0, 1.0, 0.0
      0.0, 0.0, 0.0, 1.0
    outputSize = 1280 x 720
    fovX = 30.000000
    clip = [0.000100, 10000.000000]
    rfilter = GaussianFilter[radius=2.000000, stddev=0.500000]
  ],
  meshes = {
  }
]


17:01:46.230 [main] INFO  wakame.app.Main - Rendered 1 blocks out of 920 blocks (0.11%)
17:01:46.232 [main] INFO  wakame.app.Main - Rendered 2 blocks out of 920 blocks (0.22%)
17:01:46.232 [main] INFO  wakame.app.Main - Rendered 3 blocks out of 920 blocks (0.33%)
    :
    :
    :
17:01:46.322 [main] INFO  wakame.app.Main - Rendered 920 blocks out of 920 blocks (100.00%)
17:01:46.323 [main] INFO  wakame.app.Main - Rendering took 0 min(s) 0 second(s) 967 ms
17:01:46.353 [main] INFO  wakame.app.Main - Writing a 1280x720 PFM file to "temp.pfm"

The Wakame executable echoed the property value we provided, and it printed a brief human-readable summary of the scene. The rendered scene is saved as an PFM file named test.pfm.

Secondly, a window pops up showing the image we just rendered!

The sliders at the bottom can be used to change the zoom level of the image and the exposure value of the image.

Part 6: The real normal integrator

The NormalIntegrator we worked on in the last section just produces green, uninteresting images. Let us now modify it to make its fulfill its intended purpose, which is to display the normal vector at the first point the eye ray hits a geometry. Change the NormalIntegrator class as follows:

package wakame.integrator;

import wakame.Scene;
import wakame.WakameObject;
import wakame.sampler.Sampler;
import wakame.struct.Color3d;
import wakame.struct.Intersection;
import wakame.struct.Ray;

import java.util.HashMap;

public class NormalIntegrator extends Integrator {
    private NormalIntegrator() {
        // NO-OP
    }

    @Override
    public void Li(Scene scene, Sampler sampler, Ray ray, Color3d output) {
        Intersection its = new Intersection();
        if (!scene.rayIntersect(ray, its)) {
            output.set(0,0,0);
        } else {
            output.set(Math.abs(its.shFrame.n.x),
                    Math.abs(its.shFrame.n.y),
                    Math.abs(its.shFrame.n.z));
        }
    }

    @Override
    protected void activate() {
        // NO-OP
    }

    @Override
    protected void setProperties(HashMap<String, Object> properties) {
        /* No parameters this time */
    }

    public String toString() {
        return "NormalIntegrator[]";
    }

    public static class Builder extends WakameObject.Builder {
        @Override
        protected WakameObject createInstance() {
            return new NormalIntegrator();
        }
    }
}

Invoke wakame.app.Main on data/scenes/p1/ajax-normals.xml, and you should get the image below