Custom Cycles nodes with python?

I always thought that custom nodes were possible only with OSL, but now i found this http://www.blendernation.com/2016/06/21/blender-market-color-palette-node-lets-quickly-recolor-render/ that shows I was wrong. Right?
The addon node in the video supports switches, buttons, dropdown menu, color widget, preset loading/saving.
So, how is this possible?

The custom group class allows you to do some interesting stuff in a node. It has to have an internal nodetree, and it cannot change once you start rendering, but it lets you alter the nodetree when in the editor…

I’ve a small compilation of functions/workflows/behaviours of the customgroup that I researched when doing my MetalBSDF… I’ll post it when i get back to my workstation.

So, here’s the basic for a custom node:

declaration (NodeCustomGroup has already the pool defined for cycles so there’s no need for the pool() function):

class NodeName(bpy.types.NodeCustomGroup):
    bl_name = 'Name in bpy.types' 
    bl_label = 'Label for the node'
    bl_icon = 'icon for the node'

The NodeCustomGroup as a node_tree attribute, from which the node takes the interface and the logic.
you can make a new node_tree, or use one you already have (a node group)

    def init(self, context):
        self.node_tree = bpy.data.node_groups.get('name of the nodegroup')
        # or
        self.node_tree = bpy.data.node_groups.new('name of the new nodegroup', 'ShaderNodeTree')

in case you create a new nodegroup, you need to populate it, since what the node_groups.new() gives you is totally empty.
this means:
adding nodes to it

newnode=self.node_tree.nodes.new('Node to be added')

setting nodes properties

newnode.property = somevalue

adding inputs and outputs

inputnode=self.node_tree.nodes.new('NodeGroupInput')
outputnode=self.node_tree.nodes.new('NodeGroupOutput')

making the links

self.node_tree.links.new(self.node_tree.nodes('fromnode').outputs('fromsocket), self.node_tree.nodes('tonode').inputs('tosocket))

there are two update functions that are called when there are changes made on the editor: update() and socket_value_update(). The first will be called at changes on the editor, and the second only on socket changes. Here you can add functionalities to your node, like changing the internal node_tree whenever the user changes something in the interface.
If in your interface you have other inputs, you can define the update function for these (the declaration should be in the class definition):

CustomBool=bpy.props.BoolProperty(name="MyCustomBool", default=False, <b>update=FunctionNameToBeCalledWhenChanged</b>)

and to show it in the interface, just add it to the draw_buttons():

def draw_buttons(self, context, layout):
    layout.prop(self, "CustomBool", text="CheckBox")

the interface will be a mix of the node_tree interface, and the one you define with the draw_buttons() and draw_buttons_ext().
it will first draw the output of the node_tree, then your layout defined by the draw_buttons(), and at the bottom it will draw the input of the node_tree. The draw_buttons_ext() is used to draw the node interface on the properties panel, and if not defined, the draw_buttons() will be used.

other usefull functions are the free() and the copy(), one is called when the node is deleted, and the other when is being created as a copy of another node instance.

A few notes: Node_groups are referenced, so if two instances of your CustomNode are using the same node_group, changes made from one node will be reflected in the other. If needed, each node should reference its own NodeGroup, and clear it at the free() function.

To finish, the class must be registered:

bpy.utils.register_class(NodeName)

and one can use it with

bpy.materials['CurrentMaterial'].node_tree.nodes.new('NodeName')

or it can be added to the ‘new node’ menu, thought this requires changing the default menu (check the nodeitems_builtins.py and the nodeitems_utils.py)

For further reading: https://www.blender.org/api/blender_python_api_2_76_9/bpy.types.NodeCustomGroup.html

2 Likes