前言
最近遇到一个需求,需要做一个 Unreal 材质实例属性传递工具。 Unreal 有内置批量属性编辑工具,但是似乎对无法实现材质实例属性的批量修改。
于是我利用 Python Qt 开发一个可以批量传递材质属性的工具。
上面的界面结合了之前研究的 Python Qt Overlay 堆叠组件 ,让界面维护更方便,UI 排列更紧凑。
基于材质编辑器实例的材质树获取
制作这个工具需要解决的问题是如何从材质里面知道材质的分组,分组从而构建出树形结构。
最初我是想通过材质编辑器上面的参数组来获取这个关系,为此还研究了很多 MaterialEditorInstance 相关的东西。 但是这玩意就是个巨坑,研究到最后发现修改的是 UI 的数据,没法改到材质属性,还是得借助 MaterialEditingLibrary 库对材质修改属性才可行(:з」∠)
基于上一篇文章研究 Unreal Python 利用 UtilityWidget 编写 UMG 工具界面 可以通过 C++ 罗列出所有的 UObject 通过上面的黑科技结合以前 list_menu 的方法,我可以获取到所有的 MaterialEditorInstanceConstant 类型
1 2 3 4 5 6 7 def list_material_editor (num=1000 ): return [ unreal.load_object( None , "/Engine/Transient.MaterialEditorInstanceConstant_%s" % i ) for i in range (num) ]
然而这个获取到的并非是当前打开的编辑窗口。 有可能是已经关闭的老窗口。 因此需要用 C++ 获取当前正在编辑的资产,然后再获取 MaterialEditorInstanceConstant 里面所属的材质 判断哪个是最新的材质面板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 UMaterialInstanceConstant *UPyToolkitBPLibrary::GetMaterialEditorSourceInstance (UMaterialEditorInstanceConstant *Editor) { return Editor->SourceInstance; } UObject *UPyToolkitBPLibrary::GetFocusedEditAsset () { UAssetEditorSubsystem *sub = GEditor->GetEditorSubsystem <UAssetEditorSubsystem>(); UObject *FocusObject = nullptr ; double maxLastActivationTime = 0.0 ; for (UObject *EditedAsset : sub->GetAllEditedAssets ()) { auto openedEditor = sub->FindEditorForAsset (EditedAsset, false ); if (openedEditor && openedEditor->GetLastActivationTime () > maxLastActivationTime) { maxLastActivationTime = openedEditor->GetLastActivationTime (); FocusObject = EditedAsset; } } return FocusObject; }
结合上面两个 C++ 就可以获取到当前正在编辑的 材质编辑器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 py_lib = unreal.PyToolkitBPLibrary() def list_material_editor (num=1000 ): material_editor = {} for i in range (num): editor = unreal.load_object(None ,"/Engine/Transient.MaterialEditorInstanceConstant_%s" % i) if editor: material = py_lib.get_material_editor_source_instance(editor) if material: material_editor[material] = editor return material_editor edit_asset = py_lib.get_focused_edit_asset() material_editor = list_material_editor() editor = material_editor.get(edit_asset)
通过 遍历 objects 的方式可以获取材质编辑器下的 DEditorParameterValue
通过这个方法可以直接修改 MaterialEditor UI 下的选项。
1 2 3 4 5 6 7 8 9 py_lib = unreal.PyToolkitBPLibrary() param = unreal.load_object(None ,"/Engine/Transient.DEditorScalarParameterValue_0" ) print (py_lib.get_all_properties(param))info = param.get_editor_property("ParameterInfo" ) print (info.name)param.set_editor_property("bOverride" ,True ) param.set_editor_property("ParameterValue" ,0.5 )
可以用我以前写的 get_all_properties
C++ 函数获取 UObject 内置的 property 当然直接查 C++ 源码也可以查到 UProperty ,只有 EditAnywhere 的属性可以被 Python 修改。
然而这个只是 UI 层面的修改,并没有修改到 材质 内置的属性数据。 当我关闭编辑器重新打开,数据又回滚到之前的状态了。 所以折腾了一圈放弃了这个方案。
基于母材质数据的获取
上面的方案需要打开材质编辑器,而且也无法通过修改 UI 来改到材质实例的数据。 于是我深挖了引擎的 C++ 的代码。(花了我很多时间(:з」∠) ) 最后发现这个材质组是通过 母材质 的 MaterialExpression 来解析分组的。
有了这个之后,我也可以用类似的方法用 Python 调用实现材质属性树的构建。 然而 Python 无法直接获取材质的 Expressions , 需要使用 C++ 包装一个函数进行获取。
1 2 3 4 5 6 7 8 9 TArray<UMaterialExpression *> UPyToolkitBPLibrary::GetMaterialExpressions (UMaterial *Material) { return Material->Expressions; } TArray<UMaterialExpression *> UPyToolkitBPLibrary::GetMaterialFunctionExpressions (UMaterialFunction *Function) { return Function->FunctionExpressions; }
添加上面两个 C++ 函数进行 Python 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import jsonfrom collections import defaultdictpy_lib = unreal.PyToolkitBPLibrary mat_lib = unreal.MaterialEditingLibrary def cast (typ, obj ): """ unreal cast 类型转换 """ try : return getattr (unreal, typ).cast(obj) except : return None def _get_material_paramters (expressions ): """ inspire by https://github.com/20tab/UnrealEnginePython/issues/103 reference from MaterialFunctionInterface.h `GetParameterGroupName` """ paramters = defaultdict(set ) for expresion in expressions: func = cast("MaterialExpressionMaterialFunctionCall" , expresion) if func: func = func.get_editor_property("material_function" ) expressions = py_lib.get_material_function_expressions(func) params = _get_material_paramters(expressions) for group, param in params.items(): for p in param: paramters[str (group)].add(str (p)) continue param = cast("MaterialExpressionParameter" , expresion) if not param: param = cast("MaterialExpressionTextureSampleParameter" , expresion) if not param: param = cast("MaterialExpressionFontSampleParameter" , expresion) if param: group = param.get_editor_property("group" ) parameter_name = param.get_editor_property("parameter_name" ) paramters[str (group)].add(str (parameter_name)) return paramters def get_material_paramters (material ): scalars = mat_lib.get_scalar_parameter_names(material) vectors = mat_lib.get_vector_parameter_names(material) switches = mat_lib.get_static_switch_parameter_names(material) textures = mat_lib.get_texture_parameter_names(material) parent_material = material.get_base_material() expressions = py_lib.get_material_expressions(parent_material) paramters = _get_material_paramters(expressions) collections = defaultdict(dict ) for grp, params in sorted (paramters.items()): for p in sorted (params): value = None if p in textures: func = mat_lib.get_material_instance_texture_parameter_value value = func(material, p) value = value.get_path_name() elif p in switches: value = mat_lib.get_material_instance_static_switch_parameter_value( material, p ) elif p in vectors: func = mat_lib.get_material_instance_vector_parameter_value value = func(material, p) value = "|" .join( [ "%s : %-10s" % (c.upper(), round (getattr (value, c), 2 )) for c in "rgba" ] ) elif p in scalars: func = mat_lib.get_material_instance_scalar_parameter_value value = func(material, p) collections[grp][p] = value return collections material = unreal.load_asset('/Game/ParagonShinbi/Characters/Heroes/Shinbi/Materials/M_Shinbi_Legs_Inst_2.M_Shinbi_Legs_Inst_2' ) params = get_material_paramters(material) print (json.dumps(params))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 { "None" : { "DisplaceValue" : "R : 0.0 |G : 0.0 |B : 326.06 |A : 0.0 " , "SilkROUGHNESS" : 4.0 , "HitFlashBrightness" : 0.20000000298023224 , "Input Pattern Light" : 1.0 , "asdcasdfasdfasdf" : 60.0 , "Damage" : 0.0 , "Input Pattern Dark" : 1.0 , "IriExponent" : 0.20000000298023224 , "RGB" : "/Game/ParagonShinbi/Characters/Heroes/Shinbi/Textures/T_Shinbi_Lower_RGB.T_Shinbi_Lower_RGB" , "Fabric GrungeTint" : "R : 0.73 |G : 0.73 |B : 0.73 |A : 1.0 " , "BrightnessAdjust" : 1.25 , "VertexDisplacePower" : 1.5 , "Fabric Trans" : 1.0 , "BaseColortint" : "R : 0.99 |G : 1.0 |B : 1.1 |A : 1.0 " , "GoldScratchColor1" : "R : 1.0 |G : 1.0 |B : 1.0 |A : 1.0 " , "IriMask" : 0.029999999329447746 , "goldMetGrimeColor" : "R : 0.0 |G : 0.0 |B : 0.0 |A : 1.0 " , "Normal" : "/Game/ParagonShinbi/Characters/Heroes/Shinbi/Textures/T_Shinbi_Lower_N.T_Shinbi_Lower_N" , "Color1" : "R : 0.1 |G : 0.1 |B : 0.1 |A : 0.0 " , "Color0" : "R : 0.0 |G : 0.0 |B : 0.0 |A : 0.0 " , "MaskHardness" : 2.0 , "fgjgjghg" : 4.03448486328125 , "ScaleNormal" : "R : 1.0 |G : 1.0 |B : 0.4 |A : 0.0 " , "RMax" : 2.112884998321533 , "Mask" : "/Game/ParagonShinbi/Characters/Heroes/Shinbi/Textures/T_Shinbi_Lower_Mask.T_Shinbi_Lower_Mask" , "RMin" : 0.20569999516010284 , "Alum Roughness" : 0.46039101481437683 , "GoldScratchColor" : "R : 0.07 |G : 0.07 |B : 0.07 |A : 1.0 " , "Mask_1" : "/Game/ParagonShinbi/Characters/Heroes/Shinbi/Textures/T_Shinbi_Lower_Mask_1.T_Shinbi_Lower_Mask_1" , "NoiseTextureSize" : "R : 100.0 |G : 100.0 |B : 100.0 |A : 0.0 " , "Fabric Scratch Tint" : "R : 1.5 |G : 1.5 |B : 1.5 |A : 1.0 " , "Color" : "/Game/ParagonShinbi/Characters/Heroes/Shinbi/Textures/T_Shinbi_Legs_Colour.T_Shinbi_Legs_Colour" , "BaseColorDiv" : "R : 0.27 |G : 0.37 |B : 0.56 |A : 1.0 " , "adsfsadf" : 0.25 , "ScalpColour" : "R : 0.2 |G : 0.06 |B : 0.06 |A : 1.0 " , "asdfasdf" : 0.30000001192092896 , "ScratchRopughness" : 0.8521010279655457 , "BackMetalColorB" : "R : 0.68 |G : 0.64 |B : 0.71 |A : 1.0 " } , "Variant" : { "Base Silk Color" : "R : 0.41 |G : 0.09 |B : 0.31 |A : 1.0 " , "Scatter Silk Color" : "R : 0.22 |G : 0.11 |B : 0.22 |A : 1.0 " , "Base Fabric Color" : "R : 1.0 |G : 1.0 |B : 1.0 |A : 1.0 " , "AlumMetColor" : "R : 2.0 |G : 2.0 |B : 2.0 |A : 1.0 " , "BackMetalColorA" : "R : 0.0 |G : 0.0 |B : 0.0 |A : 1.0 " } , "FadeDeath" : { "FadeOut" : 0.0 , "FadeMask" : "/Game/ParagonShinbi/Characters/Global/MaterialFunctions/Textures/T_AtmosphericCloudNoise03.T_AtmosphericCloudNoise03" } }
构建了上面的数据就可以通过 QTreeView 将数据显示出来了。
我当时使用 dayu_widgets 的时候还没有 QTreeView 的使用案例。 后来我联系作者给提供了一个。
dayu_widgets 对 MVC 做了不少简化,用法其实和我之前 批量改名工具 大差不差。 只是数据多了 children
用来做 collapse 用法链接
用法上先定义 header_list 定义 view 的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 self.property_model = MTableModel() columns = {u"property" : "属性名" , u"value" : "数值" } self.property_header_list = [ { "label" : label, "key" : key, "bg_color" : lambda x, y: y.get("bg_color" , QtGui.QColor("transparent" )), "checkable" : i == 0 , "searchable" : True , "width" : 300 , "font" : lambda x, y: {"bold" : True }, } for i, (key, label) in enumerate (columns.items()) ] self.property_model.set_header_list(self.property_header_list)
后续添加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if isinstance (asset, unreal.MaterialInterface): data = self.get_material_parameters(asset) data_list = [ { "property" : group, "value" : "" , "children" : [ { "bg_color" : QtGui.QColor("transparent" ), "property" : prop, "value" : value, } for prop, value in props.items() ], } for group, props in data.items() ] self.property_model.set_data_list(data_list)
上面 header 定义的 key , 会在 data_list 中作为显示。 加入 children 数据作为 collapse 数据。 另外直接设置数据的 bg_color
可以设置背景色。
我加了功能,当我选择右侧的资产列表时候,左侧的的 TreeView 会检测当前资产下的材质树勾选的选项是否可以传递。 可以传递标注绿色,不可传递标注红色。
属性传递
大部分参数设置都可以借助 MaterialEditingLibrary 来完成 唯独缺了 set_static_switch_parameter_names 的设置函数。
在日本的网站上可以找别人做的 C++ 函数 https://qiita.com/EGJ-Kaz_Okada/items/4fd6db895b398893cbbb 直接抄就可以使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void UPyToolkitBPLibrary::SetMaterialInstanceStaticSwitchParameterValue (UMaterialInstance *Instance, FName ParameterName, bool Value) { FStaticParameterSet StaticParameters = Instance->GetStaticParameters (); for (auto &SwitchParameter : StaticParameters.StaticSwitchParameters) { if (SwitchParameter.ParameterInfo.Name == ParameterName) { SwitchParameter.Value = Value; break ; ; } } Instance->UpdateStaticPermutation (StaticParameters); }
注: 2021-5-22 更新 上面的方案如果参数没有 override 是不起作用的,新方案如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void URedArtToolkitBPLibrary::SetMaterialInstanceStaticSwitchParameterValue (UMaterialInstance *Instance, FName ParameterName, bool SwitchValue, bool bOverride) { TArray<FGuid> Guids; TArray<FMaterialParameterInfo> OutParameterInfo; Instance->GetAllStaticSwitchParameterInfo (OutParameterInfo, Guids); FStaticParameterSet StaticParameters = Instance->GetStaticParameters (); for (int32 ParameterIdx = 0 ; ParameterIdx < OutParameterInfo.Num (); ParameterIdx++) { const FMaterialParameterInfo &ParameterInfo = OutParameterInfo[ParameterIdx]; const FGuid ExpressionId = Guids[ParameterIdx]; if (ParameterInfo.Name == ParameterName) { FStaticSwitchParameter *NewParameter = new (StaticParameters.StaticSwitchParameters) FStaticSwitchParameter (ParameterInfo, SwitchValue, bOverride, ExpressionId); break ; } } Instance->UpdateStaticPermutation (StaticParameters); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 py_lib = unreal.PyToolkitBPLibrary mat_lib = unreal.MaterialEditingLibrary def transfer_material_property (self,material, property_list ): scalars = mat_lib.get_scalar_parameter_names(material) vectors = mat_lib.get_vector_parameter_names(material) switches = mat_lib.get_static_switch_parameter_names(material) textures = mat_lib.get_texture_parameter_names(material) for p in property_list: if p in textures: getter = mat_lib.get_material_instance_texture_parameter_value setter = mat_lib.set_material_instance_texture_parameter_value elif p in switches: getter = mat_lib.get_material_instance_static_switch_parameter_value setter = py_lib.set_material_instance_static_switch_parameter_value elif p in vectors: getter = mat_lib.get_material_instance_vector_parameter_value setter = mat_lib.set_material_instance_vector_parameter_value elif p in scalars: getter = mat_lib.get_material_instance_scalar_parameter_value setter = mat_lib.set_material_instance_scalar_parameter_value else : continue setter(material, p, getter(self.source, p))
通过上面的方式就可以将不同材质类型对应的数据进行传递了。
总结
以上就是材质属性传递功能的总结。 开发的过程还是留下一些遗憾,比如目前还没在面板上制作直接调整属性数据的功能。 要接入这个功能需要 MVC 的 delegate 来实现。 dayu_widgets 在这方面还没有做支持,而且还得用 Qt 做一套色板,有点麻烦 现实需求其实目前这个程度就已经够用了,要修改的话还是用 Unreal 原生编辑器进行修改。