Tutorial 10 - Simple Data Node in Python¶
The simple data node creates one input attribute and one output attribute of each of the simple types, where “simple” refers to data types that have a single component and are not arrays. (e.g. “float” is simple, “float[3]” is not, nor is “float[]”). See also Tutorial 2 - Simple Data Node for a similar example in C++.
Automatic Python Node Registration¶
By implementing the standard Carbonite extension interfact in Python, OmniGraph will know to scan your Python import path for to recursively scan the directory, import all Python node files it finds, and register those nodes. It will also deregister those nodes when the extension shuts down. Here is an example of the directory structure for an extension with a single node in it. (For extensions that have a premake5.lua build script this will be in the build directory. For standalone extensions it is in your source directory.)
omni.my.extension/
omni/
my/
extension/
nodes/
OgnMyNode.ogn
OgnMyNode.py
OgnTutorialSimpleDataPy.ogn¶
The ogn file shows the implementation of a node named “omni.graph.tutorials.SimpleDataPy”, which has one input and one output attribute of each simple type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | { "SimpleDataPy" : { "version": 1, "categories": "tutorials", "description": [ "This is a tutorial node. It creates both an input and output attribute of every simple ", "supported data type. The values are modified in a simple way so that the compute modifies values. ", "It is the same as node omni.graph.tutorials.SimpleData, except it is implemented in Python instead of C++." ], "language": "python", "$iconOverride": [ "If for some reason the default name or colors for the icon are inappropriate you can override like this.", "This gives an alternative icon path, a shape color of Green, a border color of Red, and a background", "color of half-opaque Blue. If you just want to override the icon path then instead of a dictionary you", "can just use the string path, as in \"icon\": \"Tutorial10Icon.svg\"." ], "icon": { "path": "Tutorial10Icon.svg", "color": "#FF00FF00", "borderColor": [255, 0, 0, 255], "backgroundColor": "#7FFF0000" }, "metadata": { "uiName": "Tutorial Python Node: Attributes With Simple Data" }, "inputs": { "a_bool": { "type": "bool", "metadata": { "uiName": "Simple Boolean Input" }, "description": ["This is an attribute of type boolean"], "default": true, "$optional": "When this is set there is no checking for validity before calling compute()", "optional": true }, "a_half": { "type": "half", "description": ["This is an attribute of type 16 bit float"], "$comment": "0 is used as the decimal portion due to reduced precision of this type", "default": 0.0 }, "a_int": { "type": "int", "description": ["This is an attribute of type 32 bit integer"], "default": 0 }, "a_int64": { "type": "int64", "description": ["This is an attribute of type 64 bit integer"], "default": 0 }, "a_float": { "type": "float", "description": ["This is an attribute of type 32 bit floating point"], "default": 0 }, "a_double": { "type": "double", "description": ["This is an attribute of type 64 bit floating point"], "default": 0 }, "a_path": { "type": "path", "description": ["This is an attribute of type path"], "default": "" }, "a_string": { "type": "string", "description": ["This is an attribute of type string"], "default": "helloString" }, "a_token": { "type": "token", "description": ["This is an attribute of type interned string with fast comparison and hashing"], "default": "helloToken" }, "a_objectId": { "type": "objectId", "description": ["This is an attribute of type objectId"], "default": 0 }, "a_uchar": { "type": "uchar", "description": ["This is an attribute of type unsigned 8 bit integer"], "default": 0 }, "a_uint": { "type": "uint", "description": ["This is an attribute of type unsigned 32 bit integer"], "default": 0 }, "a_uint64": { "type": "uint64", "description": ["This is an attribute of type unsigned 64 bit integer"], "default": 0 }, "a_constant_input": { "type": "int", "description": ["This is an input attribute whose value can be set but can only be connected as a source."], "metadata": { "outputOnly": "1" } } }, "outputs": { "a_bool": { "type": "bool", "description": ["This is a computed attribute of type boolean"] }, "a_half": { "type": "half", "description": ["This is a computed attribute of type 16 bit float"] }, "a_int": { "type": "int", "description": ["This is a computed attribute of type 32 bit integer"] }, "a_int64": { "type": "int64", "description": ["This is a computed attribute of type 64 bit integer"] }, "a_float": { "type": "float", "description": ["This is a computed attribute of type 32 bit floating point"] }, "a_double": { "type": "double", "description": ["This is a computed attribute of type 64 bit floating point"] }, "a_path": { "type": "path", "description": ["This is a computed attribute of type path"], "default": "/Child" }, "a_string": { "type": "string", "description": ["This is a computed attribute of type string"], "default": "This string is empty" }, "a_token": { "type": "token", "description": ["This is a computed attribute of type interned string with fast comparison and hashing"] }, "a_objectId": { "type": "objectId", "description": ["This is a computed attribute of type objectId"] }, "a_uchar": { "type": "uchar", "description": ["This is a computed attribute of type unsigned 8 bit integer"] }, "a_uint": { "type": "uint", "description": ["This is a computed attribute of type unsigned 32 bit integer"] }, "a_uint64": { "type": "uint64", "description": ["This is a computed attribute of type unsigned 64 bit integer"] }, "a_nodeTypeUiName": { "type": "string", "description": "Computed attribute containing the UI name of this node type" }, "a_a_boolUiName": { "type": "string", "description": "Computed attribute containing the UI name of input a_bool" } }, "tests": [ { "$comment": ["Each test has a description of the test and a set of input and output values. ", "The test runs by setting all of the specified inputs on the node to their values, ", "running the compute, then comparing the computed outputs against the values ", "specified in the test. Only the inputs in the list are set; others will use their ", "default values. Only the outputs in the list are checked; others are ignored."], "description": "Check that false becomes true", "inputs:a_bool": false, "outputs:a_bool": true }, { "$comment": "This is a more verbose format of test data that provides a different grouping of values", "description": "Check that true becomes false", "inputs": { "a_bool": true }, "outputs": { "a_bool": false, "a_a_boolUiName": "Simple Boolean Input", "a_nodeTypeUiName": "Tutorial Python Node: Attributes With Simple Data" } }, { "$comment": "Make sure the path append does the right thing", "inputs:a_path": "/World/Domination", "outputs:a_path": "/World/Domination/Child" }, { "$comment": "Even though these computations are all independent they can be checked in a single test.", "description": "Check all attributes against their computed values", "inputs:a_bool": false, "outputs:a_bool": true, "inputs:a_double": 1.1, "outputs:a_double": 2.1, "inputs:a_float": 3.3, "outputs:a_float": 4.3, "inputs:a_half": 5.0, "outputs:a_half": 6.0, "inputs:a_int": 7, "outputs:a_int": 8, "inputs:a_int64": 9, "outputs:a_int64": 10, "inputs:a_token": "helloToken", "outputs:a_token": "worldToken", "inputs:a_string": "helloString", "outputs:a_string": "worldString", "inputs:a_objectId": 10, "outputs:a_objectId": 11, "inputs:a_uchar": 11, "outputs:a_uchar": 12, "inputs:a_uint": 13, "outputs:a_uint": 14, "inputs:a_uint64": 15, "outputs:a_uint64": 16 } ] } } |
OgnTutorialSimpleDataPy.py¶
The py file contains the implementation of the compute method, which modifies each of the inputs in a simple way to create outputs that have different values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | """ Implementation of the Python node accessing all of the simple data types. This class exercises access to the DataModel through the generated database class for all simple data types. It implements the same algorithm as the C++ node OgnTutorialSimpleData.cpp """ import omni.graph.tools.ogn as ogn class OgnTutorialSimpleDataPy: """Exercise the simple data types through a Python OmniGraph node""" @staticmethod def compute(db) -> bool: """Perform a trivial computation on all of the simple data types to make testing easy""" # Inside the database the contained object "inputs" holds the data references for all input attributes and the # contained object "outputs" holds the data references for all output attributes. # Each of the attribute accessors are named for the name of the attribute, with the ":" replaced by "_". # The colon is used in USD as a convention for creating namespaces so it's safe to replace it without # modifying the meaning. The "inputs:" and "outputs:" prefixes in the generated attributes are matched # by the container names. # For example attribute "inputs:translate:x" would be accessible as "db.inputs.translate_x" and attribute # "outputs:matrix" would be accessible as "db.outputs.matrix". # The "compute" of this method modifies each attribute in a subtle way so that a test can be written # to verify the operation of the node. See the .ogn file for a description of tests. db.outputs.a_bool = not db.inputs.a_bool db.outputs.a_half = 1.0 + db.inputs.a_half db.outputs.a_int = 1 + db.inputs.a_int db.outputs.a_int64 = 1 + db.inputs.a_int64 db.outputs.a_double = 1.0 + db.inputs.a_double db.outputs.a_float = 1.0 + db.inputs.a_float db.outputs.a_uchar = 1 + db.inputs.a_uchar db.outputs.a_uint = 1 + db.inputs.a_uint db.outputs.a_uint64 = 1 + db.inputs.a_uint64 db.outputs.a_string = db.inputs.a_string.replace("hello", "world") db.outputs.a_objectId = 1 + db.inputs.a_objectId # The token interface is made available in the database as well, for convenience. # By calling "db.token" you can look up the token ID of a given string. if db.inputs.a_token == "helloToken": db.outputs.a_token = "worldToken" # Path just gets a new child named "Child". # In the implementation the string is manipulated directly, as it does not care if the SdfPath is valid or # not. If you want to manipulate it using the pxr.Sdf.Path API this is how you could do it: # # from pxr import Sdf # input_path Sdf.Path(db.inputs.a_path) # if input_path.IsValid(): # db.outputs.a_path() = input_path.AppendChild("/Child").GetString(); # db.outputs.a_path = db.inputs.a_path + "/Child" # To access the metadata you have to go out to the ABI, though the hardcoded metadata tags are in the # OmniGraph Python namespace assert db.node.get_attribute("inputs:a_bool").get_metadata(ogn.MetadataKeys.UI_NAME) == "Simple Boolean Input" # You can also use the database interface to get the same data db.outputs.a_nodeTypeUiName = db.get_metadata(ogn.MetadataKeys.UI_NAME) db.outputs.a_a_boolUiName = db.get_metadata(ogn.MetadataKeys.UI_NAME, db.attributes.inputs.a_bool) return True |
Note how the attribute values are available through the OgnTutorialSimpleDataPyDatabase
class. The generated
interface creates access methods for every attribute, named for the attribute itself. They are all implemented as Python
properties, where inputs only have get methods and outputs have both get and set methods.
Pythonic Attribute Data¶
Three subsections are creating in the generated database class. The main section implements the node type ABI methods and uses introspection on your node class to call any versions of the ABI methods you have defined (see later tutorials for examples of how this works).
The other two subsections are classes containing attribute access properties for inputs and outputs. For naming
consistency the class members are called inputs and outputs. For example, you can access the value of the input
attribute named foo by referencing OgnTutorialSimpleDataPyDatabase.inputs.foo
.
For the curious, here is an example of how the boolean attribute access is set up on the simple tutorial node:
class OgnTutorialSimpleDataPyDatabase:
class InputAttributes:
def __init__(self, context_helper, node):
self.context_helper = context_helper
self.node = node
# A lock is used to prevent inputs from inadvertently being modified during a compute.
# This is equivalent to the "const" approach used in C++.
self.setting_locked = False
# Caching the Attribute object lets future data access run faster
self._a_bool = node.get_attribute('inputs:a_bool')
@property
def a_bool(self):
return self.context_helper.get_attr_value(self._a_bool)
@a_bool.setter
def a_bool(self, value):
if self.setting_locked:
raise AttributeError('inputs:a_bool')
self.context_helper.set_attr_value(value, self._a_bool)
class OutputAttributes:
def __init__(self, context_helper, node):
self.context_helper = context_helper
self.node = node
self._a_bool = node.get_attribute('outputs:a_bool')
@property
def a_bool(self):
return self.context_helper.get_attr_value(self._a_bool)
@a_bool.setter
def a_bool(self, value):
self.context_helper.set_attr_value(value, self._a_bool)
def __init__(self, context, node):
self.context = context
self.node = node
self.context_helper = ContextHelper(context)
self.inputs = OgnTutorialSimpleDataPyDatabase.InputAttributes(self.context_helper, node)
self.outputs = OgnTutorialSimpleDataPyDatabase.OutputAttributes(self.context_helper, node)
Pythonic Attribute Access¶
In the USD file the attribute names are automatically namespaced as inputs:FOO or outputs:BAR. In the Python interface the colon is illegal so the contained classes above are used to make use of the dot-separated equivalent, as inputs.FOO or outputs.BAR.
While the underlying data types are stored in their exact form there is conversion when they are passed back to Python as Python has a more limited set of data types, though they all have compatible ranges. For this class, these are the types the properties provide:
Database Property |
Returned Type |
---|---|
inputs.a_bool |
bool |
inputs.a_half |
float |
inputs.a_int |
int |
inputs.a_int64 |
int |
inputs.a_float |
float |
inputs.a_double |
float |
inputs.a_token |
str |
outputs.a_bool |
bool |
outputs.a_half |
float |
outputs.a_int |
int |
outputs.a_int64 |
int |
outputs.a_float |
float |
outputs.a_double |
float |
outputs.a_token |
str |
The data returned are all references to the real data in the FlatCache, our managed memory store, pointed to the correct location at evaluation time.
Python Helpers¶
A few helpers are provided in the database class definition to help make coding with it more natural.
Python logging¶
Two helper functions are providing in the database class to help provide more information when the compute method of a node has failed. Two methods are provided, both taking a formatted string describing the problem.
log_error(message)
is used when the compute has run into some inconsistent or unexpected data, such as two
input arrays that are supposed to have the same size but do not, like the normals and vertexes on a mesh.
log_warning(message)
can be used when the compute has hit an unusual case but can still provide a consistent
output for it, for example the deformation of an empty mesh would result in an empty mesh and a warning since that is
not a typical use for the node.
Direct Pythonic ABI Access¶
All of the generated database classes provide access to the underlying INodeType ABI for those rare situations where you want to access the ABI directly. There are two members provided, which correspond to the objects passed in to the ABI compute method.
There is the graph evaluation context member, context: Py_GraphContext
, for accessing the underlying OmniGraph
evaluation context and its interface.
There is also the OmniGraph node member, node: Py_Node
, for accessing the underlying OmniGraph node
object and its interface.