yoinked snooper's imgui export modal

This commit is contained in:
Asval 2026-06-05 20:06:26 +02:00
parent 32a12da5d0
commit 05693db228
30 changed files with 946 additions and 263 deletions

@ -1 +1 @@
Subproject commit d807df729bda91706598ef2f4531bd9799dfb8ef
Subproject commit 4c3332989787d0b57325d160137e9fa61d82a139

View File

@ -11,7 +11,9 @@ using CUE4Parse;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.Views.Snooper;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
@ -118,10 +120,12 @@ public partial class App
.WriteTo.Console(outputTemplate: template, theme: AnsiConsoleTheme.Literate)
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Debug-Log-{DateTime.Now:yyyy-MM-dd}.log"))
.MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance)
#else
.Enrich.With<CallerEnricher>()
.WriteTo.File(outputTemplate: template,
path: Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log"))
.MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance)
#endif
.CreateLogger();

View File

@ -0,0 +1,11 @@
using Serilog.Events;
namespace FModel.Extensions;
public static class LogEventExtensions
{
public static string GetContext(this LogEvent log, string propertyName)
{
return log.Properties.TryGetValue(propertyName, out var value) ? value.ToString().Trim('"') : string.Empty;
}
}

View File

@ -121,6 +121,9 @@
<None Remove="Resources\bone.frag" />
<None Remove="Resources\bone.vert" />
<None Remove="Resources\collision.vert" />
<None Remove="Resources\fa-brands-400.otf" />
<None Remove="Resources\fa-solid-900.otf" />
<None Remove="Resources\fa-regular-400.otf" />
</ItemGroup>
<ItemGroup>
@ -149,6 +152,9 @@
<EmbeddedResource Include="Resources\bone.frag" />
<EmbeddedResource Include="Resources\bone.vert" />
<EmbeddedResource Include="Resources\collision.vert" />
<EmbeddedResource Include="Resources\fa-brands-400.otf" />
<EmbeddedResource Include="Resources\fa-solid-900.otf" />
<EmbeddedResource Include="Resources\fa-regular-400.otf" />
</ItemGroup>
<ItemGroup>

View File

@ -66,18 +66,69 @@ public class ImGuiController : IDisposable
var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"));
io.NativePtr->IniFilename = (byte*)iniFileNamePtr;
}
// If not found, Fallback to default ImGui Font
var normalPath = @"C:\Windows\Fonts\segoeui.ttf";
var boldPath = @"C:\Windows\Fonts\segoeuib.ttf";
var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf";
if (File.Exists(normalPath))
FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale);
if (File.Exists(boldPath))
FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale);
if (File.Exists(semiBoldPath))
FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale);
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
var assemblyName = assembly.GetName().Name;
byte[] LoadFont(string name)
{
using var stream = assembly.GetManifestResourceStream($"{assemblyName}.Resources.{name}")
?? throw new FileNotFoundException($"Embedded font '{name}' not found.");
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
var faSolid = LoadFont("fa-solid-900.otf");
var faRegular = LoadFont("fa-regular-400.otf");
var faBrands = LoadFont("fa-brands-400.otf");
unsafe
{
// FA5 icons live in E000F8FF (brands/regular/solid)
var iconRanges = stackalloc ushort[] { 0xe000, 0xf8ff, 0 };
var cfg = ImGuiNative.ImFontConfig_ImFontConfig();
cfg->MergeMode = 1;
cfg->PixelSnapH = 1;
cfg->GlyphMinAdvanceX = 16f;
// FontDataOwnedByAtlas = 0 because we manage the GCHandle lifetime ourselves
cfg->FontDataOwnedByAtlas = 0;
void MergeFontAwesome(byte[] solid, byte[] regular, byte[] brands)
{
fixed (byte* pSolid = solid)
fixed (byte* pRegular = regular)
fixed (byte* pBrands = brands)
{
io.Fonts.AddFontFromMemoryTTF((IntPtr)pSolid, solid.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges);
io.Fonts.AddFontFromMemoryTTF((IntPtr)pRegular, regular.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges);
io.Fonts.AddFontFromMemoryTTF((IntPtr)pBrands, brands.Length, 14, (IntPtr)cfg, (IntPtr)iconRanges);
}
}
// If not found, Fallback to default ImGui Font
var normalPath = @"C:\Windows\Fonts\segoeui.ttf";
var boldPath = @"C:\Windows\Fonts\segoeuib.ttf";
var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf";
if (File.Exists(normalPath))
{
FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale);
MergeFontAwesome(faSolid, faRegular, faBrands);
}
if (File.Exists(boldPath))
{
FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale);
MergeFontAwesome(faSolid, faRegular, faBrands);
}
if (File.Exists(semiBoldPath))
{
FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale);
MergeFontAwesome(faSolid, faRegular, faBrands);
}
ImGuiNative.ImFontConfig_destroy(cfg);
}
io.Fonts.AddFontDefault();
io.Fonts.Build(); // Build font atlas

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,13 +4,9 @@ using System.IO;
using System.Windows;
using System.Windows.Input;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Animations;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse_Conversion.Options;
using CUE4Parse_Conversion.Writers.UEFormat.Enums;
using CUE4Parse.UE4.Lua.unluac;
using FModel.Framework;
using FModel.ViewModels;
@ -57,26 +53,23 @@ namespace FModel.Settings
return endpoint.Overwrite || endpoint.IsValid;
}
[JsonIgnore]
public ExporterOptions ExportOptions => new()
public static ExportOptions GetExportOptions()
{
LodFormat = Default.LodExportFormat,
MeshFormat = Default.MeshExportFormat,
NaniteMeshFormat = Default.NaniteMeshExportFormat,
AnimFormat = Default.MeshExportFormat switch
{
EMeshFormat.UEFormat => EAnimFormat.UEFormat,
_ => EAnimFormat.ActorX
},
MaterialFormat = Default.MaterialExportFormat,
TextureFormat = Default.TextureExportFormat,
SocketFormat = Default.SocketExportFormat,
CompressionFormat = Default.CompressionFormat,
Platform = Default.CurrentDir.TexturePlatform,
ExportMorphTargets = Default.SaveMorphTargets,
ExportMaterials = Default.SaveEmbeddedMaterials,
ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr
};
return new ExportOptions(
Default.MeshExportFormat,
Default.NaniteMeshExportFormat,
Default.MeshQuality,
Default.CurrentDir.TexturePlatform,
Default.TextureExportFormat,
100,
Default.SaveHdrTexturesAsHdr,
Default.MaterialExportFormat,
Default.SaveEmbeddedMaterials,
Default.SaveMorphTargets,
Default.SocketExportFormat,
Default.CompressionFormat
);
}
private bool _showChangelog = true;
public bool ShowChangelog
@ -422,15 +415,22 @@ namespace FModel.Settings
set => SetProperty(ref _meshExportFormat, value);
}
private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.OnlyNaniteLOD;
private ENaniteMeshFormat _naniteMeshExportFormat = ENaniteMeshFormat.NaniteOnly;
public ENaniteMeshFormat NaniteMeshExportFormat
{
get => _naniteMeshExportFormat;
set => SetProperty(ref _naniteMeshExportFormat, value);
}
private EMaterialFormat _materialExportFormat = EMaterialFormat.FirstLayer;
public EMaterialFormat MaterialExportFormat
private EMeshQuality _meshQuality = EMeshQuality.Highest;
public EMeshQuality MeshQuality
{
get => _meshQuality;
set => SetProperty(ref _meshQuality, value);
}
private EMaterialDepth _materialExportFormat = EMaterialDepth.TopLayerOnly;
public EMaterialDepth MaterialExportFormat
{
get => _materialExportFormat;
set => SetProperty(ref _materialExportFormat, value);
@ -457,13 +457,6 @@ namespace FModel.Settings
set => SetProperty(ref _compressionFormat, value);
}
private ELodFormat _lodExportFormat = ELodFormat.FirstLod;
public ELodFormat LodExportFormat
{
get => _lodExportFormat;
set => SetProperty(ref _lodExportFormat, value);
}
private bool _showSkybox = true;
public bool ShowSkybox
{

View File

@ -1678,32 +1678,34 @@ public class CUE4ParseViewModel : ViewModel
private void SaveExport(UObject export, bool updateUi = true)
{
var toSave = new Exporter(export, UserSettings.Default.ExportOptions);
var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory);
if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath))
{
Interlocked.Increment(ref ExportedCount);
Log.Information("Successfully saved {FilePath}", savedFilePath);
if (updateUi)
{
FLogger.Append(ELog.Information, () =>
{
FLogger.Text("Successfully saved ", Constants.WHITE);
FLogger.Link(label, savedFilePath, true);
});
}
}
else
{
Interlocked.Increment(ref FailedExportCount);
Log.Error("{FileName} could not be saved", export.Name);
FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true));
}
// TODO: export session
// var toSave = new Exporter(export, UserSettings.Default.ExportOptions);
// var toSaveDirectory = new DirectoryInfo(UserSettings.Default.ModelDirectory);
// if (toSave.TryWriteToDir(toSaveDirectory, out var label, out var savedFilePath))
// {
// Interlocked.Increment(ref ExportedCount);
// Log.Information("Successfully saved {FilePath}", savedFilePath);
// if (updateUi)
// {
// FLogger.Append(ELog.Information, () =>
// {
// FLogger.Text("Successfully saved ", Constants.WHITE);
// FLogger.Link(label, savedFilePath, true);
// });
// }
// }
// else
// {
// Interlocked.Increment(ref FailedExportCount);
// Log.Error("{FileName} could not be saved", export.Name);
// FLogger.Append(ELog.Error, () => FLogger.Text($"Could not save '{export.Name}'", Constants.WHITE, true));
// }
}
private readonly object _rawData = new ();
public void ExportData(GameFile entry, bool updateUi = true)
{
// TODO: export session
if (Provider.TrySavePackage(entry, out var assets))
{
string path = UserSettings.Default.RawDataDirectory;

View File

@ -2,14 +2,12 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Versions;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures;
using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse_Conversion.Options;
using CUE4Parse_Conversion.Writers.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports.Material;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -137,8 +135,8 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedCompressionFormat, value);
}
private ELodFormat _selectedLodExportFormat;
public ELodFormat SelectedLodExportFormat
private EMeshQuality _selectedLodExportFormat;
public EMeshQuality SelectedLodExportFormat
{
get => _selectedLodExportFormat;
set => SetProperty(ref _selectedLodExportFormat, value);
@ -151,8 +149,8 @@ public class SettingsViewModel : ViewModel
set => SetProperty(ref _selectedNaniteMeshExportFormat, value);
}
private EMaterialFormat _selectedMaterialExportFormat;
public EMaterialFormat SelectedMaterialExportFormat
private EMaterialDepth _selectedMaterialExportFormat;
public EMaterialDepth SelectedMaterialExportFormat
{
get => _selectedMaterialExportFormat;
set => SetProperty(ref _selectedMaterialExportFormat, value);
@ -191,9 +189,9 @@ public class SettingsViewModel : ViewModel
public ReadOnlyObservableCollection<EMeshFormat> MeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<ESocketFormat> SocketExportFormats { get; private set; }
public ReadOnlyObservableCollection<EFileCompressionFormat> CompressionFormats { get; private set; }
public ReadOnlyObservableCollection<ELodFormat> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMeshQuality> LodExportFormats { get; private set; }
public ReadOnlyObservableCollection<ENaniteMeshFormat> NaniteMeshExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialFormat> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<EMaterialDepth> MaterialExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETextureFormat> TextureExportFormats { get; private set; }
public ReadOnlyObservableCollection<ETexturePlatform> Platforms { get; private set; }
@ -216,9 +214,9 @@ public class SettingsViewModel : ViewModel
private EMeshFormat _meshExportFormatSnapshot;
private ESocketFormat _socketExportFormatSnapshot;
private EFileCompressionFormat _compressionFormatSnapshot;
private ELodFormat _lodExportFormatSnapshot;
private EMeshQuality _lodExportFormatSnapshot;
private ENaniteMeshFormat _naniteMeshExportFormatSnapshot;
private EMaterialFormat _materialExportFormatSnapshot;
private EMaterialDepth _materialExportFormatSnapshot;
private ETextureFormat _textureExportFormatSnapshot;
private bool _mappingsUpdate = false;
@ -260,7 +258,7 @@ public class SettingsViewModel : ViewModel
_meshExportFormatSnapshot = UserSettings.Default.MeshExportFormat;
_socketExportFormatSnapshot = UserSettings.Default.SocketExportFormat;
_compressionFormatSnapshot = UserSettings.Default.CompressionFormat;
_lodExportFormatSnapshot = UserSettings.Default.LodExportFormat;
_lodExportFormatSnapshot = UserSettings.Default.MeshQuality;
_naniteMeshExportFormatSnapshot = UserSettings.Default.NaniteMeshExportFormat;
_materialExportFormatSnapshot = UserSettings.Default.MaterialExportFormat;
_textureExportFormatSnapshot = UserSettings.Default.TextureExportFormat;
@ -294,9 +292,9 @@ public class SettingsViewModel : ViewModel
MeshExportFormats = new ReadOnlyObservableCollection<EMeshFormat>(new ObservableCollection<EMeshFormat>(EnumerateMeshExportFormat()));
SocketExportFormats = new ReadOnlyObservableCollection<ESocketFormat>(new ObservableCollection<ESocketFormat>(EnumerateSocketExportFormat()));
CompressionFormats = new ReadOnlyObservableCollection<EFileCompressionFormat>(new ObservableCollection<EFileCompressionFormat>(EnumerateCompressionFormat()));
LodExportFormats = new ReadOnlyObservableCollection<ELodFormat>(new ObservableCollection<ELodFormat>(EnumerateLodExportFormat()));
LodExportFormats = new ReadOnlyObservableCollection<EMeshQuality>(new ObservableCollection<EMeshQuality>(EnumerateLodExportFormat()));
NaniteMeshExportFormats = new ReadOnlyObservableCollection<ENaniteMeshFormat>(new ObservableCollection<ENaniteMeshFormat>(EnumerateNaniteMeshExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialFormat>(new ObservableCollection<EMaterialFormat>(EnumerateMaterialExportFormat()));
MaterialExportFormats = new ReadOnlyObservableCollection<EMaterialDepth>(new ObservableCollection<EMaterialDepth>(EnumerateMaterialExportFormat()));
TextureExportFormats = new ReadOnlyObservableCollection<ETextureFormat>(new ObservableCollection<ETextureFormat>(EnumerateTextureExportFormat()));
Platforms = new ReadOnlyObservableCollection<ETexturePlatform>(new ObservableCollection<ETexturePlatform>(EnumerateUePlatforms()));
}
@ -331,7 +329,7 @@ public class SettingsViewModel : ViewModel
UserSettings.Default.MeshExportFormat = SelectedMeshExportFormat;
UserSettings.Default.SocketExportFormat = SelectedSocketExportFormat;
UserSettings.Default.CompressionFormat = SelectedCompressionFormat;
UserSettings.Default.LodExportFormat = SelectedLodExportFormat;
UserSettings.Default.MeshQuality = SelectedLodExportFormat;
UserSettings.Default.NaniteMeshExportFormat = SelectedNaniteMeshExportFormat;
UserSettings.Default.MaterialExportFormat = SelectedMaterialExportFormat;
UserSettings.Default.TextureExportFormat = SelectedTextureExportFormat;
@ -357,9 +355,9 @@ public class SettingsViewModel : ViewModel
private IEnumerable<EMeshFormat> EnumerateMeshExportFormat() => Enum.GetValues<EMeshFormat>();
private IEnumerable<ESocketFormat> EnumerateSocketExportFormat() => Enum.GetValues<ESocketFormat>();
private IEnumerable<EFileCompressionFormat> EnumerateCompressionFormat() => Enum.GetValues<EFileCompressionFormat>();
private IEnumerable<ELodFormat> EnumerateLodExportFormat() => Enum.GetValues<ELodFormat>();
private IEnumerable<EMeshQuality> EnumerateLodExportFormat() => Enum.GetValues<EMeshQuality>();
private IEnumerable<ENaniteMeshFormat> EnumerateNaniteMeshExportFormat() => Enum.GetValues<ENaniteMeshFormat>();
private IEnumerable<EMaterialFormat> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialFormat>();
private IEnumerable<EMaterialDepth> EnumerateMaterialExportFormat() => Enum.GetValues<EMaterialDepth>();
private IEnumerable<ETextureFormat> EnumerateTextureExportFormat() => Enum.GetValues<ETextureFormat>();
private IEnumerable<ETexturePlatform> EnumerateUePlatforms() => Enum.GetValues<ETexturePlatform>();
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;
using CUE4Parse_Conversion.Options;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.Utils;

View File

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:c4pMeshes="clr-namespace:CUE4Parse_Conversion.Meshes;assembly=CUE4Parse-Conversion"
xmlns:c4pMeshes="clr-namespace:CUE4Parse_Conversion.Options;assembly=CUE4Parse-Conversion"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
@ -452,7 +452,7 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="5" Grid.Column="0" Text="Level Of Detail Format" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBlock Grid.Row="5" Grid.Column="0" Text="Mesh Quality Format" VerticalAlignment="Center" Margin="0 0 0 5" />
<ComboBox Grid.Row="5" Grid.Column="2" Grid.ColumnSpan="3" ItemsSource="{Binding SettingsView.LodExportFormats}" SelectedItem="{Binding SettingsView.SelectedLodExportFormat, Mode=TwoWay}"
DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.SettingsView}}}" Margin="0 0 0 5">
<ComboBox.ItemTemplate>

