From ff9909907c415b23ffca692dcdb6e60e94385f49 Mon Sep 17 00:00:00 2001 From: Benjamin Marquez Date: Sun, 5 Jan 2025 00:13:08 -0500 Subject: [PATCH 1/3] Standalone material exporting --- .../fortnite_porting/processing/enums.py | 3 + .../processing/import_context.py | 62 ++++++++++++++----- .../fortnite_porting/processing/utils.py | 4 +- FortnitePorting/Enums.cs | 13 +++- FortnitePorting/Export/Exporter.cs | 4 ++ .../Export/Types/MaterialExport.cs | 19 ++++++ 6 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 FortnitePorting/Export/Types/MaterialExport.cs diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py index ffa7cee6..a8c31051 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py @@ -69,6 +69,8 @@ class EExportType(IntEnum): SOUND = auto() FONT = auto() POSE_ASSET = auto() + MATERIAL = auto() + MATERIAL_INSTANCE = auto() class EPrimitiveExportType(IntEnum): @@ -78,6 +80,7 @@ class EPrimitiveExportType(IntEnum): SOUND = auto() FONT = auto() POSE_ASSET = auto() + MATERIAL = auto() class EFortCustomPartType(IntEnum): diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py index 9f9f4829..7bf451e5 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py @@ -4,6 +4,7 @@ import bpy import traceback +from math import radians from .mappings import * from .material import * from .enums import * @@ -58,6 +59,9 @@ def run(self, data): case EPrimitiveExportType.POSE_ASSET: self.import_pose_asset_data(data, get_selected_armature(), None) pass + case EPrimitiveExportType.MATERIAL: + self.import_material_standalone(data) + pass def import_mesh_data(self, data): rig_type = ERigType(self.options.get("RigType")) @@ -181,6 +185,9 @@ def import_model(self, mesh, parent=None, can_reorient=True, can_spawn_at_3d_cur imported_mesh = get_armature_mesh(imported_object) else: imported_object = self.import_mesh(path, can_reorient=can_reorient) + if imported_object is None: + Log.warn(f"Import failed for object at path: {path}") + return imported_object imported_object.name = name imported_mesh = get_armature_mesh(imported_object) @@ -420,12 +427,14 @@ def import_material(self, material_slot, material_data, meta): additional_hash = 0 texture_data = meta.get("TextureData") - for data in texture_data: - additional_hash += data.get("Hash") + if texture_data is not None: + for data in texture_data: + additional_hash += data.get("Hash") override_parameters = where(self.override_parameters, lambda param: param.get("MaterialNameToAlter") in [material_name, "Global"]) - for parameters in override_parameters: - additional_hash += parameters.get("Hash") + if override_parameters is not None: + for parameters in override_parameters: + additional_hash += parameters.get("Hash") if additional_hash != 0: material_hash += additional_hash @@ -465,20 +474,22 @@ def import_material(self, material_slot, material_data, meta): switches = material_data.get("Switches") component_masks = material_data.get("ComponentMasks") - for data in texture_data: - replace_or_add_parameter(textures, data.get("Diffuse")) - replace_or_add_parameter(textures, data.get("Normal")) - replace_or_add_parameter(textures, data.get("Specular")) - - for parameters in override_parameters: - for texture in parameters.get("Textures"): - replace_or_add_parameter(textures, texture) - - for scalar in parameters.get("Scalars"): - replace_or_add_parameter(scalars, scalar) + if texture_data is not None: + for data in texture_data: + replace_or_add_parameter(textures, data.get("Diffuse")) + replace_or_add_parameter(textures, data.get("Normal")) + replace_or_add_parameter(textures, data.get("Specular")) - for vector in parameters.get("Vectors"): - replace_or_add_parameter(vectors, vector) + if override_parameters is not None: + for parameters in override_parameters: + for texture in parameters.get("Textures"): + replace_or_add_parameter(textures, texture) + + for scalar in parameters.get("Scalars"): + replace_or_add_parameter(scalars, scalar) + + for vector in parameters.get("Vectors"): + replace_or_add_parameter(vectors, vector) output_node = nodes.new(type="ShaderNodeOutputMaterial") output_node.location = (200, 0) @@ -1489,3 +1500,20 @@ def import_pose_asset_data(self, data, selected_armature, part_type): selected_mesh.show_only_shape_key = original_shape_key_lock bpy.ops.object.mode_set(mode=original_mode) bpy.context.view_layer.objects.active = original_selected_object + + def import_material_standalone(self, data): + materials = data.get("Materials") + + if materials is None: + return + + self.collection = create_or_get_collection("Materials") if self.options.get("ImportIntoCollection") else bpy.context.scene.collection + for material in materials: + name = material.get("Name") + Log.info(f"Importing Material: {name}") + + bpy.ops.mesh.primitive_ico_sphere_add() + mat_mesh = bpy.context.active_object + mat_mesh.name = name + mat_mesh.data.materials.append(bpy.data.materials.new("Temp")) + self.import_material(mat_mesh.material_slots[material.get("Slot")], material, {}) \ No newline at end of file diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/utils.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/utils.py index 2cb2747f..d05e0861 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/utils.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/utils.py @@ -92,8 +92,8 @@ def merge_armatures(parts): if socket.casefold() == "hat": socket = "head" - # Could just change the constrain_object() calls to use socket.casefold(), but didn't want to assume all were lowercase - if socket == "Tail": + # Account for skins with lowercase tail socket bone + if socket == "Tail" and "tail" in master_skeleton.pose.bones: socket = "tail" constraint_object(skeleton, master_skeleton, socket, [0, 0, 0], rot=False) diff --git a/FortnitePorting/Enums.cs b/FortnitePorting/Enums.cs index d2522805..d506575b 100644 --- a/FortnitePorting/Enums.cs +++ b/FortnitePorting/Enums.cs @@ -193,7 +193,13 @@ public enum EExportType Font, [Description("Pose Asset"), Export(EPrimitiveExportType.PoseAsset)] - PoseAsset + PoseAsset, + + [Description("Material"), Export(EPrimitiveExportType.Material)] + Material, + + [Description("MaterialInstance"), Export(EPrimitiveExportType.Material)] + MaterialInstance } public enum EPrimitiveExportType @@ -214,7 +220,10 @@ public enum EPrimitiveExportType Font, [Description("PoseAsset")] - PoseAsset + PoseAsset, + + [Description("Material")] + Material } public enum EAssetSortType diff --git a/FortnitePorting/Export/Exporter.cs b/FortnitePorting/Export/Exporter.cs index 31ea7746..06b08977 100644 --- a/FortnitePorting/Export/Exporter.cs +++ b/FortnitePorting/Export/Exporter.cs @@ -7,6 +7,7 @@ using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Animation; using CUE4Parse.UE4.Assets.Exports.Engine.Font; +using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.Sound; using CUE4Parse.UE4.Assets.Exports.StaticMesh; @@ -145,6 +146,8 @@ public static EExportType DetermineExportType(UObject asset) UAnimMontage => EExportType.Animation, UFontFace => EExportType.Font, UPoseAsset => EExportType.PoseAsset, + UMaterialInstance => EExportType.MaterialInstance, + UMaterial => EExportType.Material, _ => EExportType.None }; @@ -213,6 +216,7 @@ private static BaseExport CreateExport(string name, UObject asset, EExportType e EPrimitiveExportType.Animation => new AnimExport(name, asset, styles, exportType, metaData), EPrimitiveExportType.Font => new FontExport(name, asset, styles, exportType, metaData), EPrimitiveExportType.PoseAsset => new PoseAssetExport(name, asset, styles, exportType, metaData), + EPrimitiveExportType.Material => new MaterialExport(name, asset, styles, exportType, metaData), _ => throw new NotImplementedException($"Exporting {primitiveType} assets is not supported yet.") }; diff --git a/FortnitePorting/Export/Types/MaterialExport.cs b/FortnitePorting/Export/Types/MaterialExport.cs new file mode 100644 index 00000000..d1652a17 --- /dev/null +++ b/FortnitePorting/Export/Types/MaterialExport.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Material; +using FortnitePorting.Export.Models; +using FortnitePorting.Models.Assets; +using FortnitePorting.Shared.Extensions; + +namespace FortnitePorting.Export.Types; + +public class MaterialExport : BaseExport +{ + public readonly List Materials = []; + + public MaterialExport(string name, UObject asset, BaseStyleData[] styles, EExportType exportType, ExportDataMeta metaData) : base(name, asset, styles, exportType, metaData) + { + Materials.AddIfNotNull(Exporter.Material((UMaterialInterface)asset, 0)); + } +} \ No newline at end of file From 9469b2dda1b1f7262c039353c57ee7a7e1d21009 Mon Sep 17 00:00:00 2001 From: Benjamin Marquez Date: Sun, 5 Jan 2025 00:54:50 -0500 Subject: [PATCH 2/3] Addressing PR comments: adding blend data export option --- .../fortnite_porting/processing/enums.py | 4 ++ .../processing/import_context.py | 39 +++++++++++-------- .../Settings/BlenderSettingsViewModel.cs | 10 +++++ .../Views/Settings/BlenderSettingsView.axaml | 7 ++++ 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py index a8c31051..26328a56 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/enums.py @@ -101,6 +101,10 @@ def _missing_(cls, value): class ETextureImportMethod(IntEnum): DATA = 0 OBJECT = auto() + +class EMaterialImportMethod(IntEnum): + DATA = 0 + OBJECT = auto() class ERigType(IntEnum): DEFAULT = 0 diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py index 7bf451e5..9e4a7aa5 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py @@ -415,12 +415,13 @@ def import_image(self, path: str): return bpy.data.images.load(path, check_existing=True) - def import_material(self, material_slot, material_data, meta): + def import_material(self, material_slot, material_data, meta, as_material_data=False): # object ref mat slots for instancing - temp_material = material_slot.material - material_slot.link = 'OBJECT' if self.type in [EExportType.WORLD, EExportType.PREFAB] else 'DATA' - material_slot.material = temp_material + if not as_material_data: + temp_material = material_slot.material + material_slot.link = 'OBJECT' if self.type in [EExportType.WORLD, EExportType.PREFAB] else 'DATA' + material_slot.material = temp_material material_name = material_data.get("Name") material_hash = material_data.get("Hash") @@ -440,7 +441,7 @@ def import_material(self, material_slot, material_data, meta): material_hash += additional_hash material_name += f"_{hash_code(material_hash)}" - if existing_material := first(bpy.data.materials, lambda mat: mat.get("Hash") == hash_code(material_hash)): + if existing_material := first(bpy.data.materials, lambda mat: mat.get("Hash") == hash_code(material_hash)) and not as_material_data: material_slot.material = existing_material return @@ -448,13 +449,14 @@ def import_material(self, material_slot, material_data, meta): if (name_existing := first(bpy.data.materials, lambda mat: mat.name == material_name)) and name_existing.get("Hash") != material_hash: material_name += f"_{hash_code(material_hash)}" - if material_slot.material.name.casefold() != material_name.casefold(): + if not as_material_data and material_slot.material.name.casefold() != material_name.casefold(): material_slot.material = bpy.data.materials.new(material_name) - material_slot.material["Hash"] = hash_code(material_hash) - material_slot.material["OriginalName"] = material_data.get("Name") + if not as_material_data: + material_slot.material["Hash"] = hash_code(material_hash) + material_slot.material["OriginalName"] = material_data.get("Name") - material = material_slot.material + material = bpy.data.materials.new(material_name) if as_material_data else material_slot.material material.use_nodes = True material.surface_render_method = "DITHERED" @@ -1502,18 +1504,23 @@ def import_pose_asset_data(self, data, selected_armature, part_type): bpy.context.view_layer.objects.active = original_selected_object def import_material_standalone(self, data): + is_object_import = EMaterialImportMethod.OBJECT == EMaterialImportMethod(self.options.get("MaterialImportMethod")) materials = data.get("Materials") if materials is None: return - self.collection = create_or_get_collection("Materials") if self.options.get("ImportIntoCollection") else bpy.context.scene.collection + if is_object_import: + self.collection = create_or_get_collection("Materials") if self.options.get("ImportIntoCollection") else bpy.context.scene.collection + for material in materials: name = material.get("Name") Log.info(f"Importing Material: {name}") - - bpy.ops.mesh.primitive_ico_sphere_add() - mat_mesh = bpy.context.active_object - mat_mesh.name = name - mat_mesh.data.materials.append(bpy.data.materials.new("Temp")) - self.import_material(mat_mesh.material_slots[material.get("Slot")], material, {}) \ No newline at end of file + if is_object_import: + bpy.ops.mesh.primitive_ico_sphere_add() + mat_mesh = bpy.context.active_object + mat_mesh.name = name + mat_mesh.data.materials.append(bpy.data.materials.new(name)) + self.import_material(mat_mesh.material_slots[material.get("Slot")], material, {}) + else: + self.import_material(None, material, {}, True) \ No newline at end of file diff --git a/FortnitePorting/ViewModels/Settings/BlenderSettingsViewModel.cs b/FortnitePorting/ViewModels/Settings/BlenderSettingsViewModel.cs index 2ce072d2..c223ace7 100644 --- a/FortnitePorting/ViewModels/Settings/BlenderSettingsViewModel.cs +++ b/FortnitePorting/ViewModels/Settings/BlenderSettingsViewModel.cs @@ -35,6 +35,7 @@ public partial class BlenderSettingsViewModel : BaseExportSettings [ObservableProperty] private float _cavity = 0.0f; [ObservableProperty] private float _subsurface = 0.0f; [ObservableProperty] private float _toonShadingBrightness = 0.5f; + [ObservableProperty] private EMaterialImportMethod _materialImportMethod = EMaterialImportMethod.Data; // Texture [ObservableProperty] private ETextureImportMethod _textureImportMethod = ETextureImportMethod.Data; @@ -80,6 +81,15 @@ public enum ETextureImportMethod [Description("As Texture Data")] Data, + [Description("As Object")] + Object +} + +public enum EMaterialImportMethod +{ + [Description("As Material Data")] + Data, + [Description("As Object")] Object } \ No newline at end of file diff --git a/FortnitePorting/Views/Settings/BlenderSettingsView.axaml b/FortnitePorting/Views/Settings/BlenderSettingsView.axaml index e53d2f41..e02862b1 100644 --- a/FortnitePorting/Views/Settings/BlenderSettingsView.axaml +++ b/FortnitePorting/Views/Settings/BlenderSettingsView.axaml @@ -200,6 +200,13 @@ + + + + + + From 785e9a3ce0a28bab33f652625dd48e59dd58dd59 Mon Sep 17 00:00:00 2001 From: Benjamin Marquez Date: Tue, 7 Jan 2025 13:19:55 -0500 Subject: [PATCH 3/3] Adding as_material_data check when existing material is found to prevent null error --- .../Blender/fortnite_porting/processing/import_context.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py index 9e4a7aa5..0bc230ec 100644 --- a/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py +++ b/FortnitePorting.Plugins/Blender/fortnite_porting/processing/import_context.py @@ -441,9 +441,10 @@ def import_material(self, material_slot, material_data, meta, as_material_data=F material_hash += additional_hash material_name += f"_{hash_code(material_hash)}" - if existing_material := first(bpy.data.materials, lambda mat: mat.get("Hash") == hash_code(material_hash)) and not as_material_data: - material_slot.material = existing_material - return + if existing_material := first(bpy.data.materials, lambda mat: mat.get("Hash") == hash_code(material_hash)): + if not as_material_data: + material_slot.material = existing_material + return # same name but different hash if (name_existing := first(bpy.data.materials, lambda mat: mat.name == material_name)) and name_existing.get("Hash") != material_hash: