This example demonstrates the traversal of compiled materials. It thereby allows investigating the structure of expression graphs based on an example application that reconstructs compilable MDL code.
With this example, we provide the compiled_material_traverser_base
, a simple base class for iterating over the structure of a material in a recursive manner. During the traversal, virtual functions are called on certain occasions and while reaching particular elements in the expression graph. Deriving classes, like the compiled_material_traverser_print
, implement those virtual methods in order to realize their intended logic. To keep track of the current state, the implementations can provide a custom context object that is passed through for each invocation without any changes by the base class.
Similar to the definition of an MDL material, a compiled material also consists of three sections: parameters, temporaries, and the body. Therefore, the traversal also has three corresponding stages: ES_PARAMETERS
, ES_TEMPORARIES
, and ES_BODY
. There is one virtual method call at the beginning of each of those stages and one at the end, respectively. For convenience, there are two additional stages provided to the deriving classes: ES_NOT_STARTED
to define the current state before a traversal has been started and ES_FINISHED
to indicate that the traversal is done. Deriving classes are free to keep track of the current state using the context object that is passed through.
During the three main stages, the traversal visits the mi::neuraylib::IExpression and mi::neuraylib::IValue nodes of the tree recursively. When reaching the next node in the tree, the visit_begin(...)
method is called. Then, the child elements are processed. In case of multiple children, there is an additional call to visit_child(...)
for each child. This will happen while iterating over the arguments of a mi::neuraylib::IExpression_direct_call and while iterating over the components of a mi::neuraylib::IValue_compound. A call to visit_end(...)
indicates that the processing of the current node is done.
Each of these three methods provides a pointer to the compiled material that is processed, the custom context object, and a Traversal_element
with information about the element that is currently visited. This includes pointers to the current expression, value, parameter, or temporary. Only one of these four pointers is different from NULL
during each invocation. Depending on the stage, not all cases are possible. During the ES_PARAMETERS
stage, for instance, only parameters and values but no temporaries or expressions can occur. The Traversal_element also provides a sibling_count
and a sibling_index
that might be useful to derived classes like the compiled_material_traverser_print
.
To start the actual traversal, the protected traverse(...)
method is called by by the deriving class. At this point the custom context object is provided to the base class.
The example application contains an MDL printer. Based on the compiled_material_traverser_base
this printer generates a serialized representation of the compiled material in MDL syntax.
The input of the traversers is a compiled material. Therefore, we first need to load a module, create a material instance, and compile this instance to get a mi::neuraylib::ICompiled_material. Before starting the iteration of the expression tree, we setup the Compiled_material_traverser_print::Context
to configure the behavior of the printer:
The constructor consumes three arguments. While the first two are required to resolve textures, measured BSDFs, and light profiles, the last one parameter requires more explanations.
The structures produced by the compiler do not always match the structure of the input material. The main reason for that is optimization and the simplification of expressions that are constant during compile time. Another example is the transformation of constants to parameters while in class compilation mode. Generating MDL code that reflects the latter directly may result in invalid MDL code. This can happen if keywords, like material_emission
, appear as parameters. However, the discussed printer focuses on generating compilable MDL code but we also want to provide a tool to investigate the produced compiler output. To enable both, the example allows choosing between the two behaviors. Setting keep_structure
to true
preserves the structure of the compiler output even if this would result in printed MDL code that will not compile. Setting keep_structure
to false
, which is the default value, will inline the transformed constants back into the body. This results in MDL code that is easier to read and that can be loaded and compiled later.
After setting up the context, the traversal can be started by calling print_mdl(...)
:
Since the mi::neuraylib::ICompiled_material lacks some information to generate a valid module, the original module name has to be provided for referencing exported functions in the original module in which the material to process was defined in. The new material also requires a name that has to be provided, too.
The returned result can then be written into a file for later investigation or processing. Besides that, the context contains some further information about the material:
If we do not inline transformed constants, we want to inform about invalid MDL after running the printer. The result will be false if an invalid case was encountered. Note, this can only be false if keep_structure
of the context was set to true.
Since the printer needs to keep track of modules that have to be imported by the generated module, we also provide that information to the user. In addition to the modules, we also track the directly referenced resources.
The example can be started from the command line. The module to process can be specified by passing a qualified name as an argument. Additionally, there are flags to choose between class and instance compilation: -class
and -instance, respectively. If none of them is provided, the class compilation mode is used. Furthermore, passing the -keep
flag will preserve the structure of the compiler output as described above. Here are some examples:
will traverse all materials in the core_definitions module in instance mode while keeping the structure produced by the compiler.
All three commands above will produce the same results as they use the default parameters -- the traversal of the materials defined in the example module. The class compilation mode will be used and the compiler generated parameters are inlined.
With the goal of demonstrating the traversal, this printer has some restrictions:
The header of compiled_material_traverser_base
is listed below. It contains the interface declarations for the basic traversal. Please also refer to the source files for more details on the printing and the formatting that is performed by the printer implementation.
Source Code Location:
examples/mdl_sdk/traversal/example_traversal.cpp
examples/mdl_sdk/shared/compiled_material_traverser_base.h
examples/mdl_sdk/shared/compiled_material_traverser_base.cpp
examples/mdl_sdk/traversal/compiled_material_traverser_print.h
examples/mdl_sdk/traversal/compiled_material_traverser_print.cpp