Custom shader node socket

Is it possible to create a dropdown shader socket using Python?

What’s a dropdown shader socket?


Is it possible to create a new custom or extend any socket at all?

I need an “enum” socket type

That doesn’t seem to exist.

Adding a custom interface for a nodegroup is only possible by extending a ShaderNodeCustomGroup class.
With it, you have access to the draw_buttons() call, and you can draw enum properties to the node’s layout.

sounds good, would you please show me an example?

This is just a small example… a special care is needed for dealing with generating/instancing/copying/freeing the private node_tree… but that depends from case to case.

import bpy
from bpy.types import ShaderNodeCustomGroup
from nodeitems_utils import NodeItem, register_node_categories, unregister_node_categories
from nodeitems_builtins import ShaderNodeCategory

class ShaderNodeColorDropDown(ShaderNodeCustomGroup):
    bl_name='ShaderNodeColorDropDown'
    bl_label='Color DropDown'
    bl_icon='NONE'
    
    cl_items=(('0', 'Black', 'BLACK'),
        ('1', 'White', 'WHITE'),
        ('2', 'Red', 'RED'),
        ('3', 'Green', 'GREEN'),
        ('4', 'Blue', 'BLUE'))

    def color_update(self, context):
        out = self.node_tree.nodes['RGB'].outputs[0]
        if self.Colors == '0':
            out.default_value = (0.0,0.0,0.0,1.0)
        elif self.Colors == '1':
            out.default_value = (1.0,1.0,1.0,1.0)
        elif self.Colors == '2':
            out.default_value = (1.0,0.0,0.0,1.0)        
        elif self.Colors == '3':
            out.default_value = (0.0,1.0,0.0,1.0)        
        elif self.Colors == '4':
            out.default_value = (0.0,0.0,1.0,1.0) 
        
    Colors: bpy.props.EnumProperty(default = '1', items = cl_items, name = "Colors_Enum", update = color_update)
    
    def init(self, context):
        #create the private node_group... just for illustration purposes!
        ntname = '.' + self.bl_name + 'nodetree' #blender hides Nodegroups with name '.*'
        self.node_tree = bpy.data.node_groups.new(ntname, 'ShaderNodeTree')
        self.node_tree.nodes.new('NodeGroupInput')
        o = self.node_tree.nodes.new('NodeGroupOutput')
        s = self.node_tree.nodes.new('ShaderNodeRGB')
        self.node_tree.outputs.new('NodeSocketColor', "Color")
        self.node_tree.links.new(s.outputs[0], o.inputs[0])
        s.outputs[0].default_value = (1.0,1.0,1.0,1.0)   
    
    def draw_buttons(self, context, layout):
        layout.prop(self, 'Colors', text='')

thisMenu = [ShaderNodeCategory("MyNodes", "My Nodes", items=[NodeItem("ShaderNodeColorDropDown")])]


def register():
    bpy.utils.register_class(ShaderNodeColorDropDown)
    register_node_categories("MY_NODES", thisMenu)
    
def unregister():
    #unregister_node_categories(thisMenu)
    bpy.utils.unregister_class(ShaderNodeColorDropDown)

if __name__ == "__main__":
    register()

This is very nice, but actually it dosn’t work in a group node:

import bpy
from bpy.types import ShaderNodeCustomGroup
from nodeitems_utils import NodeItem, register_node_categories, unregister_node_categories
from nodeitems_builtins import ShaderNodeCategory

