Merge branch 'dev' into dev

This commit is contained in:
Krowe Moh 2025-07-24 14:39:12 +10:00 committed by GitHub
commit 1e3e4df206
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 163 additions and 72 deletions

@ -1 +1 @@
Subproject commit 566bc1f731b993a09b50772fef8de4d9d5e36243
Subproject commit b5a3fd7fc4463740e885ea1d17dc9a1697b3b9b9

View File

@ -123,15 +123,9 @@ public class BaseCommunity : BaseIcon
{
if (!bShort) return base.GetCosmeticSeason(seasonNumber);
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
(int chapterIdx, int seasonIdx) = GetInternalSID(int.Parse(s));
return s switch
{
"10" => $"C{chapterIdx} SX",
"27" => $"Fortnite: OG",
"32" => $"Fortnite: Remix",
"35" => $"C{chapterIdx} MS1",
_ => $"C{chapterIdx} S{seasonIdx}"
};
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
var prefix = int.TryParse(seasonIdx, out _) ? "S" : "";
return onlySeason ? $"{prefix}{seasonIdx}" : $"C{chapterIdx} {prefix}{seasonIdx}";
}
private new void DrawBackground(SKCanvas c)

View File

@ -221,55 +221,32 @@ public class BaseIcon : UCreator
return Utils.RemoveHtmlTags(string.Format(format, name));
}
protected (int, int) GetInternalSID(int number)
protected (string, string, bool) GetInternalSID(string number)
{
static int GetSeasonsInChapter(int chapter) => chapter switch
{
1 => 10,
2 => 8,
3 => 4,
4 => 5,
5 => 5,
_ => 10
};
if (!Utils.TryLoadObject("FortniteGame/Plugins/GameFeatures/BattlePassBase/Content/DataTables/Athena_SeasonTitles.Athena_SeasonTitles", out UDataTable seasonTitles) ||
!seasonTitles.TryGetDataTableRow(number, StringComparison.InvariantCulture, out var row) ||
!row.TryGetValue(out FText chapterText, "DisplayChapterText") ||
!row.TryGetValue(out FText seasonText, "DisplaySeasonText") ||
!row.TryGetValue(out FName displayType, "DisplayType"))
return (string.Empty, string.Empty, true);
var chapterIdx = 0;
var seasonIdx = 0;
while (number > 0)
{
var seasonsInChapter = GetSeasonsInChapter(++chapterIdx);
if (number > seasonsInChapter)
number -= seasonsInChapter;
else
{
seasonIdx = number;
number = 0;
}
}
return (chapterIdx, seasonIdx);
var onlySeason = displayType.Text.EndsWith("::OnlySeason") || (chapterText.Text == seasonText.Text && !int.TryParse(seasonText.Text, out _));
return (chapterText.Text, seasonText.Text, onlySeason);
}
protected string GetCosmeticSeason(string seasonNumber)
{
var s = seasonNumber["Cosmetics.Filter.Season.".Length..];
var initial = int.Parse(s);
(int chapterIdx, int seasonIdx) = GetInternalSID(initial);
(string chapterIdx, string seasonIdx, bool onlySeason) = GetInternalSID(s);
var season = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "SeasonTextFormat", "Season {0}");
var introduced = Utils.GetLocalizedResource("Fort.Cosmetics", "CosmeticItemDescription_Season", "\nIntroduced in <SeasonText>{0}</>.");
if (s == "10") return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, "X")));
if (initial <= 10) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, s)));
if (onlySeason) return Utils.RemoveHtmlTags(string.Format(introduced, string.Format(season, seasonIdx)));
var chapter = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterTextFormat", "Chapter {0}");
var chapterFormat = Utils.GetLocalizedResource("AthenaSeasonItemDefinitionInternal", "ChapterSeasonTextFormat", "{0}, {1}");
var d = string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format(season, seasonIdx));
return s switch
{
"27" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: OG"))),
"32" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format("Fortnite: Remix"))),
"35" => Utils.RemoveHtmlTags(string.Format(introduced, string.Format(chapterFormat, string.Format(chapter, chapterIdx), string.Format("MS1")))),
_ => Utils.RemoveHtmlTags(string.Format(introduced, d))
};
return Utils.RemoveHtmlTags(string.Format(introduced, d));
}
protected void CheckGameplayTags(FInstancedStruct[] dataList)

