Cycles node factory

This is an working draft of a script to create cycles materials. You feed it a control file and it generates the material for you.

The input at first looks like this:

# b
* texvoronoi
* texvoronoi
* texmusgrave
* texnoise
# c
* texcoord
* mapping

The ‘*’ lines are the names of the shader classes, less the ‘ShaderNode’ on the front; one of these per node. The ‘#’ lines are comments. The result will be another file that you can edit to create the input to the second phase, which you run through the script again to create the actual nodes.

The input in its second form looks like this:

&ShaderNodeTexVoronoi/texvoronoi0 # ---------------- texvoronoi0 
texvoronoi0.Vector < mapping5.Vector
texvoronoi0.Scale = 2.7
texvoronoi0.coloring := 'CELLS'

Significance:
&nodeclass/name – generates a node
innode.input < outnode.output – generates a link
node.input = value – sets the (‘default’) value of an input
node.attribute := value – this sets an attribute of the node object
These lines do not need to be in order; you can refer to a node before you create it.

Code for the script

#!/usr/bin/python
# -*- coding: utf-8 -*-
import bpy
import re
print ('------
')


class Material:

    def set_cycles(self):
        scn = bpy.context.scene
        if not scn.render.engine == 'CYCLES':
            scn.render.engine = 'CYCLES'

    def make_material(self, name):
        if name in bpy.data.materials:
            bpy.data.materials.remove(bpy.data.materials[name])
        self.mat = bpy.data.materials.new(name)
        self.mat.use_nodes = True
        self.nodes = self.mat.node_tree.nodes

    def link(
        self,
        from_node,
        from_slot_name,
        to_node,
        to_slot_name,
        ):
        input = to_node.inputs[to_slot_name]
        output = from_node.outputs[from_slot_name]
        self.mat.node_tree.links.new(input, output)

    def makeNode(self, type, name):
        self.node = self.nodes.new(type)
        self.node.name = name
        self.xpos += 200
        self.node.location = (self.xpos, self.ypos)
        return self.node

    def genCode(self, node, name):
        outf.write('&%s/%s # ---------------- %s 
' % (node, name, name)) 
        n = m.makeNode(node, name)
        i = 0
        inputNames = {}
        outputNames = {}
        for ni in n.inputs:
            if ni.name not in inputNames:
                inputNames[ni.name] = 0
            inputNames[ni.name] += 1
        for no in n.outputs:
            if no.name not in outputNames:
                outputNames[no.name] = 0
            outputNames[no.name] += 1
        for ni in n.inputs:
            indic = ('\'' + ni.name + '\'' if inputNames[ni.name]
                     == 1 else i)
            className =  re.sub('NodeSocket', '', ni.__class__.__name__ )   
            outf.write('%s.%s &lt; # %s
' % (name, ni.name, className))
            i += 1
        i = 0
        outf.write('# Outputs: ')
        for no in n.outputs:
            indic = (no.name if outputNames[no.name] == 1 else i)
            className =  re.sub('NodeSocket', '', no.__class__.__name__ )  
            outf.write('%s.%s (%s) ' % (name, indic, className))
            i += 1
        outf.write('
')
        for p in dir(n):
            if p[0] != '_' and p not in self.inherited_properties:
                outf.write('%s.%s := 
' % (name, p))
        return n

    def new_row():
        self.xpos = 0
        self.ypos += 200

    def __init__(self):
        self.xpos = 0
        self.ypos = 0
        self.inherited_properties = {}
        for p in [
            'as_pointer',
            'bl_description',
            'bl_height_default',
            'bl_height_max',
            'bl_height_min',
            'bl_icon',
            'bl_idname',
            'bl_label',
            'bl_static_type',
            'bl_width_default',
            'bl_width_max',
            'bl_width_min',
            'color',
            'copy',
            'dimensions',
            'draw_buttons',
            'draw_buttons_ext',
            'driver_add',
            'driver_remove',
            'free',
            'get',
            'height',
            'hide',
            'id_data',
            'init',
            'inputs',
            'internal_links',
            'is_property_hidden',
            'is_property_set',
            'is_registered_node_type',
            'items',
            'keyframe_delete',
            'keyframe_insert',
            'keys',
            'label',
            'location',
            'mute',
            'name',
            'outputs',
            'parent',
            'path_from_id',
            'path_resolve',
            'poll',
            'poll_instance',
            'property_unset',
            'select',
            'Shaderpoll',
            'show_options',
            'show_preview',
            'show_texture',
            'socket_value_update',
            'type',
            'type_recast',
            'update',
            'use_custom_color',
            'values',
            'width',
            'width_hidden',
            ]:
            self.inherited_properties[p] = 1


shaders = {}
dataDir = '/home/martin/git/blend/'
ctlf = open(dataDir+'shader-data.txt', 'r')
for line in ctlf:
    shaders[line.rstrip().lower()] = line.strip()
ctlf.close()
m = Material()
m.set_cycles()
m.make_material('test')
inf = open(dataDir+'py/cycles-materials3b-rock-a.txt', 'r')
outf = open(dataDir+'py/cycles-materials3b-rock-a.txt.new', 'w')
reExpand = re.compile(r"\s*\*\s*([^# ]+)")
reComment = re.compile(r"\s*\#.*$")
reNode = re.compile(r"\s*&\s*([a-zA-Z]+)\s*/\s*([A-Za-z0-9]+)")
reLink = \
    re.compile(r"\s*([a-zA-Z0-9]+)\s*\.\s*([a-zA-Z0-9]+)\s*&lt;\s*([a-zA-Z0-9]+)\s*\.\s*([a-zA-Z0-9]+)\s*$"
               )
reSetVal = re.compile(r"\s*([a-zA-Z0-9]+)\s*\.\s*([a-zA-Z0-9_]+)\s*\=\s*(.*)$")
reSetAttrib = re.compile(r"\s*([a-zA-Z0-9]+)\s*\.\s*([a-zA-Z0-9_]+)\s*:=\s*(.*)$")
nodeIndex = 0
nodes = {}
lineNum = 0
links = []
setVals = []
setAttrs = []
for line in inf:
    lineNum += 1
    match = reExpand.match(line.strip())
    if match:
        shaderName = match.group(1)
        if shaderName not in shaders:
            print ('%d: unknown shader: %s' % (lineNum, shaderName))
            continue
        m.genCode('ShaderNode' + shaders[shaderName], shaderName + str(nodeIndex)) 
        nodeIndex += 1
        continue
    #if not re.search('&lt;', line) and not re.search('\\.', line):
    outf.write('%s
' % line.rstrip())
    useLine = reComment.sub('', line.rstrip())
    if useLine == '':
        continue
    match = reLink.match(useLine)
    if match:
        link = {'nodeFrom': match.group(3), 'slotFrom': match.group(4),'nodeTo': match.group(1), 'slotTo': match.group(2)}
        links.append(link) 
        continue
    match = reSetVal.match(useLine)
    if match:
        setval = {'nodeName': match.group(1), 'attrib': match.group(2), 'val': match.group(3)}
        setVals.append(setval)
        continue
    match = reSetAttrib.match(useLine)
    if match:
        setattr = {'nodeName': match.group(1), 'attrib': match.group(2), 'val': match.group(3)}
        setAttrs.append(setattr)
        continue
    match = reNode.match(useLine)
    if match:
        shaderName, name = match.group(1), match.group(2) 
        node = m.makeNode(shaderName, name)
        nodes[name] = node
        continue
    if useLine[0] == '-':
        m.new_row()
        continue
    print ('%d: cannot interpret \'%s\'' % (lineNum, useLine))
for link in links: 
        print ('linking %s %s to %s %s' % (link['nodeFrom'], link['slotFrom'],
                                    link['nodeTo'], link['slotTo']))
        if link['nodeFrom'] not in nodes:
            print('%d: no node called %s
' % (lineNum, link['nodeFrom']))
            continue
        if link['nodeTo'] not in nodes:
            print('%d: no node called %s
' % (lineNum, link['nodeTo']))
            continue
        m.link(nodes[link['nodeFrom']], link['slotFrom'], nodes[link['nodeTo']],
               link['slotTo'])
for setval in setVals:
    if setval['nodeName'] not in nodes:
            print('no node called %s
' % setval['nodeName'] )
            continue
    print('setting %s.%s slot to %s' % (setval['nodeName'], setval['attrib'], setval['val']))
    if setval['val'] != '':
            nodes[setval['nodeName']].inputs[setval['attrib']].default_value = eval(setval['val'])
    continue
for setattr in setAttrs: 
    if setattr['nodeName'] not in nodes:
            print('no node called %s
' % setattr['nodeName'] )
            continue
    print('setting %s.%s attr to %s' % (setattr['nodeName'], setattr['attrib'], setattr['val']))
    if setattr['val'] != '':
        cmnd = 'nodes[\'' + setattr['nodeName'] + '\'].' + setattr['attrib'] + ' = ' + setattr['val']
        print('running \'%s\'' % cmnd)
        exec(cmnd)
outf.close()
inf.close()

it is an interesting script and quite clean and short. I am not entirely sure what its use case is though. Peter Cassetta’s online matlib can read and write node based materials already, even to local offline libraries, but that script is a lot more complex, so your approach might still have merit.

The use case is to generate materials from an ASCII ‘script’. My theory is that this might be easier than using the node editor, at least for complex situations. One use might be to version control materials.

		 				 	@varkenvarken  	 do yo have a link for the Cassetta script?

sorry, forgot to include the link:

http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Material/Online_Material_Library