View File

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Animations.PSA;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Settings;
@ -12,7 +11,7 @@ using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
public class Animation : IDisposable
public class Animation : IExportableThing, IDisposable
{
private readonly UObject _export;
@ -82,7 +81,7 @@ public class Animation : IDisposable
AttachedModels.Clear();
}
public void ImGuiAnimation(Snooper s, Save saver, ImDrawListPtr drawList, ImFontPtr fontPtr, Vector2 timelineP0, Vector2 treeP0, Vector2 timeStep, Vector2 timeRatio, float y, float t, int i)
public void ImGuiAnimation(Snooper s, ImDrawListPtr drawList, ImFontPtr fontPtr, Vector2 timelineP0, Vector2 treeP0, Vector2 timeStep, Vector2 timeRatio, float y, float t, int i)
{
var name = $"{Name}##{i}";
var p1 = new Vector2(timelineP0.X + StartTime * timeRatio.X + t, y + t);
@ -96,7 +95,7 @@ public class Animation : IDisposable
{
s.Renderer.Options.SelectAnimation(i);
}
Popup(s, saver, i);
Popup(s, i);
drawList.AddRectFilled(p1, p2, IsSelected ? 0xFF48B048 : 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop);
for (int j = 0; j < Sequences.Length; j++)
@ -109,10 +108,10 @@ public class Animation : IDisposable
{
s.Renderer.Options.SelectAnimation(i);
}
Popup(s, saver, i);
Popup(s, i);
}
private void Popup(Snooper s, Save saver, int i)
private void Popup(Snooper s, int i)
{
SnimGui.Popup(() =>
{
@ -134,11 +133,16 @@ public class Animation : IDisposable
if (ImGui.MenuItem("Save"))
{
s.WindowShouldFreeze(true);
saver.Value = new Exporter(_export, UserSettings.Default.ExportOptions).TryWriteToDir(new DirectoryInfo(UserSettings.Default.ModelDirectory), out saver.Label, out saver.Path);
ExportModal.Instance.Export([this], UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions());
s.WindowShouldFreeze(false);
}
ImGui.Separator();
if (ImGui.MenuItem("Copy Path to Clipboard")) ImGui.SetClipboardText(Path);
});
}
public void AddToExportSession(ExportSession session)
{
session.Add(_export);
}
}

View File

@ -1,5 +1,5 @@
using System.Numerics;
using CUE4Parse_Conversion.Animations.PSA;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CUE4Parse_Conversion.Animations.PSA;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Buffers;

View File

