2
$\begingroup$

I have an Attribute node inside a node group, and for the node group inputs, I have created a NodeSocketString, Using group.inputs.new("NodeSocketString", "Factor") and I want that whenever I enter any String value in that NodeSocket, it should also be updated in the attribute node immediately I tried attribute_node.attribute_name = group.inputs[3].default_value but it is not working as expected this gives the default value set for node socket in the attribute node but what is want is that this should change whenever I change the value there so I also tried to do this using a for loop -

for default_value in group.inputs["Factor"]:
    attribute_node.attribute_name = group.inputs[3].default_value

but this is also not working, this produces an error - TypeError: 'NodeSocketInterfaceString' object is not iterable

I want that if I insert any string value in the Factor input shown in screenshot 1 that value should also be updated in the attribute node (that is present inside this node group) shown in the screenshot 2

Screenshot 1 -

Screenshot 1

I want this value(in the attribute node) to be updated every time when any value is inserted in the group node shown in screenshot-1

Screenshot 2 -

Screenshot2

So is this Possible? and do I have to declare separately a string variable? Can anyone help me with this problem?

This is the Updated Script -

bl_info = {
"name": "Add Test Material",
"author": "Rakesh Choudhary",
"version": (1, 0),
"blender": (2, 83, 0),
"location": "View3D > Sidebar > Test Material Node",
"description": "Click on the 'Test Material' button to add a material to your object.",
"warning": "",
"wiki_url": "",
"category": "3D View"
}

import bpy
from bpy.types import (
    Operator,
    Panel,
)


class TEST_MATERIAL_OT_add_material(Operator):
    bl_idname = "test_material.add_material"
    bl_label = "Add Test Material"
    bl_description = "This button will add a material to your object"

    def execute(self, context):
        self.create_material(context)
        return {'FINISHED'}

    def create_material(self, context):
        test_shader_mat = bpy.data.materials.new("TestMat")
        mesh = context.object.data
        mesh.materials.clear()
        mesh.materials.append(test_shader_mat)
        context.object.active_material.use_nodes = True

        for mat in bpy.data.materials:
            if "TestMat" in mat.name:
                nodes = mat.node_tree.nodes
                for node in nodes:
                    if node.type != 'OUTPUT_MATERIAL':  # skip the material output node as we'll need it later
                        nodes.remove(node)

        # Creating Node Group Test_Material
        group = bpy.data.node_groups.new(type="ShaderNodeTree", name="Test_Material")

        # Creating Group Input
        group.inputs.new("NodeSocketColor", "Diffuse Color")
        group.inputs.new("NodeSocketColor", "Glossy Color")
        group.inputs.new("NodeSocketFloat", "Glossyness")
        group.inputs.new("NodeSocketString", "Factor")
        input_node = group.nodes.new("NodeGroupInput")
        input_node.location = (-800, 0)


        group.inputs[0].default_value = (1, 1, 1, 1)
        group.inputs[1].default_value = (1, 1, 1, 1)
        group.inputs[2].default_value = (0.500)
        group.inputs[3].default_value = "foam"


        # Creating Group Output Node
        group.outputs.new("NodeSocketShader", "Diffuse Color")
        group.outputs.new("NodeSocketShader", "Glossy Color")
        group.outputs.new("NodeSocketShader", "Mix Output")

        output_node = group.nodes.new("NodeGroupOutput")
        output_node.location = (1500, 0)

        # Creating Diffuse Node
        diffuse_node = group.nodes.new(type='ShaderNodeBsdfDiffuse')
        diffuse_node.location = (150, 100)

        # Creating Glossy Node
        glossy_node = group.nodes.new(type='ShaderNodeBsdfGlossy')
        glossy_node.location = (300, 250)

        # Creating Mix Shader Node
        mix_shader_node = group.nodes.new(type='ShaderNodeMixShader')
        mix_shader_node.location = (450, 100)


        #Attribute Node    
        attribute_node = group.nodes.new(type='ShaderNodeAttribute')
        attribute_node.location  = (100, 400)
        attribute_node.attribute_name = group.inputs[3].default_value
    
    

        # Creating Links Between Nodes----------------------------------------------
        group.links.new(diffuse_node.outputs["BSDF"], mix_shader_node.inputs[1])
        group.links.new(glossy_node.outputs["BSDF"], mix_shader_node.inputs[2])
        group.links.new(input_node.outputs["Diffuse Color"], diffuse_node.inputs[0])
        group.links.new(input_node.outputs["Glossy Color"], glossy_node.inputs[0])
        group.links.new(input_node.outputs["Glossyness"], glossy_node.inputs[1])
        group.links.new(output_node.inputs["Diffuse Color"], diffuse_node.outputs[0])
        group.links.new(output_node.inputs["Glossy Color"], glossy_node.outputs[0])
        group.links.new(output_node.inputs["Mix Output"], mix_shader_node.outputs[0])
        group.links.new(attribute_node.outputs["Fac"], mix_shader_node.inputs[0])
    


        # Putting Node Group to the node editor
        tree = bpy.context.object.active_material.node_tree
        group_node = tree.nodes.new("ShaderNodeGroup")
        group_node.node_tree = group
        group_node.location = (-40, 300)
        group_node.use_custom_color = True
        group_node.color = (1, 0.341, 0.034)
        group_node.width = 250

        shader_node_output_material_node = tree.nodes["Material Output"]
        links = tree.links
        links.new(group_node.outputs[0], shader_node_output_material_node.inputs[0])
    
        #Material ends here------------------------------
       

