Bootstrap Chameleon Logo

用Python操作Unreal Engine材质和材质节点

除了之前介绍过的通过Python来创建User Defined Enum,Struct、DataTable以外,用Python来处理材质、材质函数也是一个很常见的一个需求。

UE5(5.0.3)引擎自带的MaterialEditingLibrary可以完成不少对材质的操作,但也有不少操作是通过它完成不了的。

比如:

  • 将材质属性连接到World Position Offset等属性

    因为MP_WorldPositionOffset 这个枚举项在cpp中被标记为Hidden,因此它无法在Python中使用

  • 为Get/SetMaterialAttributes 添加输出/输入引脚等 Get/SetMaterialAttributeNode

我们可以理解UE对部分功能的隐藏和封装,这能避免不少麻烦。比如我们愚蠢地在5.0.3中将通过脚本将ShadingModel设置为MSM_Strata (DisplayName="Strata", Hidden), 这马上就会导致编辑器的崩溃。但隐藏那些用户可以通过界面进行操作的那些选项和枚举,无疑给自动化操作带来了额外的工作量,这就是TAPython 要存在的意义。

通过MaterialEditingLibrary和TAPython的PythonMaterialLib,我们已经可以将绝大部分对材质的操作Python脚本化。

创建

创建材质

下面这段代码,将在"/Game/CreatedByPython"这个位置创建一个名为M_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())

创建材质函数

创建材质函数的方法与创建材质类似

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

添加材质节点

  • 添加一个普通的节点

my_mat添加一个 加法节点,并将其赋值给变量node_add

注意,本文介绍的方法或者说函数,绝大部分都不会触发材质编辑器UI的刷新。因此,如果你用材质编辑器打开了当前操作的材质,那么你需要关闭窗口,然后重新打开才能看到新添加的节点。

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

053_add_node

在材质函数中添加材质节点的做法也很类似,只不过调用的方法是unreal.MaterialEditingLibrary.create_material_expression_in_function,其他类似的功能也都有材质函数所用的版本,在实际使用的时候替换成对于的版本即可。

  • 添加一个贴图采样节点

添加TextureSample,并通过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

  • 添加一个材质函数节点

材质函数节点的类型是:MaterialExpressionMaterialFunctionCall,然后我们也同样需要通过set_editor_property,设置它实际使用的MF。

我们熟悉的break节点,其实也是一个材质函数

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

以上部分都是用Unreal引擎自带的MaterialEditingLibrary来完成。下面涉及到的部分内容,有的就需要TAPython中的PythonMaterialLib来加以实现

添加特殊材质节点

部分材质节点比较特殊,它们有用户可以添加的输入和输出引脚。输入和输出引脚的内容以FGuid的形式保存在Property中。如果简单地设置其中的内容,并不会引起引脚数量的添加,并且引发报错。

因此,需要使用到 PythonMaterialLib中的add_input_at_expression_set_material_attributes方法,为其添加输入/输出引脚:

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

连接材质节点

  • 将节点的输出引脚连接到材质的某个属性
unreal.MaterialEditingLibrary.connect_material_property(from_expression=node_add
                                                    , from_output_name=""
                                                    , property_=unreal.MaterialProperty.MP_BASE_COLOR)

057_connect_base_color

  • 将节点的输出引脚连接到WorldPositionOffset 等“隐藏”属性

由于unreal.MaterialProperty中并没有包含 MP_WorldPositionOffset 等在cpp中标记为hidden的枚举项,因此需要用到PythonMaterialLib中扩展的同名函数。该函数的第三个参数类型改为了字符串类型

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

注意:当输入或者输出节点名:from_output_name、to_input_name为空字符串时,会默认使用第一个输入或者输出引脚

058_connect_to_wpo

