16

I'd like to convert certain materials into executable python code without having to re-code them from scratch each time.

While doing some initial experimentation on the principled shader, I realized that there are a lot of different attribute types and sub-attributes to handle eg. ShaderNodeBsdfPrincipled.input collection is holding 23 different structs:

<bpy_struct, NodeSocketColor("Base Color") at 0x7fe51de9b0d8>
<bpy_struct, NodeSocketFloatFactor("Subsurface") at 0x7fe51de9b2a8>
<bpy_struct, NodeSocketVector("Subsurface Radius") at 0x7fe51de9b478>
...

and calling dir(ShaderNodeBsdfPrincipled) yields:

['doc', 'module', 'slots', 'bl_description', 'bl_height_default', 'bl_height_max', 'bl_height_min', 'bl_icon', 'bl_idname', 'bl_label', 'bl_rna', 'bl_static_type', 'bl_width_default', 'bl_width_max', 'bl_width_min', 'color', 'dimensions', 'distribution', 'draw_buttons', 'draw_buttons_ext', 'height', 'hide', 'input_template', 'inputs', 'internal_links', 'is_registered_node_type', 'label', 'location', 'mute', 'name', 'output_template', 'outputs', 'parent', 'poll', 'poll_instance', 'rna_type', 'select', 'show_options', 'show_preview', 'show_texture', 'socket_value_update', 'subsurface_method', 'type', 'update', 'use_custom_color', 'width', 'width_hidden']`

Q: Is there any clever python trickery using "magic" object attributes or anything in order to store and restore the nodes, their attribute values and ideally all node connections as well?


If anyone would like to have a material for testing purposes, the following blend contains some kind of production setup (from Camera projection without generating UV's?).

brockmann
  • 12,613
  • 4
  • 50
  • 93
  • 3
    Very nice...... – batFINGER Mar 13 '21 at 15:28
  • 2
    Perhaps using something like DeepDiff https://pypi.org/project/deepdiff/ on each node versus a new node to see non-default values? – Markus von Broady Mar 13 '21 at 15:33
  • All approaches are welcome @MarkusvonBroady – brockmann Mar 13 '21 at 15:57
  • If a miracle happens and batFINGER's solution isn't amazing, I'll see what I can come up with. – Markus von Broady Mar 13 '21 at 16:01
  • @MarkusvonBroady great idea re comparing against new node. Looked at this a while back, there is a q about it too, re nodes not playing the typical blender bl_rna game re default settings. For most props in blender obj.is_property_set("foo") returns False if ob.foo is default. Brockers, be a good one to have a blend with sample material to test against. – batFINGER Mar 13 '21 at 19:51
  • Sure @batFINGER. Hopefully a good choice otherwise let me know. – brockmann Mar 13 '21 at 20:29
  • @brockmann NP from me either, after all this q was written to pay homage to G's answer on another question. Sad that an undesignated bounty doesn't get distributed to all answers weighted by positive votes. (Just as I believe there could be more than one "acceptable" answer) Apologies for not getting back sooner, took some time away from BSE to regain sanity. Re https://blender.stackexchange.com/questions/218377/how-can-i-know-scripts-of-what-i-am-editing-on-mesh#comment368526_218377 check out the answerer's (Films) recent questions and answers... see any patterns? – batFINGER Apr 04 '21 at 13:31
  • Oops, missed that comment yesterday... Agree. However, it is like it is and you absolutely deserve it! Can start another one in a few weeks for Gorgious, I don't mind :) Still hope this nice answers are getting even more attention. No need to apologize, great to have you back though... reassuring that you noticed these patterns too @batFINGER – brockmann Apr 05 '21 at 07:58
  • I can serialize and deserialize any Blender material configuration in JSON. It is possible for me to save my materials in a FreeCAD file and restore all these materials in Blender from this FreeCAD file. for more information see this thread on the FreeCAD forum – psilocybe Dec 10 '23 at 09:30

2 Answers2

12

Make a dummy material with each node type of material.

Following on from the suggestion in comments by @MarkusvonBroady couldn't resist.... Am waiting to see the excellent material serialization answer from you know who,

Pretty much zip the material node / inputs / outputs etc against a newly added default of same type, and add to string buffer when not same. Have ignored any read only property and select.

Code and test Run.

import bpy
from math import isclose

LUT = dict() BLACK = [0.0, 0.0, 0.0, 1.0] WHITE = [1.0, 1.0, 1.0, 1.0] ul = f'#{"-" * 44}\n' buffer = []

def cmp(v1, v2): return ( (len(v1) == len(v2)) and all(isclose(*v) for v in zip(v1, v2)) )

def vformat(nums, n, indent=8): nums = [f"{d}" for d in nums] return f",\n{' ' * indent}".join([", ".join(nums[i: i + n]) for i in range(0, len(nums), n)])

def fes(collection, prop, data, size, indent): output( f'{" " * indent}{collection}.foreach_set(\n' f' "{prop}", [\n' f' {vformat(data, size, 8 + indent)}\n' f' ])' )

def groups_in_tree(node_tree): for node in node_tree.nodes: if hasattr(node, "node_tree"): yield node.node_tree yield from groups_in_tree(node.node_tree)

def group_io(n): output(f'node = nodes.new("{n.bl_rna.identifier}")') output(f'node.name = "{n.name}"') sockets = ("inputs", "outputs") if n.type == 'GROUP_INPUT' else ("outputs", "inputs") for skt in getattr(n, sockets[1]): if skt.type != 'CUSTOM' and skt.name: output( f"skt = group.{sockets[0]}.new('{skt.class.name}', " f'"{skt.name}", ' f')' ) output(f'skt.name = "{skt.name}"') dv = skt.default_value val = dv[:] if hasattr(dv, "foreach_get") else dv output(f"skt.default_value = {val}") output()

def colorramp(a, b): n = len(a.elements) output(f'ramp = node.color_ramp') compare(a, b, fstring="ramp.{k} = {va}") locs, deflocs = [0.0] * n, [0.0, 1.0] cols, defcols = [0.0] * (n << 2), BLACK + WHITE a.elements.foreach_get("position", locs), a.elements.foreach_get("color", cols) n = n - 2 if n: output( f"for i in range({n}):\n" f" ramp.elements.new(0.0)" )

if not cmp(locs, deflocs):
    fes(&quot;ramp.elements&quot;, &quot;position&quot;, locs, 1, 0)

if not cmp(cols, defcols):
    fes(f'ramp.elements', &quot;color&quot;, cols, 4, 0)


def mapping(a, b): output(f'map = node.mapping') compare(a, b, fstring="map.{k} = {va}")

for i, c in enumerate(a.curves):
    n = len(c.points)
    pts, default = [0, 0] * n, [-1.0, -1.0, 1.0, 1.0]
    n -= 2
    if n:
        output(
            f'for i in range({n}):\n'
            f'    map.curves[{i}].points.new(0.0, 1.0)\n'
        )

    c.points.foreach_get(&quot;location&quot;, pts)
    if not cmp(pts, default):
        fes(f&quot;map.curves[{i}].points&quot;, &quot;location&quot;, pts, 2, 0)


def output(*args): s = " ".join(args) if args else "" buffer.append(s)

def compare(a, b, fstring="{k} = {va}", sockets="", i=0, ignore={'select'}):

props = (
        (k, v)
    for k, v in a.bl_rna.properties.items()
    if (not v.is_readonly or k in (&quot;mapping&quot;, &quot;color_ramp&quot;))
    and k not in ignore
)

for k, v in props:
    va = getattr(a, k)
    vb = getattr(b, k, None)

    if v.type in ('FLOAT', 'INT'):
        if v.is_array:
            if not isinstance(va, float):
                va = va[:]
            if vb and not isinstance(vb, float):
                vb = vb[:]

    if va != vb:
        if v.type == 'ENUM':
            va = f&quot;'{va}'&quot;
        elif v.type == 'STRING':
            va = f'&quot;{va}&quot;'
        elif v.type == 'POINTER':
            if k == &quot;parent&quot;:
                va = f'nodes.get(&quot;{va.name}&quot;)'
            elif a.type == 'GROUP':
                return output(f'node.node_tree = groups.get(&quot;{a.node_tree.name}&quot;)')
            elif issubclass(v.fixed_type.__class__, bpy.types.ID):
                va = repr(va).replace(f&quot;['{va.name}']&quot;, f'.get(&quot;{va.name}&quot;)')
            elif k == &quot;mapping&quot;:
                return mapping(va, vb)
            elif k.startswith(&quot;color_ramp&quot;):
                return colorramp(va, vb)
        name = f'&quot;{a.name}&quot;' if hasattr(a, &quot;name&quot;) else i
        output(fstring.format(**locals()))


def pnode(n, dummy): if n.type in ('GROUP_INPUT', 'GROUP_OUTPUT'): return group_io(n) nodetype = n.bl_rna.identifier default = LUT.setdefault( nodetype, dummy.nodes.new(nodetype) )

output(f'node = nodes.new(&quot;{nodetype}&quot;)')
compare(n, default, fstring=&quot;node.{k} = {va}&quot;)
for sockets in (&quot;inputs&quot;, &quot;outputs&quot;):
    for i, (a, b) in enumerate(
            zip(
                getattr(n, sockets),
                getattr(default, sockets),
            )
    ):

        compare(a, b, fstring='node.{sockets}[{name}].{k} = {va}', i=i, sockets=sockets)
output()


def material_to_text(m): try: dummy = bpy.data.node_groups.get("DUMMY") if not dummy: output("import bpy") dummy = bpy.data.node_groups.new("DUMMY", "ShaderNodeTree") output("groups = {} # for node groups")

    if hasattr(m, &quot;use_nodes&quot;):
        # material
        for gn in set(groups_in_tree(m.node_tree)):
            material_to_text(gn)
        nt = m.node_tree
        output(
            f&quot;\n&quot;
            f&quot;{ul}#  Material: {m.name} \n{ul}\n&quot;
            f'mat = bpy.data.materials.new(&quot;{m.name}&quot;)\n'
            f&quot;mat.use_nodes = True\n&quot;
            f&quot;node_tree = mat.node_tree\n&quot;
            f&quot;nodes = node_tree.nodes\n&quot;
            f&quot;nodes.clear()\n&quot;
            f&quot;links = node_tree.links\n&quot;
        )

    else:
        # group
        nt = m
        output(
            f&quot;\n&quot;
            f&quot;{ul}#  NodeGroup: {m.name} \n{ul}\n&quot;
            f'group = bpy.data.node_groups.new(&quot;{m.name}&quot;, &quot;{m.bl_rna.identifier}&quot;)\n'
            f'groups[&quot;{m.name}&quot;] = group\n'
            f&quot;nodes = group.nodes\n&quot;
            f&quot;links = group.links\n&quot;
        )

    for n in sorted(
        nt.nodes,
        key=lambda n: [n.location.x, n.location.y]
    ):
        pnode(n, dummy)
    if nt.links:
        output(&quot;#Links\n&quot;)
    for l in nt.links:
        output(
            f&quot;links.new(\n&quot;
            f'    nodes[&quot;{l.from_node.name}&quot;].outputs[&quot;{l.from_socket.name}&quot;],\n'
            f'    nodes[&quot;{l.to_node.name}&quot;].inputs[&quot;{l.to_socket.name}&quot;]\n    )\n'
        )

except Exception as e:
    print(&quot;There has been an ERROR&quot;)
    print(e, e.__traceback__.tb_lineno)
    return False  # failure

if hasattr(m, &quot;use_nodes&quot;):
    bpy.data.node_groups.remove(dummy)
return True  # success


if name == "main": m = bpy.context.object.active_material material_to_text(m)

text = bpy.data.texts.new(m.name)
text.write(&quot;\n&quot;.join(buffer))

Test Run on default "Material" with base color set to Red. AFAICT Generates the material in test file linked Ok.

import bpy
groups = {}  # for node groups

#--------------------------------------------

Material: Material

#--------------------------------------------

mat = bpy.data.materials.new("Material") mat.use_nodes = True node_tree = mat.node_tree nodes = node_tree.nodes nodes.clear() links = node_tree.links

node = nodes.new("ShaderNodeBsdfPrincipled") node.location = (10.0, 300.0) node.inputs["Base Color"].default_value = (1.0, 0.0, 0.0, 1.0)

node = nodes.new("ShaderNodeOutputMaterial") node.location = (300.0, 300.0)

#Links

links.new( nodes["Principled BSDF"].outputs["BSDF"], nodes["Material Output"].inputs["Surface"] )

Revision

Unlike Madonna and @Gorgeous not so much of a "Material Guy". TBH I'm a sub-feather-weight when it comes to blenders materials and nodes, so this was a nice little excersize for me.

Ultimately the idea, as I see it, is to be able to copy a material via a script in one blend, and re-create it in another.

Default Values.

Have kept the verbosity down a bit by not outputting default values. Could turn this off with a flag. Since non-default values are determined from a newly instanced copy they will be non-default at time of creation. As noted if the defaults change, will need to run script again.

Node Groups

Prior handled a group node by simply pointing the node tree to its bpy.data.node_groups item. Instead this version makes a copy of each node group used in the material. Was a very easy step, since nodes of both is a collection of nodes. To make sure the new group is used in the generated material by means of a dictionary groups to associate new with old name

groups = {}  # for node groups

#--------------------------------------------

NodeGroup: NodeGroup.001

#--------------------------------------------

group = bpy.data.node_groups.new("NodeGroup.001", "ShaderNodeTree") groups["NodeGroup.001"] = group

could turn this on or off to use existing node groups.

Curves and Ramps.

Wired it up to generate mapping and colorramp nodes. Used foreach_set which enables to add an arbitrary point for each extra (over default) and set from a list.

Color Ramp

node = nodes.new("ShaderNodeValToRGB")
node.location = (-345.2741394042969, 142.6455841064453)
node.parent = nodes.get("Frame")
ramp = node.color_ramp
for i in range(4):
    ramp.elements.new(0.0)
ramp.elements.foreach_set(
       "position", [
        0.0,
        0.25,
        0.45396339893341064,
        0.6530487537384033,
        0.7999999523162842,
        1.0
        ])
ramp.elements.foreach_set(
       "color", [
        0.0, 0.0, 0.0, 1.0,
        0.41859403252601624, 0.000635193195194006, 0.0, 1.0,
        0.0, 0.006493096239864826, 0.21146051585674286, 1.0,
        0.14895662665367126, 0.17292265594005585, 0.2819954454898834, 1.0,
        0.5389295816421509, 0.18324723839759827, 1.0, 1.0,
        1.0, 1.0, 1.0, 1.0
        ])

RGB Curve

node = nodes.new("ShaderNodeRGBCurve")
node.location = (211.58743286132812, 275.4912414550781)
node.parent = nodes.get("Frame")
map = node.mapping
map.tone = 'FILMLIKE'
map.clip_max_x = 0.8999999761581421
for i in range(5):
    map.curves[0].points.new(0.0, 1.0)

map.curves[0].points.foreach_set( "location", [ 0.0, 0.0, 0.15146341919898987, 0.4256756901741028, 0.24731720983982086, 0.7837838530540466, 0.4675609767436981, 0.5506754517555237, 0.5421952605247498, 0.8445945978164673, 0.6585365533828735, 0.5608108639717102, 1.0, 1.0 ]) for i in range(2): map.curves[1].points.new(0.0, 1.0)

map.curves[1].points.foreach_set( "location", [ 0.0, 0.0, 0.3512195348739624, 0.6621621251106262, 0.5926830172538757, 0.3581080138683319, 1.0, 1.0 ]) for i in range(3): map.curves[2].points.new(0.0, 1.0)

map.curves[2].points.foreach_set( "location", [ 0.0, 0.0, 0.16463413834571838, 0.581081211566925, 0.5004880428314209, 0.5777024626731873, 0.7858536243438721, 0.28378361463546753, 1.0, 1.0 ]) for i in range(1): map.curves[3].points.new(0.0, 1.0)

map.curves[3].points.foreach_set( "location", [ 0.0, 0.0, 0.6782926321029663, 0.4425675868988037, 1.0, 1.0 ])

Frames

Added the frames and set as parents to respective nodes, haven't wired up re the location changing, as demonstrated by @Gorgeous.

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • 4
    Very nice ..... – brockmann Mar 14 '21 at 14:44
  • Nice !! I was scratching my head and found only hacky workarounds, I knew there was a pythonic way to handle it :) Definitely stealing that for the future ! – Gorgious Mar 18 '21 at 20:41
  • What happened to rna_xml that you you referred to in ancient history? Or would that and/or other lower-level ways of stashing data-blocks. not count for this question? – Robin Betts Mar 19 '21 at 19:51
  • 1
    @RobinBetts had a better check that moment on re-visiting that 6yo answer (be a lot less ancient myself if i was 6yo at the time...) Fortunately OP has phrased the question well --> to create something akin to https://blender.stackexchange.com/questions/65129/how-do-i-create-a-script-for-geometry-i-create whereas the rna_xml , to me, feels more similar to importing / exporting. (subtle, but different enough?, thoughts) – batFINGER Mar 20 '21 at 09:40
  • I'd rather shovel hunks of bits around, avoiding even object-description formats, if I knew how.. but I think you're right. @brockmann must have his reasons. – Robin Betts Mar 20 '21 at 11:10
  • @batFINGER This evolved in quite the nifty script :) – Gorgious Mar 24 '21 at 10:08
  • Missed this, Thankyou. @Gorgious it suffers from many problems when compared to yours. Chop, chop add groups to your answer. – batFINGER Jul 11 '21 at 15:00
  • @batFINGER Hmm yes that's insufferable alright ! I'd return the favor though, I think it would be accommodated better on your script, I really like the style, it's like reading a piece of poetry but for computers ^^ I don't understand all of it but I know magic when I see it :p while mine is a hodgepodge Frankenstein monster with mud on its boots – Gorgious Jul 11 '21 at 17:03
12

I wrote this script a few months back for a personal project. You can see that it's a lot longer than the other answer :).

It creates statements to set the value of a node input or output, even if it is the same as the default value. I debated it, and despite leading to text files with a very high number of lines in big node trees, I prefer this option. Since the goal of this script is to create a snapshot of the material, and the defaults of today might not be the defaults of tomorrow, or of someone else's custom build, I prefer still writing explicitly all properties.

It could still be changed by the user if they want to, by implementing the proposed solution of python magician @batFINGER for not overwriting default values.

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",

    &quot;texture_mapping&quot;,
    &quot;color_mapping&quot;,

    &quot;filepath&quot;,

    &quot;cache_point_density&quot;,
    &quot;calc_point_density&quot;,
    &quot;calc_point_density_minmax&quot;,

    &quot;interface&quot;,

    &quot;height&quot;,
    &quot;show_options&quot;,
    &quot;show_preview&quot;,
    &quot;show_texture&quot;,
    &quot;width_hidden&quot;,
)

def __init__(self, node):
    &quot;&quot;&quot;
    Initialize the node inputs and outputs,
    and the different fields' default values
    &quot;&quot;&quot;
    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(&quot;_&quot;) or prop_name.startswith(&quot;bl_&quot;):
            continue
        if prop_name in NodeCreator.default_props:
            continue
        self.properties.append((prop_name, getattr(node, prop_name)))

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

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 = &quot;Missing Node Group : '{value.name}'&quot; else: new_node.{prop} = ng
""") continue

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

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&quot;new_node.outputs[{i}].default_value = {dv}&quot;)

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

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(&quot;# Nodes :\n&quot;)
for node in node_tree.nodes:
    for st in NodeCreator(node).statements():
        statements.append(st)
    statements.append(&quot;&quot;)

if node_tree.links:
    statements.append(&quot;# Links :\n&quot;)
    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(&quot;\n&quot;)

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
  • 3
    Very nice ..... – brockmann Mar 16 '21 at 16:45
  • Oops, the system has "auto-selected" the answer for the bounty and only assigned half of the rep. I'm sorry, nothing intended, hope you don't mind... sh*t. I'm going to start another one next week. – brockmann Mar 24 '21 at 10:27
  • @brockmann Haha no problem :) Anyways I think the other answer deserves it more than mine, comparing the two now and my scripts feels really clunky :p – Gorgious Mar 24 '21 at 11:17
  • @Exporer Hello, while I understand your proposed edit of my answer, I think it would have made more sense in a comment. As you noticed my answer is limited in regards to the node group creation. I suggest you look into the other answer of this question, which tackles this gracefully. I created this script for my own use back then, and I didn't need to re-create node group so I never implemented it. Feel free to edit it if you find a better solution. Cheers :) – Gorgious Aug 23 '21 at 13:00