Extending framework

In this section, we will explain how to extend the features of Lightmetrica.

Note

This section focuses on describing the concepts rather than providing actual working codes to implement the extensions. For this, you can refer to Examples.

Note

This section covers only basic concepts to extend the framework. You will find more detailed discussion about the component object system used in the framework in Component.

Component interface

Lightmetrica is designed to be extensible. The most of the features of the framework, e.g., materials, lights, cameras, or rendering techniques, can be implemented as extensions. This allows the developers to design flexible experiments according to their requirements.

Extension mechanism of Lightmetrica follows well-recognized paradigm of objected-oriented programming (OOP). An extensible feature of the framework is provided as an abstract class (interface) of C++. To extend a feature, a developer wants to implement a class by inheriting the abstract class.

A typical extensible interface looks like the following code. This code example is taken from lm::Renderer class defined in include/lm/renderer.h with some clean-ups. For other examples, you can open some headers in include/lm directory (e.g., light.h or material.h).

#include "component.h"

LM_NAMESPACE_BEGIN(LM_NAMESPACE)

class Renderer : public Component {
public:
    virtual Json render() const = 0;
};

LM_NAMESPACE_END(LM_NAMESPACE)

As shown in the example, an extensible interface must inherit the common base class lm::Component, which provides a necessary features as a class being managed by the object management system of the framework. Since an extensible interface always inherits lm::Component, we call the extensible interface component interface. A component interface contains one or more virtual functions to be implemented by developers in a derived class.

Note

In the following discussion, all example codes are assumed to be defined inside lm namespace. In the actual code, this can be done by enclosing the code by LM_NAMESPACE_BEGIN() and LM_NAMESPACE_END() macros by LM_NAMESPACE, which is resolved to lm.

Configuring build environment

You want to follow the recommended practice described in Managing experiments according to your requirements of your experiment.

Implementing interface

Once you identify the component interface that you want to extend, the next step would be to implement your own class by inheriting the interface. For instance, the following code implements lm::Renderer class, which just generates a blank image with the color given as a parameter.

#include <lm/renderer.h>
// ...

class Renderer_Blank final : public Renderer {
private:
    Vec3 color_;
    Film* film_;

public:
    virtual void construct(const Json& prop) override {
        color_ = json::value<Vec3>(prop, "color");
        film_ = json::comp_ref<Film>(prop, "output");
    }

    virtual Json render() const override {
        film_->clear();
        const auto [w, h] = film_->size();
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                film_->set_pixel(x, y, color_);
            }
        }
        return {};
    }
};

LM_COMP_REG_IMPL(Renderer_Blank, "renderer::blank");

You need to register the implemented class to the framework using LM_COMP_REG_IMPL macro, which takes the type of the implemented class and the identifier (in renderer::<name> format). The identifier is used to create the instance of the component from Python API.

In the class we implement two functions. lm::Component::construct() function implements an function being called when the component instance is created. The function is a virtual function exposed in lm::Component class. lm::Component::construct() function takes a parameter prop of lm::Json type, which is typically passed from Python API.

We can pass arbitrary parameters as long as the framework supports serialization of the type. We can get the values from prop using the API of nlohmann/json library, which is used to implement the feature, or the functions in lm::json namespace.

In this example, lm::json::value() function checks color key in the given Json object. If the key is found, it tries to convert the underlying value to the type specified by the type parameter Vec3. If the key is not found, or the type of the underlying value cannot be converted to Vec3, the function throws an exception. json::comp_ref() function is used to get an instance of the other component using a given asset locator.

lm::Renderer::render() function implements the core logic of the renderering technique. In this example, it just iterates through every pixel in the given film and set it to a constant color.

Note

All built-in features of Lightmetrica are also implemented as extensions, which would be useful references for your implementation. You can find them in src/<interface_name> directories.

Using extended feature from Python API

In the case of lm::Renderer class, the instance creation of the class corresponds to lm.load_renderer() function in Python API. The following code creates a renderer using the identifier (blank) which corresponds to the second half of the identifier used for the registration (renderer::blank).

renderer = lm.load_renderer('renderer', 'blank', color=[0,1,0], output=film)

Note

For the usage of the Python API for assets loading, scene creation, or rendering, please refer to Basic rendering.

Note

Actual usage of the implemented component depends on the interface. Many of the class representing scene assets (e.g., material, light, camera, etc.) can be created by lm.load_* function, but some interfaces might use different API or might be used implicitly in the framework.

On advanced topics

This section doesn’t fully cover the entire feature of the component object system of the framework. For the full detail, please refer to Component, where we will discuss about the following advanced concepts:

  • Instance creation in C++

  • Different types of instances (owned, weak)

  • Managing component object tree

  • Supporting locator lookup

  • Supporting serialization