[{"data":1,"prerenderedAt":133},["ShallowReactive",2],{"\u002F2020-04-11-blender-ue4-texture-python-material-fix":3},{"id":4,"title":5,"body":6,"date":108,"description":12,"extension":109,"meta":110,"navigation":113,"path":125,"seo":126,"stem":127,"tags":128,"__hash__":132},"blogs\u002F_legacy\u002F2020\u002F2020-04-11-blender-ue4-texture-python-material-fix.md","Blender和UE4的贴图修复脚本",{"type":7,"value":8,"toc":101},"minimark",[9,13,16,19,22,25,28,33,36,43,46,57,60,64,67,77,83,86,92,95,98],[10,11,12],"p",{},"最近遇到了Blender导出的材质没有贴图的问题，刚好顺便研究下UE4和Blender的脚本结构。",[10,14,15],{},"当前UE4版本为UE4.25.0；当前Blender版本为Blender2.82a。",[10,17,18],{},"首先，目前Blender和UE4的脚本版本是不同的。Blender使用的是python3，而UE4停留在python2.7。",[10,20,21],{},"这边使用Blender其实主要是作为模型导出的中继，比如MMD、VRM、XPS以及一些其他的非主流模型，通过Blender这边导出为FBX，然后输入到UE4去。",[10,23,24],{},"由于Blender和UE4的材质结构是不同的，到了UE4那边之后肯定要重新调整节点。所以对于缺少贴图的情况，最致命的问题就是，我们很难把材质名称和它使用的贴图对应起来。",[10,26,27],{},"因此这边就是实现这个过程的自动化，首先，从Blender这边导出材质使用到的贴图，然后在UE4那边还原到材质中去。",[29,30,32],"h2",{"id":31},"blender贴图导出","Blender贴图导出",[10,34,35],{},"blender这边的script编辑器里面有个需要注意的地方，就是他的输出不是在左边的那个窗口输出的。虽然左边的console窗口可以用于测试指令，但是如果你在右侧的脚本上点击运行的话。输出其实是在外部的那个Console里面的。如果默认没有的话，需要在菜单中打开",[10,37,38],{},[39,40],"img",{"alt":41,"src":42},"image","\u002Fwp-content\u002Fuploads\u002F2020\u002F04\u002Fimage_thumb-2.png",[10,44,45],{},"Blender的脚本文档还是比较丰富的，不过由于功能比较多，这边只是做一个简单的材质中的贴图导出，所以并不一定是一个好的实现",[47,48,53],"pre",{"className":49,"code":51,"language":52},[50],"language-text","# blender script\n# python3\n\nimport bpy\nimport json\n\nEXPORT_PATH = 'F:\u002Fmaterials.json'\n\nclass MatCollector:\n    MatDic = {}\n\n    # Collet material texture for single material\n    def _CollectMaterial(self, InMaterial):\n        MatTextures = []\n        for ts_node in InMaterial.node_tree.nodes:\n            if ts_node.bl_static_type == 'TEX_IMAGE' and ts_node.image:\n                MatTextures.append(ts_node.image.filepath)\n\n        tstr_MatName = InMaterial.name\n        if len(MatTextures) == 0:\n            print(\"[ExportMaterialTextures] \u003C\"+tstr_MatName+\"> has no texture\")\n            return\n\n        self.MatDic[tstr_MatName] = MatTextures\n        print(\"[ExportMaterialTextures] Material \" + tstr_MatName + \" with \" + str(len(MatTextures)) + \" texture\")\n        pass\n\n# List all materials and try pickout texture info\ndef ExportMaterialTextures(InPath):\n    print(\"[ExportMaterialTextures] Begin: \"+ InPath)\n    ts_MatCollector = MatCollector()\n    for Mat in bpy.data.materials:\n        ts_MatCollector._CollectMaterial(Mat)\n        \n    print(\"[ExportMaterialTextures] Material with texture count \" + str(len(ts_MatCollector.MatDic)))\n    with open(InPath, 'w') as f:\n        json.dump(ts_MatCollector.MatDic, f)\n\n    print(\"[ExportMaterialTextures] End\")\n\n\ndef main():\n    ExportMaterialTextures(EXPORT_PATH)\n\nif __name__ == \"__main__\":\n    main()\n","text",[54,55,51],"code",{"__ignoreMap":56},"",[10,58,59],{},"脚本的作用很简单，就是遍历场景内所有的材质，然后将每个材质中带有的所有贴图都输出出来。",[29,61,63],{"id":62},"ue4导入","UE4导入",[10,65,66],{},"当前UE4的编辑器Python插件是Beta状态，需要打开才能使用。",[10,68,69,70,73,74,76],{},"友情提醒：",[71,72],"br",{},"\n使用本文中的脚本前请注意备份项目文件!",[71,75],{},"\n脚本操作可能会导致不可预期的结果，请在了解这种危险的情况下使用脚本操作！\nUE4这边在完成FBX的导入之后，会看到所有的材质都是白的。所以我们这边用脚本，将对应的贴图添加到材质中去。不过，要在代码中操作材质是很麻烦的，连接的工作还是保留在了材质制作中。",[47,78,81],{"className":79,"code":80,"language":52},[50],"# unreal script\n# python2.7\n\nimport json\nimport unreal\n\nfrom sets import Set\n\nIMPORT_PATH = 'F:\u002Fmaterials.json'\nIMPORT_TARGET = '\u002FGame\u002FChara\u002F'\nTEXTURE_PATH = '\u002FGame\u002FChara\u002FTextures\u002F'\nTEXTURE_BASE = 'F:\u002Fz'\n\n\nclass MatTextureImporter:\n    IsStateGood = True\n\n    def ReadFromConfig(self):\n        unreal.log(\"[MaterialTextureImport] Begin\")\n\n        # Load from Json config\n        with open(IMPORT_PATH, 'r') as myfile:\n            data=myfile.read()\n        MatConf = json.loads(data)\n\n        # Try import texture\n        self._PreLoadTexture(MatConf)\n\n        if self.IsStateGood == False:\n            unreal.log_error(\"[MaterialTextureImport] Asset state not good, abort\")\n            return\n\n        # Try apply texture to material\n        unreal.log(\"[MaterialTextureImport] Total target material: \" + str(len(MatConf)))\n        with unreal.ScopedSlowTask(len(MatConf), \"Modifing materials\") as slow_task:\n            for ts_key in MatConf:\n                if slow_task.should_cancel():\n                    break\n                \n                slow_task.enter_progress_frame(1)\n                self._AddMaterialTexture(ts_key, MatConf[ts_key])\n\n        unreal.log(\"[MaterialTextureImport] End\")\n        return  \n\n    def _PreLoadTexture(self, InDic):\n        print(\"[MaterialTextureImport] Pre load textures - Begin\")\n\n        # Build texture list\n        tset_TexturePath = Set()\n        for ts_MatKey in InDic:\n            for ts_Texture in InDic[ts_MatKey]:\n                ts_TextureName = unreal.Paths.get_base_filename(ts_Texture)\n                ts_TextureUePath = TEXTURE_PATH + ts_TextureName\n\n                if unreal.EditorAssetLibrary.does_asset_exist(ts_TextureUePath):\n                    ts_TextureData = unreal.EditorAssetLibrary.find_asset_data(ts_TextureUePath)\n                    if ts_TextureData.is_valid():\n                        if ts_TextureData.asset_class == 'Texture2D':\n                            # We will never find a good name while operation takes more than once, so just ignore\n                            unreal.log_warning(\"[MaterialTextureImport] Texture \" + ts_TextureUePath + \" alread exist\")\n                            continue\n                        else:\n                            # While asset name conflicts, we will stop operation, as replacing or rename may break our project\n                            unreal.log_error(\"[MaterialTextureImport] Asset \" + ts_TextureUePath + \" alread exist and it is not a texture!\")\n                            self.IsStateGood = False\n                            return\n\n                tset_TexturePath.add(ts_Texture)\n\n        print(\"[MaterialTextureImport] Total texture need load: \" + str(len(tset_TexturePath)))\n\n        # Build import tasks\n        import_tasks = []\n        for ts_Texture in tset_TexturePath:\n            print(\"[MaterialTextureImport] - \" + ts_Texture)\n            AssetImportTask = unreal.AssetImportTask()\n            AssetImportTask.set_editor_property('filename', TEXTURE_BASE + ts_Texture)\n            AssetImportTask.set_editor_property('destination_path', TEXTURE_PATH)\n            AssetImportTask.set_editor_property('save', True)\n            import_tasks.append(AssetImportTask)\n\n        # Import textures\n        AssetTools = unreal.AssetToolsHelpers.get_asset_tools() \n        AssetTools.import_asset_tasks(import_tasks)\n\n        print(\"[MaterialTextureImport] Pre load textures - End\")\n        \n        return\n\n    def _AddMaterialTexture(self, InName, InList):\n        InName = InName.replace(' ', '_')\n        InName = InName.replace('.', '_')\n\n        print(\"[MaterialTextureImport] Material: \" + InName)\n\n        # Try load material\n        matpath = \"Material'\" + IMPORT_TARGET + InName + '.' + InName + \"'\"\n        ts_LoadedMat = unreal.load_asset(matpath)\n        if ts_LoadedMat is None:\n            unreal.log_warning(\"[MaterialTextureImport] Invalid material path: \" + matpath)\n            return\n\n        # Add texture to material\n        #unreal.MaterialEditingLibrary.delete_all_material_expressions(ts_LoadedMat)\n        td_TextureAdded = 0\n        for ts_TexturePath in InList:\n            ts_TextureName = unreal.Paths.get_base_filename(ts_TexturePath)\n            ts_TextureUePath = TEXTURE_PATH + ts_TextureName\n            ts_LoadedTexture = unreal.EditorAssetLibrary.load_asset(ts_TextureUePath)\n\n            if ts_LoadedTexture is None:\n                unreal.log_warning(\"[MaterialTextureImport] Could not load texture \"+ ts_TextureUePath)\n                continue\n\n            ts_TextureNodeBc = unreal.MaterialEditingLibrary.create_material_expression(ts_LoadedMat, unreal.MaterialExpressionTextureSample, -400, 250 * td_TextureAdded)\n            if ts_TextureNodeBc is None:\n                unreal.log_warning(\"[MaterialTextureImport] Could not create node\")\n                continue\n\n            ts_TextureNodeBc.set_editor_property(\"texture\", ts_LoadedTexture)\n            ts_TextureNodeBc.set_editor_property(\"desc\", ts_TextureName)\n            td_TextureAdded += 1\n\n        print(\"[MaterialTextureImport] Material get  \" + str(td_TextureAdded) + \" texture\")\n\n        return\n\ndef main():\n    ts_MatChanger = MatTextureImporter()\n    ts_MatChanger.ReadFromConfig()\n\nif __name__ == \"__main__\":\n    main()\n\n",[54,82,80],{"__ignoreMap":56},[10,84,85],{},"在制作中遇到的另一个问题是，由于模型那边Blender的材质使用到了复杂的Group Node，这边也对应的实现了Material Function，但是需要将材质改为使用Attribute输出。于是又写了个批量修改的脚本",[47,87,90],{"className":88,"code":89,"language":52},[50],"import unreal\n\ndef list_assets(InPath, InClass):\n    MatchedAssets = []\n\n    for ts_AssetPath in unreal.EditorAssetLibrary.list_assets(InPath):\n        ts_AssetData = unreal.EditorAssetLibrary.find_asset_data(ts_AssetPath)\n        if ts_AssetData.asset_class == InClass:\n            MatchedAssets.append(ts_AssetPath)\n\n    return MatchedAssets\n\n\n\ndef ModifyMaterials():\n    ts_Mats = list_assets(\"\u002FGame\u002FChara\u002F\", 'Material')\n    print(ts_Mats)\n    ts_MatModified = []\n    for ts_matPath in ts_Mats:\n        ts_mat = unreal.EditorAssetLibrary.load_asset(ts_matPath)\n        if ts_mat is None:\n            unreal.log_warning(\"[MatModify] No material in \" + ts_mat)\n            continue\n        #ts_mat.set_editor_property(\"use_material_attributes\", True)  \n\n        #unreal.MaterialEditingLibrary.recompile_material(ts_mat)\n        ts_MatModified.append(ts_mat)\n\n    unreal.EditorAssetLibrary.save_loaded_assets(ts_MatModified)\n\ndef main():\n    ModifyMaterials()\n\nif __name__ == \"__main__\":\n    main()\n\n",[54,91,89],{"__ignoreMap":56},[29,93,94],{"id":94},"总结",[10,96,97],{},"Blender和UE4两边的Python脚本都还是挺好用的。",[10,99,100],{},"不过UE4这边在批量修改完材质后会有一个卡顿的感觉，使用SlowTask包裹依然没有改善，不过由于是自用的脚本，就不纠结了。",{"title":56,"searchDepth":102,"depth":103,"links":104},2,3,[105,106,107],{"id":31,"depth":102,"text":32},{"id":62,"depth":102,"text":63},{"id":94,"depth":102,"text":94},"2020-04-11","md",{"layout":111,"status":112,"published":113,"author":114,"author_login":116,"author_email":117,"wordpress_id":118,"wordpress_url":119,"date_gmt":120,"excerpt":121},"post","publish",true,{"display_name":115,"login":116,"email":117,"url":56},"风铃","flinkor","flinkor@foxmail.com",2815,"\u002F?p=2815","2020-04-11 13:04:30 +0000",{"type":7,"value":122},[123],[10,124,12],{},"\u002F2020-04-11-blender-ue4-texture-python-material-fix",{"title":5,"description":12},"_legacy\u002F2020\u002F2020-04-11-blender-ue4-texture-python-material-fix",[129,130,131],"UE4","Blender","Python","YOQkIQJdqcC6WBD4OxarTU9B2stqZH2vyA88rL6ZNz8",1788763177515]