@ -61,7 +61,7 @@ public class TimeTracker : IDisposable
}
private readonly string[] _icons = { "tl_forward", "tl_pause", "tl_rewind" };
public void ImGuiTimeline(Snooper s, Save saver, Dictionary<string, Texture> icons, List<Animation> animations, Vector2 outliner, ImFontPtr fontPtr)
public void ImGuiTimeline(Snooper s, Dictionary<string, Texture> icons, List<Animation> animations, Vector2 outliner, ImFontPtr fontPtr)
{
var dpiScale = ImGui.GetWindowDpiScale();
var thickness = 2.0f * dpiScale;
@ -151,7 +151,7 @@ public class TimeTracker : IDisposable
for (int i = 0; i < animations.Count; i++)
{
var y = timelineP0.Y + timeBarHeight + timeStep.Y * i;
animations[i].ImGuiAnimation(s, saver, drawList, fontPtr, timelineP0, treeP0, timeStep, timeRatio, y, thickness, i);
animations[i].ImGuiAnimation(s, drawList, fontPtr, timelineP0, treeP0, timeStep, timeRatio, y, thickness, i);
DrawSeparator(drawList, timelineP0, y + timeStep.Y, animations[i].EndTime * timeRatio.X, timeHeight, timeBarHeight, ETrackerType.End);
}
ImGui.PopStyleVar();

View File

@ -54,7 +54,7 @@ public class PickingTexture : IDisposable
Bind(0);
}
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, IDictionary<FGuid, UModel> models)
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, IDictionary<FGuid, IRenderableModel> models)
{
Bind();
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

View File

@ -0,0 +1,662 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Options;
using FModel.Extensions;
using FModel.Views.Snooper.Models;
using ImGuiNET;
using Serilog;
using Serilog.Core;
using Serilog.Events;
namespace FModel.Views.Snooper;
public sealed class ExportModal
{
public static ExportModal Instance { get; } = new();
private const string Title = "Export Progress";
private const string IconXMark = "\uf057";
private const string IconFolder = "\uf08e";
private readonly Vector4[] _pieColors =
[
new(0.22f, 0.52f, 0.90f, 1f),
new(0.28f, 0.78f, 0.44f, 1f),
new(0.90f, 0.62f, 0.22f, 1f),
new(0.75f, 0.32f, 0.75f, 1f),
new(0.32f, 0.75f, 0.85f, 1f),
new(0.85f, 0.32f, 0.45f, 1f),
new(0.90f, 0.90f, 0.22f, 1f),
new(0.55f, 0.75f, 0.32f, 1f),
];
private static readonly Vector4 _redColor = new(1f, 0.4f, 0.4f, 1f);
private static readonly Vector4 _orangeColor = new(1f, 0.5f, 0f, 1f);
private static readonly Vector4 _yellowColor = new(1f, 1f, 0.4f, 1f);
private static readonly Vector4 _greenColor = new(0.4f, 1f, 0.4f, 1f);
private bool _openPopup;
private bool _modalOpen;
private bool _inProgress;
private CancellationTokenSource? _cts;
private IReadOnlyList<ExportResult>? _exportResults;
private ExportProgress _currentProgress;
private readonly IProgress<ExportProgress> _progress;
private readonly Stopwatch _stopwatch = new();
private readonly ConcurrentQueue<LogEvent> _pendingLogs = new();
private readonly List<ClassGroup> _classGroups = [];
private const int MaxGraphSamples = 4096;
private const float GraphSampleIntervalSec = 0.25f;
private readonly List<float> _graphSamples = [];
private float _graphNextSampleAt;
private int _graphLastCompleted;
private ExportModal()
{
ImGuiSink.Instance.OnExporterLogEvent += _pendingLogs.Enqueue;
_progress = new Progress<ExportProgress>(p => _currentProgress = p);
}
public void Export(IEnumerable<IExportableThing> nodes, string exportDirectory, ExportOptions options)
{
Reset();
_openPopup = true;
_inProgress = true;
_cts = new CancellationTokenSource();
_stopwatch.Restart();
var token = _cts.Token;
_ = Task.Run(async () =>
{
try
{
var session = new ExportSession(exportDirectory, options);
foreach (var node in nodes) node.AddToExportSession(session);
_exportResults = await session.RunAsync(_progress, token);
}
catch (OperationCanceledException)
{
Log.Error("Export cancelled by user");
}
catch (Exception ex)
{
Log.Error(ex, "Export failed");
}
finally
{
_stopwatch.Stop();
_inProgress = false;
}
}, token);
}
public void Draw()
{
if (_openPopup)
{
ImGui.OpenPopup(Title);
_modalOpen = true;
_openPopup = false;
}
if (!_modalOpen) return;
var viewport = ImGui.GetMainViewport();
ImGui.SetNextWindowSize(viewport.WorkSize * 0.75f, ImGuiCond.Always);
ImGui.SetNextWindowPos(viewport.GetCenter(), ImGuiCond.Always, new Vector2(0.5f, 0.5f));
var open = true;
if (ImGui.BeginPopupModal(Title, ref open, ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize))
{
if (ImGui.BeginChild("##ModalInfoBody", Vector2.Zero, ImGuiChildFlags.FrameStyle))
{
DrawProgressBar();
ImGui.Spacing();
ImGui.SeparatorText("Throughput");
DrawThroughputGraph();
ImGui.Spacing();
ImGui.SeparatorText("Export Log");
DrawExportLog();
}
ImGui.EndChild();
ImGui.EndPopup();
}
if (!open)
{
_modalOpen = false;
Reset();
}
}
private void Reset()
{
_pendingLogs.Clear();
_classGroups.Clear();
_exportResults = null;
_currentProgress = new ExportProgress(0, 0);
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
_graphSamples.Clear();
_graphNextSampleAt = 0f;
_graphLastCompleted = 0;
}
private void DrawProgressBar()
{
var e = _stopwatch.Elapsed;
ImGui.TextDisabled("\uf2f2");
ImGui.SameLine();
ImGui.TextUnformatted($"{e.Minutes:D2}:{e.Seconds:D2}.{e.Milliseconds / 10:D2}");
if (_inProgress && _currentProgress is { Total: > 0, Completed: > 1 })
{
var rate = _currentProgress.Completed / e.TotalSeconds;
if (rate > 0)
{
var remaining = (_currentProgress.Total - _currentProgress.Completed) / rate;
var eta = TimeSpan.FromSeconds(remaining);
ImGui.SameLine();
ImGui.TextDisabled("\uf017");
ImGui.SameLine();
ImGui.TextUnformatted($"ETA {eta.Minutes:D2}:{eta.Seconds:D2}");
}
}
else if (!_inProgress && _exportResults is { Count: > 0 })
{
ImGui.SameLine();
ImGui.TextColored(_greenColor, "\uf058");
ImGui.SameLine();
ImGui.TextUnformatted($"{_exportResults?.Count(r => r.Success) ?? 0} succeeded");
ImGui.SameLine();
ImGui.TextColored(_redColor, IconXMark);
ImGui.SameLine();
ImGui.TextUnformatted($"{_exportResults?.Count(r => !r.Success) ?? 0} failed");
}
ImGui.Spacing();
var barColor = _classGroups.Any(cg => cg.ErrorCount > 0) ? new Vector4(0.75f, 0.32f, 0.32f, 1f) : _inProgress ? new Vector4(0.22f, 0.52f, 0.90f, 1f) : new Vector4(0.28f, 0.78f, 0.44f, 1f);
var label = _inProgress && _currentProgress.Total > 0 ? _currentProgress.DisplayText : _inProgress ? "Preparing..." : "Done";
var barPos = ImGui.GetCursorScreenPos();
var barSize = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetFrameHeight());
var hovered = ImGui.IsMouseHoveringRect(barPos, barPos + barSize);
if (hovered && _inProgress)
{
barColor = new Vector4(0.75f, 0.32f, 0.32f, 1f);
label = "\uf05e Cancel";
}
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 4f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, barColor);
ImGui.ProgressBar(_currentProgress.Percentage, barSize, label);
ImGui.PopStyleColor();
ImGui.PopStyleVar(2);
if (_inProgress)
{
ImGui.SetCursorScreenPos(barPos);
if (ImGui.InvisibleButton("##BarAction", barSize))
{
_cts?.Cancel();
}
}
}
private void DrawThroughputGraph()
{
var elapsedSec = (float)_stopwatch.Elapsed.TotalSeconds;
if (_inProgress && _currentProgress.Completed > 0 && elapsedSec >= _graphNextSampleAt && _graphSamples.Count < MaxGraphSamples)
{
var completed = _currentProgress.Completed;
var rate = (completed - _graphLastCompleted) / GraphSampleIntervalSec;
_graphSamples.Add(MathF.Max(rate, 0f));
_graphLastCompleted = completed;
_graphNextSampleAt = elapsedSec + GraphSampleIntervalSec;
}
var graphPos = ImGui.GetCursorScreenPos();
var graphW = ImGui.GetContentRegionAvail().X;
var graphH = ImGui.GetFrameHeight() * 3f;
var graphSize = new Vector2(graphW, graphH);
ImGui.InvisibleButton("##ThroughputGraph", graphSize);
var dl = ImGui.GetWindowDrawList();
dl.AddRectFilled(graphPos, graphPos + graphSize, 0xFF_14_14_14);
var count = _graphSamples.Count;
switch (count)
{
case 0 when !_inProgress:
{
dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A);
return;
}
case < 2:
{
if (_inProgress)
{
const string msg = "Collecting data...";
var msgSz = ImGui.CalcTextSize(msg);
dl.AddText(graphPos + (graphSize - msgSz) * 0.5f, 0x44_FF_FF_FF, msg);
}
dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A);
return;
}
}
// Y scale
var maxVal = 0f;
for (var i = 0; i < count; i++) maxVal = MathF.Max(maxVal, _graphSamples[i]);
if (maxVal < 0.01f) maxVal = 1f;
maxVal *= 1.15f; // top headroom
dl.PushClipRect(graphPos, graphPos + graphSize, true);
// Horizontal grid lines at 25 / 50 / 75 %
for (var g = 1; g < 4; g++)
{
var gy = graphPos.Y + graphH * g / 4f;
dl.AddLine(new Vector2(graphPos.X, gy), new Vector2(graphPos.X + graphW, gy), 0x18_FF_FF_FF, 0.5f);
}
var lineColor = ImGui.GetColorU32(new Vector4(0.22f, 0.52f, 0.90f, 1f));
var fillColor = ImGui.GetColorU32(new Vector4(0.22f, 0.52f, 0.90f, 0.15f));
// xStep shrinks as samples accumulate → graph zooms out naturally.
// Oldest sample is always at x=0, newest at x=graphW.
var xStep = graphW / (count - 1f);
// Filled area (series of convex quads)
for (var i = 0; i < count - 1; i++)
{
var t0 = Math.Clamp(_graphSamples[i] / maxVal, 0f, 1f);
var t1 = Math.Clamp(_graphSamples[i + 1] / maxVal, 0f, 1f);
var x0 = graphPos.X + xStep * i;
var x1 = graphPos.X + xStep * (i + 1);
var y0 = graphPos.Y + graphH - graphH * t0;
var y1 = graphPos.Y + graphH - graphH * t1;
var bot = graphPos.Y + graphH;
dl.AddQuadFilled(new Vector2(x0, y0), new Vector2(x1, y1), new Vector2(x1, bot), new Vector2(x0, bot), fillColor);
}
// Line
for (var i = 0; i < count - 1; i++)
{
var t0 = Math.Clamp(_graphSamples[i] / maxVal, 0f, 1f);
var t1 = Math.Clamp(_graphSamples[i + 1] / maxVal, 0f, 1f);
var x0 = graphPos.X + xStep * i;
var x1 = graphPos.X + xStep * (i + 1);
var y0 = graphPos.Y + graphH - graphH * t0;
var y1 = graphPos.Y + graphH - graphH * t1;
dl.AddLine(new Vector2(x0, y0), new Vector2(x1, y1), lineColor, 1.5f);
}
// Dot on the newest sample (always at the right edge)
var newestT = Math.Clamp(_graphSamples[count - 1] / maxVal, 0f, 1f);
var newestY = graphPos.Y + graphH - graphH * newestT;
dl.AddCircleFilled(new Vector2(graphPos.X + graphW, newestY), 3f, lineColor);
var rateStr = $"{_graphSamples[count - 1]:F1} items/s";
dl.AddText(new Vector2(graphPos.X + 4, graphPos.Y + 3), 0xCC_FF_FF_FF, rateStr);
dl.PopClipRect();
dl.AddRect(graphPos, graphPos + graphSize, 0xFF_2A_2A_2A);
}
private void DrawExportLog()
{
var avail = ImGui.GetContentRegionAvail();
var rowH = ImGui.GetTextLineHeightWithSpacing();
var canvasSize = 15 * rowH + ImGui.GetFrameHeightWithSpacing();
var treeW = avail.X - canvasSize - ImGui.GetStyle().ItemSpacing.X;
DrainPendingLogs();
if (ImGui.BeginChild("##ExportLogTree", avail with { X = treeW }, ImGuiChildFlags.FrameStyle))
{
if (_classGroups.Count == 0)
{
ImGui.TextDisabled(_inProgress ? "Waiting for export data..." : "No export log.");
}
else for (var i = 0; i < _classGroups.Count; i++)
{
DrawClassGroup(i, _classGroups[i]);
}
}
ImGui.EndChild();
ImGui.SameLine();
if (ImGui.BeginChild("##RightPanel", new Vector2(canvasSize, -1), ImGuiChildFlags.FrameStyle))
{
DrawPieCanvas();
if (_classGroups.Count == 0)
{
ImGui.TextDisabled(_inProgress ? "Waiting for data..." : "No export data.");
}
else
{
var total = _classGroups.Sum(cg => cg.Objects.Count);
for (var i = 0; i < _classGroups.Count; i++)
{
var cg = _classGroups[i];
ImGui.PushStyleColor(ImGuiCol.Text, _pieColors[i % _pieColors.Length]);
ImGui.TextUnformatted("\uf111");
ImGui.PopStyleColor();
ImGui.SameLine();
ImGui.TextUnformatted(cg.Name);
ImGui.SameLine();
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled));
ImGui.TextUnformatted($"({(float) cg.Objects.Count / total * 100f:F1}%)");
ImGui.PopStyleColor();
}
}
}
ImGui.EndChild();
}
private void DrawPieCanvas()
{
const int segments = 64;
var canvasPos = ImGui.GetCursorScreenPos();
var size = ImGui.GetContentRegionAvail().X;
var canvasVec = new Vector2(size);
ImGui.InvisibleButton("##PieCanvas", canvasVec);
var isHovered = ImGui.IsItemHovered();
var mousePos = ImGui.GetMousePos();
var dl = ImGui.GetWindowDrawList();
dl.AddRectFilled(canvasPos, canvasPos + canvasVec, 0xFF_14_14_14);
dl.AddRect(canvasPos, canvasPos + canvasVec, 0xFF_32_32_32);
var total = _classGroups.Sum(cg => cg.Objects.Count);
var padding = ImGui.GetFrameHeight() * 0.5f;
var radius = size * 0.5f - padding;
var center = canvasPos + new Vector2(size * 0.5f);
if (total == 0 || radius <= 0)
{
dl.AddCircleFilled(center, MathF.Max(radius, 1f), 0xFF_1F_1F_1F, segments);
return;
}
// Determine hovered slice by angle
var hoveredSlice = -1;
if (isHovered)
{
var dx = mousePos.X - center.X;
var dy = mousePos.Y - center.Y;
if (dx * dx + dy * dy <= radius * radius)
{
var angle = MathF.Atan2(dy, dx);
while (angle < -MathF.PI / 2f) angle += MathF.PI * 2f;
var cur = -MathF.PI / 2f;
for (var i = 0; i < _classGroups.Count; i++)
{
var sweep = (float)_classGroups[i].Objects.Count / total * MathF.PI * 2f;
if (angle >= cur && angle < cur + sweep) { hoveredSlice = i; break; }
cur += sweep;
}
}
}
float startAngle = -MathF.PI / 2f;
for (var i = 0; i < _classGroups.Count; i++)
{
var cg = _classGroups[i];
var ratio = (float) cg.Objects.Count / total;
var sliceAngle = ratio * MathF.PI * 2f;
var col = ImGui.GetColorU32(_pieColors[i % _pieColors.Length]);
var r = i == hoveredSlice ? radius + padding * 0.25f : radius;
dl.PathLineTo(center);
dl.PathArcTo(center, r, startAngle, startAngle + sliceAngle);
dl.PathFillConvex(col);
var midAngle = startAngle + sliceAngle * 0.5f;
var labelPos = center + new Vector2(MathF.Cos(midAngle), MathF.Sin(midAngle)) * (radius * 0.62f);
var pctStr = $"{ratio * 100f:F0}%";
dl.AddText(labelPos - ImGui.CalcTextSize(pctStr) * 0.5f, 0xFF_FF_FF_FF, pctStr);
startAngle += sliceAngle;
}
dl.AddCircle(center, radius, 0xAA_00_00_00, segments, 1.5f);
if (hoveredSlice >= 0)
{
ImGui.BeginTooltip();
var cg = _classGroups[hoveredSlice];
ImGui.PushStyleColor(ImGuiCol.Text, _pieColors[hoveredSlice % _pieColors.Length]);
ImGui.TextUnformatted("\uf111");
ImGui.PopStyleColor();
ImGui.SameLine();
ImGui.TextUnformatted(cg.Name);
ImGui.EndTooltip();
}
}
private void DrawClassGroup(int index, ClassGroup cg)
{
ImGui.PushStyleColor(ImGuiCol.Header, new Vector4(0.20f, 0.20f, 0.20f, 1.00f));
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, new Vector4(0.69f, 0.69f, 1.00f, 0.20f));
ImGui.PushStyleColor(ImGuiCol.HeaderActive, new Vector4(0.69f, 0.69f, 1.00f, 0.20f));
var open = ImGui.CollapsingHeader($"{cg.Name} ({cg.Objects.Count})##class_{cg.Name}");
ImGui.PopStyleColor(3);
var headerMin = ImGui.GetItemRectMin();
var headerMax = ImGui.GetItemRectMax();
var labelW = MathF.Floor(ImGui.GetStyle().ItemSpacing.X * 0.5f);
var col = ImGui.GetColorU32(_pieColors[index % _pieColors.Length]);
ImGui.GetWindowDrawList().AddRectFilled(headerMin, headerMax with { X = headerMin.X + labelW }, col);
if (cg.ErrorCount > 0 && ImGui.IsItemHovered())
{
ImGui.SetTooltip($"{cg.ErrorCount} error{(cg.ErrorCount > 1 ? "s" : "")} in this class");
}
if (!open) return;
foreach (var og in cg.Objects)
{
DrawObjectGroup(cg.Name, og);
}
}
private void DrawObjectGroup(string className, ObjectGroup og)
{
var rightEdge = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
var hasErr = og.ErrorCount > 0;
if (hasErr) ImGui.PushStyleColor(ImGuiCol.Text, _redColor);
var flags = ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.FramePadding;
var open = ImGui.TreeNodeEx($"{og.Name}##obj_{className}_{og.Name}", flags);
if (hasErr) ImGui.PopStyleColor();
if (og.Entries.FirstOrDefault(e => !string.IsNullOrEmpty(e.FilePath)) is { } first)
{
var style = ImGui.GetStyle();
var btnW = ImGui.CalcTextSize(IconFolder).X + style.FramePadding.X * 2;
ImGui.SameLine(rightEdge - btnW);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, style.ItemSpacing with { X = 0 });
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
if (ImGui.Button($"{IconFolder}##obj_{className}_{og.Name}"))
{
OpenInExplorer(first.FilePath!);
}
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Open In Explorer");
ImGui.PopStyleColor();
ImGui.PopStyleVar();
}
if (!open) return;
foreach (var entry in og.Entries)
{
DrawLogEntry(entry);
}
ImGui.TreePop();
}
private void DrawLogEntry(LogEntry entry)
{
ImGui.PushStyleColor(ImGuiCol.Text, entry.Color);
ImGui.TextUnformatted(entry.Icon);
ImGui.PopStyleColor();
ImGui.SameLine();
ImGui.TextUnformatted(entry.Message);
if (ImGui.IsItemHovered() && entry.Exception != null)
{
DrawExceptionTooltip(entry.Exception);
}
}
private static void DrawExceptionTooltip(Exception ex)
{
ImGui.BeginTooltip();
ImGui.PushStyleColor(ImGuiCol.Text, _redColor);
ImGui.TextUnformatted(ex.GetType().ToString());
ImGui.PopStyleColor();
ImGui.SameLine(0, 0);
ImGui.TextDisabled(":");
ImGui.SameLine();
ImGui.TextUnformatted(ex.Message);
if (!string.IsNullOrEmpty(ex.StackTrace))
{
ImGui.SetWindowFontScale(0.85f);
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled));
ImGui.TextUnformatted(ex.StackTrace);
ImGui.PopStyleColor();
ImGui.SetWindowFontScale(1.0f);
}
ImGui.EndTooltip();
}
private void OpenInExplorer(string path)
{
try
{
if (File.Exists(path) || Directory.Exists(path)) Process.Start("explorer.exe", $"/select, \"{path}\"");
else Log.Warning("File or directory does not exist: {Path}", path);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to open in explorer: {Path}", path);
}
}
private void DrainPendingLogs()
{
while (_pendingLogs.TryDequeue(out var log))
{
var className = log.GetContext("ClassName");
var objectName = log.GetContext("ObjectName");
var filePath = log.GetContext("FilePath");
var cg = FindOrCreateClass(className);
var og = FindOrCreateObject(cg, objectName);
var entry = new LogEntry(log, filePath);
if (entry.Icon == IconXMark)
{
og.ErrorCount++;
cg.ErrorCount++;
}
og.Entries.Add(entry);
}
}
private ClassGroup FindOrCreateClass(string name)
{
foreach (var cg in _classGroups)
if (cg.Name == name) return cg;
var n = new ClassGroup(name);
_classGroups.Add(n);
return n;
}
private static ObjectGroup FindOrCreateObject(ClassGroup cg, string name)
{
foreach (var og in cg.Objects)
if (og.Name == name) return og;
var n = new ObjectGroup(name);
cg.Objects.Add(n);
return n;
}
private sealed class LogEntry(LogEvent log, string? filePath)
{
public string Icon { get; } = log.Level switch
{
LogEventLevel.Error or LogEventLevel.Fatal => IconXMark,
LogEventLevel.Warning => "\uf071",
LogEventLevel.Information => "\uf05a",
LogEventLevel.Debug => "\uf188",
_ => "\uf5dc"
};
public Vector4 Color { get; } = log.Level switch
{
LogEventLevel.Error or LogEventLevel.Fatal => _redColor,
LogEventLevel.Warning => _yellowColor,
_ => new Vector4(0.5f, 0.5f, 0.5f, 1f)
};
public string Message { get; } = $"[{log.Timestamp:HH:mm:ss.fff}] {log.RenderMessage()}";
public string? FilePath { get; } = filePath;
public Exception? Exception { get; } = log.Exception;
}
private sealed class ObjectGroup(string name)
{
public string Name { get; } = name;
public List<LogEntry> Entries { get; } = [];
public int ErrorCount { get; set; }
}
private sealed class ClassGroup(string name)
{
public string Name { get; } = name;
public List<ObjectGroup> Objects { get; } = [];
public int ErrorCount { get; set; }
}
}
public class ImGuiSink : ILogEventSink
{
public static ImGuiSink Instance { get; } = new();
private ImGuiSink()
{
}
public event Action<LogEvent>? OnExporterLogEvent;
public void Emit(LogEvent logEvent)
{
if (logEvent.Properties.TryGetValue("ExporterV2", out var state) && state is ScalarValue { Value: true })
{
OnExporterLogEvent?.Invoke(logEvent);
}
}
}

