Skip to content

ShaderNodes

Bases: Tree

Source code in core/shadernodes.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class ShaderNodes(Tree):
    def __init__(self, 
            tree_name        : str,
            *,
            fake_user        : bool = False, 
            is_group         : bool = False, 
            prefix           : Optional[str] = None,
            replace_material : bool = False, 
            color_tag        : str = 'NONE',
            ):
        """ > ShaderNodes

        Parameters
        ----------
        tree_name : str
            Shader Nodes name

        fake_user : bool, optional
            set fake_user flag default=False.

        is_group : bool, optional
            tree is a group default=False.

        prefix : str, optional
            name prefix default=None.

        replace_material : bool, optional
            replace material if already exists default=False.

        color_tag : str, default='NONE'
            group or modifier color_tag attribute (groups only)
        """
        super().__init__(
            tree_name, 
            tree_type='ShaderNodeTree', 
            fake_user=fake_user, 
            is_group=is_group, 
            prefix=prefix, 
            replace_material=replace_material,
            color_tag = color_tag,
            )

    # =============================================================================================================================
    # Input Node

    @property
    def input_node(self) -> Node:
        if not self._is_group:
            raise NodeError(f"ShaderNodes '{self._material.name}' is not a group, it doesn't have an input node")
        return super().input_node

    # ====================================================================================================
    # Tree output

    @property
    def output_node(self) -> Node:
        if self._is_group:
            return super().output_node

        for node in self._nodes.values():
            if node._bnode.bl_idname ==  'ShaderNodeOutputMaterial':
                return node

        return Node('ShaderNodeOutputMaterial')

    def get_output_node(self, target: str = 'ALL') -> Node:
        """ Output node

        Standard behavior if Group or within a zone
        """

        # ---------------------------------------------------------------------------
        # Group or within a zone
        # ---------------------------------------------------------------------------

        if self._is_group:
            return super().get_output_node()

        if len(self._output_stack):
            return self._output_stack[-1]

        # ---------------------------------------------------------------------------
        # Output to shader outpout
        # ---------------------------------------------------------------------------

        for node in self._nodes.values():
            if node._bnode.bl_idname == 'ShaderNodeOutputMaterial' and node._bnode.target == target:
                return node
        node = Node('ShaderNodeOutputMaterial')
        node._bnode.target = target
        return node

    def set_surface(self, value: Any, target: str ='ALL') -> None:
        node = self.get_output_node(target=target)
        node.set_input_socket('Surface', value)

    def set_volume(self, value: Any, target: str ='ALL') -> None:
        node = self.get_output_node(target=target)
        node.set_input_socket('Volume', value)

    def set_displacement(self, value: Any, target: str ='ALL') -> None:
        node = self.get_output_node(target=target)
        node.set_input_socket('Displacement', value)

    def set_thickness(self, value: Any, target: str ='ALL') -> None:
        node = self.get_output_node(target=target)
        node.set_input_socket('Thickness', value)

    @property
    def surface(self) -> None:
        raise NodeError(f"Material 'surface' is write only")

    @surface.setter
    def surface(self, value: Any) -> None:
        if isinstance(value, tuple):
            self.set_surface(value[0], target=value[1])
        else:
            self.set_surface(value)

    @property
    def volume(self) -> None:
        raise NodeError(f"Material 'volume' is write only")

    @volume.setter
    def volume(self, value: Any) -> None:
        if isinstance(value, tuple):
            self.set_volume(value[0], target=value[1])
        else:
            self.set_volume(value)

    @property
    def displacement(self) -> None:
        raise NodeError(f"Material 'displacement' is write only")

    @displacement.setter
    def displacement(self, value: Any) -> None:
        if isinstance(value, tuple):
            self.set_displacement(value[0], target=value[1])
        else:
            self.set_displacement(value)

    @property
    def thickness(self) -> None:
        raise NodeError(f"Material 'thickness' is write only")

    @thickness.setter
    def thickness(self, value: Any) -> None:
        if isinstance(value, tuple):
            self.set_thickness(value[0], target=value[1])
        else:
            self.set_thickness(value)

    def aov_output(self, name: Optional[str] =None, color: Optional[Any] =None, value: Optional[Any] =None) -> None:
        node = Node('AOV Output', {'Color': color, 'Value': value}, aov_name=name)

