Blender和UE4的贴图修复脚本
11 Apr 2020 UE4 Blender Python
最近遇到了Blender导出的材质没有贴图的问题,刚好顺便研究下UE4和Blender的脚本结构。
当前UE4版本为UE4.25.0;当前Blender版本为Blender2.82a。
首先,目前Blender和UE4的脚本版本是不同的。Blender使用的是python3,而UE4停留在python2.7。
这边使用Blender其实主要是作为模型导出的中继,比如MMD、VRM、XPS以及一些其他的非主流模型,通过Blender这边导出为FBX,然后输入到UE4去。
由于Blender和UE4的材质结构是不同的,到了UE4那边之后肯定要重新调整节点。所以对于缺少贴图的情况,最致命的问题就是,我们很难把材质名称和它使用的贴图对应起来。
因此这边就是实现这个过程的自动化,首先,从Blender这边导出材质使用到的贴图,然后在UE4那边还原到材质中去。
Blender贴图导出
blender这边的script编辑器里面有个需要注意的地方,就是他的输出不是在左边的那个窗口输出的。虽然左边的console窗口可以用于测试指令,但是如果你在右侧的脚本上点击运行的话。输出其实是在外部的那个Console里面的。如果默认没有的话,需要在菜单中打开
Blender的脚本文档还是比较丰富的,不过由于功能比较多,这边只是做一个简单的材质中的贴图导出,所以并不一定是一个好的实现
# blender script
# python3
import bpy
import json
EXPORT_PATH = 'F:/materials.json'
class MatCollector:
MatDic = {}
# Collet material texture for single material
def _CollectMaterial(self, InMaterial):
MatTextures = []
for ts_node in InMaterial.node_tree.nodes:
if ts_node.bl_static_type == 'TEX_IMAGE' and ts_node.image:
MatTextures.append(ts_node.image.filepath)
tstr_MatName = InMaterial.name
if len(MatTextures) == 0:
print("[ExportMaterialTextures] <"+tstr_MatName+"> has no texture")
return
self.MatDic[tstr_MatName] = MatTextures
print("[ExportMaterialTextures] Material " + tstr_MatName + " with " + str(len(MatTextures)) + " texture")
pass
# List all materials and try pickout texture info
def ExportMaterialTextures(InPath):
print("[ExportMaterialTextures] Begin: "+ InPath)
ts_MatCollector = MatCollector()
for Mat in bpy.data.materials:
ts_MatCollector._CollectMaterial(Mat)
print("[ExportMaterialTextures] Material with texture count " + str(len(ts_MatCollector.MatDic)))
with open(InPath, 'w') as f:
json.dump(ts_MatCollector.MatDic, f)
print("[ExportMaterialTextures] End")
def main():
ExportMaterialTextures(EXPORT_PATH)
if __name__ == "__main__":
main()
脚本的作用很简单,就是遍历场景内所有的材质,然后将每个材质中带有的所有贴图都输出出来。
UE4导入
当前UE4的编辑器Python插件是Beta状态,需要打开才能使用。
友情提醒:
使用本文中的脚本前请注意备份项目文件!
脚本操作可能会导致不可预期的结果,请在了解这种危险的情况下使用脚本操作!
UE4这边在完成FBX的导入之后,会看到所有的材质都是白的。所以我们这边用脚本,将对应的贴图添加到材质中去。不过,要在代码中操作材质是很麻烦的,连接的工作还是保留在了材质制作中。
# unreal script
# python2.7
import json
import unreal
from sets import Set
IMPORT_PATH = 'F:/materials.json'
IMPORT_TARGET = '/Game/Chara/'
TEXTURE_PATH = '/Game/Chara/Textures/'
TEXTURE_BASE = 'F:/z'
class MatTextureImporter:
IsStateGood = True
def ReadFromConfig(self):
unreal.log("[MaterialTextureImport] Begin")
# Load from Json config
with open(IMPORT_PATH, 'r') as myfile:
data=myfile.read()
MatConf = json.loads(data)
# Try import texture
self._PreLoadTexture(MatConf)
if self.IsStateGood == False:
unreal.log_error("[MaterialTextureImport] Asset state not good, abort")
return
# Try apply texture to material
unreal.log("[MaterialTextureImport] Total target material: " + str(len(MatConf)))
with unreal.ScopedSlowTask(len(MatConf), "Modifing materials") as slow_task:
for ts_key in MatConf:
if slow_task.should_cancel():
break
slow_task.enter_progress_frame(1)
self._AddMaterialTexture(ts_key, MatConf[ts_key])
unreal.log("[MaterialTextureImport] End")
return
def _PreLoadTexture(self, InDic):
print("[MaterialTextureImport] Pre load textures - Begin")
# Build texture list
tset_TexturePath = Set()
for ts_MatKey in InDic:
for ts_Texture in InDic[ts_MatKey]:
ts_TextureName = unreal.Paths.get_base_filename(ts_Texture)
ts_TextureUePath = TEXTURE_PATH + ts_TextureName
if unreal.EditorAssetLibrary.does_asset_exist(ts_TextureUePath):
ts_TextureData = unreal.EditorAssetLibrary.find_asset_data(ts_TextureUePath)
if ts_TextureData.is_valid():
if ts_TextureData.asset_class == 'Texture2D':
# We will never find a good name while operation takes more than once, so just ignore
unreal.log_warning("[MaterialTextureImport] Texture " + ts_TextureUePath + " alread exist")
continue
else:
# While asset name conflicts, we will stop operation, as replacing or rename may break our project
unreal.log_error("[MaterialTextureImport] Asset " + ts_TextureUePath + " alread exist and it is not a texture!")
self.IsStateGood = False
return
tset_TexturePath.add(ts_Texture)
print("[MaterialTextureImport] Total texture need load: " + str(len(tset_TexturePath)))
# Build import tasks
import_tasks = []
for ts_Texture in tset_TexturePath:
print("[MaterialTextureImport] - " + ts_Texture)
AssetImportTask = unreal.AssetImportTask()
AssetImportTask.set_editor_property('filename', TEXTURE_BASE + ts_Texture)
AssetImportTask.set_editor_property('destination_path', TEXTURE_PATH)
AssetImportTask.set_editor_property('save', True)
import_tasks.append(AssetImportTask)
# Import textures
AssetTools = unreal.AssetToolsHelpers.get_asset_tools()
AssetTools.import_asset_tasks(import_tasks)
print("[MaterialTextureImport] Pre load textures - End")
return
def _AddMaterialTexture(self, InName, InList):
InName = InName.replace(' ', '_')
InName = InName.replace('.', '_')
print("[MaterialTextureImport] Material: " + InName)
# Try load material
matpath = "Material'" + IMPORT_TARGET + InName + '.' + InName + "'"
ts_LoadedMat = unreal.load_asset(matpath)
if ts_LoadedMat is None:
unreal.log_warning("[MaterialTextureImport] Invalid material path: " + matpath)
return
# Add texture to material
#unreal.MaterialEditingLibrary.delete_all_material_expressions(ts_LoadedMat)
td_TextureAdded = 0
for ts_TexturePath in InList:
ts_TextureName = unreal.Paths.get_base_filename(ts_TexturePath)
ts_TextureUePath = TEXTURE_PATH + ts_TextureName
ts_LoadedTexture = unreal.EditorAssetLibrary.load_asset(ts_TextureUePath)
if ts_LoadedTexture is None:
unreal.log_warning("[MaterialTextureImport] Could not load texture "+ ts_TextureUePath)
continue
ts_TextureNodeBc = unreal.MaterialEditingLibrary.create_material_expression(ts_LoadedMat, unreal.MaterialExpressionTextureSample, -400, 250 * td_TextureAdded)
if ts_TextureNodeBc is None:
unreal.log_warning("[MaterialTextureImport] Could not create node")
continue
ts_TextureNodeBc.set_editor_property("texture", ts_LoadedTexture)
ts_TextureNodeBc.set_editor_property("desc", ts_TextureName)
td_TextureAdded += 1
print("[MaterialTextureImport] Material get " + str(td_TextureAdded) + " texture")
return
def main():
ts_MatChanger = MatTextureImporter()
ts_MatChanger.ReadFromConfig()
if __name__ == "__main__":
main()
在制作中遇到的另一个问题是,由于模型那边Blender的材质使用到了复杂的Group Node,这边也对应的实现了Material Function,但是需要将材质改为使用Attribute输出。于是又写了个批量修改的脚本
import unreal
def list_assets(InPath, InClass):
MatchedAssets = []
for ts_AssetPath in unreal.EditorAssetLibrary.list_assets(InPath):
ts_AssetData = unreal.EditorAssetLibrary.find_asset_data(ts_AssetPath)
if ts_AssetData.asset_class == InClass:
MatchedAssets.append(ts_AssetPath)
return MatchedAssets
def ModifyMaterials():
ts_Mats = list_assets("/Game/Chara/", 'Material')
print(ts_Mats)
ts_MatModified = []
for ts_matPath in ts_Mats:
ts_mat = unreal.EditorAssetLibrary.load_asset(ts_matPath)
if ts_mat is None:
unreal.log_warning("[MatModify] No material in " + ts_mat)
continue
#ts_mat.set_editor_property("use_material_attributes", True)
#unreal.MaterialEditingLibrary.recompile_material(ts_mat)
ts_MatModified.append(ts_mat)
unreal.EditorAssetLibrary.save_loaded_assets(ts_MatModified)
def main():
ModifyMaterials()
if __name__ == "__main__":
main()
总结
Blender和UE4两边的Python脚本都还是挺好用的。
不过UE4这边在批量修改完材质后会有一个卡顿的感觉,使用SlowTask包裹依然没有改善,不过由于是自用的脚本,就不纠结了。