21
$\begingroup$

Are there any mods that allow Math nodes to accept more than just two inputs? I regularly find myself wanting to, say, get the Maximum value from multiple inputs and end up having to chain multiple Math nodes together. While this does work fine, it looks really messy and is quite inconvenient and cumbersome. It would be really nice to have a Math node that could work like the Group Input/Output nodes where a blank socket is used to add any additional inputs/outputs as required.

Obviously better than a mod would be native Blender functionality for this. Where would I post such a feature request if there isn't one already?

EDIT : The main question here is whether such a feature exists already - if it doesn't then I'll look into raising it as a feature request - but I'd like to determine that this feature doesn't already exist in some form first.

$\endgroup$
7
  • $\begingroup$ @cegaton See the edit. It is no longer a possible duplicate. $\endgroup$
    – JakeD
    Nov 18, 2016 at 18:29
  • $\begingroup$ Quick Answer: Learn Python and the Blender's API, and look into bpy.types.NodeCustomGroup. $\endgroup$
    – Secrop
    Jan 20, 2018 at 1:34
  • $\begingroup$ @Secrop I’d be very interested to see a working solution. I’ve dabbled with pynodes (see blender.stackexchange.com/a/75507/29586 for example) and I don’t think it would work - not efficiently, anyway - or do you mean something different? $\endgroup$ Jan 20, 2018 at 8:29
  • $\begingroup$ @Rich Sedman, that link offers a wrong way to use python nodes with builtin systems (textures, compositor, cycles). Deriving from the Node class will not let Cycles to dig inside because the node is registered as Custom, and there's no code to interpret the node. The workaround, as I stated above, is to use the NodeCustomGroup class which is similar to NodeGroups. This way you can build and change the internal nodetree in runtime and Cycles will still interpret this as a NodeGroup. I can write a proper answer to this later, thought there's much to cover. $\endgroup$
    – Secrop
    Jan 20, 2018 at 13:01
  • $\begingroup$ @Secrop Thanks for the explanation. Dynamically built nodegroups sounds very interesting. If you can provide a suitable working solution I’ll happily stump up a bounty. $\endgroup$ Jan 20, 2018 at 13:47

3 Answers 3

30
+250
$\begingroup$

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

default dynamic maths node

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 :

internal nodes

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 :

animated

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.

$\endgroup$
19
  • $\begingroup$ This looks potentially promising but please could you explain how I can install the example you have provided? If I run or register the code I don't see any difference in the node editor. Do I need a particular version of Blender (I'm using 2.76b). $\endgroup$ Jan 21, 2018 at 0:27
  • 1
    $\begingroup$ Added the code for creating a new category in the Node Editor. $\endgroup$
    – Secrop
    Jan 21, 2018 at 8:42
  • $\begingroup$ I used your example code to produce a Dynamic Maths node that does exactly what I required. I've edited the answer to include the code and examples of it in operation. Thanks! $\endgroup$ Jan 22, 2018 at 13:18
  • 1
    $\begingroup$ @Hitokage I presume this would be possible by changing the code to work with CompositorNode objects rather than ShaderNodes but would require some reworking and testing. I'd suggest this being asked as a new question - linking to this one - so that someone can provide a separate answer. I'll try and answer it myself when I get the chance. I've been working on a more generic maths node add-on that allows the relationship to be simply types into a text field as an equation - I'll consider expanding that for compositor nodes at some point if it's feasible also. $\endgroup$ Oct 22, 2018 at 11:36
  • 1
    $\begingroup$ @Hitokage, it might be pretty much the same thing... the only difference is that you need to use a CompositorNodeTree, and change the Poll function to be true when the node tree in the editor is Compositor.. I have to check if the rendering the compositor treat the CustomNodeGroup as a NodeGroup. $\endgroup$
    – Secrop
    Oct 22, 2018 at 22:11
4
$\begingroup$

I'm adding this additional answer (but will leave @Secrop's answer as the accepted one) to share a solution that provides this functionality and more. I've developed an add-on that can be downloaded from https://github.com/baldingwizard/Blender-Addons/wiki/Dynamic-Maths-Expression which allows an arbitrary expression to be entered and the nodes automatically generated. Editing the expression automatically re-generates the nodes, changing the function of the group as desired.

Once installed, the add-on provides a 'Maths Expression' option in the Add menu - for example, for a 5-input 'add' node simply enter a+b+c+d+e and the node group is automatically generated for you. To add extra inputs or remove inputs just click the Edit button within the group and change it to the desired expression (eg, a+b+c+d+e+f+g+h+i+j for a 10 input 'add').

maths expression

Various functions are provided so for a node group that takes the maximum of 6 inputs you could simply enter max(a,b,c,d,e,f).

$\endgroup$
7
  • $\begingroup$ That's nice.. It could also fit very well as a Python node, since you can have string inputs in the node's interface. I've thought about writing a node with this functionality, but never had the time nor the urge to do it. $\endgroup$
    – Secrop
    Apr 25, 2019 at 5:45
  • $\begingroup$ @Secrop Thanks. I did start by using string input on a custom node but I found that Eevee (at least, the version that was available at the time) wasn't accepting the outputs from the custom node (but switching to Cycles worked find) - so transferred the custom node inside the group to hold the parameters and provide the interface for the 'Edit'. This has the added benefit of the generated group being fully functional (except for the 'edit') even without the add-on. $\endgroup$ Apr 25, 2019 at 6:51
  • $\begingroup$ I'm not sure how to ask this without intruding or going off-topic for this exchange... Could this, or the loop node, be used to simplify convolution,? In particular, avoid explicit repetition of the texture node? Perhaps I should post a separate question? $\endgroup$
    – Robin Betts
    Apr 25, 2019 at 7:22
  • $\begingroup$ @RobinBetts I'm not sure what you mean... This could certainly be used as a quick way to build the maths node groups - but this doesn't provide a 'loop' node. I have managed to use this to create a hexagon shader that can provide an output for 'distance to edge' and am looking to extend it to add noise to produce a voronoi-like output - which should then remove the need for the edge detection in that particular example, so that in itself would simplify it greatly. $\endgroup$ Apr 25, 2019 at 8:05
  • $\begingroup$ @RobinBetts in this kind of matrix logic, you could simplify the way to build the internal nodetree... but it won't make the nodetree less complex than the answer in the link you mentioned. $\endgroup$
    – Secrop
    Apr 25, 2019 at 8:11
1
$\begingroup$

The obvious solution you named it allready yourself, is to use groups filled with math nodes, i dont see a reason for doing future request, since even if there where such a node, it still had to be filled with (gues what...) math nodes based logic. (cuda needs to be able to understand it). Cuda doesnt understand advanced languages such as python

As currently cycles nodes are based upon cuda math, its not possible to replace that with a python logic and have it run on GPU, the most close alternative exception to that is to use a OSL node. Though OSL scripting wont compile on GPU either.

$\endgroup$
1
  • 4
    $\begingroup$ Secrop’s solution with NodeCustomGroups addresses this by providing a convenient node interface for automatically generating it as an ‘internal’ (non-visible and non-editable) node group. This provides the convenience of being able to control the number of inputs and operation without having to manually manipulte the groups - while still being GPU/Cuda friendly since it’s all implemented as stardard nodes. $\endgroup$ Jan 25, 2018 at 16:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .