0

I want to make a button and when I press it, it will apply the material I made, that mean the nodes.

I try different way to type script but it wasn't work.

bpy.ops.node.add_node(type="ShaderNodeTexNoise")

What's wrong with this code?

batFINGER
  • 84,216
  • 10
  • 108
  • 233
8N Films
  • 163
  • 1
  • 12
  • 1
    Related https://blender.stackexchange.com/questions/5668/add-nodes-to-material-with-python My recommendation is to test things out in the the python console. https://blender.stackexchange.com/questions/160042/principled-bsdf-via-python-api – batFINGER Mar 12 '21 at 03:45
  • 1
    https://blender.stackexchange.com/questions/200547/how-to-add-node-in-the-center-of-the-shader-editor-with-python – batFINGER Mar 12 '21 at 03:52

2 Answers2

3

If I understand correctly you want to turn your material into its equivalent python nodes. Arguably this is a bit too involved for a single SE question because there are a lot of use cases. Thankfully I tackled this some months ago for a personal project so I can share what I worked on.

You'll need to learn how to use a script.

How to use :

Select your object, select the material you want to copy, run the script. The material code will be added in a new text block.

enter image description here

"""
This scripts "serializes" the active material of the currently selected object
And creates a script readable by the Blender API to recreate said Material.
As any Blender script, it is free to use in any way shape or form.
V 1.1 - 20.10.23
Fixed NodeSocketVirtual error
"""

import bpy

from bpy.types import ( NodeSocketShader, NodeSocketVirtual, NodeSocketVector, NodeSocketVectorDirection, NodeSocketVectorXYZ, NodeSocketVectorTranslation, NodeSocketVectorEuler, NodeSocketColor,

NodeReroute,

Object,
Image,
ImageUser,
Text,
ParticleSystem,
CurveMapping,
ColorRamp,

ShaderNodeTree,

)

from mathutils import Vector, Color

ERROR = "~ ERROR ~"

def get_link_statement(link): """ Build the statement to re-create given link """ return f"""
links.new({link.from_node.path_from_id()}.outputs[{get_socket_index(link.from_socket)}]
, {link.to_node.path_from_id()}.inputs[{get_socket_index(link.to_socket)}])
"""

def value_from_socket(socket): """ Returns the evaluated value of a node socket's default value """ # A Shader socket (green dot) doesn't have a default value : if isinstance(socket, (NodeSocketShader, NodeSocketVirtual)): return ERROR elif isinstance(socket, ( NodeSocketVector, NodeSocketVectorXYZ, NodeSocketVectorTranslation, NodeSocketVectorEuler, NodeSocketVectorDirection)): return f"{[socket.default_value[i] for i in range(3)]}" elif isinstance(socket, NodeSocketColor): return f"{[socket.default_value[i] for i in range(4)]}" else: return socket.default_value.str()

class NodeCreator: """ Helper class to programmatically recreate the passed node """ # These props are internal or read-only # and aren't useful in the serialization. default_props = ( "dimensions", "draw_buttons", "draw_buttons_ext", "input_template", "inputs", "internal_links", "isAnimationNode", "is_registered_node_type", "output_template", "outputs", "poll", "poll_instance", "rna_type", "socket_value_update", "type", "update", "viewLocation",

    "texture_mapping",
    "color_mapping",

    "filepath",

    "cache_point_density",
    "calc_point_density",
    "calc_point_density_minmax",

    "interface",

    "height",
    "show_options",
    "show_preview",
    "show_texture",
    "width_hidden",
)

def __init__(self, node):
    """
    Initialize the node inputs and outputs,
    and the different fields' default values
    """
    self.node = node
    self.input_default_values = []
    self.output_default_values = []
    if not isinstance(node, NodeReroute):
        for _input in node.inputs:
            self.input_default_values.append(value_from_socket(_input))
        for output in node.outputs:
            self.output_default_values.append(value_from_socket(output))

    self.type = type(node).__name__
    self.properties = []  # Could use an ordered dict instead.
    for prop_name in dir(node):
        if prop_name.startswith("_") or prop_name.startswith("bl_"):
            continue
        if prop_name in NodeCreator.default_props:
            continue
        self.properties.append((prop_name, getattr(node, prop_name)))

def statements(self):
    """
    Build the chain of statements to programmatically recreate the node
    """
    statements = []
    statements.append(f"new_node = nodes.new(type='{self.type}')")
    self.properties = sorted(self.properties, key=lambda p: p[0])
    for props_tuple in self.properties:
        prop, value = props_tuple
        if isinstance(value, ImageUser):
            statements.append(f"""\

img_text = new_node.{prop} img_text.frame_current = {value.frame_current} img_text.frame_duration = {value.frame_duration} img_text.frame_offset = {value.frame_offset} img_text.frame_start = {value.frame_start} img_text.use_auto_refresh = {value.use_auto_refresh} img_text.use_cyclic = {value.use_cyclic} img_text.tile = {value.tile}
""") continue if isinstance(value, ParticleSystem): # /!\ Make sure this is executed after node.object statement statements.append(f"""
if new_node.object: new_node.{prop} = new_node.object.particle_systems.get('{value.name}') """) continue if isinstance(value, CurveMapping): statements.append(f"""
map = new_node.{prop} map.clip_max_x = {value.clip_max_x} map.clip_max_y = {value.clip_max_y} map.clip_min_x = {value.clip_min_x} map.clip_min_y = {value.clip_min_y} map.tone = '{value.tone}' map.use_clip = {value.use_clip}
""") # Remove the 2 starting default points and only these : for i, curve in enumerate(value.curves): statements.append(f"map_c = map.curves[{i}]") for point in curve.points: statements.append(f"""
map_c.points.new({point.location[0]}, {point.location[1]})""") statements.append("""
removed_start = removed_end = False for i in range(len(map_c.points) - 1, -1, -1): p = map_c.points[i] if not removed_start and p.location[0] == map.clip_min_x and p.location[1] == map.clip_min_y: map_c.points.remove(p) removed_start = True if not removed_end and p.location[0] == 1 and p.location[1] == 1: map_c.points.remove(p) removed_end = True
""") statements.append(f"map.update()") continue if isinstance(value, ColorRamp): statements.append(f"""
cr = new_node.{prop} cr.color_mode = '{value.color_mode}' cr.hue_interpolation = '{value.hue_interpolation}' cr.interpolation = '{value.interpolation}'
""") for stop in value.elements: statements.append(f"""new_stop = cr.elements.new({stop.position}) new_stop.color = {[ch for ch in stop.color]}""") # Remove the 2 starting default stops and only these : statements.append("""
removed_black = removed_white = False for i in range(len(cr.elements) - 1, -1, -1): stop = cr.elements[i] if not removed_black and stop.position == 0 and all([stop.color[i] == (0, 0, 0, 1)[i] for i in range(4)]): cr.elements.remove(stop) removed_black = True if not removed_white and stop.position == 1 and all([stop.color[i] == (1, 1, 1, 1)[i] for i in range(4)]): cr.elements.remove(stop) removed_white = True
""") continue if isinstance(value, ShaderNodeTree): statements.append(f"""
ng = bpy.data.node_groups.get('{value.name}') if not ng: new_node.label = "Missing Node Group : '{value.name}'" else: new_node.{prop} = ng
""") continue

        if prop in ("hide", "mute", "use_custom_color"):
            if value:
                statements.append(f"new_node.{prop} = {value}")
        elif prop == "text" and not value:
            continue
        elif prop in ("select", "shrink"):
            if not value:
                statements.append(f"new_node.{prop} = {value}")
        elif isinstance(value, str):
            if value:
                statements.append(f"new_node.{prop} = '{value}'")
        elif isinstance(value, Vector):
            if len(value) == 2:
                statements.append(
                    f"new_node.{prop} = ({value[0]}, {value[1]})")
            else:
                statements.append(
                    f"new_node.{prop} = ({value[0]}, {value[1]}, {value[2]})")
        elif isinstance(value, Object):
            statements.append(
                f"new_node.{prop} = bpy.data.objects.get('{value.name}')")
        elif isinstance(value, Image):
            statements.append(
                f"new_node.{prop} = bpy.data.images.get('{value.name}')")
        elif isinstance(value, Text):
            if value:
                statements.append(
                    f"new_node.{prop} = bpy.data.texts.get('{value.name}')")
        elif prop == "parent":
            if value:
                statements.append(f"""\

parent = nodes.get('{value.name}') if parent: new_node.parent = parent while True: new_node.location += parent.location if parent.parent: parent = parent.parent else: break
""") elif isinstance(value, Color): statements.append( f"new_node.{prop} = ({value[0]}, {value[1]}, {value[2]})") else: statements.append(f"new_node.{prop} = {value}") for i, dv in enumerate(self.input_default_values): if dv == ERROR: continue statements.append(f"new_node.inputs[{i}].default_value = {dv}")

    for i, dv in enumerate(self.output_default_values):
        if dv == ERROR:
            continue
        statements.append(f"new_node.outputs[{i}].default_value = {dv}")

    if not isinstance(self.node, NodeReroute):
        for _input in self.node.inputs:
            if _input.hide:
                statements.append(
                    f"new_node.inputs[{get_socket_index(_input)}].hide = True")
        for output in self.node.outputs:
            if output.hide:
                statements.append(
                    f"new_node.outputs[{get_socket_index(output)}].hide = True")

DEBUG Print node location as a label :

statements.append("new_node.label = str(new_node.location[0]).split('.')[0] + ', ' + str(new_node.location[1]).split('.')[0]")

    return statements


def serialize_material(mat): """ Returns the ordered statements necessary to build the Mateiral generation script """ node_tree = mat.node_tree statements = [f"""
import bpy new_mat = bpy.data.materials.get('{mat.name}') if not new_mat: new_mat = bpy.data.materials.new('{mat.name}')

