Bootstrap Chameleon Logo

Manipulate Material Expression Nodes Of Material With Python In UE

Beside this tutorial How to manipulate User Defined ENum, Struct, DataTable with Python in Unreal Engine,there is also a common requirement to use Python to handle materials and material functions in Unreal Engine.

This article will about:

  • Create/Delete Material and Material Expression
  • Connect The Expressions
  • Add Custom Menu for Material Editor
  • Get deep detail from expressions and material, for instance, shadermap and hlsl code.

The MaterialEditingLibrary in Unreal Engine 5 has dozens of material APIs for python, and can do lots with material and MF, but there also something that can't handle well.

For example:

  • Can't connect expressions to the material property: "World Position offset" The Enum value MP_WorldPositionOffset was marked as "Hidden" in c++, so we can use this Enum in python.

  • Can ddd Input/Output pins for Material Expressions: Get/SetMaterialAttributes.

An image of the Get/SetMaterialAttribute Node in the Material Editor of Unreal Engine

I can understand UE has lots of good reasons for hiding that Enums, which can avoid a lot of troubles. For instance, if we foolishly set the shader mode to MSM_Strata (DisplayName="Strata", Hidden), the editor will crash immediately. But hiding the options and enumerations that users can manipulate through the UI also adds an extra workload for automation task. And that's what TAPython is for.

Right now,with MaterialEditingLibrary and PythonMaterialLib of TAPython,we can script almost every material operations in Python. My goal is to make it 100% scriptable and automatable.

Create

Create a Material

The code below will create a material named M_CreatedByPython at "/Content/CreatedByPython".

asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
my_mat = asset_tools.create_asset("M_CreatedByPython", "/Game/CreatedByPython", unreal.Material, unreal.MaterialFactoryNew())
unreal.EditorAssetLibrary.save_asset(my_mat.get_path_name())

Create a Material Function

Creating a material function is similar to creating a material.

asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
my_mf = asset_tools.create_asset("MF_CreatedByPython", "/Game/CreatedByPython", unreal.MaterialFunction, unreal.MaterialFunctionFactoryNew())

A snapshot of a material in the UE Editor created by Python code

Add Material Expression

  • Add a simple material expression node

Adding an Add node in my_mat, and assign it to the variable node_add

Note that most of the methods or functions described in this article will not trigger a refresh of the material editor UI. Therefore, if you open the current material in the Material editor, you need to close the window and then open it again to see the newly added node.

node_add = unreal.MaterialEditingLibrary.create_material_expression(my_mat, unreal.MaterialExpressionAdd, node_pos_x=-200, node_pos_y=0)

An image of the Add Node in the Material Editor of Unreal Engine

Adding a material node to a material function is similar, except that the method called is unreal.MaterialEditingLibrary.create_material_expression_in_function.

node_add = unreal.MaterialEditingLibrary.create_material_expression_in_function(my_mf, unreal.MaterialExpressionAdd)
  • Add a TextureSample

Adding a TextureSample Expression,and set the texture using set_editor_property

node_tex = unreal.MaterialEditingLibrary.create_material_expression(my_mat, unreal.MaterialExpressionTextureSampleParameter2D
            , node_pos_x=-600, node_pos_y=0)

texture_asset = unreal.load_asset("/Game/StarterContent/Textures/T_Brick_Clay_Beveled_D")
node_tex.set_editor_property("texture", texture_asset)

An image of the Add and Texture Node in the Material Editor of Unreal Engine

  • Add a Material Function node

The class type of Material Function Expression is:MaterialExpressionMaterialFunctionCall,Then we also need to set the MF asset, via set_editor_property.

node_break = unreal.MaterialEditingLibrary.create_material_expression(my_mat, unreal.MaterialExpressionMaterialFunctionCall, -600, 300)
node_break.set_editor_property("material_function", unreal.load_asset("/Engine/Functions/Engine_MaterialFunctions02/Utility/BreakOutFloat2Components"))

An image of the Break Node in the Material Editor of Unreal Engine

The above examples using the MaterialEditingLibrary that comes with the Unreal engine. Some of the following will require PythonMaterialLib in TAPython.

Add special material nodes

Some material nodes are special that they have input and output pins that user can add. The data of the input and output pins are stored in FGuid. Simply setting the value of that will not trigger the adding pins task and will cause an error.

So, we need to use the add_input_at_expression_set_material_attributes in PythonMaterialLib which will both add the input and pins for material editor.

node_sma = unreal.MaterialEditingLibrary.create_material_expression(my_mat, unreal.MaterialExpressionSetMaterialAttributes, node_pos_x=500, node_pos_y=0)
property_names = ["MP_Specular", "MP_Normal", "MP_WorldPositionOffset", "MP_CustomData0", "MP_CustomizedUVs0"]

