I won't cover the complete topic about Custom Nodes, because there's too much that can be done.
This is just a small compilation of things to know for writing custom nodes that work in Cycles. Knowledge of Python and Blender's API is required, as I won't dig deep into them.
In Blender's API we have a NodeCustomGroup class that is similar to the NodeGroup class. Before going into the NodeCustomGroup, let us explain a bit how the nodetrees work:
A Nodetree has mainly 4 collections. The nodes collection, the links collection, the inputs collection and the outputs collection. The NodeGroup node encapsulates a nodetree in order to turn it into a visible node in the editor. It scans the nodetree interface, and creates sockets for using the nodetree. This process is automatic and there's nothing we can do with it.
The good news is that we have the NodeCustomGroup. From this class, we can build the interface of a nodetree and even build/rebuild the same nodetree dynamically, have custom properties, respond to events, and have access to the Blender's context.
For the NodeCustomGroup, we'll need to create our nodetree (which must be a ShaderNodeTree, if we're going to make a Cycles' node). Then we need to populate this tree with one NodeGroupInput and one NodeGroupOutput, as they are used to create the nodetree interface. After we build our nodetree as we do for nodegroups: add nodes, input sockets, output sockets, and links to connect everything.
After that you need to define how the Custom Node is supposed to work...For example, in this case, would include functions that check if an input Virtual Socket is linked, and if so, make the changes to the nodetree, create a new Float Socket, remove that original link and add a new link to the created socket, etc. Actions for checking if some socket is disconnect might also be written.
When the node is completed one must register it, which is the same process as for any class in Blender.
For adding it to the Add Menu and the Add Panel, you should to use the nodeitems_utils module (and for a better integration, one should also use the ShaderNewNodeCategory from nodeitems_builtins). This should be used with caution as I know some well known addons that use these modules in ways that disable the interface of other addons (i.e. Blend4Web).
For the last, I leave here the code from a node from my Github page. It's small enough to paste it here, and it covers dynamic nodetree and interface management. Some examples of this node working here.
# Node Authors: Secrop
# Node Description: Node for creating loops over some nodegroup
# version: (0,0,3)
import bpy
# for blender2.80 we should derive the class from bpy.types.ShaderNodeCustomGroup
class ShaderNodeLoop(bpy.types.NodeCustomGroup):
bl_name='ShaderNodeLoop'
bl_label='Loop Node'
def nodegroups(self, context):
nt=context.space_data.edit_tree
list=[('None','None','None')]
for nd in nt.nodes:
if nd.type=='GROUP':
ng=nd.node_tree
if ng.inputs.get('iterator'):
list.append((ng.name, ng.name, ng.name))
return list
def __nodeinterface_setup__(self):
self.node_tree.outputs.clear()
self.node_tree.inputs.clear()
if self.step_nodegroup=='None':
return
for input in bpy.data.node_groups[self.step_nodegroup].inputs:
if not input.name=='iterator':
self.node_tree.nodes['Group Input'].outputs.new(input.bl_socket_idname, input.name)
self.node_tree.inputs.new(input.bl_socket_idname, input.name)
for output in bpy.data.node_groups[self.step_nodegroup].outputs:
self.node_tree.nodes['Group Output'].inputs.new(output.bl_socket_idname, output.name)
self.node_tree.outputs.new(output.bl_socket_idname, output.name)
def __nodetree_setup__(self):
self.node_tree.links.clear()
for node in self.node_tree.nodes:
if not node.name in ['Group Input','Group Output']:
self.node_tree.nodes.remove(node)
if self.step_nodegroup=='None':
return
previousnode=self.node_tree.nodes['Group Input']
for iter in range(self.iterations):
curnode=self.node_tree.nodes.new('ShaderNodeGroup')
curnode.node_tree=bpy.data.node_groups[self.step_nodegroup]
curnode.inputs['iterator'].default_value=iter
for input in curnode.inputs:
poutput=previousnode.outputs.get(input.name)
if poutput:
self.node_tree.links.new(poutput, input)
if iter==self.iterations-1:
for input in self.node_tree.nodes['Group Output'].inputs:
poutput=curnode.outputs.get(input.name)
if poutput:
self.node_tree.links.new(poutput, input)
else:
previousnode=curnode
def update_nt(self, context):
self.__nodeinterface_setup__()
self.__nodetree_setup__()
def update_it(self, context):
self.__nodetree_setup__()
# for blender 2.80, the following properties should be annotations
step_nodegroup=bpy.props.EnumProperty(name="step_nodegroup", items=nodegroups, update=update_nt)
iterations=bpy.props.IntProperty(name="iterations", min=1, max=63, default=8, update=update_it)
def init(self, context):
self.node_tree=bpy.data.node_groups.new('.' + self.bl_name, 'ShaderNodeTree')
self.node_tree.nodes.new('NodeGroupInput')
self.node_tree.nodes.new('NodeGroupOutput')
def draw_buttons(self, context, layout):
row=layout.row()
row.alert=(self.step_nodegroup=='None')
row.prop(self, 'step_nodegroup', text='')
row=layout.row()
row.prop(self, 'iterations', text='iterations')
def copy(self, node):
self.node_tree=node.node_tree.copy()
def free(self):
bpy.data.node_groups.remove(self.node_tree, do_unlink=True)
from nodeitems_utils import NodeItem, register_node_categories, unregister_node_categories
# in blender2.80 use ShaderNodeCategory
from nodeitems_builtins import ShaderNewNodeCategory
newcatlist = [ShaderNewNodeCategory("SH_NEW_CUSTOM", "Custom Nodes", items=[NodeItem("ShaderNodeLoop"),]),]
def register():
bpy.utils.register_class(ShaderNodeLoop)
register_node_categories("CUSTOM_NODES", newcatlist)
def unregister():
unregister_node_categories("CUSTOM_NODES")
bpy.utils.unregister_class(ShaderNodeLoop)
if __name__ == "__main__":
register()
And here's a small video of another node I wrote with a dynamic interface:
https://www.youtube.com/watch?v=11Xd-p3HE48
EDIT
The above techniques can be used to produce a dynamic maths node with a variable number of inputs.
For example, paste the following code into a Text Editor window, name the text block 'mathsDynamic.py', tick the Register checkbox (so it's executed automatically when the blend file is loaded) and click 'Run Script'.
# Node Authors: Secrop, Rich Sedman
# Node Description: Dynamic Maths node with variable number of inputs
# version: (0,0,1)
import bpy
# for blender2.80 we should derive the class from bpy.types.ShaderNodeCustomGroup
class MathsDynamic(bpy.types.NodeCustomGroup):
bl_name='MathsDynamic'
bl_label='Dynamic Maths'
# Return the list of valid operators
def operators(self, context):
nt=context.space_data.edit_tree
list=[('ADD','Add','Addition'),('SUBTRACT', 'Subtract', 'Subtraction'), ('MULTIPLY', 'Multiply', 'Multiplication'), ('DIVIDE', 'Divide', 'Division'), ('MAXIMUM','Max','Maximum'),('MINIMUM','Min','Minimum') ]
return list
# Manage the node's sockets, adding additional ones when needed and removing those no longer required
def __nodeinterface_setup__(self):
# No operators --> no inpout or output sockets
if self.inputSockets < 1:
self.node_tree.inputs.clear()
self.node_tree.outputs.clear()
return
# Look for input sockets that are no longer required and remove them
for i in range(len(self.node_tree.inputs),0,-1):
if i > self.inputSockets:
self.node_tree.inputs.remove(self.node_tree.inputs[-1])
# Add any additional input sockets that are now required
for i in range(0, self.inputSockets):
if i > len(self.node_tree.inputs):
self.node_tree.inputs.new("NodeSocketFloat", "Value")
# Add the output socket
if len(self.node_tree.outputs) < 1:
self.node_tree.outputs.new("NodeSocketFloat", "Value")
# Manage the internal nodes to perform the chained operation - clear all the nodes and build from scratch each time.
def __nodetree_setup__(self):
# Remove all links and all nodes that aren't Group Input or Group Output
self.node_tree.links.clear()
for node in self.node_tree.nodes:
if not node.name in ['Group Input','Group Output']:
self.node_tree.nodes.remove(node)
# Start from Group Input and add nodes as required, chaining each new one to the previous level and the next input
groupinput = self.node_tree.nodes['Group Input']
previousnode = groupinput
if self.inputSockets <= 1:
# Special case <= 1 input --> link input directly to output
self.node_tree.links.new(previousnode.outputs[0],self.node_tree.nodes['Group Output'].inputs[0])
else:
# Create one node for each input socket > 1
for i in range(1, self.inputSockets):
newnode = self.node_tree.nodes.new('ShaderNodeMath')
newnode.operation = self.selectOperator
self.node_tree.links.new(previousnode.outputs[0],newnode.inputs[0])
self.node_tree.links.new(groupinput.outputs[i],newnode.inputs[1])
previousnode = newnode
# Connect the last one to the output
self.node_tree.links.new(previousnode.outputs[0],self.node_tree.nodes['Group Output'].inputs[0])
# Chosen operator has changed - update the nodes and links
def update_operator(self, context):
self.__nodeinterface_setup__()
self.__nodetree_setup__()
# Number of inputs has changed - update the nodes and links
def update_inpSockets(self, context):
self.__nodeinterface_setup__()
self.__nodetree_setup__()
# The node properties - Operator (Add, Subtract, etc.) and number of input sockets
# for blender 2.80, the following properties should be annotations
selectOperator=bpy.props.EnumProperty(name="selectOperator", items=operators, update=update_operator)
inputSockets=bpy.props.IntProperty(name="Inputs", min=0, max=63, default=0, update=update_inpSockets)
# Setup the node - setup the node tree and add the group Input and Output nodes
def init(self, context):
self.node_tree=bpy.data.node_groups.new('.' + self.bl_name, 'ShaderNodeTree')
self.node_tree.nodes.new('NodeGroupInput')
self.node_tree.nodes.new('NodeGroupOutput')
# Draw the node components
def draw_buttons(self, context, layout):
row=layout.row()
row.alert=(self.selectOperator=='None')
row.prop(self, 'selectOperator', text='')
row=layout.row()
row.prop(self, 'inputSockets', text='Inputs')
# Copy
def copy(self, node):
self.node_tree=node.node_tree.copy()
# Free (when node is deleted)
def free(self):
bpy.data.node_groups.remove(self.node_tree, do_unlink=True)
from nodeitems_utils import NodeItem, register_node_categories, unregister_node_categories
# in blender2.80 use ShaderNodeCategory
from nodeitems_builtins import ShaderNewNodeCategory
def register():
bpy.utils.register_class(MathsDynamic)
newcatlist = [ShaderNewNodeCategory("SH_NEW_CUSTOM", "Custom Nodes", items=[NodeItem("MathsDynamic"),]),]
register_node_categories("CUSTOM_NODES", newcatlist)
def unregister():
unregister_node_categories("CUSTOM_NODES")
bpy.utils.unregister_class(MathsDynamic)
# Attempt to unregister our class (in case it's already been registered before) and register it.
try :
unregister()
except:
pass
register()
In the Node Editor you should now have an additional Custom Nodes menu in the Add menu and within that a new node, 'Dynamic Maths'.

The new node provides a drop-down to select the maths operation (Add, Subtract, etc.) and a control to specify the required number of inputs. The node is essentially a Node Group that cannot be manually edited and looks like a normal node. The nodes inside the group are rebuilt automatically each time the node parameters (the operation and the number of inputs) are changed.
For example, setting the operation to 'Add' and setting the number of inputs to 3 will produce the following internal node structure :

Note that the nodes within the group are automatically generated. Changing the node properties changes in inner workings of the group to provide the desired function with the specified number of inputs :

The node can be used anywhere a standard 'Maths' node is used and the performance should be similar to using multiple cascaded Maths nodes to create the same function (since it's essentially just a NodeGroup). It currently supports Add, Subtract, Multiply, Divide, Max and Min functions, with up to 63 inputs.
EDIT II
I'd like to add that there was some changes in the API for Blender 2.80 that affect this answer. Instead of NodeCustomGroup
, we now have two new classes we can use:
-ShaderNodeCustomGroup
, which is now compatible with Eevee, Cycles and the Viewport material shading.
-CompositorNodeCustomGroup
, which allows having custom nodes for Compositor Node_Trees.