List all materials used in scene

Modelers are handing me game meshes with multiple materials, which we need to break up for the engine.
so I wish script some solutions.

I want to list all materials used by meshes in the scene.
This is what I have, it seems to work, but Maya materials is a bit new for me
so I am posting for an stupidity check.

def list_scene_materials():
    """
    list all materials used by geometry in the scene
    """
    scene_materials=[]
    all_sgs=pm.ls(type='shadingEngine')
    for sg in all_sgs:
        # if an sg has 'sets' members, it is used in the scene
        if pm.sets(sg, q=True):
            materials = pm.listConnections('{}.surfaceShader'.format(sg))
            if materials:
                scene_materials.extend(materials)
    return list(set(scene_materials))

The mapping between shaderGroups, materials and meshes is a bit confusing.

I think it works like this:
when a mesh uses multiple materials
each material must plug into the surface shader input of a unique shader group
(I assume other inputs like Volume or Displacement won’t get used for animated game meshes)
each shader group then plugs into an ObjectGroups element of the shape node
(I assume ObjectGroups = faces in this case)

Does my code have any problems?
Are my assumptions flawed?

I think you’re assumptions are good, that’s basically how I’ve solved the problem in the past.

Though I would do one thing a bit differently, instead of building up a list in the function, I would build it as a generator. That way the calling code can decide if it needs the full collection, or if it can get away with just iterating over it.

from maya import cmds
# Doing it with purely cmds
def get_materials_in_scene():
    for shading_engine in cmds.ls(type='shadingEngine'):
        if cmds.sets(shading_engine, q=True):
            for material in cmds.ls(cmds.listConnections(shading_engine), materials=True):
                yield material

import pymel.core as pm

# Pymel version
def get_materials_in_scene():
    # No need to pass in a string to `type`, if you don't want to.
    for shading_engine in pm.ls(type=pm.nt.ShadingEngine):
        # ShadingEngines are collections, so you can check against their length
        if len(shading_engine):
            # You can call listConnections directly on the attribute you're looking for.
            for material in shading_engine.surfaceShader.listConnections():
                yield material


# And because it is stupid, here it is as a one liner
sorted(set(cmds.ls([mat for item in cmds.ls(type='shadingEngine') for mat in cmds.listConnections(item) if cmds.sets(item, q=True)], materials=True)))
5 Likes

Never knew pymel shadingengines were collections - nice tips in this snippet!

Ugh that one-liner is atrocious though, hahaha

I should note, that you can’t just do an if shading_engine: check against them. As that will always return True, you have to explicitly check their size with len. Though if shading_engine.entities(): will work just fine.

I know right?

You could list all materials via the ls -mat flag.

materials = pm.ls(mat=True)

I think this is the best one-liner.

But for your use case you may want only material used by objects you’re exporting. This is a different problem.

Yeah, none of the cases actually start from a shape node here, but its pretty easy to adapt them for that.

Also none of the “all in scene” cases account for “is the material connected to a shading engine” or “is the shading engine connected to a shape”, because it is rather easy to get orphan nodes in both cases.