for mp_name in property_names:
    unreal.PythonMaterialLib.add_input_at_expression_set_material_attributes(node_sma, mp_name)

# same with MaterialExpressionGetMaterialAttributes
node_gma = unreal.MaterialEditingLibrary.create_material_expression(my_mat, unreal.MaterialExpressionGetMaterialAttributes, node_pos_x=200, node_pos_y=0)
for mp_name in property_names:
    unreal.PythonMaterialLib.add_output_at_expression_get_material_attributes(node_gma, mp_name)

An image of the Get/SetMaterialAttribute Node in the Material Editor of Unreal Engine

Connect

Connect Material Expression to Material Property

  • Connect a material expression's output to Material Property
# use MaterialEditingLibrary
unreal.MaterialEditingLibrary.connect_material_property(from_expression=node_add
                                                    , from_output_name=""
                                                    , property_=unreal.MaterialProperty.MP_BASE_COLOR)

An image of connecting the Add Node to the Base Color in the Material Editor of Unreal Engine

  • Connect expression's output to some "Hidden" Material Property, for instance, WorldPositionOffset

As of now(UE 5.0.3), unreal.MaterialProperty does not contain any enums items such as MP_WorldPositionOffset that are marked hidden in CPP, so we can't connect expression to it with the function above.

Then we can use the function of the same name that in PythonMaterialLib. The third argument of this function's type changed to string.

# use PythonMaterialLib
unreal.PythonMaterialLib.connect_material_property(from_expression=node_add
                                                , from_output_name=""
                                                , material_property_str="MP_WorldPositionOffset")

tips: When the input or output node names from_output_name or to_input_name are empty strings, the first input or output pin is used by default.

An image of connecting the Add Node to 'World Position Offset' in the Material Editor of Unreal Engine

If a node has multiple outputs, you can use this method GetMaterialExpressionOutputNames to see which OUTPUT_names a specific node has:

  • Connect between Material Expressions
unreal.MaterialEditingLibrary.connect_material_expressions(from_expression=node_tex, from_output_name="", to_expression=node_add, to_input_name="A")

An image of connected expressions in the Material Editor of Unreal Engine

Delete

Delete Material Expression

unreal.PythonMaterialLib.delete_material_expression(my_mat, node_need_be_delete)

Disconnections

  • Disconnect with Material Property
unreal.PythonMaterialLib.disconnect_material_property(my_mat, material_property_str="MP_BaseColor")
  • Disconnect between Material Expressions

The output pin of a material expression can connect to multiple other expressions, but the input pin of it is unique. So when we want to disconnect the connections between expressions, only to specify the expression which the connection connected to and its input name.

unreal.PythonMaterialLib.disconnect_expression(node_add, "A")

Layout the Expressions

In addition to specifying the location of the material node when it is created, we can also apply automatic layout to the material after it has been connected.

  • Auto layout
unreal.MaterialEditingLibrary.layout_material_expressions(my_mat)
  • Specify the node location when creating the node
unreal.MaterialEditingLibrary=create_material_expression( material, expression_class, node_pos_x=x, node_pos_y=y):

Query

Get the instance of Material Expression (node in material)

We can add context menus in Material Editor with TAPython 1.0.8, and get the selections of the expressions. More descriptions can be found here. It's very useful for the python scripts that handle the material graphs.

    "OnMaterialEditorMenu": {
        "name": "Python Menu On Material Editor",
        "items":
        [
            {
                "name": "TA Python Material Example",
                "items": [
                    {
                        "name": "Print Editing Material / MF",
                        "command": "print(%asset_paths)"
                    },
                    {
                        "name": "Log Editing Nodes",
                        "command": "editing_asset = unreal.load_asset(%asset_paths[0]); unreal.PythonMaterialLib.log_editing_nodes(editing_asset)"
                    },
                    {
                        "name": "Selected Nodes --> global variable _r",
                        "command": "_r = unreal.PythonMaterialLib.get_selected_nodes_in_material_editor(unreal.load_asset(%asset_paths[0]))"
                    },
                    {
                        "name": "Selected Node --> _r",
                        "command": "_r = unreal.PythonMaterialLib.get_selected_nodes_in_material_editor(unreal.load_asset(%asset_paths[0]))[0]"
                    }
                ]
            }
        ]
    },

Below example will log the selected expression's brief info with the menu item "Log Editing Nodes"

A GIF displaying log material nodes in the Material Editor using TAPython

Menu item "Selected Node --> global variable _r" will get the selected expression and assign them to the global variable "_r". And we can print out its property or show all the details of it with ObjectDetailViewer.

