Trying to learn how to script

I’d like to automate copying an entire material node set from one multi-matzone figure to another, in Cycles. To that end, I’m trying to learn how to automate using Python scripting.

I thought it would be easiest to just create the whole node-set (groups and all) for all the material zones of the figure (like limbs and torso and face and so forth) from scratch using Python, so I’m trying to create a node set with a script. The simplest tutorials are head-and-shoulders over my capability or needs: this one, for example (called “Start Small”), goes into creating really sophisticated solutions including interface items, but I need something significantly simpler than that. I followed bassam’s tutorial on BlenderCookie but even that jumps into classes and how to call them from the console.

I need way simpler than that.

Here is my script. I am trying to create a simple Diffuse and Glossy Mixed shader:

import bpy

bpy.context.scene.render.engine = 'CYCLES'


bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0))


selectedObj = bpy.context.active_object


if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="NewMaterial")


if len(selectedObj.data.materials):
    selectedObj.data.materials = newMat
else:
    selectedObj.data.materials.append(newMat)


bpy.context.object.active_material.use_nodes = True
bpy.ops.node.add_node(type="ShaderNodeMixShader", use_transform=True)
#bpy.ops.node.add_node(type="ShaderNodeBsdfGlossy", use_transform=True)
#bpy.ops.node.link(detach=False)

I’m running into a ‘context’ issue, something I don’t understand. The error is as follows:

read blend: /home/robyn/Documents/Blender/Projects/AllSkin/skinShaderPy02.blendxTraceback (most recent call last):
File “/home/robyn/Documents/Blender/Projects/AllSkin/skinShaderPy02.blend/newMaterial.py”, line 20, in <module>
File “/home/robyn/blender276/2.76/scripts/modules/bpy/ops.py”, line 189, in call
ret = op_call(self.idname_py(), None, kw)
RuntimeError: Operator bpy.ops.node.add_node.poll() failed, context is incorrect

I’m pretty sure there is something fairly rudimentary I’m not getting, something that tutorial series isn’t sharing (unfortunately) which I’ll be happy to share forward once I get some answers.

Thanks so much for considering my little dilemma.

Try this.

import bpy

bpy.context.scene.render.engine = 'CYCLES'


bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0))


selectedObj = bpy.context.active_object


if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="NewMaterial")


if len(selectedObj.data.materials):
    selectedObj.data.materials = newMat
else:
    selectedObj.data.materials.append(newMat)
    
mat = bpy.data.materials.get("Material")


nodes = mat.node_tree.nodes
   
node = nodes.new('ShaderNodeMixShader')

Ah, I see what was missing there, Albert, and thank you! The fact that you had this solution indicates I need to do more studying on scripting. I’m doing the CGCookie video tutorials and what I can find on YouTube… would you have any suggestions? (so I don’t keep asking stupid questions like this… :slight_smile: )

By the way, I ran into an exception:

Traceback (most recent call last):  File "/home/robyn/Documents/Blender/Projects/AllSkin/skinShaderPy02.blend/newMaterial.py", line 26, in &lt;module&gt;
AttributeError: 'NoneType' object has no attribute 'node_tree'

…so I changed:

mat = bpy.data.materials.get("Material")

to

mat = bpy.data.materials.new(name="NewMaterial")

and that error went away. However, the following line:

nodes = mat.node_tree.nodes

then spat the dummy - apparently there was no attribute nodes in node_tree. When I removed that “.nodes” bit at the end it was happy, but then:

node = nodes.new('ShaderNodeMixShaderMixer")

failed with node has no attribute ‘new’.

It’s all a bit of a flounder because I’m not really clear on what I’m doing.

So far, this is what I have and what I understand this to be doing:

import bpy

if bpy.context.scene.render.engine == 'BLENDER_RENDER':
    bpy.context.scene.render.engine = 'CYCLES'


bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0))
selectedObj = bpy.context.active_object


if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="NewMaterial")


if len(selectedObj.data.materials):
    selectedObj.data.materials = newMat
else:
    selectedObj.data.materials.append(newMat)

First, with “import bpy” I gain access to (expose) Blender Python methods and stuff.

Then, I make sure the render engine is ‘Cycles’.

Then, I add a cube. I suppose I could phrase it this way:

selectedObj = bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0))

…because when you’ve just added some object in a brand-new scene that had nothing in it before, it will be the selected object. All clear so far.

Now, I get into a grey area:


