Bootstrap Chameleon Logo

Manipulate Material Expression Nodes of Material with Python in Unreal Engine

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 Get/SetMaterialAttributeNode

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())

052_created_mat_mf.png

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)

053_add_node

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)

054_add_texture

  • 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"))

055_add_break

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)

056_get_set_material_attributes

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)

057_connect_base_color

  • 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.

058_connect_to_wpo

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

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

059_connect_expressions

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"

G012_log_material_nodes

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.

G013_get_node_as_r

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)

060_log_node

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)

061_log_mat

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))

G014_print_hlsl

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")

G015_shadermap

More detail and APIs of PythonMaterialLib is here

References