removed guid from models + bone delta matrix + timeline scroller

This commit is contained in:
4sval 2023-02-20 20:06:43 +01:00
parent 0ed26e1a7d
commit cdb52d096f
10 changed files with 79 additions and 68 deletions

View File

@ -15,10 +15,14 @@ public class Animation : IDisposable
public readonly float StartTime; // Animation Start Time
public readonly float EndTime; // Animation End Time
public readonly float TotalElapsedTime; // Animation Max Time
public readonly string TargetSkeleton;
public int CurrentSequence;
public int FrameInSequence; // Current Sequence's Frame to Display
public bool IsActive;
public bool IsSelected;
public readonly List<FGuid> AttachedModels;
public Animation()
@ -30,13 +34,15 @@ public class Animation : IDisposable
public Animation(string name, CAnimSet animSet) : this()
{
Name = name;
TargetSkeleton = animSet.OriginalAnim.Name;
Sequences = new Sequence[animSet.Sequences.Count];
for (int i = 0; i < Sequences.Length; i++)
{
Sequences[i] = new Sequence(animSet.Sequences[i]);
TotalElapsedTime += animSet.Sequences[i].NumFrames * Sequences[i].TimePerFrame;
EndTime = Sequences[i].EndTime;
TotalElapsedTime += animSet.Sequences[i].NumFrames * Sequences[i].TimePerFrame;
}
if (Sequences.Length > 0)
@ -83,9 +89,16 @@ public class Animation : IDisposable
var p1 = new Vector2(timelineP0.X + StartTime * timeRatio.X + t, y + t);
var p2 = new Vector2(timelineP0.X + EndTime * timeRatio.X - t, y + timeStep.Y - t);
drawList.AddRectFilled(p1, p2, 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop);
ImGui.SetCursorScreenPos(p1);
ImGui.InvisibleButton($"timeline_sequencetracker_{Name}", new Vector2(EndTime * timeRatio.X - t, timeStep.Y - t), ImGuiButtonFlags.MouseButtonLeft);
IsActive = ImGui.IsItemActive();
drawList.PushClipRect(treeP0, treeP1);
drawList.AddRectFilled(p1, p2, IsSelected ? 0xFF48B048 : 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop);
drawList.PushClipRect(p1, p2, true);
drawList.AddText(p1, 0xFF000000, $"{TargetSkeleton} - {CurrentSequence + 1}/{Sequences.Length} - {FrameInSequence}/{Sequences[CurrentSequence].EndFrame}");
drawList.PopClipRect();
drawList.PushClipRect(treeP0 with { Y = p1.Y }, treeP1 with { Y = p2.Y }, true);
drawList.AddText(treeP0 with { Y = y + timeStep.Y / 4.0f }, 0xFFFFFFFF, Name);
drawList.PopClipRect();
}

View File

@ -14,6 +14,7 @@ public class Sequence
public readonly float EndTime;
public readonly int EndFrame;
public readonly int LoopingCount;
public readonly bool IsAdditive;
public Sequence(CAnimSequence sequence)
{
@ -24,6 +25,7 @@ public class Sequence
EndTime = StartTime + Duration;
EndFrame = (Duration / TimePerFrame).FloorToInt() - 1;
LoopingCount = sequence.LoopingCount;
IsAdditive = sequence.bAdditive;
}
private readonly float _height = 20.0f;

View File