if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="NewMaterial")

Well, I tested the first line in console this way:

bVal = bpy.data.materials.get("Material") is not None

and then printed bVal, which returned a False. Which meant, to me that that statement:
bpy.data.materials.get(“Material”) is not None
was false, the “else:” was going to run, which it did, generating a new material I called “NewMaterial”. But ‘.get’? what is this ‘.get’-ting? getting from where?

The Blender.org quickstart guide refers to these methods, such as ‘new’ - (I’m assuming it’s a method, since it does something):

Data is added and removed via methods on the collections in bpy.data, eg:
>>> mesh = bpy.data.meshes.new(name=“MyMesh”)
>>> print(mesh)
<bpy_struct, Mesh(“MyMesh.001”)>

>>> bpy.data.meshes.remove(mesh)

Well, ‘new’ is self-explanatory. But ‘get’?

bpy.context.object[“MyOwnProperty”] = 42
if “SomeProp” in bpy.context.object:
print(“Property found”)

# Use the get function like a python dictionary
# which can have a fallback value.
value = bpy.data.scenes[“Scene”].get(“test_prop”, “fallback value”)

I’m clearly missing something and probably looking for it in the wrong place. :rolleyes:

This is what the script accomplishes at this point:

  • sets the renderer to Cycles
  • puts a cube in the scene
  • gives it a material called “roughWall”
  • adds another material to the cube called “smoothWall”
  • sets the material area to “Use Nodes”

Here’s the code so far:

import bpy

if bpy.context.scene.render.engine == 'BLENDER_RENDER':
    bpy.context.scene.render.engine = 'CYCLES'


bpy.ops.mesh.primitive_cube_add(radius=1, view_align=False, enter_editmode=False, location=(0, 0, 0))

selectedObj = bpy.context.active_object




if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="roughWall")
if len(selectedObj.data.materials):
    selectedObj.data.materials = newMat
else:
    selectedObj.data.materials.append(newMat)



nextMat = bpy.data.materials.new(name = "smoothWall")
selectedObj.data.materials.append(nextMat)
selMats = selectedObj.data.materials
for sMs in selMats:
    sMs.use_nodes = True



The most important thing I’ve learned so far is to not try to leverage anything I know about object referencing in other languages here.

What I’d like to do now is learn how to add nodes… actually, build a node tree with groups and everything all in Python. Before I embark on that, however, I need to be a bit more clear on why things work (or don’t) the way they do (or don’t). For example, I originally had this:


selMats = selectedObj.data.materials
for sMs in selMats:
    sMs.append(nextMat)
    sMs.use_nodes = True



…but Python chucked a hissy about the “sMs.append(nextMat)”.

So, why can I say:

selectedObj.data.materials.append(nextMat)

and


selMats = selectedObj.data.materials
for sMs in selMats:
    sMs.use_nodes = True



but not have that .append(nextMat) in there? :no:

Hi,

firstly


if bpy.data.materials.get("Material") is not None:
    newMat = bpy.data.materials.get("Material")
else:
    newMat = bpy.data.materials.new(name="roughWall")

will use material “Material” if it exists, otherwise “roughWall” the first time, then “roughWall.001” the next ad infinitum.
Use the same name.

get(…) is used as it returns the obj if it exists or None, or a user defined value, in this case create(scene) returns a new object.


obj = collection.get("blah", create(scene))

whereas a direct reference


obj = collection["blah"]

will fail (throw an error) if it doesn’t exist. Using get is IMO a lot nicer that multiple try except statements.


import bpy
scene = bpy.context.scene

obj = scene.objects.active

mat = bpy.data.materials.get("roughWall")
if  mat is None:
    mat = bpy.data.materials.new(name="roughWall")

# material index
idx = obj.data.materials.find(mat.name)

if idx == -1:  # mat not in materials
    obj.data.materials.append(mat)
    obj.active_material_index = len(obj.data.materials) - 1
else:
    obj.active_material_index = idx

Lastly


selMats = selectedObj.data.materials
for sMs in selMats:
    sMs.append(nextMat)
    sMs.use_nodes = True

sMs is a material and doesn’t have an append method, it’s on the materials collection.

Wow, thank you for your detailed explanation, batFINGER. I will of course follow your suggestion about get()… it is definitely a more elegant and forgiving solution.

I have much to learn. Thank you for taking the time to explain this! :slight_smile:

Caveat, lots of hard-coded material file and path stuff in this script, all of which will eventually be in dictionaries/collections. This is all very much proof of concept. In order for it to work, you’ll need to take these steps (I know, too much busy-work :eek:):

– Save the content of the above script to a plain-text editor file, call it whatever you want.
(I call mine ‘FullSkin024.py’, because it’s Python code, version .24.)
– Open Blender (getting rid of the default cube and that)
– File -> Import -> Wavefront (obj) … navigate to your Vicky, select her OBJ (not her MTL), then chose the following Import settings:
---- Untick Smooth Groups and untick Lines and everything in Split By
---- Tick Keep Vert Order and tick Poly Groups
– Import OBJ
– Click on her in the scene, then hit [S] (for scale), 10
– Select Scripting from the Scene Selector (next to ‘Help’) - usually says ‘Default’ by default :wink:
– In the Text Editor section, select Open and navigate to the code file (the one I call ‘FullSkin024.py’)
– Scroll down to line 44 and make changes to:
---- path
---- name of your Vickie - whatever she’s called in Outliner
---- the names of your V4 texture (colour) and bump files for the face, torso and limbs
– Run the script

This will only work for the Victoria 4 figure for Daz-Studio / Poser and will only do skin, so far:


# FullSkin024.py
# 
# Copyright (c) 25-Oct-2015, Robyn Hahn
#
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****


bl_info = {
    "name": "Simple skin shader for Poser Figures",
    "author": "Robyn Hahn",
    "version": (0, 2, 4),
    "blender": (2, 76, 0),
    "location": "-= Not a real addon For Blender, yet =-",
    "description": "Generates a simple Cycles shader for a Poser figure",
    "warning": "early development",
    "wiki_url": "http://0.0.0.1",
    "category": "Simple Material Script"}


import bpy
import os.path


"""
So far, this will set the shader for all skin materials - more to come.
These are the items you change:


path     ... the fully-qualified path to your texture files like C:\MyDir\MyTex\
figurObj ... the name of your imported V4 figure   
clr files... colour maps 
bmp files... bump maps
"""
path = '/home/robyn/Documents/Blender/Projects/AllTextures/AllSkin/textures/'
figurObj = 'V4'
clrLimbs = 'Syri_Limbs.jpg'
bmpLimbs = 'Syri_LimbsB.jpg'
clrTorso = 'Syri_Torso.jpg'
bmpTorso = 'Syri_TorsoB.jpg'
clr_Face = 'Syri_Face.jpg'
bmp_Face = 'Syri_FaceB.jpg'






# sets renderer to Cycles
if bpy.context.scene.render.engine == 'BLENDER_RENDER':
    bpy.context.scene.render.engine = 'CYCLES'




