Maya related : listing all connections in a surface shader?

Hi. I’m not quite sure how to frame the question I’m asking, so if this has popped up before and I missed it during my search, I apologise.

This is related to Arnold surface shaders, Maya 2018.

I am writing a python script to look through all the nodes that are connected to a surface shader. The purpose is basically to check naming.

What I am discovering however, is that the listConnections or listHistory would not list everything. What I have noticed is listConnections and listHistory will list everything up until something like a remapHSV is reached. At that point it stops, and I have to run another loop to find specifically to track down the remapHSV and do another listConnections.

The issue I’m having is that some of the shaders are several layers deep with the remapHSV, and even if I run a second listConnections loop, it’s not going to catch everything.

I wanted to ask if there’s some other way of traversing through the entire graph and grab everything. From looking around there’s an OpenMaya thing called mtlDependencyGraph, that I might be able to try?

Unfortunately I am completely unschooled in OpenMaya. I would appreciate any sort of help that might point me in the right direction to solving this problem.

Thank you and have a great weekend.

Regards.

What nodes are you searching for exactly? You say everything, but do you mean geometry?

I tried listConnections on a basic Maya surfaceShader (no idea about Arnold), and that doesn’t traverse very far. But if you query geometry outputs, you can get the shadingEngine, and that is one connection away from the surfaceShader.

So rather than branching out from the surfaceShader, maybe you could filter all the geo that has the surface shader. But that depends what you are actually trying to find. But regardless, maybe searching in the opposite direction will help you.

Maybe list comprehension was a poor choice here, but…

import pymel.core as pm

targetShader = pm.PyNode('surfaceShader1')

geoMatches = [
    x for x in pm.ls(type='mesh')
    if x.listConnections(type='shadingEngine')
    if x.listConnections(type='shadingEngine')[0].listConnections(type='surfaceShader')
    if x.listConnections(type='shadingEngine')[0].listConnections(type='surfaceShader')[0] == targetShader
    ]
print(geoMatches)

(Hmm, I actually am not sure how that list comprehension works. I will try to unpick it)

I mean the nodes that are connected to the shader in a shading network. It wouldn’t matter if the top node was geometry or shading engine or surface shader. It’s really weird. At certain parts, the connection simply ends. Most often for me, it happens when the remapHSV is plugged into the spec roughness channel. I’m wondering if it’s because only a color channel is plugged in or something?

I’ve attached a couple of screen shot to illustrate what I’m talking about.

In the first screen, you can see in the attribute history on screen left that there are 7 inputs (minus the ColorMTGGlobals). In the node editor window, you can see that the graphing ends at the remapHSV from the spec roughness channel.

In the second screen, you can see that there are actually a few more nodes behind it. Those will only appear if I also select the remapHSV node from the spec roughness channel, and graph those two nodes at the same time.

At this point I’m just guessing it’s because, as I mentioned earlier, because the connection is non-standard (spec roughness takes outAlpha, but if it’s an outColor connection, it can only take 1 color channel).

I honestly have no idea though.

The way the node editor behaves with graphing that network is pretty similar to how listHistory and listConnection will show the nodes. I have to make a separate listConnection command to see what’s behind.

At this point, the only solution I can think of is a brute force approach to iterate over any present node, find anything that resembles a remapHSV, THEN make another loop to iterate over the first level of nodes.

I’m not sure how to structure the loop though, and what it will look like, because I have no way of telling before hand how many layers deep the shading network could bury those remapHSV nodes.

Any help would be greatly appreciated.

Hey bud,

You could create a recursive function and loop incoming connections indefinitely like so:

import maya.cmds as cmds

children = {}

def _traverse(node, children):
    
    connections = cmds.listConnections(
        node, 
        source=True, 
        destination=False, 
        skipConversionNodes=True) or {}
    
    for child in connections:
        children[child] = {}
             
def get_nodes(node, children):
    
    _traverse(node, children)
    
    for child in children:
        _traverse(child, children[child])

get_nodes("aiStandardSurface1", children)
    
print children  

# >> {u'bump2d1': {}, u'remapHsv1': {u'remapHsv2': {}}}

You can make this more elaborate - but all I’m doing is building dictionaries of dictionaries. The nesting implies the depth - you could capture anything like this - recursion for the win here :slight_smile:

-c

Hey Chalk,

I think I can understand this! Thanks a lot, really appreciate the help! This is recursion, you say?

Would you mind pointing to some reading materials?

Regards

This seems a good resource on it - essentially a recursive function is one that calls itself: https://www.programiz.com/python-programming/recursion

-c

Hey Chalk, thanks for the link.

I tried out a few things yesterday.

So what I noticed is that I could get what I need with a listHistory, provided that the nodes are linked properly.

The problem seem to be that the nodes are not connected in the right way. For instance, remapHSV outputs a color value, and specRoughness takes outAlpha.

The usual way to do it is to connect one of the RGB channel into the specRoughness. The weird thing is that if that is done through the connection editor, then it gets messed up. Some other channel is used go link to specRoughness. In the node editor, the remapHSV to specRoughness is not showing an out R(or G or B) into the specRoughness, but something else (not sure what).

If I manually pull the outR into the specRoughness, it shows up in the history and I can find it through listHistory.

