Is it possible to use Python for editing material graphs?

As stated in the title… I’ve been trying to figure out how to use Python to edit materials. So I’d like to be able to do operations like:

  • Check if a material base color has an incoming connection, and if so, what it is connected to.
  • Create nodes (constant, multiply, texture sample, etc)
  • Assign a texture to a texture sample node
  • Disconnect nodes from each other
  • Delete nodes
  • Edit node attribute values

I have some Python experience, but I’m very new to Unreal. I’ve been searching for examples of Python code that can do any of those operations, but so far I haven’t been able to find anything. Could anyone point me to any examples showing how to do any of the above?

Thanks

Hi,

I’m looking for the same information, and am bumping up this thread in case someone has an idea. I essentially want to create a python script to look into all children of a material to see what overrides have been made. So for a start, getting to access a material and it’s parameters would be great.

Thanks for the help.

Think I can share two functions from our library


def material_instances():
    """ Get a list of all assets that are material instances. """
    instances = list()
    path = "/Game/"

    for asset in unreal.EditorAssetLibrary.list_assets(path):
        asset_data = unreal.EditorAssetLibrary.find_asset_data(asset)
        if asset_data.asset_class == "MaterialInstanceConstant":
            instances.append(asset)

    return instances

def get_static_switch_values(material):
    """ Iterate over Static Bool & Switch Material Expressions for the given 
    material to return a dictionary for parameter names and their default values.

    :param material: the material object we want to get parameters from
    :type unreal.Material:
    :returns: {foo: False, bar: True}
    """

    parameters = dict()

    filter_on = (
        unreal.MaterialExpressionStaticBoolParameter,
        unreal.MaterialExpressionStaticSwitchParameter
    )

    # Iterate loaded objects that are child of material
    # and matches filter types.
    it = unreal.ObjectIterator()
    for x in it:
        if x.get_outer() == material:
            if isinstance(x, filter_on):
                name = str(x.get_editor_property("parameter_name"))
                value = x.get_editor_property("default_value")
                parameters.update({name: value})

    return parameters

Was some time since I worked with it but want to remember you could get/set most values on the expressions. Run pythons help() on material instance constant and its material expression to see the available methods.

About creating and connecting expressions the unreal.MaterialEditingLibrary have methods for that.

2 Likes

Thank you. I’ve come up with a basic set of functions to help in reporting material instance info. Posting in case it helps anyone


import unreal
import csv