@ -92,15 +92,9 @@ public class Skeleton : IDisposable
{
for (int frame = 0; frame < _animatedBonesTransform[s][boneIndices.BoneIndex].Length; frame++)
{
// TODO: append the delta transform
// ex: if bone 4, with parent 3, has its first found parent transform at 2,
// we need to append the transform from 4 to 3 so that bone 4 won't be placed where bone 3 should be
_animatedBonesTransform[s][boneIndices.BoneIndex][frame] = new Transform
{
Relation = _animatedBonesTransform[s][boneIndices.ParentTrackIndex][frame].Matrix,
Rotation = originalTransform.Rotation,
Position = originalTransform.Position,
Scale = originalTransform.Scale
Relation = originalTransform.LocalMatrix * _animatedBonesTransform[s][boneIndices.ParentTrackIndex][frame].Matrix
};
}
}
@ -219,7 +213,7 @@ public class Skeleton : IDisposable
do
{
var parentBoneIndices = BonesIndicesByLoweredName[loweredParentBoneName];
if (parentBoneIndices.HasTrack) boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex;
if (parentBoneIndices.HasTrack || parentBoneIndices.HasParentTrack) boneIndices.ParentTrackIndex = parentBoneIndices.BoneIndex;
else loweredParentBoneName = parentBoneIndices.LoweredParentBoneName;
} while (!boneIndices.HasParentTrack);
}
@ -255,7 +249,7 @@ public class Skeleton : IDisposable
_previousSequenceFrame = frameInSequence;
_ssbo.Bind();
for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++)
for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++) // interpolate here
_ssbo.Update(boneIndex, _invertedBonesMatrix[boneIndex] * _animatedBonesTransform[_previousAnimationSequence][boneIndex][_previousSequenceFrame].Matrix);
_ssbo.Unbind();
}

View File

@ -59,24 +59,26 @@ public class TimeTracker : IDisposable
private const float _thickness = 2.0f;
private const float _timeHeight = 10.0f;
private float _timeBarHeight => _timeHeight * 2.0f;
private readonly Vector2 _timeStep = new (50, 25);
private readonly Vector2 _buttonSize = new (14.0f);
private readonly string[] _icons = { "tl_forward", "tl_pause", "tl_rewind" };
public void ImGuiTimeline(Dictionary<string, Texture> icons, List<Animation> animations, Vector2 outliner, int activeAnimation, ImFontPtr fontPtr)
public void ImGuiTimeline(Dictionary<string, Texture> icons, List<Animation> animations, Vector2 outliner, ref int activeAnimation, ImFontPtr fontPtr)
{
var treeP0 = ImGui.GetCursorScreenPos();
var canvasSize = ImGui.GetContentRegionAvail();
var timelineP1 = new Vector2(treeP0.X + canvasSize.X, treeP0.Y + canvasSize.Y);
var canvasMaxY = MathF.Max(canvasSize.Y, _timeBarHeight + _timeStep.Y * animations.Count);
ImGui.BeginChild("timeline_child", canvasSize with { Y = canvasMaxY });
var timelineP1 = new Vector2(treeP0.X + canvasSize.X, treeP0.Y + canvasMaxY);
var treeP1 = timelineP1 with { X = treeP0.X + outliner.X };
var timelineP0 = treeP0 with { X = treeP1.X + _thickness };
var timelineSize = timelineP1 - timelineP0;
var timeRatio = timelineSize / MaxElapsedTime;
var timeStep = timeRatio * MaxElapsedTime / timelineSize * 50.0f;
timeStep.Y /= 2.0f;
var drawList = ImGui.GetWindowDrawList();
drawList.PushClipRect(treeP0, timelineP1, true);
drawList.AddRectFilled(treeP0, treeP1, 0xFF1F1C1C);
drawList.AddRectFilled(timelineP0, timelineP1 with { Y = timelineP0.Y + _timeBarHeight }, 0xFF141414);
drawList.AddRectFilled(timelineP0 with { Y = timelineP0.Y + _timeBarHeight }, timelineP1, 0xFF242424);
@ -97,14 +99,14 @@ public class TimeTracker : IDisposable
switch (i)
{
case 0:
SafeSetElapsedTime(ElapsedTime + timeStep.X / timeRatio.X);
SafeSetElapsedTime(ElapsedTime + _timeStep.X / timeRatio.X);
break;
case 1:
IsPaused = !IsPaused;
_icons[1] = IsPaused ? "tl_play" : "tl_pause";
break;
case 2:
SafeSetElapsedTime(ElapsedTime - timeStep.X / timeRatio.X);
SafeSetElapsedTime(ElapsedTime - _timeStep.X / timeRatio.X);
break;
}
}
@ -124,7 +126,7 @@ public class TimeTracker : IDisposable
}
{ // draw time + time grid
for (float x = 0; x < timelineSize.X; x += timeStep.X)
for (float x = 0; x < timelineSize.X; x += _timeStep.X)
{
var cursor = timelineP0.X + x;
drawList.AddLine(new Vector2(cursor, timelineP0.Y + _timeHeight + 2.5f), new Vector2(cursor, timelineP0.Y + _timeBarHeight), 0xA0FFFFFF);
@ -132,7 +134,7 @@ public class TimeTracker : IDisposable
drawList.AddText(fontPtr, 14, new Vector2(cursor + 4, timelineP0.Y + 7.5f), 0x50FFFFFF, $"{x / timeRatio.X:F1}s");
}
for (float y = _timeBarHeight; y < timelineSize.Y; y += timeStep.Y)
for (float y = _timeBarHeight; y < timelineSize.Y; y += _timeStep.Y)
{
drawList.AddLine(timelineP0 with { Y = timelineP0.Y + y }, timelineP1 with { Y = timelineP0.Y + y }, 0x28C8C8C8);
}
@ -140,12 +142,20 @@ public class TimeTracker : IDisposable
for (int i = 0; i < animations.Count; i++)
{
var y = timelineP0.Y + _timeBarHeight + timeStep.Y * i;
animations[i].ImGuiAnimation(drawList, timelineP0, treeP0, treeP1, timeStep, timeRatio, y, _thickness);
DrawSeparator(drawList, timelineP0, y + timeStep.Y, animations[i].EndTime * timeRatio.X, ETrackerType.End);
var y = timelineP0.Y + _timeBarHeight + _timeStep.Y * i;
animations[i].ImGuiAnimation(drawList, timelineP0, treeP0, treeP1, _timeStep, timeRatio, y, _thickness);
animations[i].IsSelected = activeAnimation == i;
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
activeAnimation = i;
DrawSeparator(drawList, timelineP0, y + _timeStep.Y, animations[i].EndTime * timeRatio.X, ETrackerType.End);
}
DrawSeparator(drawList, timelineP0, timelineP1.Y, ElapsedTime * timeRatio.X, ETrackerType.Frame);
drawList.PopClipRect();
ImGui.EndChild();
}
private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, float y, float time, ETrackerType separatorType)

