Python Scripting Trouble

Python Scripting Trouble

mguast20
Explorer Explorer
923 Views
3 Replies
Message 1 of 4

Python Scripting Trouble

mguast20
Explorer
Explorer

Hello!
I am new to coding in Maya and Python and I am looking to better my understanding.

In the provided code, I have received multiple Value Errors, Type Errors and Maya Command errors. I am not entirely sure why and I have been struggling with it for a while.

Any and all suggestions are largely appreciated!

import maya.cmds as cmds

# Define function for cleanup
def cleanUp():
    if cmds.objExists('tre*'):
        cmds.delete('tre*')

# Define function for creating a tree model
def modelTree(i):
    pfx = f'tre{i}_'
    # tree trunk
    trunk = cmds.polyCylinder(n=f'tre{i}_trunk', r=0.4, h=4, sa=8, sh=16)
    cmds.move(0, 2, 0)
    cmds.polyTriangulate(trunk)
    cmds.polySoftEdge(trunk, a=0)
    # first layer of leaves
    cmds.polyCone(n=f'tre{i}_leaf1',r=1,h=2,sa=8,sh=4)
    cmds.move(0,4,0)
    cmds.scale(2,2,2)
    cmds.Triangulate(n=f'tre{i}_leaf1')
    cmds.polyMoveVertex(n=f'tre{i}_leaf1',random=True,ltz=0.110)
    cmds.polySoftEdge(a=0)
    # second layer of leaves
    cmds.polyCone(n=f'tre{i}_leaf2',r=1,h=2,sa=8,sh=4)
    cmds.move(0,5.5,0)
    cmds.scale(1.5,1.5,1.5)
    cmds.Triangulate(n=f'tre{i}_leaf2')
    cmds.polyMoveVertex(n=f'tre{i}_leaf2',random=True,ltz=0.115)
    cmds.polySoftEdge(a=0)
    # third layer of leaves
    cmds.polyCone(n=f'tre{i}_leaf3',r=1,h=2,sa=8,sh=4)
    cmds.move(0,6.5,0)
    cmds.scale(1,1,1)
    cmds.Triangulate(n=f'tre{i}_leaf3')
    cmds.polyMoveVertex(n=f'tre{i}_leaf3',random=True,ltz=0.120)
    cmds.polySoftEdge(a=0)
    # Freeze transformations and delete history
    cmds.FreezeTransformations()
    cmds.DeleteHistory()

# Define function for making shaders
def makeShaders(i):
    pfx = f'tre{i}_'
    # shader argument for tree trunk
    trunkSN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_trunk_SN')
    cmds.setAttr(f'tre{i}_trunk_SN.color',0.137,0.098,0.259)
    cmds.select(f'tre{i}_trunk')
    cmds.hyperShade(a=f'tre{i}_trunk_SN')
    # shader argument for first layer of leaves
    leaf1SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf1_SN')
    cmds.setAttr(f'tre{i}_leaf1_SN.color',0.369,0.329,0.557)
    cmds.select(f'tre{i}_leaf1')
    cmds.hyperShade(a=f'tre{i}_leaf1_SN')
    # shader argument for second layer of leaves
    leaf2SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf2_SN')
    cmds.setAttr(f'tre{i}_leaf2_SN.color',0.623,0.525,0.753)
    cmds.select(f'tre{i}_leaf2')
    cmds.hyperShade(a=f'tre{i}_leaf2_SN')
    # shader argument for third layer of leaves
    leaf3SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf3_SN')
    cmds.setAttr(f'tre{i}_leaf3_SN.color',0.878,0.694,0.796)
    cmds.select(f'tre{i}_leaf3')
    cmds.hyperShade(a=f'tre{i}_leaf3_SN')
# Define function for creating skeleton
def sansSkeleton(i):
    pfx = f'tre{i}_'
    cmds.select(cl=True)
    for j in range(6):  # Changed loop variable to avoid conflict
        cmds.joint(n=f'tre{i}_{j+1}_jnt')
        cmds.jointDisplayScale(0.5)
        cmds.move(0, 0.8, 0, r=True, os=True, wd=True)
    # Freeze transformations and delete history
    cmds.FreezeTransformations()
    cmds.DeleteHistory()

# Define function for making groups
def makeGroups(i):
    pfx = f'tre{i}_*'
    cmds.select(cl=True)
    for j in range(6):  # Changed loop variable to avoid conflict
        if cmds.objExists(f'tre{j}_*'):  # Checking if object exists before selection       
            cmds.select(f'tre{j}_*', add=True)
            cmds.group(n=f'tre{j}_tree')
    for i in range(6):  # Changed loop variable to avoid conflict
        if cmds.objExists(f'tre{i}_*'):  # Checking if object exists before selection
            cmds.select(f'tre{i}_*', add=True)
            cmds.group(n=f'tre{i}_tree')

# Define function for parenting groups
def makePgroup(i):
    pfx = f'tre{i}_*'
    for j in range(6):  # Changed loop variable to avoid conflict
        if cmds.objExists(f'tre{j}_tree*'):  # Checking if object exists before parenting
            cmds.parent(f'tre{j}_tree')

# Define function to run tree creation process
def runTree(i):
    modelTree(i)
    makeShaders(i)
    sansSkeleton(i)

# Cleanup existing objects
cleanUp()

# Create trees
for i in range(6):
    runTree(i)
    makeGroups(i)
    makePgroup(i)

 

924 Views
3 Replies
Replies (3)
Message 2 of 4

FirespriteNate
Advocate
Advocate

You have several different issues here, I'll try to tackle some of them individually.

 

The first obvious thing is that you have learned to construct this code by manually performing actions and then copying the code that Maya displays for each action and collating it into a linear sequence. This is fine, and a great way to learn, if this is the result being new to Maya and Python, it's an exceptional start! - But you do need to be aware of a few tips when starting out.

 

The main issue is that you are relying too heavily on explicit, hard-coded names (i.e. "tre1_trunk"). When you create a node in Maya, even if you supply an explicit name, there is no guarentee it will be given that name (Maya auto-renames nodes to avoid name clashes where necessary). For this reason you should never assume you know what an object name string will be, you should always capture it as a variable, and then pass these variable names between your functions.

 

Any command that starts with a Capital (uppercase) letter is not a regular command, it's a RunTimeCommand. These are like aliases to mini-functions, but the critical thing to understand is that they take NO arguments and return nothing, so when you do: 

cmds.Triangulate(n='some_name')

that will have no real effect. If you want to supply args, you need to use the real underlying command:, i.e. 

cmds.polyTriangulate(n='some_name')

 

Try to avoid using the "select" command in your scripts just to operate on a node. For example, don't select a node and then issue a move command. Don't do this:

cmds.select(objectName)
cmds.move(0, 4, 0)

Instead, just issue the move command ON the node:

cmds.move(0, 4, 0, objectName)

 

Using a command that creates an object with history actually returns a list of two items, the object and the history node. Even if you forego history, it still returns a list, for example:

model = cmds.polyCylinder()
# Result: ['pCylinder1', 'polyCylinder1'] #

better to do something like this:

modelObj, modelHistory = cmds.polyCylinder()

or, if you never care about the history node, you can do this (which works fine whether you use history or not):

model = cmds.polyCylinder()[0]

 

When you write a function that creates some "thing", especially if you will later need to interact with that thing, the function should RETURN that thing, so you can capture it. For example in your case, your function that creates a tree model should return the name of the generated model(s) so you can capture it and operate on it later (like add it to a group or position it in the scene, etc..)

 

When making a function that performs some operation on an object, don't rely on selection, pass the object in as an argument, for example if you make a function that groups tree parts, pass the relevent parts into the funtion and operate on those items, don't rely on assumed names or current selection.

When using the parent command, don't rely on assumed selection, specify all the items you want to parent AND the parent node, for example: 

cmds.parent(trunk, leaf1, leaf2, leaf3, treeGroup)

or:

cmds.parent(treeParts, treeGroup)

 

It's much more complicated to write a function that creates several different models and then independently makes shaders and groups and tries to apply them to the appropriate model. It's much more sensible to create and apply the shader and group as part of each original model creation. That way you don't have to constantly keep track of so many variables. Think about making the shader creation part of the "makeTree" function for example.

 

I didn't have time to modify the shader parts of your script, and I couldn't figure out what you were trying to do with the two different grouping functionality of your original script, but here's a refactored version containing all the info from above, hopefully this gives you some better tips about avoiding these kinds of issues in future. I also proceduralised the leaf creation somewhat as an example of how you might avoid code duplication in certain situations.

from maya import cmds

# Define function for cleanup
def cleanUp():
    if cmds.objExists('tre*'):
        cmds.delete('tre*')

