├── .gitignore ├── FUNDING.yml ├── LICENSE ├── README.md ├── UE4Workspace ├── __init__.py ├── animation │ ├── __init__.py │ └── main.py ├── copy_to_unreal_engine │ ├── __init__.py │ ├── main.py │ └── utils.py ├── credit │ ├── __init__.py │ └── main.py ├── export_option │ ├── __init__.py │ └── main.py ├── groom │ ├── __init__.py │ └── main.py ├── import_asset │ ├── __init__.py │ └── main.py ├── object │ ├── __init__.py │ ├── custom_collision.py │ ├── level_of_detail.py │ ├── main.py │ ├── skeletal_mesh_part.py │ └── socket.py ├── preferences │ ├── __init__.py │ ├── animation.py │ ├── connect_unreal_engine.py │ ├── export_option.py │ ├── groom.py │ ├── import_asset.py │ ├── main.py │ ├── misc.py │ ├── skeletal_mesh.py │ └── static_mesh.py ├── skeletal_mesh │ ├── __init__.py │ └── main.py ├── static_mesh │ ├── __init__.py │ └── main.py ├── temp │ ├── import_asset_list.json │ ├── import_asset_setting.json │ ├── imported_asset_list.json │ ├── skeleton_list.json │ └── unreal_engine_import_setting.json ├── ue4_script │ ├── ExportAsset.py │ ├── GetAllImportableAsset.py │ ├── GetAllSkeleton.py │ ├── ImportAnimation.py │ ├── ImportSkeletalMesh.py │ └── ImportStaticMesh.py └── utils │ ├── __init__.py │ ├── base.py │ ├── connect.py │ ├── operator.py │ └── remote_execution.py └── build-zip.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | UE4Workspace.zip -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | # I make this addon free, You can donate if you want 2 | # any donation will be appreciated 3 | 4 | custom: ['https://gumroad.com/anasrar', 'https://gumroad.com/l/BlenderUnrealEngineWorkspace'] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender Unreal Engine Workspace 2 | 3 | ![Blender Unreal Engine Workspace](https://anasrar.github.io/Blender-UE4-Workspace/img/blender-unreal-engine-4-workspace-banner.png) 4 | 5 | Blender **2.91** *(above)* add-on for export directly to Unreal Engine 4 (**4.26** *above*) with all setting in Blender (inspired by **send to unreal** add-on). 6 | 7 | ## Feature 8 | 9 | Allow you export static mesh, skeletal mesh, and animation with single click directly to Unreal Engine 4 or to FBX file. 10 | 11 | ![Feature Node](https://anasrar.github.io/Blender-UE4-Workspace/img/feature-node.png "Feature Node") 12 | 13 | and yeah, I don't really have any future plan. so if you have any suggest just open new issue. 14 | 15 | ## Main Feature 16 | 17 | ### Static Mesh 18 | 19 | Export for Static Mesh. 20 | 21 | - Export to FBX and Unreal Engine 22 | - Custom collision from vertices 23 | - Custom collision from mesh - ```v.1.2``` 24 | - Custom lightmaps 25 | - **[ DEPRECATED - v.2.0 ]** Export profile - ```v.1.2``` 26 | - Socket System - ```v.1.3``` 27 | - Level of Detail - ```v.1.4``` 28 | - Generate Level of Detail - ```v.2.0``` 29 | - Import Static Mesh From Unreal Engine - ```v.1.4``` 30 | 31 | ### Skeletal Mesh 32 | 33 | Export for Skeletal Mesh. 34 | 35 | - Export to FBX and Unreal Engine 36 | - Modular character 37 | - **[ DEPRECATED - v.2.0 ]** Skeleton preset (Epic skeleton) 38 | - **[ DEPRECATED - v.2.0 ]** Add twist bone for skeleton preset - ```v.1.2``` 39 | - **[ DEPRECATED - v.2.0 ]** Generate rig for skeleton preset - ```v.1.2``` 40 | - **Move To Another Add-on** : TBA 41 | - **[ DEPRECATED - v.2.0 ]** Export profile - ```v.1.2``` 42 | - Socket System - ```v.1.3``` 43 | - Copy/Paste Socket Unreal Engine - ```v.2.0``` 44 | - Skeletal Mesh Part Manager - ```v.1.4``` 45 | - Import Skeletal Mesh From Unreal Engine ```v.1.4``` 46 | 47 | ### Animation 48 | 49 | Export for Animation. 50 | 51 | - Export to FBX and Unreal Engine - ```v.1.2``` 52 | - **[ DEPRECATED - v.2.0 ]** Export profile - ```v.1.2``` 53 | - Import Animation From Unreal Engine ```v.1.4``` 54 | 55 | ### Retarget Animation 56 | 57 | **[ DEPRECATED - v.2.0 ]** Retarget Animation to another skeleton - ```v.1.3``` **Experimental**, ```v.1.4``` **Production Ready** 58 | 59 | **Move To Another Add-on** : https://github.com/anasrar/ReNim 60 | 61 | ### Groom Hair [Experimental] 62 | Export Hair Particle From Blender and Import as Groom Hair In Unreal Engine (Not Support Direct Export To Unreal) - ```v.2.0``` 63 | 64 | Export Setting : [[Groom] unreal engine import setting](https://github.com/anasrar/Blender-UE4-Workspace/issues/22) 65 | 66 | ### Copy Transform To Unreal Engine Map 67 | 68 | Copy Transform Selected Object To Unreal Engine Map - ```v.2.0``` 69 | 70 | #### Support 71 | 72 | - Static Mesh 73 | 74 | ## Documentation 75 | 76 | [documentation page](https://anasrar.github.io/Blender-UE4-Workspace/) or [YouTube playlist](https://www.youtube.com/playlist?list=PLolnhUV-ZzXrXx1gJunoknuni8klsy0wH) 77 | 78 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/anasrar/Blender-UE4-Workspace?style=flat-square) 79 | 80 | ## How it works 81 | 82 | Unreal Engine 4 allow to remote execute python script, with that we can execute python script import assets (FBX File) to Unreal Engine 4. 83 | 84 | ![Blender Unreal Engine Workspace FlowChart](https://anasrar.github.io/Blender-UE4-Workspace/img/flowchart.png "Flowchart") 85 | 86 | ## Download 87 | 88 | You can download from 89 | 90 | - Gumroad for latest version 91 | - https://gumroad.com/l/BlenderUnrealEngineWorkspace 92 | 93 | - GitHub for pervious version 94 | - https://github.com/anasrar/Blender-UE4-Workspace/releases 95 | 96 | master branch is **unstable** and **bug fix version** 97 | 98 | ## Installation 99 | 100 | You can watch this video https://www.youtube.com/watch?v=38d5Myrh3ic or simply follow this instruction below. 101 | 102 | ### Blender 103 | 104 | Edit   🡆   Preferences   🡆   Add-ons   🡆   Install   🡆   Select **UE4Workspace.zip**   🡆   Install Add-ons 105 | 106 | ### Unreal Engine 4 107 | 108 | Edit   🡆   Plugins   🡆   Type "Script" On Search Bar   🡆   Enabled **Python Editor Script Plugin** and **Editor Scripting Utilities**   🡆   Reset Project 109 | 110 | Edit   🡆   Project Setting   🡆   Plugin   🡆   Python   🡆   Check **Enable Remote Execution**? 111 | 112 | Then you can try to connect your project from blender 113 | 114 | ## Usage 115 | 116 | Press **N** on Blender for open the tab menu. 117 | 118 | ## Compatibility Test 119 | 120 | * **Blender 2.91** (make sure export folder path is absolute) 121 | 122 | **Unreal Engine 4.26** 123 | 124 | using Blender latest version for better compatibility. 125 | 126 | ## Support 127 | 128 | You can support me through Gumroad 129 | 130 | any donation will be appreciated. 131 | 132 | ## Contributing 133 | 134 | For major changes or features request, please open an issue first to discuss what you would like to change or add. 135 | 136 | ## Changelog 137 | 138 | Any changelog in [documentation page](https://anasrar.github.io/Blender-UE4-Workspace/changelog/) 139 | 140 | ## License 141 | 142 | This project is licensed under the **GPL-3.0** License - see the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /UE4Workspace/__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, but 7 | # WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 | # General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | bl_info = { 15 | "name" : "UE4Workspace", 16 | "author" : "Anas Rin", 17 | "description" : "Addon For UE4 Workspace", 18 | "blender" : (2, 91, 0), 19 | "version" : (2, 0, 0), 20 | "location" : "3D View > Tools", 21 | "warning" : "", 22 | "wiki_url": "https://github.com/anasrar/Blender-UE4-Workspace", # 2.82 below 23 | "doc_url": "https://github.com/anasrar/Blender-UE4-Workspace", # 2.83 above 24 | "tracker_url": "https://github.com/anasrar/Blender-UE4-Workspace/issues", 25 | "support": "COMMUNITY", 26 | "category" : "Workspace" 27 | } 28 | 29 | import bpy 30 | 31 | from . utils import operator 32 | from . preferences import main as preferences_main 33 | from . export_option import main as export_option_main 34 | from . import_asset import main as import_asset_main 35 | from . object import main as object_main 36 | from . static_mesh import main as static_mesh_main 37 | from . skeletal_mesh import main as skeletal_mesh_main 38 | from . animation import main as animation_main 39 | from . groom import main as groom_main 40 | from . credit import main as credit_main 41 | from . copy_to_unreal_engine import main as copy_to_unreal_engine_main 42 | 43 | list_class_to_register = [ 44 | operator, 45 | preferences_main, 46 | export_option_main, 47 | import_asset_main, 48 | object_main, 49 | static_mesh_main, 50 | skeletal_mesh_main, 51 | animation_main, 52 | groom_main, 53 | credit_main, 54 | copy_to_unreal_engine_main 55 | ] 56 | 57 | def register(): 58 | for x in list_class_to_register: 59 | x.register() 60 | 61 | def unregister(): 62 | for x in list_class_to_register[::-1]: 63 | x.unregister() -------------------------------------------------------------------------------- /UE4Workspace/animation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/animation/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/animation/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import Operator, UIList 4 | from .. utils.base import ExportOperator, Panel, ExportOptionPanel 5 | 6 | class ANIMATION_UL_action_list(UIList): 7 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 8 | layout.prop(item, 'name', text='', icon='ACTION', emboss=False) 9 | layout.prop(item, 'is_export', text='') 10 | 11 | class OP_SelectExportAction(Operator): 12 | bl_idname = 'ue4workspace.select_export_action' 13 | bl_label = 'Select Action To Export' 14 | bl_options = {'UNDO'} 15 | 16 | type: bpy.props.StringProperty(default='') 17 | 18 | def execute(self, context): 19 | if self.type: 20 | for action in bpy.data.actions: 21 | action.is_export = {'SELECT': True, 'DESELECT': False, 'INVERT': not action.is_export}[self.type] 22 | return {'FINISHED'} 23 | 24 | class OP_AddMaterialCurveSuffix(Operator): 25 | bl_idname = 'ue4workspace.add_material_curve_suffix' 26 | bl_label = 'Add Suffix' 27 | bl_description = 'Add Suffix on Animation Export Setting' 28 | bl_options = {'UNDO'} 29 | 30 | val: bpy.props.StringProperty() 31 | 32 | def execute(self, context): 33 | preferences = context.preferences.addons['UE4Workspace'].preferences 34 | unreal_engine_setting = preferences.animation.unreal_engine 35 | 36 | material_curve_suffixes = unreal_engine_setting.material_curve_suffixes.split('|') if bool(unreal_engine_setting.material_curve_suffixes) else [] 37 | material_curve_suffixes.append(self.val.replace('|', '')) 38 | 39 | unreal_engine_setting.material_curve_suffixes = '|'.join(material_curve_suffixes) 40 | 41 | return {'FINISHED'} 42 | 43 | def invoke(self, context, event): 44 | return context.window_manager.invoke_props_dialog(self, width = 250) 45 | 46 | def draw(self, context): 47 | col = self.layout.column() 48 | row = col.row() 49 | split = row.split(factor=0.5) 50 | col = split.column() 51 | col.alignment = 'RIGHT' 52 | col.label(text='Value :') 53 | col = split.column() 54 | col.prop(self, 'val', text='') 55 | 56 | class OP_ClearMaterialCurveSuffixes(Operator): 57 | bl_idname = 'ue4workspace.clear_material_curve_suffix' 58 | bl_label = 'Clear Suffixes' 59 | bl_description = 'Clear Suffixes on Animation Export Setting' 60 | bl_options = {'UNDO'} 61 | 62 | def execute(self, context): 63 | preferences = context.preferences.addons['UE4Workspace'].preferences 64 | unreal_engine_setting = preferences.animation.unreal_engine 65 | 66 | unreal_engine_setting.material_curve_suffixes = '' 67 | return {'FINISHED'} 68 | 69 | class OP_RemoveMaterialCurveSuffixIndex(Operator): 70 | bl_idname = 'ue4workspace.remove_material_curve_suffix_index' 71 | bl_label = 'Remove Suffix' 72 | bl_description = 'Remove Suffix Base on Index Animation Export Setting' 73 | bl_options = {'UNDO'} 74 | 75 | index: bpy.props.IntProperty() 76 | 77 | def execute(self, context): 78 | preferences = context.preferences.addons['UE4Workspace'].preferences 79 | unreal_engine_setting = preferences.animation.unreal_engine 80 | 81 | material_curve_suffixes = unreal_engine_setting.material_curve_suffixes.split('|') if bool(unreal_engine_setting.material_curve_suffixes) else [] 82 | material_curve_suffixes.pop(self.index) 83 | 84 | unreal_engine_setting.material_curve_suffixes = '|'.join(material_curve_suffixes) 85 | return {'FINISHED'} 86 | 87 | class OP_EditMaterialCurveSuffix(Operator): 88 | bl_idname = 'ue4workspace.edit_material_curve_suffix' 89 | bl_label = 'Edit Value Suffix' 90 | bl_description = 'Edit Value Suffix on Animation Export Setting' 91 | bl_options = {'UNDO'} 92 | 93 | val: bpy.props.StringProperty() 94 | index: bpy.props.IntProperty() 95 | 96 | def execute(self, context): 97 | preferences = context.preferences.addons['UE4Workspace'].preferences 98 | unreal_engine_setting = preferences.animation.unreal_engine 99 | 100 | material_curve_suffixes = unreal_engine_setting.material_curve_suffixes.split('|') if bool(unreal_engine_setting.material_curve_suffixes) else [] 101 | material_curve_suffixes[self.index] = self.val.replace('|', '') 102 | 103 | unreal_engine_setting.material_curve_suffixes = '|'.join(material_curve_suffixes) 104 | return {'FINISHED'} 105 | 106 | def invoke(self, context, event): 107 | return context.window_manager.invoke_props_dialog(self, width = 250) 108 | 109 | def draw(self, context): 110 | col = self.layout.column() 111 | row = col.row() 112 | split = row.split(factor=0.5) 113 | col = split.column() 114 | col.alignment = 'RIGHT' 115 | col.label(text='Value :') 116 | split = split.split() 117 | col = split.column() 118 | col.prop(self, 'val', text='') 119 | 120 | class OP_ExportAnimationMesh(ExportOperator): 121 | bl_idname = 'ue4workspace.export_animation_mesh' 122 | bl_label = 'Export Animation' 123 | bl_description = 'Export Action To Animation' 124 | 125 | ext_file = 'fbx' 126 | 127 | @classmethod 128 | def poll(cls, context): 129 | preferences = context.preferences.addons['UE4Workspace'].preferences 130 | 131 | if context.mode == 'OBJECT' and context.active_object is not None and context.active_object.type == 'ARMATURE': 132 | if preferences.export.type == 'UNREAL' and preferences.animation.skeleton != 'NONE': 133 | return bool(preferences.export.temp_folder.strip()) 134 | elif preferences.export.type == 'BOTH' and preferences.animation.skeleton != 'NONE': 135 | return bool(preferences.export.export_folder.strip()) 136 | elif preferences.export.type == 'FILE': 137 | return bool(preferences.export.export_folder.strip()) 138 | return False 139 | 140 | def execute(self, context): 141 | preferences = context.preferences.addons['UE4Workspace'].preferences 142 | animation = preferences.animation 143 | fbx_setting = animation.fbx 144 | unreal_engine_setting = animation.unreal_engine 145 | 146 | active_object = context.active_object 147 | selected_objects = context.selected_objects 148 | 149 | directory = self.create_string_directory(preferences.export.export_folder if preferences.export.type in ['FILE','BOTH'] else preferences.export.temp_folder, animation.subfolder) 150 | 151 | self.create_directory_if_not_exist(directory, animation.subfolder) 152 | 153 | list_name_animations = [] 154 | unreal_engine_import_setting = { 155 | 'files': [], 156 | 'main_folder': self.safe_string_path(preferences.connect_unreal_engine.main_folder), 157 | 'subfolder': self.safe_string_path(animation.subfolder), 158 | 'overwrite_file': animation.overwrite_file, 159 | 'temporary': preferences.export.type == 'UNREAL' 160 | } 161 | 162 | unreal_engine_import_setting.update(unreal_engine_setting.to_dict()) 163 | 164 | bpy.ops.object.select_all(action='DESELECT') 165 | 166 | self.mute_attach_constraint(active_object) 167 | 168 | active_object.select_set(True) 169 | 170 | original_action = active_object.animation_data.action 171 | original_frame_start = context.scene.frame_start 172 | original_frame_end = context.scene.frame_end 173 | 174 | original_object_name = active_object.name 175 | is_armature_has_root_bone = active_object.data.bones.get('root', False) 176 | 177 | if animation.root_bone == 'ARMATURE': 178 | active_object.name = 'Armature' 179 | elif animation.root_bone == 'AUTO': 180 | if is_armature_has_root_bone: 181 | active_object.name = 'Armature' 182 | else: 183 | active_object.name = 'root' 184 | elif animation.root_bone == 'OBJECT': 185 | pass 186 | 187 | export_actions = [action for action in bpy.data.actions if action.is_export] 188 | 189 | for export_action in export_actions: 190 | filename = self.safe_string_path(export_action.name) 191 | check_duplicate = len([animation_name for animation_name in list_name_animations if animation_name.startswith(filename)]) 192 | list_name_animations.append(filename) 193 | 194 | filename += '_' + str(check_duplicate) if bool(check_duplicate) else '' 195 | filename_ext = filename + '.' + self.ext_file 196 | 197 | if not self.is_file_exist(directory, filename_ext) or animation.overwrite_file: 198 | 199 | original_location = active_object.matrix_world.to_translation() 200 | if animation.origin == 'OBJECT': 201 | active_object.matrix_world.translation = (0, 0, 0) 202 | 203 | original_rotation = active_object.rotation_quaternion.copy() if active_object.rotation_mode == 'QUATERNION' else active_object.rotation_euler.copy() 204 | if not animation.apply_rotation: 205 | if active_object.rotation_mode == 'QUATERNION': 206 | active_object.rotation_quaternion = (1, 0, 0, 0) 207 | else: 208 | active_object.rotation_euler = (0, 0, 0) 209 | 210 | active_object.animation_data.action = export_action 211 | 212 | if fbx_setting.bake_anim_force_startend_keying: 213 | context.scene.frame_start, context.scene.frame_end = export_action.frame_range 214 | 215 | export_setting = { 216 | 'filepath': self.create_string_directory(directory, filename_ext), 217 | 'check_existing': False, 218 | 'filter_glob': '*.fbx', 219 | 'use_selection': True, 220 | 'use_active_collection': False, 221 | 'object_types': {'ARMATURE'}, 222 | 'use_custom_props': animation.use_custom_props, 223 | 'bake_anim': True, 224 | 'bake_anim_use_nla_strips': False, 225 | 'bake_anim_use_all_actions': False, 226 | 'path_mode': 'AUTO', 227 | 'embed_textures': False, 228 | 'batch_mode': 'OFF' 229 | } 230 | 231 | export_setting.update(fbx_setting.to_dict()) 232 | 233 | # EXPORT 234 | bpy.ops.export_scene.fbx(**export_setting) 235 | 236 | unreal_engine_import_setting['files'].append({ 237 | 'path': export_setting['filepath'], 238 | 'skeleton': animation.skeleton 239 | }) 240 | 241 | if animation.origin == 'OBJECT': 242 | active_object.matrix_world.translation = original_location 243 | 244 | if not animation.apply_rotation: 245 | if active_object.rotation_mode == 'QUATERNION': 246 | active_object.rotation_quaternion = original_rotation 247 | else: 248 | active_object.rotation_euler = original_rotation 249 | 250 | self.unmute_attach_constraint(active_object) 251 | 252 | active_object.select_set(False) 253 | 254 | active_object.animation_data.action = original_action 255 | context.scene.frame_start = original_frame_start 256 | context.scene.frame_end = original_frame_end 257 | 258 | if animation.root_bone == 'ARMATURE': 259 | active_object.name = original_object_name 260 | elif animation.root_bone == 'AUTO': 261 | active_object.name = original_object_name 262 | elif animation.root_bone == 'OBJECT': 263 | pass 264 | 265 | for obj in selected_objects: 266 | obj.select_set(state=True) 267 | 268 | self.unreal_engine_exec_script('ImportAnimation.py', unreal_engine_import_setting) 269 | 270 | self.report({'INFO'}, 'export ' + str(len(unreal_engine_import_setting['files'])) + ' animation success') 271 | 272 | return {'FINISHED'} 273 | 274 | class PANEL(Panel): 275 | bl_idname = 'UE4WORKSPACE_PT_AnimationPanel' 276 | bl_label = 'Animation' 277 | 278 | def draw(self, context): 279 | layout = self.layout 280 | preferences = context.preferences.addons['UE4Workspace'].preferences 281 | animation = preferences.animation 282 | 283 | col_data = [ 284 | ('Subfolder', 'subfolder'), 285 | ('Overwrite File', 'overwrite_file'), 286 | ('Custom Properties', 'use_custom_props'), 287 | ('Apply Rotation', 'apply_rotation'), 288 | ('Root Bone', 'root_bone'), 289 | ('Origin', 'origin'), 290 | ('Frame Rate', 'fps'), 291 | ] 292 | 293 | if preferences.export.type in ['UNREAL','BOTH']: 294 | col_data.append(('Skeleton', 'skeleton')) 295 | 296 | for label_str, property_str in col_data: 297 | row = layout.row() 298 | split = row.split(factor=0.6) 299 | col = split.column() 300 | col.alignment = 'RIGHT' 301 | col.label(text=label_str) 302 | col = split.column() 303 | if property_str == 'fps': 304 | col.prop(context.scene.render, property_str, text='') 305 | else: 306 | col.prop(animation, property_str, text='') 307 | 308 | if property_str == 'skeleton': 309 | row = layout.row() 310 | row.scale_y = 1.5 311 | row.operator('ue4workspace.update_skeletons',icon='ARMATURE_DATA') 312 | 313 | if context.active_object is not None and context.active_object.type == 'ARMATURE': 314 | row = layout.row() 315 | row.template_list('ANIMATION_UL_action_list', '', bpy.data, 'actions', context.active_object, 'ANIMATION_index_action') 316 | 317 | row = layout.row(align=True) 318 | row.scale_y = 1.5 319 | row.operator('ue4workspace.select_export_action', text='SELECT').type = 'SELECT' 320 | row.operator('ue4workspace.select_export_action', text='DESELECT').type = 'DESELECT' 321 | row.operator('ue4workspace.select_export_action', text='INVERT').type = 'INVERT' 322 | 323 | col = layout.column() 324 | col.scale_y = 1.5 325 | col.operator('ue4workspace.export_animation_mesh',icon='RENDER_ANIMATION') 326 | 327 | class SUB_PANEL_1(ExportOptionPanel): 328 | bl_idname = 'UE4WORKSPACE_PT_AnimationFBXOption' 329 | bl_parent_id = 'UE4WORKSPACE_PT_AnimationPanel' 330 | bl_label = 'FBX Export Setting' 331 | 332 | def draw(self, context): 333 | preferences = context.preferences.addons['UE4Workspace'].preferences 334 | fbx_setting = preferences.animation.fbx 335 | self.draw_property(fbx_setting, { 336 | 'tab_transform': [ 337 | ('Scale', 'global_scale'), 338 | ('Apply Scalings', 'apply_unit_scale'), 339 | ('Forward', 'axis_forward'), 340 | ('Up', 'axis_up'), 341 | ('Apply Unit', 'apply_unit_scale'), 342 | ('Apply Transform', 'bake_space_transform'), 343 | ], 344 | 'tab_armature': [ 345 | ('Primary Bone Axis', 'primary_bone_axis'), 346 | ('Secondary Bone Axis', 'secondary_bone_axis'), 347 | ('Armature FBXNode Type', 'armature_nodetype'), 348 | ('Only Deform Bones', 'use_armature_deform_only'), 349 | ('Add Leaf Bones', 'add_leaf_bones'), 350 | ], 351 | 'tab_bake_animation': [ 352 | ('Key All Bones', 'bake_anim_use_all_bones'), 353 | ('Force Start/End Keying', 'bake_anim_force_startend_keying'), 354 | ('Sampling Rate', 'bake_anim_step'), 355 | ('Simplify', 'bake_anim_simplify_factor'), 356 | ], 357 | }) 358 | 359 | class SUB_PANEL_2(ExportOptionPanel): 360 | bl_idname = 'UE4WORKSPACE_PT_AnimationUnrealEnginePanel' 361 | bl_parent_id = 'UE4WORKSPACE_PT_AnimationPanel' 362 | bl_label = 'Unreal Engine Export Setting' 363 | 364 | def draw(self, context): 365 | preferences = context.preferences.addons['UE4Workspace'].preferences 366 | unreal_engine_setting = preferences.animation.unreal_engine 367 | 368 | setting_data = { 369 | 'tab_animation': [ 370 | ('Animation Length', 'animation_length'), 371 | ('Import Meshes in Bone Hierarchy', 'import_meshes_in_bone_hierarchy'), 372 | ('Frame Import Range', 'frame_import_range'), 373 | ('Use Default Sample Rate', 'use_default_sample_rate'), 374 | ('Custom Sample Rate', 'custom_sample_rate'), 375 | ('Import Custom Attribute', 'import_custom_attribute'), 376 | ('Delete Existing Custom Attribute Curves', 'delete_existing_custom_attribute_curves'), 377 | ('Import Bone Tracks', 'import_bone_tracks'), 378 | ('Set Material Curve Type', 'set_material_drive_parameter_on_custom_attribute'), 379 | ('Material Curve Suffixes', 'material_curve_suffixes'), 380 | ('Remove Redundant Keys', 'remove_redundant_keys'), 381 | ('Delete Existing Morph Target Curves', 'delete_existing_morph_target_curves'), 382 | ('Do not Import Curve With 0 Values', 'do_not_import_curve_with_zero'), 383 | ('Preserve Local Transform', 'preserve_local_transform'), 384 | ], 385 | 'tab_transform': [ 386 | ('Import Translation', 'import_translation'), 387 | ('Import Rotation', 'import_rotation'), 388 | ('Import Uniform Scale', 'import_uniform_scale'), 389 | ], 390 | 'tab_misc': [ 391 | ('Convert Scene', 'convert_scene'), 392 | ('Force Front XAxis', 'force_front_x_axis'), 393 | ('Convert Scene Unit', 'convert_scene_unit'), 394 | ('Override Full Name', 'override_full_name'), 395 | ] 396 | } 397 | 398 | self.draw_property(unreal_engine_setting, setting_data) 399 | 400 | list_class_to_register = [ 401 | ANIMATION_UL_action_list, 402 | OP_SelectExportAction, 403 | OP_AddMaterialCurveSuffix, 404 | OP_ClearMaterialCurveSuffixes, 405 | OP_RemoveMaterialCurveSuffixIndex, 406 | OP_EditMaterialCurveSuffix, 407 | OP_ExportAnimationMesh, 408 | PANEL, 409 | SUB_PANEL_1, 410 | SUB_PANEL_2 411 | ] 412 | 413 | def register(): 414 | bpy.types.Action.is_export = bpy.props.BoolProperty( 415 | name='Export action', 416 | default=False 417 | ) 418 | 419 | bpy.types.Object.ANIMATION_index_action = bpy.props.IntProperty( 420 | default=-1 421 | ) 422 | 423 | for x in list_class_to_register: 424 | register_class(x) 425 | 426 | def unregister(): 427 | del bpy.types.Action.is_export 428 | del bpy.types.Object.ANIMATION_index_action 429 | 430 | for x in list_class_to_register[::-1]: 431 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/copy_to_unreal_engine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/copy_to_unreal_engine/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/copy_to_unreal_engine/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import Operator 4 | from . utils import BuildStringMap 5 | 6 | class OP_CopyTransformToUnrealEngineMap(Operator): 7 | bl_idname = 'ue4workspace.copy_transform_to_unreal_engine_map' 8 | bl_label = 'Copy Transform To Unreal Engine Map' 9 | bl_description = 'Copy Transform Selected Object To Unreal Engine Map\nOnly Support Static Mesh' 10 | 11 | @classmethod 12 | def poll(cls, context): 13 | return context.mode == 'OBJECT' 14 | 15 | def execute(self, context): 16 | selected_objects = context.selected_objects 17 | selected_objects = [obj for obj in selected_objects if obj.type == 'MESH' and not obj.data.is_custom_collision] 18 | 19 | context.window_manager.clipboard = BuildStringMap.generate_string(selected_objects) 20 | 21 | self.report({'INFO'}, 'copy transform success') 22 | 23 | return {'FINISHED'} 24 | 25 | def draw_menu(self, context): 26 | layout = self.layout 27 | 28 | if context.mode == 'OBJECT': 29 | layout.separator() 30 | layout.operator('ue4workspace.copy_transform_to_unreal_engine_map') 31 | 32 | list_class_to_register = [ 33 | OP_CopyTransformToUnrealEngineMap 34 | ] 35 | 36 | def register(): 37 | bpy.types.VIEW3D_MT_object_context_menu.append(draw_menu) 38 | 39 | for x in list_class_to_register: 40 | register_class(x) 41 | 42 | def unregister(): 43 | bpy.types.VIEW3D_MT_object_context_menu.remove(draw_menu) 44 | 45 | for x in list_class_to_register[::-1]: 46 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/copy_to_unreal_engine/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | class BuildStringMap: 4 | @classmethod 5 | def generate_string(cls, objects = []): 6 | string_actors = '' 7 | 8 | for obj in objects: 9 | if obj.type == 'MESH': 10 | string_actors += cls.static_mesh(obj, objects) 11 | 12 | string_clipboard ='''Begin Map 13 | Begin Level 14 | {} 15 | End Level 16 | Begin Surface 17 | End Surface 18 | End Map'''.format(string_actors) 19 | 20 | return string_clipboard 21 | 22 | @classmethod 23 | def static_mesh(cls, obj = None, objects = []): 24 | obj_location, obj_rotation_quaternion, obj_scale = obj.matrix_world.decompose() 25 | obj_location = obj_location * 100 26 | obj_location.y = obj_location.y*-1 27 | obj_rotation_euler = obj_rotation_quaternion.to_euler('XYZ') 28 | 29 | template_string = ''' 30 | Begin Actor Class=/Script/Engine.StaticMeshActor Name={actor_name} Archetype=/Script/Engine.StaticMeshActor\'/Script/Engine.Default__StaticMeshActor\''''.format(actor_name=obj.name) 31 | 32 | if obj.parent is not None and obj.parent.type == 'MESH' and obj.parent in objects: 33 | template_string += ' ParentActor={}'.format(obj.parent.name) 34 | 35 | template_string += ''' 36 | Begin Object Class=/Script/Engine.StaticMeshComponent Name="StaticMeshComponent0" Archetype=StaticMeshComponent'/Script/Engine.Default__StaticMeshActor:StaticMeshComponent0' 37 | End Object 38 | Begin Object Name="StaticMeshComponent0" 39 | StaticMesh=StaticMesh'"/Engine/BasicShapes/Cube.Cube"' 40 | RelativeLocation=(X={location.x},Y={location.y},Z={location.z}) 41 | RelativeRotation=(Pitch={rotation[y]},Yaw={rotation[z]},Roll={rotation[x]}) 42 | RelativeScale3D=(X={scale.x},Y={scale.y},Z={scale.z}) 43 | End Object 44 | StaticMeshComponent="StaticMeshComponent0" 45 | RootComponent="StaticMeshComponent0" 46 | ActorLabel="{actor_label}" 47 | End Actor 48 | '''.format(actor_label=obj.name, location=obj_location, rotation = { 'x': math.degrees(obj_rotation_euler.x), 'y': math.degrees(obj_rotation_euler.y * -1), 'z': math.degrees(obj_rotation_euler.z * -1) }, scale=obj_scale) 49 | 50 | return template_string -------------------------------------------------------------------------------- /UE4Workspace/credit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/credit/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/credit/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bpy 3 | from bpy.utils import register_class, unregister_class 4 | from bpy.types import Panel 5 | 6 | class PANEL(Panel): 7 | bl_idname = 'UE4WORKSPACE_PT_CreditPanel' 8 | bl_label = 'Credit' 9 | bl_category = 'UE4Workspace' 10 | bl_space_type = 'VIEW_3D' 11 | bl_region_type = 'UI' 12 | 13 | addonVersion = None 14 | 15 | def draw(self, context): 16 | layout = self.layout 17 | 18 | box = layout.box() 19 | col = box.column(align=True) 20 | # Credit Box 21 | row = col.row() 22 | row.alignment = 'CENTER' 23 | row.label(text='Unreal Engine 4') 24 | row = col.row() 25 | row.alignment = 'CENTER' 26 | row.label(text='Workspace') 27 | row = col.row() 28 | row.alignment = 'CENTER' 29 | row.label(text= 'Version : 2.0.0 Alpha') 30 | # Big Button Documentation 31 | row = layout.row(align=True) 32 | row.scale_y = 1.5 33 | row.operator('wm.url_open',icon='INFO', text='DOCUMENTATION').url='https://anasrar.github.io/Blender-UE4-Workspace/' 34 | 35 | list_class_to_register = [ 36 | PANEL 37 | ] 38 | 39 | def register(): 40 | for x in list_class_to_register: 41 | register_class(x) 42 | 43 | def unregister(): 44 | for x in list_class_to_register[::-1]: 45 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/export_option/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/export_option/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/export_option/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import Panel, Operator 4 | from .. utils.connect import remote 5 | 6 | class OP_ConnectToUnrealEngine(Operator): 7 | bl_idname = 'ue4workspace.connect_to_unreal_engine' 8 | bl_label = 'Connect / Disconnect Unreal Engine' 9 | 10 | def execute(self, context): 11 | preferences = context.preferences.addons['UE4Workspace'].preferences 12 | connect_unreal_engine = preferences.connect_unreal_engine 13 | 14 | if remote.is_connect: 15 | remote.disconnect() 16 | else: 17 | remote.connect(DEFAULT_MULTICAST_TTL=connect_unreal_engine.multicast_ttl, DEFAULT_MULTICAST_GROUP_ENDPOINT=(connect_unreal_engine.multicast_group_end_point.split(':')[0], int(connect_unreal_engine.multicast_group_end_point.split(':')[1])), DEFAULT_MULTICAST_BIND_ADDRESS=connect_unreal_engine.multicast_bind_address, DEFAULT_COMMAND_ENDPOINT=('127.0.0.1', 6776)) 18 | 19 | return {'FINISHED'} 20 | 21 | class OP_RefreshProjectList(Operator): 22 | bl_idname = 'ue4workspace.refresh_project_list' 23 | bl_label = 'Refresh Project' 24 | 25 | def execute(self, context): 26 | print(remote.remote_nodes) 27 | return {'FINISHED'} 28 | 29 | class PANEL(Panel): 30 | bl_idname = 'UE4WORKSPACE_PT_ExportOptionPanel' 31 | bl_label = 'Export Option' 32 | bl_category = 'UE4Workspace' 33 | bl_space_type = 'VIEW_3D' 34 | bl_region_type = 'UI' 35 | 36 | def draw(self, context): 37 | layout = self.layout 38 | preferences = context.preferences.addons['UE4Workspace'].preferences 39 | 40 | row = layout.row() 41 | split = row.split(factor=0.6) 42 | col = split.column() 43 | col.alignment = 'RIGHT' 44 | col.label(text='Export Type') 45 | col = split.column() 46 | col.prop(preferences.export, 'type', text='') 47 | 48 | row = layout.row() 49 | split = row.split(factor=0.6) 50 | col = split.column() 51 | col.alignment = 'RIGHT' 52 | col.label(text=('Export Folder' if preferences.export.type in ['BOTH', 'FILE'] else 'Temporary Folder')) 53 | col = split.column() 54 | col.prop(preferences.export, ('export_folder' if preferences.export.type in ['BOTH', 'FILE'] else 'temp_folder'), text='') 55 | 56 | if preferences.export.type in ['BOTH', 'UNREAL']: 57 | col = layout.column() 58 | col.scale_y = 1.5 59 | col.operator('ue4workspace.connect_to_unreal_engine', icon='PLUGIN', text=('Disconnect Unreal Engine' if remote.is_connect else 'Connect Unreal Engine')) 60 | col.operator('ue4workspace.refresh_project_list', icon='FILE_REFRESH') 61 | 62 | if remote.remote_nodes: 63 | layout.prop(preferences.export, 'project_list', icon=('TRIA_DOWN' if preferences.export.project_list else 'TRIA_RIGHT'), emboss=False) 64 | if preferences.export.project_list: 65 | for X in remote.remote_nodes: 66 | box = layout.box() 67 | 68 | col = box.column() 69 | row = col.row() 70 | split = row.split(factor=0.4) 71 | col = split.column() 72 | col.label(text='Project', icon='TEXT') 73 | col.label(text='Engine', icon='TOOL_SETTINGS') 74 | col = split.column() 75 | col.label(text=X.get('project_name', 'Project')) 76 | col.label(text=X.get('engine_version', 'XX.XX.XX').split('-')[0]) 77 | 78 | list_class_to_register = [ 79 | OP_ConnectToUnrealEngine, 80 | OP_RefreshProjectList, 81 | PANEL 82 | ] 83 | 84 | def register(): 85 | for x in list_class_to_register: 86 | register_class(x) 87 | 88 | def unregister(): 89 | for x in list_class_to_register[::-1]: 90 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/groom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/groom/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/groom/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import Operator 3 | from bpy.utils import register_class, unregister_class 4 | from .. utils.base import ExportOperator, ExperimentalPanel 5 | 6 | class OP_ExportGroom(ExportOperator): 7 | bl_idname = 'ue4workspace.export_groom' 8 | bl_label = 'Export Groom' 9 | bl_description = 'Export Hair Modifiers To Groom' 10 | 11 | ext_file = 'abc' 12 | 13 | def execute(self, context): 14 | preferences = context.preferences.addons['UE4Workspace'].preferences 15 | groom = preferences.groom 16 | 17 | selected_objects = context.selected_objects 18 | objects = context.scene.objects if groom.option == 'ALL' else selected_objects 19 | objects = [obj for obj in objects if obj.type == 'MESH' and 'HAIR' in [mod.particle_system.settings.type for mod in obj.modifiers if mod.type == 'PARTICLE_SYSTEM'] and not obj.data.mesh_as_lod] 20 | 21 | directory = self.create_string_directory(preferences.export.export_folder if preferences.export.type in ['FILE','BOTH'] else preferences.export.temp_folder, groom.subfolder) 22 | 23 | self.create_directory_if_not_exist(directory, groom.subfolder) 24 | 25 | list_name_objects = [] 26 | unreal_engine_import_setting = { 27 | 'files': [], 28 | 'main_folder': self.safe_string_path(preferences.connect_unreal_engine.main_folder), 29 | 'subfolder': self.safe_string_path(groom.subfolder), 30 | 'overwrite_file': groom.overwrite_file, 31 | 'temporary': preferences.export.type == 'UNREAL' 32 | } 33 | 34 | bpy.ops.object.select_all(action='DESELECT') 35 | 36 | for obj in objects: 37 | filename = self.safe_string_path(obj.name) 38 | check_duplicate = len([obj_name for obj_name in list_name_objects if obj_name.startswith(filename)]) 39 | list_name_objects.append(filename) 40 | 41 | filename += '_' + str(check_duplicate) if bool(check_duplicate) else '' 42 | filename_ext = filename + '.' + self.ext_file 43 | 44 | if not self.is_file_exist(directory, filename_ext) or groom.overwrite_file: 45 | 46 | self.mute_attach_constraint(obj) 47 | 48 | original_location = obj.matrix_world.to_translation() 49 | if groom.origin == 'OBJECT': 50 | obj.matrix_world.translation = (0, 0, 0) 51 | 52 | original_rotation = obj.rotation_quaternion.copy() if obj.rotation_mode == 'QUATERNION' else obj.rotation_euler.copy() 53 | if not groom.apply_rotation: 54 | if obj.rotation_mode == 'QUATERNION': 55 | obj.rotation_quaternion = (1, 0, 0, 0) 56 | else: 57 | obj.rotation_euler = (0, 0, 0) 58 | 59 | self.prepare_groom(obj) 60 | 61 | obj.select_set(state=True) 62 | 63 | export_setting = { 64 | 'filepath': self.create_string_directory(directory, filename_ext), 65 | 'check_existing': False, 66 | 'filter_blender': False, 67 | 'filter_backup': False, 68 | 'filter_image': False, 69 | 'filter_movie': False, 70 | 'filter_python': False, 71 | 'filter_font': False, 72 | 'filter_sound': False, 73 | 'filter_text': False, 74 | 'filter_archive': False, 75 | 'filter_btx': False, 76 | 'filter_collada': False, 77 | 'filter_alembic': True, 78 | 'filter_usd': False, 79 | 'filter_volume': False, 80 | 'filter_folder': True, 81 | 'filter_blenlib': False, 82 | 'filemode': 8, 83 | 'display_type': 'DEFAULT', 84 | 'sort_method': 'FILE_SORT_ALPHA', 85 | 'start': 1, 86 | 'end': 1, 87 | 'xsamples': 1, 88 | 'gsamples': 1, 89 | 'sh_open': 0.0, 90 | 'sh_close': 1.0, 91 | 'selected': True, 92 | 'renderable_only': False, 93 | 'visible_objects_only': True, 94 | 'flatten': False, 95 | 'uvs': True, 96 | 'packuv': True, 97 | 'normals': True, 98 | 'vcolors': False, 99 | 'face_sets': False, 100 | 'subdiv_schema': False, 101 | 'apply_subdiv': False, 102 | 'curves_as_mesh': False, 103 | 'use_instancing': True, 104 | 'global_scale': 100.0, 105 | 'triangulate': False, 106 | 'quad_method': 'SHORTEST_DIAGONAL', 107 | 'ngon_method': 'BEAUTY', 108 | 'export_hair': True, 109 | 'export_particles': False, 110 | 'export_custom_properties': groom.use_custom_props, 111 | 'as_background_job': False, 112 | 'init_scene_frame_range': False 113 | } 114 | 115 | # EXPORT 116 | bpy.ops.wm.alembic_export(**export_setting) 117 | 118 | unreal_engine_import_setting['files'].append({ 119 | 'path': export_setting['filepath'] 120 | }) 121 | 122 | self.restore_groom(obj) 123 | 124 | obj.select_set(state=False) 125 | 126 | if groom.origin == 'OBJECT': 127 | obj.matrix_world.translation = original_location 128 | 129 | if not groom.apply_rotation: 130 | if obj.rotation_mode == 'QUATERNION': 131 | obj.rotation_quaternion = original_rotation 132 | else: 133 | obj.rotation_euler = original_rotation 134 | 135 | self.unmute_attach_constraint(obj) 136 | 137 | for obj in selected_objects: 138 | obj.select_set(state=True) 139 | 140 | # self.unreal_engine_exec_script('ImportGroom.py', unreal_engine_import_setting) 141 | 142 | self.report({'INFO'}, 'export ' + str(len(unreal_engine_import_setting['files'])) + ' groom success') 143 | 144 | return {'FINISHED'} 145 | 146 | class OP_GroomImportOctahedron(Operator): 147 | bl_idname = 'ue4workspace.groom_import_octahedron' 148 | bl_label = 'Import Octahedron' 149 | bl_description = 'Import Octahedron For Baking Groom Texture' 150 | bl_options = {'UNDO'} 151 | 152 | @classmethod 153 | def poll(cls, context): 154 | preferences = context.preferences.addons['UE4Workspace'].preferences 155 | misc = preferences.misc 156 | return misc.experimental_features 157 | 158 | def execute(self, context): 159 | new_mesh = bpy.data.meshes.new('octahedron') 160 | new_mesh.from_pydata([(1.0, -0.0, -0.0), (-0.0, 1.0, -0.0), (-0.0, -0.0, 1.0), (-0.0, -0.0, -1.0), (-0.0, -1.0, -0.0), (-1.0, -0.0, -0.0)], [], [(1, 2, 0), (3, 1, 0), (0, 2, 4), (0, 4, 3), (1, 5, 2), (5, 1, 3), (5, 4, 2), (3, 4, 5)]) 161 | for polygon in new_mesh.polygons: 162 | polygon.use_smooth = True 163 | 164 | uv = new_mesh.uv_layers.new(name='UVMap') 165 | for index_data, uv_pos in enumerate([(1.0, 1.0), (0.5, 1.0), (1.0, 0.5), (0.5, 0.0), (1.0, 0.0), (1.0, 0.5), (1.0, 0.5), (0.5, 1.0), (0.5, 0.5), (1.0, 0.5), (0.5, 0.5), (0.5, 0.0), (0.0, 1.0), (0.0, 0.5), (0.5, 1.0), (0.0, 0.5), (0.0, 0.0), (0.5, 0.0), (0.0, 0.5), (0.5, 0.5), (0.5, 1.0), (0.5, 0.0), (0.5, 0.5), (0.0, 0.5)]): 166 | uv.data[index_data].uv = uv_pos 167 | 168 | # create material (MAT_UE4BakeGroom) if not exist 169 | mat = bpy.data.materials.get('MAT_UE4BakeGroom') 170 | if mat is None: 171 | mat = bpy.data.materials.new(name='MAT_UE4BakeGroom') 172 | mat.use_nodes = True 173 | mat.use_fake_user = True 174 | image_node = mat.node_tree.nodes.new(type='ShaderNodeTexImage') 175 | image_node.location = (-280.0, 300.0) 176 | mat.node_tree.links.new(image_node.outputs[0], mat.node_tree.nodes['Principled BSDF'].inputs[0]) 177 | mat.node_tree.nodes.active = image_node 178 | new_mesh.materials.append(mat) 179 | 180 | new_object = bpy.data.objects.new('octahedron', new_mesh) 181 | new_object.matrix_world = context.scene.cursor.matrix 182 | 183 | context.view_layer.active_layer_collection.collection.objects.link(new_object) 184 | 185 | return {'FINISHED'} 186 | 187 | class PANEL(ExperimentalPanel): 188 | bl_idname = 'UE4WORKSPACE_PT_GroomPanel' 189 | bl_label = '[Experimental] Groom' 190 | 191 | def draw(self, context): 192 | layout = self.layout 193 | preferences = context.preferences.addons['UE4Workspace'].preferences 194 | groom = preferences.groom 195 | 196 | row = layout.row(align=True) 197 | row.scale_y = 1.5 198 | row.operator('ue4workspace.groom_import_octahedron', icon='KEYFRAME') 199 | 200 | col_data = [ 201 | ('Subfolder', 'subfolder'), 202 | ('Overwrite File', 'overwrite_file'), 203 | ('Custom Properties', 'use_custom_props'), 204 | ('Apply Rotation', 'apply_rotation'), 205 | ('Export Groom Option', 'option'), 206 | ('Origin', 'origin'), 207 | ] 208 | 209 | for label_str, property_str in col_data: 210 | row = layout.row() 211 | split = row.split(factor=0.6) 212 | col = split.column() 213 | col.alignment = 'RIGHT' 214 | col.label(text=label_str) 215 | col = split.column() 216 | col.prop(groom, property_str, text='') 217 | 218 | col = layout.column() 219 | col.scale_y = 1.5 220 | col.operator('ue4workspace.export_groom',icon='OUTLINER_OB_HAIR') 221 | 222 | box = layout.box() 223 | col = box.column(align=True) 224 | 225 | row = col.row() 226 | row.alignment = 'CENTER' 227 | row.label(text='Not Support Export') 228 | 229 | row = col.row() 230 | row.alignment = 'CENTER' 231 | row.label(text='Directly To Unreal Engine') 232 | 233 | row = layout.row(align=True) 234 | row.scale_y = 1.5 235 | row.operator('wm.url_open',icon='URL', text='Unreal Engine Setting').url='https://github.com/anasrar/Blender-UE4-Workspace/issues/22' 236 | 237 | list_class_to_register = [ 238 | OP_ExportGroom, 239 | OP_GroomImportOctahedron, 240 | PANEL 241 | ] 242 | 243 | def register(): 244 | for x in list_class_to_register: 245 | register_class(x) 246 | 247 | def unregister(): 248 | for x in list_class_to_register[::-1]: 249 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/import_asset/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/import_asset/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/import_asset/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import bpy 4 | from mathutils import Vector 5 | from bpy.utils import register_class, unregister_class 6 | from bpy.types import Panel, Operator, PropertyGroup,UIList 7 | from .. utils.base import ExportOptionPanel 8 | from .. utils.connect import remote 9 | 10 | class PG_ImportAsset(PropertyGroup): 11 | name: bpy.props.StringProperty(default='asset') 12 | path: bpy.props.StringProperty(default='path') 13 | is_import: bpy.props.BoolProperty(default=False) 14 | node_id: bpy.props.StringProperty(default=':)') 15 | 16 | class IMPORTASSET_UL_AssetList(UIList): 17 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 18 | row = layout.row() 19 | row.label(text=item.name) 20 | row.prop(item, 'is_import', text='') 21 | 22 | class OP_UpdateAssetList(Operator): 23 | bl_idname = 'ue4workspace.update_asset_list' 24 | bl_label = 'Update Asset List' 25 | bl_description = 'Update Asset List From Unreal Engine Project' 26 | bl_options = {'UNDO'} 27 | 28 | @classmethod 29 | def poll(self, context): 30 | preferences = context.preferences.addons['UE4Workspace'].preferences 31 | return preferences.export.type in ['BOTH', 'UNREAL'] and bool(remote.remote_nodes) 32 | 33 | def execute(self, context): 34 | collections = { 35 | 'StaticMesh': context.scene.import_asset_static_mesh, 36 | 'SkeletalMesh': context.scene.import_asset_skeletal_mesh, 37 | 'AnimSequence': context.scene.import_asset_animation 38 | } 39 | 40 | reset_import_asset_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'import_asset_list.json')), 'w+') 41 | reset_import_asset_list.write(json.dumps([])) 42 | reset_import_asset_list.close() 43 | 44 | remote.exec_script('GetAllImportableAsset.py') 45 | 46 | load_import_asset_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'import_asset_list.json')), 'r') 47 | asset_list = json.loads(load_import_asset_list.read()) 48 | load_import_asset_list.close() 49 | 50 | for collection in collections.values(): 51 | collection.clear() 52 | 53 | context.scene.index_static_mesh = -1 54 | context.scene.index_skeletal_mesh = -1 55 | context.scene.index_animation = -1 56 | 57 | for node_id, object_path, asset_name, asset_class in asset_list: 58 | collection = collections[asset_class] 59 | asset = collection.add() 60 | asset.node_id = node_id 61 | asset.path = object_path 62 | asset.name = asset_name 63 | 64 | self.report({'INFO'}, 'Update asset list success') 65 | 66 | return {'FINISHED'} 67 | 68 | class OP_SelectImportAsset(Operator): 69 | bl_idname = 'ue4workspace.select_import_asset' 70 | bl_label = 'Select Asset To Import' 71 | bl_description = 'Select Asset To Import' 72 | bl_options = {'UNDO'} 73 | 74 | type: bpy.props.StringProperty(default='') 75 | 76 | def execute(self, context): 77 | collections = { 78 | 'STATIC_MESH': context.scene.import_asset_static_mesh, 79 | 'SKELETAL_MESH': context.scene.import_asset_skeletal_mesh, 80 | 'ANIMATION': context.scene.import_asset_animation 81 | } 82 | if self.type: 83 | for collection in collections[context.scene.import_asset_tab]: 84 | collection.is_import = {'SELECT': True, 'DESELECT': False, 'INVERT': not collection.is_import}[self.type] 85 | return {'FINISHED'} 86 | 87 | class OP_ImportAsset(Operator): 88 | bl_idname = 'ue4workspace.import_asset' 89 | bl_label = 'Import Asset' 90 | bl_description = 'Import Asset From Unreal Engine Project' 91 | bl_options = {'UNDO'} 92 | 93 | @classmethod 94 | def poll(self, context): 95 | preferences = context.preferences.addons['UE4Workspace'].preferences 96 | if preferences.export.type in ['FILE', 'BOTH'] and context.mode == 'OBJECT': 97 | return bool(preferences.export.export_folder.strip()) and bool(remote.remote_nodes) 98 | return bool(preferences.export.temp_folder.strip()) and bool(remote.remote_nodes) 99 | 100 | def execute(self, context): 101 | preferences = context.preferences.addons['UE4Workspace'].preferences 102 | import_asset = preferences.import_asset 103 | fbx_setting = import_asset.fbx 104 | unreal_engine_setting = import_asset.unreal_engine 105 | 106 | collection = { 107 | 'STATIC_MESH': context.scene.import_asset_static_mesh, 108 | 'SKELETAL_MESH': context.scene.import_asset_skeletal_mesh, 109 | 'ANIMATION': context.scene.import_asset_animation 110 | }[context.scene.import_asset_tab] 111 | 112 | assets = [(asset.node_id, asset.path, index) for index, asset in enumerate([x for x in collection if x.is_import])] 113 | 114 | unreal_engine_export_setting = { 115 | 'path': preferences.export.export_folder if preferences.export.type in ['FILE', 'BOTH'] else preferences.export.temp_folder, 116 | 'files': assets, 117 | } 118 | 119 | unreal_engine_export_setting.update(unreal_engine_setting.to_dict()) 120 | 121 | import_asset_setting = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'import_asset_setting.json')), 'w+') 122 | import_asset_setting.write(json.dumps(unreal_engine_export_setting, indent=4)) 123 | import_asset_setting.close() 124 | 125 | reset_imported_asset_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'imported_asset_list.json')), 'w+') 126 | reset_imported_asset_list.write(json.dumps([])) 127 | reset_imported_asset_list.close() 128 | 129 | remote.exec_script('ExportAsset.py') 130 | 131 | load_imported_asset_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'imported_asset_list.json')), 'r') 132 | imported_asset_list = json.loads(load_imported_asset_list.read()) 133 | load_imported_asset_list.close() 134 | 135 | for asset_path, asset_type, asset_name in imported_asset_list: 136 | if os.path.isfile(asset_path): 137 | bpy.ops.object.select_all(action='DESELECT') 138 | import_fbx_setting = { 139 | 'filepath': asset_path, 140 | 'directory': '', 141 | 'filter_glob': '*.fbx', 142 | 'ui_tab': 'MAIN' 143 | } 144 | import_fbx_setting.update(fbx_setting.to_dict()) 145 | 146 | bpy.ops.import_scene.fbx(**import_fbx_setting) 147 | 148 | selected_objects = context.selected_objects 149 | 150 | if asset_type == 'StaticMesh': 151 | collision_objects = [obj for obj in selected_objects if obj.name.startswith('UCX_')] 152 | if unreal_engine_setting.collision and bool(collision_objects): 153 | for collision_object in collision_objects: 154 | main_object = bpy.data.objects.get(collision_object.name[4:]) 155 | if main_object and main_object.type == 'MESH': 156 | context.view_layer.objects.active = main_object 157 | context.scene.collision_picker = collision_object 158 | bpy.ops.ue4workspace.collision_picker() 159 | 160 | main_lod_object = next(iter([obj for obj in selected_objects if obj.name.endswith('_LOD0')]), None) 161 | if unreal_engine_setting.level_of_detail and bool(main_lod_object): 162 | main_lod_object_matrix_world = main_lod_object.matrix_world.copy() 163 | main_lod_object_world_location = main_lod_object_matrix_world.to_translation() 164 | 165 | lod_objects = [obj for obj in selected_objects if obj.type == 'MESH' and obj.name.startswith(main_lod_object.name[:-5]) and obj != main_lod_object] 166 | for index, lod_object in enumerate(lod_objects, start=1): 167 | lod_object.matrix_world.translation = main_lod_object_world_location + ((main_lod_object.dimensions * Vector((1.0, 0.0, 0.0))) * index) + (Vector((1.0, 0.0, 0.0)) * index) 168 | 169 | lod_object_matrix_world = lod_object.matrix_world.copy() 170 | lod_object.data.mesh_as_lod = True 171 | lod_object.parent = main_lod_object 172 | lod_object.matrix_world = lod_object_matrix_world 173 | 174 | lod_data = main_lod_object.data.lods.add() 175 | lod_data.obj = lod_object 176 | 177 | empty_parent = main_lod_object.parent 178 | main_lod_object.parent = None 179 | main_lod_object.matrix_world = main_lod_object_matrix_world 180 | 181 | bpy.data.objects.remove(empty_parent) 182 | 183 | elif asset_type == 'SkeletalMesh': 184 | main_object = next(iter([obj for obj in selected_objects if obj.type == 'ARMATURE']), None) 185 | if main_object is not None: 186 | main_object_matrix_world = main_object.matrix_world.copy() 187 | empty_parent = main_object.parent 188 | 189 | main_object.parent = None 190 | main_object.matrix_world = main_object_matrix_world 191 | 192 | lod_group_empty = next(iter([obj for obj in selected_objects if obj.type == 'EMPTY' and obj.name.endswith('_LodGroup')]), None) 193 | if unreal_engine_setting.level_of_detail and bool(lod_group_empty): 194 | bpy.data.objects.remove(lod_group_empty) 195 | 196 | bpy.data.objects.remove(empty_parent) 197 | 198 | elif asset_type == 'AnimSequence': 199 | main_object = next(iter([obj for obj in selected_objects if obj.type == 'ARMATURE']), None) 200 | if main_object is not None: 201 | action = main_object.animation_data.action 202 | action.name = asset_name 203 | action.use_fake_user = True 204 | 205 | for obj in selected_objects: 206 | bpy.data.objects.remove(obj) 207 | 208 | os.remove(asset_path) 209 | 210 | bpy.ops.object.select_all(action='DESELECT') 211 | 212 | self.report({'INFO'}, 'Import asset success') 213 | 214 | return {'FINISHED'} 215 | 216 | class PANEL(Panel): 217 | bl_idname = 'UE4WORKSPACE_PT_ImportAssetPanel' 218 | bl_label = 'Import Asset' 219 | bl_category = 'UE4Workspace' 220 | bl_space_type = 'VIEW_3D' 221 | bl_region_type = 'UI' 222 | bl_options = {'DEFAULT_CLOSED'} 223 | 224 | @classmethod 225 | def poll(self, context): 226 | preferences = context.preferences.addons['UE4Workspace'].preferences 227 | return preferences.export.type in ['BOTH', 'UNREAL'] and context.mode == 'OBJECT' 228 | 229 | def draw(self, context): 230 | layout = self.layout 231 | preferences = context.preferences.addons['UE4Workspace'].preferences 232 | 233 | row = layout.row() 234 | row.scale_y = 1.5 235 | row.operator('ue4workspace.update_asset_list', icon='FILE_REFRESH', text='Update Asset List') 236 | 237 | row = layout.row() 238 | row.scale_y = 1.5 239 | row.prop(context.scene, 'import_asset_tab', expand=True) 240 | 241 | layout.template_list('IMPORTASSET_UL_AssetList', '', context.scene, { 242 | 'STATIC_MESH': 'import_asset_static_mesh', 243 | 'SKELETAL_MESH': 'import_asset_skeletal_mesh', 244 | 'ANIMATION': 'import_asset_animation' 245 | }[context.scene.import_asset_tab], context.scene, { 246 | 'STATIC_MESH': 'index_static_mesh', 247 | 'SKELETAL_MESH': 'index_skeletal_mesh', 248 | 'ANIMATION': 'index_animation' 249 | }[context.scene.import_asset_tab], rows=4) 250 | 251 | row = layout.row(align=True) 252 | row.scale_y = 1.5 253 | row.operator('ue4workspace.select_import_asset', text='SELECT').type = 'SELECT' 254 | row.operator('ue4workspace.select_import_asset', text='DESELECT').type = 'DESELECT' 255 | row.operator('ue4workspace.select_import_asset', text='INVERT').type = 'INVERT' 256 | 257 | row = layout.row() 258 | row.scale_y = 1.5 259 | row.operator('ue4workspace.import_asset', icon='IMPORT') 260 | 261 | class BaseSubPanel(ExportOptionPanel): 262 | bl_parent_id = 'UE4WORKSPACE_PT_ImportAssetPanel' 263 | bl_category = 'UE4Workspace' 264 | bl_space_type = 'VIEW_3D' 265 | bl_region_type = 'UI' 266 | bl_options = {'DEFAULT_CLOSED'} 267 | 268 | class SUB_PANEL_1(BaseSubPanel): 269 | bl_idname = 'UE4WORKSPACE_PT_ImportAssetFBXOptionPanel' 270 | bl_label = 'FBX Import Setting' 271 | 272 | def draw(self, context): 273 | preferences = context.preferences.addons['UE4Workspace'].preferences 274 | fbx_setting = preferences.import_asset.fbx 275 | self.draw_property(fbx_setting, { 276 | 'tab_include': [ 277 | ('Custom Normals', 'use_custom_normals'), 278 | ('Subdivision Data', 'use_subsurf'), 279 | ('Custom Properties', 'use_custom_props'), 280 | ('Import Enums As Strings', 'use_custom_props_enum_as_string'), 281 | ('Image Search', 'use_image_search'), 282 | ], 283 | 'tab_transform': [ 284 | ('Scale', 'global_scale'), 285 | ('Decal Offset', 'decal_offset'), 286 | ('Apply Transform', 'bake_space_transform'), 287 | ('Use Pre/Post Rotation', 'use_prepost_rot'), 288 | ], 289 | 'tab_orientation': [ 290 | ('Manual Orientation', 'use_manual_orientation'), 291 | ('Forward', 'axis_forward'), 292 | ('Up', 'axis_up'), 293 | ], 294 | 'tab_animation': [ 295 | ('Import Animation', 'use_anim'), 296 | ('Animation Offset', 'anim_offset'), 297 | ], 298 | 'tab_armature': [ 299 | ('Ignore Leaf Bones', 'ignore_leaf_bones'), 300 | ('Force Connect Children', 'force_connect_children'), 301 | ('Automatic Bone Orientation', 'automatic_bone_orientation'), 302 | ('Primary Bone Axis', 'primary_bone_axis'), 303 | ('Secondary Bone Axis', 'secondary_bone_axis'), 304 | ], 305 | }) 306 | 307 | class SUB_PANEL_2(BaseSubPanel): 308 | bl_idname = 'UE4WORKSPACE_PT_ImportAssetUnrealEnginePanel' 309 | bl_label = 'Unreal Engine Import Setting' 310 | 311 | def draw(self, context): 312 | preferences = context.preferences.addons['UE4Workspace'].preferences 313 | unreal_engine_setting = preferences.import_asset.unreal_engine 314 | self.draw_property(unreal_engine_setting, { 315 | 'tab_exporter': [ 316 | ('FBX Export Compatibility', 'fbx_export_compatibility'), 317 | ('ASCII', 'ascii'), 318 | ('Force Front X Axis', 'force_front_x_axis'), 319 | ], 320 | 'tab_mesh': [ 321 | ('Vertex Color', 'vertex_color'), 322 | ('Level Of Detail', 'level_of_detail'), 323 | ], 324 | 'tab_static_mesh': [ 325 | ('Collision', 'collision'), 326 | ], 327 | 'tab_skeletal_mesh': [ 328 | ('Export Morph Targets', 'export_morph_targets'), 329 | ], 330 | 'tab_animation': [ 331 | ('Export Preview Mesh', 'export_preview_mesh'), 332 | ('Map Skeletal Motion To Root', 'map_skeletal_motion_to_root'), 333 | ('Export Local Time', 'export_local_time'), 334 | ], 335 | }) 336 | 337 | list_class_to_register = [ 338 | PG_ImportAsset, 339 | IMPORTASSET_UL_AssetList, 340 | OP_UpdateAssetList, 341 | OP_SelectImportAsset, 342 | OP_ImportAsset, 343 | PANEL, 344 | SUB_PANEL_1, 345 | SUB_PANEL_2 346 | ] 347 | 348 | def register(): 349 | 350 | bpy.types.Scene.import_asset_tab = bpy.props.EnumProperty( 351 | name='Import Asset Tab', 352 | items=[ 353 | ('STATIC_MESH', 'Static Mesh', 'Static Mesh Tab'), 354 | ('SKELETAL_MESH', 'Skeletal Mesh', 'Skeletal Mesh Tab'), 355 | ('ANIMATION', 'Animation', 'Animation Tab') 356 | ], 357 | default='STATIC_MESH' 358 | ) 359 | 360 | bpy.types.Scene.index_static_mesh = bpy.props.IntProperty(default=-1) 361 | bpy.types.Scene.index_skeletal_mesh = bpy.props.IntProperty(default=-1) 362 | bpy.types.Scene.index_animation = bpy.props.IntProperty(default=-1) 363 | 364 | for x in list_class_to_register: 365 | register_class(x) 366 | 367 | bpy.types.Scene.import_asset_static_mesh = bpy.props.CollectionProperty(type=PG_ImportAsset) 368 | bpy.types.Scene.import_asset_skeletal_mesh = bpy.props.CollectionProperty(type=PG_ImportAsset) 369 | bpy.types.Scene.import_asset_animation = bpy.props.CollectionProperty(type=PG_ImportAsset) 370 | 371 | def unregister(): 372 | del bpy.types.Scene.import_asset_tab 373 | 374 | del bpy.types.Scene.index_static_mesh 375 | del bpy.types.Scene.index_skeletal_mesh 376 | del bpy.types.Scene.index_animation 377 | 378 | del bpy.types.Scene.import_asset_static_mesh 379 | del bpy.types.Scene.import_asset_skeletal_mesh 380 | del bpy.types.Scene.import_asset_animation 381 | 382 | for x in list_class_to_register[::-1]: 383 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/object/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/object/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/object/custom_collision.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import bpy 3 | from mathutils import Matrix, Vector 4 | import bmesh 5 | from bpy.utils import register_class, unregister_class 6 | from bpy.types import Operator 7 | from .. utils.base import ObjectSubPanel 8 | 9 | class OP_CreateCollision(Operator): 10 | bl_idname = 'ue4workspace.create_collision' 11 | bl_label = 'Create Collsion' 12 | bl_description = 'Create Custom Collision Mesh\nSelect a Mesh > Edit Mode > Select Edge' 13 | bl_options = {'UNDO', 'REGISTER'} 14 | 15 | collision_name: bpy.props.StringProperty( 16 | name='Name', 17 | default='collision_name' 18 | ) 19 | 20 | size: bpy.props.FloatProperty( 21 | name='Size', 22 | min=1, 23 | default=1.015 24 | ) 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return context.object is not None and context.active_object is not None and context.active_object.type == 'MESH' and context.active_object.mode == 'EDIT' and not context.active_object.data.is_custom_collision 29 | 30 | def execute(self, context): 31 | active_object = context.active_object 32 | 33 | active_object.update_from_editmode() 34 | selected_verts = [verts.co for verts in active_object.data.vertices if verts.select] 35 | 36 | # create collection (UE4CustomCollision) if not exist 37 | collection = bpy.data.collections.get('UE4CustomCollision', False) 38 | if (not collection): 39 | collection = bpy.data.collections.new('UE4CustomCollision') 40 | context.scene.collection.children.link(collection) 41 | 42 | bm = bmesh.new() 43 | 44 | median_space = Vector(np.median([list(vert) for vert in selected_verts], axis=0)) 45 | 46 | # scale 47 | for vert_co in selected_verts: 48 | bmesh.ops.create_vert(bm, co=(median_space + self.size * (vert_co - median_space))) 49 | 50 | # convex hull 51 | bmesh.ops.convex_hull(bm, input=bm.verts, use_existing_faces=True) 52 | 53 | data_mesh = bpy.data.meshes.new(self.collision_name) 54 | bm.to_mesh(data_mesh) 55 | bm.free() 56 | 57 | obj = bpy.data.objects.new(self.collision_name, data_mesh) 58 | obj.data.is_custom_collision = True 59 | obj.show_wire = True 60 | obj.display_type = 'SOLID' 61 | obj.color = (0.15, 1.000000, 0, 0.200000) 62 | obj.parent = active_object 63 | context.space_data.shading.color_type = 'OBJECT' 64 | 65 | # create material (MAT_UE4CustomCollision) if not exist 66 | mat = bpy.data.materials.get('MAT_UE4CustomCollision') 67 | if mat is None: 68 | mat = bpy.data.materials.new(name='MAT_UE4CustomCollision') 69 | mat.blend_method = 'BLEND' 70 | mat.use_nodes = True 71 | mat.node_tree.nodes['Principled BSDF'].inputs[0].default_value = (0.15, 1.000000, 0, 1) 72 | mat.node_tree.nodes['Principled BSDF'].inputs[19].default_value = 0.1 73 | mat.use_fake_user = True 74 | 75 | if obj.data.materials: 76 | obj.data.materials[0] = mat 77 | else: 78 | obj.data.materials.append(mat) 79 | 80 | collection.objects.link(obj) 81 | 82 | return {'FINISHED'} 83 | 84 | class OP_CollisionPicker(Operator): 85 | bl_idname = 'ue4workspace.collision_picker' 86 | bl_label = 'Collision Picker' 87 | bl_description = 'Create mesh into a custom collision' 88 | bl_options = {'UNDO'} 89 | 90 | @classmethod 91 | def poll(cls, context): 92 | obj = context.scene.collision_picker 93 | if context.mode == 'OBJECT': 94 | if context.active_object is not None and obj is not None and obj.type == 'MESH' and not obj.data.is_custom_collision and context.active_object is not obj: 95 | return True 96 | return False 97 | 98 | def execute(self, context): 99 | obj = context.scene.collision_picker 100 | context.scene.collision_picker = None 101 | 102 | obj.data.is_custom_collision = True 103 | obj.parent = context.active_object 104 | 105 | # clear local transform 106 | obj.matrix_parent_inverse = context.active_object.matrix_world.inverted() 107 | 108 | obj.show_wire = True 109 | obj.display_type = 'SOLID' 110 | obj.color = (0.15, 1.000000, 0, 0.200000) 111 | context.space_data.shading.color_type = 'OBJECT' 112 | 113 | # create material (MAT_UE4CustomCollision) if not exist 114 | mat = bpy.data.materials.get('MAT_UE4CustomCollision') 115 | if mat is None: 116 | mat = bpy.data.materials.new(name='MAT_UE4CustomCollision') 117 | mat.blend_method = 'BLEND' 118 | mat.use_nodes = True 119 | mat.node_tree.nodes['Principled BSDF'].inputs[0].default_value = (0.15, 1.000000, 0, 1) 120 | mat.node_tree.nodes['Principled BSDF'].inputs[19].default_value = 0.1 121 | mat.use_fake_user = True 122 | 123 | if obj.data.materials: 124 | obj.data.materials[0] = mat 125 | else: 126 | obj.data.materials.append(mat) 127 | 128 | old_collections = obj.users_collection 129 | collection = bpy.data.collections.get('UE4CustomCollision', False) 130 | if (not collection): 131 | collection = bpy.data.collections.new('UE4CustomCollision') 132 | context.scene.collection.children.link(collection) 133 | 134 | collection.objects.link(obj) 135 | for coll in old_collections: 136 | coll.objects.unlink(obj) 137 | 138 | return {'FINISHED'} 139 | 140 | class PANEL(ObjectSubPanel): 141 | bl_idname = 'UE4WORKSPACE_PT_ObjectCustomCollisionPanel' 142 | bl_label = 'Custom Collision' 143 | 144 | @classmethod 145 | def poll(cls, context): 146 | return context.active_object is not None and context.active_object.type in ['MESH'] 147 | 148 | def draw(self, context): 149 | layout = self.layout 150 | preferences = context.preferences.addons['UE4Workspace'].preferences 151 | active_object = context.active_object 152 | 153 | if context.mode == 'OBJECT' and not active_object.data.is_custom_collision: 154 | col = layout.box().column() 155 | split = col.split(factor=0.6) 156 | col = split.column() 157 | col.alignment = 'RIGHT' 158 | col.label(text='Collision Picker') 159 | col = split.column() 160 | col.prop(context.scene, 'collision_picker', icon='MOD_SOLIDIFY', text='') 161 | col = col.row() 162 | col.scale_y = 1.5 163 | col.operator('ue4workspace.collision_picker',icon='MOD_SOLIDIFY', text='Convert') 164 | 165 | row = layout.box().row() 166 | row.scale_y = 1.5 167 | row.operator('ue4workspace.create_collision',icon='OUTLINER_OB_MESH') 168 | 169 | collision_objects = [obj for obj in context.scene.objects if obj.type == 'MESH' and obj.parent == active_object and obj.data.is_custom_collision] 170 | 171 | if collision_objects: 172 | box = layout.box() 173 | for obj in collision_objects: 174 | col = box.column() 175 | split = col.split(factor=0.6) 176 | col = split.column() 177 | col.prop(obj, 'name', text='') 178 | row = split.row() 179 | row.alignment = 'RIGHT' 180 | row.operator('ue4workspace.toggle_visibility_object', icon=('HIDE_ON' if obj.hide_get() else 'HIDE_OFF'), text='', emboss=False).object_name = obj.name 181 | row.operator('ue4workspace.remove_object', icon='TRASH', text='', emboss=False).object_name = obj.name 182 | 183 | list_class_to_register = [ 184 | OP_CreateCollision, 185 | OP_CollisionPicker, 186 | PANEL 187 | ] 188 | 189 | def register(): 190 | bpy.types.Mesh.is_custom_collision = bpy.props.BoolProperty( 191 | name='Is custom collision ?', 192 | description='custom collision ?', 193 | default=False 194 | ) 195 | 196 | bpy.types.Scene.collision_picker = bpy.props.PointerProperty( 197 | name='Collision Picker', 198 | description='Make mesh into a custom collision', 199 | type=bpy.types.Object, 200 | poll=lambda self, obj: obj.type == 'MESH' and not obj.data.is_custom_collision and not 'ARMATURE' in [mod.type for mod in obj.modifiers] and obj is not bpy.context.active_object 201 | ) 202 | 203 | for x in list_class_to_register: 204 | register_class(x) 205 | 206 | def unregister(): 207 | del bpy.types.Mesh.is_custom_collision 208 | del bpy.types.Scene.collision_picker 209 | 210 | for x in list_class_to_register[::-1]: 211 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/object/level_of_detail.py: -------------------------------------------------------------------------------- 1 | import math 2 | import bpy 3 | from mathutils import Vector 4 | from bpy.utils import register_class, unregister_class 5 | from bpy.types import Operator, PropertyGroup 6 | from .. utils.base import ObjectSubPanel 7 | 8 | class OP_GenerateLODs(Operator): 9 | bl_idname = 'ue4workspace.generate_lods' 10 | bl_label = 'Generate LODs' 11 | bl_options = {'UNDO', 'REGISTER'} 12 | 13 | total: bpy.props.IntProperty( 14 | name='Total', 15 | default=3, 16 | min=1, 17 | max=7 18 | ) 19 | 20 | angel: bpy.props.FloatProperty( 21 | name='Angel', 22 | default=0.0, 23 | min=0.0, 24 | max=math.pi*2, 25 | subtype='ANGLE', 26 | unit='ROTATION' 27 | ) 28 | 29 | margin: bpy.props.FloatProperty( 30 | name='Margin', 31 | default=1.0, 32 | min=0.0, 33 | subtype='DISTANCE', 34 | unit='LENGTH' 35 | ) 36 | 37 | ratio: bpy.props.FloatProperty( 38 | name='Ratio', 39 | default=1.0, 40 | min=0.0, 41 | max=10.0 42 | ) 43 | 44 | @classmethod 45 | def poll(cls, context): 46 | active_object = context.active_object 47 | return active_object is not None and active_object.type in ['MESH'] and not 'ARMATURE' in [mod.type for mod in active_object.modifiers] and not active_object.data.mesh_as_lod 48 | 49 | def execute(self, context): 50 | active_object = context.active_object 51 | active_object_world_location = active_object.matrix_world.to_translation() 52 | 53 | vector_angle = Vector((math.cos(self.angel), math.sin(self.angel), 0)) 54 | vector_margin = vector_angle * self.margin 55 | 56 | active_object.data.lods.clear() 57 | for index in range(self.total): 58 | index += 1 59 | lod_obj = bpy.data.objects.new(active_object.name, active_object.data.copy()) 60 | lod_obj.matrix_world = active_object.matrix_world.copy() 61 | 62 | for collection in active_object.users_collection: 63 | collection.objects.link(lod_obj) 64 | lod_obj.matrix_world.translation = active_object_world_location + ((active_object.dimensions * vector_angle) * index) + (vector_margin * index) 65 | 66 | lod_obj.data.mesh_as_lod = True 67 | lod_obj.parent = active_object 68 | lod_obj.matrix_parent_inverse = active_object.matrix_world.inverted() 69 | 70 | decimate = lod_obj.modifiers.new('LOD', 'DECIMATE') 71 | decimate.decimate_type = 'COLLAPSE' 72 | decimate.ratio = ((0.1 * self.ratio) / self.total) * (self.total - (index - 1)) 73 | decimate.use_collapse_triangulate = True 74 | decimate.use_symmetry = True 75 | 76 | lod_data = active_object.data.lods.add() 77 | lod_data.obj = lod_obj 78 | return {'FINISHED'} 79 | 80 | class OP_AddLODSlot(Operator): 81 | bl_idname = 'ue4workspace.add_lod_slot' 82 | bl_label = 'Add LOD Slot' 83 | bl_options = {'UNDO'} 84 | 85 | @classmethod 86 | def poll(cls, context): 87 | active_object = context.active_object 88 | return active_object is not None and active_object.type in ['MESH'] and not 'ARMATURE' in [mod.type for mod in active_object.modifiers] and 7 > len(active_object.data.lods) 89 | 90 | def execute(self, context): 91 | active_object = context.active_object 92 | active_object.data.lods.add() 93 | return {'FINISHED'} 94 | 95 | class OP_RemoveLODSlot(Operator): 96 | bl_idname = 'ue4workspace.remove_lod_slot' 97 | bl_label = 'Remove LOD Slot' 98 | bl_options = {'UNDO'} 99 | 100 | index: bpy.props.IntProperty(default=-1) 101 | 102 | @classmethod 103 | def poll(cls, context): 104 | active_object = context.active_object 105 | return active_object is not None and active_object.type in ['MESH'] and not 'ARMATURE' in [mod.type for mod in active_object.modifiers] 106 | 107 | def execute(self, context): 108 | active_object = context.active_object 109 | if self.index > -1: 110 | active_object.data.lods.remove(self.index) 111 | return {'FINISHED'} 112 | 113 | class PG_LOD(PropertyGroup): 114 | 115 | screen_size: bpy.props.FloatProperty( 116 | name='LOD Screen Size', 117 | description='Set a screen size value for LOD', 118 | default=0.0 119 | ) 120 | 121 | obj: bpy.props.PointerProperty( 122 | name='LOD Object', 123 | description='Mesh to LOD', 124 | type=bpy.types.Object, 125 | poll=lambda self, obj: obj.type == 'MESH' and not 'ARMATURE' in [mod.type for mod in obj.modifiers] and obj != bpy.context.active_object and obj.data.mesh_as_lod 126 | ) 127 | 128 | class PANEL(ObjectSubPanel): 129 | bl_idname = 'UE4WORKSPACE_PT_ObjectLODPanel' 130 | bl_label = 'Level of Detail' 131 | 132 | @classmethod 133 | def poll(cls, context): 134 | return context.active_object is not None and context.active_object.type in ['MESH'] and not 'ARMATURE' in [mod.type for mod in context.active_object.modifiers] 135 | 136 | def draw(self, context): 137 | layout = self.layout 138 | preferences = context.preferences.addons['UE4Workspace'].preferences 139 | active_object = context.active_object 140 | 141 | box = layout.box() 142 | 143 | split = box.column().split(factor=0.6) 144 | col = split.column() 145 | col.alignment = 'RIGHT' 146 | col.label(text='Mesh as LOD') 147 | col = split.column() 148 | col.prop(active_object.data, 'mesh_as_lod', text='') 149 | 150 | if not active_object.data.mesh_as_lod: 151 | row = layout.box().row() 152 | row.scale_y = 1.5 153 | row.operator('ue4workspace.generate_lods', icon='MOD_DECIM') 154 | 155 | split = box.column().split(factor=0.6) 156 | col = split.column() 157 | col.alignment = 'RIGHT' 158 | col.label(text='Auto LOD Screen Size') 159 | col = split.column() 160 | col.prop(active_object.data, 'auto_compute_lod_screen_size', text='') 161 | 162 | row = layout.box().row() 163 | row.scale_y = 1.5 164 | row.operator('ue4workspace.add_lod_slot', icon='PRESET_NEW') 165 | 166 | box = layout.box() 167 | 168 | split = box.split(factor=0.6) 169 | col = split.column() 170 | col.alignment = 'RIGHT' 171 | col.label(text='LOD 0') 172 | col = split.column() 173 | col.label(text=active_object.name, icon='MESH_CUBE') 174 | 175 | if not active_object.data.auto_compute_lod_screen_size: 176 | split = box.split(factor=0.6) 177 | col = split.column() 178 | col.alignment = 'RIGHT' 179 | col.label(text='Screen Size') 180 | col = split.column() 181 | col.prop(active_object.data, 'lod_0_screen_size', text='') 182 | 183 | for index, lod in enumerate(active_object.data.lods, start=1): 184 | box = layout.box() 185 | 186 | split = box.split(factor=0.6) 187 | col = split.column() 188 | row = col.row(align=True) 189 | row.operator('ue4workspace.remove_lod_slot', icon='X', text='').index = index - 1 190 | sub = row.row() 191 | sub.alignment = 'RIGHT' 192 | sub.scale_x = 2.0 193 | sub.label(text='LOD ' + str(index)) 194 | col = split.column() 195 | col.prop(lod, 'obj', text='', icon='MESH_CUBE') 196 | 197 | if not active_object.data.auto_compute_lod_screen_size: 198 | split = box.split(factor=0.6) 199 | col = split.column() 200 | col.alignment = 'RIGHT' 201 | col.label(text='Screen Size') 202 | col = split.column() 203 | col.prop(lod, 'screen_size', text='') 204 | 205 | if lod.obj: 206 | decimate = lod.obj.modifiers.get('LOD', False) 207 | if decimate: 208 | split = box.split(factor=0.6) 209 | col = split.column() 210 | col.alignment = 'RIGHT' 211 | col.label(text='Ratio') 212 | col = split.column() 213 | col.prop(decimate, 'ratio', text='', slider=True) 214 | 215 | list_class_to_register = [ 216 | PG_LOD, 217 | OP_GenerateLODs, 218 | OP_AddLODSlot, 219 | OP_RemoveLODSlot, 220 | PANEL 221 | ] 222 | 223 | def register(): 224 | 225 | bpy.types.Mesh.mesh_as_lod = bpy.props.BoolProperty( 226 | name='Mesh as LOD', 227 | description='If checked, will not export as static mesh instead will be a part of LOD', 228 | default=False 229 | ) 230 | 231 | bpy.types.Mesh.auto_compute_lod_screen_size = bpy.props.BoolProperty( 232 | name='Auto Compute LOD ScreenSize', 233 | description='If checked, the editor will automatically compute screen size values for the static mesh’s LODs. If unchecked, the user can enter custom screen size values for each LOD', 234 | default=True 235 | ) 236 | 237 | bpy.types.Mesh.lod_0_screen_size = bpy.props.FloatProperty( 238 | name='LOD 0 Screen Size', 239 | description='Set a screen size value for LOD 0', 240 | default=1.0 241 | ) 242 | 243 | for x in list_class_to_register: 244 | register_class(x) 245 | 246 | bpy.types.Mesh.lods = bpy.props.CollectionProperty(type=PG_LOD) 247 | 248 | def unregister(): 249 | del bpy.types.Mesh.mesh_as_lod 250 | del bpy.types.Mesh.auto_compute_lod_screen_size 251 | 252 | for x in list_class_to_register[::-1]: 253 | unregister_class(x) 254 | 255 | del bpy.types.Mesh.lods -------------------------------------------------------------------------------- /UE4Workspace/object/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from .. utils.base import ObjectPanel 4 | 5 | from . import custom_collision 6 | from . import socket 7 | from . import level_of_detail 8 | from . import skeletal_mesh_part 9 | 10 | class PANEL(ObjectPanel): 11 | bl_idname = 'UE4WORKSPACE_PT_ObjectPanel' 12 | bl_label = 'Object' 13 | 14 | def draw(self, context): 15 | pass 16 | 17 | list_class_to_register = [ 18 | custom_collision, 19 | socket, 20 | level_of_detail, 21 | skeletal_mesh_part 22 | ] 23 | 24 | def register(): 25 | register_class(PANEL) 26 | for x in list_class_to_register: 27 | x.register() 28 | 29 | def unregister(): 30 | unregister_class(PANEL) 31 | for x in list_class_to_register[::-1]: 32 | x.unregister() -------------------------------------------------------------------------------- /UE4Workspace/object/skeletal_mesh_part.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from .. utils.base import ObjectSubPanel 4 | 5 | class PANEL(ObjectSubPanel): 6 | bl_idname = 'UE4WORKSPACE_PT_ObjectSkeletalMeshPartPanel' 7 | bl_label = 'Skeletal Mesh Part' 8 | 9 | @classmethod 10 | def poll(cls, context): 11 | return context.active_object is not None and context.active_object.type in ['ARMATURE'] 12 | 13 | def draw(self, context): 14 | layout = self.layout 15 | preferences = context.preferences.addons['UE4Workspace'].preferences 16 | active_object = context.active_object 17 | 18 | meshes = [obj for obj in active_object.children if obj.type == 'MESH'] 19 | 20 | if bool(meshes): 21 | layout.box().label(text='Export Part') 22 | for obj in meshes: 23 | row = layout.box().row() 24 | split = row.split(factor=0.7) 25 | row = split.row() 26 | row.prop(obj.data, 'is_export_skeletal_mesh_part', text='') 27 | row.prop(obj, 'name', text='') 28 | split = split.split() 29 | row = split.row() 30 | row.alignment = 'RIGHT' 31 | row.operator('ue4workspace.toggle_visibility_object', icon=('HIDE_ON' if obj.hide_get() else 'HIDE_OFF'), text='', emboss=False).object_name = obj.name 32 | row.operator('ue4workspace.remove_object', icon='TRASH', text='', emboss=False).object_name = obj.name 33 | else: 34 | col = layout.box().column(align=True) 35 | col.label(text='This armature does not') 36 | col.label(text='have any mesh to export.') 37 | 38 | list_class_to_register = [ 39 | PANEL 40 | ] 41 | 42 | def register(): 43 | bpy.types.Mesh.is_export_skeletal_mesh_part = bpy.props.BoolProperty( 44 | name='Export Skeletal Mesh Part', 45 | description='If true, it will export the mesh', 46 | default=True 47 | ) 48 | 49 | for x in list_class_to_register: 50 | register_class(x) 51 | 52 | def unregister(): 53 | del bpy.types.Mesh.is_export_skeletal_mesh_part 54 | 55 | for x in list_class_to_register[::-1]: 56 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/object/socket.py: -------------------------------------------------------------------------------- 1 | import math 2 | import bpy 3 | from bpy.utils import register_class, unregister_class 4 | from bpy.types import Operator 5 | from mathutils import Matrix, Euler 6 | from .. utils.base import ObjectSubPanel, create_matrix_scale_from_vector 7 | 8 | class OP_AttachObject(Operator): 9 | bl_idname = 'ue4workspace.attach_object' 10 | bl_label = 'Attach Object' 11 | bl_description = 'Attach or Detach Object' 12 | bl_options = {'UNDO', 'REGISTER'} 13 | 14 | snap: bpy.props.BoolProperty( 15 | name='Snap', 16 | default=True 17 | ) 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | return context.active_object is not None and context.active_object.attach_to is not None and context.active_object.attach_to.parent is not context.active_object 22 | 23 | def execute(self, context): 24 | active_object = context.active_object 25 | is_attach_to_object = active_object.is_attach_to_object 26 | active_object.is_attach_to_object = not is_attach_to_object 27 | 28 | if not is_attach_to_object: 29 | # attach 30 | constraint = active_object.constraints.new(type='CHILD_OF') 31 | constraint.name = 'attach_to' 32 | constraint.target = active_object.attach_to 33 | if self.snap: 34 | socket_world_location, socket_world_rotation, socket_world_scale = active_object.attach_to.matrix_world.decompose() 35 | active_object.matrix_world = Matrix.Translation(socket_world_location) @ socket_world_rotation.to_matrix().to_4x4() @ create_matrix_scale_from_vector(active_object.matrix_world.to_scale()) 36 | else: 37 | # detach 38 | constraint = active_object.constraints.get('attach_to') 39 | if constraint: 40 | active_object.constraints.remove(constraint) 41 | 42 | self.report({'INFO'}, 'Detach Object Success' if is_attach_to_object else 'Attach Object Success') 43 | 44 | return {'FINISHED'} 45 | 46 | class OP_CreateSocket(Operator): 47 | bl_idname = 'ue4workspace.create_socket' 48 | bl_label = 'Create Socket' 49 | bl_description = 'Create socket for attach object' 50 | bl_options = {'UNDO', 'REGISTER'} 51 | 52 | socket_name: bpy.props.StringProperty( 53 | name='Name', 54 | default='name_socket' 55 | ) 56 | 57 | size: bpy.props.FloatProperty( 58 | name='Size', 59 | min=0.01, 60 | default=1 61 | ) 62 | 63 | rotation: bpy.props.FloatVectorProperty( 64 | name='Rotation', 65 | subtype='XYZ', 66 | unit='ROTATION', 67 | default=[0, 0, 0] 68 | ) 69 | 70 | @classmethod 71 | def poll(cls, context): 72 | return context.active_object is not None and context.active_object.type in ['ARMATURE', 'MESH'] 73 | 74 | def execute(self, context): 75 | is_armature = context.active_object.type == 'ARMATURE' and context.mode in ['POSE', 'EDIT_ARMATURE'] 76 | bone = None 77 | if is_armature: 78 | case_bone = { 79 | 'EDIT_ARMATURE': context.active_bone, 80 | 'POSE': context.active_pose_bone 81 | } 82 | bone = case_bone.get(context.mode) 83 | 84 | # create collection (UE4Socket) if not exist 85 | collection = bpy.data.collections.get('UE4Socket', False) 86 | if (not collection): 87 | collection = bpy.data.collections.new('UE4Socket') 88 | context.scene.collection.children.link(collection) 89 | 90 | socket = bpy.data.objects.new(name=self.socket_name, object_data=None) 91 | socket.is_socket = True 92 | socket.rotation_euler = self.rotation 93 | socket.location = context.scene.cursor.location 94 | socket.show_name = True 95 | socket.empty_display_type = 'ARROWS' 96 | socket.empty_display_size = self.size 97 | collection.objects.link(socket) 98 | socket.parent = context.active_object 99 | if is_armature and bone: 100 | socket.parent_type = 'BONE' 101 | socket.parent_bone = bone.name 102 | # clear Local Transform 103 | socket.matrix_parent_inverse = (context.active_object.matrix_world @ Matrix.Translation(bone.tail - bone.head) @ bone.matrix).inverted() 104 | else: 105 | socket.parent_type = 'OBJECT' 106 | # clear Local Transform 107 | socket.matrix_parent_inverse = context.active_object.matrix_world.inverted() 108 | 109 | return {'FINISHED'} 110 | 111 | class OP_CopySocket(Operator): 112 | bl_idname = 'ue4workspace.copy_socket' 113 | bl_label = 'Copy Socket' 114 | bl_description = 'Copy socket for unreal engine skeleton' 115 | bl_options = {'UNDO', 'REGISTER'} 116 | 117 | @classmethod 118 | def poll(self, context): 119 | return context.mode == 'OBJECT' and context.active_object is not None and context.active_object.type == 'ARMATURE' 120 | 121 | def execute(self, context): 122 | active_object = context.active_object 123 | 124 | socket_bone_objects = [obj for obj in active_object.children if obj.type == 'EMPTY' and obj.is_socket and obj.parent_type == 'BONE' and obj.parent_bone] 125 | 126 | string_clipboard = 'SocketCopyPasteBuffer\n\nNumSockets={}\n\n'.format(len(socket_bone_objects)) 127 | 128 | for index, socket_obj in enumerate(socket_bone_objects): 129 | 130 | pose_bone_target = active_object.pose.bones.get(socket_obj.parent_bone) 131 | 132 | if pose_bone_target is not None: 133 | socket_point_location, socket_point_rotation_quaternion, socket_point_scale = ((active_object.matrix_world @ pose_bone_target.matrix).inverted() @ socket_obj.matrix_world.copy()).decompose() 134 | socket_point_rotation_euler = socket_point_rotation_quaternion.to_euler('XYZ') 135 | 136 | string_clipboard += 'IsOnSkeleton=1\nBegin Object Class=/Script/Engine.SkeletalMeshSocket Name=\"SkeletalMeshSocket_{index}\"\nSocketName=\"{socket_name}\"\nBoneName=\"{bone_name}\"\nRelativeLocation=(X={location[x]},Y={location[y]},Z={location[z]})\nRelativeRotation=(Pitch={rotation[y]},Yaw={rotation[z]},Roll={rotation[x]})\nRelativeScale=(X={scale[x]},Y={scale[y]},Z={scale[z]})\nEnd Object\n\n'.format( 137 | index = index, 138 | socket_name = socket_obj.name, 139 | bone_name = socket_obj.parent_bone, 140 | location = { 141 | "x": socket_point_location.x, 142 | "y": socket_point_location.y * -1, 143 | "z": socket_point_location.z 144 | }, 145 | rotation = { 146 | "x": math.degrees(socket_point_rotation_euler.x), 147 | "y": math.degrees(socket_point_rotation_euler.y * -1), 148 | "z": math.degrees(socket_point_rotation_euler.z * -1) 149 | }, 150 | scale = { 151 | "x": float(socket_point_scale.x / 100), 152 | "y": float(socket_point_scale.y / 100), 153 | "z": float(socket_point_scale.z / 100) 154 | } 155 | ) 156 | 157 | context.window_manager.clipboard = string_clipboard 158 | 159 | self.report({'INFO'}, 'Copy socket success') 160 | 161 | return {"FINISHED"} 162 | 163 | class SocketObject(object): 164 | _temp_dict = { 165 | 'SocketName': 'Socket', 166 | 'BoneName': 'Bone', 167 | 'RelativeLocation': 'X=0.0,Y=0.0,Z=0.0', 168 | 'RelativeRotation': 'Pitch=0.0,Yaw=0.0,Roll=0.0', 169 | 'RelativeScale': 'X=1.0,Y=1.0,Z=1.0' 170 | } 171 | 172 | SocketName = 'Socket' 173 | BoneName = 'Bone' 174 | RelativeLocation = {} 175 | RelativeRotation = {} 176 | RelativeScale = {} 177 | 178 | def __init__(self, **bone): 179 | self._temp_dict.update(bone) 180 | self.update_transform() 181 | for name in ['SocketName', 'BoneName']: 182 | setattr(self, name, self._temp_dict[name]) 183 | 184 | def update_transform(self): 185 | def serialize(key: str): 186 | dict_value = {key: float(val) for key, val in [temp_string.strip().split('=', 1) for temp_string in self._temp_dict[key].split(',')]} 187 | setattr(self, name, dict_value) 188 | for name in ['RelativeLocation', 'RelativeRotation', 'RelativeScale']: 189 | serialize(name) 190 | 191 | self.RelativeLocation['Y'] *= -1 192 | 193 | self.RelativeRotation['Pitch'] *= -1 194 | self.RelativeRotation['Yaw'] *= -1 195 | 196 | class OP_PasteSocket(Operator): 197 | bl_idname = 'ue4workspace.paste_socket' 198 | bl_label = 'Paste Socket' 199 | bl_description = 'Paste socket from unreal engine skeleton' 200 | bl_options = {'UNDO', 'REGISTER'} 201 | 202 | size: bpy.props.FloatProperty( 203 | name='Size', 204 | min=0.01, 205 | default=0.1 206 | ) 207 | 208 | @classmethod 209 | def poll(self, context): 210 | return context.mode == 'OBJECT' and context.active_object is not None and context.active_object.type == 'ARMATURE' 211 | 212 | def execute(self, context): 213 | active_object = context.active_object 214 | clipboard = context.window_manager.clipboard 215 | num_socket_pasted = 0 216 | 217 | if clipboard and ('Begin Object Class=/Script/Engine.SkeletalMeshSocket' in clipboard) and ('End Object' in clipboard): 218 | clipboard_list = clipboard.splitlines() 219 | socket_objects = [] 220 | 221 | for index in [index for index, string in enumerate(clipboard_list) if string.strip().startswith('Begin Object Class=/Script/Engine.SkeletalMeshSocket Name="SkeletalMeshSocket_')]: 222 | socket_dict = {} 223 | loop = True 224 | index_loop = index + 1 225 | while(loop): 226 | string = clipboard_list[index_loop] 227 | loop = not string.strip().startswith('End Object') 228 | if loop: 229 | index_loop += 1 230 | key, value = string.strip().split('=', 1) 231 | socket_dict[key] = value.strip('"').strip('()') 232 | socket_objects.append(SocketObject(**socket_dict)) 233 | 234 | # create collection (UE4Socket) if not exist 235 | collection = bpy.data.collections.get('UE4Socket', False) 236 | if (not collection): 237 | collection = bpy.data.collections.new('UE4Socket') 238 | context.scene.collection.children.link(collection) 239 | 240 | for socket_object in socket_objects: 241 | pose_bone = active_object.pose.bones.get(socket_object.BoneName, None) 242 | if pose_bone is not None: 243 | socket_matrix_world = (active_object.matrix_world @ pose_bone.matrix) @ (Matrix.Translation((socket_object.RelativeLocation['X'], socket_object.RelativeLocation['Y'], socket_object.RelativeLocation['Z'])) @ Euler((math.radians(socket_object.RelativeRotation['Roll']), math.radians(socket_object.RelativeRotation['Pitch']), math.radians(socket_object.RelativeRotation['Yaw'])), 'XYZ').to_matrix().to_4x4()) 244 | 245 | socket = bpy.data.objects.new(name=socket_object.SocketName, object_data=None) 246 | socket.is_socket = True 247 | socket.show_name = True 248 | socket.matrix_world = socket_matrix_world 249 | socket.scale.x = (socket_object.RelativeScale['X'] * (active_object.scale.x/0.01)) 250 | socket.scale.y = (socket_object.RelativeScale['Y'] * (active_object.scale.y/0.01)) 251 | socket.scale.z = (socket_object.RelativeScale['Z'] * (active_object.scale.z/0.01)) 252 | socket.empty_display_type = 'ARROWS' 253 | socket.empty_display_size = self.size 254 | collection.objects.link(socket) 255 | socket.parent = active_object 256 | socket.parent_type = 'BONE' 257 | socket.parent_bone = pose_bone.name 258 | # clear Local Transform 259 | socket.matrix_parent_inverse = (active_object.matrix_world @ Matrix.Translation(pose_bone.tail - pose_bone.head) @ pose_bone.matrix).inverted() 260 | 261 | num_socket_pasted += 1 262 | 263 | self.report({'INFO'}, f'Paste {num_socket_pasted} socket success') 264 | 265 | return {"FINISHED"} 266 | 267 | class PANEL(ObjectSubPanel): 268 | bl_idname = 'UE4WORKSPACE_PT_ObjectSocketPanel' 269 | bl_label = 'Socket' 270 | 271 | @classmethod 272 | def poll(cls, context): 273 | return context.active_object is not None and context.active_object.type in ['ARMATURE', 'MESH'] 274 | 275 | def draw(self, context): 276 | layout = self.layout 277 | preferences = context.preferences.addons['UE4Workspace'].preferences 278 | active_object = context.active_object 279 | 280 | col = layout.box().column() 281 | split = col.split(factor=0.6) 282 | col = split.column() 283 | col.alignment = 'RIGHT' 284 | col.label(text='Attach to') 285 | col = split.column() 286 | row = col.row() 287 | row.enabled = not active_object.is_attach_to_object 288 | row.prop(active_object, 'attach_to', text='', icon='EMPTY_ARROWS') 289 | row = col.row() 290 | row.scale_y = 1.5 291 | row.operator('ue4workspace.attach_object',icon='CON_PIVOT', text=('Detach' if active_object.is_attach_to_object else 'Attach')) 292 | 293 | row = layout.box().row() 294 | row.scale_y = 1.5 295 | row.operator('ue4workspace.create_socket',icon='EMPTY_ARROWS') 296 | 297 | socket_objects = [obj for obj in context.scene.objects if obj.type == 'EMPTY' and obj.is_socket and obj.parent is active_object] 298 | 299 | if active_object.type == 'ARMATURE': 300 | box = layout.box() 301 | col = box.column(align=True) 302 | col.scale_y = 1.5 303 | col.operator('ue4workspace.paste_socket',icon='DECORATE_ANIMATE', text='Paste Socket') 304 | if socket_objects: 305 | col.operator('ue4workspace.copy_socket',icon='DECORATE_ANIMATE', text='Copy Socket') 306 | 307 | if socket_objects: 308 | for obj in socket_objects: 309 | box = layout.box() 310 | 311 | col = box.column() 312 | split = col.split(factor=0.6) 313 | col = split.column() 314 | col.prop(obj, 'name', text='') 315 | row = split.row() 316 | row.alignment = 'RIGHT' 317 | row.operator('ue4workspace.toggle_visibility_object', icon=('HIDE_ON' if obj.hide_get() else 'HIDE_OFF'), text='', emboss=False).object_name = obj.name 318 | row.operator('ue4workspace.remove_object', icon='TRASH', text='', emboss=False).object_name = obj.name 319 | 320 | col = box.column() 321 | split = col.split(factor=0.6) 322 | col = split.column() 323 | col.alignment = 'RIGHT' 324 | col.label(text='Size') 325 | col = split.column() 326 | col.prop(obj, 'empty_display_size', text='') 327 | 328 | col = box.column() 329 | split = col.split(factor=0.6) 330 | col = split.column() 331 | col.alignment = 'RIGHT' 332 | col.label(text='Show Name') 333 | col = split.column() 334 | col.prop(obj, 'show_name', text='') 335 | 336 | # socket parent bone for character 337 | if obj.parent_type == 'BONE': 338 | col = box.column() 339 | split = col.split(factor=0.6) 340 | col = split.column() 341 | col.alignment = 'RIGHT' 342 | col.label(text='Bone') 343 | col = split.column() 344 | col.prop_search(obj, 'parent_bone', active_object.data, 'bones', text='') 345 | 346 | list_class_to_register = [ 347 | OP_AttachObject, 348 | OP_CreateSocket, 349 | OP_CopySocket, 350 | OP_PasteSocket, 351 | PANEL 352 | ] 353 | 354 | def register(): 355 | bpy.types.Object.is_socket = bpy.props.BoolProperty( 356 | name='Is socket ?', 357 | description='socket ?', 358 | default=False 359 | ) 360 | 361 | bpy.types.Object.is_attach_to_object = bpy.props.BoolProperty( 362 | default=False 363 | ) 364 | 365 | bpy.types.Object.attach_to = bpy.props.PointerProperty( 366 | name='Attach to', 367 | description='Attach object to socket', 368 | type=bpy.types.Object, 369 | poll=lambda self, obj: obj.type == 'EMPTY' and obj.is_socket and obj.parent is not bpy.context.active_object 370 | ) 371 | 372 | for x in list_class_to_register: 373 | register_class(x) 374 | 375 | def unregister(): 376 | del bpy.types.Object.is_socket 377 | del bpy.types.Object.is_attach_to_object 378 | del bpy.types.Object.attach_to 379 | 380 | for x in list_class_to_register[::-1]: 381 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/preferences/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/preferences/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/preferences/animation.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | from .. utils.connect import skeletons 4 | 5 | class ANIMATION_FBX_export(PropertyGroup): 6 | 7 | tab_transform: bpy.props.BoolProperty( 8 | name='Transform', 9 | description='Transform Tab', 10 | default=False 11 | ) 12 | 13 | global_scale: bpy.props.FloatProperty( 14 | name='Scale', 15 | description='Scale all data (Some importers do not support scaled armatures!)', 16 | default=1.0, 17 | min=0.001, 18 | max=1000 19 | ) 20 | 21 | apply_unit_scale: bpy.props.EnumProperty( 22 | name='Apply Scalings', 23 | description='How to apply custom and units scalings in generated FBX file (Blender uses FBX scale to detect units on import, but many other applications do not handle the same way)', 24 | items=[ 25 | ('FBX_SCALE_NONE', 'All Local', 'Apply custom scaling and units scaling to each object transformation, FBX scale remains at 1.0'), 26 | ('FBX_SCALE_UNITS', 'FBX Units Scale', 'Apply custom scaling to each object transformation, and units scaling to FBX scale'), 27 | ('FBX_SCALE_CUSTOM', 'FBX Custom Scale', 'Apply custom scaling to FBX scale, and units scaling to each object transformation'), 28 | ('FBX_SCALE_ALL', 'FBX All', 'Apply custom scaling and units scaling to FBX scale') 29 | ], 30 | default='FBX_SCALE_NONE' 31 | ) 32 | 33 | axis_forward: bpy.props.EnumProperty( 34 | name='Forward', 35 | description='Forward', 36 | items=[ 37 | ('X', 'X Forward', 'X Forward'), 38 | ('Y', 'Y Forward', 'Y Forward'), 39 | ('Z', 'Z Forward', 'Z Forward'), 40 | ('-X', '-X Forward', '-X Forward'), 41 | ('-Y', '-Y Forward', '-Y Forward'), 42 | ('-Z', '-Z Forward', '-Z Forward') 43 | ], 44 | default='-Z' 45 | ) 46 | 47 | axis_up: bpy.props.EnumProperty( 48 | name='Up', 49 | description='Up', 50 | items=[ 51 | ('X', 'X Up', 'X Up'), 52 | ('Y', 'Y Up', 'Y Up'), 53 | ('Z', 'Z Up', 'Z Up'), 54 | ('-X', '-X Up', '-X Up'), 55 | ('-Y', '-Y Up', '-Y Up'), 56 | ('-Z', '-Z Up', '-Z Up') 57 | ], 58 | default='Y' 59 | ) 60 | 61 | apply_unit_scale: bpy.props.BoolProperty( 62 | name='Apply Unit', 63 | description='Take into account current Blender units settings (if unset, raw Blender Units values are used as-is)', 64 | default=True 65 | ) 66 | 67 | bake_space_transform: bpy.props.BoolProperty( 68 | name='!EXPERIMENTAL! Apply Transform', 69 | description='Bake space transform into object data, avoids getting unwanted rotations to objects when target space is not aligned with Blender’s space (WARNING! experimental option, use at own risks, known broken with armatures/animations)', 70 | default=False 71 | ) 72 | 73 | tab_armature: bpy.props.BoolProperty( 74 | name='Armature', 75 | description='Armature Tab', 76 | default=False 77 | ) 78 | 79 | primary_bone_axis: bpy.props.EnumProperty( 80 | name='Primary Bone Axis', 81 | description='Primary Bone Axis', 82 | items=[ 83 | ('X', 'X Axis', 'X Axis'), 84 | ('Y', 'Y Axis', 'Y Axis'), 85 | ('Z', 'Z Axis', 'Z Axis'), 86 | ('-X', '-X Axis', '-X Axis'), 87 | ('-Y', '-Y Axis', '-Y Axis'), 88 | ('-Z', '-Z Axis', '-Z Axis') 89 | ], 90 | default='Y' 91 | ) 92 | 93 | secondary_bone_axis: bpy.props.EnumProperty( 94 | name='Secondary Bone Axis', 95 | description='Secondary Bone Axis', 96 | items=[ 97 | ('X', 'X Axis', 'X Axis'), 98 | ('Y', 'Y Axis', 'Y Axis'), 99 | ('Z', 'Z Axis', 'Z Axis'), 100 | ('-X', '-X Axis', '-X Axis'), 101 | ('-Y', '-Y Axis', '-Y Axis'), 102 | ('-Z', '-Z Axis', '-Z Axis') 103 | ], 104 | default='X' 105 | ) 106 | 107 | armature_nodetype: bpy.props.EnumProperty( 108 | name='Armature FBXNode Type', 109 | description='FBX type of node (object) used to represent Blender’s armatures (use Null one unless you experience issues with other app, other choices may no import back perfectly in Blender…)', 110 | items=[ 111 | ('NULL', 'Null', '‘Null’ FBX node, similar to Blender’s Empty (default)'), 112 | ('ROOT', 'Root', '‘Root’ FBX node, supposed to be the root of chains of bones'), 113 | ('LIMBNODE', 'LimbNode', '‘LimbNode’ FBX node, regular joint between two bones') 114 | ], 115 | default='NULL' 116 | ) 117 | 118 | use_armature_deform_only: bpy.props.BoolProperty( 119 | name='Only Deform Bones', 120 | description='Only write deforming bones (and non-deforming ones when they have deforming children)', 121 | default=False 122 | ) 123 | 124 | add_leaf_bones: bpy.props.BoolProperty( 125 | name='Add Leaf Bones', 126 | description='Only write deforming bones (and non-deforming ones when they have deforming children)', 127 | default=True 128 | ) 129 | 130 | tab_bake_animation: bpy.props.BoolProperty( 131 | name='Bake Animation', 132 | description='Bake Animation Tab', 133 | default=False 134 | ) 135 | 136 | bake_anim_use_all_bones: bpy.props.BoolProperty( 137 | name='Key All Bones', 138 | description='Force exporting at least one key of animation for all bones (needed with some target applications, like UE4)', 139 | default=True 140 | ) 141 | 142 | bake_anim_force_startend_keying: bpy.props.BoolProperty( 143 | name='Force Start/End Keying', 144 | description='Always add a keyframe at start and end of actions for animated channels', 145 | default=True 146 | ) 147 | 148 | bake_anim_step: bpy.props.FloatProperty( 149 | name='Sampling Rate', 150 | description='How often to evaluate animated values (in frames)', 151 | default=1.0, 152 | min=0.01, 153 | max=100 154 | ) 155 | 156 | bake_anim_simplify_factor: bpy.props.FloatProperty( 157 | name='Simplify', 158 | description='How much to simplify baked values (0.0 to disable, the higher the more simplified)', 159 | default=1.0, 160 | min=0, 161 | max=100 162 | ) 163 | 164 | def to_dict(self): 165 | return {prop: getattr(self, prop, None) for prop in ['global_scale', 'apply_unit_scale', 'axis_forward', 'axis_up', 'apply_unit_scale', 'bake_space_transform', 'primary_bone_axis', 'secondary_bone_axis', 'armature_nodetype', 'use_armature_deform_only', 'add_leaf_bones', 'bake_anim_use_all_bones', 'bake_anim_force_startend_keying', 'bake_anim_step', 'bake_anim_simplify_factor']} 166 | 167 | class ANIMATION_unreal_engine_export(PropertyGroup): 168 | 169 | tab_animation: bpy.props.BoolProperty( 170 | name='Animation', 171 | description='Animation Tab', 172 | default=False 173 | ) 174 | 175 | animation_length: bpy.props.EnumProperty( 176 | name='Animation Length', 177 | description='Which animation range to import. The one defined at Exported, at Animated time or define a range manually', 178 | items=[ 179 | ('FBXALIT_EXPORTED_TIME', 'Exported Time', 'This option imports animation frames based on what is defined at the time of export'), 180 | ('FBXALIT_ANIMATED_KEY', 'Animated Time', 'Will import the range of frames that have animation. Can be useful if the exported range is longer than the actual animation in the FBX file'), 181 | ('FBXALIT_SET_RANGE', 'Set Range', 'This will enable the Start Frame and End Frame properties for you to define the frames of animation to import') 182 | ], 183 | default='FBXALIT_EXPORTED_TIME' 184 | ) 185 | 186 | import_meshes_in_bone_hierarchy: bpy.props.BoolProperty( 187 | name='Import Meshes in Bone Hierarchy', 188 | description='If checked, meshes nested in bone hierarchies will be imported instead of being converted to bones', 189 | default=True 190 | ) 191 | 192 | def update_frame_import_range(self, context): 193 | range_min, range_max = self.frame_import_range 194 | if range_min > range_max: 195 | self.frame_import_range[1] = range_min 196 | if range_max < range_min: 197 | self.frame_import_range[0] = range_max 198 | 199 | frame_import_range: bpy.props.IntVectorProperty( 200 | name='Frame Import Range', 201 | description='Frame range used when Set Range is used in Animation Length', 202 | default=[0, 0], 203 | size=2, 204 | update=update_frame_import_range 205 | ) 206 | 207 | use_default_sample_rate: bpy.props.BoolProperty( 208 | name='Use Default Sample Rate', 209 | description='If enabled, samples all animation curves to 30 FPS', 210 | default=False 211 | ) 212 | 213 | custom_sample_rate: bpy.props.IntProperty( 214 | name='Custom Sample Rate', 215 | description='Sample fbx animation data at the specified sample rate, 0 find automaticaly the best sample rate', 216 | default=0, 217 | min=0, 218 | max=60 219 | ) 220 | 221 | import_custom_attribute: bpy.props.BoolProperty( 222 | name='Import Custom Attribute', 223 | description='Import if custom attribute as a curve within the animation', 224 | default=True 225 | ) 226 | 227 | delete_existing_custom_attribute_curves: bpy.props.BoolProperty( 228 | name='Delete Existing Custom Attribute Curves', 229 | description='If true, all previous custom attribute curves will be deleted when doing a re-import', 230 | default=False 231 | ) 232 | 233 | import_bone_tracks: bpy.props.BoolProperty( 234 | name='Import Bone Tracks', 235 | description='Import bone transform tracks. If false, this will discard any bone transform tracks. (useful for curves only animations)', 236 | default=True 237 | ) 238 | 239 | set_material_drive_parameter_on_custom_attribute: bpy.props.BoolProperty( 240 | name='Set Material Curve Type', 241 | description='Set Material Curve Type for all custom attributes that exists', 242 | default=False 243 | ) 244 | 245 | material_curve_suffixes: bpy.props.StringProperty( 246 | name='Material Curve Suffixes', 247 | description='Set Material Curve Type for the custom attribute with the following suffixes. This doesn’t matter if Set Material Curve Type is true', 248 | default='_mat' 249 | ) 250 | 251 | remove_redundant_keys: bpy.props.BoolProperty( 252 | name='Remove Redundant Keys', 253 | description='When importing custom attribute as curve, remove redundant keys', 254 | default=True 255 | ) 256 | 257 | delete_existing_morph_target_curves: bpy.props.BoolProperty( 258 | name='Delete Existing Morph Target Curves', 259 | description='If enabled, this will delete this type of asset from the FBX', 260 | default=False 261 | ) 262 | 263 | do_not_import_curve_with_zero: bpy.props.BoolProperty( 264 | name='Do not Import Curve With 0 Values', 265 | description='When importing custom attribute or morphtarget as curve, do not import if it doens’t have any value other than zero. This is to avoid adding extra curves to evaluate', 266 | default=True 267 | ) 268 | 269 | preserve_local_transform: bpy.props.BoolProperty( 270 | name='Preserve Local Transform', 271 | description='If enabled, this will import a curve within the animation', 272 | default=False 273 | ) 274 | 275 | tab_transform: bpy.props.BoolProperty( 276 | name='Transform', 277 | description='Transform Tab', 278 | default=False 279 | ) 280 | 281 | import_translation: bpy.props.FloatVectorProperty( 282 | name='Import Translation', 283 | description='Import Translation', 284 | subtype='XYZ', 285 | default=(0.0, 0.0, 0.0) 286 | ) 287 | 288 | import_rotation: bpy.props.FloatVectorProperty( 289 | name='Import Rotation', 290 | description='Import Rotation', 291 | subtype='XYZ', 292 | default=(0.0, 0.0, 0.0) 293 | ) 294 | 295 | import_uniform_scale: bpy.props.FloatProperty( 296 | name='Import Uniform Scale', 297 | description='Import Uniform Scale', 298 | default=1.0 299 | ) 300 | 301 | tab_misc: bpy.props.BoolProperty( 302 | name='Misc.', 303 | description='Miscellaneous Tab', 304 | default=False 305 | ) 306 | 307 | convert_scene: bpy.props.BoolProperty( 308 | name='Convert Scene', 309 | description='Convert the scene from FBX coordinate system to UE4 coordinate system', 310 | default=True 311 | ) 312 | 313 | force_front_x_axis: bpy.props.BoolProperty( 314 | name='Force Front XAxis', 315 | description='Convert the scene from FBX coordinate system to UE4 coordinate system with front X axis instead of -Y', 316 | default=False 317 | ) 318 | 319 | convert_scene_unit: bpy.props.BoolProperty( 320 | name='Convert Scene Unit', 321 | description='Convert the scene from FBX unit to UE4 unit (centimeter)', 322 | default=False 323 | ) 324 | 325 | override_full_name: bpy.props.BoolProperty( 326 | name='Override Full Name', 327 | description='Use the string in “Name” field as full name of mesh. The option only works when the scene contains one mesh', 328 | default=True 329 | ) 330 | 331 | def to_dict(self): 332 | return {prop: list(getattr(self, prop, [])) if prop in ['frame_import_range', 'import_translation', 'import_rotation'] else ((self.material_curve_suffixes.split('|') if bool(self.material_curve_suffixes) else []) if prop == 'material_curve_suffixes' else getattr(self, prop, None)) for prop in [ 333 | 'animation_length', 334 | 'import_meshes_in_bone_hierarchy', 335 | 'frame_import_range', 336 | 'use_default_sample_rate', 337 | 'custom_sample_rate', 338 | 'import_custom_attribute', 339 | 'delete_existing_custom_attribute_curves', 340 | 'import_bone_tracks', 341 | 'set_material_drive_parameter_on_custom_attribute', 342 | 'material_curve_suffixes', 343 | 'remove_redundant_keys', 344 | 'delete_existing_morph_target_curves', 345 | 'do_not_import_curve_with_zero', 346 | 'preserve_local_transform', 347 | 348 | 'import_translation', 349 | 'import_rotation', 350 | 'import_uniform_scale', 351 | 352 | 'convert_scene', 353 | 'force_front_x_axis', 354 | 'convert_scene_unit', 355 | 'override_full_name' 356 | ]} 357 | 358 | class ANIMATION_export(PropertyGroup): 359 | 360 | subfolder: bpy.props.StringProperty( 361 | name='Subfolder', 362 | description='Subfolder for skeletal mesh export folder, leave it blank if you want to export to root project folder', 363 | default='' 364 | ) 365 | 366 | overwrite_file: bpy.props.BoolProperty( 367 | name='Overwrite File', 368 | description='Overwrite file if filename exist, if false will not export', 369 | default=True 370 | ) 371 | 372 | apply_rotation: bpy.props.BoolProperty( 373 | name='Apply Rotation', 374 | description='Apply local rotation object', 375 | default=True 376 | ) 377 | 378 | use_custom_props: bpy.props.BoolProperty( 379 | name='Custom Properties', 380 | description='Export custom properties', 381 | default=False 382 | ) 383 | 384 | root_bone: bpy.props.EnumProperty( 385 | name='Skeletal Mesh Root', 386 | description='Skeletal Mesh Root Bone', 387 | items=[ 388 | ('ARMATURE', 'Armature Hierarchy', 'Use original armature hierarchy'), 389 | ('AUTO', 'Auto Add', 'Auto add root bone if does not exist on the armature otherwise will use armature hierarchy'), 390 | ('OBJECT', 'Object Name', 'Use object name as root bone') 391 | ], 392 | default='ARMATURE' 393 | ) 394 | 395 | origin: bpy.props.EnumProperty( 396 | name='Skeletal Mesh Origin', 397 | description='Skeletal Mesh Origin', 398 | items=[ 399 | ('OBJECT', 'Object', 'Use object origin'), 400 | ('SCENE', 'Scene', 'Use scene origin') 401 | ], 402 | default='OBJECT' 403 | ) 404 | 405 | def get_skeleton(self, context): 406 | return [('NONE', 'None', 'None')] + skeletons 407 | 408 | skeleton: bpy.props.EnumProperty( 409 | name='Skeleton', 410 | description='Skeleton', 411 | items=get_skeleton, 412 | default=None 413 | ) 414 | 415 | fbx: bpy.props.PointerProperty( 416 | type=ANIMATION_FBX_export 417 | ) 418 | 419 | unreal_engine: bpy.props.PointerProperty( 420 | type=ANIMATION_unreal_engine_export 421 | ) -------------------------------------------------------------------------------- /UE4Workspace/preferences/connect_unreal_engine.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | 4 | class CONNECT_unreal_engine(PropertyGroup): 5 | 6 | main_folder: bpy.props.StringProperty( 7 | name='Main Folder', 8 | default='Blender' 9 | ) 10 | 11 | multicast_group_end_point: bpy.props.StringProperty( 12 | name='Multicast Group Endpoint', 13 | default='239.0.0.1:6766' 14 | ) 15 | 16 | multicast_bind_address: bpy.props.StringProperty( 17 | name='Multicast Bind Address', 18 | default='0.0.0.0' 19 | ) 20 | 21 | multicast_ttl: bpy.props.IntProperty( 22 | name='Multicast Time-To-Live', 23 | default=0 24 | ) 25 | 26 | @classmethod 27 | def draw_panel(cls, context, layout, preferences): 28 | box = layout.box() 29 | 30 | box.label(text='Remote Execution', icon='FRAME_NEXT') 31 | 32 | data_properties = [ 33 | (preferences.connect_unreal_engine, 'Main Folder', 'main_folder'), 34 | (preferences.connect_unreal_engine, 'Multicast Group Endpoint', 'multicast_group_end_point'), 35 | (preferences.connect_unreal_engine, 'Multicast Bind Address', 'multicast_bind_address'), 36 | (preferences.connect_unreal_engine, 'Multicast Time-To-Live', 'multicast_ttl'), 37 | ] 38 | 39 | for data, label_str, property_str in data_properties: 40 | row = box.row() 41 | split = row.split(factor=0.5) 42 | col = split.column() 43 | col.alignment = 'LEFT' 44 | col.label(text=label_str, icon='DECORATE') 45 | col = split.column() 46 | col.prop(data, property_str, text='') 47 | -------------------------------------------------------------------------------- /UE4Workspace/preferences/export_option.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | 4 | class EXPORT_option(PropertyGroup): 5 | 6 | type: bpy.props.EnumProperty( 7 | name='Export Type', 8 | description='Select the way you want export', 9 | items=[ 10 | ('FILE', 'To File', 'Export as a file'), 11 | ('UNREAL', 'To Unreal Engine', 'Export directly to Unreal Engine project'), 12 | ('BOTH', 'To File and Unreal Engine', 'Export as a file and directly export to Unreal Engine project') 13 | ], 14 | default='BOTH' 15 | ) 16 | 17 | export_folder: bpy.props.StringProperty( 18 | name='Export Folder', 19 | description='Folder to export, must have write permissions', 20 | default='', 21 | maxlen=1024, 22 | subtype='DIR_PATH' 23 | ) 24 | 25 | temp_folder: bpy.props.StringProperty( 26 | name='Temporary Folder', 27 | description='Temporary folder for export, must have write permissions', 28 | default='', 29 | maxlen=1024, 30 | subtype='DIR_PATH' 31 | ) 32 | 33 | project_list: bpy.props.BoolProperty( 34 | name='Project List', 35 | description='Project List Tab', 36 | default=False 37 | ) -------------------------------------------------------------------------------- /UE4Workspace/preferences/groom.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | 4 | class GROOM_export(PropertyGroup): 5 | 6 | subfolder: bpy.props.StringProperty( 7 | name='Subfolder', 8 | description='Subfolder for groom export folder, leave it blank if you want to export to root project folder', 9 | default='' 10 | ) 11 | 12 | overwrite_file: bpy.props.BoolProperty( 13 | name='Overwrite File', 14 | description='Overwrite file if filename exist, if false will not export', 15 | default=True 16 | ) 17 | 18 | apply_rotation: bpy.props.BoolProperty( 19 | name='Apply Rotation', 20 | description='Apply local rotation object', 21 | default=True 22 | ) 23 | 24 | use_custom_props: bpy.props.BoolProperty( 25 | name='Custom Properties', 26 | description='Export custom properties', 27 | default=True 28 | ) 29 | 30 | option: bpy.props.EnumProperty( 31 | name='Export Groom Option', 32 | description='Export Groom Option', 33 | items=[ 34 | ('SELECT', 'Select', 'Export selected mesh with hair on scene'), 35 | ('ALL', 'All', 'Export all mesh with hair on scene') 36 | ], 37 | default='SELECT' 38 | ) 39 | 40 | origin: bpy.props.EnumProperty( 41 | name='Mesh Origin', 42 | description='Mesh Origin', 43 | items=[ 44 | ('OBJECT', 'Object', 'Use object origin'), 45 | ('SCENE', 'Scene', 'Use scene origin') 46 | ], 47 | default='OBJECT' 48 | ) -------------------------------------------------------------------------------- /UE4Workspace/preferences/import_asset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | 4 | class IMPORT_ASSET_fbx_import(PropertyGroup): 5 | 6 | tab_include: bpy.props.BoolProperty( 7 | name='Include', 8 | description='Include Tab', 9 | default=False 10 | ) 11 | 12 | tab_transform: bpy.props.BoolProperty( 13 | name='Transform', 14 | description='Transform Tab', 15 | default=False 16 | ) 17 | 18 | tab_orientation: bpy.props.BoolProperty( 19 | name='Orientation', 20 | description='Orientation Tab', 21 | default=False 22 | ) 23 | 24 | tab_animation: bpy.props.BoolProperty( 25 | name='Animation', 26 | description='Animation Tab', 27 | default=False 28 | ) 29 | 30 | tab_armature: bpy.props.BoolProperty( 31 | name='Armature', 32 | description='Armature Tab', 33 | default=False 34 | ) 35 | 36 | use_manual_orientation: bpy.props.BoolProperty( 37 | name='Manual Orientation', 38 | description='Specify orientation and scale, instead of using embedded data in FBX file', 39 | default=False 40 | ) 41 | 42 | global_scale: bpy.props.FloatProperty( 43 | name='Scale', 44 | description='Scale', 45 | default=1.0, 46 | min=0.001, 47 | max=1000.0 48 | ) 49 | 50 | bake_space_transform: bpy.props.BoolProperty( 51 | name='Apply Transform', 52 | description='Bake space transform into object data, avoids getting unwanted rotations to objects when target space is not aligned with Blender’s space (WARNING! experimental option, use at own risks, known broken with armatures/animations)', 53 | default=False 54 | ) 55 | 56 | use_custom_normals: bpy.props.BoolProperty( 57 | name='Custom Normals', 58 | description='if available (otherwise Blender will recompute them)', 59 | default=True 60 | ) 61 | 62 | use_image_search: bpy.props.BoolProperty( 63 | name='Image Search', 64 | description='Search subdirs for any associated images (WARNING: bpy.props.may be slow)', 65 | default=True 66 | ) 67 | 68 | use_alpha_decals: bpy.props.BoolProperty( 69 | name='Alpha Decals', 70 | description='Treat materials with alpha as decals (no shadow casting)', 71 | default=True 72 | ) 73 | 74 | decal_offset: bpy.props.FloatProperty( 75 | name='Decal Offset', 76 | description='Displace geometry of alpha meshes', 77 | default=0.0, 78 | min=0.0, 79 | max=1.0 80 | ) 81 | 82 | use_anim: bpy.props.BoolProperty( 83 | name='Animation', 84 | description='Import FBX animation', 85 | default=True 86 | ) 87 | 88 | anim_offset: bpy.props.FloatProperty( 89 | name='Animation Offset', 90 | description='Offset to apply to animation during import, in frames', 91 | default=1.0 92 | ) 93 | 94 | use_subsurf: bpy.props.BoolProperty( 95 | name='Subdivision Data', 96 | description='Import FBX subdivision information as subdivision surface modifiers', 97 | default=False 98 | ) 99 | 100 | use_custom_props: bpy.props.BoolProperty( 101 | name='Custom Properties', 102 | description='Import user properties as custom properties', 103 | default=True 104 | ) 105 | 106 | use_custom_props_enum_as_string: bpy.props.BoolProperty( 107 | name='Import Enums As Strings', 108 | description='Store enumeration values as strings', 109 | default=True 110 | ) 111 | 112 | ignore_leaf_bones: bpy.props.BoolProperty( 113 | name='Ignore Leaf Bones', 114 | description='Ignore the last bone at the end of each chain (used to mark the length of the previous bone)', 115 | default=False 116 | ) 117 | 118 | force_connect_children: bpy.props.BoolProperty( 119 | name='Force Connect Children', 120 | description='Force connection of children bones to their parent, even if their computed head/tail positions do not match (can be useful with pure-joints-type armatures)', 121 | default=False 122 | ) 123 | 124 | automatic_bone_orientation: bpy.props.BoolProperty( 125 | name='Automatic Bone Orientation', 126 | description='Try to align the major bone axis with the bone children', 127 | default=False 128 | ) 129 | 130 | primary_bone_axis: bpy.props.EnumProperty( 131 | name='Primary Bone Axis', 132 | description='', 133 | items=[ 134 | ('X', 'X Axis', ''), 135 | ('Y', 'Y Axis', ''), 136 | ('Z', 'Z Axis', ''), 137 | ('-X', '-X Axis', ''), 138 | ('-Y', '-Y Axis', ''), 139 | ('-Z', '-Z Axis', '') 140 | ], 141 | default='Y' 142 | ) 143 | 144 | secondary_bone_axis: bpy.props.EnumProperty( 145 | name='Secondary Bone Axis', 146 | description='', 147 | items=[ 148 | ('X', 'X Axis', ''), 149 | ('Y', 'Y Axis', ''), 150 | ('Z', 'Z Axis', ''), 151 | ('-X', '-X Axis', ''), 152 | ('-Y', '-Y Axis', ''), 153 | ('-Z', '-Z Axis', '') 154 | ], 155 | default='X' 156 | ) 157 | 158 | use_prepost_rot: bpy.props.BoolProperty( 159 | name='Use Pre/Post Rotation', 160 | description='Use pre/post rotation from FBX transform (you may have to disable that in some cases)', 161 | default=True 162 | ) 163 | 164 | axis_forward: bpy.props.EnumProperty( 165 | name='Axis Forward', 166 | description='Forward', 167 | items=[ 168 | ('X', 'X Forward', ''), 169 | ('Y', 'Y Forward', ''), 170 | ('Z', 'Z Forward', ''), 171 | ('-X', '-X Forward', ''), 172 | ('-Y', '-Y Forward', ''), 173 | ('-Z', '-Z Forward', '') 174 | ], 175 | default='-Z' 176 | ) 177 | 178 | axis_up: bpy.props.EnumProperty( 179 | name='Axis Up', 180 | description='Up', 181 | items=[ 182 | ('X', 'X Up', ''), 183 | ('Y', 'Y Up', ''), 184 | ('Z', 'Z Up', ''), 185 | ('-X', '-X Up', ''), 186 | ('-Y', '-Y Up', ''), 187 | ('-Z', '-Z Up', '') 188 | ], 189 | default='Y' 190 | ) 191 | 192 | def to_dict(self): 193 | return {prop: getattr(self, prop, None) for prop in [ 194 | 'use_manual_orientation', 195 | 'global_scale', 196 | 'bake_space_transform', 197 | 'use_custom_normals', 198 | 'use_image_search', 199 | 'use_alpha_decals', 200 | 'decal_offset', 201 | 'use_anim', 202 | 'anim_offset', 203 | 'use_subsurf', 204 | 'use_custom_props', 205 | 'use_custom_props_enum_as_string', 206 | 'ignore_leaf_bones', 207 | 'force_connect_children', 208 | 'automatic_bone_orientation', 209 | 'primary_bone_axis', 210 | 'secondary_bone_axis', 211 | 'use_prepost_rot', 212 | 'axis_forward', 213 | 'axis_up' 214 | ]} 215 | 216 | class IMPORT_ASSET_unreal_engine(PropertyGroup): 217 | 218 | tab_exporter: bpy.props.BoolProperty( 219 | name='Exporter', 220 | description='Exporter Tab', 221 | default=False 222 | ) 223 | 224 | fbx_export_compatibility: bpy.props.EnumProperty( 225 | name='FBX Export Compatibility', 226 | description='This will set the fbx sdk compatibility when exporting to fbx file. The default value is 2013', 227 | items=[ 228 | ('FBX_2011', '2011', 'FBX 2011'), 229 | ('FBX_2012', '2012', 'FBX 2012'), 230 | ('FBX_2013', '2013', 'FBX 2013'), 231 | ('FBX_2014', '2014', 'FBX 2014'), 232 | ('FBX_2016', '2016', 'FBX 2016'), 233 | ('FBX_2018', '2018', 'FBX 2018') 234 | ], 235 | default='FBX_2013' 236 | ) 237 | 238 | ascii: bpy.props.BoolProperty( 239 | name='ASCII', 240 | description='If enabled, save as ascii instead of binary', 241 | default=False 242 | ) 243 | 244 | force_front_x_axis: bpy.props.BoolProperty( 245 | name='Force Front X Axis', 246 | description='If enabled, export with X axis as the front axis instead of default -Y', 247 | default=False 248 | ) 249 | 250 | tab_mesh: bpy.props.BoolProperty( 251 | name='Mesh', 252 | description='Mesh Tab', 253 | default=False 254 | ) 255 | 256 | vertex_color: bpy.props.BoolProperty( 257 | name='Vertex Color', 258 | description='If enabled, export vertex color', 259 | default=True 260 | ) 261 | 262 | level_of_detail: bpy.props.BoolProperty( 263 | name='Level Of Detail', 264 | description='If enabled, export the level of detail', 265 | default=True 266 | ) 267 | 268 | tab_static_mesh: bpy.props.BoolProperty( 269 | name='Static Mesh', 270 | description='Static Mesh Tab', 271 | default=False 272 | ) 273 | 274 | collision: bpy.props.BoolProperty( 275 | name='Collision', 276 | description='If enabled, export collision', 277 | default=True 278 | ) 279 | 280 | tab_skeletal_mesh: bpy.props.BoolProperty( 281 | name='Skeletal Mesh', 282 | description='Skeletal Mesh Tab', 283 | default=False 284 | ) 285 | 286 | export_morph_targets: bpy.props.BoolProperty( 287 | name='Export Morph Targets', 288 | description='If enabled, export the morph targets', 289 | default=True 290 | ) 291 | 292 | tab_animation: bpy.props.BoolProperty( 293 | name='Animation', 294 | description='Animation Tab', 295 | default=False 296 | ) 297 | 298 | export_preview_mesh: bpy.props.BoolProperty( 299 | name='Export Preview Mesh', 300 | description='If enable, the preview mesh link to the exported animations will be also exported', 301 | default=False 302 | ) 303 | 304 | map_skeletal_motion_to_root: bpy.props.BoolProperty( 305 | name='Map Skeletal Motion To Root', 306 | description='If enable, Map skeletal actor motion to the root bone of the skeleton', 307 | default=False 308 | ) 309 | 310 | export_local_time: bpy.props.BoolProperty( 311 | name='Export Local Time', 312 | description='If enabled, export sequencer animation in its local time, relative to its master sequence', 313 | default=True 314 | ) 315 | 316 | def to_dict(self): 317 | return {prop: getattr(self, prop, None) for prop in [ 318 | 'fbx_export_compatibility', 319 | 'ascii', 320 | 'force_front_x_axis', 321 | 'vertex_color', 322 | 'level_of_detail', 323 | 'collision', 324 | 'export_morph_targets', 325 | 'export_preview_mesh', 326 | 'map_skeletal_motion_to_root', 327 | 'export_local_time', 328 | ]} 329 | 330 | class PG_IMPORT_ASSET(PropertyGroup): 331 | 332 | fbx: bpy.props.PointerProperty( 333 | type=IMPORT_ASSET_fbx_import 334 | ) 335 | 336 | unreal_engine: bpy.props.PointerProperty( 337 | type=IMPORT_ASSET_unreal_engine 338 | ) -------------------------------------------------------------------------------- /UE4Workspace/preferences/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import AddonPreferences, PropertyGroup 4 | 5 | from . export_option import EXPORT_option 6 | from . connect_unreal_engine import CONNECT_unreal_engine 7 | from . import_asset import IMPORT_ASSET_fbx_import, IMPORT_ASSET_unreal_engine, PG_IMPORT_ASSET 8 | from . static_mesh import STATIC_MESH_FBX_export, STATIC_MESH_unreal_engine_export, STATIC_MESH_export 9 | from . skeletal_mesh import SKELETAL_MESH_FBX_export, SKELETAL_MESH_unreal_engine_export, SKELETAL_MESH_export 10 | from . animation import ANIMATION_FBX_export, ANIMATION_unreal_engine_export, ANIMATION_export 11 | from . groom import GROOM_export 12 | from . misc import MISC_option 13 | 14 | class Preferences(AddonPreferences): 15 | bl_idname = 'UE4Workspace' 16 | 17 | export: bpy.props.PointerProperty( 18 | type=EXPORT_option 19 | ) 20 | 21 | connect_unreal_engine: bpy.props.PointerProperty( 22 | type=CONNECT_unreal_engine 23 | ) 24 | 25 | import_asset: bpy.props.PointerProperty( 26 | type=PG_IMPORT_ASSET 27 | ) 28 | 29 | static_mesh: bpy.props.PointerProperty( 30 | type=STATIC_MESH_export 31 | ) 32 | 33 | skeletal_mesh: bpy.props.PointerProperty( 34 | type=SKELETAL_MESH_export 35 | ) 36 | 37 | animation: bpy.props.PointerProperty( 38 | type=ANIMATION_export 39 | ) 40 | 41 | groom: bpy.props.PointerProperty( 42 | type=GROOM_export 43 | ) 44 | 45 | misc: bpy.props.PointerProperty( 46 | type=MISC_option 47 | ) 48 | 49 | active_tab_addon: bpy.props.EnumProperty( 50 | name='Tab', 51 | description='Tab setting', 52 | items=[ 53 | ('CONNECT', 'Connect To Unreal Engine', ''), 54 | ('MISC', 'Misc.', ''), 55 | ], 56 | default='CONNECT' 57 | ) 58 | 59 | def draw(self, context): 60 | layout = self.layout 61 | 62 | layout.row().prop(self, 'active_tab_addon', expand=True) 63 | 64 | draw_preferences_tab = { 65 | 'CONNECT': CONNECT_unreal_engine.draw_panel, 66 | 'MISC': MISC_option.draw_panel 67 | } 68 | 69 | draw_preferences_tab[self.active_tab_addon](context, layout, self) 70 | 71 | list_class_to_register = [ 72 | EXPORT_option, 73 | CONNECT_unreal_engine, 74 | IMPORT_ASSET_fbx_import, 75 | IMPORT_ASSET_unreal_engine, 76 | PG_IMPORT_ASSET, 77 | STATIC_MESH_FBX_export, 78 | STATIC_MESH_unreal_engine_export, 79 | STATIC_MESH_export, 80 | SKELETAL_MESH_FBX_export, 81 | SKELETAL_MESH_unreal_engine_export, 82 | SKELETAL_MESH_export, 83 | ANIMATION_FBX_export, 84 | ANIMATION_unreal_engine_export, 85 | ANIMATION_export, 86 | GROOM_export, 87 | MISC_option, 88 | Preferences 89 | ] 90 | 91 | def register(): 92 | for x in list_class_to_register: 93 | register_class(x) 94 | 95 | def unregister(): 96 | for x in list_class_to_register[::-1]: 97 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/preferences/misc.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import PropertyGroup 3 | 4 | class MISC_option(PropertyGroup): 5 | 6 | experimental_features: bpy.props.BoolProperty( 7 | name='Experimental Features', 8 | description='Show some experimental features', 9 | default=False 10 | ) 11 | 12 | @classmethod 13 | def draw_panel(cls, context, layout, preferences): 14 | box = layout.box() 15 | 16 | box.label(text='Misc.', icon='FRAME_NEXT') 17 | 18 | data_properties = [ 19 | (preferences.misc, 'Experimental Features', 'experimental_features'), 20 | ] 21 | 22 | for data, label_str, property_str in data_properties: 23 | row = box.row() 24 | split = row.split(factor=0.5) 25 | col = split.column() 26 | col.alignment = 'LEFT' 27 | col.label(text=label_str, icon='DECORATE') 28 | col = split.column() 29 | col.prop(data, property_str, text='') 30 | -------------------------------------------------------------------------------- /UE4Workspace/skeletal_mesh/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/skeletal_mesh/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/skeletal_mesh/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import bpy 4 | from bpy.utils import register_class, unregister_class 5 | from bpy.types import Operator 6 | from .. utils.base import ExportOperator, Panel, ExportOptionPanel 7 | from .. utils.connect import remote, skeletons 8 | 9 | class OP_UpdateSkeleton(Operator): 10 | bl_idname = 'ue4workspace.update_skeletons' 11 | bl_label = 'Update Skeleton' 12 | bl_description = 'Update Skeleton From Unreal Engine' 13 | 14 | @classmethod 15 | def poll(self, context): 16 | preferences = context.preferences.addons['UE4Workspace'].preferences 17 | return preferences.export.type in ['UNREAL', 'BOTH'] and bool(remote.remote_nodes) 18 | 19 | def execute(self, context): 20 | preferences = context.preferences.addons['UE4Workspace'].preferences 21 | 22 | reset_skeleton_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'skeleton_list.json')), 'w+') 23 | reset_skeleton_list.write(json.dumps([])) 24 | reset_skeleton_list.close() 25 | 26 | remote.exec_script('GetAllSkeleton.py') 27 | 28 | load_skeleton_list = open(os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'skeleton_list.json')), 'r') 29 | skeleton_list = json.loads(load_skeleton_list.read()) 30 | load_skeleton_list.close() 31 | 32 | skeletons.clear() 33 | 34 | for node_id, package_name, asset_name in skeleton_list: 35 | skeletons.append((node_id + ':' + package_name, asset_name, package_name)) 36 | 37 | self.report({'INFO'}, 'Update asset list success') 38 | 39 | return {'FINISHED'} 40 | 41 | class OP_ExportSkeletalMesh(ExportOperator): 42 | bl_idname = 'ue4workspace.export_skeletal_mesh' 43 | bl_label = 'Export Skeletal Mesh' 44 | bl_description = 'Export Armature To Skeletal Mesh' 45 | 46 | ext_file = 'fbx' 47 | 48 | def execute(self, context): 49 | preferences = context.preferences.addons['UE4Workspace'].preferences 50 | skeletal_mesh = preferences.skeletal_mesh 51 | fbx_setting = skeletal_mesh.fbx 52 | unreal_engine_setting = skeletal_mesh.unreal_engine 53 | 54 | selected_objects = context.selected_objects 55 | objects = context.scene.objects if skeletal_mesh.option == 'ALL' else selected_objects 56 | objects = [obj for obj in objects if obj.type == 'ARMATURE'] 57 | 58 | directory = self.create_string_directory(preferences.export.export_folder if preferences.export.type in ['FILE','BOTH'] else preferences.export.temp_folder, skeletal_mesh.subfolder) 59 | 60 | self.create_directory_if_not_exist(directory, skeletal_mesh.subfolder) 61 | 62 | list_name_objects = [] 63 | unreal_engine_import_setting = { 64 | 'files': [], 65 | 'main_folder': self.safe_string_path(preferences.connect_unreal_engine.main_folder), 66 | 'subfolder': self.safe_string_path(skeletal_mesh.subfolder), 67 | 'overwrite_file': skeletal_mesh.overwrite_file, 68 | 'temporary': preferences.export.type == 'UNREAL' 69 | } 70 | 71 | unreal_engine_import_setting.update(unreal_engine_setting.to_dict()) 72 | 73 | bpy.ops.object.select_all(action='DESELECT') 74 | 75 | for obj in objects: 76 | if skeletal_mesh.mesh == 'COMBINE': 77 | filename = self.safe_string_path(obj.name) 78 | check_duplicate = len([obj_name for obj_name in list_name_objects if obj_name.startswith(filename)]) 79 | list_name_objects.append(filename) 80 | 81 | filename += '_' + str(check_duplicate) if bool(check_duplicate) else '' 82 | filename_ext = filename + '.' + self.ext_file 83 | 84 | if not self.is_file_exist(directory, filename_ext) or skeletal_mesh.overwrite_file: 85 | 86 | self.mute_attach_constraint(obj) 87 | 88 | original_location = obj.matrix_world.to_translation() 89 | if skeletal_mesh.origin == 'OBJECT': 90 | obj.matrix_world.translation = (0, 0, 0) 91 | 92 | original_rotation = obj.rotation_quaternion.copy() if obj.rotation_mode == 'QUATERNION' else obj.rotation_euler.copy() 93 | if not skeletal_mesh.apply_rotation: 94 | if obj.rotation_mode == 'QUATERNION': 95 | obj.rotation_quaternion = (1, 0, 0, 0) 96 | else: 97 | obj.rotation_euler = (0, 0, 0) 98 | 99 | original_object_name = obj.name 100 | is_armature_has_root_bone = obj.data.bones.get('root', False) 101 | 102 | if skeletal_mesh.root_bone == 'ARMATURE': 103 | obj.name = 'Armature' 104 | elif skeletal_mesh.root_bone == 'AUTO': 105 | if is_armature_has_root_bone: 106 | obj.name = 'Armature' 107 | else: 108 | obj.name = 'root' 109 | elif skeletal_mesh.root_bone == 'OBJECT': 110 | pass 111 | 112 | self.prepare_skeletal_meshes(obj) 113 | 114 | obj.select_set(state=True) 115 | 116 | export_setting = { 117 | 'filepath': self.create_string_directory(directory, filename_ext), 118 | 'check_existing': False, 119 | 'filter_glob': '*.fbx', 120 | 'use_selection': True, 121 | 'use_active_collection': False, 122 | 'object_types': {'MESH', 'ARMATURE'}, 123 | 'use_custom_props': skeletal_mesh.use_custom_props, 124 | 'bake_anim': False, 125 | 'path_mode': 'AUTO', 126 | 'embed_textures': False, 127 | 'batch_mode': 'OFF' 128 | } 129 | 130 | export_setting.update(fbx_setting.to_dict()) 131 | 132 | # EXPORT 133 | bpy.ops.export_scene.fbx(**export_setting) 134 | 135 | unreal_engine_import_setting['files'].append({ 136 | 'path': export_setting['filepath'], 137 | 'skeleton': skeletal_mesh.skeleton 138 | }) 139 | 140 | self.restore_skeletal_meshes(obj) 141 | 142 | obj.select_set(state=False) 143 | 144 | if skeletal_mesh.root_bone == 'ARMATURE': 145 | obj.name = original_object_name 146 | elif skeletal_mesh.root_bone == 'AUTO': 147 | obj.name = original_object_name 148 | elif skeletal_mesh.root_bone == 'OBJECT': 149 | pass 150 | 151 | if skeletal_mesh.origin == 'OBJECT': 152 | obj.matrix_world.translation = original_location 153 | 154 | if not skeletal_mesh.apply_rotation: 155 | if obj.rotation_mode == 'QUATERNION': 156 | obj.rotation_quaternion = original_rotation 157 | else: 158 | obj.rotation_euler = original_rotation 159 | 160 | self.unmute_attach_constraint(obj) 161 | else: 162 | self.mute_attach_constraint(obj) 163 | 164 | original_location = obj.matrix_world.to_translation() 165 | if skeletal_mesh.origin == 'OBJECT': 166 | obj.matrix_world.translation = (0, 0, 0) 167 | 168 | original_rotation = obj.rotation_quaternion.copy() if obj.rotation_mode == 'QUATERNION' else obj.rotation_euler.copy() 169 | if not skeletal_mesh.apply_rotation: 170 | if obj.rotation_mode == 'QUATERNION': 171 | obj.rotation_quaternion = (1, 0, 0, 0) 172 | else: 173 | obj.rotation_euler = (0, 0, 0) 174 | 175 | original_object_name = obj.name 176 | is_armature_has_root_bone = obj.data.bones.get('root', False) 177 | 178 | if skeletal_mesh.root_bone == 'ARMATURE': 179 | obj.name = 'Armature' 180 | elif skeletal_mesh.root_bone == 'AUTO': 181 | if is_armature_has_root_bone: 182 | obj.name = 'Armature' 183 | else: 184 | obj.name = 'root' 185 | elif skeletal_mesh.root_bone == 'OBJECT': 186 | pass 187 | 188 | obj.select_set(state=True) 189 | 190 | for skeletal_mesh_object, hide, hide_select, hide_viewport in [(children_obj, children_obj.hide_get(), children_obj.hide_select, children_obj.hide_viewport) for children_obj in obj.children if children_obj.type == 'MESH' and children_obj.data.is_export_skeletal_mesh_part]: 191 | filename = self.safe_string_path(obj.name + '_' + skeletal_mesh_object.name) 192 | check_duplicate = len([obj_name for obj_name in list_name_objects if obj_name.startswith(filename)]) 193 | list_name_objects.append(filename) 194 | 195 | filename += '_' + str(check_duplicate) if bool(check_duplicate) else '' 196 | filename_ext = filename + '.' + self.ext_file 197 | 198 | if not self.is_file_exist(directory, filename_ext) or skeletal_mesh.overwrite_file: 199 | skeletal_mesh_object.hide_set(False) 200 | skeletal_mesh_object.hide_select = False 201 | skeletal_mesh_object.hide_viewport = False 202 | 203 | skeletal_mesh_object.select_set(state=True) 204 | 205 | export_setting = { 206 | 'filepath': self.create_string_directory(directory, filename_ext), 207 | 'check_existing': False, 208 | 'filter_glob': '*.fbx', 209 | 'use_selection': True, 210 | 'use_active_collection': False, 211 | 'object_types': {'MESH', 'ARMATURE'}, 212 | 'use_custom_props': False, 213 | 'bake_anim': False, 214 | 'path_mode': 'AUTO', 215 | 'embed_textures': False, 216 | 'batch_mode': 'OFF' 217 | } 218 | 219 | export_setting.update(fbx_setting.to_dict()) 220 | 221 | # EXPORT 222 | bpy.ops.export_scene.fbx(**export_setting) 223 | 224 | unreal_engine_import_setting['files'].append({ 225 | 'path': export_setting['filepath'], 226 | 'skeleton': skeletal_mesh.skeleton 227 | }) 228 | 229 | skeletal_mesh_object.select_set(state=False) 230 | 231 | skeletal_mesh_object.hide_set(hide) 232 | skeletal_mesh_object.hide_select = hide_select 233 | skeletal_mesh_object.hide_viewport = hide_viewport 234 | 235 | obj.select_set(state=False) 236 | 237 | if skeletal_mesh.root_bone == 'ARMATURE': 238 | obj.name = original_object_name 239 | elif skeletal_mesh.root_bone == 'AUTO': 240 | obj.name = original_object_name 241 | elif skeletal_mesh.root_bone == 'OBJECT': 242 | pass 243 | 244 | if skeletal_mesh.origin == 'OBJECT': 245 | obj.matrix_world.translation = original_location 246 | 247 | if not skeletal_mesh.apply_rotation: 248 | if obj.rotation_mode == 'QUATERNION': 249 | obj.rotation_quaternion = original_rotation 250 | else: 251 | obj.rotation_euler = original_rotation 252 | 253 | self.unmute_attach_constraint(obj) 254 | 255 | for obj in selected_objects: 256 | obj.select_set(state=True) 257 | 258 | self.unreal_engine_exec_script('ImportSkeletalMesh.py', unreal_engine_import_setting) 259 | 260 | self.report({'INFO'}, 'export ' + str(len(unreal_engine_import_setting['files'])) + ' skeletal mesh success') 261 | 262 | return {'FINISHED'} 263 | 264 | class PANEL(Panel): 265 | bl_idname = 'UE4WORKSPACE_PT_SkeletalMeshPanel' 266 | bl_label = 'Skeletal Mesh' 267 | 268 | def draw(self, context): 269 | layout = self.layout 270 | preferences = context.preferences.addons['UE4Workspace'].preferences 271 | skeletal_mesh = preferences.skeletal_mesh 272 | 273 | col_data = [ 274 | ('Subfolder', 'subfolder'), 275 | ('Overwrite File', 'overwrite_file'), 276 | ('Custom Properties', 'use_custom_props'), 277 | ('Apply Rotation', 'apply_rotation'), 278 | ('Root Bone', 'root_bone'), 279 | ('Export Skeletal Mesh Option', 'option'), 280 | ('Origin', 'origin'), 281 | ('Mesh Option', 'mesh'), 282 | ] 283 | 284 | if preferences.export.type in ['UNREAL','BOTH']: 285 | col_data.append(('Skeleton', 'skeleton')) 286 | 287 | for label_str, property_str in col_data: 288 | row = layout.row() 289 | split = row.split(factor=0.6) 290 | col = split.column() 291 | col.alignment = 'RIGHT' 292 | col.label(text=label_str) 293 | col = split.column() 294 | col.prop(skeletal_mesh, property_str, text='') 295 | 296 | if property_str == 'skeleton': 297 | row = layout.row() 298 | row.scale_y = 1.5 299 | row.operator('ue4workspace.update_skeletons',icon='ARMATURE_DATA') 300 | 301 | col = layout.column() 302 | col.scale_y = 1.5 303 | col.operator('ue4workspace.export_skeletal_mesh',icon='OUTLINER_OB_ARMATURE') 304 | 305 | class SUB_PANEL_1(ExportOptionPanel): 306 | bl_idname = 'UE4WORKSPACE_PT_SkeletalMeshFBXOption' 307 | bl_parent_id = 'UE4WORKSPACE_PT_SkeletalMeshPanel' 308 | bl_label = 'FBX Export Setting' 309 | 310 | def draw(self, context): 311 | preferences = context.preferences.addons['UE4Workspace'].preferences 312 | fbx_setting = preferences.skeletal_mesh.fbx 313 | self.draw_property(fbx_setting, { 314 | 'tab_transform': [ 315 | ('Scale', 'global_scale'), 316 | ('Apply Scalings', 'apply_unit_scale'), 317 | ('Forward', 'axis_forward'), 318 | ('Up', 'axis_up'), 319 | ('Apply Unit', 'apply_unit_scale'), 320 | ('Apply Transform', 'bake_space_transform'), 321 | ], 322 | 'tab_geometry': [ 323 | ('Smoothing', 'mesh_smooth_type'), 324 | ('Export Subdivision Surface', 'use_subsurf'), 325 | ('Apply Modifiers', 'use_mesh_modifiers'), 326 | ('Loose Edges', 'use_mesh_edges'), 327 | ('Tangent Space', 'use_tspace'), 328 | ], 329 | 'tab_armature': [ 330 | ('Primary Bone Axis', 'primary_bone_axis'), 331 | ('Secondary Bone Axis', 'secondary_bone_axis'), 332 | ('Armature FBXNode Type', 'armature_nodetype'), 333 | ('Only Deform Bones', 'use_armature_deform_only'), 334 | ('Add Leaf Bones', 'add_leaf_bones'), 335 | ], 336 | }) 337 | 338 | class SUB_PANEL_2(ExportOptionPanel): 339 | bl_idname = 'UE4WORKSPACE_PT_SkeletalMeshUnrealEnginePanel' 340 | bl_parent_id = 'UE4WORKSPACE_PT_SkeletalMeshPanel' 341 | bl_label = 'Unreal Engine Export Setting' 342 | 343 | def draw(self, context): 344 | preferences = context.preferences.addons['UE4Workspace'].preferences 345 | unreal_engine_setting = preferences.skeletal_mesh.unreal_engine 346 | 347 | setting_data = { 348 | 'tab_mesh': [ 349 | ('Import Content Type', 'import_content_type'), 350 | ('Vertex Color Import Option', 'vertex_color_import_option'), 351 | ('Vertex Override Color', 'vertex_override_color'), 352 | ('Update Skeleton Reference Pose', 'update_skeleton_reference_pose'), 353 | ('Use T0 As Ref Pose', 'use_t0_as_ref_pose'), 354 | ('Preserve Smoothing Groups', 'preserve_smoothing_groups'), 355 | ('Import Meshes In Bone Hierarchy', 'import_meshes_in_bone_hierarchy'), 356 | ('Import Morph Targets', 'import_morph_targets'), 357 | ('Import Mesh LODs', 'import_mesh_lo_ds'), 358 | ('Normal Import Method', 'normal_import_method'), 359 | ('Normal Generation Method', 'normal_generation_method'), 360 | ('Compute Weighted Normals', 'compute_weighted_normals'), 361 | ('Threshold Position', 'threshold_position'), 362 | ('Threshold Tangent Normal', 'threshold_tangent_normal'), 363 | ('Threshold UV', 'threshold_uv'), 364 | ], 365 | 'tab_transform': [ 366 | ('Import Translation', 'import_translation'), 367 | ('Import Rotation', 'import_rotation'), 368 | ('Import Uniform Scale', 'import_uniform_scale'), 369 | ], 370 | 'tab_misc': [ 371 | ('Convert Scene', 'convert_scene'), 372 | ('Force Front XAxis', 'force_front_x_axis'), 373 | ('Convert Scene Unit', 'convert_scene_unit'), 374 | ('Override Full Name', 'override_full_name'), 375 | ], 376 | 'tab_material': [ 377 | ('Search Location', 'material_search_location'), 378 | ('Import Material', 'import_materials'), 379 | ('Import Texture', 'import_textures'), 380 | ('Invert Normal Maps', 'invert_normal_maps'), 381 | ('Reorder Material To FBX Order', 'reorder_material_to_fbx_order'), 382 | ] 383 | } 384 | 385 | self.draw_property(unreal_engine_setting, setting_data) 386 | 387 | list_class_to_register = [ 388 | OP_UpdateSkeleton, 389 | OP_ExportSkeletalMesh, 390 | PANEL, 391 | SUB_PANEL_1, 392 | SUB_PANEL_2 393 | ] 394 | 395 | def register(): 396 | for x in list_class_to_register: 397 | register_class(x) 398 | 399 | def unregister(): 400 | for x in list_class_to_register[::-1]: 401 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/static_mesh/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/static_mesh/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/static_mesh/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import Operator 4 | from .. utils.base import ExportOperator, Panel, ExportOptionPanel 5 | 6 | class OP_ExportStaticMesh(ExportOperator): 7 | bl_idname = 'ue4workspace.export_static_mesh' 8 | bl_label = 'Export Static Mesh' 9 | bl_description = 'Export Mesh To Static Mesh' 10 | 11 | ext_file = 'fbx' 12 | 13 | def execute(self, context): 14 | preferences = context.preferences.addons['UE4Workspace'].preferences 15 | static_mesh = preferences.static_mesh 16 | fbx_setting = static_mesh.fbx 17 | unreal_engine_setting = static_mesh.unreal_engine 18 | 19 | selected_objects = context.selected_objects 20 | objects = context.scene.objects if static_mesh.option == 'ALL' else selected_objects 21 | objects = [obj for obj in objects if obj.type == 'MESH' and not 'ARMATURE' in [mod.type for mod in obj.modifiers] and not obj.data.mesh_as_lod] 22 | 23 | directory = self.create_string_directory(preferences.export.export_folder if preferences.export.type in ['FILE','BOTH'] else preferences.export.temp_folder, static_mesh.subfolder) 24 | 25 | self.create_directory_if_not_exist(directory, static_mesh.subfolder) 26 | 27 | list_unhide_collection_name = [('UE4CustomCollision', static_mesh.custom_collision), ('UE4Socket', static_mesh.socket)] 28 | 29 | self.unhide_collection(*list_unhide_collection_name) 30 | 31 | list_name_objects = [] 32 | unreal_engine_import_setting = { 33 | 'files': [], 34 | 'main_folder': self.safe_string_path(preferences.connect_unreal_engine.main_folder), 35 | 'subfolder': self.safe_string_path(static_mesh.subfolder), 36 | 'overwrite_file': static_mesh.overwrite_file, 37 | 'temporary': preferences.export.type == 'UNREAL' 38 | } 39 | 40 | unreal_engine_import_setting.update(unreal_engine_setting.to_dict()) 41 | 42 | bpy.ops.object.select_all(action='DESELECT') 43 | 44 | for obj in objects: 45 | filename = self.safe_string_path(obj.name) 46 | check_duplicate = len([obj_name for obj_name in list_name_objects if obj_name.startswith(filename)]) 47 | list_name_objects.append(filename) 48 | 49 | filename += '_' + str(check_duplicate) if bool(check_duplicate) else '' 50 | filename_ext = filename + '.' + self.ext_file 51 | 52 | if not self.is_file_exist(directory, filename_ext) or static_mesh.overwrite_file: 53 | 54 | is_object_has_custom_collision = bool([True for children_object in obj.children if children_object.type == 'MESH' and children_object.data.is_custom_collision]) 55 | if static_mesh.custom_collision and is_object_has_custom_collision: 56 | self.prepare_custom_collision(obj) 57 | 58 | is_object_has_socket = bool([True for children_object in obj.children if children_object.type == 'EMPTY' and children_object.is_socket]) 59 | if static_mesh.socket and is_object_has_socket: 60 | self.prepare_socket(obj) 61 | 62 | self.mute_attach_constraint(obj) 63 | 64 | original_location = obj.matrix_world.to_translation() 65 | if static_mesh.origin == 'OBJECT': 66 | obj.matrix_world.translation = (0, 0, 0) 67 | 68 | original_rotation = obj.rotation_quaternion.copy() if obj.rotation_mode == 'QUATERNION' else obj.rotation_euler.copy() 69 | if not static_mesh.apply_rotation: 70 | if obj.rotation_mode == 'QUATERNION': 71 | obj.rotation_quaternion = (1, 0, 0, 0) 72 | else: 73 | obj.rotation_euler = (0, 0, 0) 74 | 75 | if static_mesh.lod: 76 | self.prepare_lod(obj) 77 | 78 | obj.select_set(state=True) 79 | 80 | export_setting = { 81 | 'filepath': self.create_string_directory(directory, filename_ext), 82 | 'check_existing': False, 83 | 'filter_glob': '*.fbx', 84 | 'use_selection': True, 85 | 'use_active_collection': False, 86 | 'object_types': {'MESH', 'EMPTY'}, 87 | 'use_custom_props': static_mesh.use_custom_props, 88 | 'bake_anim': False, 89 | 'path_mode': 'AUTO', 90 | 'embed_textures': False, 91 | 'batch_mode': 'OFF' 92 | } 93 | 94 | export_setting.update(fbx_setting.to_dict()) 95 | 96 | # EXPORT 97 | bpy.ops.export_scene.fbx(**export_setting) 98 | 99 | unreal_engine_import_setting['files'].append({ 100 | 'path': export_setting['filepath'], 101 | 'custom_lightmap': 'lightmap' in [uv.name.lower() for uv in obj.data.uv_layers], 102 | 'custom_collision': (static_mesh.custom_collision and is_object_has_custom_collision), 103 | 'auto_compute_lod_distances': obj.data.auto_compute_lod_screen_size, 104 | 'lod': (([obj.data.lod_0_screen_size] + [lod.screen_size for lod in obj.data.lods if lod.obj is not None]) if (static_mesh.lod and bool([True for lod in obj.data.lods if lod.obj is not None])) else []) 105 | }) 106 | 107 | obj.select_set(state=False) 108 | 109 | if static_mesh.custom_collision and is_object_has_custom_collision: 110 | self.restore_custom_collision() 111 | 112 | if static_mesh.socket and is_object_has_socket: 113 | self.restore_socket() 114 | 115 | if static_mesh.lod: 116 | self.restore_lod() 117 | 118 | if static_mesh.origin == 'OBJECT': 119 | obj.matrix_world.translation = original_location 120 | 121 | if not static_mesh.apply_rotation: 122 | if obj.rotation_mode == 'QUATERNION': 123 | obj.rotation_quaternion = original_rotation 124 | else: 125 | obj.rotation_euler = original_rotation 126 | 127 | self.unmute_attach_constraint(obj) 128 | 129 | self.restore_collection(*list_unhide_collection_name) 130 | 131 | for obj in selected_objects: 132 | obj.select_set(state=True) 133 | 134 | self.unreal_engine_exec_script('ImportStaticMesh.py', unreal_engine_import_setting) 135 | 136 | self.report({'INFO'}, 'export ' + str(len(unreal_engine_import_setting['files'])) + ' static mesh success') 137 | 138 | return {'FINISHED'} 139 | 140 | class PANEL(Panel): 141 | bl_idname = 'UE4WORKSPACE_PT_StaticMeshPanel' 142 | bl_label = 'Static Mesh' 143 | 144 | def draw(self, context): 145 | layout = self.layout 146 | preferences = context.preferences.addons['UE4Workspace'].preferences 147 | static_mesh = preferences.static_mesh 148 | 149 | col_data = [ 150 | ('Subfolder', 'subfolder'), 151 | ('Overwrite File', 'overwrite_file'), 152 | ('Custom Properties', 'use_custom_props'), 153 | ('Apply Rotation', 'apply_rotation'), 154 | ('Custom Collision', 'custom_collision'), 155 | ('Socket', 'socket'), 156 | ('Level of Detail', 'lod'), 157 | ('Export Static Mesh Option', 'option'), 158 | ('Origin', 'origin'), 159 | ] 160 | 161 | for label_str, property_str in col_data: 162 | row = layout.row() 163 | split = row.split(factor=0.6) 164 | col = split.column() 165 | col.alignment = 'RIGHT' 166 | col.label(text=label_str) 167 | col = split.column() 168 | col.prop(static_mesh, property_str, text='') 169 | 170 | col = layout.column() 171 | col.scale_y = 1.5 172 | col.operator('ue4workspace.export_static_mesh',icon='MESH_CUBE') 173 | 174 | class SUB_PANEL_1(ExportOptionPanel): 175 | bl_idname = 'UE4WORKSPACE_PT_StaticMeshFBXOption' 176 | bl_parent_id = 'UE4WORKSPACE_PT_StaticMeshPanel' 177 | bl_label = 'FBX Export Setting' 178 | 179 | def draw(self, context): 180 | preferences = context.preferences.addons['UE4Workspace'].preferences 181 | fbx_setting = preferences.static_mesh.fbx 182 | self.draw_property(fbx_setting, { 183 | 'tab_transform': [ 184 | ('Scale', 'global_scale'), 185 | ('Apply Scalings', 'apply_unit_scale'), 186 | ('Forward', 'axis_forward'), 187 | ('Up', 'axis_up'), 188 | ('Apply Unit', 'apply_unit_scale'), 189 | ('Apply Transform', 'bake_space_transform'), 190 | ], 191 | 'tab_geometry': [ 192 | ('Smoothing', 'mesh_smooth_type'), 193 | ('Export Subdivision Surface', 'use_subsurf'), 194 | ('Apply Modifiers', 'use_mesh_modifiers'), 195 | ('Loose Edges', 'use_mesh_edges'), 196 | ('Tangent Space', 'use_tspace'), 197 | ] 198 | }) 199 | 200 | class SUB_PANEL_2(ExportOptionPanel): 201 | bl_idname = 'UE4WORKSPACE_PT_StaticMeshUnrealEnginePanel' 202 | bl_parent_id = 'UE4WORKSPACE_PT_StaticMeshPanel' 203 | bl_label = 'Unreal Engine Export Setting' 204 | 205 | def draw(self, context): 206 | preferences = context.preferences.addons['UE4Workspace'].preferences 207 | unreal_engine_setting = preferences.static_mesh.unreal_engine 208 | 209 | setting_data = { 210 | 'tab_mesh': [ 211 | ('Auto Generate Collision', 'auto_generate_collision'), 212 | ('Vertex Color Import Option', 'vertex_color_import_option'), 213 | ('Vertex Override Color', 'vertex_override_color'), 214 | ('Remove Degenerates', 'remove_degenerates'), 215 | ('Build Adjacency Buffer', 'build_adjacency_buffer'), 216 | ('Build Reversed Index Buffer', 'build_reversed_index_buffer'), 217 | ('Generate Lightmaps UVs', 'generate_lightmap_u_vs'), 218 | ('One Convex Hull Per UCX', 'one_convex_hull_per_ucx'), 219 | ('Combine Meshes', 'combine_meshes'), 220 | ('Transform Vertex to Absolute', 'transform_vertex_to_absolute'), 221 | ('Bake Pivot in Vertex', 'bake_pivot_in_vertex'), 222 | ('Import Mesh LODs', 'import_mesh_lo_ds'), 223 | ('Normal Import Method', 'normal_import_method'), 224 | ('Normal Generation Method', 'normal_generation_method'), 225 | ('Compute Weighted Normals', 'compute_weighted_normals'), 226 | ], 227 | 'tab_transform': [ 228 | ('Import Translation', 'import_translation'), 229 | ('Import Rotation', 'import_rotation'), 230 | ('Import Uniform Scale', 'import_uniform_scale'), 231 | ], 232 | 'tab_misc': [ 233 | ('Convert Scene', 'convert_scene'), 234 | ('Force Front XAxis', 'force_front_x_axis'), 235 | ('Convert Scene Unit', 'convert_scene_unit'), 236 | ('Override Full Name', 'override_full_name'), 237 | ], 238 | } 239 | 240 | if unreal_engine_setting.import_mesh_lo_ds: 241 | setting_data['tab_lod'] = [ 242 | ('Auto Compute LOD Screen Size', 'auto_compute_lod_distances') 243 | ] 244 | 245 | for index in range(8): 246 | setting_data['tab_lod'].append(('LOD ' + str(index) + ' Screen Size', 'lod_distance' + str(index))) 247 | 248 | setting_data['tab_lod'].append(('Minimum LOD', 'minimum_lod_number')) 249 | setting_data['tab_lod'].append(('Number of LODs', 'lod_number')) 250 | 251 | setting_data['tab_material'] = [ 252 | ('Search Location', 'material_search_location'), 253 | ('Import Material', 'import_materials'), 254 | ('Import Texture', 'import_textures'), 255 | ('Invert Normal Maps', 'invert_normal_maps'), 256 | ('Reorder Material To FBX Order', 'reorder_material_to_fbx_order'), 257 | ] 258 | 259 | self.draw_property(unreal_engine_setting, setting_data) 260 | 261 | list_class_to_register = [ 262 | OP_ExportStaticMesh, 263 | PANEL, 264 | SUB_PANEL_1, 265 | SUB_PANEL_2 266 | ] 267 | 268 | def register(): 269 | for x in list_class_to_register: 270 | register_class(x) 271 | 272 | def unregister(): 273 | for x in list_class_to_register[::-1]: 274 | unregister_class(x) -------------------------------------------------------------------------------- /UE4Workspace/temp/import_asset_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "A8CF65F74F8C640F351B56876D4013AD", 4 | "/Game/Mannequin/Animations/ThirdPersonWalk.ThirdPersonWalk", 5 | "ThirdPersonWalk", 6 | "AnimSequence" 7 | ], 8 | [ 9 | "A8CF65F74F8C640F351B56876D4013AD", 10 | "/Game/Mannequin/Animations/ThirdPersonRun.ThirdPersonRun", 11 | "ThirdPersonRun", 12 | "AnimSequence" 13 | ], 14 | [ 15 | "A8CF65F74F8C640F351B56876D4013AD", 16 | "/Game/Mannequin/Animations/ThirdPersonIdle.ThirdPersonIdle", 17 | "ThirdPersonIdle", 18 | "AnimSequence" 19 | ], 20 | [ 21 | "A8CF65F74F8C640F351B56876D4013AD", 22 | "/Game/Mannequin/Animations/ThirdPersonJump_Start.ThirdPersonJump_Start", 23 | "ThirdPersonJump_Start", 24 | "AnimSequence" 25 | ], 26 | [ 27 | "A8CF65F74F8C640F351B56876D4013AD", 28 | "/Game/Mannequin/Animations/ThirdPersonJump_Loop.ThirdPersonJump_Loop", 29 | "ThirdPersonJump_Loop", 30 | "AnimSequence" 31 | ], 32 | [ 33 | "A8CF65F74F8C640F351B56876D4013AD", 34 | "/Game/Mannequin/Animations/ThirdPerson_Jump.ThirdPerson_Jump", 35 | "ThirdPerson_Jump", 36 | "AnimSequence" 37 | ], 38 | [ 39 | "A8CF65F74F8C640F351B56876D4013AD", 40 | "/Game/Mannequin/Character/Mesh/SK_Mannequin.SK_Mannequin", 41 | "SK_Mannequin", 42 | "SkeletalMesh" 43 | ], 44 | [ 45 | "A8CF65F74F8C640F351B56876D4013AD", 46 | "/Game/Blender/lod_test.lod_test", 47 | "lod_test", 48 | "StaticMesh" 49 | ], 50 | [ 51 | "A8CF65F74F8C640F351B56876D4013AD", 52 | "/Game/Blender/axis.axis", 53 | "axis", 54 | "StaticMesh" 55 | ], 56 | [ 57 | "A8CF65F74F8C640F351B56876D4013AD", 58 | "/Game/Geometry/Meshes/1M_Cube.1M_Cube", 59 | "1M_Cube", 60 | "StaticMesh" 61 | ], 62 | [ 63 | "A8CF65F74F8C640F351B56876D4013AD", 64 | "/Game/Geometry/Meshes/1M_Cube_Chamfer.1M_Cube_Chamfer", 65 | "1M_Cube_Chamfer", 66 | "StaticMesh" 67 | ], 68 | [ 69 | "A8CF65F74F8C640F351B56876D4013AD", 70 | "/Game/Geometry/Meshes/TemplateFloor.TemplateFloor", 71 | "TemplateFloor", 72 | "StaticMesh" 73 | ], 74 | [ 75 | "A8CF65F74F8C640F351B56876D4013AD", 76 | "/Game/Mannequin/Character/Mesh/SK_Mannequin_Female.SK_Mannequin_Female", 77 | "SK_Mannequin_Female", 78 | "SkeletalMesh" 79 | ], 80 | [ 81 | "A8CF65F74F8C640F351B56876D4013AD", 82 | "/Game/ThirdPerson/Meshes/Bump_StaticMesh.Bump_StaticMesh", 83 | "Bump_StaticMesh", 84 | "StaticMesh" 85 | ], 86 | [ 87 | "A8CF65F74F8C640F351B56876D4013AD", 88 | "/Game/ThirdPerson/Meshes/LeftArm_StaticMesh.LeftArm_StaticMesh", 89 | "LeftArm_StaticMesh", 90 | "StaticMesh" 91 | ], 92 | [ 93 | "A8CF65F74F8C640F351B56876D4013AD", 94 | "/Game/ThirdPerson/Meshes/Linear_Stair_StaticMesh.Linear_Stair_StaticMesh", 95 | "Linear_Stair_StaticMesh", 96 | "StaticMesh" 97 | ], 98 | [ 99 | "A8CF65F74F8C640F351B56876D4013AD", 100 | "/Game/ThirdPerson/Meshes/Ramp_StaticMesh.Ramp_StaticMesh", 101 | "Ramp_StaticMesh", 102 | "StaticMesh" 103 | ], 104 | [ 105 | "A8CF65F74F8C640F351B56876D4013AD", 106 | "/Game/ThirdPerson/Meshes/RightArm_StaticMesh.RightArm_StaticMesh", 107 | "RightArm_StaticMesh", 108 | "StaticMesh" 109 | ], 110 | [ 111 | "A8CF65F74F8C640F351B56876D4013AD", 112 | "/Game/Mannequin/Animations/ThirdPersonJump_End.ThirdPersonJump_End", 113 | "ThirdPersonJump_End", 114 | "AnimSequence" 115 | ], 116 | [ 117 | "A8CF65F74F8C640F351B56876D4013AD", 118 | "/Game/Mixamo/Ch40_nonPBR.Ch40_nonPBR", 119 | "Ch40_nonPBR", 120 | "SkeletalMesh" 121 | ], 122 | [ 123 | "A8CF65F74F8C640F351B56876D4013AD", 124 | "/Game/Mixamo/Jog_Forward.Jog_Forward", 125 | "Jog_Forward", 126 | "AnimSequence" 127 | ] 128 | ] -------------------------------------------------------------------------------- /UE4Workspace/temp/import_asset_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "C:\\Users\\user\\Documents\\TESTING\\", 3 | "files": [ 4 | [ 5 | "A8CF65F74F8C640F351B56876D4013AD", 6 | "/Game/Blender/axis.axis", 7 | 0 8 | ] 9 | ], 10 | "fbx_export_compatibility": "FBX_2013", 11 | "ascii": false, 12 | "force_front_x_axis": false, 13 | "vertex_color": true, 14 | "level_of_detail": true, 15 | "collision": true, 16 | "export_morph_targets": true, 17 | "export_preview_mesh": false, 18 | "map_skeletal_motion_to_root": false, 19 | "export_local_time": true 20 | } -------------------------------------------------------------------------------- /UE4Workspace/temp/imported_asset_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "C:/Users/user/Documents/TESTING/temp_file_0.fbx", 4 | "StaticMesh", 5 | "axis" 6 | ] 7 | ] -------------------------------------------------------------------------------- /UE4Workspace/temp/skeleton_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "27C0FCBB4FFB22BE3CBF45B7BB60FC7B", 4 | "/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton", 5 | "UE4_Mannequin_Skeleton" 6 | ], 7 | [ 8 | "27C0FCBB4FFB22BE3CBF45B7BB60FC7B", 9 | "/Game/Mixamo/Ch40_nonPBR_Skeleton", 10 | "Ch40_nonPBR_Skeleton" 11 | ] 12 | ] -------------------------------------------------------------------------------- /UE4Workspace/temp/unreal_engine_import_setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "C:\\Users\\user\\Documents\\TESTING\\run.fbx", 5 | "skeleton": "27C0FCBB4FFB22BE3CBF45B7BB60FC7B:/Game/Mixamo/Ch40_nonPBR_Skeleton" 6 | } 7 | ], 8 | "main_folder": "Blender", 9 | "subfolder": "", 10 | "overwrite_file": true, 11 | "temporary": false, 12 | "animation_length": "FBXALIT_EXPORTED_TIME", 13 | "import_meshes_in_bone_hierarchy": true, 14 | "frame_import_range": [ 15 | 0, 16 | 0 17 | ], 18 | "use_default_sample_rate": false, 19 | "custom_sample_rate": 0, 20 | "import_custom_attribute": true, 21 | "delete_existing_custom_attribute_curves": false, 22 | "import_bone_tracks": true, 23 | "set_material_drive_parameter_on_custom_attribute": false, 24 | "material_curve_suffixes": [ 25 | "_mat" 26 | ], 27 | "remove_redundant_keys": true, 28 | "delete_existing_morph_target_curves": false, 29 | "do_not_import_curve_with_zero": true, 30 | "preserve_local_transform": false, 31 | "import_translation": [ 32 | 0.0, 33 | 0.0, 34 | 0.0 35 | ], 36 | "import_rotation": [ 37 | 0.0, 38 | 0.0, 39 | 0.0 40 | ], 41 | "import_uniform_scale": 1.0, 42 | "convert_scene": true, 43 | "force_front_x_axis": false, 44 | "convert_scene_unit": false, 45 | "override_full_name": true 46 | } -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/ExportAsset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unreal import ( 5 | AssetRegistryHelpers, 6 | FbxExportOption, 7 | FbxExportCompatibility, 8 | AssetExportTask, 9 | Exporter, 10 | 11 | AnimSequenceExporterFBX, 12 | SkeletalMeshExporterFBX, 13 | StaticMeshExporterFBX 14 | ) 15 | 16 | # addon_path is Blender Unreal Engien 4 Workspace addon path 17 | # node_id is unreal engine project instance 18 | 19 | asset_registry = AssetRegistryHelpers.get_asset_registry() 20 | 21 | load_imported_asset_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'imported_asset_list.json')), 'r') 22 | original_imported_asset_list = json.loads(load_imported_asset_list.read()) 23 | load_imported_asset_list.close() 24 | 25 | json_file = open(os.path.normpath(os.path.join(addon_path, 'temp', 'import_asset_setting.json')), 'r') 26 | import_asset_setting = json.loads(json_file.read()) 27 | json_file.close() 28 | 29 | assets = [(asset_path, index) for asset_node_id, asset_path, index in import_asset_setting['files'] if asset_node_id == node_id] 30 | 31 | for asset_path, index in assets: 32 | asset = asset_registry.get_asset_by_object_path(asset_path) 33 | asset_object = asset.get_asset() 34 | 35 | if bool(asset_object): 36 | target_path = os.path.join(import_asset_setting['path'], 'temp_file_' + str(index) + '.fbx').replace(os.sep, '/') 37 | export_options = FbxExportOption() 38 | 39 | for prop, value in ([ 40 | ('fbx_export_compatibility', getattr(FbxExportCompatibility, import_asset_setting['fbx_export_compatibility'])), 41 | ('ascii', import_asset_setting['ascii']), 42 | ('force_front_x_axis', import_asset_setting['force_front_x_axis']), 43 | ('vertex_color', import_asset_setting['vertex_color']), 44 | ('level_of_detail', import_asset_setting['level_of_detail']), 45 | ('collision', import_asset_setting['collision']), 46 | ('export_morph_targets', import_asset_setting['export_morph_targets']), 47 | ('export_preview_mesh', import_asset_setting['export_preview_mesh']), 48 | ('map_skeletal_motion_to_root', import_asset_setting['map_skeletal_motion_to_root']), 49 | ('export_local_time', import_asset_setting['export_local_time']), 50 | ]): 51 | export_options.set_editor_property(prop, value) 52 | 53 | export_task = AssetExportTask() 54 | 55 | for prop, value in [ 56 | ('automated', True), 57 | ('object', asset_object), 58 | ('filename', target_path), 59 | ('exporter', { 60 | 'StaticMesh': StaticMeshExporterFBX(), 61 | 'SkeletalMesh': SkeletalMeshExporterFBX(), 62 | 'AnimSequence': AnimSequenceExporterFBX() 63 | }[str(asset.asset_class)]), 64 | ('options', export_options), 65 | ('prompt', False), 66 | ('replace_identical', True), 67 | ]: 68 | export_task.set_editor_property(prop, value) 69 | 70 | is_export_success = Exporter.run_asset_export_task(export_task) 71 | if is_export_success: 72 | original_imported_asset_list.append([target_path, str(asset.asset_class), str(asset.asset_name)]) 73 | 74 | save_imported_asset_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'imported_asset_list.json')), 'w+') 75 | save_imported_asset_list.write(json.dumps(original_imported_asset_list, indent=4)) 76 | save_imported_asset_list.close() -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/GetAllImportableAsset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from unreal import ( 4 | AssetRegistryHelpers 5 | ) 6 | 7 | # addon_path is Blender Unreal Engien 4 Workspace addon path 8 | # node_id is unreal engine project instance 9 | 10 | asset_registry = AssetRegistryHelpers.get_asset_registry() 11 | 12 | all_assets = asset_registry.get_assets_by_path('/Game', recursive=True) 13 | 14 | asset_list = [(node_id, str(asset.object_path), str(asset.asset_name), str(asset.asset_class)) for asset in all_assets if str(asset.asset_class) in ['StaticMesh', 'SkeletalMesh', 'AnimSequence']] 15 | 16 | load_import_asset_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'import_asset_list.json')), 'r') 17 | original_asset_list = json.loads(load_import_asset_list.read()) 18 | load_import_asset_list.close() 19 | 20 | original_asset_list.extend(asset_list) 21 | 22 | save_import_asset_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'import_asset_list.json')), 'w+') 23 | save_import_asset_list.write(json.dumps(original_asset_list, indent=4)) 24 | save_import_asset_list.close() -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/GetAllSkeleton.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from unreal import ( 4 | AssetRegistryHelpers 5 | ) 6 | 7 | # addon_path is Blender Unreal Engien 4 Workspace addon path 8 | # node_id is unreal engine project instance 9 | 10 | asset_registry = AssetRegistryHelpers.get_asset_registry() 11 | 12 | all_assets = asset_registry.get_assets_by_path('/Game', recursive=True) 13 | 14 | skeleton_list = [(node_id, str(asset.package_name), str(asset.asset_name)) for asset in all_assets if str(asset.asset_class) == 'Skeleton'] 15 | 16 | load_skeleton_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'skeleton_list.json')), 'r') 17 | original_skeleton_list = json.loads(load_skeleton_list.read()) 18 | load_skeleton_list.close() 19 | 20 | original_skeleton_list.extend(skeleton_list) 21 | 22 | save_skeleton_list = open(os.path.normpath(os.path.join(addon_path, 'temp', 'skeleton_list.json')), 'w+') 23 | save_skeleton_list.write(json.dumps(original_skeleton_list, indent=4)) 24 | save_skeleton_list.close() -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/ImportAnimation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | import json 4 | 5 | from unreal import ( 6 | EditorAssetLibrary, 7 | FbxImportUI, 8 | AssetImportTask, 9 | AssetToolsHelpers, 10 | load_asset, 11 | 12 | FBXAnimationLengthImportType, 13 | Int32Interval, 14 | Vector, 15 | Rotator, 16 | ) 17 | 18 | # addon_path is Blender Unreal Engien 4 Workspace addon path 19 | # node_id is unreal engine project instance 20 | 21 | json_file = open(os.path.normpath(os.path.join(addon_path, 'temp', 'unreal_engine_import_setting.json')), 'r') 22 | unreal_engine_import_setting = json.loads(json_file.read()) 23 | json_file.close() 24 | 25 | target_path = '/' + os.path.join('Game', unreal_engine_import_setting['main_folder'], unreal_engine_import_setting['subfolder']).replace(os.sep, '/') 26 | 27 | if EditorAssetLibrary.does_directory_exist(directory_path=target_path): 28 | EditorAssetLibrary.make_directory(directory_path=target_path) 29 | 30 | for file in unreal_engine_import_setting['files']: 31 | source_file = file['path'].replace(os.sep, '/') 32 | target_node_id, skeleton_path = file['skeleton'].split(':') 33 | 34 | skeleton_asset = load_asset(skeleton_path) if skeleton_path != 'NONE' else None 35 | 36 | if os.path.exists(source_file) and bool(skeleton_asset): 37 | import_options = FbxImportUI() 38 | 39 | frame_import_range_min, frame_import_range_max = unreal_engine_import_setting['frame_import_range'] 40 | frame_import_range = Int32Interval() 41 | 42 | for option, prop, value in ([ 43 | (import_options, 'import_mesh', False), 44 | (import_options, 'import_as_skeletal', False), 45 | (import_options, 'import_animations', True), 46 | 47 | (import_options, 'skeleton', skeleton_asset), 48 | 49 | (import_options.anim_sequence_import_data, 'animation_length', getattr(FBXAnimationLengthImportType, unreal_engine_import_setting['animation_length'])), 50 | (import_options.anim_sequence_import_data, 'import_meshes_in_bone_hierarchy', unreal_engine_import_setting['import_meshes_in_bone_hierarchy']), 51 | (frame_import_range, 'min', frame_import_range_min), 52 | (frame_import_range, 'max', frame_import_range_max), 53 | (import_options.anim_sequence_import_data, 'frame_import_range', frame_import_range), 54 | (import_options.anim_sequence_import_data, 'use_default_sample_rate', unreal_engine_import_setting['use_default_sample_rate']), 55 | (import_options.anim_sequence_import_data, 'custom_sample_rate', unreal_engine_import_setting['custom_sample_rate']), 56 | (import_options.anim_sequence_import_data, 'import_custom_attribute', unreal_engine_import_setting['import_custom_attribute']), 57 | (import_options.anim_sequence_import_data, 'delete_existing_custom_attribute_curves', unreal_engine_import_setting['delete_existing_custom_attribute_curves']), 58 | (import_options.anim_sequence_import_data, 'import_bone_tracks', unreal_engine_import_setting['import_bone_tracks']), 59 | (import_options.anim_sequence_import_data, 'set_material_drive_parameter_on_custom_attribute', unreal_engine_import_setting['set_material_drive_parameter_on_custom_attribute']), 60 | (import_options.anim_sequence_import_data, 'material_curve_suffixes', unreal_engine_import_setting['material_curve_suffixes']), 61 | (import_options.anim_sequence_import_data, 'remove_redundant_keys', unreal_engine_import_setting['remove_redundant_keys']), 62 | (import_options.anim_sequence_import_data, 'delete_existing_morph_target_curves', unreal_engine_import_setting['delete_existing_morph_target_curves']), 63 | (import_options.anim_sequence_import_data, 'do_not_import_curve_with_zero', unreal_engine_import_setting['do_not_import_curve_with_zero']), 64 | (import_options.anim_sequence_import_data, 'preserve_local_transform', unreal_engine_import_setting['preserve_local_transform']), 65 | 66 | # Transform 67 | 68 | (import_options.anim_sequence_import_data, 'import_translation', Vector(*unreal_engine_import_setting['import_translation'])), 69 | (import_options.anim_sequence_import_data, 'import_rotation', Rotator(*unreal_engine_import_setting['import_rotation'])), 70 | (import_options.anim_sequence_import_data, 'import_uniform_scale', unreal_engine_import_setting['import_uniform_scale']), 71 | 72 | # Misc. 73 | 74 | (import_options.anim_sequence_import_data, 'convert_scene', unreal_engine_import_setting['convert_scene']), 75 | (import_options.anim_sequence_import_data, 'force_front_x_axis', unreal_engine_import_setting['force_front_x_axis']), 76 | (import_options.anim_sequence_import_data, 'convert_scene_unit', unreal_engine_import_setting['convert_scene_unit']), 77 | (import_options, 'override_full_name', unreal_engine_import_setting['override_full_name']), 78 | ]): 79 | option.set_editor_property(prop, value) 80 | 81 | # Task 82 | 83 | import_task = AssetImportTask() 84 | 85 | for prop, value in [ 86 | ('automated', True), 87 | ('destination_name', ''), 88 | ('destination_path', target_path), 89 | ('filename', source_file), 90 | ('replace_existing', unreal_engine_import_setting['overwrite_file']), 91 | ('save', False), 92 | ('options', import_options), 93 | ]: 94 | import_task.set_editor_property(prop, value) 95 | 96 | AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task]) 97 | 98 | if unreal_engine_import_setting['temporary']: 99 | try: 100 | os.remove(source_file) 101 | except: 102 | print('Failed to Remove Temporary File, Location : ' + source_file) 103 | -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/ImportSkeletalMesh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unreal import ( 5 | EditorAssetLibrary, 6 | FbxImportUI, 7 | load_asset, 8 | AssetImportTask, 9 | AssetToolsHelpers, 10 | 11 | FBXImportContentType, 12 | VertexColorImportOption, 13 | Color, 14 | FBXNormalImportMethod, 15 | FBXNormalGenerationMethod, 16 | 17 | Vector, 18 | Rotator, 19 | 20 | MaterialSearchLocation 21 | ) 22 | 23 | # addon_path is Blender Unreal Engien 4 Workspace addon path 24 | # node_id is unreal engine project instance 25 | 26 | json_file = open(os.path.normpath(os.path.join(addon_path, 'temp', 'unreal_engine_import_setting.json')), 'r') 27 | unreal_engine_import_setting = json.loads(json_file.read()) 28 | json_file.close() 29 | 30 | target_path = '/' + os.path.join('Game', unreal_engine_import_setting['main_folder'], unreal_engine_import_setting['subfolder']).replace(os.sep, '/') 31 | 32 | if EditorAssetLibrary.does_directory_exist(directory_path=target_path): 33 | EditorAssetLibrary.make_directory(directory_path=target_path) 34 | 35 | for file in unreal_engine_import_setting['files']: 36 | source_file = file['path'].replace(os.sep, '/') 37 | 38 | if os.path.exists(source_file): 39 | import_options = FbxImportUI() 40 | 41 | unreal_engine_import_setting['vertex_override_color'] = [float(value) * 255 for value in unreal_engine_import_setting['vertex_override_color']] 42 | target_node_id, skeleton = file['skeleton'].split(':') 43 | 44 | for option, prop, value in ([ 45 | (import_options, 'import_mesh', True), 46 | (import_options, 'import_as_skeletal', True), 47 | (import_options, 'import_animations', False), 48 | 49 | (import_options, 'skeleton', (None if file['skeleton'] == 'CREATE' else load_asset(skeleton))), 50 | 51 | (import_options.skeletal_mesh_import_data, 'import_content_type', getattr(FBXImportContentType, unreal_engine_import_setting['import_content_type'])), 52 | (import_options.skeletal_mesh_import_data, 'vertex_color_import_option', getattr(VertexColorImportOption, unreal_engine_import_setting['vertex_color_import_option'])), 53 | (import_options.skeletal_mesh_import_data, 'vertex_override_color', Color(r=unreal_engine_import_setting['vertex_override_color'][0], g=unreal_engine_import_setting['vertex_override_color'][1], b=unreal_engine_import_setting['vertex_override_color'][2], a=unreal_engine_import_setting['vertex_override_color'][3])), 54 | (import_options.skeletal_mesh_import_data, 'update_skeleton_reference_pose', unreal_engine_import_setting['update_skeleton_reference_pose']), 55 | (import_options.skeletal_mesh_import_data, 'use_t0_as_ref_pose', unreal_engine_import_setting['use_t0_as_ref_pose']), 56 | (import_options.skeletal_mesh_import_data, 'preserve_smoothing_groups', unreal_engine_import_setting['preserve_smoothing_groups']), 57 | (import_options.skeletal_mesh_import_data, 'import_meshes_in_bone_hierarchy', unreal_engine_import_setting['import_meshes_in_bone_hierarchy']), 58 | (import_options.skeletal_mesh_import_data, 'import_morph_targets', unreal_engine_import_setting['import_morph_targets']), 59 | (import_options.skeletal_mesh_import_data, 'import_mesh_lo_ds', unreal_engine_import_setting['import_mesh_lo_ds']), 60 | (import_options.skeletal_mesh_import_data, 'normal_import_method', getattr(FBXNormalImportMethod, 'FBXNIM_' + unreal_engine_import_setting['normal_import_method'])), 61 | (import_options.skeletal_mesh_import_data, 'normal_generation_method', getattr(FBXNormalGenerationMethod, unreal_engine_import_setting['normal_generation_method'])), 62 | (import_options.skeletal_mesh_import_data, 'compute_weighted_normals', unreal_engine_import_setting['compute_weighted_normals']), 63 | (import_options.skeletal_mesh_import_data, 'threshold_position', unreal_engine_import_setting['threshold_position']), 64 | (import_options.skeletal_mesh_import_data, 'threshold_tangent_normal', unreal_engine_import_setting['threshold_tangent_normal']), 65 | (import_options.skeletal_mesh_import_data, 'threshold_uv', unreal_engine_import_setting['threshold_uv']), 66 | (import_options, 'create_physics_asset', (unreal_engine_import_setting['create_physics_asset'] == 'CREATE')), 67 | 68 | # Transform 69 | 70 | (import_options.skeletal_mesh_import_data, 'import_translation', Vector(*unreal_engine_import_setting['import_translation'])), 71 | (import_options.skeletal_mesh_import_data, 'import_rotation', Rotator(*unreal_engine_import_setting['import_rotation'])), 72 | (import_options.skeletal_mesh_import_data, 'import_uniform_scale', unreal_engine_import_setting['import_uniform_scale']), 73 | 74 | # Misc. 75 | 76 | (import_options.skeletal_mesh_import_data, 'convert_scene', unreal_engine_import_setting['convert_scene']), 77 | (import_options.skeletal_mesh_import_data, 'force_front_x_axis', unreal_engine_import_setting['force_front_x_axis']), 78 | (import_options.skeletal_mesh_import_data, 'convert_scene_unit', unreal_engine_import_setting['convert_scene_unit']), 79 | (import_options, 'override_full_name', unreal_engine_import_setting['override_full_name']), 80 | 81 | # Material 82 | 83 | (import_options.texture_import_data, 'material_search_location', getattr(MaterialSearchLocation, unreal_engine_import_setting['material_search_location'])), 84 | (import_options, 'import_materials', unreal_engine_import_setting['import_materials']), 85 | (import_options, 'import_textures', unreal_engine_import_setting['import_textures']), 86 | (import_options.texture_import_data, 'invert_normal_maps', unreal_engine_import_setting['invert_normal_maps']), 87 | (import_options.skeletal_mesh_import_data, 'reorder_material_to_fbx_order', unreal_engine_import_setting['reorder_material_to_fbx_order']), 88 | ]): 89 | option.set_editor_property(prop, value) 90 | 91 | # Task 92 | 93 | import_task = AssetImportTask() 94 | 95 | for prop, value in [ 96 | ('automated', True), 97 | ('destination_name', ''), 98 | ('destination_path', target_path), 99 | ('filename', source_file), 100 | ('replace_existing', unreal_engine_import_setting['overwrite_file']), 101 | ('save', False), 102 | ('options', import_options), 103 | ]: 104 | import_task.set_editor_property(prop, value) 105 | 106 | AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task]) 107 | 108 | if unreal_engine_import_setting['temporary']: 109 | try: 110 | os.remove(source_file) 111 | except: 112 | print('Failed to Remove Temporary File, Location : ' + source_file) 113 | -------------------------------------------------------------------------------- /UE4Workspace/ue4_script/ImportStaticMesh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unreal import ( 5 | EditorAssetLibrary, 6 | FbxImportUI, 7 | AssetImportTask, 8 | AssetToolsHelpers, 9 | 10 | VertexColorImportOption, 11 | Color, 12 | FBXNormalImportMethod, 13 | FBXNormalGenerationMethod, 14 | 15 | Vector, 16 | Rotator, 17 | 18 | MaterialSearchLocation 19 | ) 20 | 21 | # addon_path is Blender Unreal Engien 4 Workspace addon path 22 | # node_id is unreal engine project instance 23 | 24 | json_file = open(os.path.normpath(os.path.join(addon_path, 'temp', 'unreal_engine_import_setting.json')), 'r') 25 | unreal_engine_import_setting = json.loads(json_file.read()) 26 | json_file.close() 27 | 28 | target_path = '/' + os.path.join('Game', unreal_engine_import_setting['main_folder'], unreal_engine_import_setting['subfolder']).replace(os.sep, '/') 29 | 30 | if EditorAssetLibrary.does_directory_exist(directory_path=target_path): 31 | EditorAssetLibrary.make_directory(directory_path=target_path) 32 | 33 | for file in unreal_engine_import_setting['files']: 34 | source_file = file['path'].replace(os.sep, '/') 35 | 36 | if os.path.exists(source_file): 37 | import_options = FbxImportUI() 38 | 39 | unreal_engine_import_setting['vertex_override_color'] = [float(value) * 255 for value in unreal_engine_import_setting['vertex_override_color']] 40 | 41 | for option, prop, value in ([ 42 | (import_options, 'import_mesh', True), 43 | (import_options, 'import_as_skeletal', False), 44 | (import_options, 'import_animations', False), 45 | 46 | (import_options.static_mesh_import_data, 'auto_generate_collision', (True if file['custom_collision'] else unreal_engine_import_setting['auto_generate_collision'])), 47 | (import_options.static_mesh_import_data, 'vertex_color_import_option', getattr(VertexColorImportOption, unreal_engine_import_setting['vertex_color_import_option'])), 48 | (import_options.static_mesh_import_data, 'vertex_override_color', Color(r=unreal_engine_import_setting['vertex_override_color'][0], g=unreal_engine_import_setting['vertex_override_color'][1], b=unreal_engine_import_setting['vertex_override_color'][2], a=unreal_engine_import_setting['vertex_override_color'][3])), 49 | (import_options.static_mesh_import_data, 'remove_degenerates', unreal_engine_import_setting['remove_degenerates']), 50 | (import_options.static_mesh_import_data, 'build_adjacency_buffer', unreal_engine_import_setting['build_adjacency_buffer']), 51 | (import_options.static_mesh_import_data, 'build_reversed_index_buffer', unreal_engine_import_setting['build_reversed_index_buffer']), 52 | (import_options.static_mesh_import_data, 'generate_lightmap_u_vs', (False if file['custom_lightmap'] else unreal_engine_import_setting['generate_lightmap_u_vs'])), 53 | (import_options.static_mesh_import_data, 'one_convex_hull_per_ucx', unreal_engine_import_setting['one_convex_hull_per_ucx']), 54 | (import_options.static_mesh_import_data, 'combine_meshes', unreal_engine_import_setting['combine_meshes']), 55 | (import_options.static_mesh_import_data, 'transform_vertex_to_absolute', unreal_engine_import_setting['transform_vertex_to_absolute']), 56 | (import_options.static_mesh_import_data, 'bake_pivot_in_vertex', unreal_engine_import_setting['bake_pivot_in_vertex']), 57 | (import_options.static_mesh_import_data, 'import_mesh_lo_ds', (True if bool(file['lod']) else unreal_engine_import_setting['import_mesh_lo_ds'])), 58 | (import_options.static_mesh_import_data, 'normal_import_method', getattr(FBXNormalImportMethod, 'FBXNIM_' + unreal_engine_import_setting['normal_import_method'])), 59 | (import_options.static_mesh_import_data, 'normal_generation_method', getattr(FBXNormalGenerationMethod, unreal_engine_import_setting['normal_generation_method'])), 60 | (import_options.static_mesh_import_data, 'compute_weighted_normals', unreal_engine_import_setting['compute_weighted_normals']), 61 | 62 | # Transform 63 | 64 | (import_options.static_mesh_import_data, 'import_translation', Vector(*unreal_engine_import_setting['import_translation'])), 65 | (import_options.static_mesh_import_data, 'import_rotation', Rotator(*unreal_engine_import_setting['import_rotation'])), 66 | (import_options.static_mesh_import_data, 'import_uniform_scale', unreal_engine_import_setting['import_uniform_scale']), 67 | 68 | # Misc. 69 | 70 | (import_options.static_mesh_import_data, 'convert_scene', unreal_engine_import_setting['convert_scene']), 71 | (import_options.static_mesh_import_data, 'force_front_x_axis', unreal_engine_import_setting['force_front_x_axis']), 72 | (import_options.static_mesh_import_data, 'convert_scene_unit', unreal_engine_import_setting['convert_scene_unit']), 73 | (import_options, 'override_full_name', unreal_engine_import_setting['override_full_name']), 74 | 75 | # LODSetting 76 | 77 | (import_options, 'auto_compute_lod_distances', (file['auto_compute_lod_distances'] if bool(file['lod']) else unreal_engine_import_setting['auto_compute_lod_distances'])), 78 | (import_options, 'minimum_lod_number', unreal_engine_import_setting['minimum_lod_number']), 79 | (import_options, 'lod_number', unreal_engine_import_setting['lod_number']), 80 | 81 | # Material 82 | 83 | (import_options.texture_import_data, 'material_search_location', getattr(MaterialSearchLocation, unreal_engine_import_setting['material_search_location'])), 84 | (import_options, 'import_materials', unreal_engine_import_setting['import_materials']), 85 | (import_options, 'import_textures', unreal_engine_import_setting['import_textures']), 86 | (import_options.texture_import_data, 'invert_normal_maps', unreal_engine_import_setting['invert_normal_maps']), 87 | (import_options.static_mesh_import_data, 'reorder_material_to_fbx_order', unreal_engine_import_setting['reorder_material_to_fbx_order']), 88 | ]+[ 89 | # LOD Screen Size 90 | (import_options, 'lod_distance' + str(index), screen_size) for index, screen_size in enumerate((file['lod']+[unreal_engine_import_setting['lod_distance' + str(index)] for index in range(8)][len(file['lod']):])) 91 | ]): 92 | option.set_editor_property(prop, value) 93 | 94 | # Task 95 | 96 | import_task = AssetImportTask() 97 | 98 | for prop, value in [ 99 | ('automated', True), 100 | ('destination_name', ''), 101 | ('destination_path', target_path), 102 | ('filename', source_file), 103 | ('replace_existing', unreal_engine_import_setting['overwrite_file']), 104 | ('save', False), 105 | ('options', import_options), 106 | ]: 107 | import_task.set_editor_property(prop, value) 108 | 109 | AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task]) 110 | 111 | if unreal_engine_import_setting['temporary']: 112 | try: 113 | os.remove(source_file) 114 | except: 115 | print('Failed to Remove Temporary File, Location : ' + source_file) 116 | -------------------------------------------------------------------------------- /UE4Workspace/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anasrar/Blender-UE4-Workspace/6c93aa80e5c63c2b562aa8ad88fad6204031e64a/UE4Workspace/utils/__init__.py -------------------------------------------------------------------------------- /UE4Workspace/utils/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import re 4 | import json 5 | import bpy 6 | from bpy.types import Panel as OriginalPanel, Operator as OriginalOperator 7 | from mathutils import Matrix 8 | from . connect import remote 9 | 10 | def create_matrix_scale_from_vector(vec): 11 | return Matrix.Scale(vec[0], 4, (1.0, 0.0, 0.0)) @ Matrix.Scale(vec[1], 4, (0.0, 1.0, 0.0)) @ Matrix.Scale(vec[2], 4, (0.0, 0.0, 1.0)) 12 | 13 | class Panel(OriginalPanel): 14 | bl_category = 'UE4Workspace' 15 | bl_space_type = 'VIEW_3D' 16 | bl_region_type = 'UI' 17 | bl_options = {'DEFAULT_CLOSED'} 18 | 19 | @classmethod 20 | def poll(cls, context): 21 | return (context.mode == 'OBJECT') 22 | 23 | class ExperimentalPanel(OriginalPanel): 24 | bl_category = 'UE4Workspace' 25 | bl_space_type = 'VIEW_3D' 26 | bl_region_type = 'UI' 27 | bl_options = {'DEFAULT_CLOSED'} 28 | 29 | @classmethod 30 | def poll(cls, context): 31 | preferences = context.preferences.addons['UE4Workspace'].preferences 32 | misc = preferences.misc 33 | return (context.mode == 'OBJECT' and misc.experimental_features) 34 | 35 | class ObjectPanel(OriginalPanel): 36 | bl_category = 'UE4Workspace' 37 | bl_space_type = 'VIEW_3D' 38 | bl_region_type = 'UI' 39 | 40 | class ObjectSubPanel(Panel): 41 | bl_parent_id = 'UE4WORKSPACE_PT_ObjectPanel' 42 | 43 | class ExportOptionPanel(Panel): 44 | 45 | def draw_property(self, data, properties): 46 | layout = self.layout 47 | 48 | for tab, data_properties in properties.items(): 49 | layout.prop(data, tab, icon=('TRIA_DOWN' if getattr(data, tab, False) else 'TRIA_RIGHT'), emboss=False) 50 | if getattr(data, tab, False): 51 | box = layout.box() 52 | 53 | for label_str, property_str in data_properties: 54 | row = box.row() 55 | split = row.split(factor=0.6) 56 | col = split.column() 57 | col.alignment = 'RIGHT' 58 | col.label(text=label_str) 59 | col = split.column() 60 | if property_str == 'frame_import_range': 61 | col.enabled = data.animation_length == 'FBXALIT_SET_RANGE' 62 | col.prop(data, property_str, text='Min', index=0) 63 | col.prop(data, property_str, text='Max', index=1) 64 | elif property_str == 'material_curve_suffixes': 65 | material_curve_suffixes = getattr(data, property_str, '').split('|') if bool(getattr(data, property_str, '')) else [] 66 | 67 | row = col.row(align=True) 68 | row.label(text=str(len(material_curve_suffixes)) + ' Array' if bool(material_curve_suffixes) else '0 Array') 69 | 70 | row.operator('ue4workspace.add_material_curve_suffix',icon='PLUS', text='') 71 | row.operator('ue4workspace.clear_material_curve_suffix',icon='TRASH', text='') 72 | 73 | if bool(material_curve_suffixes): 74 | for index, surffix in enumerate(material_curve_suffixes): 75 | row = box.row() 76 | split = row.split(factor=0.6) 77 | col = split.column() 78 | col.alignment = 'RIGHT' 79 | col.label(text=str(index)) 80 | row = split.row(align=True) 81 | row.label(text=surffix) 82 | op = row.operator('ue4workspace.edit_material_curve_suffix',icon='GREASEPENCIL', text='') 83 | op.index = index 84 | op.val = surffix 85 | row.operator('ue4workspace.remove_material_curve_suffix_index',icon='TRASH', text='').index = index 86 | else: 87 | col.prop(data, property_str, text='') 88 | 89 | class ExportOperator(OriginalOperator): 90 | 91 | ext_file = '' 92 | collections_dict = {} 93 | temp_attach_mute_state = False 94 | temp_custom_collision = [] 95 | temp_socket = [] 96 | temp_lod = [] 97 | temp_main_lod_matrix_and_parent = None 98 | temp_skeletal_meshes = [] 99 | temp_hair_particle = [] 100 | 101 | @classmethod 102 | def description(cls, context, properties): 103 | preferences = context.preferences.addons['UE4Workspace'].preferences 104 | description = '' 105 | 106 | if preferences.export.type in ['FILE', 'BOTH']: 107 | return '' if bool(preferences.export.export_folder.strip()) else 'File folder not valid' 108 | return '' if bool(preferences.export.temp_folder.strip()) else 'Temporary folder not valid' 109 | 110 | @classmethod 111 | def poll(cls, context): 112 | preferences = context.preferences.addons['UE4Workspace'].preferences 113 | 114 | if preferences.export.type in ['FILE', 'BOTH']: 115 | return bool(preferences.export.export_folder.strip()) and context.mode == 'OBJECT' 116 | return bool(preferences.export.temp_folder.strip()) and context.mode == 'OBJECT' 117 | 118 | def safe_string_path(self, string): 119 | return re.sub("[\\/:<>\'\"|?*&]", '', string).strip() 120 | 121 | def create_string_directory(self, root, subfolder): 122 | subfolder_safe_string = self.safe_string_path(subfolder) 123 | return os.path.join(root, subfolder_safe_string) 124 | 125 | def create_directory_if_not_exist(self, directory, subfolder): 126 | if not os.path.isdir(directory) and subfolder: 127 | os.mkdir(directory) 128 | 129 | def is_file_exist(self, *args): 130 | return os.path.isfile(os.path.join(*args)) 131 | 132 | def unhide_collection(self, *args): 133 | self.collections_dict = {} 134 | for collection, state in args: 135 | collection_data = bpy.data.collections.get(collection, False) 136 | self.collections_dict[collection] = { 137 | 'render': None, 138 | 'select': None, 139 | 'viewport': None 140 | } 141 | if collection_data and state: 142 | for key in self.collections_dict[collection]: 143 | self.collections_dict[collection][key] = getattr(collection_data, 'hide_' + key) 144 | setattr(collection_data, 'hide_' + key, False) 145 | 146 | def restore_collection(self, *args): 147 | for collection, state in args: 148 | collection_data = bpy.data.collections.get(collection, False) 149 | if collection_data and state: 150 | for key, val in self.collections_dict[collection].items(): 151 | setattr(collection_data, 'hide_' + key, val) 152 | self.collections_dict = {} 153 | 154 | def unreal_engine_exec_script(self, script='ImportStaticMesh.py', unreal_engine_import_setting={}): 155 | preferences = bpy.context.preferences.addons['UE4Workspace'].preferences 156 | if preferences.export.type in ['UNREAL', 'BOTH'] and remote.remote_nodes: 157 | unreal_engine_import_setting_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'temp', 'unreal_engine_import_setting.json')).replace(os.sep, '/') 158 | 159 | file = open(unreal_engine_import_setting_path, 'w+') 160 | file.write(json.dumps(unreal_engine_import_setting, indent=4)) 161 | file.close() 162 | 163 | remote.exec_script(script) 164 | 165 | def prepare_custom_collision(self, obj): 166 | self.temp_custom_collision = [] 167 | base_collision_name = 'UCX_' + obj.name + '_' 168 | collision_collection = bpy.data.collections.get('UE4CustomCollision', False) 169 | if collision_collection: 170 | for index, collision_object in enumerate([collision_object for collision_object in collision_collection.objects if collision_object.parent is obj and collision_object.type == 'MESH' and collision_object.data.is_custom_collision], start=1): 171 | self.temp_custom_collision.append((collision_object, collision_object.name, collision_object.hide_get(), collision_object.hide_select, collision_object.hide_viewport)) 172 | 173 | collision_object.hide_set(False) 174 | collision_object.hide_select = False 175 | collision_object.hide_viewport = False 176 | collision_object.select_set(state=True) 177 | 178 | collision_object.name = base_collision_name + ('0' if index <= 9 else '') + str(index) 179 | 180 | def restore_custom_collision(self): 181 | for collision_object, original_name, hide, hide_select, hide_viewport in self.temp_custom_collision: 182 | collision_object.hide_set(hide) 183 | collision_object.hide_select = hide_select 184 | collision_object.hide_viewport = hide_viewport 185 | collision_object.select_set(state=False) 186 | 187 | collision_object.name = original_name 188 | 189 | self.temp_custom_collision = [] 190 | 191 | def prepare_socket(self, obj): 192 | self.temp_socket = [] 193 | socket_collection = bpy.data.collections.get('UE4Socket', False) 194 | if socket_collection: 195 | for socket_object in [socket_object for socket_object in socket_collection.objects if socket_object.parent is obj and socket_object.type == 'EMPTY' and socket_object.is_socket]: 196 | self.temp_socket.append((socket_object, socket_object.hide_get(), socket_object.hide_select, socket_object.hide_viewport)) 197 | 198 | socket_object.hide_set(False) 199 | socket_object.hide_select = False 200 | socket_object.hide_viewport = False 201 | socket_object.select_set(state=True) 202 | 203 | socket_object.scale /= 100 204 | 205 | socket_object.rotation_euler.x += math.radians(90) 206 | 207 | socket_object.name = 'SOCKET_' + socket_object.name 208 | 209 | def restore_socket(self): 210 | for socket_object, hide, hide_select, hide_viewport in self.temp_socket: 211 | socket_object.hide_set(hide) 212 | socket_object.hide_select = hide_select 213 | socket_object.hide_viewport = hide_viewport 214 | socket_object.select_set(state=False) 215 | 216 | socket_object.scale *= 100 217 | socket_object.rotation_euler.x -= math.radians(90) 218 | 219 | socket_object.name = socket_object.name[7:] 220 | 221 | self.temp_socket = [] 222 | 223 | def prepare_lod(self, obj): 224 | self.temp_lod = [] 225 | lods_data = [(lod.obj, lod.screen_size) for lod in obj.data.lods if lod.obj is not None] 226 | if bool(lods_data): 227 | lod_parent = bpy.data.objects.new('LOD_' + obj.name, None) 228 | 229 | for collection in obj.users_collection: 230 | collection.objects.link(lod_parent) 231 | 232 | lod_parent.matrix_world.translation = obj.matrix_world.copy().translation 233 | lod_parent.empty_display_size = 2 234 | lod_parent.empty_display_type = 'ARROWS' 235 | lod_parent['fbx_type'] = 'LodGroup' 236 | lod_parent.select_set(state=True) 237 | 238 | self.temp_main_lod_matrix_and_parent = (lod_parent, obj, obj.matrix_world.copy(), obj.parent) 239 | 240 | obj.parent = lod_parent 241 | 242 | obj.matrix_parent_inverse = lod_parent.matrix_world.inverted() 243 | 244 | bpy.context.view_layer.update() 245 | 246 | for lod_obj, screen_size in lods_data: 247 | lod_obj_copy = lod_obj.copy() 248 | lod_obj_copy.parent = lod_parent 249 | lod_obj_copy.matrix_world = obj.matrix_world.copy() 250 | 251 | for collection in lod_obj.users_collection: 252 | collection.objects.link(lod_obj_copy) 253 | 254 | lod_obj_copy.hide_set(False) 255 | lod_obj_copy.hide_select = False 256 | lod_obj_copy.hide_viewport = False 257 | lod_obj_copy.select_set(state=True) 258 | 259 | self.temp_lod.append(lod_obj_copy) 260 | 261 | def restore_lod(self): 262 | if bool(self.temp_lod): 263 | lod_parent, obj, obj_matrix_world, obj_original_parent = self.temp_main_lod_matrix_and_parent 264 | 265 | obj.parent = obj_original_parent 266 | obj.matrix_world = obj_matrix_world 267 | 268 | for lod_obj_copy in self.temp_lod: 269 | bpy.data.objects.remove(lod_obj_copy) 270 | 271 | bpy.data.objects.remove(lod_parent) 272 | self.temp_lod = [] 273 | self.temp_main_lod_matrix_and_parent = None 274 | 275 | def mute_attach_constraint(self, obj): 276 | constraint = obj.constraints.get('attach_to') 277 | if constraint: 278 | self.temp_attach_mute_state = constraint.mute 279 | constraint.mute = True 280 | 281 | def unmute_attach_constraint(self, obj): 282 | constraint = obj.constraints.get('attach_to') 283 | if constraint: 284 | constraint.mute = self.temp_attach_mute_state 285 | 286 | def prepare_skeletal_meshes(self, obj): 287 | self.temp_skeletal_meshes = [] 288 | meshes = [children_obj for children_obj in obj.children if children_obj.type == 'MESH' and children_obj.data.is_export_skeletal_mesh_part] 289 | if bool(meshes): 290 | for skeletal_mesh_object in meshes: 291 | self.temp_skeletal_meshes.append((skeletal_mesh_object, skeletal_mesh_object.hide_get(), skeletal_mesh_object.hide_select, skeletal_mesh_object.hide_viewport)) 292 | 293 | skeletal_mesh_object.hide_set(False) 294 | skeletal_mesh_object.hide_select = False 295 | skeletal_mesh_object.hide_viewport = False 296 | skeletal_mesh_object.select_set(state=True) 297 | 298 | def restore_skeletal_meshes(self, obj): 299 | for skeletal_mesh_object, hide, hide_select, hide_viewport in self.temp_skeletal_meshes: 300 | 301 | skeletal_mesh_object.hide_set(hide) 302 | skeletal_mesh_object.hide_select = hide_select 303 | skeletal_mesh_object.hide_viewport = hide_viewport 304 | skeletal_mesh_object.select_set(state=False) 305 | 306 | self.temp_skeletal_meshes = [] 307 | 308 | def prepare_groom(self, obj): 309 | self.temp_hair_particle = [obj.show_instancer_for_render, obj.show_instancer_for_viewport] 310 | obj.show_instancer_for_render, obj.show_instancer_for_viewport = [False, True] 311 | 312 | def restore_groom(self, obj): 313 | obj.show_instancer_for_render, obj.show_instancer_for_viewport = self.temp_hair_particle 314 | self.temp_hair_particle = [] -------------------------------------------------------------------------------- /UE4Workspace/utils/connect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import abc 3 | from abc import ABC, abstractmethod 4 | 5 | from . remote_execution import REMOTE_EXEC, RemoteExecutionConfig 6 | 7 | class AbstractConnect(ABC): 8 | 9 | @property 10 | @abstractmethod 11 | def _remote(self): 12 | pass 13 | 14 | @abstractmethod 15 | def connect(self, DEFAULT_MULTICAST_TTL=0, DEFAULT_MULTICAST_GROUP_ENDPOINT=('239.0.0.1', 6766), DEFAULT_MULTICAST_BIND_ADDRESS='0.0.0.0', DEFAULT_COMMAND_ENDPOINT=('127.0.0.1', 6776)): 16 | pass 17 | 18 | @property 19 | @abstractmethod 20 | def is_connect(self): 21 | pass 22 | 23 | @abstractmethod 24 | def disconnect(self): 25 | pass 26 | 27 | @abstractmethod 28 | def exec_script(self, script='ImportStaticMesh.py'): 29 | pass 30 | 31 | class ConnectToUnrealEngine(AbstractConnect): 32 | 33 | _remote = REMOTE_EXEC 34 | 35 | def connect(self, DEFAULT_MULTICAST_TTL=0, DEFAULT_MULTICAST_GROUP_ENDPOINT=('239.0.0.1', 6766), DEFAULT_MULTICAST_BIND_ADDRESS='0.0.0.0', DEFAULT_COMMAND_ENDPOINT=('127.0.0.1', 6776)): 36 | self._remote.start(config=RemoteExecutionConfig(DEFAULT_MULTICAST_TTL=DEFAULT_MULTICAST_TTL, DEFAULT_MULTICAST_GROUP_ENDPOINT=DEFAULT_MULTICAST_GROUP_ENDPOINT, DEFAULT_MULTICAST_BIND_ADDRESS=DEFAULT_MULTICAST_BIND_ADDRESS, DEFAULT_COMMAND_ENDPOINT=DEFAULT_COMMAND_ENDPOINT)) 37 | 38 | @property 39 | def is_connect(self): 40 | return not (self._remote._broadcast_connection is None) 41 | 42 | def disconnect(self): 43 | self._remote.stop() 44 | 45 | @property 46 | def remote_nodes(self): 47 | return self._remote.remote_nodes 48 | 49 | def exec_script(self, script='ImportStaticMesh.py'): 50 | for node_id in [user['node_id'] for user in self._remote.remote_nodes]: 51 | self._remote.open_command_connection(node_id) 52 | script_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'ue4_script', script)).replace(os.sep, '/') 53 | addon_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')).replace(os.sep, '/') 54 | self._remote.run_command('exec(open("' + script_path + '").read(), {"addon_path": "' + addon_path + '", "node_id": "' + str(node_id) + '"})', exec_mode='ExecuteStatement') 55 | self._remote.close_command_connection() 56 | 57 | remote = ConnectToUnrealEngine() 58 | skeletons = [] -------------------------------------------------------------------------------- /UE4Workspace/utils/operator.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.utils import register_class, unregister_class 3 | from bpy.types import Operator 4 | 5 | class OP_ToggleVisibilityObject(Operator): 6 | bl_idname = 'ue4workspace.toggle_visibility_object' 7 | bl_label = 'Toggle Visibility Object' 8 | bl_description = 'Toggle Visibility Object' 9 | bl_options = {'UNDO'} 10 | 11 | object_name: bpy.props.StringProperty(name='Object name') 12 | 13 | def execute(self, context): 14 | obj = context.scene.objects[self.object_name] 15 | obj.hide_set(not obj.hide_get()) 16 | return {'FINISHED'} 17 | 18 | class OP_RemoveObject(Operator): 19 | bl_idname = 'ue4workspace.remove_object' 20 | bl_label = 'Remove Object' 21 | bl_description = 'Remove Object' 22 | bl_options = {'UNDO'} 23 | 24 | object_name: bpy.props.StringProperty(name='Object name') 25 | 26 | def execute(self, context): 27 | obj = context.scene.objects[self.object_name] 28 | bpy.data.objects.remove(obj, do_unlink=True) 29 | return {'FINISHED'} 30 | 31 | list_class_to_register = [ 32 | OP_ToggleVisibilityObject, 33 | OP_RemoveObject 34 | ] 35 | 36 | def register(): 37 | for x in list_class_to_register: 38 | register_class(x) 39 | 40 | def unregister(): 41 | for x in list_class_to_register[::-1]: 42 | unregister_class(x) -------------------------------------------------------------------------------- /build-zip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # script for build UE4Workspace.zip 3 | 4 | import os 5 | import zipfile 6 | 7 | if __name__ == '__main__': 8 | zipf = zipfile.ZipFile('UE4Workspace.zip', 'w', zipfile.ZIP_DEFLATED) 9 | 10 | for dirname, subdirs, files in os.walk('UE4Workspace'): 11 | if '__pycache__' in subdirs: 12 | subdirs.remove('__pycache__') 13 | zipf.write(dirname) 14 | for filename in files: 15 | zipf.write(os.path.join(dirname, filename)) 16 | 17 | zipf.close() --------------------------------------------------------------------------------