# class builds mat-specific node set (shader)
class buildShader():
    def __init__(self, cObj, cRegion, ImgClr, ImgBum=None):
        self.selObj = cObj
        self.Region = cRegion
        self.ImgCol = ImgClr
        self.ImgBmp = ImgBum


        self.selObj.select = True
        
        self.selMats = self.selObj.active_material
        self.selMats.use_nodes = True
        
        self.treeNodes = self.selMats.node_tree
        self.nodeLinks = self.treeNodes.links
        
        self.makeSkin()
    
    def makeSkin(self):
        # clears existing nodes, if any
        for n in self.treeNodes.nodes:
            self.treeNodes.nodes.remove(n)
            
        # create nodes: Texture Coordinate and Mapping
        shaTcd = self.treeNodes.nodes.new('ShaderNodeTexCoord')
        shaMap = self.treeNodes.nodes.new('ShaderNodeMapping')
        
        # create node: Colour Image Texture
        ImgCol = self.treeNodes.nodes.new('ShaderNodeTexImage')    
        ImgCol.image = self.ImgCol
        ImgCol.color_space = 'COLOR'
        
        # create node: Bump Image Texture
        ImgBmp = self.treeNodes.nodes.new('ShaderNodeTexImage')    
        ImgBmp.image = self.ImgBmp
        ImgBmp.color_space = 'NONE'




        # create nodes: Diffuse, set second input [1] roughness to .15
        shaDif = self.treeNodes.nodes.new('ShaderNodeBsdfDiffuse')
        shaDif.inputs[1].default_value = .15
        
        # create node: Translucent
        shaTrn = self.treeNodes.nodes.new('ShaderNodeBsdfTranslucent')
        shaTrn.inputs[0].default_value = [.8, .22, .06, 1]
        
        # create node: Glossy
        shaGls = self.treeNodes.nodes.new('ShaderNodeBsdfGlossy')
        shaGls.inputs[1].default_value = .12


        # create node: Mix-RGB
        clrMix = self.treeNodes.nodes.new('ShaderNodeMixRGB')
        clrMix.inputs[0].default_value = .1
        clrMix.inputs[2].default_value = [.5, .35, .275, 1]


        mathMl = self.treeNodes.nodes.new('ShaderNodeMath')
        mathMl.operation = 'MULTIPLY'
        mathMl.inputs[1].default_value = .01


        shaMx3 = self.treeNodes.nodes.new('ShaderNodeMixShader')
        shaMx2 = self.treeNodes.nodes.new('ShaderNodeMixShader')
        shaMx1 = self.treeNodes.nodes.new('ShaderNodeMixShader')
        
        # create node: Material Output (sort-of like PoserSurface)
        shaOut = self.treeNodes.nodes.new('ShaderNodeOutputMaterial')


        # Set node locations, roughly
        shaTcd.location = -850, 300
        shaMap.location = -600, 300
        ImgCol.location = -200, 400
        ImgBmp.location = -200, 200
        shaDif.location = 0, 400
        shaTrn.location = 0, 200
        clrMix.location = 0, 100
        shaMx3.location = 200, 400
        shaMx2.location = 400, 400
        shaGls.location = 400, 100
        shaMx1.location = 600, 400
        mathMl.location = 600, 100
        shaOut.location = 800, 300


        # Link nodes: tex cood and mapping
        self.nodeLinks.new(shaTcd.outputs[2], shaMap.inputs[0])
        self.nodeLinks.new(shaMap.outputs[0], ImgCol.inputs[0])
        self.nodeLinks.new(shaMap.outputs[0], ImgBmp.inputs[0])
        # Link nodes: clr and bmp
        self.nodeLinks.new(ImgCol.outputs[0], shaDif.inputs[0])
        self.nodeLinks.new(ImgCol.outputs[0], clrMix.inputs[1])
        self.nodeLinks.new(ImgBmp.outputs[0], mathMl.inputs[0])
        # Mixing colorMap and Translucent ( reddish )
        self.nodeLinks.new(shaDif.outputs[0], shaMx3.inputs[1])
        self.nodeLinks.new(shaTrn.outputs[0], shaMx3.inputs[2])
        shaMx3.inputs[0].default_value = .065
        # Mixing ClrMap/Translu and ClrMix ( beige )
        self.nodeLinks.new(shaMx3.outputs[0], shaMx2.inputs[1])
        self.nodeLinks.new(clrMix.outputs[0], shaMx2.inputs[2])
        shaMx2.inputs[0].default_value = .05
        # Mixing ClrMap/Transl/ClrMix and Glossy
        self.nodeLinks.new(shaMx2.outputs[0], shaMx1.inputs[1])
        self.nodeLinks.new(shaGls.outputs[0], shaMx1.inputs[2])
        shaMx1.inputs[0].default_value = .05
        
        # Output
        self.nodeLinks.new(shaMx1.outputs[0], shaOut.inputs[0])
        self.nodeLinks.new(mathMl.outputs[0], shaOut.inputs[2])
        
        return self.selMats
        


# make sure your figure is selected and nothing else
# bpy.ops.object.select_all(action='DESELECT')
bpy.context.object.active_material_index = 0
curr_obj = bpy.data.objects[figurObj]


for i in range(27):
    bPasteTex = False 
    curr_obj.active_material_index = i
    matName = curr_obj.active_material.name
    matType = matName[0:1]
    if matType == '3':
        img_Clr = clrLimbs
        img_Bmp = bmpLimbs
        bPasteTex = True
    if matType == '2':
        img_Clr = clrTorso
        img_Bmp = bmpTorso
        bPasteTex = True
    if matType == '1':
        img_Clr = clr_Face
        img_Bmp = bmp_Face
        bPasteTex = True


    if bPasteTex:
        ImgColour = path + img_Clr
        ImgBump = path + img_Bmp
        imgC = bpy.data.images.load(filepath = ImgColour)
        imgB = bpy.data.images.load(filepath = ImgBump)


        newMat = buildShader(curr_obj, 'Skin', imgC, imgB)

Very much proof-of-concept, thus far.

