Ana Modules, Classes, and Functions
Ana provides a set of shared modules, base classes, and helper functions to support channel development.
The Context Module
Nodes often need information about the current execution context. This includes package configuration information, channel configuration information, runtime parameters, etc. This information is stored in the a module called “anatools.lib.context”.
Here is a node that uses context to print the current run number:
The following attributes can be retrieved from the context module:
ctx.channel - a pointer to the Channel class instance for this channel
ctx.seed - the initial seed for random numbers generated by “ctx.random” functions
ctx.interp_num - the current run number
ctx.preview - a boolean that specifies whether or not the current execution is a preview
ctx.output - the value passed in from the “--output” command line switch
ctx.data - the value passed in from the “--data” command line switch
ctx.random - an instance of the numpy “random” function seeded by ctx.seed
ctx.packages - a dictionary of package configurations used by the channel, one entry per package
Base classes and Helper Functions
Ana provides a number of base classes and helper functions to simplify node construction.
Base Class: Node
This is the abstract base class for all nodes. It implements input and output port processing and stores information used in node execution. Here’s a simple Node example:
For details on how to use this class, see the Node section above.
Base Class: AnaScene
The AnaScene base class simplifies the management of a scene in Blender. The AnaScene class encapsulates the Blender scene data block, allows the user to add objects to the scene, sets up a basic compositor for rendering, and provides methods for generating annotation and metadata files for objects in the scene. Here’s an example of a node that creates an AnaScene.
Base Class: AnaObject
The AnaObject base class simplifies the management of 3D objects in Blender. AnaObject encapsulates the Blender object data block, provides a common mechanism for creating objects, and stores annotation and metadata information for the object.
The following example node creates an AnaObject, loads it from a Blender file, and adds it directly to an AnaScene:
The load method requires the Blender file be configured as follows:
The object must be in a collection that has the same name as the object_type, e.g. “truck”
The object must have a single root object that has the same name as the object_type, e.g. “truck”. If your blender object is made up of separate components then you can create an “empty” object to be the root and make the separate components children of that empty.
To manipulate the Blender object encapsulated by AnaObject, you access the “root” attribute which is a pointer to the blender data block. For example, by default new AnaObjects are placed at location [0,0,0] in the Blender scene. To move the object to a new location, you modify the location attribute of the root object. Here’s an example of a node that moves an AnaObject to a new location.
Any blender object data block attribute can be modified in this way.
By default, when the AnaObject load method loads the object from a Blender file. To change this behavior you subclass of AnaObject and override the load method. If you override the load method then your new method must do all of the following :
Create the Blender object. The object must have a single root object. Set “self.root” to equal the root object's Blender data block.
Create a collection to hold the object. Link the root object and all its children to that collection. Set “self.collection” to equal the collection’s data block.
Set “self.loaded = True”
Here is an example that creates an AnaObject from a Blender primitive.
To manipulate an AnaObject, you access the “root” attribute which points to the blender data block. For example, by default a AnaObject is placed at location [0,0,0] in the Blender scene. To move the object to a new location, you modify the location attribute of the root object. Here’s an example of a node that moves an AnaObject to a new location.
Any blender object data block attribute can be modified in this way.
Base Classes: ObjectGenerator and ObjectModifier
The ObjectGenerator and ObjectModifier classes provide a scalable, probability-based mechanism for creating and modifying objects in a scene.
Typical use case: A user wants to create images that contain objects randomly selected from a pool of different object types. The user wants the probability of selecting any given object type to be exposed in the graph. The user also has a set of modifications they would like to apply to those objects. Which modifications can be applied to which objects and the probability of a given modification being applied to a given object type must also be exposed in the graph. This can be challenging to represent in a graph if the combination of object types and object modifications is large.
One solution is to build a sample space of object generator and object modifier code fragments. Each entry in the sample space is one of the allowed generator / object modifier combinations along with the probability that it will occur. At run time, one of these generator/modifier combinations is drawn from the sample space, the object is generated, and the modifiers are applied. This process is repeated until the desired number of objects have been added to the scene.
In Ana, the object generator / object modifier sample space is implemented as a tree structure. The tree has a root, intermediate nodes are modifiers and end nodes are generators. Each branch of the tree has a weight assigned to it that determines the probability that branch of the tree will be taken when a root to leaf path is constructed. To select a sample, a path is generated through the tree and the generator and modifier nodes along the selected path are executed in reverse order to create and then modify an object. This process is repeated until the desired number of objects have been created.
Here is an example of a simple generator/modifier tree:
The values on the branches are the relative weights. The bold lines indicate the path constructed for one sample. To create this path we start at the root and select one of the child branches. The branch on the left has a normalized weight of 1/4 (25% probability of being selected) and the branch on the right has a normalized weight of 3/4 (75% probability of being selected). We generate a random number, select the right branch and move to the dirt modifier. From there the left and right child branches each have a weight of 1/2 (50% probability of being selected). We generate a random number, select the left branch, and move to the truck generator. This is an end node so we have a full path which is “dirt modifier → truck generator”. We then execute these code units in reverse order, first generating the truck and then applying the dirt.
This tree can be constructed by executing a graph with Ana nodes that create and link ObjectGenerators and ObjectModifiers into the desired tree structure. Here is an example graph that does this:
This graph is executed from left to right.
The Tank node creates an ObjectGenerator of type “Tank” and passes it to the RustModifier and the DentModifier.
The Bradley node creates an ObjectGenerator of type “Bradley” and passes it to the RustModifier and DentModifier.
The RustModifier node creates an ObjectModifier of type “Rust” and sets the Tank and Bradley generators as children. This subtree is passed to the Weight node.
The DustModifier node creates an ObjectModifier of type “Dust” and sets the Tank and Bradley generators as children. This subtree is passed to the PlaceObjectsRandom node.
The weight node changes the weight of the branch to the subtree that was passed in from the default of 1 to a value of 3. The Weight node then passes that subtree on to the PlaceObjectsRandom node.
The PlaceObjectsRandom node creates a “Branch” ObjectModifier and sets the two subtrees passed to it as children. This completes the generator/modifier tree.
The PlaceObjectsRandom loops 10 times, each time generating a path through the tree and then executing it in reverse order to create and modify an object.
Here is the code for a Node that creates an ObjectGenerator:
This node uses a helper function called “get_blendfile_generator” that creates an ObjectGenerator from the object definition specified in the “package.yml” file. The helper function takes three parameters
package - name of the package that defines the object
object_class - the Python class that will be used to instantiate the object
object_type - the object type as specified in the package.yml file
Object modifiers come in three parts - the node that will generate the ObjectModifier and the object modifier method that does the actual modification.
Here is the code for a Node that creates an ObjectModifier:
In this example, the node takes one or more object generators as inputs as well as the scale factor to apply. It then creates an ObjectModifier and makes the incoming object generators its children. It then passes the new generator tree on to the next node.
The ObjectModifier class has two required parameters plus optional keyword parameters.
“method” - this is the name of the modifier method, specified as a text string
“children” - this is a list of all children of the ObjectModifier
keyword arguments - these arguments will be passed as keword argument parameters to the object modifier method
The object modifier method is a member of the object class that is to be modified. The simplest way to implement this is to include the method in the object class definition. Here is an example of a Jeep object that implements the “scale” modifier method.
The problem with this approach is the modifier can only be applied to that specific object class. In most cases we want modifiers to apply to more than one class. The easiest way to do this is to use the mixin design pattern.
A mixin is an abstract base class that defines a method that will be used by other classes. Any class that wants to implement this method specifies the mixin class as one of its parents.
This example shows how to implement the mixin. Note in this example, the mixin class is implemented in a module called mypack.lib.mixins and the classes that inherit from it are in a separate module.
from abc import ABC class ScaleMixin(ABC): def scale(self, scale): self.root.scale[0] = scale self.root.scale[1] = scale self.root.scale[2] = scale
Here is are several classes that use the mixin:
‘file_metadata’ helper function
The ‘file_metadata’ function can be imported from the anatools.lib.file_metadata module. It takes a filename as input and returns a dictionary that contains the metadata for the file. Note this is intended for files contained in package and workspace volumes.
To store metadata for a file ‘myfile.blend’, you create the file ‘myfile.blend.anameta’ in the same directory. The metadata in the anameta file and is in YAML format.
Last updated