View File

@ -95,6 +95,8 @@ public class BaseIconStats : BaseIcon
weaponRowValue.TryGetValue(out float heatMax, "OverheatingMaxValue"); //Maximum heat overheating weapons can hold before they need to cool off
weaponRowValue.TryGetValue(out float heatPerShot, "OverheatHeatingValue"); //Heat generated per shot on overheat weapons
weaponRowValue.TryGetValue(out float overheatCooldown, "OverheatedCooldownDelay"); //Cooldown after a weapon reaches its maximum heat capacity
weaponRowValue.TryGetValue(out int cartridgePerFire, "CartridgePerFire"); //Amount of bullets shot after pressing the fire button once
weaponRowValue.TryGetValue(out float burstFiringRate, "BurstFiringRate"); //Item firing rate during a burst, value is shots per second
{
var multiplier = bpc != 0f ? bpc : 1;
if (dmgPb != 0f)
@ -122,7 +124,12 @@ public class BaseIconStats : BaseIcon
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), clipSize, 40));
}
if (firingRate != 0f)
var burstEquation = cartridgePerFire / (((cartridgePerFire - 1f) / burstFiringRate) + (1f / firingRate));
if (burstEquation != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), burstEquation, 11));
}
else if (firingRate != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "27B80BA44805ABD5A2D2BAB2902B250C", "Fire Rate"), firingRate, 11));
}

View File

@ -30,6 +30,21 @@ public class CustomDirectory : ViewModel
new("Shop Backgrounds", "ShooterGame/Content/UI/OutOfGame/MainMenu/Store/Shared/Textures/"),
new("Weapon Renders", "ShooterGame/Content/UI/Screens/OutOfGame/MainMenu/Collection/Assets/Large/")
};
case "Dead by Daylight":
return new List<CustomDirectory>
{
new("Characters V1", "DeadByDaylight/Plugins/DBDCharacters/"),
new("Characters V2", "DeadByDaylight/Plugins/Runtime/Bhvr/DBDCharacters/"),
new("Characters (Deprecated)", "DeadbyDaylight/Content/Characters/"),
new("Meshes", "DeadByDaylight/Content/Meshes/"),
new("Textures", "DeadByDaylight/Content/Textures/"),
new("Icons", "DeadByDaylight/Content/UI/UMGAssets/Icons/"),
new("Blueprints", "DeadByDaylight/Content/Blueprints/"),
new("Audio Events", "DeadByDaylight/Content/Audio/Events/"),
new("Audio", "DeadByDaylight/Content/WwiseAudio/Cooked/"),
new("Data Tables", "DeadByDaylight/Content/Data/"),
new("Localization", "DeadByDaylight/Content/Localization/")
};
default:
return new List<CustomDirectory>();
}

View File

@ -73,7 +73,8 @@ namespace FModel.Settings
CompressionFormat = Default.CompressionFormat,
Platform = Default.CurrentDir.TexturePlatform,
ExportMorphTargets = Default.SaveMorphTargets,
ExportMaterials = Default.SaveEmbeddedMaterials
ExportMaterials = Default.SaveEmbeddedMaterials,
ExportHdrTexturesAsHdr = Default.SaveHdrTexturesAsHdr
};
private bool _showChangelog = true;
@ -446,6 +447,13 @@ namespace FModel.Settings
set => SetProperty(ref _cameraMode, value);
}
private int _wwiseMaxBnkPrefetch;
public int WwiseMaxBnkPrefetch
{
get => _wwiseMaxBnkPrefetch;
set => SetProperty(ref _wwiseMaxBnkPrefetch, value);
}
private int _previewMaxTextureSize = 1024;
public int PreviewMaxTextureSize
{
@ -508,5 +516,12 @@ namespace FModel.Settings
get => _saveSkeletonAsMesh;
set => SetProperty(ref _saveSkeletonAsMesh, value);
}
private bool _saveHdrTexturesAsHdr = true;
public bool SaveHdrTexturesAsHdr
{
get => _saveHdrTexturesAsHdr;
set => SetProperty(ref _saveHdrTexturesAsHdr, value);
}
}
}

View File

@ -31,6 +31,7 @@ using CUE4Parse.UE4.Localization;
using CUE4Parse.UE4.Objects.Core.Serialization;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Oodle.Objects;
using CUE4Parse.UE4.Pak;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.Shaders;
using CUE4Parse.UE4.Versions;
@ -38,7 +39,9 @@ using CUE4Parse.UE4.Wwise;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Sounds;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.GameTypes.AshEchoes.FileProvider;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.BinaryConfig;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.Utils;
using EpicManifestParser;
@ -86,6 +89,13 @@ public class CUE4ParseViewModel : ViewModel
set => SetProperty(ref _modelIsOverwritingMaterial, value);
}
private bool _modelIsWaitingAnimation;
public bool ModelIsWaitingAnimation
{
get => _modelIsWaitingAnimation;
set => SetProperty(ref _modelIsWaitingAnimation, value);
}
public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true };
private Snooper _snooper;
@ -126,6 +136,8 @@ public class CUE4ParseViewModel : ViewModel
public SearchViewModel SearchVm { get; }
public TabControlViewModel TabControl { get; }
public ConfigIni IoStoreOnDemand { get; }
private Lazy<WwiseProvider> _wwiseProviderLazy;
public WwiseProvider WwiseProvider => _wwiseProviderLazy.Value;
public CUE4ParseViewModel()
{
@ -164,6 +176,7 @@ public class CUE4ParseViewModel : ViewModel
[
new(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\KONAMI\\eFootball\\ST\\Download")
], SearchOption.AllDirectories, versionContainer, pathComparer),
_ when versionContainer.Game is EGame.GAME_AshEchoes => new AEDefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer),
_ => new DefaultFileProvider(gameDirectory, SearchOption.AllDirectories, versionContainer, pathComparer)
};
@ -275,6 +288,7 @@ public class CUE4ParseViewModel : ViewModel
}
Provider.Initialize();
_wwiseProviderLazy = new Lazy<WwiseProvider>(() => new WwiseProvider(Provider, UserSettings.Default.WwiseMaxBnkPrefetch));
Log.Information($"{Provider.Versions.Game} ({Provider.Versions.Platform}) | Archives: x{Provider.UnloadedVfs.Count} | AES: x{Provider.RequiredKeys.Count} | Loose Files: x{Provider.Files.Count}");
});
}
@ -594,6 +608,16 @@ public class CUE4ParseViewModel : ViewModel
break;
}
case "ini" when entry.Name.Contains("BinaryConfig"):
{
var ar = entry.CreateReader();
var configCache = new FConfigCacheIni(ar);
TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(configCache, Formatting.Indented), saveProperties, updateUi);
break;
}
case "upluginmanifest":
case "code-workspace":
case "projectstore":
@ -606,6 +630,7 @@ public class CUE4ParseViewModel : ViewModel
case "gitignore":
case "LICENSE":
case "template":
case "stUMeta": // LIS: Double Exposure
case "vmodule":
case "glslfx":
case "cptake":
@ -691,9 +716,11 @@ public class CUE4ParseViewModel : ViewModel
var archive = entry.CreateReader();
var wwise = new WwiseReader(archive);
TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(wwise, Formatting.Indented), saveProperties, updateUi);
foreach (var (name, data) in wwise.WwiseEncodedMedias)
var medias = WwiseProvider.ExtractBankSounds(wwise);
foreach (var media in medias)
{
SaveAndPlaySound(entry.Path.SubstringBeforeWithLast('/') + name, "WEM", data);
SaveAndPlaySound(media.OutputPath, media.Extension, media.Data);
}
break;
@ -772,6 +799,8 @@ public class CUE4ParseViewModel : ViewModel
break;
}
case "res": // just skip
case "luac": // compiled lua
case "bytes": // wuthering waves
break;
default:
{
@ -878,22 +907,12 @@ public class CUE4ParseViewModel : ViewModel
TabControl.SelectedTab.AddImage(sourceFile.SubstringAfterLast('/'), false, bitmap, false, updateUi);
return false;
}
case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent { EventCookedData: { } wwiseData }:
case UAkAudioEvent when isNone && pointer.Object.Value is UAkAudioEvent audioEvent:
{
foreach (var kvp in wwiseData.EventLanguageMap)
var extractedSounds = WwiseProvider.ExtractAudioEventSounds(audioEvent);
foreach (var sound in extractedSounds)
{
if (!kvp.Value.HasValue) continue;
foreach (var media in kvp.Value.Value.Media)
{
if (!Provider.TrySaveAsset(Path.Combine("Game/WwiseAudio/", media.MediaPathName.Text), out var data)) continue;
var namedPath = string.Concat(
Provider.ProjectName, "/Content/WwiseAudio/",
media.DebugName.Text.SubstringBeforeLast('.').Replace('\\', '/'),
" (", kvp.Key.LanguageName.Text, ")");
SaveAndPlaySound(namedPath, media.MediaPathName.Text.SubstringAfterLast('.'), data);
}
SaveAndPlaySound(sound.OutputPath, sound.Extension, sound.Data);
}
return false;
}
@ -940,7 +959,7 @@ public class CUE4ParseViewModel : ViewModel
SnooperViewer.Run();
return true;
}
case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || SnooperViewer.Renderer.Options.ModelIsWaitingAnimation:
case UAnimSequenceBase when isNone && UserSettings.Default.PreviewAnimations || ModelIsWaitingAnimation:
{
// animate all animations using their specified skeleton or when we explicitly asked for a loaded model to be animated (ignoring whether we wanted to preview animations)
SnooperViewer.Renderer.Animate(pointer.Object.Value);

View File

@ -99,7 +99,7 @@ public class GameSelectorViewModel : ViewModel
yield return GetUnrealEngineGame("9361c8c6d2f34b42b5f2f61093eedf48", "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds);
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks", EGame.GAME_Valorant);
yield return DirectorySettings.Default("VALORANT [LIVE]", Constants._VAL_LIVE_TRIGGER, ue: EGame.GAME_Valorant);
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_UE4_27); // Dead By Daylight
yield return GetSteamGame(381210, "\\DeadByDaylight\\Content\\Paks", EGame.GAME_DeadByDaylight, aesKey: "0x22b1639b548124925cf7b9cbaa09f9ac295fcf0324586d6b37ee1d42670b39b3"); // Dead By Daylight
yield return GetSteamGame(578080, "\\TslGame\\Content\\Paks", EGame.GAME_PlayerUnknownsBattlegrounds); // PUBG
yield return GetSteamGame(1172380, "\\SwGame\\Content\\Paks", EGame.GAME_StarWarsJediFallenOrder); // STAR WARS Jedi: Fallen Order™
yield return GetSteamGame(677620, "\\PortalWars\\Content\\Paks", EGame.GAME_Splitgate); // Splitgate
@ -151,13 +151,13 @@ public class GameSelectorViewModel : ViewModel
return null;
}
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion)
private DirectorySettings GetSteamGame(int id, string pakDirectory, EGame ueVersion, string aesKey = "")
{
var steamInfo = SteamDetection.GetSteamGameById(id);
if (steamInfo is not null)
{
Log.Debug("Found {GameName} in steam manifests", steamInfo.Name);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion);
return DirectorySettings.Default(steamInfo.Name, $"{steamInfo.GameRoot}{pakDirectory}", ue: ueVersion, aes: aesKey);
}
return null;

