Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone material exporting #88

Open
wants to merge 3 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class EExportType(IntEnum):
SOUND = auto()
FONT = auto()
POSE_ASSET = auto()
MATERIAL = auto()
MATERIAL_INSTANCE = auto()


class EPrimitiveExportType(IntEnum):
Expand All @@ -78,6 +80,7 @@ class EPrimitiveExportType(IntEnum):
SOUND = auto()
FONT = auto()
POSE_ASSET = auto()
MATERIAL = auto()


class EFortCustomPartType(IntEnum):
Expand All @@ -98,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import bpy
import traceback
from math import radians
from .mappings import *
from .material import *
from .enums import *
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -408,44 +415,49 @@ 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")
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
material_name += f"_{hash_code(material_hash)}"

if existing_material := first(bpy.data.materials, lambda mat: mat.get("Hash") == hash_code(material_hash)):
material_slot.material = existing_material
return
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:
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"

Expand All @@ -465,20 +477,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)
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 scalar in parameters.get("Scalars"):
replace_or_add_parameter(scalars, scalar)

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)
Expand Down Expand Up @@ -1489,3 +1503,25 @@ 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):
is_object_import = EMaterialImportMethod.OBJECT == EMaterialImportMethod(self.options.get("MaterialImportMethod"))
materials = data.get("Materials")

if materials is None:
return

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}")
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)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions FortnitePorting/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -214,7 +220,10 @@ public enum EPrimitiveExportType
Font,

[Description("PoseAsset")]
PoseAsset
PoseAsset,

[Description("Material")]
Material
}

public enum EAssetSortType
Expand Down
4 changes: 4 additions & 0 deletions FortnitePorting/Export/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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.")
};

Expand Down
19 changes: 19 additions & 0 deletions FortnitePorting/Export/Types/MaterialExport.cs
Original file line number Diff line number Diff line change
@@ -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<ExportMaterial> 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));
}
}
10 changes: 10 additions & 0 deletions FortnitePorting/ViewModels/Settings/BlenderSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions FortnitePorting/Views/Settings/BlenderSettingsView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>

<ui:SettingsExpander Header="Method" Description="The way standalone materials should be imported.">
<ui:SettingsExpander.Footer>
<ComboBox ItemsSource="{ext:EnumToItemsSource {x:Type settings:EMaterialImportMethod}}"
SelectedItem="{Binding MaterialImportMethod, Converter={StaticResource EnumToRecord}}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>

<TextBlock Text="Texture" Classes="SubtitleTextBlockStyle" HorizontalAlignment="Center"/>

<ui:SettingsExpander Header="Image Format" Description="The file type that textures should be exported as.">
Expand Down