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:

import anatools.lib.context as ctx
from anatools.lib.node import Node

class PrintRunNumber(Node):
    def exec(self):
        print(f'Current run number {ctx.interp_num')
        return {}

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:

from anatools.lib.node import Node

class MyNode(Node):
    def exec(self):
        return {}

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.

import bpy
from anatools.lib.node import Node
from anatools.lib.scene import AnaScene

class CreateEmptyScene(Node):
    def exec(self):
      ana_scene = AnaScene(blender_scene=bpy.data.scenes["Scene"])
      return {"Scene": ana_scene}

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:

import bpy
from anatools.lib.node import Node
from anatools.lib.ana_object import AnaObject

class AddTruck(Node):
    def exec(self):
        ana_scene = self.inputs["Scene"][0] # get the AnaScene as input
        truck = AnaObject(object_type="truck")
        truck.load(blender_file="path-to-file/truck.blend")
        ana_scene.add_object(my_truck)
        return {"Scene": ana_scene}

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.

from anatools.lib.node import Node

class MoveObject(Node):
    def exec(self):
        # Inputs are an AnaObject and the x,y,z coordinates where it will be moved
        obj = self.inputs["Object"][0]
        x = float(self.inputs["X"][0])
        y = float(self.inputs["Y"][0])
        z = float(self.inputs["Z"][0])
        obj.root.location = [x, y, z]

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.

import bpy
from anatools.lib.ana_object import AnaObject
from anatools.lib.node import Node

class SuzanneObject(AnaObject):
    def load(self, **kwargs):
        # create the Blender object
        bpy.ops.mesh.primitive_monkey_add(
            size=2, enter_editmode=False,
            align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
        )
        # set the root pointer
        self.root = bpy.context.object
        # create the collection and set its data block pointer
        self.collection = bpy.data.collections.new(self.object_type)
        # link the object to the collection
        self.collection.objects.link(self.root)
        # set the loaded flag
        self.loaded = True
        
class AddSuzanne(Node):
    def exec(self):
        ana_scene = self.inputs["Scene"][0] # get the AnaScene as input
        suz = SuzanneObject(object_type="Suzanne")
        suz.load()
        ana_scene.add_object(suz)
        return {"Scene": ana_scene}

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.

from anatools.lib.node import Node

class MoveObject(Node):
    def exec(self):
        # input the AnaObject
        obj = self.inputs["Object"][0]
        # input the x,y,z coordinates where it will be moved
        x = float(self.inputs["X"][0])
        y = float(self.inputs["Y"][0])
        z = float(self.inputs["Z"][0])
        obj.root.location = [x, y, z]
        return {"Object": obj}

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.

  1. The Tank node creates an ObjectGenerator of type “Tank” and passes it to the RustModifier and the DentModifier.

  2. The Bradley node creates an ObjectGenerator of type “Bradley” and passes it to the RustModifier and DentModifier.

  3. 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.

  4. 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.

  5. 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.

  6. The PlaceObjectsRandom node creates a “Branch” ObjectModifier and sets the two subtrees passed to it as children. This completes the generator/modifier tree.

  7. 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:

from anatools.lib.node import Node
from anatools.lib.generator import get_blendfile_generator

class TankGenerator(Node):
    def exec(self):
        generator = get_blendfile_generator("satrgb", AnaObject, "Tank")
        return {"object_generator": generator}

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:

from anatools.lib.node import Node
from anatools.lib.generators import ObjectModifier

class ScaleModifier(Node):
    def exec(self):
        # takes one or more object generators as input
        children = self.inputs["object_generator"]
        scale = float(self.inputs["scale"][0])

        # add modifier to the generator tree
        generator = ObjectModifier(
            method="scale",
            children=children,
            scale=scale)
        return {"object_generator": generator}

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.

from anatools.lib.ana_object import AnaObject

class Jeep(AnaObject):
    def scale(self, scale):
        self.root.scale[0] = scale
        self.root.scale[1] = scale
        self.root.scale[2] = scale

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:

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

‘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