Writing a new scene element class
Contents
Writing a new scene element class#
Warning
Please first read carefully the Contributing to Eradiate section. Writing scene elements requires general knowledge of the attrs, Pint and Pinttrs libraries.
Scene elements, deriving from the SceneElement
class, are the core of Eradiate’s scene generation system. They provide an
interface to quickly and safely generate kernel scene dictionary elements
(see KernelDict
).
The SceneElement
base class#
SceneElement
is the abstract base class for all
scene elements. We will see here how this class works, and then how to write a
new scene element subclass.
SceneElement
is decorated byattr.s()
. Although not required, this is a hint at the user: all scene element classes are written with theattrs
library.SceneElement
has anid
instance attribute with a default value: consequently, all instance attributes defined for child classes must have default values.SceneElement
derives fromabc.ABC
: it is an abstract class and cannot be instantiated.SceneElement
has an abstract methodkernel_dict()
which must be implemented by its derived classes: it returns a dictionary which can be then used as an input to the kernel.SceneElement
is the base class of a set of abstract interfaces (e.g.Illumination
,Spectrum
, etc.) which are the ones from which new scene elements should derive.
Factory registration#
All interfaces derived from SceneElement
are
associated a specialised factory (see Factory guide).
New SceneElement
subclasses should be registered
to the relevant factory so that Eradiate’s dictionary-based object and
initialisation system works properly.
import attr
from eradiate.scenes.spectra import Spectrum, spectrum_factory
from eradiate import ureg
@spectrum_factory.register(type_id="my_spectrum")
@attr.s
class MySpectrum(Spectrum):
field = pinttr.ib(default=1.0, units=ureg.m)
def eval(ctx=None): ... # Definition skipped
def kernel_dict(ctx=None): ... # Definition skipped
obj = spectrum_factory.convert({"type": "my_spectrum", "field": 1.0})
As mentioned in the Factory guide, factory registration occurs only upon class definition: a module defining a scene element must be imported for the defined class to be registered to a factory.
Using factory converters#
As mentioned in the Factory guide, Eradiate’s
factories implement a convert()
class
method which can turn a dictionary into a registered object—and if the method
receives something else than a dictionary, it simply does nothing.
This method can be used as a converter in the attribute initialisation sequence to automatically convert a dictionary to a specified object. This allows for the use of nested dictionaries to instantiate multiple objects.
import attr
import pinttr
from eradiate import unit_registry as ureg
from eradiate.scenes.illumination import Illumination, illumination_factory
from eradiate.scenes.spectrum import Spectrum, spectrum_factory
@spectrum_factory.register(type_id="my_spectrum")
@attr.s
class MySpectrum(Spectrum):
field = pinttr.ib(default=1.0, units=ureg.m)
def eval(ctx=None): ... # Definition skipped
def kernel_dict(ctx=None): ... # Definition skipped
@illumination_factory.register("my_illumination")
@attr.s
class MyIllumination(Illumination):
radiance = attr.ib(
factory=MySpectrum,
converter=spectrum_factory.convert
)
def kernel_dict(): ... # Definition skipped
# Pass object created with constructor
obj = MyIllumination(radiance=MySpectrum(field=2.0))
# Use the factory to convert a dictionary to ElementA
obj = MyIllumination(element_a={"type": "my_spectrum", "field": 3.0})
# Instantiate MyIllumination using nested dicts
obj = illumination_factory.create({
"type": "my_illumination",
"radiance": {"type": "my_spectrum", "field": 4.0},
})
The kernel_dict()
method#
Any scene element must implement a kernel_dict()
method
which will return a dictionary suitable for merge into a kernel scene
dictionary. These dictionaries are written following the Mitsuba scene
specification and the interested reader is referred to kernel docs for further
information.
Note
When writing the kernel_dict()
method, there are
a few precautions to keep in mind:
kernel imports must be local to the method;
if a kernel import is required to build the dictionary, a kernel variant must be selected when it is called (in practice, this means that Eradiate’s operational mode must have been selected);
kernel_dict()
’s signature should allow for the processing of aKernelDictContext
instance, which carries around state variables during recursive kernel dictionary generation.
In practice: Steps to write a new scene element class#
Following the above description, a new scene element class requires the following steps:
Derive a new class from one of the
SceneElement
subclasses. Decorate it withattr.s()
.Declare your custom attributes using
attr.ib()
. Don’t forget to add default values to all of them. Usepinttr.ib()
if the field represents a physical quantity with units. Callables can be used to evaluate units dynamically. If the field requires it, it is possible to run custom converters and validators.Implement the
kernel_dict()
method. Things to keep in mind:kernel imports must be local to the
kernel_dict()
method;the function’s signature should allow for the processing of a
ctx
keyword argument of typeKernelDictContext
(but using it is not required).
The following steps are optional:
implement a post-init hook steps using the
__attrs_post_init__()
method;enable factory-based instantiation using the
register()
decorator defined by the appropriate factory.