A GIF demonstrating how to get the selected node and assign it to the variable '_r' in the Material Editor using TAPython

Get properties of the Material Expression

  • Get the input names

Get the input pin's names of the material expression

unreal.PythonMaterialLib.get_material_expression_input_names(some_node)
  • Get the output names
unreal.PythonMaterialLib.get_material_expression_output_names(some_node)
  • Get the captions
unreal.PythonMaterialLib.get_material_expression_captions(some_node)
  • print out a brief of the material expression
unreal.PythonMaterialLib.log_material_expression(some_node)

A snapshot of material info in the Output window of the UE Editor using TAPython

The type of "Input Type" and "Output Type" in above image is EMaterialValueType, which can be found in Utilities.Utils. Value 15 means MCT_Float1|MCT_Float2|MCT_Float3|MCT_Float4.

class EMaterialValueType(IntFlag):
    MCT_Float1          = 1,
    MCT_Float2          = 2,
    MCT_Float3          = 4,
    MCT_Float4          = 8,
    MCT_Texture2D       = 1 << 4,
    ....
    MCT_VoidStatement   = 1 << 22
  • Print out node connections in the material in a tree form

The number in square brackets are the indexes of the material expressions. The expressions can be get via get_material_expressions.

unreal.PythonMaterialLib.log_mat(my_mat)

A snapshot of the material connection graph info in the Output window of the UE Editor using TAPython

The material function also has a similar function

unreal.PythonMaterialLib.log_mf(my_mf)

Get the material expressions and the connections

  • Get all expressions of the material
all_expressions = unreal.PythonMaterialLib.get_material_expressions(my_mat)
  • Get all expressions of the material function
all_expressions_in_mf = unreal.PythonMaterialLib.get_material_function_expressions(my_mf)
  • Get the connections between expressions in the material

Below code will return all the connections in a list, the type of item is TAPythonMaterialConnection.

for connection in unreal.PythonMaterialLib.get_material_connections(my_mat)
    print(connection)
TAPythonMaterialConnection(StructBase):

class TAPythonMaterialConnection(StructBase):
    r"""
    TAPython Material Connection

    **C++ Source:**

    - **Plugin**: TAPython
    - **Module**: TAPython
    - **File**: PythonMaterialLib.h

    **Editor Properties:** (see get_editor_property/set_editor_property)

    - ``left_expression_index`` (int32):  [Read-Write] Left Expression Index:
      The index of material expression in source material's expressions, which the connection from
    - ``left_output_index`` (int32):  [Read-Write] Left Output Index:
      The index of output in the expression
    - ``left_output_name`` (str):  [Read-Write] Left Output Name:
      The name of output pin
    - ``right_expression_index`` (int32):  [Read-Write] Right Expression Index:
      The index of material expression in source material's expressions, which the connection to
    - ``right_expression_input_index`` (int32):  [Read-Write] Right Expression Input Index:
      The index of input in the expression
    - ``right_expression_input_name`` (str):  [Read-Write] Right Expression Input Name:
      The name of input pin

Properties of the connection:

  • left_expression_index
  • left_output_index
  • left_output_name
  • right_expression_index
  • right_expression_input_index
  • right_expression_input_name

"Left_expression_index" and "right_expression_index" are the node's index of the left and right expressions beside the "connection wire".

Export

Export Material in JSON format

Below codes will export the material including its expressions and connections between them into a json file.

This function is essentially the same as exporting materials as T3D or COPY, but in the more general JSON format. In this way, we can get every detail about the nodes of material. Then we can analyze, optimize, and do other cool things.

For instance, exporting UE5's material to UE4, or other internal engine which modifies the serialize of the assets.

    # export the material content to a JSON file
    content_in_json = unreal.PythonMaterialLib.get_material_content(my_mat)
    with open("file_path_of_json_file", 'w') as f:
        f.write(content_in_json)

And there will be an example in this repo showing how to re-create the material from exported Json file.

Get HLSL code

We can get the material's HLSL code under FeatureLevel: SM5.

print(unreal.PythonMaterialLib.get_hlsl_code(my_mat))

A GIF displaying the print HLSL code in the Material Editor

Get shadermap in JSON format

Below data of the material's shadermap will be exported in JSON format, for future analysis.

  • Name
  • FunctionName
  • VertexFactoryType
  • FrequencyType
  • PermutationId
  • ShaderFilename
  • TextureSamplersNum
  • NumInstructions
  • CodeSize
unreal.PythonMaterialLib.get_shader_map_info(my_mat, "PCD3D_ES3_1")

A GIF showcasing the shader map of a material in the UE Editor using TAPython

More detail and APIs of PythonMaterialLib is here

References