View File

@ -24,7 +24,7 @@ public class Attachment
_attachedFor = new List<string>();
}
public void Attach(UModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
public void Attach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Add(info);
@ -38,13 +38,13 @@ public class Attachment
transform.Scale = FVector.OneVector;
}
public void Detach(UModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
public void Detach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Remove(info);
SafeDetach(attachedTo, transform);
}
public void SafeDetach(UModel attachedTo, Transform transform)
public void SafeDetach(IRenderableModel attachedTo, Transform transform)
{
_attachedTo = string.Empty;
attachedTo.Attachments.RemoveAttachment(_modelName);

View File

@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse.UE4.Objects.Core.Math;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
namespace FModel.Views.Snooper.Models;
public interface IRenderableModel : IDisposable
public interface IRenderableModel : IExportableThing, IDisposable
{
protected int Handle { get; set; }
protected BufferObject<uint> Ebo { get; set; }
@ -24,15 +26,36 @@ public interface IRenderableModel : IDisposable
public List<Transform> Transforms { get; }
public Attachment Attachments { get; }
public FBox Box { get; protected init; }
public List<Socket> Sockets { get; }
public List<Collision> Collisions { get; }
public Material[] Materials { get; protected init; }
public bool IsTwoSided { get; internal set; }
public bool IsProp { get; internal set; }
public bool HasSockets { get; }
public bool HasCollisions { get; }
public int TransformsCount { get; }
public bool IsSetup { get; set; }
public bool IsVisible { get; set; }
public bool IsSelected { get; set; }
public bool ShowWireframe { get; set; }
public bool ShowCollisions { get; set; }
public int SelectedInstance { get; set; }
public void Setup(Options options);
public void SetupInstances();
public void Render(Shader shader, Texture checker = null, bool outline = false);
public void RenderCollision(Shader shader);
public void PickingRender(Shader shader);
public void Update(Options options);
public void AddInstance(Transform transform);
public Transform GetTransform();
}
public interface IExportableThing
{
public void AddToExportSession(ExportSession session);
}

View File

@ -61,7 +61,7 @@ public class Morph : IDisposable
}
}
public Morph(float[] vertices, Dictionary<uint, int> dict, UMorphTarget morphTarget, int index = 0)
public Morph(float[] vertices, Dictionary<uint, int> dict, UMorphTarget morphTarget, uint index = 0)
{
Name = morphTarget.Name;
Vertices = new float[vertices.Length];

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Objects.Core.Math;
@ -14,7 +13,7 @@ using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class SkeletalModel : UModel
public class SkeletalModel : UModel<SkinnedMeshVertex>
{
private BufferObject<float> _morphVbo;
@ -25,10 +24,10 @@ public class SkeletalModel : UModel
public float MorphTime;
public SkeletalModel(USkeletalMesh export, CSkeletalMesh skeletalMesh, Transform transform = null)
: base(export, skeletalMesh.LODs[LodLevel], export.Materials, skeletalMesh.LODs[LodLevel].Verts, skeletalMesh.LODs.Count, transform)
public SkeletalModel(USkeletalMesh export, SkeletalMeshDto skeletalMesh, Transform transform = null)
: base(export, skeletalMesh.LODs[LodLevel], export.Materials, skeletalMesh.LODs[LodLevel].Vertices, skeletalMesh.LODs.Count, transform)
{
Box = skeletalMesh.BoundingBox * Constants.SCALE_DOWN_RATIO;
Box = skeletalMesh.Bounds * Constants.SCALE_DOWN_RATIO;
Skeleton = new Skeleton(export.ReferenceSkeleton);
var sockets = new List<FPackageIndex>();
@ -101,10 +100,10 @@ public class SkeletalModel : UModel
foreach (var morph in export.MorphTargets)
{
if (!morph.TryLoad(out UMorphTarget morphTarget) || morphTarget.MorphLODModels.Length < 1 ||
morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].LODIndex].Vertices.Length < 1)
morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].SourceLodIndex].Vertices.Length < 1)
continue;
Morphs.Add(new Morph(cachedVertices, vertexLookup, morphTarget, skeletalMesh.LODs[LodLevel].LODIndex));
Morphs.Add(new Morph(cachedVertices, vertexLookup, morphTarget, skeletalMesh.LODs[LodLevel].SourceLodIndex));
}
}