View File

@ -105,7 +105,7 @@ public class TabImage : ViewModel
if (PixelFormatUtils.IsHDR(bitmap.PixelFormat) || (UserSettings.Default.TextureExportFormat != ETextureFormat.Jpeg && UserSettings.Default.TextureExportFormat != ETextureFormat.Png))
{
ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, out var ext);
ImageBuffer = bitmap.Encode(UserSettings.Default.TextureExportFormat, UserSettings.Default.SaveHdrTexturesAsHdr, out var ext);
ExportName += "." + ext;
}
else

View File

@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:system="clr-namespace:System;assembly=mscorlib"

View File

@ -232,6 +232,11 @@
<TextBlock Grid.Row="18" Grid.Column="0" Text="Show Blueprint Decompile" VerticalAlignment="Center" Margin="0 0 0 5" ToolTip="Shows Decompiled Blueprints in a cpp format" />
<CheckBox Grid.Row="18" Grid.Column="2" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding ShowDecompileOption, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}" Margin="0 5 0 10"/>
<TextBlock Grid.Row="18" Grid.Column="0" Text="Max Wwise Bank (.BNK) Prefetch" VerticalAlignment="Center" Margin="0 0 0 5" />
<Slider Grid.Row="18" Grid.Column="2" Grid.ColumnSpan="5" TickPlacement="None" Minimum="0" Maximum="512" Ticks="0,2,4,8,16,32,64,128,256,512"
AutoToolTipPlacement="BottomRight" IsMoveToPointEnabled="True" IsSnapToTickEnabled="True" Margin="0 5 0 5"
Value="{Binding WwiseMaxBnkPrefetch, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CreatorTemplate">
@ -322,6 +327,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -494,6 +500,11 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="20" Grid.Column="0" Text="Save HDR Textures as Radiance .hdr" VerticalAlignment="Center" />
<CheckBox Grid.Row="20" Grid.Column="2" Grid.ColumnSpan="3" Content="{Binding IsChecked, RelativeSource={RelativeSource Self}, Converter={x:Static converters:BoolToToggleConverter.Instance}}"
IsChecked="{Binding SaveHdrTexturesAsHdr, Source={x:Static local:Settings.UserSettings.Default}, Mode=TwoWay}"
Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" Margin="0 5 0 5"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="KeybindingsTemplate">