class TEST_MATERIAL_PT_layout_panel(Panel):
    bl_label = "Test Material Node"
    bl_category = "Test Material"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        layout.operator("test_material.add_material", icon='IMPORT')
    
    def get_group_nodes(material):
            if material.use_nodes:
                return [n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)]
            return []


    def set_value(self, value):
        print(self, self.id_data, value)
        if not 'Factor' in self.inputs.keys():
           return None
           self.inputs['Factor'].default_value = value
        name = self.node_tree.name
        bpy.data.node_groups[name].nodes['Attribute'].attribute_name = value


    def get_value(self):
        if not 'Factor' in self.inputs.keys():
            return ""
        return self.inputs["Factor"].default_value

    bpy.types.ShaderNodeGroup.factor = bpy.props.StringProperty(
             get=get_value,
             set=set_value)

    def draw_prop(self, context):
        mat = context.material 
        nodes = get_group_nodes(mat)
        for n in nodes: 
            self.layout.prop(n, "factor")

    bpy.types.NODE_HT_header.prepend(draw_prop)
    



classes = (TEST_MATERIAL_OT_add_material, TEST_MATERIAL_PT_layout_panel)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)



def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()

Follow up -

I tried from the answer I just put that code into the panel class, but that is not working and the error I am getting is -

NameError: name 'get_group_nodes' is not defined

what I did wrong?

Follow up - If I split the script and use init.py to import file, this method does not work I get this error -

Traceback (most recent call last): File "C:\Users\ADMIN\AppData\Roaming\Blender Foundation\Blender\2.83\scripts\addons\Test for init\panel.py", line 19, in draw for n in get_group_nodes(mat): NameError: name 'get_group_nodes' is not defined

location: :-1

location: :-1 Traceback (most recent call last):
File "C:\Users\ADMIN\AppData\Roaming\Blender Foundation\Blender\2.83\scripts\addons\Test for init\panel.py", line 19, in draw for n in get_group_nodes(mat): NameError: name 'get_group_nodes' is not defined

location: :-1

I split this script into 3 files first is init.py, second is material_operator.py and third is panel.py

_ init _.py

bl_info = {
"name": "Add Test Material",
"author": "Rakesh Choudhary",
"version": (1, 0),
"blender": (2, 83, 0),
"location": "View3D > Sidebar > Test Material Node",
"description": "Click on the 'Test Material' button to add a material to your object.",
"warning": "",
"wiki_url": "",
"category": "3D View"
}

import bpy
from bpy.types import (
    Operator,
    Panel,
)
from . import material_operator
from . import panel



def register():
    material_operator.register()
    panel.register()

def unregister():
    material_operator.unregister()
    panel.unregister()

if __name__ == "__main__":
    register()

material operator.py -

import bpy
from bpy.types import (
    Operator,
    Panel,
)
def get_group_nodes(material):
    if material.use_nodes:
        return [n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)]
    return []


def set_value(self, value):
    print(self, self.id_data, value)
    if not 'Factor' in self.inputs.keys():
        return None
    self.inputs['Factor'].default_value = value
    name = self.node_tree.name
    bpy.data.node_groups[name].nodes['Attribute'].attribute_name = value


def get_value(self):
    if not 'Factor' in self.inputs.keys():
        return ""
    return self.inputs["Factor"].default_value

class TEST_MATERIAL_OT_add_material(Operator):
    bl_idname = "test_material.add_material"
    bl_label = "Add Test Material"
    bl_description = "This button will add a material to your object"

    def execute(self, context):
        self.create_material()
        return {'FINISHED'}

    def create_material(self):
        test_shader_mat = bpy.data.materials.new("TestMat")
        mesh = bpy.context.object.data
        mesh.materials.clear()
        mesh.materials.append(test_shader_mat)
        bpy.context.object.active_material.use_nodes = True

        for mat in bpy.data.materials:
            if "TestMat" in mat.name:
                nodes = mat.node_tree.nodes
                for node in nodes:
                    if node.type != 'OUTPUT_MATERIAL':  # skip the material output node as we'll need it later
                        nodes.remove(node)

        # Creating Node Group Test_Material
        group = bpy.data.node_groups.new(type="ShaderNodeTree", name="Test_Material")

        # Creating Group Input
        group.inputs.new("NodeSocketColor", "Diffuse Color")
        group.inputs.new("NodeSocketColor", "Glossy Color")
        group.inputs.new("NodeSocketFloat", "Glossyness")
        group.inputs.new("NodeSocketString", "Factor")
        input_node = group.nodes.new("NodeGroupInput")
        input_node.location = (-800, 0)

        # Creating Group Output Node
        group.outputs.new("NodeSocketShader", "Diffuse Color")
        group.outputs.new("NodeSocketShader", "Glossy Color")
        group.outputs.new("NodeSocketShader", "Mix Output")

        output_node = group.nodes.new("NodeGroupOutput")
        output_node.location = (1500, 0)

        # Creating Diffuse Node
        diffuse_node = group.nodes.new(type='ShaderNodeBsdfDiffuse')
        diffuse_node.location = (150, 100)

        # Creating Glossy Node
        glossy_node = group.nodes.new(type='ShaderNodeBsdfGlossy')
        glossy_node.location = (300, 250)

        # Creating Mix Shader Node
        mix_shader_node = group.nodes.new(type='ShaderNodeMixShader')
        mix_shader_node.location = (450, 100)

        # Creating Attribute Node
        attribute_node = group.nodes.new(type="ShaderNodeAttribute")
        attribute_node.location = (400, -300)
        attribute_node.attribute_name = group.inputs[3].default_value

        # Creating Links Between Nodes----------------------------------------------
        group.links.new(diffuse_node.outputs["BSDF"], mix_shader_node.inputs[1])
        group.links.new(glossy_node.outputs["BSDF"], mix_shader_node.inputs[2])
        group.links.new(input_node.outputs["Diffuse Color"], diffuse_node.inputs[0])
        group.links.new(input_node.outputs["Glossy Color"], glossy_node.inputs[0])
        group.links.new(input_node.outputs["Glossyness"], glossy_node.inputs[1])
        group.links.new(output_node.inputs["Diffuse Color"], diffuse_node.outputs[0])
        group.links.new(output_node.inputs["Glossy Color"], glossy_node.outputs[0])
        group.links.new(output_node.inputs["Mix Output"], mix_shader_node.outputs[0])

    


        # Putting Node Group to the node editor
        tree = bpy.context.object.active_material.node_tree
        group_node = tree.nodes.new("ShaderNodeGroup")
        group_node.node_tree = group
        group_node.location = (-40, 300)
        group_node.use_custom_color = True
        group_node.color = (1, 0.341, 0.034)
        group_node.width = 250

        shader_node_output_material_node = tree.nodes["Material Output"]
        links = tree.links
        links.new(group_node.outputs[0], shader_node_output_material_node.inputs[0])
    
        #Material ends here------------------------------
    


def register():
    bpy.utils.register_class(TEST_MATERIAL_OT_add_material)



def unregister():
    bpy.utils.unregister_class(TEST_MATERIAL_OT_add_material)

if __name__ == "__main__":
    register()

panel.py -

import bpy
from bpy.types import (
    Operator,
    Panel,
)

from material_operator import get_group_nodes

class TEST_MATERIAL_PT_layout_panel(Panel):
    bl_label = "Test Material Node"
    bl_category = "Test Material"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        layout.operator("test_material.add_material", icon='IMPORT')
        mat = context.object.active_material
        if mat:
            for n in get_group_nodes(mat):
                layout.prop(n, "factor")


def register():
    bpy.utils.register_class(TEST_MATERIAL_PT_layout_panel)
    bpy.types.ShaderNodeGroup.factor = bpy.props.StringProperty(
         get=get_value,
         set=set_value)

def unregister():
    bpy.utils.unregister_class(TEST_MATERIAL_PT_layout_panel)

if __name__ == "__main__":
    register()

Can you show me where I am doing wrong?

$\endgroup$
6
  • 2
    $\begingroup$ I'd suggest you upload a minimal working example of your addon. The creation of all those nodes does not seem relevant to your problem. $\endgroup$
    – Leander
    Jun 30, 2020 at 9:37
  • 1
    $\begingroup$ Agree with Leander. Also, can't see any StringProperty declared... Where and how the user can enter a new string? $\endgroup$
    – brockmann
    Jun 30, 2020 at 14:27
  • $\begingroup$ @brockmann I updated my question so that it is easy to understand, I want that whatever string value I feed in the Factor Socket in the group node that value should automatically be updated inside the attribute node present inside the node group. Is it possible? $\endgroup$ Jun 30, 2020 at 15:32
  • $\begingroup$ AFAIK there is no way to call a function for NodeGroupInputs on update: docs.blender.org/api/current/… Not entirely sure though... Too bad that the attribute node doesn't provide any input string socket by default, might be a nice feature request IMHO. You can try msgbus per node: blender.stackexchange.com/questions/150809/… or just add a button to force an update. $\endgroup$
    – brockmann
    Jun 30, 2020 at 15:59
  • $\begingroup$ @brockmann thanks for the update, as you said I can add a button to force an Update can you show me how to do that. $\endgroup$ Jul 1, 2020 at 6:14

1 Answer 1

4
+100
$\begingroup$

Updating a user defined property

enter image description here

Similarly to Get all materials inside an object for controlling mapping nodes with single var

In the proof of concept (ie needs checks and balances to test for particular group nodes) code below:

  • Defines a custom property on a shader node group. As with all properties defined this way, all shadernode groups will have this value.

  • When the custom property is updated it sets the group node input and attribute nodes attribute name to value.

  • Have instead of an update method on the property used a getter and setter by way of example.. An update, as demonstrated in linked, could be used instead.

  • Use an enum property to only be able to choose from a particular set. Gif above uses "froth" by way of example, with no idea if it is a viable option.

  • Example uses shader editor header where context.material is defined. To put the property in panel in 3D view UI would require getting material from, for example context objects active material

Test code:

import bpy

def get_group_nodes(material):
    if material.use_nodes:
        return [n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)]
    return []


def set_value(self, value):
    print(self, self.id_data, value)
    if not 'Factor' in self.inputs.keys():
        return None
    self.inputs['Factor'].default_value = value
    name = self.node_tree.name
    bpy.data.node_groups[name].nodes['Attribute'].attribute_name = value


def get_value(self):
    if not 'Factor' in self.inputs.keys():
        return ""
    return self.inputs["Factor"].default_value

bpy.types.ShaderNodeGroup.factor = bpy.props.StringProperty(
         get=get_value,
         set=set_value)

def draw_prop(self, context):
    mat = context.material 
    nodes = get_group_nodes(mat)
    for n in nodes: 
        self.layout.prop(n, "factor")

bpy.types.NODE_HT_header.prepend(draw_prop)

Re edit, quicker to do than explain, here is question code with above added. Recommend you read https://stackoverflow.com/questions/291978/short-description-of-the-scoping-rules re the scope of variables / methods in python.

bl_info = {
"name": "Add Test Material",
"author": "Rakesh Choudhary",
"version": (1, 0),
"blender": (2, 83, 0),
"location": "View3D > Sidebar > Test Material Node",
"description": "Click on the 'Test Material' button to add a material to your object.",
"warning": "",
"wiki_url": "",
"category": "3D View"
}

import bpy
from bpy.types import (
    Operator,
    Panel,
)


def get_group_nodes(material):
    if material.use_nodes:
        return [n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)]
    return []


def set_value(self, value):
    print(self, self.id_data, value)
    if not 'Factor' in self.inputs.keys():
        return None
    self.inputs['Factor'].default_value = value
    name = self.node_tree.name
    bpy.data.node_groups[name].nodes['Attribute'].attribute_name = value


def get_value(self):
    if not 'Factor' in self.inputs.keys():
        return ""
    return self.inputs["Factor"].default_value


class TEST_MATERIAL_OT_add_material(Operator):
    bl_idname = "test_material.add_material"
    bl_label = "Add Test Material"
    bl_description = "This button will add a material to your object"

    def execute(self, context):
        self.create_material(context)
        return {'FINISHED'}

    def create_material(self, context):
        test_shader_mat = bpy.data.materials.new("TestMat")
        mesh = context.object.data
        mesh.materials.clear()
        mesh.materials.append(test_shader_mat)
        context.object.active_material.use_nodes = True

        for mat in bpy.data.materials:
            if "TestMat" in mat.name:
                nodes = mat.node_tree.nodes
                for node in nodes:
                    if node.type != 'OUTPUT_MATERIAL':  # skip the material output node as we'll need it later
                        nodes.remove(node)

        # Creating Node Group Test_Material
        group = bpy.data.node_groups.new(type="ShaderNodeTree", name="Test_Material")

        # Creating Group Input
        group.inputs.new("NodeSocketColor", "Diffuse Color")
        group.inputs.new("NodeSocketColor", "Glossy Color")
        group.inputs.new("NodeSocketFloat", "Glossyness")
        group.inputs.new("NodeSocketString", "Factor")
        input_node = group.nodes.new("NodeGroupInput")
        input_node.location = (-800, 0)


        group.inputs[0].default_value = (1, 1, 1, 1)
        group.inputs[1].default_value = (1, 1, 1, 1)
        group.inputs[2].default_value = (0.500)
        group.inputs[3].default_value = "foam"


        # Creating Group Output Node
        group.outputs.new("NodeSocketShader", "Diffuse Color")
        group.outputs.new("NodeSocketShader", "Glossy Color")
        group.outputs.new("NodeSocketShader", "Mix Output")

        output_node = group.nodes.new("NodeGroupOutput")
        output_node.location = (1500, 0)

        # Creating Diffuse Node
        diffuse_node = group.nodes.new(type='ShaderNodeBsdfDiffuse')
        diffuse_node.location = (150, 100)

        # Creating Glossy Node
        glossy_node = group.nodes.new(type='ShaderNodeBsdfGlossy')
        glossy_node.location = (300, 250)

        # Creating Mix Shader Node
        mix_shader_node = group.nodes.new(type='ShaderNodeMixShader')
        mix_shader_node.location = (450, 100)


        #Attribute Node    
        attribute_node = group.nodes.new(type='ShaderNodeAttribute')
        attribute_node.location  = (100, 400)
        attribute_node.attribute_name = group.inputs[3].default_value
    
    

        # Creating Links Between Nodes----------------------------------------------
        group.links.new(diffuse_node.outputs["BSDF"], mix_shader_node.inputs[1])
        group.links.new(glossy_node.outputs["BSDF"], mix_shader_node.inputs[2])
        group.links.new(input_node.outputs["Diffuse Color"], diffuse_node.inputs[0])
        group.links.new(input_node.outputs["Glossy Color"], glossy_node.inputs[0])
        group.links.new(input_node.outputs["Glossyness"], glossy_node.inputs[1])
        group.links.new(output_node.inputs["Diffuse Color"], diffuse_node.outputs[0])
        group.links.new(output_node.inputs["Glossy Color"], glossy_node.outputs[0])
        group.links.new(output_node.inputs["Mix Output"], mix_shader_node.outputs[0])
        group.links.new(attribute_node.outputs["Fac"], mix_shader_node.inputs[0])
    


        # Putting Node Group to the node editor
        tree = bpy.context.object.active_material.node_tree
        group_node = tree.nodes.new("ShaderNodeGroup")
        group_node.node_tree = group
        group_node.location = (-40, 300)
        group_node.use_custom_color = True
        group_node.color = (1, 0.341, 0.034)
        group_node.width = 250

        shader_node_output_material_node = tree.nodes["Material Output"]
        links = tree.links
        links.new(group_node.outputs[0], shader_node_output_material_node.inputs[0])
    
        #Material ends here------------------------------
       