View File

@ -1,11 +1,8 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports.Component.SplineMesh;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Math;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
@ -74,7 +71,7 @@ public class SplineModel : StaticModel
private readonly List<GpuParams> _splineParams;
private BufferObject<GpuParams> _ssbo;
public SplineModel(UStaticMesh export, CStaticMesh staticMesh, USplineMeshComponent splineMesh, Transform transform = null) : base(export, staticMesh, transform)
public SplineModel(UStaticMesh export, StaticMeshDto staticMesh, USplineMeshComponent splineMesh, Transform transform = null) : base(export, staticMesh, transform)
{
_splineParams = [new GpuParams(splineMesh)];

View File

@ -1,6 +1,6 @@
using System;
using System.Numerics;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
@ -11,24 +11,24 @@ using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class StaticModel : UModel
public class StaticModel : UModel<MeshVertex>
{
public StaticModel(UMaterialInterface unrealMaterial, CStaticMesh staticMesh) : base(unrealMaterial)
public StaticModel(UMaterialInterface unrealMaterial, StaticMeshDto staticMesh) : base(unrealMaterial)
{
var lod = staticMesh.LODs[LodLevel];
Indices = new uint[lod.Indices.Value.Length];
Indices = new uint[lod.Indices.Length];
for (int i = 0; i < Indices.Length; i++)
{
Indices[i] = (uint) lod.Indices.Value[i];
Indices[i] = lod.Indices[i];
}
Vertices = new float[lod.NumVerts * VertexSize];
for (int i = 0; i < lod.Verts.Length; i++)
Vertices = new float[lod.Vertices.Length * VertexSize];
for (int i = 0; i < lod.Vertices.Length; i++)
{
var count = 0;
var baseIndex = i * VertexSize;
var vert = lod.Verts[i];
var vert = lod.Vertices[i];
Vertices[baseIndex + count++] = i;
Vertices[baseIndex + count++] = vert.Position.X * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vert.Position.Z * Constants.SCALE_DOWN_RATIO;
@ -39,8 +39,8 @@ public class StaticModel : UModel
Vertices[baseIndex + count++] = vert.Tangent.X;
Vertices[baseIndex + count++] = vert.Tangent.Z;
Vertices[baseIndex + count++] = vert.Tangent.Y;
Vertices[baseIndex + count++] = vert.UV.U;
Vertices[baseIndex + count++] = vert.UV.V;
Vertices[baseIndex + count++] = vert.Uv.U;
Vertices[baseIndex + count++] = vert.Uv.V;
Vertices[baseIndex + count++] = .5f;
}
@ -52,7 +52,7 @@ public class StaticModel : UModel
AddInstance(Transform.Identity);
Box = staticMesh.BoundingBox * 1.5f * Constants.SCALE_DOWN_RATIO;
Box = staticMesh.Bounds * 1.5f * Constants.SCALE_DOWN_RATIO;
}
public StaticModel(UPaperSprite paperSprite, UTexture2D texture) : base(paperSprite)
@ -108,8 +108,8 @@ public class StaticModel : UModel
Box = new FBox(-backward, backward) * Constants.SCALE_DOWN_RATIO;
}
public StaticModel(UStaticMesh export, CStaticMesh staticMesh, Transform transform = null)
: base(export, staticMesh.LODs[LodLevel], export.Materials, staticMesh.LODs[LodLevel].Verts, staticMesh.LODs.Count, transform)
public StaticModel(UStaticMesh export, StaticMeshDto staticMesh, Transform transform = null)
: base(export, staticMesh.LODs[LodLevel], export.Materials, staticMesh.LODs[LodLevel].Vertices, staticMesh.LODs.Count, transform)
{
if (export.BodySetup.TryLoad(out UBodySetup bodySetup) && bodySetup.AggGeom != null)
{
@ -135,7 +135,7 @@ public class StaticModel : UModel
}
}
Box = staticMesh.BoundingBox * Constants.SCALE_DOWN_RATIO;
Box = staticMesh.Bounds * Constants.SCALE_DOWN_RATIO;
for (int i = 0; i < export.Sockets.Length; i++)
{
if (export.Sockets[i].Load<UStaticMeshSocket>() is not { } socket) continue;

View File

@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse.UE4.Assets;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.Utils;
using FModel.Settings;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
@ -24,7 +21,7 @@ public class VertexAttribute
public bool Enabled;
}
public abstract class UModel : IRenderableModel
public abstract class UModel<TVertex> : IRenderableModel where TVertex : struct, IMeshVertex
{
protected const int LodLevel = 0;
@ -58,12 +55,12 @@ public abstract class UModel : IRenderableModel
public List<Transform> Transforms { get; }
public Attachment Attachments { get; }
public FBox Box;
public readonly List<Socket> Sockets;
public readonly List<Collision> Collisions;
public Material[] Materials;
public bool IsTwoSided;
public bool IsProp;
public FBox Box { get; init; }
public List<Socket> Sockets { get; }
public List<Collision> Collisions { get; }
public Material[] Materials { get; init; }
public bool IsTwoSided { get; set; }
public bool IsProp { get; set; }
public int VertexSize => _vertexAttributes.Where(x => x.Enabled).Sum(x => x.Size);
public bool HasVertexColors => _vertexAttributes[(int) EAttribute.Colors].Enabled;
@ -76,7 +73,7 @@ public abstract class UModel : IRenderableModel
public bool IsSelected { get; set; }
public bool ShowWireframe { get; set; }
public bool ShowCollisions { get; set; }
public int SelectedInstance;
public int SelectedInstance { get; set; }
protected UModel()
{
@ -110,16 +107,16 @@ public abstract class UModel : IRenderableModel
_vertexAttributes[(int) EAttribute.Layer].Enabled = true;
}
protected UModel(UObject export, CBaseMeshLod lod, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CMeshVertex> vertices, int numLods, Transform transform = null) : this(export)
protected UModel(UObject export, MeshLodDto<TVertex> lod, IReadOnlyList<FPackageIndex> materials, IReadOnlyList<TVertex> vertices, int numLods, Transform transform = null) : this(export)
{
var hasCustomUvs = lod.ExtraUV.IsValueCreated;
UvCount = hasCustomUvs ? Math.Max(lod.NumTexCoords, numLods) : lod.NumTexCoords;
var hasCustomUvs = lod.ExtraUvs.Length > 0;
UvCount = hasCustomUvs ? Math.Max(lod.ExtraUvs.Length, numLods) : lod.ExtraUvs.Length + 1;
IsTwoSided = lod.IsTwoSided;
Indices = new uint[lod.Indices.Value.Length];
Indices = new uint[lod.Indices.Length];
for (int i = 0; i < Indices.Length; i++)
{
Indices[i] = (uint) lod.Indices.Value[i];
Indices[i] = lod.Indices[i];
}
Materials = new Material[materials.Count];
@ -129,11 +126,11 @@ public abstract class UModel : IRenderableModel
Materials[m] = new Material(unrealMaterial); else Materials[m] = new Material();
}
_vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0};
_vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0 };
_vertexAttributes[(int) EAttribute.BonesId].Enabled =
_vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is CSkelMeshVertex[];
_vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is SkinnedMeshVertex[];
Vertices = new float[lod.NumVerts * VertexSize];
Vertices = new float[vertices.Count * VertexSize];
for (int i = 0; i < vertices.Count; i++)
{
var count = 0;
@ -149,18 +146,18 @@ public abstract class UModel : IRenderableModel
Vertices[baseIndex + count++] = vert.Tangent.X;
Vertices[baseIndex + count++] = vert.Tangent.Z;
Vertices[baseIndex + count++] = vert.Tangent.Y;
Vertices[baseIndex + count++] = vert.UV.U;
Vertices[baseIndex + count++] = vert.UV.V;
Vertices[baseIndex + count++] = hasCustomUvs ? lod.ExtraUV.Value[0][i].U - 1 : .5f;
Vertices[baseIndex + count++] = vert.Uv.U;
Vertices[baseIndex + count++] = vert.Uv.V;
Vertices[baseIndex + count++] = hasCustomUvs ? lod.ExtraUvs[0][i].U - 1 : .5f;
if (HasVertexColors)
{
Vertices[baseIndex + count++] = lod.VertexColors[i].ToPackedARGB();
Vertices[baseIndex + count++] = lod.VertexColors![0].Colors[i].ToPackedARGB();
}
if (vert is CSkelMeshVertex skelVert)
if (vert is SkinnedMeshVertex skelVert)
{
int max = skelVert.Influences.Count;
int max = skelVert.Influences.Length;
for (int j = 0; j < 8; j++)
{
var boneID = j < max ? skelVert.Influences[j].Bone : (ushort) 0;
@ -172,10 +169,10 @@ public abstract class UModel : IRenderableModel
}
}
Sections = new Section[lod.Sections.Value.Length];
Sections = new Section[lod.Sections.Length];
for (var s = 0; s < Sections.Length; s++)
{
var section = lod.Sections.Value[s];
var section = lod.Sections[s];
Sections[s] = new Section(section.MaterialIndex, section.NumFaces * 3, section.FirstIndex);
if (section.IsValid) Sections[s].SetupMaterial(Materials[section.MaterialIndex]);
}
@ -392,10 +389,9 @@ public abstract class UModel : IRenderableModel
return socket.Transform.LocalMatrix * socketRelation;
}
public bool Save(out string label, out string savedFilePath)
public void AddToExportSession(ExportSession session)
{
var toSave = new Exporter(_export, UserSettings.Default.ExportOptions);
return toSave.TryWriteToDir(new DirectoryInfo(UserSettings.Default.ModelDirectory), out label, out savedFilePath);
session.Add(_export);
}
public virtual void Dispose()

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Texture;
@ -20,7 +21,7 @@ public class Options
public int SelectedMorph { get; private set; }
public int SelectedAnimation{ get; private set; }
public readonly Dictionary<FGuid, UModel> Models;
public readonly Dictionary<FGuid, IRenderableModel> Models;
public readonly Dictionary<FGuid, Texture> Textures;
public readonly List<Light> Lights;
@ -33,7 +34,7 @@ public class Options
public Options()
{
Models = new Dictionary<FGuid, UModel>();
Models = new Dictionary<FGuid, IRenderableModel>();
Textures = new Dictionary<FGuid, Texture>();
Lights = new List<Light>();
@ -110,30 +111,30 @@ public class Options
public void RemoveModel(FGuid guid)
{
if (!TryGetModel(guid, out var m) || m is not UModel model)
if (!TryGetModel(guid, out var m) || m is null)
return;
DetachAndRemoveModels(model, true);
model.Dispose();
DetachAndRemoveModels(m, true);
m.Dispose();
Models.Remove(guid);
}
private void DetachAndRemoveModels(UModel model, bool detach)
private void DetachAndRemoveModels(IRenderableModel model, bool detach)
{
foreach (var socket in model.Sockets.ToList())
{
foreach (var info in socket.AttachedModels)
{
if (!TryGetModel(info.Guid, out var m) || m is not UModel attachedModel)
if (!TryGetModel(info.Guid, out var m) || m is null)
continue;
var t = attachedModel.GetTransform();
if (attachedModel.IsProp)
var t = m.GetTransform();
if (m.IsProp)
{
attachedModel.Attachments.SafeDetach(model, t);
m.Attachments.SafeDetach(model, t);
RemoveModel(info.Guid);
}
else if (detach) attachedModel.Attachments.SafeDetach(model, t);
else if (detach) m.Attachments.SafeDetach(model, t);
}
if (socket.IsVirtual)
@ -209,8 +210,8 @@ public class Options
return texture != null;
}
public bool TryGetModel(out UModel model) => Models.TryGetValue(SelectedModel, out model);
public bool TryGetModel(FGuid guid, out UModel model) => Models.TryGetValue(guid, out model);
public bool TryGetModel([MaybeNullWhen(false)] out IRenderableModel model) => Models.TryGetValue(SelectedModel, out model);
public bool TryGetModel(FGuid guid, [MaybeNullWhen(false)] out IRenderableModel model) => Models.TryGetValue(guid, out model);
public bool TryGetSection(out Section section) => TryGetSection(SelectedModel, out section);
public bool TryGetSection(FGuid guid, out Section section)
@ -223,7 +224,7 @@ public class Options
section = null;
return false;
}
public bool TryGetSection(UModel model, out Section section)
public bool TryGetSection(IRenderableModel model, out Section section)
{
if (SelectedSection >= 0 && SelectedSection < model.Sections.Length)
section = model.Sections[SelectedSection]; else section = null;

View File

@ -6,13 +6,13 @@ using System.Threading;
using System.Windows;
using CUE4Parse_Conversion.Animations;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Options;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.Component.SplineMesh;
using CUE4Parse.UE4.Assets.Exports.Component.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.GeometryCollection;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Nanite;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
@ -29,7 +29,6 @@ using FModel.Views.Snooper.Lights;
using FModel.Views.Snooper.Models;
using FModel.Views.Snooper.Shading;
using OpenTK.Windowing.GraphicsLibraryFramework;
using UModel = FModel.Views.Snooper.Models.UModel;
namespace FModel.Views.Snooper;
@ -175,7 +174,7 @@ public class Renderer : IDisposable
t.Scale = offset.Scale3D;
}
UModel addedModel = null;
IRenderableModel addedModel = null;
switch (export)
{
case UStaticMesh st:
@ -344,7 +343,7 @@ public class Renderer : IDisposable
wnd.WindowShouldClose(true, true);
}
private void LoadStaticMesh(UStaticMesh original, ENaniteMeshFormat naniteFormat = ENaniteMeshFormat.OnlyNormalLODs)
private void LoadStaticMesh(UStaticMesh original, ENaniteMeshFormat naniteFormat = ENaniteMeshFormat.NoNanite)
{
var guid = original.LightingGuid;
if (Options.TryGetModel(guid, out var model))
@ -713,7 +712,7 @@ public class Renderer : IDisposable
continue;
var parameters = new CMaterialParams2();
unrealMaterial.GetParams(parameters, EMaterialFormat.FirstLayer);
unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly);
if (!parameters.TryGetLinearColor(out var color, "Color"))
color = FLinearColor.Gray;
@ -726,7 +725,7 @@ public class Renderer : IDisposable
if (!material.TryLoad(out UMaterialInterface unrealMaterial)) continue;
var parameters = new CMaterialParams2();
unrealMaterial.GetParams(parameters, EMaterialFormat.FirstLayer);
unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly);
if (!byte.TryParse(material.Name.SubstringAfterLast("_"), out var indexAsByte))
indexAsByte = byte.MaxValue;