def makeLeafLayer(i, y, size, rnd):
    leafModel, history = cmds.polyCone(n=f'tre{i}_leaf1', r=1, h=2, sa=8, sh=4)
    # we can always name it "leaf1" and let Maya handle the auto-renaming
    cmds.move(0, y, 0, leafModel)
    cmds.scale(size, size, size, leafModel)
    cmds.polyTriangulate(leafModel)
    cmds.polyMoveVertex(leafModel, random=True, ltz=rnd)
    cmds.polySoftEdge(leafModel, a=0)
    return leafModel

# Define function for creating a tree model
def modelTree(i):
    parts = []    # this will be the list of parts we create and return
    # tree trunk
    trunk, history = cmds.polyCylinder(n=f'tre{i}_trunk', r=0.4, h=4, sa=8, sh=16)
    cmds.move(0, 2, 0, trunk)
    cmds.polyTriangulate(trunk)
    cmds.polySoftEdge(trunk, a=0)
    parts.append(trunk)
    # first layer of leaves
    leaf1 = makeLeafLayer(i, y=4, size=2, rnd=0.110)
    parts.append(leaf1)
    # second layer of leaves
    leaf2 = makeLeafLayer(i, y=5.5, size=1.5, rnd=0.115)
    parts.append(leaf2)
    # third layer of leaves
    leaf3 = makeLeafLayer(i, y=6.5, size=1, rnd=0.120)
    parts.append(leaf3)

    # delete history:
    cmds.delete(parts, constructionHistory=True)
    # Freeze transformations:
    cmds.makeIdentity(parts, t=True, r=True, s=True, a=True)
    # (we don't use the RunTimeCommands as we don't want to rely on selection)
    return parts

# Define function for making shaders
def makeShaders(i):
    pfx = f'tre{i}_'
    # shader argument for tree trunk
    trunkSN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_trunk_SN')
    cmds.setAttr(f'tre{i}_trunk_SN.color',0.137,0.098,0.259)
    cmds.select(f'tre{i}_trunk')
    cmds.hyperShade(a=f'tre{i}_trunk_SN')
    # shader argument for first layer of leaves
    leaf1SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf1_SN')
    cmds.setAttr(f'tre{i}_leaf1_SN.color',0.369,0.329,0.557)
    cmds.select(f'tre{i}_leaf1')
    cmds.hyperShade(a=f'tre{i}_leaf1_SN')
    # shader argument for second layer of leaves
    leaf2SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf2_SN')
    cmds.setAttr(f'tre{i}_leaf2_SN.color',0.623,0.525,0.753)
    cmds.select(f'tre{i}_leaf2')
    cmds.hyperShade(a=f'tre{i}_leaf2_SN')
    # shader argument for third layer of leaves
    leaf3SN = cmds.shadingNode('blinn',asShader=True,n=f'tre{i}_leaf3_SN')
    cmds.setAttr(f'tre{i}_leaf3_SN.color',0.878,0.694,0.796)
    cmds.select(f'tre{i}_leaf3')
    cmds.hyperShade(a=f'tre{i}_leaf3_SN')

# Define function for creating skeleton
def sansSkeleton(i, jointCount=6):
    # this has a global effect, no need to issue it once per iteration
    cmds.jointDisplayScale(0.5)
    # creating a joint parents it under selection, so we need to ensure 
    # nothing is selected when we start
    cmds.select(cl=True)

    allJoints = []    # list of joints we created
    for j in range(jointCount):
        jnt = cmds.joint(n=f'tre{i}_{j+1}_jnt')
        allJoints.append(jnt)
        cmds.move(0, 0.8, 0, jnt, r=True, os=True, wd=True)
    return allJoints[0]    # return the FIRST joint created (root)

# Define function for making groups
def makeGroups(i, meshes, rootJoint):
    grp = cmds.group(n=f'tre{i}_tree', empty=True)
    cmds.parent(meshes, rootJoint, grp)
    return grp

# Define function to run tree creation process
def runTree(i):
    parts = modelTree(i)
    makeShaders(i)
    root = sansSkeleton(i)
    return parts, root

# Cleanup existing objects
cleanUp()

# Create trees
for i in range(6):
    treeParts, treeRoot = runTree(i)
    makeGroups(i, treeParts, treeRoot)

 

0 Likes
Message 3 of 4

