3
$\begingroup$

I am searching of ways to serialize Blender Cycles materials to files using Python. But as of now I've found no such ways.

Maybe by writing the data of the materials to files but how can I do that? Even the slightest of contributions are appreciated.

If only there was a way to serialize materials to JSON/Binary/XML.

Perhaps someone here can lead me to light.

Maybe in the way Blender stores the materials?

$\endgroup$
5
  • $\begingroup$ To read the values and connections this should be a great starting point. $\endgroup$
    – p2or
    Commented Dec 14, 2015 at 15:29
  • $\begingroup$ There's a node exporter if i recall correctly.try to look it up $\endgroup$
    – Chebhou
    Commented Dec 14, 2015 at 16:07
  • 2
    $\begingroup$ @Chebhou This one: blenderartists.org/forum/…? $\endgroup$
    – p2or
    Commented Dec 14, 2015 at 16:20
  • $\begingroup$ Meanwhile, I have found one way, but that's not what I asked in the question - To save the materials in a blender file and write an add-on to import only the materials from it. But I do hope someone pops with a better script only way $\endgroup$ Commented Dec 15, 2015 at 8:34
  • $\begingroup$ 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 $\endgroup$
    – psilocybe
    Commented Dec 10, 2023 at 9:41

3 Answers 3

6
$\begingroup$

There is the rna_xml module, which produces pretty verbose xml

from rna_xml import rna2xml

rna2xml(root_node="MyRootName",
        root_rna=bpy.data.materials['Material'].node_tree)

produces (not full output). There is also an rna2xml routine. To output to a file use rna2xml(fw=f.write, ...) where f is a file.

<MyRootName>
  <inputs>
  </inputs>
  <links>
    <NodeLink is_valid="TRUE"
              from_node="ShaderNodeBsdfDiffuse::Diffuse BSDF"
              to_node="ShaderNodeOutputMaterial::Material Output"
              is_hidden="FALSE">
      <from_socket>
        <NodeSocketShader name="BSDF"
                          identifier="BSDF"
                          is_output="TRUE"
                          hide="FALSE"
                          enabled="TRUE"
                          link_limit="4095"
                          is_linked="TRUE"
                          show_expanded="FALSE"
                          hide_value="FALSE"
                          node="ShaderNodeBsdfDiffuse::Diffuse BSDF"
                          type="SHADER"
                          bl_idname="NodeSocketShader">
        </NodeSocketShader>
      </from_socket>
      <to_socket>
        <NodeSocketShader name="Surface"
                          identifier="Surface"
                          is_output="FALSE"
                          hide="FALSE"
                          enabled="TRUE"
                          link_limit="1"
                          is_linked="TRUE"
                          show_expanded="FALSE"
                          hide_value="FALSE"
                          node="ShaderNodeOutputMaterial::Material Output"
                          type="SHADER"
                          bl_idname="NodeSocketShader">
        </NodeSocketShader>
      </to_socket>
    </NodeLink>
  </links>
  <nodes>
    <ShaderNodeOutputMaterial type="OUTPUT_MATERIAL"
                              location="300 300"
                              width="140"
                              width_hidden="42"
                              height="100"
                              dimensions="178.889 116.333"
                              name="Material Output"
                              label=""
                              parent="NONE"
                              use_custom_color="FALSE"
                              color="0.608 0.608 0.608"
                              select="TRUE"
                              show_options="TRUE"
                              show_preview="FALSE"
                              hide="FALSE"
                              mute="FALSE"
                              show_texture="FALSE"
                              shading_compatibility="{NEW_SHADING}"
                              bl_idname="ShaderNodeOutputMaterial"
                              bl_label="Material Output"
                              bl_description=""
                              bl_icon="NONE"
                              bl_static_type="OUTPUT_MATERIAL"
                              bl_width_default="140"
                              bl_width_min="100"
                              bl_width_max="320"
                              bl_height_default="100"
                              bl_height_min="30"
                              bl_height_max="30"
                              is_active_output="TRUE">
      <inputs>
        <NodeSocketShader name="Surface"
                          identifier="Surface"
                          is_output="FALSE"
                          hide="FALSE"
$\endgroup$
1
  • $\begingroup$ Warning: You can't currently use xml2rna to deserialize most materials because of this bug $\endgroup$ Commented Jan 18, 2022 at 17:57
1
$\begingroup$

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.