View File

@ -317,7 +317,7 @@ public class Material : IDisposable
}
}
public bool ImGuiTextures(Dictionary<string, Texture> icons, UModel model)
public bool ImGuiTextures(Dictionary<string, Texture> icons, IRenderableModel model)
{
if (ImGui.BeginTable("material_textures", 2))
{

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Framework;
using ImGuiNET;
@ -38,30 +37,10 @@ public class Swap
}
}
public class Save
{
public bool Value;
public string Label;
public string Path;
public Save()
{
Reset();
}
public void Reset()
{
Value = false;
Label = string.Empty;
Path = string.Empty;
}
}
public class SnimGui
{
public readonly ImGuiController Controller;
private readonly Swap _swapper = new ();
private readonly Save _saver = new ();
private readonly string _renderer;
private readonly string _version;
private readonly float _tableWidth;
@ -96,7 +75,7 @@ public class SnimGui
SectionWindow("Material Inspector", s.Renderer, DrawMaterialInspector, false);
AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) =>
tracker.ImGuiTimeline(s, _saver, icons, animations, _outlinerSize, Controller.FontSemiBold));
tracker.ImGuiTimeline(s, icons, animations, _outlinerSize, Controller.FontSemiBold));
Window("World", () => DrawWorld(s), false);
@ -144,36 +123,13 @@ public class SnimGui
}
});
Modal("Saved", _saver.Value, () =>
{
ImGui.TextWrapped($"Successfully saved {_saver.Label}");
ImGui.Separator();
var size = new Vector2(120, 0);
if (ImGui.Button("OK", size))
{
_saver.Reset();
ImGui.CloseCurrentPopup();
}
ImGui.SetItemDefaultFocus();
ImGui.SameLine();
if (ImGui.Button("Show In Explorer", size))
{
Process.Start("explorer.exe", $"/select, \"{_saver.Path.Replace('/', '\\')}\"");
_saver.Reset();
ImGui.CloseCurrentPopup();
}
});
ExportModal.Instance.Draw();
}
private void DrawWorld(Snooper s)
{
if (ImGui.BeginTable("world_details", 2, ImGuiTableFlags.SizingStretchProp))
{
var b = false;
var length = s.Renderer.Options.Models.Count;
NoFramePaddingOnY(() =>
@ -184,31 +140,7 @@ public class SnimGui
if (ImGui.SmallButton("Save All"))
{
foreach (var model in s.Renderer.Options.Models.Values)
{
b |= model.Save(out _, out _);
}
}
});
Modal("Saved", b, () =>
{
ImGui.TextWrapped($"Successfully saved {length} models");
ImGui.Separator();
var size = new Vector2(120, 0);
if (ImGui.Button("OK", size))
{
ImGui.CloseCurrentPopup();
}
ImGui.SetItemDefaultFocus();
ImGui.SameLine();
if (ImGui.Button("Show In Explorer", size))
{
Process.Start("explorer.exe", $"/select, \"{UserSettings.Default.ModelDirectory.Replace('/', '\\')}\"");
ImGui.CloseCurrentPopup();
ExportModal.Instance.Export(s.Renderer.Options.Models.Values, UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions());
}
});
@ -412,7 +344,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
if (ImGui.MenuItem("Save"))
{
s.WindowShouldFreeze(true);
_saver.Value = model.Save(out _saver.Label, out _saver.Path);
ExportModal.Instance.Export([model], UserSettings.Default.ModelDirectory, UserSettings.GetExportOptions());
s.WindowShouldFreeze(false);
}
if (ImGui.MenuItem("Animate", model is SkeletalModel))
@ -591,7 +523,11 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
{
ImGui.PushID(0); ImGui.BeginDisabled(model.TransformsCount < 2);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.SliderInt("", ref model.SelectedInstance, 0, model.TransformsCount - 1, "Instance %i", ImGuiSliderFlags.AlwaysClamp);
var instance = model.SelectedInstance;
if (ImGui.SliderInt("", ref instance, 0, model.TransformsCount - 1, "Instance %i", ImGuiSliderFlags.AlwaysClamp))
{
model.SelectedInstance = instance;
}
ImGui.EndDisabled(); ImGui.PopID();
if (ImGui.BeginTable("guizmo_controls", 2, ImGuiTableFlags.SizingStretchProp))
@ -669,7 +605,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.PopStyleVar();
}
private void DrawMaterialInspector(Dictionary<string, Texture> icons, UModel model, Section section)
private void DrawMaterialInspector(Dictionary<string, Texture> icons, IRenderableModel model, Section section)
{
var material = model.Materials[section.MaterialIndex];
@ -906,7 +842,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.End();
}
private void MeshWindow(string name, Renderer renderer, Action<Dictionary<string, Texture>, UModel> content, bool styled = true)
private void MeshWindow(string name, Renderer renderer, Action<Dictionary<string, Texture>, IRenderableModel> content, bool styled = true)
{
Window(name, () =>
{
@ -915,7 +851,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
}, styled);
}
private void SectionWindow(string name, Renderer renderer, Action<Dictionary<string, Texture>, UModel, Section> content, bool styled = true)
private void SectionWindow(string name, Renderer renderer, Action<Dictionary<string, Texture>, IRenderableModel, Section> content, bool styled = true)
{
MeshWindow(name, renderer, (icons, model) =>
{