__init__(tree_name, *, fake_user=False, is_group=False, prefix=None, replace_material=False, color_tag='NONE')

ShaderNodes

Parameters:

Name Type Description Default
tree_name str

Shader Nodes name

required
fake_user bool

set fake_user flag default=False.

False
is_group bool

tree is a group default=False.

False
prefix str

name prefix default=None.

None
replace_material bool

replace material if already exists default=False.

False
color_tag str

group or modifier color_tag attribute (groups only)

'NONE'
Source code in core/shadernodes.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def __init__(self, 
        tree_name        : str,
        *,
        fake_user        : bool = False, 
        is_group         : bool = False, 
        prefix           : Optional[str] = None,
        replace_material : bool = False, 
        color_tag        : str = 'NONE',
        ):
    """ > ShaderNodes

    Parameters
    ----------
    tree_name : str
        Shader Nodes name

    fake_user : bool, optional
        set fake_user flag default=False.

    is_group : bool, optional
        tree is a group default=False.

    prefix : str, optional
        name prefix default=None.

    replace_material : bool, optional
        replace material if already exists default=False.

    color_tag : str, default='NONE'
        group or modifier color_tag attribute (groups only)
    """
    super().__init__(
        tree_name, 
        tree_type='ShaderNodeTree', 
        fake_user=fake_user, 
        is_group=is_group, 
        prefix=prefix, 
        replace_material=replace_material,
        color_tag = color_tag,
        )

add_method(target_class, func_name=None, self_attr=None, ret_class=None, **fixed)

Add a method calling the Group.

Parameters:

Name Type Description Default
target_class type

class to add the method to

required
func_name str

name of the method to create (snae case version of group name if None) default=None.

None
self_attr str

self name attribute name default=None.

None
ret_class type

class to use to transtype the output socket default=None.

None
fixed dict

fixed values for sockets

{}
Source code in core/treeclass.py
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
def add_method(self, 
    target_class    : type, 
    func_name       : str = None,
    self_attr       : str = None, 
    ret_class       : type = None, 
    **fixed):
    """ Add a method calling the Group.

    Parameters
    ----------
    target_class : type
        class to add the method to

    func_name : str, optional
        name of the method to create (snae case version of group name if None) default=None.

    self_attr : str, optional
        self name attribute name default=None.

    ret_class : type, optional
        class to use to transtype the output socket default=None.

    fixed : dict
        fixed values for sockets        

    """
    from .nodeclass import Group

    group_name = self._btree.name
    if self._prefix != "":
        group_name = group_name[len(self._prefix) + 1:]

    Group.add_method(group_name, target_class,
        func_name       = func_name,
        self_attr       = self_attr, 
        ret_class       = ret_class, 
        prefix          = self._prefix,
        **fixed)

arrange()

Arrange the nodes in the editor.

Try to arrange properly the nodes from left to right.

This method is called when the Tree is poped from the stack.

Returns:

Type Description
None
Source code in core/treeclass.py
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
def arrange(self):
    """ > Arrange the nodes in the editor.

    Try to arrange properly the nodes from left to right.

    This method is called when the Tree is poped from the stack.

    Returns
    -------
    None
    """
    if self._use_arrange:
        treearrange.arrange(self._btree)

clear(keep_nodes=True)

Clear the content of the Tree.

Remove all the nodes in the Tree.

Source code in core/treeclass.py
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
def clear(self, keep_nodes: bool = True):
    """ Clear the content of the Tree.

    Remove all the nodes in the Tree.
    """
    self._nodes.clear()

    # Keep menu switch nodes to preserver input links
    # They will be deleted at the end

    if keep_nodes:
        del_nodes = []
        self._kept_nodes = []
        for bnode in self._btree.nodes:
            if bnode.bl_idname in ['GeometryNodeMenuSwitch', 'NodeGroupInput']:
                self._kept_nodes.append(bnode)
            else:
                del_nodes.append(bnode)

        for bnode in del_nodes:
            self._btree.nodes.remove(bnode)

    else:
        self._btree.links.clear()
        self._btree.nodes.clear()