class ShaderNodeColorDropDown(ShaderNodeCustomGroup):
    bl_name='ShaderNodeColorDropDown'
    bl_label='Color DropDown'
    bl_icon='NONE'
    
    cl_items=(('0', 'Black', 'BLACK'),
        ('1', 'White', 'WHITE'),
        ('2', 'Red', 'RED'),
        ('3', 'Green', 'GREEN'),
        ('4', 'Blue', 'BLUE'))

    def color_update(self, context):
        out = self.node_tree.nodes['RGB'].outputs[0]
        if self.Colors == '0':
            out.default_value = (0.0,0.0,0.0,1.0)
        elif self.Colors == '1':
            out.default_value = (1.0,1.0,1.0,1.0)
        elif self.Colors == '2':
            out.default_value = (1.0,0.0,0.0,1.0)        
        elif self.Colors == '3':
            out.default_value = (0.0,1.0,0.0,1.0)        
        elif self.Colors == '4':
            out.default_value = (0.0,0.0,1.0,1.0) 
        
    Colors: bpy.props.EnumProperty(default = '1', items = cl_items, name = "Colors_Enum", update = color_update)
    
    def init(self, context):
        #create the private node_group... just for illustration purposes!
        ntname = '.' + self.bl_name + 'nodetree' #blender hides Nodegroups with name '.*'
        self.node_tree = bpy.data.node_groups.new(ntname, 'ShaderNodeTree')
        self.node_tree.nodes.new('NodeGroupInput')
        o = self.node_tree.nodes.new('NodeGroupOutput')
        s = self.node_tree.nodes.new('ShaderNodeRGB')
        self.node_tree.outputs.new('NodeSocketColor', "Color")
        self.node_tree.links.new(s.outputs[0], o.inputs[0])
        s.outputs[0].default_value = (1.0,1.0,1.0,1.0)   
    
    def draw_buttons(self, context, layout):
        layout.prop(self, 'Colors', text='')

thisMenu = [ShaderNodeCategory("MyNodes", "My Nodes", items=[NodeItem("ShaderNodeColorDropDown")])]


def register():
    bpy.utils.register_class(ShaderNodeColorDropDown)
    register_node_categories("MY_NODES", thisMenu)
    
def unregister():
    #unregister_node_categories(thisMenu)
    bpy.utils.unregister_class(ShaderNodeColorDropDown)

if __name__ == "__main__":
    register()


bpy.ops.mesh.primitive_monkey_add()
obj = bpy.context.object
mat = bpy.data.materials.new(name = 'Material')
mat.use_nodes = True
obj.active_material = mat

node_group = bpy.data.node_groups.new('node_group', 'ShaderNodeTree')
# works:
node_group.inputs.new('NodeSocketIntFactor','test 1')
# not working:
# node_group.inputs.new('ShaderNodeColorDropDown','test 2')
node_group_container = mat.node_tree.nodes.new('ShaderNodeGroup')
node_group_container.location = (-500,0)
node_group_container.width = 300
node_group_container.node_tree = node_group

A ShaderNodeCustomGroup is the same as a ShaderNodeGroup. They are just a visual interface for something (a node_group!).
They both reference a ShaderNodeTree that has a GroupInput and a GroupOutput (node_groups).
Both follow the same ‘draw function’:

  • [icon, label]
  • [outputsockets from self.node_tree.outputs]
  • [layout*] #has draw_buttons()
  • [inputsockets from self.node_tree.inputs]

The ShaderNodeGroup has just one layout.prop in the draw_buttons() and it’s just a dynamic enum of all nodegroups compatible with the current NodeTreeType stored in bpy.data.node_groups. Once chosen, it will become the self.node_tree of that instance.
In a ShaderNodeCustomGroup you have the same thing… well, at least a node_tree property (that might be initially empty), but there’s no graphic interface to change it. The draw_buttons() is not showing anything. You are in charge!
You can still add a dynamic enum for existent nodegroups as in the ShaderNodeGroup if you like (see here).
And you can still add more options, expose Colorramp, CurveMaps, etc, directly from some Datablock with those elements, in your node’s GUI.
It’s up to you. :wink:

(and of course: you can change the node_tree as you like!! :nerd_face:)

1 Like

Thank you so much! it helps me a lot!
I will discover it more…

I just checked you code and I couldn’t realize how we can add custom options to a ShaderNodeGroup, would you please show me an example?
My goal is to have an EDITABLE group node with CUSTOM OPTIONS.

you need to describe exactly what kind of ‘CUSTOM OPTIONS’ do you really want for ‘EDITABLE’ node_groups, and what would those options change in the referenced node_tree…
Because, if your plan is to add some control to the vanilla ShaderNodeGroup, it may be better to add that functionality to the NodeEditor instead.

So does this have an input socket?

ShaderNodeGroup:
We can visually edit node tree inside it (Editable). But we don’t have enum socket (Custom option).