class TEST_MATERIAL_PT_layout_panel(Panel):
    bl_label = "Test Material Node"
    bl_category = "Test Material"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        layout.operator("test_material.add_material", icon='IMPORT')
        mat = context.object.active_material
        if mat:
            for n in get_group_nodes(mat):
                layout.prop(n, "factor")
    
    



classes = (TEST_MATERIAL_OT_add_material, TEST_MATERIAL_PT_layout_panel)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.ShaderNodeGroup.factor = bpy.props.StringProperty(
         get=get_value,
         set=set_value)



def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()
$\endgroup$
9
  • $\begingroup$ thanks for answering but still I have one problem, maybe I made some mistake in the code because it is not working for me I updated in the question whatever I did after your answer $\endgroup$ Jul 1, 2020 at 14:41
  • 1
    $\begingroup$ NP. note made a small change making material in panel draw objects active material. $\endgroup$
    – batFINGER
    Jul 1, 2020 at 15:09
  • 1
    $\begingroup$ If its elsewhere import it eg from test_addon.operators import get_nodes $\endgroup$
    – batFINGER
    Jul 2, 2020 at 12:23
  • 1
    $\begingroup$ Re multi file I would ask myself whats the logical way to split up the files. Why not have the operator that creates the material and the panel that displays it all in the one module. On the errors and assoc edits to question. These are, strictly speaking, basic python errors, not associated with the use of blender. Strongly recommend googling eg "python nameerror" the causes and fixes are well documented. Stackoverflow is good in this regard. $\endgroup$
    – batFINGER
    Jul 2, 2020 at 12:40
  • 1
    $\begingroup$ One operator could make multiple materials. Anyway please review my previous comment re import. Don't mind helping, but this is getting way past the original question or related to usage of python in blender. Is basic relative importing. I'm not here to teach you python, nor am I inclined to argue your point on what is or isn't good practice. $\endgroup$
    – batFINGER
    Jul 2, 2020 at 14:00

You must log in to answer this question.

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