create_input_socket(bl_idname, name, panel='', **props)

Create a new input socket.

This is an input socket of the zone, hence an output socket of the input node.

Parameters:

Name Type Description Default
bl_idname str

Socket bl_idname

required
name str

Socket name

required
panel str

Panel to place the socket in

''
**props dict

Properties specific to interface socket

{}

Returns:

Type Description
Socket
Source code in core/treeclass.py
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
def create_input_socket(self, bl_idname, name, panel="", **props):
    """ Create a new input socket.

    This is an **input socket** of the zone, hence an **output socket** of the input node.

    Parameters
    ----------
    bl_idname : str
        Socket bl_idname

    name : str
        Socket name

    panel : str, optional
        Panel to place the socket in

    **props : dict, optional
        Properties specific to interface socket


    Returns
    -------
    Socket
    """        
    input_node = self.get_input_node()
    socket = input_node.create_socket('OUTPUT', bl_idname, name, panel=panel, **props)
    def_val = props.get('default_value')
    if def_val is not None:
        try:
            utils.get_bsocket(socket).default_value = def_val
        except Exception as e:
            print(f"WARNING Tree.create_input_socket: {str(e)}")

    return socket

current_tree() staticmethod

Get the Current Tree.

Returns None if no tree is currently open

Returns:

Type Description
Tree

current tree or None

Source code in core/treeclass.py
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
@staticmethod
def current_tree():
    """ > Get the Current Tree.

    Returns None if no tree is currently open

    Returns
    -------
    Tree
        current tree or None

    """
    if len(Tree.TREE_STACK):
        return Tree.TREE_STACK[-1]
    else:
        return None

get_output_node(target='ALL')

Output node

Standard behavior if Group or within a zone

Source code in core/shadernodes.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def get_output_node(self, target: str = 'ALL') -> Node:
    """ Output node

    Standard behavior if Group or within a zone
    """

    # ---------------------------------------------------------------------------
    # Group or within a zone
    # ---------------------------------------------------------------------------

    if self._is_group:
        return super().get_output_node()

    if len(self._output_stack):
        return self._output_stack[-1]

    # ---------------------------------------------------------------------------
    # Output to shader outpout
    # ---------------------------------------------------------------------------

    for node in self._nodes.values():
        if node._bnode.bl_idname == 'ShaderNodeOutputMaterial' and node._bnode.target == target:
            return node
    node = Node('ShaderNodeOutputMaterial')
    node._bnode.target = target
    return node

get_signature(include=None, exclude=[], enabled_only=True, with_sockets=False)

Build the closure signature of the tree.

The closure signature is the tuple made of the output signature of the input node and the input signature of the output node

Returns:

Type Description
Signature
Source code in core/treeclass.py
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
def get_signature(self, include: list = None, exclude: list = [], enabled_only=True, with_sockets: bool = False):
    """ Build the closure signature of the tree.

    The closure signature is the tuple made of the output signature of the input node
    and the input signature of the output node

    Returns
    -------
    Signature
    """
    return Signature(
        self.input_node.get_signature(
            include         = include,
            exclude         = exclude,
            enabled_only    = enabled_only,
            with_sockets    = with_sockets).outputs,
        self.output_node.get_signature(
            enabled_only    = enabled_only,
            with_sockets    = with_sockets).inputs)

is_geonodes() classmethod

Current Tree is Geometry Nodes.

Returns:

Type Description
True if Tree is GeoNodes, False otherwise
Source code in core/treeclass.py
874
875
876
877
878
879
880
881
882
@classmethod
def is_geonodes(cls):
    """ > Current Tree is Geometry Nodes.

    Returns
    -------
    True if Tree is GeoNodes, False otherwise
    """
    return cls.current_tree()._btree.bl_idname == 'GeometryNodeTree'

