' '

Difficulty

3

We consider two kinds of patterns: 2D and 3D patterns. Let’s start with 2D patterns, because those are the easiest to reason about.

1. 2D Patterns

A 2D pattern can be seen as a black-and-white image. With black and white, we mean literally black and white, not grayscale. In code, a 2D pattern is represented as an object that takes 2D coordinates \((x, y)\) and returns the corresponding color, i.e., black or white. In code, we’ll represent these two colors as true and false.

Note

Which boolean value corresponds to which color does not really matter. In a later step, we will make this configurable: true will be associated with one material and false with another, meaning you can choose black and white, white and black, red and blue, etc.

For now though, we limit ourselves to simply true and false, which we’ll also refer to as black and white, since that makes it easier to visualize.

1.1. Examples

Below are a few examples of patterns:

Horizontal lines

hlines

Vertical lines

vlines

Checkered

checkered

1.2. Target Syntax

How would we model a 2D pattern in code? As explained above, there really is just one operation: we can ask a 2D pattern what color appears at a certain position.

// Possible implementation
Pattern2D some_pattern;
Point2D p(x, y);
bool color = some_pattern.get_color_at(p);

However, we’d rather see patterns as functions, so instead of calling a get_color_at method, we’d rather write

// Function syntax
Pattern2D some_pattern;
Point2D p(x, y);
bool color = some_pattern(p);

This function-like syntax will be achieved by defining a class Pattern2D and overloading the operator () on it.

1.3. Value Semantics

We would like Pattern2D objects to be easily usable, i.e., we would prefer having to think about references and pointers and such. Ideally, we would use Pattern2Ds as if they were ints, bools or any other simple value type. In fact, this kind of types have already been implemented: Primitive is an example of this.

Take a good at the types Primitive and PrimitiveImplementation, as you will want to use it as a guide to implement patterns.

  • Note how PrimitiveImplementation is the actual base class for all primitives (spheres, planes, etc.)

  • PrimitiveImplementation defines all functionality related to primitives. More concretely, it defines all overridable methods.

  • Primitive internally keeps a shared_ptr<PrimitiveImplementation>.

  • Primitive overloads the operator, which allows you to access the PrimitiveImplementation methods. For example, primitive→method() actually calls primitive→implementation→method().

1.4. Implementation

1.4.1. Setting Things Up

  • Create a new folder patterns on the same level as primitives, materials, lights, etc.

  • Create a file patterns/pattern.h. No corresponding pattern.cpp will be necessary.

  • Add a namespace declaration patterns.

  • Also create an empty file patterns/patterns.h. When you later add patterns, you will add an #include directive here so that client code can include all patterns in one go.

1.4.2. Pattern2DImplementation

This class will be fully abstract. It acts as a base class for all 2D patterns and therefore will define all functionality common to 2D patterns.

Inside patterns/pattern.h, define a class Pattern2DImplementation. It has one member: a public method at.

  • at takes a Point2D. Think about how to pass this parameter along. A Point2D is a fairly large object, so don’t pass it by value. It also doesn’t need to be modified.

  • at returns a bool.

  • at must be overridable.

  • at has no body at this level.

  • Calling at will not modify the pattern object in any way.

If you’re wondering about the () syntax: this is an internal class, client code won’t come into direct contact with it. Let’s keep the fancy stuff for where it can actually be seen.

1.4.3. Pattern2D

If client code were to have to use Pattern2DImplementation directly, it would have to keep track of these objects using pointers, as the object is involved in an object hierarchy. To simplify usage, we also provide a wrapper class named Pattern2D which will allow client code to manipulate pattern objects as if they were values, i.e., pass them by value.

In patterns/pattern.h, add a Pattern2D class.

  • Have it track of a Pattern2DImplementation object using a shared_ptr.

  • Define a constructor that receives a shared_ptr and uses it to initialize it the field.

  • Overload the () operator. The goal is for pattern2d(point2d) to mean the same as pattern2d.implementation→at(point2d) were implementation public.

2. 3D Patterns

3D patterns are identical to 2D pattern, except that they take a 3D point instead of a 2D point.

Pattern3D some_pattern;
Point3D p(x, y, z);
bool color = some_pattern(p);

2.1. Implementation

Still in patterns/pattern.h, add a class Pattern3DImplementation. It should be identical to Pattern2DImplementation except for the Point2D parameter of at.

To the same file, define the class Pattern3D which again is the same as Pattern2D except for the Point2D.

3. Bindings

As a last step, we need to set up the necessary foundations to expose pattern-related functionality to the scripting language. We would like all such functionality to be hosted in a separate module named Patterns, just like there is are Animations, Samplers and Raytracers modules.

Create files scripting/patterns-module.cpp and scripting/patterns-module.h. Copy scripting/lights-module.cpp and scripting/lights-module.h to the corresponding patterns files and make the necessary changes.

  • You will need a PatternLibrary that will represent the module. If you want a function Patterns.func() to be available in the scripting language, you need to add a corresponding func() in the PatternLibrary.

  • The PatternLibrary will be empty for now. Future pattern-related extensions will each add methods.

  • You will need to register the types Pattern2D and Pattern3D.

4. Evaluation

Create two patterns (2D and 3D) that (rather boringly) always returns the same value. Place the code in files named patterns/constant-pattern.h

namespace patterns
{
    Pattern2D constant2d(bool);
    Pattern3D constant3d(bool);
}

For example, constant2d(true) should return true for all \((x, y)\).

Write tests to ensure that both constant2d and constant3d work as intended.