Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a40dad0
implemented exporting all LODs for skeletal meshes on the main export…
DropTheSquid Nov 27, 2025
0580e48
implemented material mapping for psk export.
DropTheSquid Dec 3, 2025
e74b62a
Merge branch 'Beta' into MeshImprovements
DropTheSquid Dec 4, 2025
82f1596
Fixed some issues with PSK LOD export, implemented partial support fo…
DropTheSquid Dec 9, 2025
b8a9b7d
Added experiment for importing a psk over an existing skeletal mesh.
DropTheSquid Dec 9, 2025
caccf51
added a new experiment to export textures from a MIC, effects mat, or…
DropTheSquid Dec 10, 2025
8731a42
implemented static mesh export. It works well so far in my testing. I…
DropTheSquid Jan 1, 2026
78e9519
fixed a bug reported by Sidious that caused BioMorphFace export to fa…
DropTheSquid Jan 17, 2026
28f7f94
fixed a bug in PSK texture export so it exports base material texture…
DropTheSquid Jan 20, 2026
1f23a68
Started building out glTF pipeline. I think basic skeletal mesh expor…
DropTheSquid Jan 20, 2026
2cb790b
got the skeletal mesh export to match the output of UModel very well.
DropTheSquid Jan 21, 2026
f84fb7e
Got bones rotted so they look much better in Blender. I need to test …
DropTheSquid Jan 22, 2026
08209e9
minor refactors, bug fixes. gltf Export now takes LOD material maps i…
DropTheSquid Jan 23, 2026
71c63f9
Quick POC of attaching textures. it seems to work, though it needs mo…
DropTheSquid Jan 24, 2026
660a6f7
implemented attaching a normal texture in a format that is glTF compl…
DropTheSquid Jan 25, 2026
61be603
added support for exporting sockets. the results visually match manua…
DropTheSquid Jan 25, 2026
12e3a9f
Rearranged a bit, added support for exporting as glb, started impleme…
DropTheSquid Jan 26, 2026
ebc5d88
Implemented most of Static mesh export to gltf.
DropTheSquid Jan 26, 2026
3237c97
implemented exporting collision for static meshes.
DropTheSquid Jan 26, 2026
e854caf
fixed various bugs, got things mostly working how I want them to work.
DropTheSquid Jan 27, 2026
333474d
finally got it working exactly how I want.
DropTheSquid Jan 27, 2026
8b6f511
fixed static mesh export.
DropTheSquid Jan 27, 2026
38c2a31
Implemented several good things:
DropTheSquid Jan 27, 2026
c8b630d
implemented most of skeletal mesh import as new export. I still have …
DropTheSquid Jan 29, 2026
5265c09
cleaned up getting sockets into the glTF a bit, tested that everythin…
DropTheSquid Jan 30, 2026
2577353
got all bone and socket import stuff working correctly I think.
DropTheSquid Jan 30, 2026
3c87cfe
dealt with various small TODOs, finished up socket scale.
DropTheSquid Jan 30, 2026
2f4d8fb
rearranged things a little bit, got static mesh import mostly working.
DropTheSquid Jan 31, 2026
02e19c9
Lots of progress. Rearrnaged things so that most of the code lives in…
DropTheSquid Feb 2, 2026
adf7c07
Added support for generating new normals and tangents, if needed. Nor…
DropTheSquid Feb 2, 2026
ce1966a
improved error handling, fixed some bugs in material texture lookup, …
DropTheSquid Feb 4, 2026
b576e79
Merge beta into my feature branch.
DropTheSquid Feb 4, 2026
8ad7077
implemented a few BioPawn adjacent classes for exporting, especially …
DropTheSquid Feb 4, 2026
6984223
implemented better handling of multiple skeletal meshes using the sam…
DropTheSquid Feb 5, 2026
25fb63a
Usability imprements. gltf import/export s now available in the mesh …
DropTheSquid Feb 6, 2026
d5aa225
Added a couple more small experiments.
DropTheSquid Feb 10, 2026
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
220 changes: 220 additions & 0 deletions LegendaryExplorer/LegendaryExplorer/Misc/GltfHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using LegendaryExplorer.Dialogs;
using LegendaryExplorer.SharedUI.Bases;
using LegendaryExplorer.UserControls.ExportLoaderControls;
using LegendaryExplorerCore.Helpers;
using LegendaryExplorerCore.Packages;
using LegendaryExplorerCore.Unreal;
using LegendaryExplorerCore.Unreal.ObjectInfo;
using Microsoft.Win32;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace LegendaryExplorer.Misc
{
/// <summary>
/// Wraps the GLTF class in LEC to make the functionality available to various parts of the user interface
/// </summary>
public static class GltfHelper
{
public static bool CanExportMeshToGltf(WPFBase owningWindow, IMEPackage package, IEntry selectedEntry)
{
// TODO is the selectedEntry a valid type?
// are experiments enabled?
return true;
}
public static void ExportMeshToGltf(WPFBase owningWindow, MeshRenderer meshRenderer, IMEPackage package, IEntry selectedEntry, GLTF.MaterialExportLevel materialExportLevel = GLTF.MaterialExportLevel.NameOnly)
{
if (package == null)
{
return;
}
if (package.Game == MEGame.ME1)
{
ShowError("This experiment does not yet support OT1; if you must do this, port it to another game first");
return;
}
if (package.Game == MEGame.UDK)
{
ShowError("This experiment does not support UDK files;");
return;
}
if (FilterSelectedItem(selectedEntry, ["SkeletalMesh", "StaticMesh", "SkeletalMeshComponent", "BioPawn", "SFXStuntActor", "SkeletalMeshActor"], out var export))
{
if (export.ClassName == "StaticMesh" && !(package.Game.IsGame3() || package.Game.IsLEGame()))
{
ShowError("This experiment does not yet support OT1 or OT2 for static meshes.");
return;
}
var d = new SaveFileDialog { Filter = "glTF binary|*.glb|glTF|*.glTF", FileName = $"{selectedEntry.ObjectName.Instanced}.glb" };
if (d.ShowDialog() == true)
{
Task.Run(() =>
{
if (owningWindow != null)
{
owningWindow.BusyText = "Exporting to glTF...";
owningWindow.IsBusy = true;
}
else
{
meshRenderer?.BusyText = "Exporting to glTF...";
meshRenderer?.IsBusy = true;
}
GLTF.ExportMeshToGltf(export, d.FileName, materialExportLevel, $"Legendary Explorer {AppVersion.DisplayedVersion}");
}).ContinueWithOnUIThread(x =>
{
if (owningWindow != null)
{
owningWindow?.IsBusy = false;
}
else
{
meshRenderer?.IsBusy = false;
}
if (x.Exception != null)
{
ShowError(x.Exception.FlattenException());
}
});
}
}
else
{
ShowError("You must select a skeletal mesh, static mesh, SkeletalMeshComponent, SFXStuntActor, SkeletalMeshActor, or BioPawn");
}
}

public static void ReplaceFromGltf(WPFBase window, IEntry selectedEntry)
{
if (window.Pcc == null)
{
return;
}
if (window.Pcc.Game == MEGame.ME1)
{
ShowError("This experiment does not yet support OT1; if you must do this, import it into another game and port it to OT1");
}
if (window.Pcc.Game == MEGame.UDK)
{
ShowError("This experiment does not support UDK files;");
}
if (GetGltfFromFile(out var gltf, out string _))
{
FilterSelectedItem(selectedEntry, ["SkeletalMesh", "StaticMesh"], out ExportEntry selectedMeshToReplace);
GLTF.QueryMeshes(gltf, out var skeletalMeshes, out var staticMeshes);
string specificMesh = null;
if (selectedMeshToReplace.ClassName == "SkeletalMesh")
{
var meshCount = skeletalMeshes.Count();
if (meshCount == 0)
{
ShowError("You are trying to replace a skeletal mesh but the glTF file does not contain any skeletal meshes.");
return;
}
else if (meshCount > 1)
{
var prompt = new DropdownPromptDialog("Select which mesh to use to replace your mesh.",
"Select mesh", "Mesh", skeletalMeshes, window);
prompt.ShowDialog();
if (prompt.DialogResult == true)
{
specificMesh = prompt.Response;
}
else { return; }
}
}
else if (selectedMeshToReplace.ClassName == "StaticMesh")
{
var meshCount = staticMeshes.Count();
if (meshCount == 0)
{
ShowError("You are trying to replace a static mesh but the glTF file does not contain any static meshes.");
return;
}
else if (meshCount > 1)
{
var prompt = new DropdownPromptDialog("Select which mesh to use to replace your mesh.",
"Select mesh", "Mesh", staticMeshes, window);
prompt.ShowDialog();
if (prompt.DialogResult == true)
{
specificMesh = prompt.Response;
}
else { return; }
}
}
GLTF.ConvertGltfToMesh(gltf, window.Pcc, selectedMeshToReplace, specificMesh);
}
}

public static void ImportNewFromGltf(WPFBase window)
{
if (window.Pcc == null)
{
return;
}
if (window.Pcc.Game == MEGame.ME1)
{
ShowError("This experiment does not yet support OT1; if you must do this, import it into another game and port it to OT1");
}
if (window.Pcc.Game == MEGame.UDK)
{
ShowError("This experiment does not support UDK files;");
}
if (GetGltfFromFile(out var gltf, out string _))
{
GLTF.QueryMeshes(gltf, out var skeletalMeshes, out var staticMeshes);
if (!skeletalMeshes.Any() && !staticMeshes.Any())
{
ShowError("The gltf you are trying to import does not contain any meshes.");
return;
}
GLTF.ConvertGltfToMesh(gltf, window.Pcc);
}
}

private static bool FilterSelectedItem(IEntry selectedItem, string[] expectedTypes, out ExportEntry entry)
{
entry = null;
if (selectedItem == null)
{
return false;
}

foreach (var expectedType in expectedTypes)
{
if (selectedItem.IsA(expectedType))
{
entry = (ExportEntry)selectedItem;
return entry != null;
}
}
return false;
}

private static bool GetGltfFromFile(out SharpGLTF.Schema2.ModelRoot gltf, out string filePath)
{
var d = new OpenFileDialog
{
Filter = "gLTF|*.gltf;*.glb",
Title = "Select a gLTF or glb file"
};
if (d.ShowDialog() == true)
{
filePath = d.FileName;
gltf = SharpGLTF.Schema2.ModelRoot.Load(filePath, SharpGLTF.Validation.ValidationMode.Skip);
return true;
}

gltf = null;
filePath = null;
return false;
}

private static void ShowError(string errMsg)
{
MessageBox.Show(errMsg, "Warning", MessageBoxButton.OK);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
<MenuItem Header="Convert Skeletal Mesh to Static Mesh" Command="{Binding ConvertToStaticMeshCommand}"/>
<MenuItem Header="Export Mesh to PSK with UModel" Command="{Binding ExportToPSKUModelCommand}"/>
<MenuItem Header="Export Mesh to PSK (Experimental)" Command="{Binding ExportToPSKCommand}"/>
<MenuItem Header="Export as glTF wihtout textures (Experimental)" Command="{Binding ExportToGltfCommand}"/>
<MenuItem Header="Export as glTF with textures (Experimental)" Command="{Binding ExportToGltfTexturesCommand}"/>
<MenuItem Header="Replace from glTF (Experimental)" Command="{Binding ReplaceFromGltfCommand}"/>
<MenuItem Header="Import new mesh(es) from glTF (Experimental)" Command="{Binding ImportNewFromGltfCommand}"/>
</MenuItem>
</Menu>
<StatusBar Height="23" DockPanel.Dock="Bottom">
Expand Down Expand Up @@ -186,6 +190,10 @@
</MenuItem>
<MenuItem Header="Export Mesh to PSK (Experimental)" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ExportToPSKCommand}"/>
<MenuItem Header="Convert ME3 Skeletal Mesh to ME3 Static Mesh" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ConvertToStaticMeshCommand}"/>
<MenuItem Header="Export as glTF without textures (Experimental)" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ExportToGltfCommand}"/>
<MenuItem Header="Export as glTF with textures (Experimental)" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ExportToGltfTexturesCommand}"/>
<MenuItem Header="Replace from glTF (Experimental)" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ReplaceFromGltfCommand}"/>
<MenuItem Header="Import new mesh(es) from glTF (Experimental)" Command="{Binding Source={StaticResource bindingProxy}, Path=Data.ImportNewFromGltfCommand}"/>
</ContextMenu>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="ContextMenu" Value="{StaticResource MyMenu}"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ private bool FilterExportList(object obj)
public ICommand ReplaceLODFromUDKCommand { get; set; }
public ICommand ExportToPSKUModelCommand { get; set; }
public ICommand ExportToPSKCommand { get; set; }
public ICommand ExportToGltfCommand { get; set; }
public ICommand ExportToGltfTexturesCommand { get; set; }
public ICommand ReplaceFromGltfCommand { get; set; }
public ICommand ImportNewFromGltfCommand { get; set; }
private void LoadCommands()
{
OpenFileCommand = new GenericCommand(OpenFile);
Expand All @@ -165,6 +169,25 @@ private void LoadCommands()
ReplaceLODFromUDKCommand = new GenericCommand(ImportLODFromUDK, IsSkeletalMeshSelected);
ExportToPSKUModelCommand = new GenericCommand(() => Mesh3DViewer.EnsureUModelAndExport(), IsMeshSelected);
ExportToPSKCommand = new GenericCommand(ExportToPSK, IsSkeletalMeshSelected);
ExportToGltfCommand = new GenericCommand(() => ExportToGltf(GLTF.MaterialExportLevel.NameOnly), IsMeshSelected);
ExportToGltfTexturesCommand = new GenericCommand(() => ExportToGltf(GLTF.MaterialExportLevel.Basic), IsMeshSelected);
ReplaceFromGltfCommand = new GenericCommand(ReplaceFromGltf, IsMeshSelected);
ImportNewFromGltfCommand = new GenericCommand(ImportNewFromGltf);
}

private void ExportToGltf(GLTF.MaterialExportLevel materialExportLevel)
{
GltfHelper.ExportMeshToGltf(this, null, this.Pcc, CurrentExport, materialExportLevel);
}

private void ReplaceFromGltf()
{
GltfHelper.ReplaceFromGltf(this, CurrentExport);
}

private void ImportNewFromGltf()
{
GltfHelper.ImportNewFromGltf(this);
}

private void ExportToPSK()
Expand Down
Loading