Hello Sphere of Operators

Let's start by creating a simple Operator that creates one or more spheres of a given radius. Click on create_spheres below to expand and view the code block.

See Creating a Simple Plugin for further information about compiling the operators

create_spheres.cpp
#include <ai.h>
#include <sstream>

AI_OPERATOR_NODE_EXPORT_METHODS(OpMethods);

node_parameters
{
	AiParameterUInt("num_spheres", 1);
	AiParameterFlt("radius", 0.5f);
}

operator_init
{
	return true;
}

operator_cleanup
{
	return true;
}

operator_cook
{
	unsigned int num_spheres = AiNodeGetInt(op, "num_spheres");
	float radius = AiNodeGetFlt(op, "radius");

	for (unsigned int i = 0; i < num_spheres; ++i)
	{
		std::stringstream node_name;
		node_name << "sphere" << i;

		AtNode* sphere = AiNode("sphere", AtString(node_name.str().c_str()));
		AiNodeSetFlt(sphere, "radius", radius);
	}

	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("create_spheres");
	node->node_type = AI_NODE_OPERATOR;
	strcpy(node->version, AI_VERSION);

	return true;
}

The transformation of the spheres depends on how the Operator is cooked. If it's cooked as part of a graph that's connected to the options node the spheres will be at the world origin. If however the Operator is cooked as part of a graph that's connected to a procedural the spheres will inherit the procedural's transform chain.

 

In the next example, we'll expand on this to create an Operator that also creates spheres, but it also creates a child instance of itself to dynamically keep on making spheres. We'll also explain the benefits of doing this.

 

Dynamic Creation of Spheres Using Child Operators

Let's build on the sphere Operator above 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 by expanding 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;
}

Scattering and Shading Spheres

Before we describe the operators we're going to use to scatter and shade the spheres generated by private_ryan, we'll look at a simple example that summarises the chain of events. The diagram below shows 4 chained Operators and images, where each image represents what the scene would look like if it was rendered with the respective Operator set as the target Operator.

 

The first Operator is the built-in Collection Operator which creates a reusable selection expression. Nothing is visible if we render from the collect_spheres Operator. The second Operator is private_ryan which we described above, and as expected we get one or more overlapping spheres at the origin if we render with it as a target. We attach the scatterer Operator (see below) which randomly scatters the spheres if we render from this point. Lastly, we attach a shade Operator (see below) which shades the spheres if rendered from there.

Click on the Arnold test scene below to look at how this is set up using a .ass file.

Arnold test scene - Create/scatter/shade spheres
options
{
	AA_samples 3
	outputs "RGBA rgba filter render"
	xres 640
	yres 480
	camera "camera"
	operator "shade_spheres"
}

gaussian_filter
{ 
	name filter
	width 2
}

driver_png
{
	name render
	filename "test.png"
}

persp_camera
{
	name camera
	matrix 1 1 MATRIX
	 1 0 0 0
	 0 1 0 0
	 0 0 1 0
	 0 0 6 1
	fov 70
}
 
skydome_light
{
	name sky
	intensity 0.8
	camera 1
}
 
distant_light
{
	name light
	matrix
	 0.788675129 -0.211324871 -0.577350259 0
	 -0.211324871 0.788675129 -0.577350259 0
	 0.577350259 0.577350259 0.577350259 0
	 1 1 1 1
}
 
collection
{
	name collect_spheres
	collection "all_spheres"
	selection "*.(@node == 'sphere')"
}
 
private_ryan
{
	name make_spheres
	inputs "collect_spheres"
	stop_if_radius_below 0.0001
	initial_seed 9999
	max_instances 50
}
 
scatterer
{
	name scatter_spheres
	inputs "make_spheres"
	selection "#all_spheres"
	min_x -4.0
	max_x 4.0
	min_y -2.5
	max_y 2.5
	min_z -10.0
	max_z 0.0
}
 
shade
{
	name shade_spheres
	inputs "scatter_spheres"
	selection "#all_spheres"
}
 

You can render the scene with different targets using kick, e.g.

kick -op scatterer

Scatterer Operator Example

The scatterer has a selection expression which tells it which objects to scatter (translation only). It randomly places each object that's selected within a region which the user can specify. Remember that from the Operator's perspective it only sees a single object at a time in the cook function.

scatterer.cpp
#include <ai.h>
#include <random>
AI_OPERATOR_NODE_EXPORT_METHODS(OpMethods);

std::uniform_real_distribution<float> dist(0.0, 1.0);
std::mt19937 rand_generator;

