0%

Unreal 模型材质绑定传递工具

摘要

模型美术的小伙伴 , 某日跑过来表示, 每次临时, 替换或者更新, 一个新的模型资产时

都需要手动把之前指过的材质再指一次, 非常麻烦, 特别是有50+材质的超级复杂模型, 简直想要自杀.

于是有了这个小脚本~

Python 实现代码

  • 人生苦短!
  • CV大法好 !!
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# coding=utf-8
# This Script was designed to copy material which have same slotName with different meshes asset object in Unreal Engine
# script by MineClever @ 2023

import unreal as unreal
import os as os
import copy as copy
import collections

class UnrealAssetSrcCtrl():
u"""用于简单处理版本控制, 大规模检入检出时不应当使用此类型"""
_provider = unreal.SourceControl.current_provider()
_projectContentPath = unreal.Paths.project_content_dir()

def __init__(self, assetPath):
# type: (str) -> None
self.currentFile = self.ConvertPackagePathToFilePath(assetPath)
self.currentProvider = self._provider
self.currentFileState = unreal.SourceControl.query_file_state(self.currentFile)
# self.currentFileState = unreal.SourceControlState(self.currentFile)
self.hasFileCheckOut = False

@staticmethod
def ConvertPackagePathToFilePath (inPackagePath) :
# type: (str) -> str
"""Convert '/Game/StarterContent/SomeName.SomeName' to 'Drive:/Project/Content/StarterContent/SomeName.uasset'"""
standardPath = unreal.Paths.make_standard_filename(inPackagePath)
uAssetPath = os.path.join( *unreal.Paths.split(standardPath)[0:2] ) + ".uasset"
fileSysPath = os.path.join(__class__._projectContentPath,uAssetPath.replace("/Game/","")).replace("\\","/")
return fileSysPath

def UpdateFile(self):
currentFileState = self.currentFileState
if (currentFileState.is_valid and currentFileState.is_source_controlled):
bRevert = unreal.SourceControl.revert_file(self.currentFile,True)
bSync = unreal.SourceControl.sync_file(self.currentFile, True)
return (bRevert or bSync)

def CheckOutFile(self):
currentFileState = self.currentFileState
self.hasFileCheckOut = False
if (currentFileState.is_valid and currentFileState.is_source_controlled):
if (currentFileState.is_checked_out_other):
unreal.log_warning(u"当前文件{}已经被其他人检出(锁定)".format(self.currentFile))
elif (not currentFileState.is_current):
unreal.log_warning(u"当前文件{}版本与服务器版本不匹配,尝试更新...".format(self.currentFile))
if not self.UpdateFile():
unreal.log_error(u"当前文件{}无法同步, 请手动操作")
else:
unreal.SourceControl.check_out_file(self.currentFile, True)
self.hasFileCheckOut = True
else:
unreal.log_warning(u"当前文件{}未找到版本控制状态".format(self.currentFile))
self.hasFileCheckOut = True


class BaseAssetHelper(object):
_assetEditorSubsystem = unreal.AssetEditorSubsystem()

@classmethod
def GetSelectedAssets(cls):
# type: (...) -> list[unreal.Object]
return unreal.EditorUtilityLibrary.get_selected_assets()

@classmethod
def CloseEditorForAssetPath(cls, assetName):
# type: (str) -> None
try:
assetObject = unreal.find_asset(assetName)
cls._assetEditorSubsystem.close_all_editors_for_asset(assetObject)
except Exception as E:
unreal.log_error("Can not close asset: {};\nErr: {}".format(assetName, E))

@classmethod
def CloseEditorForAssetObject(cls, assetObject):
# type: (unreal.Object) -> None
cls._assetEditorSubsystem.close_all_editors_for_asset(assetObject)


class MeshAssetSelectionHelper(BaseAssetHelper):

@classmethod
def GetMeshSelections(cls):
allSelectedAssets = cls.GetSelectedAssets()
validAsset = list() # type: list[unreal.StreamableRenderAsset]
for curAsset in allSelectedAssets:
if (isinstance(curAsset, (unreal.StaticMesh, unreal.SkeletalMesh))):
validAsset.append(curAsset)
return validAsset


class MeshMaterialHelper ():

