UE5 Python Automation | Making a material from selected texture assets

Preface: Afaik creating a single master material for common assets, and then creating material instances to assign unique textures is more performant than creating many master materials. This tool was created to meet the needs of the current project’s workflow and scope. Better solutions are likely available.

Working with many artists this year, I noticed that a significant amount of time was spent creating the exact same generic master materials for the various different assets. To speed up this process (as well as learn more about python scripting in UE5), I decided to create a Python script and Editor Utility Widget to automate this process, allowing new master materials (that require BaseColor, Normal and AORM attributes) to be generated.

This tool allows either automatic name generation, or custom name input for the material. All the user has to do is select the three texture assets, assign a name and press “Make Material”

The Blueprint for the Editor Utility Widget, as well as the original .py scripts I developed can be found below, if you decide this tool could be useful. Thanks to Matt Lake and AtomicPerception for providing resources essential to this tool.

import unreal

def MakeMaterial(mat_name):
    # Get the selected textures
    sel_assets = unreal.EditorUtilityLibrary.get_selected_assets()

    if len(sel_assets) == 0:
        unreal.log_warning("No asset selected!")
        unreal.EditorDialog.show_message("No Assets Selected!", "No Assets Selected!", unreal.AppMsgType.OK, unreal.AppReturnType.NO)
        exit()

    # Get folder of the currently selected assets
    asset_path = sel_assets[0].get_path_name()
    folder_path = unreal.Paths.get_path(asset_path)

    # Create new Material asset in the current folder
    assetTools = unreal.AssetToolsHelpers.get_asset_tools()
    mat_closure = assetTools.create_asset("M_" + mat_name, folder_path, unreal.Material, unreal.MaterialFactoryNew())

    # Itterate through textures
    for tex_asset in sel_assets:
        if tex_asset.get_class().get_name() != "Texture2D":
            continue

        # Get the asset name and folder
        asset_name = tex_asset.get_name()
        asset_folder = unreal.Paths.get_path(tex_asset.get_path_name())

        # Make a texture diffuse node.
        if asset_name.endswith("_BaseColor"):
            ts_node_diffuse = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionTextureSample,-384,-200)
            unreal.MaterialEditingLibrary.connect_material_property(ts_node_diffuse, "RGBA", unreal.MaterialProperty.MP_BASE_COLOR)
            ts_node_diffuse.texture = tex_asset

        # Make a texture normal node.
        if asset_name.endswith("_Normal"):
            ts_node_normal = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionTextureSample,-384,200)
            unreal.MaterialEditingLibrary.connect_material_property(ts_node_normal, "RGB", unreal.MaterialProperty.MP_NORMAL)
            # Change this sampler color sample type to work with normal types.
            ts_node_normal.sampler_type = unreal.MaterialSamplerType.SAMPLERTYPE_NORMAL
            ts_node_normal.texture = tex_asset

        # Make a texture AO/Roughness/Metallic node.
        if asset_name.endswith("_OcclusionRoughnessMetallic"):
            ts_node_AORM = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionTextureSample,-384,500)
            unreal.MaterialEditingLibrary.connect_material_property(ts_node_AORM, "R", unreal.MaterialProperty.MP_AMBIENT_OCCLUSION)
            unreal.MaterialEditingLibrary.connect_material_property(ts_node_AORM, "G", unreal.MaterialProperty.MP_ROUGHNESS)
            unreal.MaterialEditingLibrary.connect_material_property(ts_node_AORM, "B", unreal.MaterialProperty.MP_METALLIC)
            ts_node_AORM.texture = tex_asset

def GetNameFromTextures():
    # Get the selected textures
    sel_assets = unreal.EditorUtilityLibrary.get_selected_assets()

    # Check assets have been selected
    if len(sel_assets) == 0:
            unreal.log_warning("No asset selected!")
            unreal.EditorDialog.show_message("No Assets Selected!", "No Assets Selected!", unreal.AppMsgType.OK, unreal.AppReturnType.NO)
            exit()

    # Create an asset index counter
    asset_index = 0

    # Itterate through all selected assets, check if they are texture assets and increment the index couint if they are not, and the array length is shorter than the counter
    for asset in sel_assets:
        if asset.get_class().get_name() != "Texture2D":
            if len(sel_assets) < asset_index:
                asset_index += 1
        else:
            break

    # Check if the asset is a texture, remove special chars from the name and set the asset name as the material name return
    if sel_assets[asset_index].get_class().get_name() == "Texture2D":
        mat_name = str(sel_assets[asset_index].get_name())
        mat_name = "_".join(mat_name.split("_")[:-1])
        return mat_name
        unreal.log(mat_name)
    else:
        mat_name = "null"
        return mat_name