mguast20
Explorer
Explorer
I apologize for the lateness of my response but thank you for taking the time to do all of this, it is greatly appreciated!
This was extremely helpful, I have been stuck on this for two weeks straight (I am completely serious).
Would it be bothersome to ask for some more assistance? I need to take my code and make it scatter trees on a ground plane with random scale, rotation and slight color variation.
I have also been struggling with incorporating that into my code.
Thank you again for helping me out with my script! Please let me know if you need my file again.
Message 4 of 4

Kahylan
Advisor
Advisor

Hi there,

 

@FirespriteNatealready gave you some great advice.
I would like to add one more to it. You are very reliant on string formatting and direct names. I would try to do this as little as possible and use variables instead.

You are often already creating these variables, but don't end up using them after all. example your variable pfx:

def modelTree(i):
    pfx = f'tre{i}_'
    # tree trunk
    trunk = cmds.polyCylinder(n=f'tre{i}_trunk', r=0.4, h=4, sa=8, sh=16)

you already created it, but you still write out the prefix in the command itself, instead of doing it like this:

def modelTree(i):
    pfx = f'tre{i}_'
    # tree trunk
    trunk = cmds.polyCylinder(n=f'{pfx}trunk', r=0.4, h=4, sa=8, sh=16)

Also this variable doesn't really make any sense where it is, but it would be great if you changed it to be a keyword argument parameter, because then the user could actually use it to give his own prefix, instead of just giving a number in it. This would also make your cleanup function obsolete, because you wouldn't have name collisions.

def modelTree(i,pfx = f'tre{i}_'):
    # tree trunk
    trunk = cmds.polyCylinder(n=f'{pfx}trunk', r=0.4, h=4, sa=8, sh=16)

 

Now to show you what I mean by using variables instead of direct names as strings, I reworte your "makeshaders" function, since @FirespriteNate didn't have time for that.
Please be aware that his and my coding style are very different, I use a lot more keyworded parameters. Also I tend to work with list indexing and nesting a lot. Its just different styles of working with variables. The code still works, I'd just be aware that you don't get a too many cooks situation here. 😉

I also fixed your problem with random positioning and color changes. Using the random library

from maya import cmds
import random as random

# Define function for cleanup
def cleanUp():
    if cmds.objExists('tre*'):
        cmds.delete('tre*')

def makeLeafLayer(i, y, size, rnd):
    leafModel, history = cmds.polyCone(n=f'tre{i}_leaf1', r=1, h=2, sa=8, sh=4)
    # we can always name it "leaf1" and let Maya handle the auto-renaming
    cmds.move(0, y, 0, leafModel)
    cmds.scale(size, size, size, leafModel)
    cmds.polyTriangulate(leafModel)
    cmds.polyMoveVertex(leafModel, random=True, ltz=rnd)
    cmds.polySoftEdge(leafModel, a=0)
    return leafModel

# Define function for creating a tree model
def modelTree(i):
    parts = []    # this will be the list of parts we create and return
    # tree trunk
    trunk, history = cmds.polyCylinder(n=f'tre{i}_trunk', r=0.4, h=4, sa=8, sh=16)
    cmds.move(0, 2, 0, trunk)
    cmds.polyTriangulate(trunk)
    cmds.polySoftEdge(trunk, a=0)
    parts.append(trunk)
    # first layer of leaves
    leaf1 = makeLeafLayer(i, y=4, size=2, rnd=0.110)
    parts.append(leaf1)
    # second layer of leaves
    leaf2 = makeLeafLayer(i, y=5.5, size=1.5, rnd=0.115)
    parts.append(leaf2)
    # third layer of leaves
    leaf3 = makeLeafLayer(i, y=6.5, size=1, rnd=0.120)
    parts.append(leaf3)

    # delete history:
    cmds.delete(parts, constructionHistory=True)
    # Freeze transformations:
    cmds.makeIdentity(parts, t=True, r=True, s=True, a=True)
    # (we don't use the RunTimeCommands as we don't want to rely on selection)
    return parts