is_shader() classmethod

Current Tree is Shader Nodes.

Returns:

Type Description
True if Tree is ShaderNodes, False otherwise
Source code in core/treeclass.py
884
885
886
887
888
889
890
891
892
@classmethod
def is_shader(cls):
    """ > Current Tree is Shader Nodes.

    Returns
    -------
    True if Tree is ShaderNodes, False otherwise
    """
    return cls.current_tree()._btree.bl_idname == 'ShaderNodeTree'

Create a link between two sockets.

Parameters:

Name Type Description Default
out_socket socket

a node output socket

required
in_socket socket

input socket from another node

required

Returns:

Type Description
Link
Source code in core/treeclass.py
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
def link(self, out_socket, in_socket, handle_dynamic_sockets=False):
    """ > Create a link between two sockets.

    Parameters
    ----------
    out_socket : socket
        a node output socket

    in_socket : socket
        input socket from another node


    Returns
    -------
    Link
    """

    #if hasattr(out_socket, '_bsocket'):
    if '_bsocket' in dir(out_socket):
        out_socket = out_socket._bsocket

    #if hasattr(in_socket, '_bsocket'):
    if '_bsocket' in dir(in_socket):
        in_socket = in_socket._bsocket

    link = self._btree.links.new(out_socket, in_socket, handle_dynamic_sockets=handle_dynamic_sockets)

    utils.check_link(link)
    utils.check_zones(self._btree)

    return link

pop(error=False)

Remove this tree from the stack

Important

This methods shouldn't be called directly, better use a with context block.

Raises:

Type Description
NodeError

if this tree is not the current one

``` python
with Tree("My Name"):

pass

```
Source code in core/treeclass.py
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
def pop(self, error=False):
    """ > Remove this tree from the stack

    !!! important
        This methods shouldn't be called directly, better use a **with** context block.

    Raises
    ------
    NodeError
        if this tree is not the current one


    ``` python
    with Tree("My Name"):
        pass
    ```
    """
    # ---------------------------------------------------------------------------
    # Ignore changes
    # ---------------------------------------------------------------------------

    if self.ignore:
        print(f"CHANGES IGNORED : Material '{self._btree.name}' already exists. Use 'replace_material=True' to overwite it.")

        assert self._btree.bl_idname == 'ShaderNodeTree'
        assert self._material is not None

        bpy.data.materials.remove(self._material)
        self._btree = None

        return

    if self._loaded:
        return

    # ---------------------------------------------------------------------------
    # Finalize
    # ---------------------------------------------------------------------------

    # Remove kept nodes
    for bnode in self._kept_nodes:
        self._btree.nodes.remove(bnode)

    # Adjust menu inputs
    if self._has_tree and not error:
        for node in self._nodes.values():
            if '_default_menu' in node.__slots__:
                node._tree_is_completed(self._mod_vals)

        innode = self.input_node
        for bsock in innode._bnode.outputs:
            if bsock.type != 'MENU' or not bsock.is_linked:
                continue
            isock = self._interface.by_identifier(bsock.identifier)
            enums = list(utils.get_enums(isock, 'default_value'))
            if not len(enums):
                continue

            for mod in self.get_modifiers().values():

                cur_val = mod.get(bsock.identifier)
                if cur_val is not None and (cur_val < 2 or cur_val > len(enums) + 2):
                    mod[bsock.identifier] = enums.index(isock.default_value) + 2


    # Check dead ends
    warnings = 0
    if not error:
        warnings = self.check_warnings()

    # Remove from stack
    tree = Tree.TREE_STACK.pop()
    if tree != self:
        raise RuntimeError(f"Error in tree stack management")

    # Empty the interface bin
    if (not error) and self._interface is not None:
        self._interface.empty_bin()

    # Last node in error
    if error and len(self._nodes):
        for node in reversed(self._nodes.values()):
            if node._bnode.bl_idname in ['NodeGroupOutput', 'NodeGroupInput']:
                continue
            utils.set_node_error(node._bnode)
            break

    # Arrange the nodes
    self.arrange()

    # Stats
    if self._btree.bl_idname == 'ShaderNodeTree' and self._material is not None:
        name = f"Material '{self._material.name}'"
    else:
        name = f"Tree '{self._btree.name}'"

    print(f"{name} built: {self._str_stats}, warnings: {warnings}")

    Tree._total_nodes += len(self._btree.nodes)
    Tree._total_links += len(self._btree.links)      