Serialize:

    # browse all materiels in the Blender scene
    for bmat in bpy.data.materials:
        if not bmat.use_nodes:
            continue
            msg = f"Blender material: {bmat.name}, don't use node, skipped...\n"
            warnings.append(msg)
            App.Console.PrintMessage(msg)
            continue
        if bmat.name in materials and materials[bmat.name].Material.get(root):
            msg = f"FreeCAD material: {bmat.name}, already has a Blender configuration, skipped...\n"
            warnings.append(msg)
            App.Console.PrintMessage(msg)
            continue

        matdata = {}
        for node in bmat.node_tree.nodes:
            links = {}
            inputs = {}
            outputs = {}
            sockets = {}

            if node.inputs:
                link, input = _getInputData(warnings, bmat, node)
                links.update(link)
                inputs.update(input)

            if node.outputs:
                output = _getOutputs(warnings, bmat, node)
                outputs.update(output)

            sockets = _getSocketProperties(warnings, node, bmat.name, node.name, [])

            matdata[node.name] = {'Type':     node.__class__.__name__,
                                  'Sockets':  sockets,
                                  'Link':     links,
                                  'Inputs':   inputs,
                                  'Outputs':  outputs}

        if matdata:
            if bmat.name in materials:
                mat = materials[bmat.name]
            else:
                mat = makeMaterial(bmat.name)
                materials[bmat.name] = mat
            temp = mat.Material.copy()
            temp[root] = json.dumps(tuple(matdata.keys()))
            for node, data in matdata.items():
                temp[root + '.' + node] = json.dumps(data)
            mat.Material = temp

def _getInputData(warnings, bmat, node):
    links = {}
    inputs = {}
    for input in node.inputs:
        for link in input.links:
            links[input.name] = (link.from_node.name, link.from_socket.name)
        if input.type == 'VALUE':
            v = input.default_value
            value = v
        elif input.type == 'VECTOR':
            v = input.default_value
            value = (v[0], v[1], v[2])
        elif input.type == 'RGBA':
            v = input.default_value
            value = (v[0], v[1], v[2], v[3])
        elif input.type == 'SHADER':
            continue
        else:
            msg = f"FreeCAD material {bmat.name} on node {node.name} can't read input: {input.name} type {input.type} is not supported, skipping...\n"
            warnings.append(msg)
            App.Console.PrintMessage(msg)
            continue
        App.Console.PrintMessage(f"Material Name: {bmat.name} Node: {node.name} Input: {input.name} - Value: {value}\n")
        inputs[input.name] = value
    return links, inputs

def _getOutputs(warnings, bmat, node):
    outputs = {}
    for output in node.outputs:
        if output.type == 'VALUE':
            v = output.default_value
            value = v
        elif output.type == 'VECTOR':
            v = output.default_value
            value = (v[0], v[1], v[2])
        elif output.type == 'RGBA':
            v = output.default_value
            value = (v[0], v[1], v[2], v[3])
        elif output.type == 'SHADER':
            continue
        else:
            msg = f"FreeCAD material {bmat.name} on node {node.name} can't read output: {output.name} type {output.type} is not supported, skipping...\n"
            warnings.append(msg)
            App.Console.PrintMessage(msg)
            continue
        App.Console.PrintMessage(f"Material Name: {bmat.name} Node: {node.name} output: {output.name} - Value: {value}\n")
        outputs[output.name] = value
    return outputs

def _getSocketProperties(warnings, obj, mat, node, properties):
    data = {}
    skipping = ('rna_type', 'inputs', 'outputs', 'dimensions', 'type',
                'is_hidden', 'from_node', 'from_socket', 'to_node', 'to_socket')
    for p in dir(obj):
        if p not in skipping and not (p.startswith('_') or p.startswith('bl_')):
            v = getattr(obj, p)
            if v is None or isinstance(v, bpy.types.bpy_func):
                continue
            elif isinstance(v, (str, int, float, bool)):
                value = v
            elif isinstance(v, mathutils.Color):
                value = {'r': v.r, 'g': v.g, 'b': v.b}
            elif isinstance(v, mathutils.Euler):
                value = {'x': v.x, 'y': v.y, 'z': v.z, 'order': v.order}
            elif isinstance(v, mathutils.Vector):
                if len(v) == 2:
                    value = {'x': v.x, 'y': v.y}
                elif len(v) == 3:
                    value = {'x': v.x, 'y': v.y, 'z': v.z}
                elif len(v) == 4:
                    value = {'x': v.x, 'y': v.y, 'z': v.z, 'w': v.w}
            elif isinstance(v, bpy.types.bpy_prop_array):
                value = []
                for d in v:
                    value.append(d)
            elif isinstance(v, bpy.types.bpy_prop_collection):
                value = {}
                properties.append(p)
                for k, d in v.items():
                    value[k] = _getSocketProperties(warnings, d, mat, node, properties)
            elif isinstance(v, bpy.types.TexMapping):
                properties.append(p)
                value = _getSocketProperties(warnings, v, mat, node, properties)
            elif isinstance(v, bpy.types.ColorRamp):
                properties.append(p)
                value = _getSocketProperties(warnings, v, mat, node, properties)
            elif isinstance(v, bpy.types.ColorMapping):
                properties.append(p)
                value = _getSocketProperties(warnings, v, mat, node, properties)
            else:
                msg = f"FreeCAD material {mat} on node {node} can't read property: {p} type {type(v)} is not supported, skipping...\n"
                warnings.append(msg)
                App.Console.PrintMessage(msg)
                continue
            data[p] = value
            properties.append(p)
            App.Console.PrintMessage(f"Node socket: {node} property: {'.'.join(properties)} value: {str(value)}\n")
    return data