View File

@ -16,7 +16,6 @@ namespace FModel.Views.Snooper;
public class Options
{
public FGuid SelectedModel { get; private set; }
public bool ModelIsWaitingAnimation { get; private set; }
public int SelectedSection { get; private set; }
public int SelectedMorph { get; private set; }
public int SelectedAnimation{ get; private set; }
@ -238,7 +237,7 @@ public class Options
public void AnimateMesh(bool value)
{
ModelIsWaitingAnimation = value;
Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation = value;
}
public void ResetModelsLightsAnimations()

View File

@ -123,7 +123,7 @@ public class Renderer : IDisposable
public void Animate(UObject anim)
{
if (!Options.ModelIsWaitingAnimation)
if (!Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation)
{
if (anim is UAnimSequenceBase animBase)
{

View File

@ -624,6 +624,9 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
ImGui.EndTable();
}
ImGui.SeparatorText("Manual Inputs");
model.Transforms[model.SelectedInstance].ImGuiTransform(s.Renderer.CameraOp.Speed / 100f);
ImGui.EndTabItem();
}

View File

@ -1,5 +1,6 @@
using System.Numerics;
using CUE4Parse.UE4.Objects.Core.Math;
using ImGuiNET;
namespace FModel.Views.Snooper;
@ -48,5 +49,55 @@ public class Transform
ModifyLocal(_saved.Value);
}
public void ImGuiTransform(float speed)
{
const float width = 100f;
if (ImGui.TreeNode("Position"))
{
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("X", ref Position.X, speed, 0f, 0f, "%.2f m");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Y", ref Position.Y, speed, 0f, 0f, "%.2f m");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Z", ref Position.Z, speed, 0f, 0f, "%.2f m");
ImGui.TreePop();
}
if (ImGui.TreeNode("Rotation"))
{
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("W", ref Rotation.W, .005f, 0f, 0f, "%.3f rad");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("X", ref Rotation.X, .005f, 0f, 0f, "%.3f rad");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Y", ref Rotation.Y, .005f, 0f, 0f, "%.3f rad");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Z", ref Rotation.Z, .005f, 0f, 0f, "%.3f rad");
ImGui.TreePop();
}
if (ImGui.TreeNode("Scale"))
{
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("X", ref Scale.X, speed, 0f, 0f, "%.3f");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Y", ref Scale.Y, speed, 0f, 0f, "%.3f");
ImGui.SetNextItemWidth(width);
ImGui.DragFloat("Z", ref Scale.Z, speed, 0f, 0f, "%.3f");
ImGui.TreePop();
}
}
public override string ToString() => Matrix.Translation.ToString();
}