I tried the sample code you gave Chalk. I think I understand the concept, although my coding skills isn’t good enough to get it to loop I think.

The sample code runs through 2 layers, then it stops running. If I nest another two loops inside the main loop I can get it to something like 6 layers before it stops, although the dictionary looks like a complete mess after that.

I’m probably missing something. I appreciate the time you have invested in my questions so far. If you have the time to help out with another explanation about how to code recursion for an amateur scripter, I would be really grateful.

Many thanks, and if anyone else care to share their knowledge, that would be great!

Regards.

Edit - made a mistake, details below…

Hi,

What code are you doing to connect the remapHsv.outColorR attribute to the specRoughness?- I tested both the connection editor and via code successfully:

import maya.cmds as cmds

# Create the nodes

base_shader_node = cmds.createNode("aiStandardSurface")

remap_hsv_node_01 = cmds.createNode("remapHsv")

cmds.connectAttr(
    "{0}.outColorR".format(remap_hsv_node_01),
    "{0}.specularRoughness".format(base_shader_node))
    
    
remap_hsv_node_02 = cmds.createNode("remapHsv")

cmds.connectAttr(
    "{0}.outColor".format(remap_hsv_node_02),
    "{0}.color".format(remap_hsv_node_01))


file_node_01 = cmds.createNode("file")

cmds.connectAttr(
    "{0}.outColor".format(file_node_01),
    "{0}.color".format(remap_hsv_node_02))
    
place_2d_node_01 = cmds.createNode("place2dTexture")

cmds.connectAttr(
    "{0}.coverage".format(place_2d_node_01),
    "{0}.coverage".format(file_node_01))

Ack! - I made a mistake! - i forgot to call the get_nodes code inside itself (the recursive part) and was only calling the traverse once - this is fixed now:

import maya.cmds as cmds

def _traverse(node, children):
    
    connections = cmds.listConnections(
        node, 
        source=True, 
        destination=False, 
        skipConversionNodes=True) or {}
    
    
    for child in connections:
        children[child] = {}

        
def get_nodes(node, children):
    
    _traverse(node, children)
    
    for child in children:
        get_nodes(child, children[child])

children = {}
get_nodes("aiStandardSurface1", children)

print children

# >> {u'remapHsv1': {u'remapHsv2': {u'file1': {u'place2dTexture1': {}, u'defaultColorMgtGlobals': {}}}}}

So i have a traverse function _traverse - which basically gets child nodes plugged into a give node. We supply a children argument as essentially a ‘by reference’ to basically pass data to it - specifically mutable data - that is data that is changeable.

Function scope is inherent to whats inside it - so you need a way to change external data given to it. Its why if you have a variable at the base scope of a module - (a python file) functions within are able to access it (essentially because they inherit the outer scope):

a = 10

def myFunc():
    
    return a
    
print myFunc()

# >> 10

…but crucially cannot change it:


a = 10

def myFunc():
   a = 20

myFunc()
print a

# >> 10

A is immutable and doesn’t have any mutability - i.e. we can’t change it within the internal scope. In classes this changes a little basically your binding the class instance with the self which references variables of the instance. So what is a mutable type? Things like lists, and dictionaries - they can be appended. So something like this will work:

a = []

def myFunc(val):
    
    val.append(20)
    
myFunc(a)
print a

# >> [20]

When we call the get_nodes on itself - we’re passing in the children dict into itself indefinitely - take a hierarchy like so:

a
b c
de fg

When we call the get nodes on a we pass the children dictionary to it - because its mutable we collect b and c into it. Because children is a dictionary, b and c become keys added to the dictionary and there values are dictionaries themselves!:

{"b": {}, "c": {}}

Now crucially in our loop we call this code:

    for child in children:
        get_nodes(child, children[child])

So what were doing is for each child in the children dictionary we’ll run the function itself again on - ie. we call get_nodes on b and c. But instead of passing our children dictionary we’ll pass the dictionaries of its children i.e. children[b]:

note* calling dict[key] will return the value of the key.

So when we call get_nodes(child, children[child]) - what we’re really saying is get_nodes(“b”, {}} - So b’s value becomes the new children dictionary i.e the mutable value we’re adding too!

{"b": {"d":{}, "e": {}}, "c": {"f":{}, "g": {}}

Because b and c values are dictionaries - we then keep continuing to call the function.

I.e. Pass a mutable variable to the function and for each child of the viable call the function.

The Fibonacci sequence is a perfect example of this - its basically a recurve function adding the last value to create the next - creating a number sequence that can reflect nature:

1 1 2 3 5 8 13 21 …

1 + 0 = new 1
new 1 + previous 1 = new 2
new 2 + previous 1 = new 3
new 3 + previous 2 = new 5

Hope this helps - It can be tricky to distill scope knowledge :slight_smile:

-c

1 Like

Hey Chalk,

Oh man, this makes total sense! Thanks very much for putting in all this effort Chalk.

I did a few tests and it works!

Regarding what I was doing with the shaders:

The shaders are already built. What I wanted to do was rename the connected nodes based on the name of the surface shader, to get rid of that place2dtexture123456 thing and make the shaders neater and easier to go through.

The listHistory/Connections always just stops at certain nodes though (remapHSV into anything with an alpha input) so I was trying to figure out a way to get to those nodes.

Thanks so much again Chalk, hope your weekend was great!