如果节点有多个输出,想要查看具体节点有那些output_name 可以使用这个方法:[GetMaterialExpressionOutputNames][#_断开节点]

  • 将两个材质节点之间的属性进行连接
unreal.MaterialEditingLibrary.connect_material_expressions(from_expression=node_tex, from_output_name="", to_expression=node_add, to_input_name="A")

059_connect_expressions

删除节点

unreal.MaterialEditingLibrary.delete_material_expression(my_mat, node_need_be_delete)

断开节点

  • 断开节点之间的连接

一个材质节点的输出节点可以连接到多个其他节点,但一个材质节点的输入节点是唯一的。因此,我们要断开节点间的连接的话,只需要指定被连接到节点和它的输入名:

unreal.PythonMaterialLib.disconnect_expression(node_add, "A")
  • 断开材质节点到母材质的连接
unreal.PythonMaterialLib.disconnect_material_property(my_mat, material_property_str="MP_BaseColor")

布局材质节点

除了在创建材质节点时指定它的位置以外,可可以在材质连接之后,对其应用自动布局:

  • 自动布局
unreal.MaterialEditingLibrary.layout_material_expressions(my_mat)
  • 创建时指定节点位置
unreal.MaterialEditingLibrary=create_material_expression( material, expression_class, node_pos_x=x, node_pos_y=y):

查询

获取材质节点实例

我们可以在材质编辑中添加快捷菜单,迅速获取当前选中的节点,具体的添加方法可见1.0.8的更新说明。

    "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]"
                    }
                ]
            }
        ]
    },

比如下图中的 "Log Editing Nodes" 可以输出选中的节点的简要信息 G012_log_material_nodes

"Selected Nodes --> global variable _r" 可以将当前选中的材质节点赋值个全局变量 "_r",然后可以在Python console中继续对其进行操作,当然也可以通过ObjectDetailViewer查看该节点的各个Property和数值 G013_get_node_as_r

获取材质节点属性

  • 获取材质节点输入名列表
unreal.PythonMaterialLib.get_material_expression_input_names(some_node)
  • 获取材质节点输出名列表
unreal.PythonMaterialLib.get_material_expression_output_names(some_node)
  • 获取材质节点上的标题列表
unreal.PythonMaterialLib.get_material_expression_captions(some_node)
  • 输出材质节点的简要信息
unreal.PythonMaterialLib.log_material_expression(some_node)

060_log_node

输出连接关系

  • 以树状形式输出材质中的节点连接关系
unreal.PythonMaterialLib.log_mat(my_mat)

061_log_mat

同样对于材质函数,也有类似的功能

unreal.PythonMaterialLib.log_mf(my_mf)

获取材质、材质函数中的节点及其连接关系

  • 获取当前材质中的所有节点
all_expressions = unreal.PythonMaterialLib.get_material_expressions(my_mat)
  • 获取材质函数中的所有节点
all_expressions_in_mf = unreal.PythonMaterialLib.get_material_function_expressions(my_mf)
  • 获材质中的个节点的连接关系

下面例子将以数组的形式返回材质中所有连接,连接的类型为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

其中的属性有:

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

其中"left_expression_index", "right_expression_index" 分别为节点连线左侧和右侧的节点。

其他属性还包含,连接的引脚序号和引脚名称等。我们可以更具实际需要选择使用引脚索引或者引脚名,(比如不同的项目间,引脚的顺序不同,或者修改了引脚名)

导出材质信息

  • 以JSON的形式获取整个材质的连接信息

这个功能与将材质以T3D或者COPY的形式导出的功能实际上是一样的,只不过使用了更为通用的JSON格式。通过这个方法,我们可以掌握整个材质中的节点信息,进而对其进行分析、优化和其他操作:

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

获取材质的HLSL代码

下面代码将打印出当前材质在SM5下的hlsl代码

print(unreal.PythonMaterialLib.get_hlsl_code(my_mat))
  • 获取材质的shadermap信息

包括材质变体,shaderCode大小,贴图使用数量等都会包含在输出信息中

unreal.PythonMaterialLib.get_shader_map_info(_r, "PCD3D_ES3_1")

062_shadermap_part

范例

可见TAPython_TestSamples中的材质部分(暂缺)

参考