push()

Make this tree zone the current one

Important

This methods shouldn't be called directly, better use a with context block.

with Tree("My Name"):
    pass
Source code in core/treeclass.py
582
583
584
585
586
587
588
589
590
591
592
593
594
def push(self):
    """ > Make this tree zone the current one

    !!! important
        This methods shouldn't be called directly, better use a **with** context block.

    ``` python
    with Tree("My Name"):
        pass
    ```
    """
    Tree.TREE_STACK.append(self)
    return self

register_node(node)

Register a new Node.

This method is called by the Node class constructor. Never call directly.

Registered Nodes are stored in the dictionary : _nodes = bpy.types.Node.name -> Node

Parameters:

Name Type Description Default
node Node

the newly created Node to register

required

Returns:

Type Description
Node

the passed argument

Source code in core/treeclass.py
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
def register_node(self, node):
    """ Register a new Node.

    This method is called by the Node class constructor. Never call directly.

    Registered Nodes are stored in the dictionary : _nodes = bpy.types.Node.name -> Node

    Parameters
    ----------
    node : Node
        the newly created Node to register


    Returns
    -------
    Node
        the passed argument

    """

    self._nodes[node._bnode.name] = node
    if len(self._layouts):
        self._layouts[-1].include_node(node)

    node._stack = NodeError.stack_lines()

    if self._input_node is None and node._bnode.bl_idname == 'NodeGroupInput':
        self._input_node = node

    if self._output_node is None and node._bnode.bl_idname == 'NodeGroupOutput':
        self._output_node = node

    return node

remove_groups(names=None, prefix=None, geonodes=True, shadernodes=True) staticmethod

Remove Groups created by GeoNodes.

Important

This method can only remove groups created by GeoNodes.

Parameters:

Name Type Description Default
names str or list of strs

name of the node groups to remove (all if None)

None
prefix str

name prefix for the groups to delete

None
geonodes bool

remove geometry nodes groups

True
shadernodes bool

remove shader nodes groups

True

Returns:

Type Description
None
Source code in core/treeclass.py
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
@staticmethod
def remove_groups(names=None, prefix=None, geonodes=True, shadernodes=True):
    """ > Remove Groups created by GeoNodes.

    !!! important
         This method can only remove groups created by **GeoNodes**.

    Parameters
    ----------
    names : str or list of strs, optional
        name of the node groups to remove (all if None)

    prefix : str, optional
        name prefix for the groups to delete

    geonodes : bool, default=True
        remove geometry nodes groups

    shadernodes : bool, default=True
        remove shader nodes groups


    Returns
    -------
    None
    """

    groups = []
    if prefix is None:
        prefix_ = ""
    else:
        prefix_ = prefix + ' '

    tree_types = []
    if geonodes:
        tree_types.append('GeometryNodeTree')
    if shadernodes:
        tree_types.append('ShaderNodeTree')

    for group in bpy.data.node_groups:
        if group.bl_idname not in tree_types:
            continue

        if not group.description.startswith('GEONODES'):
            continue

        if prefix is not None and group.name[:len(prefix_)] != prefix_:
            continue

        if names is None:
            pass
        elif isinstance(names, str):
            if group.name != prefix_ + names:
                continue
        elif isinstance(names, list):
            ok = False
            for name in names:
                if group.name == prefix_ + name:
                    ok = True
                    break
            if not ok:
                continue
        else:
            raise Exception(f"Argument names '{names}' is not valid: None, str or list of strs expected")

        groups.append(group)

    print("Remove node groups")
    for group in groups:
        print(' -', group.name)
        bpy.data.node_groups.remove(group)

    print(f"{len(groups)} node groups removed")