From d52a6a154d41554c4e50aeea8ac67a6f3e27bcba Mon Sep 17 00:00:00 2001 From: BleuRaven Date: Sat, 15 Sep 2018 23:25:29 +0200 Subject: [PATCH] Add files via upload --- blender-for-unrealengine/__init__.py | 1004 +++++++++++++++++++ blender-for-unrealengine/bfu_basics.py | 79 ++ blender-for-unrealengine/bfu_exportasset.py | 323 ++++++ blender-for-unrealengine/bfu_utils.py | 678 +++++++++++++ blender-for-unrealengine/bfu_writetext.py | 476 +++++++++ 5 files changed, 2560 insertions(+) create mode 100644 blender-for-unrealengine/__init__.py create mode 100644 blender-for-unrealengine/bfu_basics.py create mode 100644 blender-for-unrealengine/bfu_exportasset.py create mode 100644 blender-for-unrealengine/bfu_utils.py create mode 100644 blender-for-unrealengine/bfu_writetext.py diff --git a/blender-for-unrealengine/__init__.py b/blender-for-unrealengine/__init__.py new file mode 100644 index 00000000..2064edb4 --- /dev/null +++ b/blender-for-unrealengine/__init__.py @@ -0,0 +1,1004 @@ +#====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +#======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# This addons allows to easily export several objects at the same time in .fbx +# for use in unreal engine 4 by removing the usual constraints +# while respecting UE4 naming conventions and a clean tree structure. +# It also contains a small toolkit for collisions and sockets +# xavierloux.com +# ---------------------------------------------- + + +bl_info = { + 'name': 'Blender for UnrealEngine', + 'description': "This add-ons allows to easily export several " + "objects at the same time for use in unreal engine 4.", + 'author': 'Loux Xavier (BleuRaven)', + 'version': (0, 2, 0), + 'blender': (2, 79, 0), + 'location': 'View3D > Tool > Unreal Engine 4', + 'warning': '', + "wiki_url": "https://github.com/xavier150/blender-for-unrealengine-addons", + 'tracker_url': '', + 'support': 'COMMUNITY', + 'category': 'Import-Export'} + + +import os +import bpy +import fnmatch +import time +from bpy.props import ( + StringProperty, + BoolProperty, + EnumProperty, + IntProperty, + FloatProperty, + BoolVectorProperty, + PointerProperty, + CollectionProperty + ) + + +import importlib +from . import bfu_exportasset +importlib.reload(bfu_exportasset) +from . import bfu_writetext +importlib.reload(bfu_writetext) + +from .bfu_basics import * +from .bfu_utils import * + + +class ue4ObjectPropertiesPanel(bpy.types.Panel): + #Is Object Properties panel + + bl_idname = "panel.ue4.obj-properties" + bl_label = "Object Properties" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + bpy.types.Object.ExportEnum = EnumProperty( + name = "Export type", + description = "Export procedure", + items = [ + ("auto", "Auto", "Exports only if one of the parents is \"Export recursive\"", "KEY_HLT", 1), + ("export_recursive", "Export recursive", "Export self object and all children", "KEYINGSET", 2), + ("dont_export", "Not exported", "Will never export", "KEY_DEHLT", 3) + ] + ) + + bpy.types.Object.exportFolderName = StringProperty( + name = "Sub folder name", + description = 'Sub folder name. No Sub folder created if left empty', + maxlen = 64, + default = "", + subtype = 'FILE_NAME' + ) + + bpy.types.Object.ForceStaticMesh = BoolProperty( + name="Force staticMesh", + description="Force export asset like a StaticMesh if is ARMATURE type", + default=False + ) + + bpy.types.Object.exportDeformOnly = BoolProperty( + name="Export only deform Bones", + description="Only write deforming bones (and non-deforming ones when they have deforming children)", + default=True + ) + + + def draw(self, context): + + + layout = self.layout + obj = context.object + if obj is not None: + + AssetType = layout.row() + AssetType.prop(obj, 'name', text="", icon='OBJECT_DATA') + AssetType.label('('+ GetAssetType(obj)+')') #Show asset type + + ExportType = layout.column() + ExportType.prop(obj, 'ExportEnum') + + if obj.ExportEnum == "export_recursive": + folderNameProperty = layout.column() + folderNameProperty.prop(obj, 'exportFolderName', icon='FILE_FOLDER') + + if obj.type == "ARMATURE": + AssetType2 = layout.column() + AssetType2.prop(obj, "ForceStaticMesh") #Show asset type + if GetAssetType(obj) == "SkeletalMesh": + AssetType2.prop(obj, 'exportDeformOnly') + else: + layout.label('Pleas select obj for show properties.') + + +class ue4ObjectImportPropertiesPanel(bpy.types.Panel): + #Is Object Properties panel + + bl_idname = "panel.ue4.obj-import-properties" + bl_label = "Object Import Properties" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + #ImportUI + #https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxImportUI/index.html + + bpy.types.Object.CreatePhysicsAsset = BoolProperty( + name = "Create PhysicsAsset", + description = "If checked, create a PhysicsAsset when is imported", + default=True + ) + + #StaticMeshImportData + #https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxStaticMeshImportData/index.html + + bpy.types.Object.UseStaticMeshLODGroup = BoolProperty( + name = "", + description = '', + default=False + ) + bpy.types.Object.StaticMeshLODGroup = StringProperty( + name = "LOD Group", + description = "The LODGroup to associate with this mesh when it is imported. Default: LevelArchitecture, SmallProp, LargeProp, Deco, Vista, Foliage, HighDetail" , + maxlen = 32, + default = "SmallProp" + ) + + bpy.types.Object.UseStaticMeshLightMapRes = BoolProperty( + name = "", + description = '', + default=False + ) + bpy.types.Object.StaticMeshLightMapRes = IntProperty( + name = "Light Map resolution", + description = " This is the resolution of the light map" , + soft_max = 2048, + soft_min = 16, + max = 4096, #Max for unreal + min = 4, #Min for unreal + default = 16 + ) + + bpy.types.Object.GenerateLightmapUVs = BoolProperty( + name = "Generate LightmapUVs", + description = "" , + default=True, + ) + + #SkeletalMeshImportData + #https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxSkeletalMeshImportData/index.html + + #UFbxTextureImportData + #https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxTextureImportData/index.html + + bpy.types.Object.MaterialSearchLocation = EnumProperty( + name = "Material search location", + description = "Specify where we should search for matching materials when importing", + items = [ + ("Local", "Local", "Search for matching material in local import folder only.", 1), + ("UnderParent", "UnderParent", "Search for matching material recursively from parent folder.", 2), + ("UnderRoot", "UnderRoot", "Search for matching material recursively from root folder.", 3), + ("AllAssets", "AllAssets", "Search for matching material in all assets folders.", 4) + ] + ) + + + + def draw(self, context): + + + layout = self.layout + obj = context.object + if obj is not None: + if obj.ExportEnum == "export_recursive": + + #StaticMesh and SkeletalMesh prop + if GetAssetType(obj) == "StaticMesh" or GetAssetType(obj) == "SkeletalMesh": + MaterialSearchLocation = layout.row() + MaterialSearchLocation.prop(obj, 'MaterialSearchLocation') + + #StaticMesh prop + if GetAssetType(obj) == "StaticMesh": + StaticMeshLODGroup = layout.row() + StaticMeshLODGroup.prop(obj, 'UseStaticMeshLODGroup', text="") + StaticMeshLODGroupChild = StaticMeshLODGroup.column() + StaticMeshLODGroupChild.enabled = obj.UseStaticMeshLODGroup + StaticMeshLODGroupChild.prop(obj, 'StaticMeshLODGroup') + + StaticMeshLightMapRes = layout.row() + StaticMeshLightMapRes.prop(obj, 'UseStaticMeshLightMapRes', text="") + StaticMeshLightMapResChild = StaticMeshLightMapRes.column() + StaticMeshLightMapResChild.enabled = obj.UseStaticMeshLightMapRes + StaticMeshLightMapResChild.prop(obj, 'StaticMeshLightMapRes') + + GenerateLightmapUVs = layout.row() + GenerateLightmapUVs.prop(obj, 'GenerateLightmapUVs') + + + #SkeletalMesh prop + if GetAssetType(obj) == "SkeletalMesh": + CreatePhysicsAsset = layout.row() + CreatePhysicsAsset.prop(obj, "CreatePhysicsAsset") + layout.label('...') + + else: + layout.label('...') + layout.label('Pleas select obj for show properties.') + + +class ue4AnimPropertiesPanel(bpy.types.Panel): + #Is Animation Properties panel + + bl_idname = "panel.ue4.Anim-properties" + bl_label = "Animation Properties" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + #Animation : + + class ACTION_UL_ExportTarget(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + ActionIsValid = False + try: + bpy.data.actions[item.name] + ActionIsValid = True + except: + pass + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if ActionIsValid: #If action is valid + #layout.prop(item, "name", text="", emboss=False, icon="ACTION") #Debug only for see target line + layout.prop(bpy.data.actions[item.name], "name", text="", emboss=False, icon="ACTION") + layout.prop(item, "use", text="") + else: + dataText = ('Action data named "' + item.name + '" Not Found. Please clic on update') + layout.label(text=dataText, icon="ERROR") + # Not optimised for 'GRID' layout type. + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) + + class ObjExportAction(bpy.types.PropertyGroup): + name = StringProperty(name="Action data name", default="Unknown") + use = BoolProperty(name="use this action", default=False) + + bpy.utils.register_class(ACTION_UL_ExportTarget) + bpy.utils.register_class(ObjExportAction) + + bpy.types.Object.exportActionList = CollectionProperty( + #properties used with ""export_specific_list" on exportActionEnum + type=ObjExportAction + ) + + bpy.types.Object.exportActionEnum = EnumProperty( + name = "Action to export", + description = "Export procedure for actions (Animations and poses)", + items = [ + ("export_auto", "Export auto", "Export all actions connected to the bones names", "FILE_SCRIPT", 1), + ("export_specific_list", "Export specific list", "Export only actions that are checked in the list", "LINENUMBERS_ON", 2), + ("export_specific_prefix", "Export specific prefix", "Export only actions with a specific prefix or the beginning of the actions names", "SYNTAX_ON", 3), + ("dont_export", "Not exported", "No action will be exported", "MATPLANE", 4) + ] + ) + + bpy.types.Object.active_ObjectAction = IntProperty( + name="Active Scene Action", + description="Index of the currently active object action", + default=0 + ) + + + bpy.types.Object.PrefixNameToExport = StringProperty( + #properties used with ""export_specific_prefix" on exportActionEnum + name = "Prefix name", + description = "Indicate the prefix of the actions that must be exported", + maxlen = 32, + default = "Example_", + ) + + bpy.types.Object.AnimStartEndTimeEnum = EnumProperty( + name = "Animation start/end time", + description = "Set when animation starts and end", + items = [ + ("with_keyframes", "Auto", "The time will be defined according to the first and the last frame", "KEYTYPE_KEYFRAME_VEC", 1), + ("with_sceneframes", "Scene time", "Time will be equal to the scene time", "SCENE_DATA", 2), + ("with_customframes", "Custom time", 'The time of all the animations of this object is defined by you. Use "AnimCustomStartTime" and "AnimCustomEndTime"', "HAND", 3), + ] + ) + + bpy.types.Object.AnimCustomStartTime = IntProperty( + name = "Custom start time", + description = "Set when animation start", + default=0 + ) + + bpy.types.Object.AnimCustomEndTime = IntProperty( + name = "Custom end time", + description = "Set when animation end", + default=1 + ) + + + bpy.types.Object.SampleAnimForExport = FloatProperty( + name="Sampling Rate", + description="How often to evaluate animated values (in frames)", + min=0.01, max=100.0, + soft_min=0.01, soft_max=100.0, + default=1.0, + ) + + bpy.types.Object.SimplifyAnimForExport = FloatProperty( + name="Simplify animations", + description="How much to simplify baked values (0.0 to disable, the higher the more simplified)", + min=0.0, max=100.0, # No simplification to up to 10% of current magnitude tolerance. + soft_min=0.0, soft_max=10.0, + default=0.0, + ) + + class UpdateObjActionButton(bpy.types.Operator): + bl_label = "Update action list" + bl_idname = "object.updateobjaction" + bl_description = "Update action list" + + def execute(self, context): + def UpdateExportActionList(obj): + #Update the provisional action list known by the object + + def SetUseFromLast(list, ActionName): + for item in list: + if item[0] == ActionName: + if item[1] == True: + return True + return False + + AnimSave = [["", False]] + for Anim in obj.exportActionList: #CollectionProperty + name = Anim.name + use = Anim.use + AnimSave.append([name, use]) + obj.exportActionList.clear() + for action in bpy.data.actions: + obj.exportActionList.add().name = action.name + obj.exportActionList[action.name].use = SetUseFromLast(AnimSave, action.name) + UpdateExportActionList(bpy.context.object) + return {'FINISHED'} + + class ShowActionToExport(bpy.types.Operator): + bl_label = "Show action(s)" + bl_idname = "object.showobjaction" + bl_description = "Click to show actions that are to be exported with this armature." + + def execute(self, context): + obj = context.object + actions = GetActionToExport(obj) + popup_title = "Action list" + if len(actions) > 1: + popup_title = str(len(actions))+' action(s) found for obj named "'+obj.name+'".' + else: + popup_title = 'No actions found for obj named "'+obj.name+'".' + + def draw(self, context): + col = self.layout.column() + for action in actions: + row = col.row() + row.label("- "+action.name+GetActionType(action)) + bpy.context.window_manager.popup_menu(draw, title=popup_title, icon='ACTION') + return {'FINISHED'} + + + def draw(self, context): + + layout = self.layout + obj = context.object + if obj is not None: + if obj.ExportEnum == "export_recursive": + if GetAssetType(obj) == "SkeletalMesh" or GetAssetType(obj) == "Camera": + + #Action time + ActionTimeProperty = layout.column() + if obj.type != "CAMERA": + ActionTimeProperty.prop(obj, 'AnimStartEndTimeEnum') + if obj.AnimStartEndTimeEnum == "with_customframes": + ActionTimePropertyChild=ActionTimeProperty = layout.row() + ActionTimePropertyChild.prop(obj, 'AnimCustomStartTime') + ActionTimePropertyChild.prop(obj, 'AnimCustomEndTime') + else: + layout.label("Note: animation start/end use scene frames with the camera for the sequencer.") + + if GetAssetType(obj) == "SkeletalMesh": + #Action list + ActionListProperty = layout.column() + ActionListProperty.prop(obj, 'exportActionEnum') + if obj.exportActionEnum == "export_specific_list": + ActionListProperty.template_list( + "ACTION_UL_ExportTarget", "", # type and unique id + obj, "exportActionList", # pointer to the CollectionProperty + obj, "active_ObjectAction", # pointer to the active identifier + maxrows=5, + rows=5 + ) + ActionListProperty.operator("object.updateobjaction", icon='RECOVER_LAST') + if obj.exportActionEnum == "export_specific_prefix": + ActionListProperty.prop(obj, 'PrefixNameToExport') + + #Action fbx properties + propsFbx = layout.row() + propsFbx.prop(obj, 'SampleAnimForExport') + propsFbx.prop(obj, 'SimplifyAnimForExport') + + #Armature export action list feedback + if GetAssetType(obj) == "SkeletalMesh": + ArmaturePropertyInfo = layout.row().box().split(percentage = 0.75 ) + ActionNum = len(GetActionToExport(obj)) + actionFeedback = str(ActionNum) + " Action(s) will be exported with this armature." + ArmaturePropertyInfo.label( actionFeedback, icon='INFO') + ArmaturePropertyInfo.operator("object.showobjaction") + layout.label('Note: The Action with only one frame are exported like Pose.') + else: + layout.label('This assets is not a SkeletalMesh or Camera') + else: + layout.label('...') + + +class ue4CollisionsAndSocketsPanel(bpy.types.Panel): + #Is Collisions And Sockets panel + + bl_idname = "panel.ue4.collisionsandsockets" + bl_label = "Collisions And Sockets" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + class ConvertToUECollisionButtonBox(bpy.types.Operator): + bl_label = "Convert to box (UBX)" + bl_idname = "object.converttoboxcollision" + bl_description = "Convert selected mesh(es) to Unreal collision ready for export (Boxes type)" + + def execute(self, context): + ConvertedObj = ConvertToUe4SubObj("Box", bpy.context.selected_objects, True) + if len(ConvertedObj) > 0 : + self.report({'INFO'}, str(len(ConvertedObj)) + " object(s) of the selection have be converted to UE4 Box collisions." ) + else : + self.report({'WARNING'}, "Please select two objects. (Active object is the owner of the collision)") + return {'FINISHED'} + + + class ConvertToUECollisionButtonCapsule(bpy.types.Operator): + bl_label = "Convert to capsule (UCP)" + bl_idname = "object.converttocapsulecollision" + bl_description = "Convert selected mesh(es) to Unreal collision ready for export (Capsules type)" + + def execute(self, context): + ConvertedObj = ConvertToUe4SubObj("Capsule", bpy.context.selected_objects, True) + if len(ConvertedObj) > 0 : + self.report({'INFO'}, str(len(ConvertedObj)) + " object(s) of the selection have be converted to UE4 Capsule collisions." ) + else : + self.report({'WARNING'}, "Please select two objects. (Active object is the owner of the collision)") + return {'FINISHED'} + + + class ConvertToUECollisionButtonSphere(bpy.types.Operator): + bl_label = "Convert to sphere (USP)" + bl_idname = "object.converttospherecollision" + bl_description = "Convert selected mesh(es) to Unreal collision ready for export (Spheres type)" + + def execute(self, context): + ConvertedObj = ConvertToUe4SubObj("Sphere", bpy.context.selected_objects, True) + if len(ConvertedObj) > 0 : + self.report({'INFO'}, str(len(ConvertedObj)) + " object(s) of the selection have be converted to UE4 Sphere collisions." ) + else : + self.report({'WARNING'}, "Please select two objects. (Active object is the owner of the collision)") + return {'FINISHED'} + + + class ConvertToUECollisionButtonConvex(bpy.types.Operator): + bl_label = "Convert to convex shape (UCX)" + bl_idname = "object.converttoconvexcollision" + bl_description = "Convert selected mesh(es) to Unreal collision ready for export (Convex shapes type)" + + def execute(self, context): + ConvertedObj = ConvertToUe4SubObj("Convex", bpy.context.selected_objects, True) + if len(ConvertedObj) > 0 : + self.report({'INFO'}, str(len(ConvertedObj)) + " object(s) of the selection have be converted to UE4 Convex Shape collisions.") + else : + self.report({'WARNING'}, "Please select two objects. (Active object is the owner of the collision)") + return {'FINISHED'} + + + class ConvertToUESocketButton(bpy.types.Operator): + bl_label = "Convert to socket (SOCKET)" + bl_idname = "object.converttosocket" + bl_description = "Convert selected Empty(s) to Unreal sockets ready for export" + + def execute(self, context): + ConvertedObj = ConvertToUe4SubObj("Socket", bpy.context.selected_objects, True) + if len(ConvertedObj) > 0 : + self.report({'INFO'}, str(len(ConvertedObj)) + " object(s) of the selection have be converted to to UE4 Socket." ) + else : + self.report({'WARNING'}, "Please select two objects. (Active object is the owner of the collision)") + return {'FINISHED'} + + def draw(self, context): + + def FoundTypeInSelect(targetType): #Return True is a specific type is found + for obj in bpy.context.selected_objects: + if obj != bpy.context.active_object: + if obj.type == targetType: + return True + return False + + self.layout.label("Convert selected object to Unreal collision or socket (Static Mesh only)", icon='PHYSICS') + + convertMeshButtons = self.layout.row().split(percentage = 0.80 ) + convertMeshButtons = convertMeshButtons.column() + convertMeshButtons.enabled = FoundTypeInSelect("MESH") + convertMeshButtons.operator("object.converttoboxcollision", icon='MESH_CUBE') + convertMeshButtons.operator("object.converttoconvexcollision", icon='MESH_ICOSPHERE') + convertMeshButtons.operator("object.converttocapsulecollision", icon='MESH_CAPSULE') + convertMeshButtons.operator("object.converttospherecollision", icon='SOLID') + + convertMeshButtons = self.layout.row().split(percentage = 0.80 ) + convertEmptyButtons = convertMeshButtons.column() + convertEmptyButtons.enabled = FoundTypeInSelect("EMPTY") + convertEmptyButtons.operator("object.converttosocket", icon='OUTLINER_DATA_EMPTY') + + +class ue4NomenclaturePanel(bpy.types.Panel): + #Is FPS Export panel + + bl_idname = "panel.ue4.exportnomenclature" + bl_label = "Nomenclature" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + + #Prefix + bpy.types.Scene.static_prefix_export_name = bpy.props.StringProperty( + name = "StaticMesh Prefix", + description = "Prefix of staticMesh", + maxlen = 32, + default = "SM_") + + bpy.types.Scene.skeletal_prefix_export_name = bpy.props.StringProperty( + name = "SkeletalMesh Prefix ", + description = "Prefix of SkeletalMesh", + maxlen = 32, + default = "SK_") + + bpy.types.Scene.anim_prefix_export_name = bpy.props.StringProperty( + name = "AnimationSequence Prefix", + description = "Prefix of AnimationSequence", + maxlen = 32, + default = "Anim_") + + bpy.types.Scene.pose_prefix_export_name = bpy.props.StringProperty( + name = "AnimationSequence(Pose) Prefix", + description = "Prefix of AnimationSequence with only one frame", + maxlen = 32, + default = "Pose_") + + bpy.types.Scene.camera_prefix_export_name = bpy.props.StringProperty( + name = "Camera anim Prefix", + description = "Prefix of camera animations", + maxlen = 32, + default = "Cam_") + + #Sub folder + bpy.types.Scene.anim_subfolder_name = bpy.props.StringProperty( + name = "Animations sub folder name", + description = "name of sub folder for animations", + maxlen = 32, + default = "Anim") + + #File path + bpy.types.Scene.export_static_file_path = bpy.props.StringProperty( + name = "StaticMesh export file path", + description = "Choose a directory to export StaticMesh(s)", + maxlen = 512, + default = "//ExportedFbx\StaticMesh\\", + subtype = 'DIR_PATH') + + bpy.types.Scene.export_skeletal_file_path = bpy.props.StringProperty( + name = "SkeletalMesh export file path", + description = "Choose a directory to export SkeletalMesh(s)", + maxlen = 512, + default = "//ExportedFbx\SkeletalMesh\\", + subtype = 'DIR_PATH') + + bpy.types.Scene.export_camera_file_path = bpy.props.StringProperty( + name = "Camera export file path", + description = "Choose a directory to export Camera(s)", + maxlen = 512, + default = "//ExportedFbx\Sequencer\\", + subtype = 'DIR_PATH') + + bpy.types.Scene.export_other_file_path = bpy.props.StringProperty( + name = "Other export file path", + description = "Choose a directory to export text file and other", + maxlen = 512, + default = "//ExportedFbx\Other\\", + subtype = 'DIR_PATH') + + + def draw(self, context): + scn = context.scene + + #Prefix + propsPrefix = self.layout.row() + propsPrefix = propsPrefix.column() + propsPrefix.prop(scn, 'static_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scn, 'skeletal_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scn, 'anim_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scn, 'pose_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scn, 'camera_prefix_export_name', icon='OBJECT_DATA') + + #Sub folder + propsSub = self.layout.row() + propsSub = propsSub.column() + propsSub.prop(scn, 'anim_subfolder_name', icon='FILE_FOLDER') + + #File path + propsPath = self.layout.row() + propsPath = propsPath.column() + propsPath.prop(scn, 'export_static_file_path') + propsPath.prop(scn, 'export_skeletal_file_path') + propsPath.prop(scn, 'export_camera_file_path') + propsPath.prop(scn, 'export_other_file_path') + + +class ue4ImportScriptPanel(bpy.types.Panel): + #Is Import script panel + + bl_idname = "panel.ue4.importScript" + bl_label = "Import Script" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + bpy.types.Scene.unreal_import_location = bpy.props.StringProperty( + name = "Unreal import location", + description = "Unreal import location in /Game/", + maxlen = 512, + default = 'ImportedFbx') + + bpy.types.Scene.unreal_levelsequence_reference = bpy.props.StringProperty( + name = "Unreal LevelSequence reference", + description = "Copy Reference from unreal ine Content Browser", + maxlen = 512, + default = "LevelSequence'/Game/ImportedFbx/MySequence.MySequence'") + + def draw(self, context): + scn = context.scene + + #Sub folder + propsSub = self.layout.row() + propsSub = propsSub.column() + propsSub.prop(scn, 'unreal_import_location', icon='FILE_FOLDER') + propsSub.prop(scn, 'unreal_levelsequence_reference', icon='FILE_FOLDER') + + +class ue4ExportPanel(bpy.types.Panel): + #Is Export panel + + bl_idname = "panel.ue4.export" + bl_label = "Export" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Unreal Engine 4" + + class UnrealExportedAsset(bpy.types.PropertyGroup): + #[AssetName , AssetType , ExportPath, ExportTime] + assetName = StringProperty(default="None") + assetType = StringProperty(default="None") #return from GetAssetType() + exportPath = StringProperty(default="None") + exportTime = FloatProperty(default=0) + object = PointerProperty(type=bpy.types.Object) + + bpy.utils.register_class(UnrealExportedAsset) + bpy.types.Scene.UnrealExportedAssetsList = CollectionProperty( + type=UnrealExportedAsset + ) + + class UnrealPotentialError(bpy.types.PropertyGroup): + type = IntProperty(default=0) #0:Info, 1:Warning, 2:Error + object = PointerProperty(type=bpy.types.Object) + itemName = StringProperty(default="None") + text = StringProperty(default="Unknown") + correctRef = StringProperty(default="None") + correctlabel = StringProperty(default="Fix it !") + correctDesc = StringProperty(default="Correct target error") + + bpy.utils.register_class(UnrealPotentialError) + bpy.types.Scene.potentialErrorList = CollectionProperty( + type=UnrealPotentialError + ) + + class ShowAssetToExport(bpy.types.Operator): + bl_label = "Show asset(s)" + bl_idname = "object.showasset" + bl_description = "Click to show assets that are to be exported." + + def execute(self, context): + obj = context.object + assets = GetFinalAssetToExport() + popup_title = "Assets list" + if len(assets) > 0: + popup_title = str(len(assets))+' asset(s) will be exported.' + else: + popup_title = 'No exportable assets were found.' + + def draw(self, context): + col = self.layout.column() + for asset in assets: + row = col.row() + if asset[0] is not None: + if asset[1] is not None: + row.label(" --> "+asset[1].name+" ("+asset[2]+")") + else: + row.label("- "+asset[0].name+" ("+asset[2]+")") + else: + row.label("- ("+asset[2]+")") + bpy.context.window_manager.popup_menu(draw, title=popup_title, icon='EXTERNAL_DATA') + return {'FINISHED'} + + + class CheckPotentialErrorPopup(bpy.types.Operator): + bl_label = "Check potential errors" + bl_idname = "object.checkpotentialerror" + bl_description = "Check potential errors" + correctedProperty = 0 + + class CorrectButton(bpy.types.Operator): + bl_label = "Fix it !" + bl_idname = "object.fixit" + bl_description = "Correct target error" + errorIndex = bpy.props.IntProperty(default=-1) + + def execute(self, context): + result = TryToCorrectPotentialError(self.errorIndex) + self.report({'INFO'}, result) + return {'FINISHED'} + + def execute(self, context): + self.report({'INFO'}, "ok") + return {'FINISHED'} + + def invoke(self, context, event): + def CorrectBadProperty(): + #Corrects bad properties + UpdatedProp = 0 + for obj in GetAllCollisionAndSocketsObj(): + if obj.ExportEnum == "export_recursive": + obj.ExportEnum = "auto" + UpdatedProp += 1 + return UpdatedProp + + def UpdateNameHierarchy(): + #Updates hierarchy names + for obj in GetAllCollisionAndSocketsObj(): + if fnmatch.fnmatchcase(obj.name, "UBX*"): + ConvertToUe4SubObj("Box", [obj]) + if fnmatch.fnmatchcase(obj.name, "UCP*"): + ConvertToUe4SubObj("Capsule", [obj]) + if fnmatch.fnmatchcase(obj.name, "USP*"): + ConvertToUe4SubObj("Sphere", [obj]) + if fnmatch.fnmatchcase(obj.name, "UCX*"): + ConvertToUe4SubObj("Convex", [obj]) + if fnmatch.fnmatchcase(obj.name, "SOCKET*"): + ConvertToUe4SubObj("Socket", [obj]) + + self.correctedProperty = CorrectBadProperty() + UpdateNameHierarchy() + UpdateUnrealPotentialError() + wm = context.window_manager + return wm.invoke_popup(self, width = 900) + + def check(self, context): + return True + + def draw(self, context): + + + layout = self.layout + if len(bpy.context.scene.potentialErrorList) > 0 : + popup_title = str(len(bpy.context.scene.potentialErrorList))+" potential error(s) found!" + else: + popup_title = "No potential error to correct!" + + if self.correctedProperty > 0 : + CheckInfo = str(self.correctedProperty) + " properties corrected." + else: + CheckInfo = "no properties to correct." + + layout.label(popup_title) + layout.label("Hierarchy names updated and " + CheckInfo) + layout.separator() + row = layout.row() + col = row.column() + for x in range(len(bpy.context.scene.potentialErrorList)): + error = bpy.context.scene.potentialErrorList[x] + + myLine = col.box().split(percentage = 0.85 ) + #myLine = col.split(percentage = 0.85 ) + #---- + if error.type == 0: + msgType = 'INFO' + msgIcon = 'INFO' + elif error.type == 1: + msgType = 'WARNING' + msgIcon = 'ERROR' + elif error.type == 2: + msgType = 'ERROR' + msgIcon = 'CANCEL' + #---- + errorFullMsg = msgType+": "+error.text + myLine.label(text=errorFullMsg, icon=msgIcon) + if error.correctRef != "None": + props = myLine.operator("object.fixit", text=error.correctlabel) + props.errorIndex = x + + + class ExportForUnrealEngineButton(bpy.types.Operator): + bl_label = "Export for UnrealEngine 4" + bl_idname = "object.exportforunreal" + bl_description = "Export all assets of this scene." + + def execute(self, context): + scene = bpy.context.scene + def GetIfOneTypeCheck(): + if (scene.static_export + or scene.skeletal_export + or scene.anin_export + or scene.pose_export + or scene.camera_export): + return True + else: + return False + + if GetIfOneTypeCheck(): + #Primary check if file is saved to avoid windows PermissionError + if bpy.data.is_saved: + scene.UnrealExportedAssetsList.clear() + start_time = time.process_time() + bfu_exportasset.ExportForUnrealEngine() + bfu_writetext.WriteAllTextFiles() + + if len(scene.UnrealExportedAssetsList) > 0: + self.report({'INFO'}, "Export of "+str(len(scene.UnrealExportedAssetsList))+ + " asset(s) has been finalized in "+str(time.process_time()-start_time)+" sec. Look in console for more info.") + print("========================= Exported asset(s) =========================") + print("") + for line in bfu_writetext.WriteExportLog().splitlines(): + print(line) + print("") + print("========================= ... =========================") + else: + self.report({'WARNING'}, "Not found assets. with \"Export and child\" properties.") + else: + self.report({'WARNING'}, "Please save this blend file before export") + else: + self.report({'WARNING'}, "No asset type is checked.") + return {'FINISHED'} + + + #Categories : + bpy.types.Scene.static_export = bpy.props.BoolProperty( + name = "StaticMesh(s)", + description = "Check mark to export StaticMesh(es)", + default = True + ) + + bpy.types.Scene.skeletal_export = bpy.props.BoolProperty( + name = "SkeletalMesh(s)", + description = "Check mark to export SkeletalMesh(es)", + default = True + ) + + bpy.types.Scene.anin_export = bpy.props.BoolProperty( + name = "Animation(s)", + description = "Check mark to export Animation(s)", + default = True + ) + + bpy.types.Scene.pose_export = bpy.props.BoolProperty( + name = "Pose(s)", + description = "Check mark to export Pose(s)", + default = True + ) + + bpy.types.Scene.camera_export = bpy.props.BoolProperty( + name = "Camera(s)", + description = "Check mark to export Camera(s)", + default = False + ) + + #Additional file + bpy.types.Scene.text_exportLog = bpy.props.BoolProperty( + name = "Export Log", + description = "Check mark to write export log in file", + default = True + ) + + bpy.types.Scene.text_ImportAssetScript = bpy.props.BoolProperty( + name = "Import assets script", + description = "Check mark to write import assets script in file", + default = True + ) + + bpy.types.Scene.text_ImportSequenceScript = bpy.props.BoolProperty( + name = "Import sequence script", + description = "Check mark to write import sequence script in file", + default = True + ) + + + def draw(self, context): + scn = context.scene + + #Categories : + layout = self.layout + row = layout.row() + col = row.column() + + #Assets + AssetsCol = row.column() + AssetsCol.label("Asset types to export", icon='EXTERNAL_DATA') + AssetsCol.prop(scn, 'static_export') + AssetsCol.prop(scn, 'skeletal_export') + AssetsCol.prop(scn, 'anin_export') + AssetsCol.prop(scn, 'pose_export') + AssetsCol.prop(scn, 'camera_export') + layout.separator() + #Additional file + FileCol = row.column() + FileCol.label("Additional file", icon='EXTERNAL_DATA') + FileCol.prop(scn, 'text_exportLog') + FileCol.prop(scn, 'text_ImportAssetScript') + FileCol.prop(scn, 'text_ImportSequenceScript') + + #Feedback info : + AssetNum = len(GetFinalAssetToExport()) + AssetInfo = layout.row().box().split(percentage = 0.75 ) + AssetFeedback = str(AssetNum) + " Asset(s) will be exported." + AssetInfo.label( AssetFeedback, icon='INFO') + AssetInfo.operator("object.showasset") + #Export button : + checkButton = layout.row() + checkButton.operator("object.checkpotentialerror", icon='FILE_TICK') + exportButton = layout.row() + exportButton.scale_y = 2.0 + exportButton.operator("object.exportforunreal", icon='EXPORT') + + +#############################[...]############################# + + +def register(): + bpy.utils.register_module(__name__) + +def unregister(): + bpy.utils.unregister_module(__name__) \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_basics.py b/blender-for-unrealengine/bfu_basics.py new file mode 100644 index 00000000..2ed68079 --- /dev/null +++ b/blender-for-unrealengine/bfu_basics.py @@ -0,0 +1,79 @@ +#====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +#======================= END GPL LICENSE BLOCK ============================= + +import os +import bpy +from mathutils import Vector +from mathutils import Quaternion + +def ChecksRelationship(arrayA, arrayB): + #Checks if it exits an identical variable in two lists + + for a in arrayA: + for b in arrayB: + if a == b: + return True + return False + + +def GetChilds(obj): + #Get all direct childs of a object + + ChildsObj = [] + for childObj in bpy.data.objects: + pare = childObj.parent + if pare is not None: + if pare.name == obj.name: + ChildsObj.append(childObj) + + return ChildsObj + + +def GetRecursiveChilds(obj): + #Get all recursive childs of a object + + saveObj = [] + for newobj in GetChilds(obj): + for childs in GetRecursiveChilds(newobj): + saveObj.append(childs) + saveObj.append(newobj) + return saveObj + + +def VerifiDirs(directory): + #Check and create a folder if it does not exist + + if not os.path.exists(directory): + os.makedirs(directory) + + +def ValidFilename(filename): + # remove not allowed characters + import string + valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits) + filename = ''.join(c for c in filename if c in valid_chars) + return filename + + +def ResetArmaturePose(obj): + #Reset armature pose + + for x in obj.pose.bones: + x.rotation_quaternion = Quaternion((0,0,0),0) + x.scale = Vector((1,1,1)) + x.location = Vector((0,0,0)) \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_exportasset.py b/blender-for-unrealengine/bfu_exportasset.py new file mode 100644 index 00000000..300bead5 --- /dev/null +++ b/blender-for-unrealengine/bfu_exportasset.py @@ -0,0 +1,323 @@ +#====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +#======================= END GPL LICENSE BLOCK ============================= + + +import bpy +import time +import math +from .bfu_basics import * +from .bfu_utils import * + +import importlib +from . import bfu_writetext +importlib.reload(bfu_writetext) + +def ExportSingleFbxAnimation(dirpath, filename, obj, targetAction, actionType): + #Export a single animation or pose + + + scene = bpy.context.scene + filename = ValidFilename(filename) + curr_time = time.process_time() + print(obj.name) + UserAction = obj.animation_data.action #Save current action + + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + originalLoc = Vector((0,0,0)) + originalLoc = originalLoc + obj.location #Save object location + + obj.location = (0,0,0) #Moves object to the center of the scene for export + + SelectParentAndDesiredChilds(obj) + + ResetArmaturePose(obj) + obj.animation_data.action = targetAction #Apply desired action + scene.frame_start = GetDesiredActionStartEndTime(obj, targetAction)[0] + scene.frame_end = GetDesiredActionStartEndTime(obj, targetAction)[1] + + absdirpath = bpy.path.abspath(dirpath) + VerifiDirs(absdirpath) + fullpath = os.path.join( absdirpath , filename ) + + bpy.ops.export_scene.fbx( + filepath=fullpath, + check_existing=False, + version='BIN7400', + use_selection=True, + object_types={'ARMATURE', 'MESH'}, + add_leaf_bones=False, + use_armature_deform_only=obj.exportDeformOnly, + bake_anim=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + bake_anim_force_startend_keying=True, + bake_anim_step=obj.SampleAnimForExport, + bake_anim_simplify_factor=obj.SimplifyAnimForExport + ) + obj.location = originalLoc #Resets previous object location + ResetArmaturePose(obj) + obj.animation_data.action = UserAction #Resets previous action + exportTime = time.process_time()-curr_time + + MyAsset = scene.UnrealExportedAssetsList.add() + MyAsset.assetName = filename + MyAsset.assetType = actionType + MyAsset.exportPath = absdirpath + MyAsset.exportTime = exportTime + MyAsset.object = obj + return MyAsset + + +def ExportSingleFbxMesh(dirpath, filename, obj): + #Export a single Mesh + + scene = bpy.context.scene + filename = ValidFilename(filename) + curr_time = time.process_time() + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode = 'OBJECT') + originalLoc = Vector((0,0,0)) + originalLoc = originalLoc + obj.location #Save current object location + obj.location = (0,0,0) #Moves object to the center of the scene for export + #Set socket scale for Unreal + for socket in GetAllChildSocket(obj): + socket.delta_scale*=0.01 + + SelectParentAndDesiredChilds(obj) + absdirpath = bpy.path.abspath(dirpath) + VerifiDirs(absdirpath) + fullpath = os.path.join( absdirpath , filename ) + meshType = GetAssetType(obj) + + object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LAMP', 'MESH', 'OTHER'} + if meshType == "StaticMesh": + #Dont export ARMATURE with static mesh + object_types={'CAMERA', 'EMPTY', 'LAMP', 'MESH', 'OTHER'} + + bpy.ops.export_scene.fbx(filepath=fullpath, + check_existing=False, + version='BIN7400', + use_selection=True, + object_types=object_types, + mesh_smooth_type="FACE", + add_leaf_bones=False, + use_armature_deform_only=obj.exportDeformOnly, + bake_anim=False + ) + + obj.location = originalLoc #Resets previous object location + exportTime = time.process_time()-curr_time + + #Reset socket scale + for socket in GetAllChildSocket(obj): + socket.delta_scale*=100 + + MyAsset = scene.UnrealExportedAssetsList.add() + MyAsset.assetName = filename + MyAsset.assetType = meshType + MyAsset.exportPath = absdirpath + MyAsset.exportTime = exportTime + MyAsset.object = obj + return MyAsset + + +def ExportSingleFbxCamera(dirpath, filename, obj): + #Export single camera + + scene = bpy.context.scene + filename = ValidFilename(filename) + if obj.type != 'CAMERA': + return; + curr_time = time.process_time() + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode = 'OBJECT') + bpy.ops.object.select_all(action='DESELECT') + + #Select and rescale camera for export + obj.select = True + scene.objects.active = obj + obj.delta_scale*=0.01 + if obj.animation_data is not None: + action = obj.animation_data.action + scene.frame_start = GetDesiredActionStartEndTime(obj, action)[0] + scene.frame_end = GetDesiredActionStartEndTime(obj, action)[1] + + absdirpath = bpy.path.abspath(dirpath) + VerifiDirs(absdirpath) + fullpath = os.path.join( absdirpath , filename ) + + bpy.ops.export_scene.fbx( + filepath=fullpath, + check_existing=False, + axis_forward = ('-Z'), #-Z + axis_up = ("Y"), #Y + version='BIN7400', + use_selection=True, + object_types={'CAMERA'}, + add_leaf_bones=False, + use_armature_deform_only=obj.exportDeformOnly, + bake_anim=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + bake_anim_force_startend_keying=True, + bake_anim_step=obj.SampleAnimForExport, + bake_anim_simplify_factor=obj.SimplifyAnimForExport + ) + + #Reset camera scale + obj.delta_scale*=100 + + exportTime = time.process_time()-curr_time + + MyAsset = scene.UnrealExportedAssetsList.add() + MyAsset.assetName = filename + MyAsset.assetType = "Camera" + MyAsset.exportPath = absdirpath + MyAsset.exportTime = exportTime + MyAsset.object = obj + return MyAsset + +def ExportSingleAdditionalTrackCamera(dirpath, filename, obj): + #Export additional camera track for ue4 + #FocalLength + #FocusDistance + #Aperture + + absdirpath = bpy.path.abspath(dirpath) + VerifiDirs(absdirpath) + CameraAdditionalTrack = bfu_writetext.WriteSingleCameraAdditionalTrack(obj) + return bfu_writetext.ExportSingleText(CameraAdditionalTrack, absdirpath, filename) + +def ExportAllAssetByList(targetobjects): + #Export all objects that need to be exported from a list + + + if len(targetobjects) < 1: + return + + scene = bpy.context.scene + wm = bpy.context.window_manager + wm.progress_begin(0, len(GetFinalAssetToExport())) + + def UpdateProgress(): + wm.progress_update(len(scene.UnrealExportedAssetsList)) + UpdateProgress() + + for obj in targetobjects: + if obj.ExportEnum == "export_recursive": + + #Camera + if GetAssetType(obj) == "Camera" and scene.camera_export: + exportDir = os.path.join( scene.export_camera_file_path, obj.exportFolderName ) + UserStartFrame = scene.frame_start #Save current start frame + UserEndFrame = scene.frame_end #Save current end frame + ExportSingleFbxCamera(exportDir, GetObjExportFileName(obj), obj) + + ExportSingleAdditionalTrackCamera(exportDir, GetCameraTrackFileName(obj), obj) + scene.frame_start = UserStartFrame #Resets previous start frame + scene.frame_end = UserEndFrame #Resets previous end frame + UpdateProgress() + + #StaticMesh + if GetAssetType(obj) == "StaticMesh" and scene.static_export: + exportDir = os.path.join( scene.export_static_file_path, obj.exportFolderName ) + ExportSingleFbxMesh(exportDir, GetObjExportFileName(obj), obj) + UpdateProgress() + + if GetAssetType(obj) == "SkeletalMesh": + exportDir = os.path.join( scene.export_skeletal_file_path , obj.exportFolderName , obj.name ) + #SkeletalMesh + if scene.skeletal_export: + ExportSingleFbxMesh(exportDir, GetObjExportFileName(obj), obj) + UpdateProgress() + + for action in GetActionToExport(obj): + animExportDir = os.path.join( exportDir, scene.anim_subfolder_name ) + animType = GetActionType(action) + + #Animation + if animType == "Animation" and bpy.context.scene.anin_export == True: + UserStartFrame = scene.frame_start #Save current start frame + UserEndFrame = scene.frame_end #Save current end frame + ExportSingleFbxAnimation(animExportDir, GetActionExportFileName(obj, action), obj, action, "Animation") + scene.frame_start = UserStartFrame #Resets previous start frame + scene.frame_end = UserEndFrame #Resets previous end frame + UpdateProgress() + + #pose + if animType == "Pose" and bpy.context.scene.pose_export == True: + UserStartFrame = scene.frame_start #Save current start frame + UserEndFrame = scene.frame_end #Save current end frame + ExportSingleFbxAnimation(animExportDir, GetActionExportFileName(obj, action), obj, action, "Pose") + scene.frame_start = UserStartFrame #Resets previous start frame + scene.frame_end = UserEndFrame #Resets previous end frame + UpdateProgress() + + wm.progress_end() + + + + +def PrepareAndSaveDataForExport(): + + scene = bpy.context.scene + #----------------------------------------Save data + UserObjHide = [] + UserObjHideSelect = [] + for obj in scene.objects: #Save previous object visibility + UserObjHide.append(obj.hide) + UserObjHideSelect.append(obj.hide_select) + obj.hide = False + obj.hide_select = False + + LayerVisibility = [] + for x in range(20): #Save previous layer visibility + LayerVisibility.append(scene.layers[x]) + scene.layers[x] = True + + if obj is None: + scene.objects.active = bpy.data.objects[0] + + UserActive = bpy.context.active_object #Save current active object + UserMode = None + if UserActive and UserActive.mode != 'OBJECT' and bpy.ops.object.mode_set.poll(): + UserMode = UserActive.mode #Save current mode + bpy.ops.object.mode_set(mode='OBJECT') + UserSelected = bpy.context.selected_objects #Save current selected objects + #---------------------------------------- + + + ExportAllAssetByList(GetAllobjectsByExportType("export_recursive")) + + + #----------------------------------------Reset data + for x in range(20): + scene.layers[x] = LayerVisibility[x] + bpy.ops.object.select_all(action='DESELECT') + for obj in UserSelected: obj.select = True #Resets previous selected object + scene.objects.active = UserActive #Resets previous active object + if UserActive and UserMode and bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode=UserMode) #Resets previous mode + for x, obj in enumerate(scene.objects): + obj.hide = UserObjHide[x] #Resets previous object visibility + obj.hide_select = UserObjHideSelect[x] #Resets previous object visibility(select) + #---------------------------------------- + +def ExportForUnrealEngine(): + PrepareAndSaveDataForExport() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_utils.py b/blender-for-unrealengine/bfu_utils.py new file mode 100644 index 00000000..811dc71d --- /dev/null +++ b/blender-for-unrealengine/bfu_utils.py @@ -0,0 +1,678 @@ +#====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +#======================= END GPL LICENSE BLOCK ============================= + + +import bpy +import fnmatch +import bmesh +from .bfu_basics import * + + + +def GetAllobjectsByExportType(exportType): + #Find all objects with a specific ExportEnum property + + targetObj = [] + for obj in bpy.context.scene.objects: + prop = obj.ExportEnum + if prop == exportType: + targetObj.append(obj) + return(targetObj) + + +def GetAllCollisionAndSocketsObj(): + #Get any object that can be understood as a collision or a socket by unreal + + colObjs = [obj for obj in bpy.context.scene.objects if + fnmatch.fnmatchcase(obj.name, "UBX*") or + fnmatch.fnmatchcase(obj.name, "UCP*") or + fnmatch.fnmatchcase(obj.name, "USP*") or + fnmatch.fnmatchcase(obj.name, "UCX*") or + fnmatch.fnmatchcase(obj.name, "SOCKET*")] + return colObjs + + +def GetExportDesiredChilds(obj): + #Get only all child objects that must be exported with parent object + + DesiredObj = [] + for child in GetRecursiveChilds(obj): + print(child.name) + if child.ExportEnum != "dont_export": + DesiredObj.append(child) + return DesiredObj + + +def GetAllChildSocket(targetObj): + socket = [obj for obj in GetRecursiveChilds(targetObj) if + fnmatch.fnmatchcase(obj.name, "SOCKET*")] + return socket + + +def GetAllCollisionObj(): + #Get any object that can be understood as a collision or a socket by unreal + + colObjs = [obj for obj in bpy.context.scene.objects if + fnmatch.fnmatchcase(obj.name, "UBX*") or + fnmatch.fnmatchcase(obj.name, "UCP*") or + fnmatch.fnmatchcase(obj.name, "USP*") or + fnmatch.fnmatchcase(obj.name, "UCX*")] + return colObjs + + +def GetActionToExport(obj): + #Returns only the actions that will be exported with the Armature + + if obj.animation_data is None: + return [] + + TargetActionToExport = [] #Action list + if obj.exportActionEnum == "dont_export": + return [] + elif obj.exportActionEnum == "export_specific_list": + for action in bpy.data.actions: + for targetAction in obj.exportActionList: + if targetAction.use == True: + if targetAction.name == action.name: + TargetActionToExport.append(action) + + elif obj.exportActionEnum == "export_specific_prefix": + for action in bpy.data.actions: + if fnmatch.fnmatchcase(action.name, obj.PrefixNameToExport+"*"): + TargetActionToExport.append(action) + elif obj.exportActionEnum == "export_auto": + objBoneNames = [bone.name for bone in obj.data.bones] + for action in bpy.data.actions: + actionBoneNames = [group.name for group in action.groups] + if ChecksRelationship(objBoneNames, actionBoneNames): + TargetActionToExport.append(action) + + return TargetActionToExport + + +def GetDesiredActionStartEndTime(obj, action): + #Returns desired action or camera anim start/end time + #Return start with index 0 and end with index 1 + + scene = bpy.context.scene + if obj.type == "CAMERA": + startTime = scene.frame_start + endTime = scene.frame_end + return (startTime,endTime) + + elif obj.AnimStartEndTimeEnum == "with_keyframes": + startTime = action.frame_range.x #GetFirstActionFrame + endTime = action.frame_range.y #GetLastActionFrame + return (startTime,endTime) + + elif obj.AnimStartEndTimeEnum == "with_sceneframes": + startTime = scene.frame_start + endTime = scene.frame_end + return (startTime,endTime) + + elif obj.AnimStartEndTimeEnum == "with_customframes": + startTime = obj.AnimCustomStartTime + endTime = obj.AnimCustomEndTime + return (startTime,endTime) + + +def GetActionType(action): + #return action type + + if action.frame_range.y - action.frame_range.x == 1: + return "Pose" + return "Animation" + + +def GetAssetType(obj): + #Return asset type of a object + + if obj.type == "ARMATURE": + if obj.ForceStaticMesh == False: + return "SkeletalMesh" + return "StaticMesh" + elif obj.type == "CAMERA": + return "Camera" + else: + return "StaticMesh" + + +def CheckIsCollision(target): + #Return true if obj is a collision + for obj in GetAllCollisionObj(): + if obj == target: + return True + return False + + +def SelectParentAndDesiredChilds(obj): + #Selects only all child objects that must be exported with parent object + + bpy.ops.object.select_all(action='DESELECT') + for selectObj in GetExportDesiredChilds(obj): + selectObj.select = True + obj.select = True + bpy.context.scene.objects.active = obj + + +def GetFinalAssetToExport(): + #Returns all assets that will be exported + + scene = bpy.context.scene + TargetAassetToExport = [] #Obj, Action, type + + + for obj in GetAllobjectsByExportType("export_recursive"): + if GetAssetType(obj) == "SkeletalMesh": + #SkeletalMesh + if scene.skeletal_export: + TargetAassetToExport.append((obj,None,"SkeletalMesh")) + for action in GetActionToExport(obj): + #Animation + if scene.anin_export: + if GetActionType(action) == "Animation": + TargetAassetToExport.append((obj,action,"Animation")) + #Pose + if scene.pose_export: + if GetActionType(action) == "Pose": + TargetAassetToExport.append((obj,action,"Pose")) + #Camera + if GetAssetType(obj) == "Camera" and scene.camera_export: + TargetAassetToExport.append((obj,None,"Camera")) + + #StaticMesh + if GetAssetType(obj) == "StaticMesh" and scene.static_export: + TargetAassetToExport.append((obj,None,"StaticMesh")) + + return TargetAassetToExport + + +def GetObjExportFileName(obj): + #Generate assset file name + + scene = bpy.context.scene + assetType = GetAssetType(obj) + if assetType == "Camera": + return scene.camera_prefix_export_name+obj.name+".fbx" + elif assetType == "StaticMesh": + return scene.static_prefix_export_name+obj.name+".fbx" + elif assetType == "SkeletalMesh": + return scene.skeletal_prefix_export_name+obj.name+".fbx" + else: + return None + + +def GetActionExportFileName(obj, action): + #Generate action file name + + scene = bpy.context.scene + animType = GetActionType(action) + if animType == "Animation": + return scene.anim_prefix_export_name+obj.name+"_"+action.name+".fbx" + elif animType == "Pose": + return scene.pose_prefix_export_name+obj.name+"_"+action.name+".fbx" + else: + return None + + +def GetCameraTrackFileName(camera): + #Generate additional camera track file name + + scene = bpy.context.scene + return scene.camera_prefix_export_name+camera.name+"_AdditionalTrack.ini" + + +def GenerateUe4Name(name): + #Generate a new name with suffix number + + def IsValidName(testedName): + #Checks if an object uses this name. (If not is a valid name) + + for obj in bpy.context.scene.objects: + if testedName == obj.name: + return False + return True + + valid = False + number = 1 + newName = "" + if IsValidName(name): + return name + else: + while valid == False: + newName = name+"_"+str(number) + if IsValidName(newName): + return newName + else: + number = number+1 + return newName + + +def ConvertToUe4SubObj(collisionType, objsToConvert, useActiveAsOwner=False): + #Convect obj to ue4 sub objects (Collisions Shapes or Socket) + + ConvertedObjs = [] + + def ApplyConvexHull(obj): + mesh = obj.data + if not mesh.is_editmode: + bm = bmesh.new() + bm.from_mesh(mesh) #Mesh to Bmesh + acb = bmesh.ops.convex_hull(bm, input=bm.verts, use_existing_faces=True) + #acb = bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + bm.to_mesh(mesh) #BMesh to Mesh + + #Set the name of the Prefix depending on the type of collision in agreement with unreal FBX Pipeline + if collisionType == "Box": + prefixName = "UBX_" + elif collisionType == "Capsule": + prefixName = "UCP_" + elif collisionType == "Sphere": + prefixName = "USP_" + elif collisionType == "Convex": + prefixName = "UCX_" + + mat = bpy.data.materials.get("UE4Collision") + if mat is None: + mat = bpy.data.materials.new(name="UE4Collision") + mat.diffuse_color = (0, 0.6, 0) + mat.alpha = 0.1 + mat.use_transparency = True + mat.use_nodes = False + if bpy.context.scene.render.engine == 'CYCLES': + #sets up the nodes to create a transparent material with GLSL mat in Cycle + mat.use_nodes = True + node_tree = mat.node_tree + nodes = node_tree.nodes + nodes.clear() + out = nodes.new('ShaderNodeOutputMaterial') + out.location = (0,0) + mix = nodes.new('ShaderNodeMixShader') + mix.location = (-200,000) + mix.inputs[0].default_value = (0.95) + diff = nodes.new('ShaderNodeBsdfDiffuse') + diff.location = (-400,100) + diff.inputs[0].default_value = (0, 0.6, 0, 1) + trans = nodes.new('ShaderNodeBsdfTransparent') + trans.location = (-400,-100) + trans.inputs[0].default_value = (0, 0.6, 0, 1) + node_tree.links.new(diff.outputs['BSDF'], mix.inputs[1]) + node_tree.links.new(trans.outputs['BSDF'], mix.inputs[2]) + node_tree.links.new(mix.outputs['Shader'], out.inputs[0]) + + + #node + + for obj in objsToConvert: + if useActiveAsOwner == True: + ownerObj = bpy.context.active_object + bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) + else: + ownerObj = obj.parent + if ownerObj is not None: + if obj != ownerObj: + #Mesh + if obj.type == 'MESH': + ApplyConvexHull(obj) + obj.modifiers.clear() + obj.data + obj.data.materials.clear() + obj.active_material_index = 0 + obj.data.materials.append(mat) + obj.name = GenerateUe4Name(prefixName+ownerObj.name) + obj.show_wire = True + obj.show_transparent = True + ConvertedObjs.append(obj) + #Socket + if obj.type == 'EMPTY' and collisionType == "Socket": + if ownerObj.type == 'MESH': + if not obj.name.startswith("SOCKET_"): + obj.name = GenerateUe4Name("SOCKET_"+obj.name) + ConvertedObjs.append(obj) + else: + print(obj.name+" is already a socket") + ConvertedObjs.append(obj) + return ConvertedObjs + + +def UpdateUnrealPotentialError(): + #Find and reset list of all potential error in scene + + + PotentialErrors = bpy.context.scene.potentialErrorList + PotentialErrors.clear() + + #prepares the data to avoid unnecessary loops + objToCheck = [] + for obj in GetAllobjectsByExportType("export_recursive"): + objToCheck.append(obj) + for obj2 in GetExportDesiredChilds(obj): + objToCheck.append(obj2) + + MeshTypeToCheck = [] + for obj in objToCheck: + if obj.type == 'MESH': + MeshTypeToCheck.append(obj) + + MeshTypeWithoutCol = [] # is Mesh Type To Check Without Collision + for obj in MeshTypeToCheck: + if not CheckIsCollision(obj): + MeshTypeWithoutCol.append(obj) + + def CheckObjType(): + #Check if objects use a non-recommended type + for obj in objToCheck: + if obj.type == "SURFACE" or obj.type == "META" or obj.type == "FONT": + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.text = 'Object "'+obj.name+'" is a '+obj.type+'. The object of the type SURFACE, META and FONT is not recommended.' + MyError.object = obj + MyError.correctRef = "ConvertToMesh" + MyError.correctlabel = 'Convert to mesh' + def CheckShapeKeys(): + for obj in MeshTypeToCheck: + if obj.data.shape_keys is not None: + #Check that no modifiers is destructive for the key shapes + if len(obj.data.shape_keys.key_blocks) > 0: + for modif in obj.modifiers: + if modif.type != "ARMATURE" : + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.object = obj + MyError.itemName = modif.name + MyError.text = 'In object "'+obj.name+'" the modifier '+modif.type+' named "'+modif.name+'" can destroy shape keys. Please use only Armature modifier with shape keys.' + MyError.correctRef = "RemoveModfier" + MyError.correctlabel = 'Remove modifier' + + #Check that the key shapes are not out of bounds for Unreal + for key in obj.data.shape_keys.key_blocks: + #Min + if key.slider_min < -5: + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.object = obj + MyError.itemName = key.name + MyError.text = 'In object "'+obj.name+'" the shape key "'+key.name+'" is out of bounds for Unreal. The min range of must not be inferior to -5.' + MyError.correctRef = "SetKeyRangeMin" + MyError.correctlabel = 'Set min range to -5' + + #Max + if key.slider_max > 5: + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.object = obj + MyError.itemName = key.name + MyError.text = 'In object "'+obj.name+'" the shape key "'+key.name+'" is out of bounds for Unreal. The max range of must not be superior to 5.' + MyError.correctRef = "SetKeyRangeMax" + MyError.correctlabel = 'Set max range to -5' + + def CheckUVMaps(): + #Check that the objects have at least one UV map valid + for obj in MeshTypeWithoutCol: + if len(obj.data.uv_layers) < 1: + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.text = 'Object "'+obj.name+'" does not have any UV Layer.' + MyError.object = obj + MyError.correctRef = "CreateUV" + MyError.correctlabel = 'Create Smart UV Project' + + def CheckBadStaicMeshExportedLikeSkeletalMesh(): + #Check if the correct object is defined as exportable + for obj in MeshTypeToCheck: + for modif in obj.modifiers: + if modif.type == "ARMATURE" : + if obj.ExportEnum == "export_recursive": + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.text = 'In object "'+obj.name+'" the modifier '+modif.type+' named "'+modif.name+'will not be applied when exported with StaticMesh assets.' + + def CheckArmatureModNumber(): + #check that there is no more than one Modifier ARMATURE at the same time + for obj in MeshTypeToCheck: + ArmatureModifiers = 0 + for modif in obj.modifiers: + if modif.type == "ARMATURE" : + ArmatureModifiers = ArmatureModifiers + 1 + if ArmatureModifiers > 1: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'In object "'+obj.name+'" there are several Armature modifiers at the same time. Please use only one Armature modifier.' + + def CheckArmatureModData(): + #check the parameter of Modifier ARMATURE + for obj in MeshTypeToCheck: + for modif in obj.modifiers: + if modif.type == "ARMATURE" : + if modif.use_deform_preserve_volume == True: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'In object "'+obj.name+'" the modifier '+modif.type+' named "'+modif.name+'". The parameter Preserve Volume must be set to False.' + MyError.object = obj + MyError.itemName = modif.name + MyError.correctRef = "PreserveVolume" + MyError.correctlabel = 'Set Preserve Volume to False' + + def CheckArmatureBoneData(): + #check the parameter of the ARMATURE bones + for obj in objToCheck: + if GetAssetType(obj) == "SkeletalMesh": + for bone in obj.data.bones: + if bone.bbone_segments > 1: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'In object3 "'+obj.name+'" the bone named "'+bone.name+'". The parameter Bendy Bones / Segments must be set to 1.' + MyError.object = obj + MyError.itemName = bone.name + MyError.correctRef = "BoneSegments" + MyError.correctlabel = 'Set Bone Segments to 1' + + if bone.use_inherit_scale == False: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'In object2 "'+obj.name+'" the bone named "'+bone.name+'". The parameter Inherit Scale must be set to True.' + MyError.object = obj + MyError.itemName = bone.name + MyError.correctRef = "InheritScale" + MyError.correctlabel = 'Set Inherit Scale to True' + + def CheckArmatureValidChild(): + #Check that skeleton also has a mesh to export + for obj in objToCheck: + if GetAssetType(obj) == "SkeletalMesh": + childs = GetExportDesiredChilds(obj) + validChild = 0 + for child in childs: + if child.type == "MESH": + validChild += 1 + if validChild < 1: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'Object "'+obj.name+'" is an Armature and does not have any valid children.' + + def CheckVertexGroupWeight(): + #Check that all vertex have a weight + for obj in objToCheck: + if GetAssetType(obj) == "SkeletalMesh": + childs = GetExportDesiredChilds(obj) + for child in childs: + if child.type == "MESH": + #Prepare data + VertexWithZeroWeight = 0 + for vertex in child.data.vertices: + cumulateWeight = 0 + if len(vertex.groups) > 0: + for group in vertex.groups: + cumulateWeight += group.weight + if not cumulateWeight > 0: + VertexWithZeroWeight += 1 + else: + VertexWithZeroWeight += 1 + #Result data + if VertexWithZeroWeight > 0: + MyError = PotentialErrors.add() + MyError.type = 1 + MyError.text = 'Object named "'+child.name+'" contains '+str(VertexWithZeroWeight)+' vertex with zero cumulative weight.' + + + def CheckZeroScaleKeyframe(): + #Check that animations do not use a invalid value + for action in bpy.data.actions: + for fcurve in action.fcurves: + if fcurve.data_path.split(".")[-1] == "scale": + for key in fcurve.keyframe_points: + xCurve, yCurve = key.co + if key.co[1] == 0: + MyError = PotentialErrors.add() + MyError.type = 2 + MyError.text = 'In action "'+action.name+'" at frame '+str(key.co[0])+', the bone named "'+fcurve.data_path.split('"')[1]+'" has a zero value in scale transform. This is invalid in Unreal.' + + + CheckObjType() + CheckShapeKeys() + CheckUVMaps() + CheckBadStaicMeshExportedLikeSkeletalMesh() + CheckArmatureModNumber() + CheckArmatureModData() + CheckArmatureBoneData() + CheckArmatureValidChild() + CheckVertexGroupWeight() + CheckZeroScaleKeyframe() + return PotentialErrors + + +def TryToCorrectPotentialError(errorIndex): + #Try to correct potential error + + scene = bpy.context.scene + error = scene.potentialErrorList[errorIndex] + global successCorrect + successCorrect = False + #----------------------------------------Save data + UserActive = bpy.context.active_object #Save current active object + UserMode = None + if UserActive and UserActive.mode != 'OBJECT' and bpy.ops.object.mode_set.poll(): + UserMode = UserActive.mode #Save current mode + bpy.ops.object.mode_set(mode='OBJECT') + UserSelected = bpy.context.selected_objects #Save current selected objects + #---------------------------------------- + print("Start correct") + print("test START: "+error.correctRef) + def SelectObj(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select = True + bpy.context.scene.objects.active = obj + + + #Correction list + + if error.correctRef == "ConvertToMesh": + obj = error.object + SelectObj(obj) + bpy.ops.object.convert(target='MESH') + successCorrect = True + + print("testA: "+error.correctRef) + if error.correctRef == "SetKeyRangeMin": + obj = error.object + key = obj.data.shape_keys.key_blocks[error.itemName] + key.slider_min = -5 + successCorrect = True + + print("testB: "+error.correctRef) + if error.correctRef == "SetKeyRangeMax": + obj = error.object + key = obj.data.shape_keys.key_blocks[error.itemName] + key.slider_max = 5 + successCorrect = True + + if error.correctRef == "CreateUV": + obj = error.object + SelectObj(obj) + bpy.ops.uv.smart_project() + successCorrect = True + + if error.correctRef == "RemoveModfier": + obj = error.object + mod = obj.modifiers[error.itemName] + obj.modifiers.remove(mod) + successCorrect = True + + if error.correctRef == "PreserveVolume": + obj = error.object + mod = obj.modifiers[error.itemName] + mod.use_deform_preserve_volume = False + successCorrect = True + + if error.correctRef == "BoneSegments": + obj = error.object + bone = obj.data.bones[error.itemName] + bone.bbone_segments = 1 + successCorrect = True + + if error.correctRef == "InheritScale": + obj = error.object + bone = obj.data.bones[error.itemName] + bone.use_inherit_scale = True + successCorrect = True + + #----------------------------------------Reset data + bpy.ops.object.select_all(action='DESELECT') + for obj in UserSelected: obj.select = True #Resets previous selected object + scene.objects.active = UserActive #Resets previous active object + if UserActive and UserMode and bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode=UserMode) #Resets previous mode + #---------------------------------------- + + if successCorrect == True: + scene.potentialErrorList.remove(errorIndex) + print("end correct, Error: " + error.correctRef) + scene.update() + return "Corrected" + print("end correct, Error not found") + return "Correct fail" + + #Returns all assets that will be exported + + scene = bpy.context.scene + TargetAassetToExport = [] #Obj, Action, type + + + for obj in GetAllobjectsByExportType("export_recursive"): + if GetAssetType(obj) == "SkeletalMesh": + #SkeletalMesh + if scene.skeletal_export: + TargetAassetToExport.append((obj,None,"SkeletalMesh")) + for action in GetActionToExport(obj): + #Animation + if scene.anin_export: + if GetActionType(action) == "Animation": + TargetAassetToExport.append((obj,action,"Animation")) + #Pose + if scene.pose_export: + if GetActionType(action) == "Pose": + TargetAassetToExport.append((obj,action,"Pose")) + #Camera + if GetAssetType(obj) == "Camera" and scene.camera_export: + TargetAassetToExport.append((obj,None,"Camera")) + + #StaticMesh + if GetAssetType(obj) == "StaticMesh" and scene.static_export: + TargetAassetToExport.append((obj,None,"StaticMesh")) + + return TargetAassetToExport diff --git a/blender-for-unrealengine/bfu_writetext.py b/blender-for-unrealengine/bfu_writetext.py new file mode 100644 index 00000000..e678ea6c --- /dev/null +++ b/blender-for-unrealengine/bfu_writetext.py @@ -0,0 +1,476 @@ +#====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +#======================= END GPL LICENSE BLOCK ============================= + + +import bpy +import time +from .bfu_basics import * +from .bfu_utils import * + + +def ExportSingleText(text, dirpath, filename): + #Export single text + + filename = ValidFilename(filename) + curr_time = time.process_time() + + absdirpath = bpy.path.abspath(dirpath) + VerifiDirs(absdirpath) + fullpath = os.path.join( absdirpath , filename ) + + with open(fullpath, "w") as file: + file.write(text) + + exportTime = time.process_time()-curr_time + return([filename,"TextFile",absdirpath,exportTime]) #[AssetName , AssetType , ExportPath, ExportTime] + + +def WriteExportLog(): + #Write Export log with exported assets in scene.UnrealExportedAssetsList + + def GetNumberByType(targetType): + foundNumber = 0 + scene = bpy.context.scene + for assets in scene.UnrealExportedAssetsList: + if assets.assetType == targetType: + foundNumber = foundNumber + 1 + return foundNumber + + scene = bpy.context.scene + StaticNum = GetNumberByType("StaticMesh") + SkeletalNum = GetNumberByType("SkeletalMesh") + AnimNum = GetNumberByType("Animation") + PoseNum = GetNumberByType("Pose") + CameraNum = GetNumberByType("Camera") + OtherNum = len(scene.UnrealExportedAssetsList)-(StaticNum+SkeletalNum+AnimNum+PoseNum+CameraNum) + AssetNumberByType = str(StaticNum)+" StaticMesh(s) | " + AssetNumberByType += str(SkeletalNum)+" SkeletalMesh(s) | " + AssetNumberByType += str(AnimNum)+" Animation(s) | " + AssetNumberByType += str(PoseNum)+" Pose(s) | " + AssetNumberByType += str(CameraNum)+" Camera(s)" + AssetNumberByType += str(OtherNum)+" Other(s)" + "\n" + + ExportLog = "..." + "\n" + ExportLog += AssetNumberByType + ExportLog += "\n" + for asset in scene.UnrealExportedAssetsList: + ExportLog +=("["+asset.assetType+"] -> "+"\""+asset.assetName+"\" exported in "+str(asset.exportTime)+" sec.\n") + ExportLog +=(asset.exportPath + "\n") + ExportLog += "\n" + + return ExportLog + + +def WriteImportAssetScript(): + #Generate a script for import assets in Ue4 + scene = bpy.context.scene + + #Comment + ImportScript = "#This script was generated with the addons Blender for UnrealEngine : https://github.com/xavier150/Blender-For-UnrealEngine-Addons" + "\n" + ImportScript += "#It will import into Unreal Engine all the assets of type StaticMesh, SkeletalMesh, Animation and Pose" + "\n" + ImportScript += "#The script must be used in Unreal Engine Editor with UnrealEnginePython : https://github.com/20tab/UnrealEnginePython" + "\n" + ImportScript += "\n" + ImportScript += "import os.path" + "\n" + ImportScript += "import unreal_engine as ue" + "\n" + ImportScript += "from unreal_engine.classes import PyFbxFactory" + "\n" + ImportScript += "from unreal_engine.enums import EFBXImportType, EMaterialSearchLocation" + "\n" + ImportScript += "\n" + + #Prepare var and process import + ImportScript += "#Prepare var and process import" + "\n" + ImportScript += "UnrealImportLocation = r'/Game/" + scene.unreal_import_location + "'" + "\n" + ImportScript += "ImportedAsset = []" + "\n" + ImportScript += "\n" + ImportScript += "print('========================= Import started ! =========================')" + "\n" + ImportScript += "\n" + ImportScript += "\n" + ImportScript += "\n" + + #Import asset + for asset in scene.UnrealExportedAssetsList: + if (asset.assetType == "StaticMesh" + or asset.assetType == "SkeletalMesh" + or asset.assetType == "Animation" + or asset.assetType == "Pose"): + + obj = asset.object + MeshRelatifImportLoc = obj.exportFolderName + AnimRelatifImportLoc = os.path.join( obj.exportFolderName, scene.anim_subfolder_name ) + FbxLoc = (os.path.join(asset.exportPath, asset.assetName)) + ImportScript += "################[ Import "+obj.name+" as "+asset.assetType+" type ]################" + "\n" + + if asset.assetType == "StaticMesh": + ImportScript += "fbx_factory = PyFbxFactory()" + "\n" + ImportScript += "fbx_factory.ImportUI.bImportMaterials = True" + "\n" + ImportScript += "fbx_factory.ImportUI.bImportTextures = False" + "\n" + ImportScript += "fbx_factory.ImportUI.TextureImportData.MaterialSearchLocation = EMaterialSearchLocation." + obj.MaterialSearchLocation + "\n" + ImportScript += "fbx_factory.ImportUI.StaticMeshImportData.bCombineMeshes = True" + "\n" + ImportScript += "fbx_factory.ImportUI.StaticMeshImportData.bAutoGenerateCollision = False" + "\n" + + if (obj.UseStaticMeshLODGroup == True): + ImportScript += "fbx_factory.ImportUI.StaticMeshImportData.StaticMeshLODGroup = '" + obj.StaticMeshLODGroup + "'" + "\n" + ImportScript += "fbx_factory.ImportUI.StaticMeshImportData.bGenerateLightmapUVs = " + str(obj.GenerateLightmapUVs) + "\n" + + ImportScript += "FbxLoc = os.path.join(r'"+FbxLoc+"')" + "\n" + ImportScript += r"AssetImportLocation = os.path.join(UnrealImportLocation, r'"+MeshRelatifImportLoc+r"').replace('\\','/')" + "\n" + ImportScript += "AssetImportLocation = AssetImportLocation.rstrip('/')" + "\n" + #-----------Import + ImportScript += "try:" + "\n" + ImportScript += "\t" + "asset = fbx_factory.factory_import_object(FbxLoc, AssetImportLocation)" + "\n" + ImportScript += "\t" + "ImportedAsset.append(asset)" + "\n" + if (obj.UseStaticMeshLODGroup == True): + ImportScript += "\t" "asset.LODGroup = '" + obj.StaticMeshLODGroup + "'" + "\n" + if (obj.UseStaticMeshLightMapRes == True): + ImportScript += "\t" "asset.LightMapResolution = " + str(obj.StaticMeshLightMapRes) + "\n" + ImportScript += "except:" + "\n" + ImportScript += "\t" + "ImportedAsset.append('Import error for asset named \""+obj.name+"\" ')" + "\n" + + if asset.assetType == "SkeletalMesh": + ImportScript += "fbx_factory = PyFbxFactory()" + "\n" + ImportScript += "fbx_factory.ImportUI.bImportMaterials = True" + "\n" + ImportScript += "fbx_factory.ImportUI.bImportTextures = False" + "\n" + ImportScript += "fbx_factory.ImportUI.TextureImportData.MaterialSearchLocation = EMaterialSearchLocation." + obj.MaterialSearchLocation + "\n" + ImportScript += "fbx_factory.ImportUI.bImportAnimations = False" + "\n" + ImportScript += "fbx_factory.ImportUI.SkeletalMeshImportData.bImportMorphTargets = True" + "\n" + ImportScript += "fbx_factory.ImportUI.bCreatePhysicsAsset = " + str(obj.CreatePhysicsAsset) + "\n" + ImportScript += "FbxLoc = os.path.join(r'"+FbxLoc+"')" + "\n" + ImportScript += r"AssetImportLocation = os.path.join(UnrealImportLocation, r'"+MeshRelatifImportLoc+r"').replace('\\','/')" + "\n" + ImportScript += "AssetImportLocation = AssetImportLocation.rstrip('/')" + "\n" + #-----------Import + ImportScript += "try:" + "\n" + ImportScript += "\t" + "asset = fbx_factory.factory_import_object(FbxLoc, AssetImportLocation)" + "\n" + ImportScript += "\t" + "ImportedAsset.append(asset)" + "\n" + ImportScript += "except:" + "\n" + ImportScript += "\t" + "ImportedAsset.append('Import error for asset named \""+obj.name+"\" ')" + "\n" + + if (asset.assetType == "Animation" or asset.assetType == "Pose"): + SkeletonName = scene.skeletal_prefix_export_name+obj.name+"_Skeleton."+scene.skeletal_prefix_export_name+obj.name+"_Skeleton" + SkeletonLoc = os.path.join(obj.exportFolderName,SkeletonName) + ImportScript += "SkeletonLocation = os.path.join(UnrealImportLocation, r'" + SkeletonLoc + r"').replace('\\','/')" + "\n" + ImportScript += "OriginSkeleton = ue.find_asset(SkeletonLocation)" + "\n" + ImportScript += "if OriginSkeleton:" + "\n" + ImportScript += "\t" + "fbx_factory = PyFbxFactory()" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.bImportMesh = False" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.bCreatePhysicsAsset = False" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.Skeleton = OriginSkeleton" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.bImportAnimations = True" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.MeshTypeToImport = EFBXImportType.FBXIT_Animation" + "\n" + ImportScript += "\t" + "fbx_factory.ImportUI.SkeletalMeshImportData.bImportMorphTargets = True" + "\n" + ImportScript += "\t" + "FbxLoc = os.path.join(r'"+FbxLoc+"')" + "\n" + ImportScript += "\t" + r"AssetImportLocation = os.path.join(UnrealImportLocation, r'"+AnimRelatifImportLoc+r"').replace('\\','/')" + "\n" + ImportScript += "\t" + "AssetImportLocation = AssetImportLocation.rstrip('/')" + "\n" + #-----------Import + ImportScript += "\t" + "try:" + "\n" + ImportScript += "\t\t" + "asset = fbx_factory.factory_import_object(FbxLoc, AssetImportLocation)" + "\n" + ImportScript += "\t\t" + "ImportedAsset.append(asset)" + "\n" + ImportScript += "\t" + "except:" + "\n" + ImportScript += "\t\t" + "ImportedAsset.append('Import error for asset named \""+obj.name+"\" ')" + "\n" + ImportScript += "else:" + "\n" + ImportScript += "\t" + "ImportedAsset.append('Skeleton \"'+SkeletonLocation+'\" Not found for \""+obj.name+"\" asset ')" + "\n" + + ImportScript += "\n" + ImportScript += "\n" + ImportScript += "\n" + + #import result + ImportScript += "print('========================= Imports completed ! =========================')" + "\n" + ImportScript += "\n" + ImportScript += "for asset in ImportedAsset:" + "\n" + ImportScript += "\t" + "print(asset)" + "\n" + ImportScript += "\n" + ImportScript += "print('=========================')" + "\n" + + return ImportScript + + +def WriteImportSequencerScript(): + #Generate a script for import camera in Ue4 sequencer + scene = bpy.context.scene + + #Comment + ImportScript = "#This script was generated with the addons Blender for UnrealEngine : https://github.com/xavier150/Blender-For-UnrealEngine-Addons" + "\n" + ImportScript += "#This script will import in unreal all camera in target sequencer" + "\n" + ImportScript += "#The script must be used in Unreal Engine Editor with UnrealEnginePython : https://github.com/20tab/UnrealEnginePython" + "\n" + ImportScript += "\n" + ImportScript += "import os.path" + "\n" + ImportScript += "import configparser" + "\n" + ImportScript += "import unreal_engine as ue" + "\n" + ImportScript += "from unreal_engine.classes import MovieSceneCameraCutTrack, MovieScene3DTransformSection, MovieScene3DTransformTrack, MovieSceneAudioTrack, CineCameraActor" + "\n" + ImportScript += "from unreal_engine.structs import FloatRange, FloatRangeBound, MovieSceneObjectBindingID, FrameRate" + "\n" + ImportScript += "from unreal_engine import FTransform, FVector, FColor" + "\n" + ImportScript += "from unreal_engine.enums import EMovieSceneObjectBindingSpace" + "\n" + ImportScript += "\n" + + #Prepare def + ImportScript += "def AddSequencerSectionKeysByIniFile(SequencerSection, SectionFileName, FileLoc):" + "\n" + ImportScript += "\t" + "Config = configparser.ConfigParser()" + "\n" + ImportScript += "\t" + "Config.read(FileLoc)" + "\n" + ImportScript += "\t" + "for option in Config.options(SectionFileName):" + "\n" + ImportScript += "\t\t" + "frame = float(option)/"+str(scene.render.fps)+" #FrameRate" + "\n" + ImportScript += "\t\t" + "value = float(Config.get(SectionFileName, option))" + "\n" + ImportScript += "\t\t" + "SequencerSection.sequencer_section_add_key(frame,value)" + "\n" + + #Prepare var and process import + + + + ImportScript += 'seq = ue.find_asset("'+scene.unreal_levelsequence_reference+'")' + "\n" + ImportScript += 'if seq:' + "\n" + ImportScript += "\t" + 'print("Sequencer reference found")' + "\n" + ImportScript += "\t" + "ImportedCamera = [] #(CameraName, CameraGuid)" + "\n" + ImportScript += "\t" + 'print("========================= Import started ! =========================")' + "\n" + ImportScript += "\t" + "\n" + #Set frame rate + ImportScript += "\t" + "#Set frame rate" + "\n" + ImportScript += "\t" + "myFFrameRate = FrameRate()" + "\n" + ImportScript += "\t" + "myFFrameRate.Denominator = 1" + "\n" + ImportScript += "\t" + "myFFrameRate.Numerator = " + str(scene.render.fps) + "\n" + ImportScript += "\t" + "seq.MovieScene.DisplayRate = myFFrameRate" + "\n" + ImportScript += "\t" + "\n" + #Set playback range + StartPlayback = str(scene.frame_start/scene.render.fps) + EndPlayback = str(scene.frame_end/scene.render.fps) + ImportScript += "\t" + "#Set playback range" + "\n" + ImportScript += "\t" + "seq.sequencer_set_playback_range("+StartPlayback+", "+EndPlayback+")" + "\n" + ImportScript += "\t" + "camera_cut_track = seq.sequencer_add_camera_cut_track()" + "\n" + ImportScript += "\t" + "world = ue.get_editor_world()" + "\n" + + ImportScript += "else:" + "\n" + ImportScript += "\t" + 'print("Sequencer reference not valid !")' + "\n" + ImportScript += "\n" + ImportScript += "\n" + + #Import camera + for asset in scene.UnrealExportedAssetsList: + if (asset.assetType == "Camera"): + camera = asset.object + ImportScript += "#import " + camera.name + "\n" + ImportScript += "if seq:" + "\n" + ImportScript += "\t" + 'print("Start import ' + camera.name + '")' + "\n" + ImportScript += "\t" + "\n" + + #Create spawnable camera + ImportScript += "\t" + "#Create spawnable camera" + "\n" + ImportScript += "\t" + "cine_camera_actor = world.actor_spawn(CineCameraActor) #Create camera" + "\n" + ImportScript += "\t" + "cine_camera_actor.set_actor_label('" + camera.name + "')" + "\n" + ImportScript += "\t" + "cine_camera_actor.CameraComponent.LensSettings.MinFStop = 0" + "\n" + ImportScript += "\t" + "cine_camera_actor.CameraComponent.LensSettings.MaxFStop = 1000" + "\n" + ImportScript += "\t" + "camera_spawnable_guid = seq.sequencer_make_new_spawnable(cine_camera_actor) #Add camera in sequencer" + "\n" + ImportScript += "\t" + "cine_camera_actor.actor_destroy()" + "\n" + ImportScript += "\t" + "ImportedCamera.append(('"+camera.name+"', camera_spawnable_guid))" + + ImportScript += "\n" + + #Import fbx transform + ImportScript += "\t" + "#Import fbx transform" + "\n" + FbxLoc = (os.path.join(asset.exportPath, GetObjExportFileName(camera))) + ImportScript += "\t" + "FbxLoc = os.path.join(r'"+FbxLoc+"')" + "\n" + ImportScript += "\t" + "for obj in seq.MovieScene.ObjectBindings:" + "\n" + ImportScript += "\t\t" + "if obj.ObjectGuid == ue.string_to_guid(camera_spawnable_guid):" + "\n" + ImportScript += "\t\t\t" + "transform_track = obj.tracks[0]" + "\n" + ImportScript += "\t\t\t" + "camera_section = transform_track.Sections[0]" + "\n" + ImportScript += "\t\t\t" + "camera_section.sequencer_import_fbx_transform(FbxLoc, '" + camera.name + "')" + "\n" + ImportScript += "\n" + + #Import additional tracks + ImportScript += "\t" + "#Import additional tracks" + "\n" + TracksLoc = (os.path.join(asset.exportPath, GetCameraTrackFileName(camera))) + ImportScript += "\t" + "TracksLoc = os.path.join(r'"+TracksLoc+"')" + "\n" + ImportScript += "\t" + "Component = seq.MovieScene.ObjectBindings[-1]" + "\n" + ImportScript += "\t" + "sectionFocalLength = Component.Tracks[0].Sections[0]" + "\n" + ImportScript += "\t" + "AddSequencerSectionKeysByIniFile(sectionFocalLength, 'FocalLength', TracksLoc)" + "\n" + ImportScript += "\t" + "sectionFocusDistance = Component.Tracks[1].Sections[0]" + "\n" + ImportScript += "\t" + "AddSequencerSectionKeysByIniFile(sectionFocusDistance, 'FocusDistance', TracksLoc)" + "\n" + ImportScript += "\t" + "sectionAperture = Component.Tracks[2].Sections[0]" + "\n" + ImportScript += "\t" + "AddSequencerSectionKeysByIniFile(sectionAperture, 'Aperture', TracksLoc)" + "\n" + ImportScript += "\n" + ImportScript += "\n\n" + + def getMarkerSceneSections(): + scene = bpy.context.scene + markersOrderly = [] + firstMarkersFrame = scene.frame_start + lastMarkersFrame = scene.frame_end+1 + + + for marker in scene.timeline_markers: + #Re set first frame + if marker.frame < firstMarkersFrame: + firstMarkersFrame = marker.frame + + for x in range(firstMarkersFrame, lastMarkersFrame): + for marker in scene.timeline_markers: + if marker.frame == x: + markersOrderly.append(marker) + #--- + sectionCuts = [] + for x in range(len(markersOrderly)): + if scene.frame_end > markersOrderly[x].frame: + startTime = markersOrderly[x].frame/scene.render.fps + if x+1 != len(markersOrderly): + EndTime = markersOrderly[x+1].frame/scene.render.fps + else: + EndTime = scene.frame_end/scene.render.fps + sectionCuts.append([startTime, EndTime, markersOrderly[x]]) + + return sectionCuts + + for section in getMarkerSceneSections(): + #Camera cut sections + ImportScript += "#Import camera cut section" + "\n" + ImportScript += "if seq:" + "\n" + ImportScript += "\t" + "camera_cut_section = camera_cut_track.sequencer_track_add_section()" + "\n" + if section[2].camera is not None: + if section[2].camera.ExportEnum == "export_recursive" or section[2].camera.ExportEnum == "auto": + ImportScript += "\t" + "for camera in ImportedCamera:" + "\n" + ImportScript += "\t\t" + "if camera[0] == '"+section[2].camera.name+"':" + "\n" + ImportScript += "\t\t\t" + "camera_cut_section.CameraBindingID = MovieSceneObjectBindingID( Guid=ue.string_to_guid( camera[1] ), Space=EMovieSceneObjectBindingSpace.Local )" + "\n" + else: + ImportScript += "\t" + "#Not camera found for this section" + "\n" + else: + ImportScript += "\t" + "#Not camera found for this section" + "\n" + ImportScript += "\t" + "camera_cut_section.sequencer_set_section_range("+str(section[0])+", "+str(section[1])+")" + "\n" + + #import result + + ImportScript += "if seq:" + "\n" + ImportScript += "\t" + "print('========================= Imports completed ! =========================')" + "\n" + ImportScript += "\t" + "\n" + ImportScript += "\t" + "for cam in ImportedCamera:" + "\n" + ImportScript += "\t\t" + "print(cam[0])" + "\n" + ImportScript += "\t" + "\n" + ImportScript += "\t" + "print('=========================')" + "\n" + ImportScript += "\t" + "seq.sequencer_changed(True)" + "\n" + + return ImportScript + + +def WriteSingleCameraAdditionalTrack(obj): + + def getCameraFocusDistance(Camera, Target): + transA = Camera.matrix_world.copy() + transB = Target.matrix_world.copy() + transA.invert() + distance = (transA*transB).translation.z #Z is the Fosrward + if distance < 0: + distance *= -1 + return distance + + def getAllCamDistKeys(Camera, Target): + scene = bpy.context.scene + saveFrame = scene.frame_current #Save current frame + keys = [] + for frame in range(scene.frame_start, scene.frame_end+1): + scene.frame_set(frame) + v = getCameraFocusDistance(Camera, Target) + keys.append((frame,v)) + scene.frame_set(saveFrame) #Resets previous start frame + return keys + + + def getOneDataKeysByPath(obj,DataPath, DataValue, Frame): + scene = bpy.context.scene + if obj.data.animation_data is not None: + f = obj.data.animation_data.action.fcurves.find(DataPath) + if f is not None: + return f.evaluate(Frame) + return DataValue + + def getAllDataKeysByPath(obj,DataPath, DataValue): + scene = bpy.context.scene + keys = [] + if obj.data.animation_data is not None: + if obj.data.animation_data.action is not None: + f = obj.data.animation_data.action.fcurves.find(DataPath) + if f is not None: + for frame in range(scene.frame_start, scene.frame_end+1): + v = f.evaluate(frame) + keys.append((frame,v)) + return keys + return[(scene.frame_start,DataValue)] + + scene = bpy.context.scene + ImportScript = ";This file was generated with the addons Blender for UnrealEngine : https://github.com/xavier150/Blender-For-UnrealEngine-Addons" + "\n" + ImportScript += ";This file contains animation informations that is not supported with fbx files" + "\n" + ImportScript += ";The script must be used in Unreal Engine Editor with UnrealEnginePython : https://github.com/20tab/UnrealEnginePython" + "\n" + ImportScript += "\n\n\n" + + #Get FocalLength keys + ImportScript += "[FocalLength]" + "\n" + for key in getAllDataKeysByPath(obj,"lens",obj.data.lens): + #Fov type return auto to lens + ImportScript += str(key[0])+": "+str(key[1]) + "\n" + ImportScript += "\n\n\n" + + #Get FocusDistance keys + ImportScript += "[FocusDistance]" + "\n" + if obj.data.dof_object is None: + DataKeys = getAllDataKeysByPath(obj,"dof_distance",obj.data.dof_distance) + else: + DataKeys = getAllCamDistKeys(obj, obj.data.dof_object) + for key in DataKeys: + CorrectedValue = key[1]*100 + if CorrectedValue > 0: + ImportScript += str(key[0])+": "+str(CorrectedValue) + "\n" + else: + ImportScript += str(key[0])+": "+str(100000) + "\n" #100000 is default value in ue4 + ImportScript += "\n\n\n" + + #Get Aperture (Depth of Field) keys + ImportScript += "[Aperture]" + "\n" + if scene.render.engine == 'CYCLES': #Work only with cycle. + if obj.data.cycles.aperture_type == 'FSTOP': + DataKeys = getAllDataKeysByPath(obj,"cycles.aperture_fstop",obj.data.cycles.aperture_fstop) + else: + DataKeys = getAllDataKeysByPath(obj,"cycles.aperture_size",obj.data.cycles.aperture_size) + for key in DataKeys: + CorrectedValue = key[1] + if obj.data.cycles.aperture_type == 'RADIUS': + #Convert radius to Fstop + FocalLength = getOneDataKeysByPath(obj,"lens",obj.data.lens,key[0]) + if CorrectedValue == 0: + CorrectedValue = 64 + else: + CorrectedValue = (FocalLength/(key[1]*2000)) + ImportScript += str(key[0])+": "+str(CorrectedValue) + "\n" + else: + ImportScript += "0: 21\n" #21 is default value in ue4 + ImportScript += "\n\n\n" + return ImportScript + +def WriteAllTextFiles(): + + scene = bpy.context.scene + if scene.text_exportLog: + exportLog = WriteExportLog() + if exportLog is not None: + exportLogFilename = "ExportLog.txt" + ExportSingleText(exportLog, scene.export_other_file_path, exportLogFilename) + + if scene.text_ImportAssetScript: + ImportAssetScript = WriteImportAssetScript() + if ImportAssetScript is not None: + ImportAssetScriptFilename = "ImportAssetScript.py" + ExportSingleText(ImportAssetScript, scene.export_other_file_path, ImportAssetScriptFilename) + + if scene.text_ImportSequenceScript: + ImportSequencerScript = WriteImportSequencerScript() + if ImportSequencerScript is not None: + ImportSequencerScriptFilename = "ImportSequencerScript.py" + ExportSingleText(ImportSequencerScript, scene.export_other_file_path, ImportSequencerScriptFilename) +