# Define function for making shaders
def makeShaders(parts, partColors = [[0.137,0.098,0.259],[0.369,0.329,0.557],[0.623,0.525,0.753],[0.878,0.694,0.796]], colorRand = [False,False,False,False], randRange = 0):
    """
    This function takes 1 positional Argument parts, must be oftype List, Elements in List must be Strings referring to existing objects in Scene

    Keyword Argument partColors, this argument takes a nested List, containing elements of type list or tuple with 3 or more number type Elements each,
    it is used to provide Colors for each part in parts, if not enough colors are provided, addidional parts will be assigned black shaders

    Keyword Argument colorRand, this argument takes a List, Elements must be of type boolean or binary integers, each booleand will decide
    wether the part in parts with the same index gets a randomized color on S and V value, if not enough Elements are provided, additional parts won't have randomized color

    Keyword Argument randRange, takes variable of type float, randomized colors will be randomised in this value upwards and downwards from given value
    """

    #Store selection so we can restore it after using the hypershade command
    currentSelection = cmds.ls(sl = True)
    #check if there are enough elements in the partColors list, add addidional if needed
    if len(parts) > len(partColors):
        for i in range(0,(len(parts)-len(partColors))):
            partColors.append([0,0,0])

    #check if there are enough elements in the partColors list, add addidional if needed
    if len(parts) > len(colorRand):
        for i in range(0,(len(parts)-len(colorRand))):
            colorRand.append(False)
    #create shaders for each element of parts and connect them
    for i in range(0,len(parts)-1):
        SN = cmds.shadingNode('blinn',asShader=True,n=f'{parts[i]}_SN')
        if colorRand[i] == True:
            cmds.setAttr(f"{SN}.color",partColors[i][0],random.uniform(partColors[i][1]-randRange,partColors[i][1]+randRange),random.uniform(partColors[i][2]-randRange,partColors[i][2]+randRange))
        else:
            cmds.setAttr(f"{SN}.color",partColors[i][0],partColors[i][1],partColors[i][2])
        cmds.select(parts[i])
        cmds.hyperShade(a = SN)
    #restore selection
    cmds.select(currentSelection, r = True)


# Define function for creating skeleton
def sansSkeleton(i, jointCount=6):
    # this has a global effect, no need to issue it once per iteration
    cmds.jointDisplayScale(0.5)
    # creating a joint parents it under selection, so we need to ensure 
    # nothing is selected when we start
    cmds.select(cl=True)

    allJoints = []    # list of joints we created
    for j in range(jointCount):
        jnt = cmds.joint(n=f'tre{i}_{j+1}_jnt')
        allJoints.append(jnt)
        cmds.move(0, 0.8, 0, jnt, r=True, os=True, wd=True)
    return allJoints[0]    # return the FIRST joint created (root)

# Define function for making groups
def makeGroups(i, meshes, rootJoint):
    grp = cmds.group(n=f'tre{i}_tree', empty=True)
    cmds.parent(meshes, rootJoint, grp)
    return grp

# Define function to run tree creation process
def runTree(i,partColors = [[0.137,0.098,0.259],[0.369,0.329,0.557],[0.623,0.525,0.753],[0.878,0.694,0.796]], colorRand = [False,False,False,False], randRange = 0):
    """
    This function takes 1 positional Argument i, type integer

    Keyword Argument partColors, this argument takes a nested List, containing elements of type list or tuple with 3 or more number type Elements each,
    it is used to provide Colors for each part of your tree in order of creation, if not enough colors are provided, addidional parts will be assigned black shaders

    Keyword Argument colorRand, this argument takes a List, Elements must be of type boolean or binary integers, each booleand will decide
    wether the part of the tree, in order of creation gets a randomized color on S and V value, if not enough Elements are provided, additional parts won't have randomized color

    Keyword Argument randRange, takes variable of type float, randomized colors will be randomised in this value upwards and downwards from given value

    Current order of creation: trunk, leaf1, leaf2, leaf3
    """
    parts = modelTree(i)
    makeShaders(parts,colorRand= colorRand,partColors = partColors, randRange= randRange)
    root = sansSkeleton(i)
    return parts, root

# Cleanup existing objects
cleanUp()

# Create trees
treegroup = cmds.group(n= "tree_group",em = True)
for i in range(6):
    treeParts, treeRoot = runTree(i, colorRand = [False,True,True,True],randRange = 0.2)
    grp = makeGroups(i, treeParts, treeRoot)

    cmds.move(random.uniform(20,-20),0,random.uniform(20,-20),grp)
    cmds.parent(grp, treegroup)

 

I'd still ask you to give @FirespriteNate the Accepted solution for this post, as he fixed your inital problems and also did most of the work here.

I hope it helps! 🙂

 

0 Likes