Not sure if any Poser / V4 users are out there in Blenderland, but I’ve made this little script for those who might be interested in something quick-n-dirty to give V4 shaders and textures in Blender. You’ll actually have to edit the script itself to reflect your own path and texture file names - like above - but so far, this seems to to do the trick.

Thanks to all who have helped so far. And no, this is not an elegant solution… it works for me, so I’m putting it out there for others who might need something of the sort. I’m working on a similar script for a figure called ‘Dawn’. Doesn’t use a bump map, but a spec map, so the shaders will be different.

Here’s the updated tick/flick:

– Unzip the script to somewhere you can find it easily in Blender.
– Open Blender (getting rid of the default cube and that)
– File -> Import -> Wavefront (obj) … navigate to your Vicky, select her OBJ (not her MTL), then chose the following Import settings:
---- Untick Smooth Groups and untick Lines and everything in ‘Split By’
---- Tick ‘Keep Vert Order’ and tick ‘Poly Groups’
– Import OBJ
– Click on the figure in the scene, press [S] (for scale), and type 10
– Select Scripting from the Scene Selector (next to ‘Help’ up the top) - usually says ‘Default’ by default
– In the Text Editor section, select Open and navigate to the code file (called ‘FullV4028.py’)
– Scroll down to line 258 (might need to enable line numbering, furthest icon of the three to the left of ‘Run Script’) and change:
---- path: path to your texture files: the images to paint on V4
---- figurObj: name of your Vickie - whatever she’s called in Outliner
---- clrLimbs / bmpLimbs etc: the names of your V4 texture (colour) and bump files for the face, torso and limbs, etc
– Run the script

Oh, and you’ll need this.

A wee update for those who might be even slightly interested. :wink:

This has been an interesting exercise in Python, definitely. I’m actually invoking a separate .py that holds a dictionary of 4 figure material slot names and what material type they are. There’s no rhyme or reason to Poser figure material slot naming, so the dictionary makes it possible to assign textures to different figures even though the naming is quite disparate.

What I’ve discovered, however, is that not only are the material names an issue: the way the figures are textured is, as well. For example, the Victoria4 figure generally has colour maps and bump maps. She may have specular maps. The Dawn figure uses colour maps, no bump maps but does use specular maps.

Nightmare coding one script to manage all that. :eek:

What I’m hoping to nut out is a sort of configuration file that can be read by the script that will contain:

  • Generic name of the figure
  • Path to the texture files
  • Names of the texture files and what they are meant to be painted onto

I do hope to make this into a proper addon with a place for folks to enter this info in a little gui thing, which info then gets written to that config file.

Well, that’s where things are now, anyway. The dictionary approach to material slots does work, so that’s sorted, but when you upturn one rock, other interesting challenges crawl out, don’t they? :yes:

If anyone is interested in trying out what I have so far, do let me know… be happy to post a WIP, otherwise I’ll just carry on.

I’ve put the current flavour of the script on GitHub (for the curious), not so much because I’m being bombarded with requests by real developers to work on this, but it’s more about sharing and version control (I keep over-writing working with non-working code :eek: ). I use gEdit to edit the script. I haven’t managed to sort out how to get it to auto-indent like here, after the colon:


def myFunct(stuff):
    other = stuff

…but I’m sure there’s an add-on for that. It does do syntax highlighting, so that’s nice. Which brings me to my first question… the example given here:


import bpy  
import os  
  
filename = os.path.join(os.path.dirname(bpy.data.filepath), "addon-move3.py")  
exec(compile(open(filename).read(), filename, 'exec'))

…which lets you execute a script outside of Blender assumes the script is in the same folder as the .blend file. Apart from hard-coding the actual path to the script, is there another way to add the path to the sys.path thing? I tried this:


import sys
import os
import bpy


blend_dir = os.path.basename(bpy.data.filepath)
if blend_dir not in sys.path:
   sys.path.append(blend_dir)

print(blend_dir)

…from the API tips and tricks page, but the script still spat the dummy when what I was trying to import was not in the same folder as the .blend - tried running this in console and it didn’t give me the path statement at all: it gave me the name of the current .blend.

So, I’m clearly missing something.

The other question I had was about panel operators (buttons and that): lots of examples on how to run internal commands but what if I want to run a method of an 'import’ed script?

Many thanks to all who read and especially those who consider my little problems and reply. :slight_smile:

(By the way, I am having a read of this page - still not quite clear how I would address calling a custom function by clicking a button on an addon panel, though).