Create a complex operator - Arnold Developer Guide
Dynamic Creation of Spheres Using Child Operators
Let's build on the sphere Operator in Create Simple Operator and show a powerful way Operators can be used. An Operator can create another Operator , including itself. When doing so, the Operator can send private information to its children. A practical example would be an Operator authoring a cache such as Alembic, where the Operator can pass the Alembic iterator to child Operators in order to create sub-trees of the hierarchy. The benefit of doing this is twofold. Firstly, the work is split across more threads as each node, including Operators , is processed by a single thread so this effectively chunks the work up across Operators , and consequently threads. Secondly, any hierarchical information is expressed in the Operator graph which helps with maintaining and manipulating the data.
To keep things simple, let's create an Operator that:
- Creates a single sphere and a single instance of itself
- Each child instance keeps doing this until a certain criterion is met, namely:
- Each op creates a random radius and passes it to its child
- Stop if the radius received is below than some threshold or we have exceeded a given maximum number of instances, otherwise, keep going
Check the implementation in the source code for private_ryan below. The Operator doesn't set a transform on the spheres so they are subject to the same rules as described above.
The RyanData structure contains the information that's passed to the child Operators using AiOpSetChildData. As well as the data we must provide a cleanup function using AtOpCleanupChildData.
private_ryan.cpp
#include <ai.h>
#include <sstream>
#include <random>
AI_OPERATOR_NODE_EXPORT_METHODS(OpMethods);
struct RyanData
{
RyanData(float radius = 0.5f, int max_instances = 50) : radius(radius), max_instances(max_instances), num_instances(0)
{}
float radius;
int max_instances;
int num_instances;
};
namespace
{
std::uniform_real_distribution<float> dist(0.0, 1.0);
std::mt19937 rand_generator;
float random_value()
{
return dist(rand_generator);
}
}
node_parameters
{
AiParameterFlt("radius_scale", 1.0f);
AiParameterFlt("stop_if_radius_below", 0.1f);
AiParameterInt("initial_seed", 1000);
AiParameterInt("max_instances", 50);
}
operator_init
{
return true;
}
operator_cleanup
{
return true;
}
bool cleanup_child_data(void* child_data)
{
delete static_cast<RyanData*>(child_data);
return true;
}
operator_cook
{
RyanData* data = static_cast<RyanData*>(child_data);
float radius_scale = AiNodeGetFlt(op, "radius_scale");
float stop_if_below = AiNodeGetFlt(op, "stop_if_radius_below");
int max_instances = AiNodeGetInt(op, "max_instances");
std::stringstream child_name;
child_name << AiNodeGetName(node) << "/";
child_name << "ryan";
std::string name = child_name.str();
bool create_child = true;
int num_instances = 1;
if (data)
{
std::stringstream sphere_name;
sphere_name << AiNodeGetName(node) << "/";
sphere_name << "sphere";
AtNode* node = AiNode("sphere", sphere_name.str().c_str());
AiNodeSetFlt(node, AtString("radius"), data->radius);
num_instances = data->num_instances + 1;
if (data->radius < stop_if_below || data->max_instances == data->num_instances)
create_child = false;
}
else
{
int initialSeed = AiNodeGetInt(op, AtString("initial_seed"));
rand_generator.seed(initialSeed);
}
if (create_child)
{
float radius = random_value() * radius_scale;
RyanData* child_data = new RyanData(radius, max_instances);
child_data->num_instances = num_instances;
AtNode* child_op = AiNode("private_ryan", name.c_str());
AtOpCleanupChildData cleanup = &cleanup_child_data;
AiOpSetChildData(child_op, child_data, cleanup);
}
return true;
}
operator_post_cook
{
return true;
}
node_loader
{
if (i > 0) return false;
node->methods = OpMethods;
node->output_type = AI_TYPE_NONE;
node->name = AtString("private_ryan");
node->node_type = AI_NODE_OPERATOR;
strcpy(node->version, AI_VERSION);
return true;
}
private_ryan.ass
options
{
AA_samples 9
outputs "RGBA RGBA /out/arnold1:gaussian_filter /out/arnold1:jpeg"
camera "camera"
GI_diffuse_depth 1
GI_specular_depth 1
GI_diffuse_samples 3
operator "make_spheres"
}
driver_jpeg
{
name /out/arnold1:jpeg
filename "simple.jpg"
}
gaussian_filter
{
name /out/arnold1:gaussian_filter
}
persp_camera
{
name camera
matrix
1 0 0 0
0 1 0 0
0 0 1 0
0 0 30 1
}
private_ryan
{
name make_spheres
stop_if_radius_below 0.0001
initial_seed 9999
max_instances 50
}