PyNodes and the Blender Python API
As you probably already know, PyNodes (or Dynamic Nodes) are defined by Python scripts saved as texts in a .blend file. These scripts have access to the Blender Python API, just like normal scripts with or without guis, script links, space handlers, pydriver expressions, etc do.
Most Relevant Modules
The main module is Blender.Node, it contains data required by all pynode scripts. Blender.Mathutils and Blender.Noise should provide useful helper functions, of course.
For the new API being worked on, called bpy, we should have a new interface, possibly something like bpy.nodes, bpy.math, etc.
A PyNode script simply defines a derived Python node class, by inheriting from Blender.Node.node. Besides its __init__ method, where the node input and output sockets are defined, this derived class must have a __call__ method, where the actual work is done. That's all.
from Blender import Node from Blender.Noise import random class RandomNode(Node.Scripted): def __init__(self, sockets): col = Node.Socket('Color', val = 4*[1.0]) sockets.input = [col] sockets.output = [col] def __call__(self): self.output.Color = [x * random() for x in self.input.Color] __node__ = RandomNode
Let's check this piece by piece...
from Blender import Node
All pynodes need access to the node class and data that is in Blender.Node, so this module is always needed.
from Blender.Noise import random
In our trivial example we used the random function from Blender.Noise, too.
The base class we inherit from is Blender.Node.node:
The __init__ method of our derived class is used to define both the input and output sockets of the new node we're creating. It is only called when the pynode is created or reparsed (when users press the pynode's "update" button).
def __init__(self, sockets): col = Node.Socket('Color', val = 4*[1.0]) sockets.input = [col] sockets.output = [col]
The "sockets" parameter
The "sockets" parameter is passed to the __init__ method. It's where we can store the definitions, specifically in:
- sockets.input or its alias: sockets.i
- sockets.output or its alias: sockets.o
Reminder: of course the script author can use another name instead of sockets, since it's only a parameter to a method.
In this example a socket called "Color" of type RGBA (four float values) is defined and added as both input and output. Note that since we pass lists to sockets.input and sockets.output, instead of a dictionary, the user controls the order in which the sockets appear in the final node: [first socket, second socket, etc.].
The Socket object type
Here are the parameters used to create a node socket:
socket = Node.Socket(name, val = 1.0, min = 0.0, max = 1.0)
- name: (string) the name that appears in the socket
- val: the default values for this node. The socket type is automatically inferred from the data passed by the user:
- 1 float: a scalar value
- sequence or vector with 3 floats: a vector
- sequence or vector with 4 floats: a color (RGBA)
- min (float: 0.0), max (float: 1.0): the minimum and maximum values acceptable for the numeric values used.
The __call__ method is called for each pixel being shaded during rendering (for shader pynodes) and for each pixel being composited during compositing (for composite pynodes, not yet implemented). It's here that we define what our pynode does with its data (self.input and self.shi), what values it passes (self.output) to the next node in the pipeline. In this case we multiply each color channel with a random value.
def __call__(self): self.output.Color = map(lambda x: x * random(), self.input.Color)
Example above uses functional programming utilities provided by Python. In the next example no functional programming is used. Both ways have their uses but functional one tends to be more compact.
def __call__(self): ocol = list(self.input.Color) ocol *= random() ocol *= random() ocol *= random() self.output.Color = ocol
All input data is accessed as tuples of values that can be read but not written to.
- Input sockets: self.input or alias: self.i
As defined via sockets.input in the __init__ method. All input nodes we defined there can be accessed here by name or by their indices in the sockets input list. In our example we defined a single input socket, called "Color", which can be accessed as self.input.Color or self.input['Color'] or self.input.
- Shade Input: self.shi or alias: self.s
Besides data from the input sockets we create with the script, we also have access to many other variables used during rendering (for shader pynodes) and during compositing (for compositing pynodes, not yet implemented).
There is a lot of data inside self.shi and we should still add more, but this will require a better understanding of the huge ShadeInput structure used during rendering, to know which variables return meaningful data during rendering.
Right now self.shi exposes the following information, as written in the BlenderDev/PyNodes page (on the right the name of the variables in the ShadeInput C struct):
- shi.color = (fff) # float r, g, b;
- shi.specularColor = (fff) # float specr, specg, specb;
- shi.mirrorColor = (fff) # float mirr, mirg, mirb;
- shi.ambientColor = (fff) # float ambr, ambb, ambg;
- shi.ambient = f # float amb;
- shi.emit = f # float emit;
- shi.pixel = (ii) # int xs, int ys;
- shi.surfaceNormal = (fff) # float facenor;
- shi.viewNormal = (fff) # float vn;
- shi.surfaceViewVector = (fff) # float view;
Texture coordinates will currently work only if you've got a texture assigned to the material, and the proper toggle button set for the texture to get anything useful
- shi.texture = (fff) # float lo
- shi.textureGlobal = (fff) # float gl
- shi.displace = (fff) # float displace
- shi.strand = f # float strand
- shi.tangent = (fff) # float tang
- shi.stress = f # float stress
- shi.surfaceD = ((fff),(fff)) # float dxco, dyco;
- shi.textureD = ((fff),(fff)) # float dxlo, dylo;
- shi.textureGlobalD = ((fff),(fff)) # float dxgl, dygl;
- shi.reflectionD = ((fff),(fff)) # float dxref, dyref;
- shi.normalD = ((fff),(fff)) # float dxno, dyno;
- shi.stickyD = ((fff),(fff)) # float dxsticky, dysticky;
- shi.refractD = ((fff),(fff)) # float dxrefract, dyrefract;
- shi.strandD = (f,f) # float dxstrand, dystrand;
Other variables we can add are listed here: BlenderDev/PyNodes/ShadeInputSuggestions
The API here is not finished either. Some things still open:
- Naming guidelines. The bpy API favors myVarName instead of my_var_name.
- Actual names:
- We should find the better way to group vars. Example: textureGlobal seems better than globalTexture, since the first "keeps together" all texture related vars, while the second joins all global ones.
- Better use "RGB" instead of "Color", which is longer and could mean RGB or RGBA or even other color representations?
- What about those myNameD variables? In the ShadeInput header file they are under a "/* dx/dy OSA coordinates */" comment. Obviously "D" is not meaningful enough, what would be better? "OSA"?
- Output sockets: self.output or alias: self.o
As defined via sockets.output in the __init__ method. All output nodes we defined there can be accessed here by name or by their indices in the sockets output list. In our example we defined a single output socket, called "Color", which can be accessed as self.output.Color or self.output['Color'] or self.output.
This is where we pass the node results out to the next node in the user's setup.
The __node__ variable
Right now Blender uses a simple, naive approach to finding the actual pynode class definition in the script. Basically, it searches for an object of type "type" in the script's global dictionary. Since Sockets are also objects of type "type", they are explicitely ignored in this search. The same applies to the base pynode class Node.Scripted. So, excluding these two, when the code finds an object of type "type" it assumes it is the desired pynode.
For simple cases that should be good enough, but if you define or import more pynodes (objects derived from Node.Scripted) in the script, for example to derive your pynode from another derived one, the naive approach is not enough. For that reason, we added the "__node__" variable, that script writers can use to point to the actual pynode they want Blender to find, initialize and call:
__node__ = RandomNode
Again, this is only necessary if there are multiple objects of type "type" defined in (or imported to) the pynode script. In simple cases like our example above, there's no need to use the "__node__" variable.
Notes for Blender Python API developers
- The interface to create dynamic nodes and assign a text (script) to them is in src/drawnode.c.
- nodes/intern/SHD_nodes/SHD_dynamic.c is the main file where pynodes are initialized, parsed, configured, executed, etc.
- the API side of pynodes is done in python/api2_2x/Node.[ch].
- blenkernel/intern/node.c is where nodes and nodetrees in general are initialized and managed.
- important headers: makesdna/DNA_node_types.h and blenkernel/BKE_node.h. For ShadeInput (self.shi in the __call__ method): render/extern/include/RE_shader_ext.h.
- Add support for more ShadeInput (shi) data.
- Create some nice example scripts and also improve this documentation.
- Implement composite pynodes.
- Add to Blender.Mathutils or bpy.math or ??? the helper functions present in the Renderman(R) Shader API, like smoothstep, spline, noise, etc. etc. With this it should be easy to port many Renderman surface shaders to PyNode scripts.