def get_instance_materials(asset_path, material_name):
    master_list = list()
    static_override_combo_dict = {}
    all_references_list = list()
    asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
    unreal_project_dir = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
    all_assets = asset_registry.get_assets_by_path(asset_path, recursive=True)

    for asset in all_assets:
        loaded_parent_mat = unreal.EditorAssetLibrary.load_asset(asset.get_full_name())
        if (asset.asset_class == 'Material' or asset.asset_class == 'MaterialInstanceConstant') and asset.asset_name == material_name:
            master_list = get_all_instances(loaded_parent_mat)
            static_override_combo_dict = get_material_static_override_groups(loaded_parent_mat, master_list, static_override_combo_dict)
            all_references_list = get_all_references(asset_path + '/' + material_name)

    csv_file = unreal_project_dir + material_name + '_instances.csv'
    with open(csv_file, 'w+') as f:
        f.write(asset_path + '/' + material_name + '
')
        write_log_recursively(f, master_list, 1)

    csv_file = unreal_project_dir + material_name + '_instances_static_groups.csv'
    with open(csv_file, 'w+') as f:
        write_dict_to_file(f, static_override_combo_dict)

    csv_file = unreal_project_dir + material_name + '_references.csv'
    with open(csv_file, 'w+') as f:
        f.write(asset_path + '/' + material_name + '
')
        for a in all_references_list:
            f.write('	' + a + '
')

    log_file = unreal_project_dir + material_name + '_report.log'
    generate_report(material_name, master_list, all_references_list, static_override_combo_dict, log_file)

def generate_report(material_name, all_instances_list, all_references_list, override_dict, log_file):
    total_instances = 0
    direct_instances = 0
    total_references = 0
    unique_static_combos = 0

    total_instances = get_instance_count(all_instances_list, True)
    direct_instances = get_instance_count(all_instances_list, False)
    unique_static_combos = len(override_dict.keys())

    with open(log_file, 'w+') as f:
        f.write(material_name + ' total instances:	' + str(total_instances) + '
')
        f.write(material_name + ' total direct instances:	' + str(direct_instances) + '
')
        f.write(material_name + ' total references:	' + str(len(all_references_list)) + '
')
        f.write(material_name + ' Unique Static Switches:	' + str(len(override_dict.keys())-1) + '
')

def get_instance_count(all_instances_list, recursive):
    count = 0
    for a in xrange(0, len(all_instances_list)):
        if type(all_instances_list[a]) == str:
            count += 1
        elif recursive:
            count += get_instance_count(all_instances_list[a], True)
    return count

def get_all_references(parent_material):
    mat_children_paths = unreal.EditorAssetLibrary.find_package_referencers_for_asset(parent_material, load_assets_to_confirm=False)
    return mat_children_paths

def write_dict_to_file(log_file, my_dict):
    for key, values in my_dict.iteritems():
        log_file.write(key + '
')
        for v in values:
            log_file.write('	' + v + '
')

def write_log_recursively(log_file, my_list, depth):
    for x in xrange (0,len(my_list)):
        for d in xrange(0,depth):
             if type(my_list[x]) == str:
                log_file.write('	')
        if type(my_list[x]) == str:
            log_file.write(my_list[x] + '
')
        else:
            write_log_recursively(log_file, my_list[x], depth + 1)

def get_material_static_override_groups(loaded_parent_material, all_instances_list, return_dict):
    if len(return_dict) == 0:
        static_override_combo_dict = {}
    else:
        static_override_combo_dict = return_dict

    for i in all_instances_list:
        if type(i) == str:
            instance_static_override_list = list()
            loaded_instance = unreal.EditorAssetLibrary.load_asset(i)
            instance_static_params = unreal.MaterialEditingLibrary.get_static_switch_parameter_names(loaded_instance)

            for s in instance_static_params:
                default_static_switch_value = unreal.MaterialEditingLibrary.get_material_default_static_switch_parameter_value(loaded_parent_material, s)
                static_switch_instance_value = unreal.MaterialEditingLibrary.get_material_instance_static_switch_parameter_value(loaded_instance, s)
                if default_static_switch_value != static_switch_instance_value:
                    instance_static_override_list.append(str(s))

            override_string = ''
            for t in instance_static_override_list:
                if override_string == '':
                    override_string = override_string + t
                else:
                    override_string = override_string + '_' + t

            if override_string == '':
                override_string = 'NoStaticOverride'

            if override_string in static_override_combo_dict:
                static_override_combo_dict[override_string].append(i)
            else:
                static_override_combo_dict[override_string] = *
        else:
            get_material_static_override_groups(loaded_parent_material, i, static_override_combo_dict)
    return static_override_combo_dict

def get_all_instances(parent_material):
    all_instances = list()
    child_instances = unreal.MaterialEditingLibrary.get_child_instances(parent_material)

    for ch in child_instances:
        loaded_instance = unreal.EditorAssetLibrary.load_asset(ch.get_full_name())
        all_instances.append(str(ch.package_name))
        if len(unreal.MaterialEditingLibrary.get_child_instances(loaded_instance)) > 0:
            all_instances.append(get_all_instances(loaded_instance))
    return all_instances

Seems like a small treasure trove of code! Thank you both for taking the time to respond.

I can say also, Epic actually already have an amazing tool for what I assume you’re doing - Material Analyzer. I too reinvented the wheel before finding this tool. Was nice introduction to python in Unreal but thankfully I found it before I got around to work out how to properly display the data.

Yes. It was my inspiration of sorts. What I’m eventually trying to do is reduce shader compile times by reducing the number of unique static switches. I kind of had to get these commands to continue.

Hey I’m looking into actually getting a list of nodes (expressions) in a material and can’t find a way.
It seems that in C++, the material instance has an Expressions property that contains a list of nodes.
That is however not exposed to Python.
Has anyone been able to iterate through the nodes of a material (not a material instance)?

Thanks

I don’t know if this helps, but you can try:

    asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
    all_assets = asset_registry.get_assets_by_path(asset_path, recursive=True)

    if asset.asset_class == 'Material' and asset.asset_name == material_name:
        loaded_material = unreal.EditorAssetLibrary.load_asset(asset.get_full_name())

    parameters = unreal.MaterialEditingLibrary.get_static_switch_parameter_names(loaded_material)

    for p in parameters:
        static_switch_params = unreal.MaterialEditingLibrary.get_material_default_static_switch_parameter_value(loaded_material, p)
    print static_switch_params

print_static_switch_params("/Game/Art/Shaders/", "MyMaterial_MAT")```](https://docs.unrealengine.com/en-US/PythonAPI/class/MaterialEditingLibrary.html)

Disclaimer: I've extracted this code from my existing script, so there might some issues with variable referencing etc., but this should give you an idea. More info on the MaterialEditingLibrary can be found here: [https://docs.unrealengine.com/en-US/PythonAPI/class/MaterialEditingLibrary.html.](https://docs.unrealengine.com/en-US/PythonAPI/class/MaterialEditingLibrary.html)

Hope this helps.

Hey Vikers1510,

Thanks but that’s not what I’m looking for. The MaterialEditingLibrary module has methods to access Material Parameters but that’s not what i need. I’d like to loop through all the nodes in the graph. The way you access them in C++ is through the Expressions property of the material which doesn’t seem to be accessible through Python.

stat3d, the code I posted using ObjectIterator loops over all nodes in the material, just that my code filters for material expressions I was looking for.

Good morning. May I poke at this topic some more.

The goal in mind is to integrate pipeline tools into the Unreal system with the rest of our tools.
I believe what stat3d was aiming for is Pythonic method find the entire tree of what is connected to a Material. Texture2D nodes, OneMinus nodes, Multiply nodes.


unreal.MaterialEditingLibrary.connect_material_property(from_expression,
                                                        from_output_name,
                                                        property)

seems to be the closest in the Python module to being able to access what is connected to a [FONT=Courier New]MaterialProperty.
Searching the documentation, only [FONT=Courier New]unreal.MaterialProperty, and the above [FONT=Courier New]MaterialEditingLibrary method seem to make use of [FONT=Courier New]MaterialProperty.

For Python pipeline tools, we would need to be able to explore all the connections, disconnect and connect them.
Depending upon some lookdev needs, we will need to insert other material nodes such as [FONT=Courier New]OneMinus, or discover the [FONT=Courier New]Constant3Vector to set or verify its color.

The desire is to use the EPIC provided “[FONT=Courier New]unreal” module to be able to do this.
Also knowing the [FONT=Courier New]unreal module is really new, there is hopes there is some how-tos, examples, or confirmations that the current module can or cannot do some of these functions.

Any pointers or questions is greatly appreciated.
Thanks everyone

Hi, I meet the same problem, have you solved this problem? Could you give me some advice? Thank you.

Thank you!
Golden in a roundabout way!
Would be really nice to have this inside the unreal.MaterialEditingLibrary module for easier access and discoverability.
Thanks for posting!

Hello, I have managed to create a VERY simple material graph where i only connect the base color in Unreal 5.3. I know it is a bit late in the run of this thread, but i hope it helps as a reference. The most important things are in this link: unreal.MaterialEditingLibrary — Unreal Python 5.3 (Experimental) documentation (unrealengine.com)

'''Create a new material'''
def material(name, color):
    assetTools = unreal.AssetToolsHelpers.get_asset_tools()

    # create new material asset
    mat = assetTools.create_asset(name, "/Game/Materials", unreal.Material, unreal.MaterialFactoryNew())
    # create a new node to the material graph
    exp = unreal.MaterialEditingLibrary.create_material_expression(mat, unreal.MaterialExpressionConstant3Vector, -450, 250)
    # set the material color value
    exp.constant = color
    # link the node to the material color property
    print(unreal.MaterialEditingLibrary.connect_material_property(exp, "", unreal.MaterialProperty.MP_BASE_COLOR))
    # save the changes
    unreal.MaterialEditingLibrary.recompile_material(mat)
    return mat```