ShaderNodeCustomGroup
Is not editable, but we can have enum socket.

My goal is to have enum socket in ShaderNodeGroup.

Just to clarify, because using the wrong terminology is a step to chaos…
There’s no such thing as an Enum Socket (at least in ShaderNodeTrees)!
A Socket is an input/output that can be connected to other sockets.
The DropDown is not a Socket, its a Layout.Prop that references an Enum Property.

If you were using a CustomNodeTree, you can create a Socket that draws as a DropDown, so long your NodeTree logic has an understanding of the values being passed to/from the socket. But in ShaderNodeTree that’s impossible, because the Engines (Cycles and Eevee) won’t understand what the new socket represents, and they will fail to compile the shader.

In this view, adding Enum Properties to a ShaderNodeGroup needs to be translated to something that the engine can understand. In my example, changing the ‘Colors’ Enum changes the color of the RGB node inside the ‘.ShaderNodeColorDropDownnodetree’. And this works because the engines understand what a NodeSocketColor represents.

In the code you posted:
node_group.inputs.new('ShaderNodeColorDropDown','test 2')
you’re trying to add a ShaderNodeCustomGroup to a NodeTree’s inputs… that doesn’t work.
A ShaderNodeCustomGroup is not a Socket. It’s a Node (with a Node_Tree inside, just like the ShaderNodeGroup)!

But if you change the ShaderNodeCustomGroup init(), and move this logic inside the init call (replacing the code that is there):

node_group = bpy.data.node_groups.new('node_group', 'ShaderNodeTree')
node_group.inputs.new('NodeSocketIntFactor','test 1')

and then set the node_group as the Node’s node_tree:
self.node_tree = node_group
it will work!

Now you can add this node into the material:
mat.node_tree.nodes.new("ShaderNodeColorDropDown")
thought the node will have just one input, and the Colors Enum will fail to reference the now inexistent RGB node inside this new Node_Tree.

And by the way… The Tab key can only be used with the NodeGroup node. It’s still possible to edit the NodeTree if you load that NodeTree into a ShaderNodeGroup, but you’ll loose your own layout (note that node_trees with a name started with a period are hidden, so renaming them will let you use the NodeTree in a normal NodeGroup.
By default, you shouldn’t edit NodeTrees from NodeCustomGroups… To make this happening, you can create an Operator to push the NodeTree into the editor, and override the Tab key bindings… (not really desirable, but I use a similar approach in my newest version of the LoopNode, where the Tab pushes the step_nodetree into the Editor as if it was the nodetree inside).

So, summarising this:

  • A Node_Tree is a Graph of interconnected nodes.
  • A Node_Tree with GroupInput and GroupOutput is “considered” a NodeGroup (but this is not the node itself, as it’s still a node_tree!)
  • A Node has InputSockets, OutputSockets, a Layout and an Ext_Layout (side menu).
  • A NodeGroup(the node!) is a Node that reads the inputs and outputs of a node_tree and draws it into its own interface. It’s Layout is just a DynamicEnumProp to choose an existent Node_Tree.
  • A NodeCustomGroup(also a node) does the same for the inputs and outputs of a Node_Tree, but let’s you have your own Layouts, plus listen to Editor changes, etc.
1 Like

Thank You!
Let’s skip the Tab editing, and talk about the layout capabilities. How many controls we have for layout?

Ohh Yeah!! … the layout is the same as any other class that has a drawing call:
https://docs.blender.org/api/current/bpy.types.UILayout.html

1 Like

I just have another question that you may know why “NodeSocketBool” dosn’t work as expected?

Cycles and Eevee don’t use booleans, so they don’t understand the NodeSocketBool.
Instead they use the NodeSocketFloat where (0 is False and 1 is True).
If you use a NodeSocketBool, you need to do your own translation to something the render engine can understand, in the same similar way as the EnumProperty.
For example, add a ValueNode to the NodeTree, and in the change the its defaiult value when the socket value changes (socket_value_update()).

The problem is that you have to have your own logic if you connect anything to the socket, since that socket won’t be use by the shader compiler. That means you need to recursively check the source of that link until you get the original value.

1 Like