View File

@ -1,13 +1,12 @@
using CUE4Parse_Conversion.Meshes.PSK;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Shading;
namespace FModel.Views.Snooper.Models;
public class Cube : Model
{
public Cube(CStaticMesh mesh, FGuid guid, UMaterialInterface unrealMaterial) : base(unrealMaterial, guid)
public Cube(CStaticMesh mesh, UMaterialInterface unrealMaterial) : base(unrealMaterial)
{
var lod = mesh.LODs[0];

View File

@ -13,7 +13,6 @@ using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Extensions;
using FModel.Settings;
using FModel.Views.Snooper.Animations;
@ -70,7 +69,6 @@ public class Model : IDisposable
private const int _faceSize = 3;
private readonly UObject _export;
public readonly FGuid Guid;
public readonly string Path;
public readonly string Name;
public readonly string Type;
@ -110,10 +108,9 @@ public class Model : IDisposable
public int SelectedInstance;
public float MorphTime;
protected Model(UObject export, FGuid guid)
protected Model(UObject export)
{
_export = export;
Guid = guid;
Path = _export.GetPathName();
Name = Path.SubstringAfterLast('/').SubstringBefore('.');
Type = export.ExportType;
@ -124,8 +121,8 @@ public class Model : IDisposable
Transforms = new List<Transform>();
}
public Model(UStaticMesh export, FGuid guid, CStaticMesh staticMesh) : this(export, guid, staticMesh, Transform.Identity) {}
public Model(UStaticMesh export, FGuid guid, CStaticMesh staticMesh, Transform transform) : this(export, guid, export.Materials, staticMesh.LODs, transform)
public Model(UStaticMesh export, CStaticMesh staticMesh) : this(export, staticMesh, Transform.Identity) {}
public Model(UStaticMesh export, CStaticMesh staticMesh, Transform transform) : this(export, export.Materials, staticMesh.LODs, transform)
{
Box = staticMesh.BoundingBox * Constants.SCALE_DOWN_RATIO;
@ -136,8 +133,8 @@ public class Model : IDisposable
}
}
public Model(USkeletalMesh export, FGuid guid, CSkeletalMesh skeletalMesh) : this(export, guid, skeletalMesh, Transform.Identity) {}
public Model(USkeletalMesh export, FGuid guid, CSkeletalMesh skeletalMesh, Transform transform) : this(export, guid, export.Materials, skeletalMesh.LODs, transform)
public Model(USkeletalMesh export, CSkeletalMesh skeletalMesh) : this(export, skeletalMesh, Transform.Identity) {}
public Model(USkeletalMesh export, CSkeletalMesh skeletalMesh, Transform transform) : this(export, export.Materials, skeletalMesh.LODs, transform)
{
Box = skeletalMesh.BoundingBox * Constants.SCALE_DOWN_RATIO;
Skeleton = new Skeleton(export.ReferenceSkeleton);
@ -163,11 +160,11 @@ public class Model : IDisposable
}
}
private Model(UObject export, FGuid guid, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CStaticMeshLod> lods, Transform transform = null)
: this(export, guid, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
private Model(UObject export, FGuid guid, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CSkelMeshLod> lods, Transform transform = null)
: this(export, guid, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
private Model(UObject export, FGuid guid, IReadOnlyList<ResolvedObject> materials, CBaseMeshLod lod, IReadOnlyList<CMeshVertex> vertices, int numLods, Transform transform = null) : this(export, guid)
private Model(UObject export, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CStaticMeshLod> lods, Transform transform = null)
: this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
private Model(UObject export, IReadOnlyList<ResolvedObject> materials, IReadOnlyList<CSkelMeshLod> lods, Transform transform = null)
: this(export, materials, lods[_LOD_INDEX], lods[_LOD_INDEX].Verts, lods.Count, transform) {}
private Model(UObject export, IReadOnlyList<ResolvedObject> materials, CBaseMeshLod lod, IReadOnlyList<CMeshVertex> vertices, int numLods, Transform transform = null) : this(export)
{
var hasCustomUvs = lod.ExtraUV.IsValueCreated;
UvCount = hasCustomUvs ? Math.Max(lod.NumTexCoords, numLods) : lod.NumTexCoords;
@ -293,9 +290,9 @@ public class Model : IDisposable
_morphVbo.Unbind();
}
public void AttachModel(Model attachedTo, Socket socket)
public void AttachModel(Model attachedTo, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Add(new SocketAttachementInfo { Guid = Guid, Instance = SelectedInstance });
socket.AttachedModels.Add(info);
_attachedTo = $"'{socket.Name}' from '{attachedTo.Name}'{(!socket.BoneName.IsNone ? $" at '{socket.BoneName}'" : "")}";
attachedTo._attachedFor.Add($"'{Name}'");
@ -305,9 +302,9 @@ public class Model : IDisposable
Transforms[SelectedInstance].Scale = FVector.OneVector;
}
public void DetachModel(Model attachedTo, Socket socket)
public void DetachModel(Model attachedTo, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Remove(new SocketAttachementInfo { Guid = Guid, Instance = SelectedInstance });
socket.AttachedModels.Remove(info);
SafeDetachModel(attachedTo);
}

View File

@ -17,7 +17,7 @@ public class Options
public FGuid SelectedModel { get; private set; }
public int SelectedSection { get; private set; }
public int SelectedMorph { get; private set; }
public int SelectedAnimation { get; private set; }
public int SelectedAnimation;
public readonly Dictionary<FGuid, Model> Models;
public readonly Dictionary<FGuid, Texture> Textures;

View File

@ -92,15 +92,11 @@ public class Renderer : IDisposable
Options.SwapMaterial(false);
}
public void Animate(UObject anim)
public void Animate(UObject anim) => Animate(anim, Options.SelectedModel);
private void Animate(UObject anim, FGuid guid)
{
if (!Options.TryGetModel(out var model))
if (!Options.TryGetModel(guid, out var model) || !model.HasSkeleton)
return;
Animate(anim, model);
}
private void Animate(UObject anim, Model model)
{
if (!model.HasSkeleton) return;
float maxElapsedTime;
switch (anim)
@ -108,7 +104,7 @@ public class Renderer : IDisposable
case UAnimSequence animSequence when animSequence.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animSequence);
var animation = new Animation(animSequence.Name, animSet, model.Guid);
var animation = new Animation(animSequence.Name, animSet, guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.AddAnimation(animation);
@ -117,7 +113,7 @@ public class Renderer : IDisposable
case UAnimMontage animMontage when animMontage.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animMontage);
var animation = new Animation(animMontage.Name, animSet, model.Guid);
var animation = new Animation(animMontage.Name, animSet, guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.AddAnimation(animation);
@ -136,7 +132,6 @@ public class Renderer : IDisposable
t.Scale = offset.Scale3D;
}
FGuid guid;
switch (export)
{
case UStaticMesh st:
@ -145,14 +140,14 @@ public class Renderer : IDisposable
if (Options.TryGetModel(guid, out var instancedModel))
instancedModel.AddInstance(t);
else if (st.TryConvert(out var mesh))
Options.Models[guid] = new Model(st, guid, mesh, t);
Options.Models[guid] = new Model(st, mesh, t);
break;
}
case USkeletalMesh sk:
{
guid = new FGuid((uint) sk.GetFullName().GetHashCode());
guid = Guid.NewGuid();
if (!Options.Models.ContainsKey(guid) && sk.TryConvert(out var mesh))
Options.Models[guid] = new Model(sk, guid, mesh, t);
Options.Models[guid] = new Model(sk, mesh, t);
break;
}
default:
@ -164,7 +159,7 @@ public class Renderer : IDisposable
addedModel.IsAnimatedProp = true;
if (notifyClass.TryGetValue(out UObject skeletalMeshPropAnimation, "SkeletalMeshPropAnimation", "Animation"))
Animate(skeletalMeshPropAnimation, addedModel);
Animate(skeletalMeshPropAnimation, guid);
if (notifyClass.TryGetValue(out FName socketName, "SocketName"))
{
t = Transform.Identity;
@ -177,7 +172,7 @@ public class Renderer : IDisposable
var s = new Socket($"TL_{addedModel.Name}", socketName, t, true);
model.Sockets.Add(s);
addedModel.AttachModel(model, s);
addedModel.AttachModel(model, s, new SocketAttachementInfo { Guid = guid, Instance = addedModel.SelectedInstance });
}
}
break;
@ -185,7 +180,7 @@ public class Renderer : IDisposable
case UAnimComposite animComposite when animComposite.Skeleton.TryLoad(out USkeleton skeleton):
{
var animSet = skeleton.ConvertAnims(animComposite);
var animation = new Animation(animComposite.Name, animSet, model.Guid);
var animation = new Animation(animComposite.Name, animSet, guid);
maxElapsedTime = animation.TotalElapsedTime;
model.Skeleton.Animate(animSet, AnimateWithRotationOnly);
Options.AddAnimation(animation);
@ -280,7 +275,7 @@ public class Renderer : IDisposable
if (!original.TryConvert(out var mesh))
return;
Options.Models[guid] = new Model(original, guid, mesh);
Options.Models[guid] = new Model(original, mesh);
Options.SelectModel(guid);
SetupCamera(Options.Models[guid].Box);
}
@ -290,7 +285,7 @@ public class Renderer : IDisposable
var guid = new FGuid((uint) original.GetFullName().GetHashCode());
if (Options.Models.ContainsKey(guid) || !original.TryConvert(out var mesh)) return;
Options.Models[guid] = new Model(original, guid, mesh);
Options.Models[guid] = new Model(original, mesh);
Options.SelectModel(guid);
SetupCamera(Options.Models[guid].Box);
}
@ -311,7 +306,7 @@ public class Renderer : IDisposable
if (!editorCube.TryConvert(out var mesh))
return;
Options.Models[guid] = new Cube(mesh, guid, original);
Options.Models[guid] = new Cube(mesh, original);
Options.SelectModel(guid);
SetupCamera(Options.Models[guid].Box);
}
@ -403,7 +398,7 @@ public class Renderer : IDisposable
}
else if (m.TryConvert(out var mesh))
{
model = new Model(m, guid, mesh, t);
model = new Model(m, mesh, t);
model.TwoSided = actor.GetOrDefault("bMirrored", staticMeshComp.GetOrDefault("bDisallowMeshPaintPerInstance", model.TwoSided));
if (actor.TryGetValue(out FPackageIndex baseMaterial, "BaseMaterial") &&

View File

@ -358,7 +358,7 @@ public class Material : IDisposable
var canvasSize = ImGui.GetContentRegionAvail();
if (canvasSize.X < 50.0f) canvasSize.X = 50.0f;
if (canvasSize.Y < 50.0f) canvasSize.Y = 50.0f;
var canvasP1 = new Vector2(canvasP0.X + canvasSize.X, canvasP0.Y + canvasSize.Y);
var canvasP1 = canvasP0 + canvasSize;
var origin = new Vector2(canvasP0.X + _scrolling.X, canvasP0.Y + _scrolling.Y);
var absoluteMiddle = canvasSize / 2.0f;

View File

@ -74,7 +74,7 @@ public class SnimGui
SectionWindow("Material Inspector", s.Renderer, DrawMaterialInspector, false);
AnimationWindow("Timeline", s.Renderer, (icons, tracker, animations) =>
tracker.ImGuiTimeline(icons, animations, _outlinerSize, s.Renderer.Options.SelectedAnimation, Controller.FontSemiBold));
tracker.ImGuiTimeline(icons, animations, _outlinerSize, ref s.Renderer.Options.SelectedAnimation, Controller.FontSemiBold));
Window("World", () => DrawWorld(s), false);
@ -412,6 +412,7 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
{
MeshWindow("Sockets", s.Renderer, (icons, selectedModel) =>
{
var info = new SocketAttachementInfo { Guid = s.Renderer.Options.SelectedModel, Instance = selectedModel.SelectedInstance };
foreach (var model in s.Renderer.Options.Models.Values)
{
if (!model.HasSockets || model.IsSelected) continue;
@ -420,16 +421,16 @@ Snooper aims to give an accurate preview of models, materials, skeletal animatio
var i = 0;
foreach (var socket in model.Sockets)
{
var isAttached = socket.AttachedModels.Contains(new SocketAttachementInfo { Guid = selectedModel.Guid, Instance = selectedModel.SelectedInstance });
var isAttached = socket.AttachedModels.Contains(info);
ImGui.PushID(i);
ImGui.BeginDisabled(selectedModel.IsAttached && !isAttached);
switch (isAttached)
{
case false when ImGui.Button($"Attach to '{socket.Name}'"):
selectedModel.AttachModel(model, socket);
selectedModel.AttachModel(model, socket, info);
break;
case true when ImGui.Button($"Detach from '{socket.Name}'"):
selectedModel.DetachModel(model, socket);
selectedModel.DetachModel(model, socket, info);
break;
}
ImGui.EndDisabled();