Deserialize:

def _setMaterialNodes(bmat, mat, root):
    links = {}
    sockets = {}
    inputs = {}
    outputs = {}
    data = mat.Material.get(root)
    print("_setMaterialNodes() 1 data: %s" % data)
    if data:
        nodes = json.loads(data)
        for name in nodes:
            link, socket, input, output = _createNode(bmat, mat, root, name)
            links.update(link)
            sockets.update(socket)
            inputs.update(input)
            outputs.update(output)
        for node, link in links.items():
            bnode = bmat.node_tree.nodes[node]
            _setLinks(bmat, bnode, link)
        for node, socket in sockets.items():
            bnode = bmat.node_tree.nodes[node]
            _setSockets(bnode, socket)
        for node, input in inputs.items():
            bnode = bmat.node_tree.nodes[node]
            _setInputs(bnode, input)
        for node, output in outputs.items():
            bnode = bmat.node_tree.nodes[node]
            _setOutputs(bnode, output)

def _createNode(bmat, mat, root, name):
    links = {}
    sockets = {}
    inputs = {}
    outputs = {}
    data = mat.Material.get(root + '.' + name)
    if data:
        node = json.loads(data)
        print(f"Create Node socket {name} of type {node['Type']}")
        bnode = bmat.node_tree.nodes.new(type=node['Type'])
        bnode.name = name
        links[bnode.name] = node['Link']
        sockets[bnode.name] = node['Sockets']
        inputs[bnode.name] = node['Inputs']
        outputs[bnode.name] = node['Outputs']
    return links, sockets, inputs, outputs

def _setLinks(bmat, bnode, links):
    for input, outputs in links.items():
        _setLink(bmat, bnode, input, *outputs)

def _setLink(bmat, bnode, input, node2, output):
    bmat.node_tree.links.new(bmat.node_tree.nodes[node2].outputs[output],
                             bnode.inputs[input])

def _setSockets(obj, sockets):
    for property, value in sockets.items():
        if isinstance(value, dict):
            if property.isnumeric():
                _setSockets(obj[int(property)], value)
            else:
                _setSockets(getattr(obj, property), value)
        else:
            setattr(obj, property, value)

def _setInputs(bnode, inputs):
    for input, value in inputs.items():
        binput = bnode.inputs.get(input)
        if binput:
            binput.default_value = value
        else:
            print("_setInputs() ERROR *********************************************")

def _setOutputs(bnode, outputs):
    for output, value in outputs.items():
        boutput = bnode.outputs.get(output)
        if boutput:
            boutput.default_value = value
        else:
            print("_setOutputs() ERROR *********************************************")
$\endgroup$
0
0
$\begingroup$

As mentioned by @batFINGER you can use rna2xml to serialize node trees to an xml file. Then if you want to deserialize the node tree you can use rna2xml.xml2rna function.

Here is a snippet of code describing the whole procedure.Be aware though that you might get Read-only attributes error when importing the xml, in that case you can simply delete the attributes of the root node that pose a problem.

Cheers

EDIT: Actually this blender module is useful to serialize structures that have a pre-determined content but it is in no way useful to serialize whole materials and node trees from scratch. One easy way to get around that issue is to have another scene in the same .blend file with all the materials. Those can be accessed across all scenes with bpy.data.materials[<key>]. One could decide to augment the RNA serializer offered by Blender but I decided that it would take too much time for my current project.

$\endgroup$

You must log in to answer this question.

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