@classmethod
def GetMeshMaterials(cls, pMesh):
# type: (unreal.StreamableRenderAsset) -> unreal.Array[unreal.SkeletalMaterial] |unreal.Array[unreal.StaticMaterial] | None
if isinstance(pMesh, unreal.StaticMesh):
targetMaterials = pMesh.static_materials
elif isinstance(pMesh, unreal.SkeletalMesh):
targetMaterials = pMesh.materials
else:
return None
return targetMaterials


class MeshAssetMaterialCopyTool (MeshMaterialHelper):

@classmethod
def CopyMaterialSlotA(cls, pSource, pTarget):
# type: (unreal.StreamableRenderAsset, unreal.StreamableRenderAsset) -> bool

# NOTE: get source material interfaces
sourceMaterials = cls.GetMeshMaterials(pSource)
if not sourceMaterials:
return False

# NOTE: get target materials
targetMaterials = cls.GetMeshMaterials(pSource)
if not targetMaterials:
return False
targetMatLength = targetMaterials.__len__()

raiseMeshTypeException: collections.Callable[[unreal.StreamableRenderAsset],Exception] = \
lambda inMesh: Exception("unsupported mesh type: {}".format(inMesh.get_class().get_name()))

buildSrcMatMapping : collections.Callable[[],dict[unreal.Name, int]] = \
lambda : dict((v,k) for k, v in enumerate(mat.material_slot_name for mat in sourceMaterials))

if isinstance(pTarget, unreal.StaticMesh):

# NOTE : source is static mesh
if isinstance(pSource, unreal.StaticMesh):
for tarMatId in range(targetMatLength):
curTarMatSlotName = targetMaterials[tarMatId].material_slot_name
srcMatId = pSource.get_material_index(curTarMatSlotName)
srcMatInterface = pSource.get_material(srcMatId)
pTarget.set_material(tarMatId, srcMatInterface)
# NOTE: source is skeletal mesh
elif isinstance(pSource, unreal.SkeletalMesh):
# NOTE: build a enum dict for current source material map
srcMatMapping = buildSrcMatMapping()
for tarMatId in range(targetMatLength):
curTarMatSlotName = targetMaterials[tarMatId].material_slot_name
srcMatId = srcMatMapping.get(curTarMatSlotName, 0)
if srcMatId:
srcMatInterface = sourceMaterials[srcMatId].material_interface
pTarget.set_material(tarMatId, srcMatInterface)
else :
raise raiseMeshTypeException(pSource)

elif isinstance(pTarget, unreal.SkeletalMesh):
unreal.log_warning("Cast material into skeletal mesh object, this method may cast fail ... ")

srcMatMapping = buildSrcMatMapping()
# NOTE: use copy module to easy copy our material array
newTarMatArray = copy.copy(targetMaterials)
for tarMatId in range(targetMatLength):
curTarMatSlotName = targetMaterials[tarMatId].material_slot_name
srcMatId = srcMatMapping.get(curTarMatSlotName, 0)
if srcMatId:
srcMatInterface = sourceMaterials[srcMatId].material_interface
newTarMatArray[tarMatId].material_interface = srcMatInterface
pTarget.materials = newTarMatArray
else :
raise raiseMeshTypeException(pTarget)

return True


def runScript (*args, **kw):
unreal.log("Start to process material transforming ...")
allMeshSelections = MeshAssetSelectionHelper.GetMeshSelections()
if not allMeshSelections:
unreal.log_error("{}".format("Have to select two or more mesh asset to start ..."))
return
srcMesh = allMeshSelections[0]
for i in range(1,allMeshSelections.__len__()):
currentAsset = allMeshSelections[i]
unreal.log_warning("Current Target is : {}".format(currentAsset.get_full_name()))
try:
MeshAssetSelectionHelper.CloseEditorForAssetObject(currentAsset)
UnrealAssetSrcCtrl(currentAsset.get_path_name()).CheckOutFile()
MeshAssetMaterialCopyTool.CopyMaterialSlotA(srcMesh, currentAsset)
unreal.EditorAssetLibrary.save_loaded_asset(currentAsset)
except Exception as E:
unreal.log_error("{}".format(E))


if __name__ == "__main__":
runScript()

更新

  • 20230223 修复了版本控制在Unreal5中失败的问题

注释

由于Unreal Engine 的神奇设计…

导致即使都是网格类型, 但是, 接口也非常不统一

所有在处理Skeletal Mesh类型的材质时用了一些奇怪的方法.

原则上都可以用自己的方法来统一处理.

但是总感觉那里不大对, 因此按类型去判断了

歡迎關注我的其它發布渠道