new_mat.use_nodes = True node_tree = new_mat.node_tree nodes = node_tree.nodes nodes.clear()

links = node_tree.links links.clear() """]

statements.append("# Nodes :\n")
for node in node_tree.nodes:
    for st in NodeCreator(node).statements():
        statements.append(st)
    statements.append("")

if node_tree.links:
    statements.append("# Links :\n")
    for link in node_tree.links:
        statements.append(get_link_statement(link))

return statements


def write_material_to_text_block(obj): """ Create or overwrite a text block with the same name as the material Which contains all the necessary statements to duplicate the material """ if not obj or obj.type not in ('MESH', 'CURVE', 'VOLUME', 'SURFACE', 'FONT', 'META', 'GPENCIL'): return am = obj.active_material if not am or not am.use_nodes: return statements = serialize_material(am)

text_block = bpy.data.texts.get(am.name)
if text_block:
    text_block.clear()
else:
    text_block = bpy.data.texts.new(am.name)

for st in statements:
    text_block.write(st)
    text_block.write("\n")

return text_block


def get_socket_index(socket): return socket.path_from_id().split(".")[-1].split("[")[-1][:-1]

if name == "main": text_block = write_material_to_text_block(bpy.context.active_object)

The code is available there for grabbing https://github.com/Gorgious56/Material2Script/blob/main/material_to_script.py

Gorgious
  • 30,723
  • 2
  • 44
  • 101
  • @brockmann Yeah figured I'd spend 5 minutes writing the answer rather than 10 mins trying to figure out the actual question :) – Gorgious Mar 12 '21 at 11:42
  • 1
    I can feel your fortune-telling skills. – brockmann Mar 12 '21 at 11:54
  • @Gorgious very cool script. Coded something similar a few yrs back, as usual started neat, got spaghettified and back-burnered. Pinching this instead. – batFINGER Mar 12 '21 at 14:19
  • @batFINGER Well a fair number of your answers helped me in the process so it's only fair ^^ thanks :) I'm thinking of updating it, a few nodes have been tweaked/added since (vectore rotate and attribute come to mind) – Gorgious Mar 12 '21 at 23:54
  • Finally UV'd, seems the OP is not willing to edit the q. – brockmann Mar 13 '21 at 09:20
  • Hard to put a finger on the "mind-reader factor. eg OP knows what they want and so should we, no edits required. @Gorgious thanks, consider asking a new Q re converting nodes to script. Given the accepted answer could prob re-title this one and shut it down as dupe. – batFINGER Mar 13 '21 at 09:44
  • Asked a new question @Gorgious, feel free to edit it and copy and paste your answer. – brockmann Mar 13 '21 at 15:26
2

Adding nodes to a material is best done using low-level functions. I'd suggest use Nodes.new() instead of bpy.ops.node.add_node(). The add_node() operator is more for direct user interaction, and requires the correct context to run.

How to create a new node via material data-block, assuming a material name of "Material":

bpy.data.materials["Material"].node_tree.nodes.new("ShaderNodeTexNoise")

Better practice is using pythons get() method to basically perform a search for the material in the first place and (avoids potential index errors):

import bpy

mat = bpy.data.materials.get("Material") if mat: mat.node_tree.nodes.new("ShaderNodeTexNoise")

Related:

brockmann
  • 12,613
  • 4
  • 50
  • 93
Lewis
  • 1,310
  • 1
  • 13
  • 26
  • 1
    Disagree on "best done via bpy.data", depends on the situation, you can also get the node tree reference from the editor or any object. Recommend use get() on the material data block (to avoid our famous index error) and ideally use the active object and its active material in context: bpy.context.object.active_material.node_tree.nodes.new("ShaderNodeTexNoise"), can be easily incorporated into an operator as well (might be part of the question, not sure). – brockmann Mar 12 '21 at 08:32
  • 1
    Re the question.. an option would be to add a panel to node editor and put operator button there, where there is context.active_node etc .... suggest requesting clarity re what the OP means by apply the node to material linked and settings too? @brockmann can you recall a node to script addon? – batFINGER Mar 12 '21 at 08:46
  • I mean turning nodes to script. I don't mean to use it in shader editor. I would like to add a button that apply the nodes when I press it – 8N Films Mar 12 '21 at 09:35
  • 1
    @BNFilms edit your question to clarify. Including where you want the button. It really is not clear from question in its current form. – batFINGER Mar 12 '21 at 11:39