node_parameters
{
	AiParameterStr("selection", "");
   
	AiParameterFlt("min_x", 0.0);
	AiParameterFlt("max_x", 1.0);
	AiParameterFlt("min_y", 0.0);
	AiParameterFlt("max_y", 1.0);
	AiParameterFlt("min_z", 0.0);
	AiParameterFlt("max_z", 1.0);
}
 
operator_init
{
	rand_generator.seed(600);
	return true;
}
 
operator_cleanup
{
	return true;
}
 
operator_cook
{
	// For this we only want to operate on shape nodes
   	const AtNodeEntry* nentry = AiNodeGetNodeEntry(node);
	if (AiNodeEntryGetType(nentry) != AI_NODE_SHAPE) return 1;
	
	// Get the shape's matrix and scatter region parameters
	AtMatrix xform = AiNodeGetMatrix(node, "matrix");
	float min_x = AiNodeGetFlt(op, "min_x");
	float max_x = AiNodeGetFlt(op, "max_x");
	float min_y = AiNodeGetFlt(op, "min_y");
	float max_y = AiNodeGetFlt(op, "max_y");
	float min_z = AiNodeGetFlt(op, "min_z");
	float max_z = AiNodeGetFlt(op, "max_z");
	
	// Generate a random translation within the specified region
	float x = min_x + dist(rand_generator) * (max_x - min_x);
	float y = min_y + dist(rand_generator) * (max_y - min_y);
	float z = min_z + dist(rand_generator) * (max_z - min_z);
	
	// Apply the translate transformation to the shape's matrix
	AtMatrix translate = AiM4Translation(AtVector(x, y, z));
	xform = AiM4Mult(xform, translate);
	AiNodeSetMatrix(node, "matrix", xform);
 
	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("scatterer");
	node->node_type = AI_NODE_OPERATOR;
	strcpy(node->version, AI_VERSION);
 
	return true;
}

Shade Operator Example

The shade Operator creates a shader for each selected shape (using the selection expression) and attaches it to the shape. The base color of the shader is a random value which is generated with the shape's hash as a seed.

shade.cpp
#include <ai.h>
#include <sstream>
#include <random>
AI_OPERATOR_NODE_EXPORT_METHODS(OpMethods);

std::uniform_real_distribution<float> dist(0.0, 1.0);
std::mt19937 rand_generator;

float random_value()
{
	return dist(rand_generator);
}
 
node_parameters
{
	AiParameterStr("selection", "");
	AiParameterBool("use_rgb", false);
	AiParameterRGB("rgb", 0.0f, 0.0f, 0.0f);
	AiParameterBool("use_matte", false);
}
 
operator_init
{
	return true;
}
 
operator_cleanup
{
	return true;
}
 
operator_cook
{
	// For this we only want to operate on shape nodes
   	const AtNodeEntry* nentry = AiNodeGetNodeEntry(node);
	if (AiNodeEntryGetType(nentry) != AI_NODE_SHAPE) return 1;
 
	// Set the random seed using the name of the node we are cooking
	AtString cooked_node_name(AiNodeGetName(node));
    rand_generator.seed(cooked_node_name.hash());

	// Grab the given rgb value if specified, otherwise generate a random colour
	AtRGB rgb;
	if (AiNodeGetBool(op, AtString("use_rgb")))
	{
		rgb = AiNodeGetRGB(op, "rgb");
	}
	else
	{
		rgb.r = random_value();
		rgb.g = random_value();
		rgb.b = random_value();
	}

	// Create a name for our shading node based on the cooked node
	// (for simplicity each node gets a separate shader instead of
	// reusing shaders)
	std::stringstream ss_shader_name;
	ss_shader_name << "shader_" << AiNodeGetName(node);
	std::string shader_name = ss_shader_name.str();
 
	// Create a shader node if it doesn't exist 
	AtNode* shader = AiNodeLookUpByName(shader_name.c_str());
	if (!shader)
	{
		// If we are using a matte we create a matte shader, otherwise we
		// create a standard_surface shader
		if (AiNodeGetBool(op, "use_matte"))
			shader = AiNode("matte", shader_name.c_str());
		else
			shader = AiNode("standard_surface", shader_name.c_str());
	}
	if (!shader) // something went wrong when creating the shader
		return false;
    
	// Set the colour and assign the shader to the cooked node
	AiNodeSetRGB(shader, "base_color", rgb.r, rgb.g, rgb.b);
	AiNodeSetPtr(node, AtString("shader"), shader);

	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("shade");
	node->node_type = AI_NODE_OPERATOR;
	strcpy(node->version, AI_VERSION);

	return true;
}
  • No labels