poc snooper running

This commit is contained in:
Asval
2026-08-15 12:48:52 +02:00
parent 773bbd9dc3
commit c21d6d6369
52 changed files with 228 additions and 8183 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "CUE4Parse"]
path = CUE4Parse
url = https://github.com/FabianFG/CUE4Parse
[submodule "Snooper"]
path = Snooper
url = https://github.com/FModel/Snooper

5
Directory.Build.props Normal file
View File

@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<CUE4ParseDir>$(MSBuildThisFileDirectory)CUE4Parse\</CUE4ParseDir>
</PropertyGroup>
</Project>

View File

@@ -7,11 +7,10 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
using CUE4Parse;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.Views.Snooper;
using FModel.ViewModels;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
@@ -134,7 +133,7 @@ public partial class App
.Filter.ByIncludingOnly(IsConversionLibrary)
.WriteTo.Console(outputTemplate: template2, theme: AnsiConsoleTheme.Literate)
.WriteTo.File(outputTemplate: template2, path: filePath, shared: true))
.MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance)
// .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance)
.CreateLogger();
CacheManager.MigrateLegacyFiles();
@@ -150,6 +149,7 @@ public partial class App
private void AppExit(object sender, ExitEventArgs e)
{
SnooperViewModel.Instance.Dispose();
Log.Information("––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––");
Log.CloseAndFlush();
UserSettings.Save();

View File

@@ -169,19 +169,18 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NVorbis" Version="0.10.5" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="RestSharp" Version="114.0.0" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="[2.88.9]" />
<PackageReference Include="Svg.Skia" Version="[4.8.0]" /> <!--because of skia 3.0 in newer versions-->
<PackageReference Include="Twizzle.ImGui-Bundle.NET" Version="1.91.5.2" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj" />
<ProjectReference Include="..\CUE4Parse\CUE4Parse\CUE4Parse.csproj" />
<ProjectReference Include="..\Snooper\Editor\Editor.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,7 @@
<Solution>
<Project Path="../CUE4Parse/CUE4Parse-Conversion/CUE4Parse-Conversion.csproj" />
<Project Path="../CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
<Project Path="../Snooper/Editor/Editor.csproj" />
<Project Path="../Snooper/Snooper/Snooper.csproj" />
<Project Path="FModel.csproj" />
</Solution>

View File

@@ -1,672 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using FModel.Settings;
using ImGuiNET;
using ImGuizmoNET;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
using ErrorCode = OpenTK.Graphics.OpenGL4.ErrorCode;
using Keys = OpenTK.Windowing.GraphicsLibraryFramework.Keys;
namespace FModel.Framework;
public class ImGuiController : IDisposable
{
private bool _frameBegun;
private int _vertexArray;
private int _vertexBuffer;
private int _vertexBufferSize;
private int _indexBuffer;
private int _indexBufferSize;
//private Texture _fontTexture;
private int _fontTexture;
private int _shader;
private int _shaderFontTextureLocation;
private int _shaderProjectionMatrixLocation;
private int _windowWidth;
private int _windowHeight;
public ImFontPtr FontNormal;
public ImFontPtr FontBold;
public ImFontPtr FontSemiBold;
private readonly Vector2 _scaleFactor = Vector2.One;
public readonly float DpiScale = GetDpiScale();
private static bool KHRDebugAvailable = false;
public ImGuiController(int width, int height)
{
_windowWidth = width;
_windowHeight = height;
int major = GL.GetInteger(GetPName.MajorVersion);
int minor = GL.GetInteger(GetPName.MinorVersion);
KHRDebugAvailable = (major == 4 && minor >= 3) || IsExtensionSupported("KHR_debug");
IntPtr context = ImGui.CreateContext();
ImGui.SetCurrentContext(context);
ImGuizmo.SetImGuiContext(context);
var io = ImGui.GetIO();
unsafe
{
var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"));
io.NativePtr->IniFilename = (byte*)iniFileNamePtr;
}
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 E000–F8FF (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
io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines;
io.ConfigDockingWithShift = true;
io.ConfigWindowsMoveFromTitleBarOnly = true;
io.BackendRendererUserData = 0;
CreateDeviceResources();
}
public void Normal() => PushFont(FontNormal);
public void Bold() => PushFont(FontBold);
public void SemiBold() => PushFont(FontSemiBold);
private void PushFont(ImFontPtr ptr) => ImGui.PushFont(ptr);
public void WindowResized(int width, int height)
{
_windowWidth = width;
_windowHeight = height;
}
public void DestroyDeviceObjects()
{
Dispose();
}
public void CreateDeviceResources()
{
_vertexBufferSize = 10000;
_indexBufferSize = 2000;
int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding);
int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding);
_vertexArray = GL.GenVertexArray();
GL.BindVertexArray(_vertexArray);
LabelObject(ObjectLabelIdentifier.VertexArray, _vertexArray, "ImGui");
_vertexBuffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer);
LabelObject(ObjectLabelIdentifier.Buffer, _vertexBuffer, "VBO: ImGui");
GL.BufferData(BufferTarget.ArrayBuffer, _vertexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
_indexBuffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ElementArrayBuffer, _indexBuffer);
LabelObject(ObjectLabelIdentifier.Buffer, _indexBuffer, "EBO: ImGui");
GL.BufferData(BufferTarget.ElementArrayBuffer, _indexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
RecreateFontDeviceTexture();
string VertexSource = @"#version 330 core
uniform mat4 projection_matrix;
layout(location = 0) in vec2 in_position;
layout(location = 1) in vec2 in_texCoord;
layout(location = 2) in vec4 in_color;
out vec4 color;
out vec2 texCoord;
void main()
{
gl_Position = projection_matrix * vec4(in_position, 0, 1);
color = in_color;
texCoord = in_texCoord;
}";
string FragmentSource = @"#version 330 core
uniform sampler2D in_fontTexture;
in vec4 color;
in vec2 texCoord;
out vec4 outputColor;
void main()
{
outputColor = color * texture(in_fontTexture, texCoord);
}";
_shader = CreateProgram("ImGui", VertexSource, FragmentSource);
_shaderProjectionMatrixLocation = GL.GetUniformLocation(_shader, "projection_matrix");
_shaderFontTextureLocation = GL.GetUniformLocation(_shader, "in_fontTexture");
int stride = Marshal.SizeOf<ImDrawVert>();
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, 0);
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, 8);
GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, stride, 16);
GL.EnableVertexAttribArray(0);
GL.EnableVertexAttribArray(1);
GL.EnableVertexAttribArray(2);
GL.BindVertexArray(prevVAO);
GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer);
CheckGLError("End of ImGui setup");
}
/// <summary>
/// Recreates the device texture used to render text.
/// </summary>
public void RecreateFontDeviceTexture()
{
ImGuiIOPtr io = ImGui.GetIO();
io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height, out int bytesPerPixel);
int mips = (int)Math.Floor(Math.Log(Math.Max(width, height), 2));
int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture);
GL.ActiveTexture(TextureUnit.Texture0);
int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
_fontTexture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, _fontTexture);
GL.TexStorage2D(TextureTarget2d.Texture2D, mips, SizedInternalFormat.Rgba8, width, height);
LabelObject(ObjectLabelIdentifier.Texture, _fontTexture, "ImGui Text Atlas");
GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, width, height, PixelFormat.Bgra, PixelType.UnsignedByte, pixels);
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, mips - 1);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
// Restore state
GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
GL.ActiveTexture((TextureUnit)prevActiveTexture);
io.Fonts.SetTexID((IntPtr)_fontTexture);
io.Fonts.ClearTexData();
}
/// <summary>
/// Renders the ImGui draw list data.
/// </summary>
public void Render()
{
if (_frameBegun)
{
_frameBegun = false;
ImGui.Render();
RenderImDrawData(ImGui.GetDrawData());
}
}
/// <summary>
/// Updates ImGui input and IO configuration state.
/// </summary>
public void Update(GameWindow wnd, float deltaSeconds)
{
if (_frameBegun)
{
ImGui.Render();
}
SetPerFrameImGuiData(deltaSeconds);
UpdateImGuiInput(wnd);
_frameBegun = true;
ImGui.NewFrame();
ImGuizmo.BeginFrame();
}
/// <summary>
/// Sets per-frame data based on the associated window.
/// This is called by Update(float).
/// </summary>
private void SetPerFrameImGuiData(float deltaSeconds)
{
ImGuiIOPtr io = ImGui.GetIO();
io.DisplaySize = new Vector2(
_windowWidth / _scaleFactor.X,
_windowHeight / _scaleFactor.Y);
io.DisplayFramebufferScale = _scaleFactor;
io.DeltaTime = deltaSeconds; // DeltaTime is in seconds.
}
readonly List<char> PressedChars = new List<char>();
private void UpdateImGuiInput(GameWindow wnd)
{
ImGuiIOPtr io = ImGui.GetIO();
var mState = wnd.MouseState;
var kState = wnd.KeyboardState;
io.AddMousePosEvent(mState.X, mState.Y);
io.AddMouseButtonEvent(0, mState[MouseButton.Left]);
io.AddMouseButtonEvent(1, mState[MouseButton.Right]);
io.AddMouseButtonEvent(2, mState[MouseButton.Middle]);
io.AddMouseButtonEvent(3, mState[MouseButton.Button1]);
io.AddMouseButtonEvent(4, mState[MouseButton.Button2]);
io.AddMouseWheelEvent(mState.ScrollDelta.X, mState.ScrollDelta.Y);
foreach (Keys key in Enum.GetValues<Keys>())
{
if (key == Keys.Unknown) continue;
io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key));
}
foreach (var c in PressedChars)
{
io.AddInputCharacter(c);
}
PressedChars.Clear();
io.KeyShift = kState.IsKeyDown(Keys.LeftShift) || kState.IsKeyDown(Keys.RightShift);
io.KeyCtrl = kState.IsKeyDown(Keys.LeftControl) || kState.IsKeyDown(Keys.RightControl);
io.KeyAlt = kState.IsKeyDown(Keys.LeftAlt) || kState.IsKeyDown(Keys.RightAlt);
io.KeySuper = kState.IsKeyDown(Keys.LeftSuper) || kState.IsKeyDown(Keys.RightSuper);
}
internal void PressChar(char keyChar)
{
PressedChars.Add(keyChar);
}
private void RenderImDrawData(ImDrawDataPtr draw_data)
{
if (draw_data.CmdListsCount == 0)
{
return;
}
// Get intial state.
int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding);
int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding);
int prevProgram = GL.GetInteger(GetPName.CurrentProgram);
bool prevBlendEnabled = GL.GetBoolean(GetPName.Blend);
bool prevScissorTestEnabled = GL.GetBoolean(GetPName.ScissorTest);
int prevBlendEquationRgb = GL.GetInteger(GetPName.BlendEquationRgb);
int prevBlendEquationAlpha = GL.GetInteger(GetPName.BlendEquationAlpha);
int prevBlendFuncSrcRgb = GL.GetInteger(GetPName.BlendSrcRgb);
int prevBlendFuncSrcAlpha = GL.GetInteger(GetPName.BlendSrcAlpha);
int prevBlendFuncDstRgb = GL.GetInteger(GetPName.BlendDstRgb);
int prevBlendFuncDstAlpha = GL.GetInteger(GetPName.BlendDstAlpha);
bool prevCullFaceEnabled = GL.GetBoolean(GetPName.CullFace);
bool prevDepthTestEnabled = GL.GetBoolean(GetPName.DepthTest);
int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture);
GL.ActiveTexture(TextureUnit.Texture0);
int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
Span<int> prevScissorBox = stackalloc int[4];
unsafe
{
fixed (int* iptr = &prevScissorBox[0])
{
GL.GetInteger(GetPName.ScissorBox, iptr);
}
}
// Bind the element buffer (thru the VAO) so that we can resize it.
GL.BindVertexArray(_vertexArray);
// Bind the vertex buffer so that we can resize it.
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer);
for (int i = 0; i < draw_data.CmdListsCount; i++)
{
ImDrawListPtr cmd_list = draw_data.CmdLists[i];
int vertexSize = cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>();
if (vertexSize > _vertexBufferSize)
{
int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize);
GL.BufferData(BufferTarget.ArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
_vertexBufferSize = newSize;
}
int indexSize = cmd_list.IdxBuffer.Size * sizeof(ushort);
if (indexSize > _indexBufferSize)
{
int newSize = (int)Math.Max(_indexBufferSize * 1.5f, indexSize);
GL.BufferData(BufferTarget.ElementArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
_indexBufferSize = newSize;
}
}
// Setup orthographic projection matrix into our constant buffer
ImGuiIOPtr io = ImGui.GetIO();
var mvp = OpenTK.Mathematics.Matrix4.CreateOrthographicOffCenter(
0.0f,
io.DisplaySize.X,
io.DisplaySize.Y,
0.0f,
-1.0f,
1.0f);
GL.UseProgram(_shader);
GL.UniformMatrix4(_shaderProjectionMatrixLocation, false, ref mvp);
GL.Uniform1(_shaderFontTextureLocation, 0);
CheckGLError("Projection");
GL.BindVertexArray(_vertexArray);
CheckGLError("VAO");
draw_data.ScaleClipRects(io.DisplayFramebufferScale);
GL.Enable(EnableCap.Blend);
GL.Enable(EnableCap.ScissorTest);
GL.BlendEquation(BlendEquationMode.FuncAdd);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Disable(EnableCap.CullFace);
GL.Disable(EnableCap.DepthTest);
// Render command lists
for (int n = 0; n < draw_data.CmdListsCount; n++)
{
ImDrawListPtr cmd_list = draw_data.CmdLists[n];
GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>(), cmd_list.VtxBuffer.Data);
CheckGLError($"Data Vert {n}");
GL.BufferSubData(BufferTarget.ElementArrayBuffer, IntPtr.Zero, cmd_list.IdxBuffer.Size * sizeof(ushort), cmd_list.IdxBuffer.Data);
CheckGLError($"Data Idx {n}");
for (int cmd_i = 0; cmd_i < cmd_list.CmdBuffer.Size; cmd_i++)
{
ImDrawCmdPtr pcmd = cmd_list.CmdBuffer[cmd_i];
if (pcmd.UserCallback != IntPtr.Zero)
{
throw new NotImplementedException();
}
else
{
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, (int)pcmd.TextureId);
CheckGLError("Texture");
// We do _windowHeight - (int)clip.W instead of (int)clip.Y because gl has flipped Y when it comes to these coordinates
var clip = pcmd.ClipRect;
GL.Scissor((int)clip.X, _windowHeight - (int)clip.W, (int)(clip.Z - clip.X), (int)(clip.W - clip.Y));
CheckGLError("Scissor");
if ((io.BackendFlags & ImGuiBackendFlags.RendererHasVtxOffset) != 0)
{
GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (IntPtr)(pcmd.IdxOffset * sizeof(ushort)), unchecked((int)pcmd.VtxOffset));
}
else
{
GL.DrawElements(BeginMode.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (int)pcmd.IdxOffset * sizeof(ushort));
}
CheckGLError("Draw");
}
}
}
GL.Disable(EnableCap.Blend);
GL.Disable(EnableCap.ScissorTest);
// Reset state
GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
GL.ActiveTexture((TextureUnit)prevActiveTexture);
GL.UseProgram(prevProgram);
GL.BindVertexArray(prevVAO);
GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]);
GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer);
GL.BlendEquationSeparate((BlendEquationMode)prevBlendEquationRgb, (BlendEquationMode)prevBlendEquationAlpha);
GL.BlendFuncSeparate(
(BlendingFactorSrc)prevBlendFuncSrcRgb,
(BlendingFactorDest)prevBlendFuncDstRgb,
(BlendingFactorSrc)prevBlendFuncSrcAlpha,
(BlendingFactorDest)prevBlendFuncDstAlpha);
if (prevBlendEnabled) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend);
if (prevDepthTestEnabled) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest);
if (prevCullFaceEnabled) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace);
if (prevScissorTestEnabled) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest);
}
/// <summary>
/// Frees all graphics resources used by the renderer.
/// </summary>
public void Dispose()
{
GL.DeleteVertexArray(_vertexArray);
GL.DeleteBuffer(_vertexBuffer);
GL.DeleteBuffer(_indexBuffer);
GL.DeleteTexture(_fontTexture);
GL.DeleteProgram(_shader);
}
public static void LabelObject(ObjectLabelIdentifier objLabelIdent, int glObject, string name)
{
if (KHRDebugAvailable)
GL.ObjectLabel(objLabelIdent, glObject, name.Length, name);
}
static bool IsExtensionSupported(string name)
{
int n = GL.GetInteger(GetPName.NumExtensions);
for (int i = 0; i < n; i++)
{
string extension = GL.GetString(StringNameIndexed.Extensions, i);
if (extension == name) return true;
}
return false;
}
public static int CreateProgram(string name, string vertexSource, string fragmentSoruce)
{
int program = GL.CreateProgram();
LabelObject(ObjectLabelIdentifier.Program, program, $"Program: {name}");
int vertex = CompileShader(name, ShaderType.VertexShader, vertexSource);
int fragment = CompileShader(name, ShaderType.FragmentShader, fragmentSoruce);
GL.AttachShader(program, vertex);
GL.AttachShader(program, fragment);
GL.LinkProgram(program);
GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int success);
if (success == 0)
{
string info = GL.GetProgramInfoLog(program);
Debug.WriteLine($"GL.LinkProgram had info log [{name}]:\n{info}");
}
GL.DetachShader(program, vertex);
GL.DetachShader(program, fragment);
GL.DeleteShader(vertex);
GL.DeleteShader(fragment);
return program;
}
private static int CompileShader(string name, ShaderType type, string source)
{
int shader = GL.CreateShader(type);
LabelObject(ObjectLabelIdentifier.Shader, shader, $"Shader: {name}");
GL.ShaderSource(shader, source);
GL.CompileShader(shader);
GL.GetShader(shader, ShaderParameter.CompileStatus, out int success);
if (success == 0)
{
string info = GL.GetShaderInfoLog(shader);
Debug.WriteLine($"GL.CompileShader for shader '{name}' [{type}] had info log:\n{info}");
}
return shader;
}
public static void CheckGLError(string title)
{
ErrorCode error;
int i = 1;
while ((error = GL.GetError()) != ErrorCode.NoError)
{
Debug.Print($"{title} ({i++}): {error}");
}
}
public static float GetDpiScale()
{
return Math.Max((float)(Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth), (float)(Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight));
}
public static ImGuiKey TranslateKey(Keys key)
{
if (key >= Keys.D0 && key <= Keys.D9)
return key - Keys.D0 + ImGuiKey._0;
if (key >= Keys.A && key <= Keys.Z)
return key - Keys.A + ImGuiKey.A;
if (key >= Keys.KeyPad0 && key <= Keys.KeyPad9)
return key - Keys.KeyPad0 + ImGuiKey.Keypad0;
if (key >= Keys.F1 && key <= Keys.F24)
return key - Keys.F1 + ImGuiKey.F24;
return key switch
{
Keys.Tab => ImGuiKey.Tab,
Keys.Left => ImGuiKey.LeftArrow,
Keys.Right => ImGuiKey.RightArrow,
Keys.Up => ImGuiKey.UpArrow,
Keys.Down => ImGuiKey.DownArrow,
Keys.PageUp => ImGuiKey.PageUp,
Keys.PageDown => ImGuiKey.PageDown,
Keys.Home => ImGuiKey.Home,
Keys.End => ImGuiKey.End,
Keys.Insert => ImGuiKey.Insert,
Keys.Delete => ImGuiKey.Delete,
Keys.Backspace => ImGuiKey.Backspace,
Keys.Space => ImGuiKey.Space,
Keys.Enter => ImGuiKey.Enter,
Keys.Escape => ImGuiKey.Escape,
Keys.Apostrophe => ImGuiKey.Apostrophe,
Keys.Comma => ImGuiKey.Comma,
Keys.Minus => ImGuiKey.Minus,
Keys.Period => ImGuiKey.Period,
Keys.Slash => ImGuiKey.Slash,
Keys.Semicolon => ImGuiKey.Semicolon,
Keys.Equal => ImGuiKey.Equal,
Keys.LeftBracket => ImGuiKey.LeftBracket,
Keys.Backslash => ImGuiKey.Backslash,
Keys.RightBracket => ImGuiKey.RightBracket,
Keys.GraveAccent => ImGuiKey.GraveAccent,
Keys.CapsLock => ImGuiKey.CapsLock,
Keys.ScrollLock => ImGuiKey.ScrollLock,
Keys.NumLock => ImGuiKey.NumLock,
Keys.PrintScreen => ImGuiKey.PrintScreen,
Keys.Pause => ImGuiKey.Pause,
Keys.KeyPadDecimal => ImGuiKey.KeypadDecimal,
Keys.KeyPadDivide => ImGuiKey.KeypadDivide,
Keys.KeyPadMultiply => ImGuiKey.KeypadMultiply,
Keys.KeyPadSubtract => ImGuiKey.KeypadSubtract,
Keys.KeyPadAdd => ImGuiKey.KeypadAdd,
Keys.KeyPadEnter => ImGuiKey.KeypadEnter,
Keys.KeyPadEqual => ImGuiKey.KeypadEqual,
Keys.LeftShift => ImGuiKey.ModShift,
Keys.LeftControl => ImGuiKey.LeftCtrl,
Keys.LeftAlt => ImGuiKey.LeftAlt,
Keys.LeftSuper => ImGuiKey.LeftSuper,
Keys.RightShift => ImGuiKey.RightShift,
Keys.RightControl => ImGuiKey.RightCtrl,
Keys.RightAlt => ImGuiKey.RightAlt,
Keys.RightSuper => ImGuiKey.RightSuper,
Keys.Menu => ImGuiKey.Menu,
_ => ImGuiKey.None
};
}
}

View File

@@ -123,7 +123,7 @@ public partial class MainWindow
_applicationView.CUE4Parse.InitMappings(),
ApplicationViewModel.InitDetex(),
ApplicationViewModel.InitVgmStream(),
ApplicationViewModel.InitImGuiSettings(newOrUpdated),
// ApplicationViewModel.InitImGuiSettings(newOrUpdated),
Task.Run(() =>
{
if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always)

View File

@@ -12,7 +12,6 @@ using FModel.Extensions.Themes;
using FModel.Framework;
using FModel.ViewModels;
using FModel.ViewModels.ApiEndpoints.Models;
using FModel.Views.Snooper;
using Newtonsoft.Json;
namespace FModel.Settings;
@@ -564,13 +563,6 @@ public sealed class UserSettings : ViewModel
set => SetProperty(ref _animateWithRotationOnly, value);
}
private Camera.WorldMode _cameraMode = Camera.WorldMode.Arcball;
public Camera.WorldMode CameraMode
{
get => _cameraMode;
set => SetProperty(ref _cameraMode, value);
}
private int _previewMaxTextureSize = 1024;
public int PreviewMaxTextureSize
{

View File

@@ -9,7 +9,6 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AdonisUI.Controls;
using CUE4Parse;
using CUE4Parse.Compression;
@@ -78,11 +77,8 @@ using FModel.Services;
using FModel.Settings;
using FModel.Views;
using FModel.Views.Resources.Controls;
using FModel.Views.Snooper;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using Serilog;
using SkiaSharp;
using Svg.Skia;
@@ -116,39 +112,6 @@ public class CUE4ParseViewModel : ViewModel
set => SetProperty(ref _modelIsWaitingAnimation, value);
}
public bool IsSnooperOpen => _snooper is { Exists: true, IsVisible: true };
private Snooper _snooper;
public Snooper SnooperViewer
{
get
{
if (_snooper != null) return _snooper;
return Application.Current.Dispatcher.Invoke(delegate
{
var scale = ImGuiController.GetDpiScale();
var htz = Snooper.GetMaxRefreshFrequency();
return _snooper = new Snooper(
new GameWindowSettings { UpdateFrequency = htz },
new NativeWindowSettings
{
ClientSize = new OpenTK.Mathematics.Vector2i(
Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale),
Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale)),
NumberOfSamples = Constants.SAMPLES_COUNT,
WindowBorder = WindowBorder.Resizable,
Flags = ContextFlags.ForwardCompatible,
Profile = ContextProfile.Core,
Vsync = VSyncMode.Adaptive,
APIVersion = new Version(4, 6),
StartVisible = false,
StartFocused = false,
Title = "3D Viewer"
});
});
}
}
public AbstractVfsFileProvider Provider { get; }
public GameDirectoryViewModel GameDirectory { get; }
public AssetsFolderViewModel AssetsFolder { get; }
@@ -1569,21 +1532,22 @@ public class CUE4ParseViewModel : ViewModel
pkg.Name.Contains("/RenderSwitch_Materials/", StringComparison.OrdinalIgnoreCase) ||
pkg.Name.Contains("/MI_BPTile/", StringComparison.OrdinalIgnoreCase))):
{
if (SnooperViewer.TryLoadExport(cancellationToken, dummy, pointer.Object))
SnooperViewer.Run();
SnooperViewModel.Instance.Load(pointer.Object.Value);
// if (SnooperViewer.TryLoadExport(cancellationToken, dummy, pointer.Object))
// SnooperViewer.Run();
return true;
}
case UMaterialInstance when isNone && ModelIsOverwritingMaterial && pointer.Object.Value is UMaterialInstance m:
{
SnooperViewer.Renderer.Swap(m);
SnooperViewer.Run();
// SnooperViewer.Renderer.Swap(m);
// SnooperViewer.Run();
return true;
}
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);
SnooperViewer.Run();
// SnooperViewer.Renderer.Animate(pointer.Object.Value);
// SnooperViewer.Run();
return true;
}
case UStaticMesh when HasFlag(bulk, EBulkType.Meshes):

View File

@@ -38,7 +38,8 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
contextViewModel.CUE4Parse.TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(contextViewModel.CUE4Parse.GameDirectory.DirectoryFiles, Formatting.Indented), false, false);
break;
case "Views_3dViewer":
contextViewModel.CUE4Parse.SnooperViewer.Run();
SnooperViewModel.Instance.Load(null);
SnooperViewModel.Instance.Run();
break;
case "Views_ExportSession":
Helper.OpenWindow<AdonisWindow>("Export Session", () => new ExportSessionWindow().Show());

View File

@@ -18,7 +18,6 @@ using FModel.Framework;
using FModel.Settings;
using FModel.Views;
using FModel.Views.Resources.Controls;
using FModel.Views.Snooper;
using Serilog.Events;
namespace FModel.ViewModels;
@@ -145,7 +144,7 @@ public class ExportSessionViewModel : ViewModel
private ExportSessionViewModel()
{
ImGuiSink.Instance.OnExporterLogEvent += OnLogEvent;
// ImGuiSink.Instance.OnExporterLogEvent += OnLogEvent;
}
private void OnLogEvent(LogEvent log)

View File

@@ -0,0 +1,197 @@
using System;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Forms;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Objects.Engine;
using Editor;
using FModel.Framework;
using FModel.Services;
using OpenTK.Windowing.Desktop;
using Serilog;
using Snooper.Rendering.Actors;
using Snooper.Rendering.Components;
using Snooper.Rendering.Components.Light;
using Snooper.Rendering.Components.Primitive;
using Snooper.Rendering.Components.Transforms;
namespace FModel.ViewModels;
public class SnooperViewModel : ViewModel, IDisposable
{
public static SnooperViewModel Instance { get; } = new();
private readonly Lazy<EditorWindow> _editor;
private SnooperViewModel()
{
var scale = GetDpiScale();
var htz = GetMaxRefreshFrequency();
var width = Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale);
var height = Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale);
_editor = new Lazy<EditorWindow>(() => StartEditor(htz, width, height));
}
private static EditorWindow StartEditor(int htz, int width, int height)
{
GLFWProvider.CheckForMainThread = false;
var ready = new ManualResetEventSlim();
EditorWindow? editor = null;
Exception? failure = null;
new Thread(() =>
{
try
{
editor = new EditorWindow(htz, width, height, ApplicationService.ApplicationView.CUE4Parse.Provider, false, true);
}
catch (Exception e)
{
failure = e;
return;
}
finally
{
ready.Set();
}
try
{
editor.Run();
}
catch (Exception e)
{
Log.Error(e, "Snooper crashed");
}
}) { IsBackground = true, Name = "Snooper" }.Start();
ready.Wait();
return editor ?? throw new InvalidOperationException("Snooper failed to start", failure);
}
public void Load(UObject? obj)
{
var scene = new Actor("Example Scene");
scene.Components.Add(new BoxComponent(Vector3.Zero, Vector3.One));
var camera = new CameraActor("Camera");
camera.CameraComponent.LocalTransform.Position = new Vector3(1, 2, -0.5f);
camera.CameraComponent.LocalTransform.Rotation = new Quaternion(0, -1, 0, 1);
scene.Children.Add(camera);
var sun = new Actor("Sun Light");
sun.Components.Add(new DirectionalLightComponent(MathF.PI, new Vector3(1.0f, 0.87f, 0.72f), new Transform(new Quaternion(new Vector3(0.5f, -0.5f, 0.0f), 1.0f)), "Directional Light"));
scene.Children.Add(sun);
GridComponent gridComponent = new OpaqueGridComponent();
switch (obj)
{
case UStaticMesh sm:
scene.Children.Add(new MeshActor(sm));
break;
case USkeletalMesh sk:
scene.Children.Add(new MeshActor(sk));
break;
case UWorld w:
gridComponent = new GridComponent();
scene.Children.Add(new WorldActor(w));
break;
}
var grid = new Actor("Grid");
grid.Components.Add(gridComponent);
scene.Children.Insert(0, grid);
var editor = _editor.Value;
editor.Invoke(() =>
{
editor.Manager.LoadScene(scene);
editor.Show();
});
}
public void Run()
{
var editor = _editor.Value;
editor.Invoke(editor.Show);
}
[DllImport("user32.dll")]
private static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
[StructLayout(LayoutKind.Sequential)]
private struct DEVMODE
{
private const int CCHDEVICENAME = 0x20;
private const int CCHFORMNAME = 0x20;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public ScreenOrientation dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
private static float GetDpiScale()
{
if (Screen.PrimaryScreen is not { } primaryScreen)
return 1.0f;
return (float)Math.Max(
primaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth,
primaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight
);
}
private static int GetMaxRefreshFrequency()
{
var rf = 60;
var vDevMode = new DEVMODE();
var i = 0;
while (EnumDisplaySettings(null, i, ref vDevMode))
{
i++;
rf = Math.Max(rf, vDevMode.dmDisplayFrequency);
}
return rf;
}
public void Dispose()
{
if (!_editor.IsValueCreated) return;
_editor.Value.Dispose();
}
}

View File

@@ -49,9 +49,9 @@ public class ThreadWorkerViewModel : ViewModel
public async Task Begin(Action<CancellationToken> action)
{
if (_applicationView.CUE4Parse.IsSnooperOpen)
/*if (_applicationView.CUE4Parse.IsSnooperOpen)
_applicationView.CUE4Parse.SnooperViewer.Close();
else if (!_applicationView.Status.IsReady)
else */if (!_applicationView.Status.IsReady)
{
SignalOperationInProgress();
return;
@@ -88,8 +88,8 @@ public class ThreadWorkerViewModel : ViewModel
catch (OperationCanceledException)
{
_applicationView.Status.SetStatus(EStatusKind.Stopped);
if (_applicationView.CUE4Parse.IsSnooperOpen)
_applicationView.CUE4Parse.SnooperViewer.Close();
// if (_applicationView.CUE4Parse.IsSnooperOpen)
// _applicationView.CUE4Parse.SnooperViewer.Close();
CurrentCancellationTokenSource = null; // kill token
OperationCancelled = true;
OperationCancelled = false;

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Settings;
using FModel.Views.Snooper.Models;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
public class Animation : IExportableThing, IDisposable
{
private readonly UObject _export;
public readonly CAnimSet UnrealAnim;
public readonly string Path;
public readonly string Name;
public readonly Sequence[] Sequences;
public readonly float StartTime; // Animation Start Time
public readonly float EndTime; // Animation End Time
public readonly float TotalElapsedTime; // Animation Max Time
public readonly Dictionary<int, float> Framing;
public bool IsActive;
public bool IsSelected;
public readonly List<FGuid> AttachedModels;
public Animation(UObject export)
{
_export = export;
Path = _export.GetPathName();
Name = _export.Name;
Sequences = [];
Framing = new Dictionary<int, float>();
AttachedModels = [];
}
public Animation(UObject export, CAnimSet animSet) : this(export)
{
UnrealAnim = animSet;
Sequences = new Sequence[UnrealAnim.Sequences.Count];
for (int i = 0; i < Sequences.Length; i++)
{
Sequences[i] = new Sequence(UnrealAnim.Sequences[i]);
EndTime = Sequences[i].EndTime;
}
TotalElapsedTime = EndTime;
if (Sequences.Length > 0)
StartTime = Sequences[0].StartTime;
}
public Animation(UObject export, CAnimSet animSet, params FGuid[] animatedModels) : this(export, animSet)
{
AttachedModels.AddRange(animatedModels);
}
public void TimeCalculation(float elapsedTime)
{
for (int i = 0; i < Sequences.Length; i++)
{
var sequence = Sequences[i];
if (elapsedTime <= sequence.EndTime && elapsedTime >= sequence.StartTime)
{
Framing[i] = (elapsedTime - sequence.StartTime) * sequence.RateScale / sequence.SecondsPerFrame;
}
else Framing.Remove(i);
}
if (elapsedTime >= TotalElapsedTime)
Framing.Clear();
}
public void Dispose()
{
AttachedModels.Clear();
}
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);
var p2 = new Vector2(timelineP0.X + EndTime * timeRatio.X - t, y + timeStep.Y - t);
ImGui.SetCursorScreenPos(p1);
ImGui.InvisibleButton($"timeline_sequencetracker_{name}", new Vector2(EndTime * timeRatio.X - t, timeStep.Y - t), ImGuiButtonFlags.MouseButtonLeft);
IsActive = ImGui.IsItemActive();
IsSelected = s.Renderer.Options.SelectedAnimation == i;
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
s.Renderer.Options.SelectAnimation(i);
}
Popup(s, i);
drawList.AddRectFilled(p1, p2, IsSelected ? 0xFF48B048 : 0xFF175F17, 5.0f, ImDrawFlags.RoundCornersTop);
for (int j = 0; j < Sequences.Length; j++)
{
Sequences[j].DrawSequence(drawList, fontPtr, timelineP0.X, p2, timeStep, timeRatio, t, IsSelected);
}
ImGui.SetCursorScreenPos(treeP0 with { Y = p1.Y });
if (ImGui.Selectable(name, s.Renderer.Options.SelectedAnimation == i, ImGuiSelectableFlags.SpanAllColumns, new Vector2(p1.X - treeP0.X, timeStep.Y - t - t)))
{
s.Renderer.Options.SelectAnimation(i);
}
Popup(s, i);
}
private void Popup(Snooper s, int i)
{
SnimGui.Popup(() =>
{
s.Renderer.Options.SelectAnimation(i);
if (ImGui.BeginMenu("Animate"))
{
foreach ((var guid, var model) in s.Renderer.Options.Models)
{
var selected = AttachedModels.Contains(guid);
if (model is SkeletalModel skeletalModel && ImGui.MenuItem(model.Name, null, selected, !skeletalModel.Skeleton.IsAnimated || selected))
{
if (selected) AttachedModels.Remove(guid); else AttachedModels.Add(guid);
skeletalModel.Skeleton.ResetAnimatedData(true);
if (!selected) skeletalModel.Skeleton.Animate(UnrealAnim);
}
}
ImGui.EndMenu();
}
if (ImGui.MenuItem("Save"))
{
s.WindowShouldFreeze(true);
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,37 +0,0 @@
using System.Collections.Generic;
namespace FModel.Views.Snooper.Animations;
public class Bone
{
public readonly int Index;
public int ParentIndex;
public readonly Transform Rest;
public readonly bool IsVirtual;
public string LoweredParentName;
public List<string> LoweredChildNames;
public int SkeletonIndex = -1;
public readonly List<int> AnimatedBySequences;
public Bone(int i, int p, Transform t, bool isVirtual = false)
{
Index = i;
ParentIndex = p;
Rest = t;
IsVirtual = isVirtual;
LoweredChildNames = new List<string>();
AnimatedBySequences = new List<int>();
}
public bool IsRoot => Index == 0 && ParentIndex == -1 && string.IsNullOrEmpty(LoweredParentName);
public bool IsDaron => LoweredChildNames.Count > 0;
public bool IsMapped => SkeletonIndex > -1;
public bool IsAnimated => AnimatedBySequences.Count > 0;
public bool IsNative => Index == SkeletonIndex;
public uint Color => !IsMapped || !IsAnimated ? 0xFFA0A0A0 : 0xFF48B048;
public override string ToString() => $"Mesh Ref '{Index}' is Skel Ref '{SkeletonIndex}'";
}

View File

@@ -1,50 +0,0 @@
using System.Numerics;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
public class Sequence
{
public readonly string Name;
public readonly float RateScale;
public readonly float StartTime;
public readonly float Duration;
public readonly float EndTime;
public readonly int EndFrame;
public readonly int LoopingCount;
public readonly bool IsAdditive;
public readonly float SecondsPerFrame;
public Sequence(CAnimSequence sequence)
{
Name = sequence.Name;
RateScale = sequence.OriginalSequence.RateScale;
StartTime = sequence.StartPos;
Duration = sequence.AnimEndTime;
EndTime = StartTime + Duration / RateScale;
EndFrame = sequence.NumFrames;
LoopingCount = sequence.LoopingCount;
IsAdditive = sequence.IsAdditive;
SecondsPerFrame = Duration / EndFrame;
}
public void DrawSequence(ImDrawListPtr drawList, ImFontPtr fontPtr, float x, Vector2 p2, Vector2 timeStep, Vector2 timeRatio, float t, bool animSelected)
{
var halfThickness = t / 2.0f;
var q1 = new Vector2(x + StartTime * timeRatio.X + t + halfThickness, p2.Y - timeStep.Y / 2.0f);
var q2 = p2 with { X = x + EndTime * timeRatio.X - t * 2.0f };
drawList.PushClipRect(q1, q2 with { X = q2.X + t }, true);
var lineColor = animSelected ? 0xA0FFFFFF : 0x50FFFFFF;
drawList.AddLine(new Vector2(q1.X, q2.Y), q1, lineColor, 1.0f);
drawList.AddLine(q1, new Vector2(q2.X, q1.Y), lineColor, 1.0f);
drawList.AddLine(new Vector2(q2.X, q1.Y), q2, lineColor, 1.0f);
if (IsAdditive)
drawList.AddText(fontPtr, 12 * ImGui.GetWindowDpiScale(), new Vector2(q1.X + t, q1.Y + halfThickness), animSelected ? 0xFFFFFFFF : 0x50FFFFFF, "Is Additive");
drawList.PopClipRect();
}
}

View File

@@ -1,393 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CUE4Parse_Conversion.Writers.ActorX.Structs.Animations;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
using Serilog;
namespace FModel.Views.Snooper.Animations;
public class Skeleton : IDisposable
{
private int _handle;
private BufferObject<Matrix4x4> _rest;
private BufferObject<Matrix4x4> _ssbo;
private Matrix4x4[] _boneMatriceAtFrame;
private readonly List<string> _breadcrumb;
public string Name;
public FGuid Guid;
public readonly Dictionary<string, Bone> BonesByLoweredName;
public readonly int BoneCount;
private int _additionalBoneCount;
private int TotalBoneCount => BoneCount + _additionalBoneCount;
public bool IsAnimated { get; private set; }
public string SelectedBone;
private const int _vertexSize = 12;
private BufferObject<float> _vbo;
private int _vaoHandle;
public Skeleton()
{
BonesByLoweredName = new Dictionary<string, Bone>();
_breadcrumb = new List<string>();
}
public Skeleton(FReferenceSkeleton referenceSkeleton) : this()
{
BoneCount = referenceSkeleton.FinalRefBoneInfo.Length;
for (int boneIndex = 0; boneIndex < BoneCount; boneIndex++)
{
var info = referenceSkeleton.FinalRefBoneInfo[boneIndex];
var boneName = info.Name.Text.ToLower();
var bone = new Bone(boneIndex, info.ParentIndex, new Transform
{
Rotation = referenceSkeleton.FinalRefBonePose[boneIndex].Rotation,
Position = referenceSkeleton.FinalRefBonePose[boneIndex].Translation * Constants.SCALE_DOWN_RATIO,
Scale = referenceSkeleton.FinalRefBonePose[boneIndex].Scale3D
});
if (!bone.IsRoot)
{
bone.LoweredParentName = referenceSkeleton.FinalRefBoneInfo[bone.ParentIndex].Name.Text.ToLower();
var parentBone = BonesByLoweredName[bone.LoweredParentName];
bone.Rest.Relation = parentBone.Rest.Matrix;
parentBone.LoweredChildNames.Add(boneName);
}
else bone.Rest.Scale = FVector.OneVector;
if (boneIndex == 0) SelectedBone = boneName;
BonesByLoweredName[boneName] = bone;
}
_breadcrumb.Add(SelectedBone);
_boneMatriceAtFrame = new Matrix4x4[BoneCount];
}
public void Merge(FReferenceSkeleton referenceSkeleton)
{
for (int boneIndex = 0; boneIndex < referenceSkeleton.FinalRefBoneInfo.Length; boneIndex++)
{
var info = referenceSkeleton.FinalRefBoneInfo[boneIndex];
var boneName = info.Name.Text.ToLower();
if (!BonesByLoweredName.TryGetValue(boneName, out var bone))
{
bone = new Bone(BoneCount + _additionalBoneCount, -1, new Transform
{
Rotation = referenceSkeleton.FinalRefBonePose[boneIndex].Rotation,
Position = referenceSkeleton.FinalRefBonePose[boneIndex].Translation * Constants.SCALE_DOWN_RATIO,
Scale = referenceSkeleton.FinalRefBonePose[boneIndex].Scale3D
}, true);
if (!bone.IsRoot)
{
bone.LoweredParentName = referenceSkeleton.FinalRefBoneInfo[info.ParentIndex].Name.Text.ToLower();
var parentBone = BonesByLoweredName[bone.LoweredParentName];
bone.ParentIndex = parentBone.Index;
bone.Rest.Relation = parentBone.Rest.Matrix;
parentBone.LoweredChildNames.Add(boneName);
}
BonesByLoweredName[boneName] = bone;
_additionalBoneCount++;
}
}
_boneMatriceAtFrame = new Matrix4x4[TotalBoneCount];
}
public void Setup()
{
_handle = GL.CreateProgram();
_vaoHandle = GL.GenVertexArray();
GL.BindVertexArray(_vaoHandle);
_vbo = new BufferObject<float>(_vertexSize * BoneCount, BufferTarget.ArrayBuffer);
var sf = sizeof(float);
var half = _vertexSize / 2;
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, sf * half, sf * 0);
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, sf * half, sf * 3);
GL.BindVertexArray(0);
_rest = new BufferObject<Matrix4x4>(BoneCount, BufferTarget.ShaderStorageBuffer);
foreach (var bone in BonesByLoweredName.Values)
{
if (bone.IsVirtual) break;
_rest.Update(bone.Index, bone.Rest.Matrix);
}
_rest.Unbind();
_ssbo = new BufferObject<Matrix4x4>(TotalBoneCount, BufferTarget.ShaderStorageBuffer);
_ssbo.UpdateRange(Matrix4x4.Identity);
}
public void Animate(CAnimSet animation)
{
IsAnimated = true;
ResetAnimatedData();
// map bones
for (int boneIndex = 0; boneIndex < animation.Skeleton.BoneCount; boneIndex++)
{
var info = animation.Skeleton.ReferenceSkeleton.FinalRefBoneInfo[boneIndex];
if (!BonesByLoweredName.TryGetValue(info.Name.Text.ToLower(), out var bone))
continue;
bone.SkeletonIndex = boneIndex;
}
// find playable sequences
for (int s = 0; s < animation.Sequences.Count; s++)
{
var sequence = animation.Sequences[s];
foreach (var bone in BonesByLoweredName.Values.Where(bone => sequence.OriginalSequence.FindTrackForBoneIndex(bone.SkeletonIndex) >= 0))
{
bone.AnimatedBySequences.Add(s);
}
sequence.RetargetTracks(animation.Skeleton);
}
#if DEBUG
foreach ((var boneName, var bone) in BonesByLoweredName)
{
if (bone.IsRoot || bone.IsMapped) // assuming root bone always is mapped
continue;
Log.Warning($"{Name} Bone Mismatch: {boneName} ({bone.Index}) was not present in the anim's target skeleton");
}
#endif
}
public void ResetAnimatedData(bool full = false)
{
foreach (var bone in BonesByLoweredName.Values)
{
bone.SkeletonIndex = -1;
bone.AnimatedBySequences.Clear();
}
if (!full) return;
IsAnimated = false;
_ssbo.UpdateRange(Matrix4x4.Identity);
}
public void UpdateAnimationMatrices(Animation animation, bool rotationOnly)
{
if (!IsAnimated) return;
_ssbo.Bind();
foreach (var bone in BonesByLoweredName.Values)
{
var boneMatrix = bone.IsRoot ? bone.Rest.Relation : bone.Rest.LocalMatrix * _boneMatriceAtFrame[bone.ParentIndex];
if (bone.IsAnimated)
{
var (s, f) = GetBoneFrameData(bone, animation);
var sequence = animation.UnrealAnim.Sequences[s];
var boneOrientation = bone.Rest.Rotation;
var bonePosition = bone.Rest.Position;
var boneScale = bone.Rest.Scale;
sequence.Tracks[bone.SkeletonIndex].GetBoneTransform(f, sequence.NumFrames, ref boneOrientation, ref bonePosition, ref boneScale);
if (!bone.IsRoot) boneMatrix = _boneMatriceAtFrame[bone.ParentIndex];
else boneScale = bone.Rest.Scale;
bonePosition = rotationOnly ? bone.Rest.Position : bonePosition * Constants.SCALE_DOWN_RATIO;
boneMatrix = new Transform
{
Relation = boneMatrix,
Rotation = boneOrientation,
Position = bonePosition,
Scale = boneScale
}.Matrix;
}
_ssbo.Update(bone.Index, boneMatrix);
_boneMatriceAtFrame[bone.Index] = boneMatrix;
}
_ssbo.Unbind();
}
public void UpdateVertices()
{
_vbo.Bind();
foreach (var (boneName, bone) in BonesByLoweredName)
{
var boneMatrix = IsAnimated ? _boneMatriceAtFrame[bone.Index] : bone.Rest.Matrix;
var parentBoneMatrix = bone.IsRoot ? boneMatrix :
IsAnimated ? _boneMatriceAtFrame[bone.ParentIndex] :
BonesByLoweredName[bone.LoweredParentName].Rest.Matrix;
var count = 0;
var baseIndex = bone.Index * _vertexSize;
_vbo.Update(baseIndex + count++, boneMatrix.Translation.X);
_vbo.Update(baseIndex + count++, boneMatrix.Translation.Y);
_vbo.Update(baseIndex + count++, boneMatrix.Translation.Z);
_vbo.Update(baseIndex + count++, 1.0f);
_vbo.Update(baseIndex + count++, boneName == SelectedBone ? 0.0f : 1.0f);
_vbo.Update(baseIndex + count++, boneName == SelectedBone ? 0.0f : 1.0f);
_vbo.Update(baseIndex + count++, parentBoneMatrix.Translation.X);
_vbo.Update(baseIndex + count++, parentBoneMatrix.Translation.Y);
_vbo.Update(baseIndex + count++, parentBoneMatrix.Translation.Z);
_vbo.Update(baseIndex + count++, 1.0f);
_vbo.Update(baseIndex + count++, bone.LoweredParentName == SelectedBone ? 0.0f : 1.0f);
_vbo.Update(baseIndex + count++, bone.LoweredParentName == SelectedBone ? 0.0f : 1.0f);
}
_vbo.Unbind();
}
private (int, float) GetBoneFrameData(Bone bone, Animation animation)
{
int s = -1;
float f = 0.0f;
void Get(Bone b)
{
foreach (var i in b.AnimatedBySequences)
{
s = i;
if (animation.Framing.TryGetValue(s, out f))
break;
}
}
Get(bone);
if (s == -1)
{
var parent = BonesByLoweredName[bone.LoweredParentName];
while (!parent.IsAnimated)
{
parent = BonesByLoweredName[parent.LoweredParentName];
}
Get(parent);
}
return (s, f);
}
public Matrix4x4 GetBoneMatrix(Bone bone) => IsAnimated ? _boneMatriceAtFrame[bone.Index] : bone.Rest.Matrix;
public void Render(Shader shader)
{
shader.SetUniform("uIsAnimated", IsAnimated);
_ssbo.BindBufferBase(1);
_rest.BindBufferBase(2);
}
public void RenderBones()
{
GL.Disable(EnableCap.DepthTest);
GL.BindVertexArray(_vaoHandle);
GL.DrawArrays(PrimitiveType.Lines, 0, _vbo.Size);
GL.DrawArrays(PrimitiveType.Points, 0, _vbo.Size);
GL.BindVertexArray(0);
GL.Enable(EnableCap.DepthTest);
}
public void ImGuiBoneBreadcrumb()
{
var p1 = ImGui.GetCursorScreenPos();
var canvasSize = ImGui.GetContentRegionAvail() with { Y = 20 };
var p2 = p1 + canvasSize;
ImGui.BeginChild("skeleton_breadcrumb", canvasSize);
var drawList = ImGui.GetWindowDrawList();
drawList.AddRectFilled(p1, p2, 0xFF242424);
var x = p1.X;
var y = p1.Y + (p2.Y - p1.Y) / 2;
for (int i = Math.Min(_breadcrumb.Count - 1, 5); i >= 0; i--)
{
var boneName = _breadcrumb[i];
var size = ImGui.CalcTextSize(boneName);
var position = new Vector2(x + 5, y - size.Y / 2f);
ImGui.SetCursorScreenPos(position);
if (ImGui.InvisibleButton($"breakfast_{boneName}", size, ImGuiButtonFlags.MouseButtonLeft))
{
SelectedBone = boneName;
_breadcrumb.RemoveRange(0, i);
break;
}
drawList.AddText(position, i == 0 || ImGui.IsItemHovered() ? 0xFFFFFFFF : 0xA0FFFFFF, boneName);
x += size.X + 7.5f;
drawList.AddText(position with { X = x }, 0xA0FFFFFF, ">");
x += 7.5f;
}
ImGui.EndChild();
}
public void ImGuiBoneHierarchy()
{
foreach (var name in BonesByLoweredName[SelectedBone].LoweredChildNames)
{
DrawBoneTree(name, BonesByLoweredName[name]);
}
}
private void DrawBoneTree(string boneName, Bone bone)
{
ImGui.PushID(bone.Index);
ImGui.TableNextRow();
ImGui.TableNextColumn();
var flags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.SpanFullWidth;
if (bone.IsVirtual) flags |= ImGuiTreeNodeFlags.Leaf;
else if (!bone.IsDaron) flags |= ImGuiTreeNodeFlags.Bullet;
ImGui.SetNextItemOpen(bone.LoweredChildNames.Count <= 1, ImGuiCond.Appearing);
var open = ImGui.TreeNodeEx(boneName, flags);
if (ImGui.IsItemClicked() && !ImGui.IsItemToggledOpen() && bone.IsDaron)
{
SelectedBone = boneName;
_breadcrumb.Clear();
do
{
_breadcrumb.Add(boneName);
boneName = BonesByLoweredName[boneName].LoweredParentName;
} while (boneName != null);
}
ImGui.TableNextColumn();
ImGui.TextColored(ImGui.ColorConvertU32ToFloat4(bone.Color), bone.SkeletonIndex.ToString());
if (open)
{
foreach (var name in bone.LoweredChildNames)
{
DrawBoneTree(name, BonesByLoweredName[name]);
}
ImGui.TreePop();
}
ImGui.PopID();
}
public void Dispose()
{
BonesByLoweredName.Clear();
_rest?.Dispose();
_ssbo?.Dispose();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,226 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
namespace FModel.Views.Snooper.Animations;
public enum ETrackerType
{
Start,
Frame,
InBetween,
End
}
public class TimeTracker : IDisposable
{
public bool IsPaused;
public bool IsActive;
public float ElapsedTime;
public float MaxElapsedTime;
public float TimeMultiplier;
public TimeTracker()
{
Reset();
}
public void Update(float deltaSeconds)
{
if (IsPaused || IsActive) return;
ElapsedTime += deltaSeconds * TimeMultiplier;
if (ElapsedTime >= MaxElapsedTime) Reset(false);
}
public void SafeSetElapsedTime(float elapsedTime)
{
ElapsedTime = Math.Clamp(elapsedTime, 0.0f, MaxElapsedTime);
}
public void SafeSetMaxElapsedTime(float maxElapsedTime)
{
MaxElapsedTime = MathF.Max(maxElapsedTime, MaxElapsedTime);
}
public void Reset(bool doMet = true)
{
IsPaused = false;
ElapsedTime = 0.0f;
if (doMet)
{
MaxElapsedTime = 0.01f;
TimeMultiplier = 1f;
}
}
public void Dispose()
{
Reset();
}
private readonly string[] _icons = { "tl_forward", "tl_pause", "tl_rewind" };
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;
var buttonWidth = 14.0f * dpiScale;
var timeHeight = 10.0f * dpiScale;
var timeBarHeight = timeHeight * 2.0f;
var timeStep = new Vector2(50 * dpiScale, 25 * dpiScale);
var treeP0 = ImGui.GetCursorScreenPos();
var canvasSize = ImGui.GetContentRegionAvail();
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 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);
drawList.AddLine(new Vector2(treeP1.X, treeP0.Y), treeP1, 0xFF504545, thickness);
drawList.AddLine(treeP0 with { Y = timelineP0.Y + timeBarHeight }, timelineP1 with { Y = timelineP0.Y + timeBarHeight }, 0x50504545, thickness);
// adding margin
var margin = 5.0f * dpiScale;
treeP0.X += margin;
treeP1.X -= margin;
// control buttons
for (int i = 0; i < _icons.Length; i++)
{
var x = buttonWidth * 2.0f * i;
ImGui.SetCursorScreenPos(treeP0 with { X = treeP1.X - x - buttonWidth * 2.0f + thickness });
if (ImGui.ImageButton($"timeline_actions_{_icons[i]}", icons[i == 1 ? IsPaused ? "tl_play" : "tl_pause" : _icons[i]].GetPointer(), new Vector2(buttonWidth)))
{
switch (i)
{
case 0:
SafeSetElapsedTime(ElapsedTime + timeStep.X / timeRatio.X);
break;
case 1:
IsPaused = !IsPaused;
break;
case 2:
SafeSetElapsedTime(ElapsedTime - timeStep.X / timeRatio.X);
break;
}
}
}
drawList.AddText(treeP0 with { Y = treeP0.Y + thickness }, 0xA0FFFFFF, $"{ElapsedTime:F1}/{MaxElapsedTime:F1} seconds");
ImGui.SetCursorScreenPos(timelineP0);
ImGui.InvisibleButton("timeline_timetracker_canvas", timelineSize with { Y = timeBarHeight }, ImGuiButtonFlags.MouseButtonLeft);
IsActive = ImGui.IsItemActive();
if (IsActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
var mousePosCanvas = ImGui.GetIO().MousePos - timelineP0;
SafeSetElapsedTime(mousePosCanvas.X / timelineSize.X * MaxElapsedTime);
foreach (var animation in animations)
{
animation.TimeCalculation(ElapsedTime);
}
}
{ // draw time + time grid
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);
drawList.AddLine(new Vector2(cursor, timelineP0.Y + timeBarHeight), timelineP1 with { X = cursor }, 0x28C8C8C8);
drawList.AddText(fontPtr, 14 * dpiScale, new Vector2(cursor + 4, timelineP0.Y + 7.5f), 0x50FFFFFF, $"{x / timeRatio.X:F1}s");
}
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);
}
}
ImGui.PushStyleVar(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.0f, 0.5f));
for (int i = 0; i < animations.Count; i++)
{
var y = timelineP0.Y + timeBarHeight + timeStep.Y * 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();
for (int i = 0; i < animations.Count; i++)
{
var y = timelineP0.Y + timeBarHeight + timeStep.Y * i;
for (int j = 0; j < animations[i].Sequences.Length - 1; j++)
{
DrawSeparator(drawList, timelineP0, y + timeStep.Y - thickness, animations[i].Sequences[j].EndTime * timeRatio.X - 0.5f, timeHeight, timeBarHeight, ETrackerType.InBetween);
}
}
DrawSeparator(drawList, timelineP0, timelineP1.Y, ElapsedTime * timeRatio.X, timeHeight, timeBarHeight, ETrackerType.Frame);
drawList.PopClipRect();
ImGui.EndChild();
}
private void DrawSeparator(ImDrawListPtr drawList, Vector2 origin, float y, float time, float timeHeight, float timeBarHeight, ETrackerType separatorType)
{
float size = separatorType switch
{
ETrackerType.Frame => 5,
ETrackerType.End => 5,
ETrackerType.InBetween => 7.5f,
_ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null)
};
Vector2 p1 = separatorType switch
{
ETrackerType.Frame => new Vector2(origin.X + time, origin.Y + timeBarHeight),
ETrackerType.End => origin with { X = origin.X + time },
ETrackerType.InBetween => origin with { X = origin.X + time },
_ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null)
};
var p2 = p1 with { Y = y };
uint color = separatorType switch
{
ETrackerType.Frame => 0xFF6F6F6F,
ETrackerType.End => 0xFF2E3E82,
ETrackerType.InBetween => 0xA0FFFFFF,
_ => throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null)
};
switch (separatorType)
{
case ETrackerType.Frame:
color = 0xFF30478C;
var xl = p1.X - size;
var xr = p1.X + size;
var yb = origin.Y + timeBarHeight - timeHeight / 2.0f;
drawList.AddLine(p1, p2, color, 1f);
drawList.AddQuadFilled(origin with { X = xl }, origin with { X = xr }, new Vector2(xr, yb), new Vector2(xl, yb), color);
drawList.AddTriangleFilled(new Vector2(xl, yb), new Vector2(xr, yb), p1, color);
break;
case ETrackerType.End:
drawList.AddLine(p1, p2, color, 1f);
drawList.AddTriangleFilled(p1, p1 with { X = p1.X - size }, p1 with { Y = p1.Y + size }, color);
break;
case ETrackerType.InBetween:
p1.Y += timeBarHeight;
drawList.AddLine(p1, p2, color, 1f);
drawList.AddTriangleFilled(p1, new Vector2(p1.X - size / 2.0f, p1.Y - size), new Vector2(p1.X + size / 2.0f, p1.Y - size), color);
break;
default:
throw new ArgumentOutOfRangeException(nameof(separatorType), separatorType, null);
}
}
}

View File

@@ -1,83 +0,0 @@
using System;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Buffers;
public class BufferObject<TDataType> : IDisposable where TDataType : unmanaged
{
private readonly int _handle;
private readonly int _sizeOf;
private readonly BufferTarget _bufferTarget;
public readonly int Size;
private unsafe BufferObject(BufferTarget bufferTarget)
{
_bufferTarget = bufferTarget;
_handle = GL.GenBuffer();
_sizeOf = sizeof(TDataType);
Bind();
}
public BufferObject(TDataType[] data, BufferTarget bufferTarget) : this(bufferTarget)
{
Size = data.Length;
GL.BufferData(bufferTarget, Size * _sizeOf, data, BufferUsageHint.StaticDraw);
}
public BufferObject(int length, BufferTarget bufferTarget) : this(bufferTarget)
{
Size = length;
GL.BufferData(bufferTarget, Size * _sizeOf, IntPtr.Zero, BufferUsageHint.DynamicDraw);
}
public void UpdateRange(TDataType data) => UpdateRange(Size, data);
public void UpdateRange(int count, TDataType data)
{
Bind();
for (int i = 0; i < count; i++) Update(i, data);
Unbind();
}
public void Update(int offset, TDataType data)
{
GL.BufferSubData(_bufferTarget, (IntPtr) (offset * _sizeOf), _sizeOf, ref data);
}
public void Update(TDataType[] data)
{
Bind();
GL.BufferSubData(_bufferTarget, IntPtr.Zero, data.Length * _sizeOf, data);
Unbind();
}
public TDataType Get(int offset)
{
TDataType data = default;
GL.GetBufferSubData(_bufferTarget, (IntPtr) (offset * _sizeOf), _sizeOf, ref data);
return data;
}
public void Bind()
{
GL.BindBuffer(_bufferTarget, _handle);
}
public void BindBufferBase(int index)
{
if (_bufferTarget != BufferTarget.ShaderStorageBuffer)
throw new ArgumentException("BindBufferBase is not allowed for anything but Shader Storage Buffers");
GL.BindBufferBase(BufferRangeTarget.ShaderStorageBuffer, index, _handle);
}
public void Unbind()
{
GL.BindBuffer(_bufferTarget, 0);
}
public void Dispose()
{
GL.DeleteBuffer(_handle);
}
}

View File

@@ -1,126 +0,0 @@
using System;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Buffers;
public class FramebufferObject : IDisposable
{
private int _framebufferHandle;
private int _postProcessingHandle;
private int _width;
private int _height;
private readonly RenderbufferObject _renderbuffer;
private BufferObject<uint> _ebo;
private BufferObject<float> _vbo;
private VertexArrayObject<float, uint> _vao;
private Shader _shader;
private Texture _framebufferTexture;
private Texture _postProcessingTexture;
public readonly uint[] Indices = { 0, 1, 2, 3, 4, 5 };
public readonly float[] Vertices = {
// Coords // texCoords
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
public FramebufferObject(OpenTK.Mathematics.Vector2i size)
{
_width = size.X;
_height = size.Y;
_renderbuffer = new RenderbufferObject(_width, _height);
}
public void Setup()
{
_framebufferHandle = GL.GenFramebuffer();
Bind();
_framebufferTexture = new Texture((uint) _width, (uint) _height);
_renderbuffer.Setup();
_shader = new Shader("framebuffer");
_shader.Use();
_shader.SetUniform("screenTexture", 0);
_ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
_vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
_vao = new VertexArrayObject<float, uint>(_vbo, _ebo);
_vao.VertexAttributePointer(0, 2, VertexAttribPointerType.Float, 4, 0); // position
_vao.VertexAttributePointer(1, 2, VertexAttribPointerType.Float, 4, 2); // uv
var status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != FramebufferErrorCode.FramebufferComplete)
{
throw new Exception($"Framebuffer failed to bind with error: {GL.GetProgramInfoLog(_framebufferHandle)}");
}
_postProcessingHandle = GL.GenFramebuffer();
Bind(_postProcessingHandle);
_postProcessingTexture = new Texture(_width, _height);
status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != FramebufferErrorCode.FramebufferComplete)
{
throw new Exception($"Post-Processing framebuffer failed to bind with error: {GL.GetProgramInfoLog(_postProcessingHandle)}");
}
}
public void Bind() => Bind(_framebufferHandle);
public void Bind(int handle)
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, handle);
}
public void BindMsaa()
{
GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, _framebufferHandle);
GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _postProcessingHandle);
GL.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height, ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Nearest);
GL.Disable(EnableCap.DepthTest);
_shader.Use();
_vao.Bind();
_postProcessingTexture.Bind(TextureUnit.Texture0);
GL.DrawArrays(PrimitiveType.Triangles, 0, Indices.Length);
GL.Enable(EnableCap.DepthTest);
}
public IntPtr GetPointer() => _postProcessingTexture.GetPointer();
public void WindowResized(int width, int height)
{
_width = width;
_height = height;
_renderbuffer.WindowResized(width, height);
_framebufferTexture.WindowResized(width, height);
_postProcessingTexture.WindowResized(width, height);
}
public void Dispose()
{
_vao?.Dispose();
_shader?.Dispose();
_framebufferTexture?.Dispose();
_postProcessingTexture?.Dispose();
_renderbuffer?.Dispose();
GL.DeleteFramebuffer(_framebufferHandle);
GL.DeleteFramebuffer(_postProcessingHandle);
}
}

View File

@@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Objects.Core.Misc;
using OpenTK.Graphics.OpenGL4;
using System.Numerics;
using FModel.Views.Snooper.Models;
using FModel.Views.Snooper.Shading;
namespace FModel.Views.Snooper.Buffers;
public class PickingTexture : IDisposable
{
private int _width;
private int _height;
private int _framebufferHandle;
private Shader _shader;
private int _pickingTexture;
private int _depthTexture;
public PickingTexture(int width, int height)
{
_width = width;
_height = height;
}
public void Setup()
{
_framebufferHandle = GL.GenFramebuffer();
Bind();
_shader = new Shader("picking");
_pickingTexture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, _pickingTexture);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32ui, _width, _height, 0, PixelFormat.RgbaInteger, PixelType.UnsignedInt, IntPtr.Zero);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) TextureMinFilter.Nearest);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _pickingTexture, 0);
_depthTexture = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, _depthTexture);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, _width, _height, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, TextureTarget.Texture2D, _depthTexture, 0);
var status = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != FramebufferErrorCode.FramebufferComplete)
{
throw new Exception($"Framebuffer failed to bind with error: {GL.GetProgramInfoLog(_framebufferHandle)}");
}
GL.BindTexture(TextureTarget.Texture2D, 0);
Bind(0);
}
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, IDictionary<FGuid, IRenderableModel> models)
{
Bind();
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_shader.Render(viewMatrix, projMatrix);
foreach ((var guid, var model) in models)
{
_shader.SetUniform("uA", guid.A);
_shader.SetUniform("uB", guid.B);
_shader.SetUniform("uC", guid.C);
_shader.SetUniform("uD", guid.D);
if (!model.IsVisible) continue;
model.PickingRender(_shader);
}
Bind(0);
}
public void Bind() => Bind(_framebufferHandle);
public void Bind(int handle)
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, handle);
}
public FGuid ReadPixel(Vector2 mousePos, Vector2 windowPos, Vector2 windowSize)
{
Bind();
FGuid pixel = default;
var scaleX = windowSize.X / _width;
var scaleY = windowSize.Y / _height;
var x = Convert.ToInt32((mousePos.X - windowPos.X) / scaleX);
var y = -Convert.ToInt32((mousePos.Y - windowPos.Y) / scaleY);
GL.ReadBuffer(ReadBufferMode.ColorAttachment0);
GL.ReadPixels(x, y, 1, 1, PixelFormat.RgbaInteger, PixelType.UnsignedInt, ref pixel);
GL.ReadBuffer(ReadBufferMode.None);
Bind(0);
return pixel;
}
public IntPtr GetPointer() => (IntPtr) _pickingTexture;
public void WindowResized(int width, int height)
{
_width = width;
_height = height;
GL.BindTexture(TextureTarget.Texture2D, _pickingTexture);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32ui, _width, _height, 0, PixelFormat.RgbaInteger, PixelType.UnsignedInt, IntPtr.Zero);
GL.BindTexture(TextureTarget.Texture2D, _depthTexture);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, _width, _height, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
}
public void Dispose()
{
_shader?.Dispose();
GL.DeleteTexture(_pickingTexture);
GL.DeleteTexture(_depthTexture);
GL.DeleteFramebuffer(_framebufferHandle);
}
}

View File

@@ -1,41 +0,0 @@
using System;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Buffers;
public class RenderbufferObject : IDisposable
{
private int _handle;
private int _width;
private int _height;
public RenderbufferObject(int width, int height)
{
_width = width;
_height = height;
}
public void Setup()
{
_handle = GL.GenRenderbuffer();
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _handle);
GL.RenderbufferStorageMultisample(RenderbufferTarget.Renderbuffer, Constants.SAMPLES_COUNT, RenderbufferStorage.Depth24Stencil8, _width, _height);
GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthStencilAttachment, RenderbufferTarget.Renderbuffer, _handle);
}
public void WindowResized(int width, int height)
{
_width = width;
_height = height;
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _handle);
GL.RenderbufferStorageMultisample(RenderbufferTarget.Renderbuffer, Constants.SAMPLES_COUNT, RenderbufferStorage.Depth24Stencil8, _width, _height);
}
public void Dispose()
{
GL.DeleteRenderbuffer(_handle);
}
}

View File

@@ -1,75 +0,0 @@
using System;
using System.Numerics;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Buffers;
public class VertexArrayObject<TVertexType, TIndexType> : IDisposable where TVertexType : unmanaged where TIndexType : unmanaged
{
private readonly int _handle;
private readonly int _sizeOfVertex;
private readonly int _sizeOfIndex;
public unsafe VertexArrayObject(BufferObject<TVertexType> vbo, BufferObject<TIndexType> ebo)
{
_handle = GL.GenVertexArray();
_sizeOfVertex = sizeof(TVertexType);
_sizeOfIndex = sizeof(TIndexType);
Bind();
vbo.Bind();
ebo.Bind();
}
public void VertexAttributePointer(uint index, int count, VertexAttribPointerType type, int vertexSize, int offset)
{
switch (type)
{
case VertexAttribPointerType.Int:
case VertexAttribPointerType.UnsignedInt:
GL.VertexAttribIPointer(index, count, (VertexAttribIntegerType) type, vertexSize * _sizeOfVertex, offset * _sizeOfVertex);
break;
default:
GL.VertexAttribPointer(index, count, type, false, vertexSize * _sizeOfVertex, offset * _sizeOfVertex);
break;
}
GL.EnableVertexAttribArray(index);
}
public void Bind()
{
GL.BindVertexArray(_handle);
}
public void Unbind()
{
GL.BindVertexArray(0);
}
public unsafe void BindInstancing()
{
Bind();
var size = sizeof(Vector4);
GL.EnableVertexAttribArray(9);
GL.VertexAttribPointer(9, 4, VertexAttribPointerType.Float, false, 4 * size, 0);
GL.EnableVertexAttribArray(10);
GL.VertexAttribPointer(10, 4, VertexAttribPointerType.Float, false, 4 * size, 1 * size);
GL.EnableVertexAttribArray(11);
GL.VertexAttribPointer(11, 4, VertexAttribPointerType.Float, false, 4 * size, 2 * size);
GL.EnableVertexAttribArray(12);
GL.VertexAttribPointer(12, 4, VertexAttribPointerType.Float, false, 4 * size, 3 * size);
GL.VertexAttribDivisor(9, 1);
GL.VertexAttribDivisor(10, 1);
GL.VertexAttribDivisor(11, 1);
GL.VertexAttribDivisor(12, 1);
Unbind();
}
public void Dispose()
{
GL.DeleteVertexArray(_handle);
}
}

View File

@@ -1,207 +0,0 @@
using System;
using System.Numerics;
using CUE4Parse.UE4.Objects.Core.Math;
using FModel.Settings;
using ImGuiNET;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace FModel.Views.Snooper;
public class Camera
{
public enum WorldMode
{
FlyCam,
Arcball
}
public Vector3 Position;
public Vector3 Direction;
public WorldMode Mode;
public Vector3 PositionArc => Position - Direction;
public Vector3 DirectionArc => Direction - Position;
public Vector3 Up => Vector3.UnitY;
public float Zoom = 60f;
public float Speed = 1f;
public float Far = 100f;
public float Near => 0.01f;
public float AspectRatio = 16f / 9f;
public Camera()
{
Position = new Vector3(0, 1, 1);
Direction = Vector3.Zero;
Mode = UserSettings.Default.CameraMode;
}
public void Setup(FBox box) => Teleport(FVector.ZeroVector, box, true);
public void Teleport(Vector3 instancePos, FBox box, bool updateAll = false)
{
box.GetCenterAndExtents(out var center, out var extents);
center += new FVector(instancePos.X, instancePos.Z, instancePos.Y);
var distance = extents.AbsMax();
Position = new Vector3(instancePos.X, center.Z, instancePos.Z + distance * 2);
Direction = new Vector3(center.X, center.Z, center.Y);
if (updateAll)
{
Far = Math.Max(Far, box.Max.AbsMax() * 50f);
Speed = Math.Max(Speed, distance);
}
}
public void Modify(Vector2 mouseDelta)
{
var lookSensitivity = Mode switch
{
WorldMode.FlyCam => 0.002f,
WorldMode.Arcball => 0.003f,
_ => throw new ArgumentOutOfRangeException()
};
mouseDelta *= lookSensitivity;
const float tolerance = 0.001f;
var rotationX = Matrix4x4.CreateFromAxisAngle(-Up, mouseDelta.X);
switch (Mode)
{
case WorldMode.FlyCam:
{
Direction = Vector3.Transform(DirectionArc, rotationX) + Position;
var right = Vector3.Normalize(Vector3.Cross(Up, DirectionArc));
var currentPitch = MathF.Acos(Vector3.Dot(DirectionArc, Up) / (DirectionArc.Length() * Up.Length()));
var newPitch = currentPitch + mouseDelta.Y;
var clampedPitch = Math.Clamp(newPitch, tolerance, MathF.PI - tolerance);
var pitchDelta = clampedPitch - currentPitch;
var rotationY = Matrix4x4.CreateFromAxisAngle(right, pitchDelta);
Direction = Vector3.Transform(DirectionArc, rotationY) + Position;
break;
}
case WorldMode.Arcball:
{
Position = Vector3.Transform(PositionArc, rotationX) + Direction;
var right = Vector3.Normalize(Vector3.Cross(-Up, PositionArc));
var currentPitch = MathF.Acos(Vector3.Dot(PositionArc, -Up) / (PositionArc.Length() * Up.Length()));
var newPitch = currentPitch + mouseDelta.Y;
var clampedPitch = Math.Clamp(newPitch, tolerance, MathF.PI - tolerance);
var pitchDelta = clampedPitch - currentPitch;
var rotationY = Matrix4x4.CreateFromAxisAngle(right, pitchDelta);
Position = Vector3.Transform(PositionArc, rotationY) + Direction;
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
public void Modify(KeyboardState keyboard, float time)
{
if (!keyboard.IsAnyKeyDown) return;
var multiplier = keyboard.IsKeyDown(Keys.LeftShift) ? 2f : 1f;
var moveSpeed = Speed * multiplier * time;
var moveAxis = Vector3.Normalize(-PositionArc);
var panAxis = Vector3.Normalize(Vector3.Cross(moveAxis, Up));
switch (Mode)
{
case WorldMode.FlyCam:
{
if (keyboard.IsKeyDown(Keys.W)) // forward
{
var d = moveSpeed * moveAxis;
Position += d;
Direction += d;
}
if (keyboard.IsKeyDown(Keys.S)) // backward
{
var d = moveSpeed * moveAxis;
Position -= d;
Direction -= d;
}
break;
}
case WorldMode.Arcball:
{
if (keyboard.IsKeyDown(Keys.W)) // forward
Position += moveSpeed * moveAxis;
if (keyboard.IsKeyDown(Keys.S)) // backward
Position -= moveSpeed * moveAxis;
break;
}
default:
throw new ArgumentOutOfRangeException();
}
if (keyboard.IsKeyDown(Keys.A)) // left
{
var d = panAxis * moveSpeed;
Position -= d;
Direction -= d;
}
if (keyboard.IsKeyDown(Keys.D)) // right
{
var d = panAxis * moveSpeed;
Position += d;
Direction += d;
}
if (keyboard.IsKeyDown(Keys.E)) // up
{
var d = moveSpeed * Up;
Position += d;
Direction += d;
}
if (keyboard.IsKeyDown(Keys.Q)) // down
{
var d = moveSpeed * Up;
Position -= d;
Direction -= d;
}
if (keyboard.IsKeyDown(Keys.C)) // zoom in
ModifyZoom(+.5f);
if (keyboard.IsKeyDown(Keys.X)) // zoom out
ModifyZoom(-.5f);
}
private void ModifyZoom(float zoomAmount)
{
//We don't want to be able to zoom in too close or too far away so clamp to these values
Zoom = Math.Clamp(Zoom - zoomAmount, 1.0f, 89f);
}
public Matrix4x4 GetViewMatrix() => Matrix4x4.CreateLookAt(Position, Direction, Up);
public Matrix4x4 GetProjectionMatrix()
=> Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, Near, Far);
private const float _step = 0.01f;
private const float _zero = 0.000001f; // doesn't actually work if _infinite is used as max value /shrug
private const float _infinite = 0.0f;
private const ImGuiSliderFlags _clamp = ImGuiSliderFlags.AlwaysClamp;
public void ImGuiCamera()
{
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(8, 3));
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(0, 1));
if (ImGui.BeginTable("camera_editor", 2))
{
SnimGui.Layout("Mode");
ImGui.PushID(1);var m = (int) Mode;
ImGui.Combo("world_mode", ref m, "Fly Cam\0Arcball\0");
Mode = (WorldMode) m;ImGui.PopID();
SnimGui.Layout("Speed");ImGui.PushID(2);
ImGui.DragFloat("", ref Speed, _step, _zero, _infinite, "%.2f m/s", _clamp);
ImGui.PopID();SnimGui.Layout("Far Plane");ImGui.PushID(3);
ImGui.DragFloat("", ref Far, 0.1f, 0.1f, Far * 2f, "%.2f m", _clamp);
ImGui.PopID();
ImGui.EndTable();
}
ImGui.PopStyleVar(2);
}
}

View File

@@ -1,664 +0,0 @@
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 CUE4Parse.Utils;
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();
foreach (var node in nodes) node.AddToExportSession(session);
_exportResults = await session.RunAsync(exportDirectory, options, _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 objectPath = log.GetContext("ObjectPath");
var filePath = log.GetContext("FilePath");
var cg = FindOrCreateClass(className);
var og = FindOrCreateObject(cg, objectPath);
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 path)
{
foreach (var og in cg.Objects)
if (og.Path == path) return og;
var n = new ObjectGroup(path);
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 path)
{
public string Path { get; } = path;
public string Name { get; } = path.SubstringAfterLast('.');
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

@@ -1,137 +0,0 @@
using System;
using System.Numerics;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Lights;
public abstract class Light : IDisposable
{
private int _handle;
private BufferObject<uint> _ebo;
private BufferObject<float> _vbo;
private BufferObject<Matrix4x4> _matrixVbo;
private VertexArrayObject<float, uint> _vao;
public readonly uint[] Indices = { 0, 1, 2, 3, 4, 5 };
public readonly float[] Vertices = {
1f, 1f, 0f,
-1f, -1f, 0f,
-1f, 1f, 0f,
-1f, -1f, 0f,
1f, 1f, 0f,
1f, -1f, 0
};
public readonly FGuid Model;
public readonly Texture Icon;
public Transform Transform;
public Vector4 Color;
public float Intensity;
public bool IsSetup;
public Light(Texture icon, UObject light)
{
Transform = new Transform
{
Position = light.GetOrDefault("RelativeLocation", FVector.ZeroVector) * Constants.SCALE_DOWN_RATIO,
Rotation = light.GetOrDefault("RelativeRotation", FRotator.ZeroRotator).Quaternion(),
Scale = light.GetOrDefault("RelativeScale3D", FVector.OneVector)
};
Model = new FGuid((uint) light.GetFullName().GetHashCode());
Icon = icon;
Color = light.GetOrDefault("LightColor", new FColor(0xFF, 0xFF, 0xFF, 0xFF));
Intensity = light.GetOrDefault("Intensity", 1.0f);
}
public Light(FGuid model, Texture icon, UObject parent, UObject light, Transform transform)
{
Transform = new Transform
{
Relation = transform.Matrix,
Position = light.GetOrDefault("RelativeLocation", parent.GetOrDefault("RelativeLocation", FVector.ZeroVector)) * Constants.SCALE_DOWN_RATIO,
Rotation = light.GetOrDefault("RelativeRotation", parent.GetOrDefault("RelativeRotation", FRotator.ZeroRotator)).Quaternion(),
Scale = light.GetOrDefault("RelativeScale3D", parent.GetOrDefault("RelativeScale3D", FVector.OneVector))
};
Model = model;
Icon = icon;
Color = light.GetOrDefault("LightColor", parent.GetOrDefault("LightColor", new FColor(0xFF, 0xFF, 0xFF, 0xFF)));
Intensity = light.GetOrDefault("Intensity", parent.GetOrDefault("Intensity", 1.0f));
}
public void SetupInstances()
{
var instanceMatrix = new [] {Transform.Matrix};
_matrixVbo = new BufferObject<Matrix4x4>(instanceMatrix, BufferTarget.ArrayBuffer);
_vao.BindInstancing(); // VertexAttributePointer
}
public void UpdateMatrices()
{
_matrixVbo.Bind();
_matrixVbo.Update(0, Transform.Matrix);
_matrixVbo.Unbind();
}
public void Setup()
{
_handle = GL.CreateProgram();
_ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
_vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
_vao = new VertexArrayObject<float, uint>(_vbo, _ebo);
_vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 3, 0); // position
SetupInstances();
IsSetup = true;
}
public void Render(Shader shader)
{
GL.Disable(EnableCap.CullFace);
_vao.Bind();
Icon?.Bind(TextureUnit.Texture0);
shader.SetUniform("uIcon", 0);
shader.SetUniform("uColor", Color);
GL.DrawArrays(PrimitiveType.Triangles, 0, Indices.Length);
GL.Enable(EnableCap.CullFace);
}
public virtual void Render(int i, Shader shader)
{
shader.SetUniform($"uLights[{i}].Base.Color", Color);
shader.SetUniform($"uLights[{i}].Base.Position", Transform.Matrix.Translation);
shader.SetUniform($"uLights[{i}].Base.Intensity", Intensity);
}
public virtual void ImGuiLight()
{
SnimGui.Layout("Color");ImGui.PushID(1);
ImGui.ColorEdit4("", ref Color, ImGuiColorEditFlags.NoAlpha);
ImGui.PopID();SnimGui.Layout("Intensity");ImGui.PushID(2);
ImGui.DragFloat("", ref Intensity, 0.1f);ImGui.PopID();
}
public void Dispose()
{
_ebo?.Dispose();
_vbo?.Dispose();
_vao?.Dispose();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,51 +0,0 @@
using System;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
namespace FModel.Views.Snooper.Lights;
public class PointLight : Light
{
public float Linear;
public float Quadratic;
public PointLight(Texture icon, UObject point) : base(icon, point)
{
if (!point.TryGetValue(out float radius, "SourceRadius", "AttenuationRadius"))
radius = 1.0f;
radius *= Constants.SCALE_DOWN_RATIO;
Linear = 4.5f / radius;
Quadratic = 75.0f / MathF.Pow(radius, 2.0f);
}
public PointLight(FGuid model, Texture icon, UObject parent, UObject point, Transform transform) : base(model, icon, parent, point, transform)
{
if (!point.TryGetValue(out float radius, "AttenuationRadius", "SourceRadius"))
radius = 1.0f;
radius *= Constants.SCALE_DOWN_RATIO;
Linear = 4.5f / radius;
Quadratic = 75.0f / MathF.Pow(radius, 2.0f);
}
public override void Render(int i, Shader shader)
{
base.Render(i, shader);
shader.SetUniform($"uLights[{i}].Linear", Linear);
shader.SetUniform($"uLights[{i}].Quadratic", Quadratic);
shader.SetUniform($"uLights[{i}].Type", 0);
}
public override void ImGuiLight()
{
base.ImGuiLight();
SnimGui.Layout("Linear");ImGui.PushID(3);
ImGui.DragFloat("", ref Linear, 0.1f);
ImGui.PopID();SnimGui.Layout("Quadratic");ImGui.PushID(4);
ImGui.DragFloat("", ref Quadratic, 0.1f);ImGui.PopID();
}
}

View File

@@ -1,58 +0,0 @@
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Views.Snooper.Shading;
using ImGuiNET;
namespace FModel.Views.Snooper.Lights;
public class SpotLight : Light
{
public float Attenuation;
public float InnerConeAngle;
public float OuterConeAngle;
public SpotLight(Texture icon, UObject spot) : base(icon, spot)
{
if (!spot.TryGetValue(out Attenuation, "SourceRadius", "AttenuationRadius"))
Attenuation = 1.0f;
Attenuation *= Constants.SCALE_DOWN_RATIO;
InnerConeAngle = spot.GetOrDefault("InnerConeAngle", 50.0f);
OuterConeAngle = spot.GetOrDefault("OuterConeAngle", InnerConeAngle + 10);
if (OuterConeAngle < InnerConeAngle)
InnerConeAngle = OuterConeAngle - 10;
}
public SpotLight(FGuid model, Texture icon, UObject parent, UObject spot, Transform transform) : base(model, icon, parent, spot, transform)
{
if (!spot.TryGetValue(out Attenuation, "AttenuationRadius", "SourceRadius"))
Attenuation = 1.0f;
Attenuation *= Constants.SCALE_DOWN_RATIO;
InnerConeAngle = spot.GetOrDefault("InnerConeAngle", 50.0f);
OuterConeAngle = spot.GetOrDefault("OuterConeAngle", InnerConeAngle + 10);
if (OuterConeAngle < InnerConeAngle)
InnerConeAngle = OuterConeAngle - 10;
}
public override void Render(int i, Shader shader)
{
base.Render(i, shader);
shader.SetUniform($"uLights[{i}].Attenuation", Attenuation);
shader.SetUniform($"uLights[{i}].InnerConeAngle", InnerConeAngle);
shader.SetUniform($"uLights[{i}].OuterConeAngle", OuterConeAngle);
shader.SetUniform($"uLights[{i}].Type", 1);
}
public override void ImGuiLight()
{
base.ImGuiLight();
SnimGui.Layout("Attenuation");ImGui.PushID(3);
ImGui.DragFloat("", ref Attenuation, 0.1f);ImGui.PopID();
SnimGui.Layout("Inner Cone Angle");ImGui.PushID(4);
ImGui.DragFloat("", ref InnerConeAngle, 0.1f, 0.0f, 90.0f, "%.1f°");ImGui.PopID();
SnimGui.Layout("Outer Cone Angle");ImGui.PushID(5);
ImGui.DragFloat("", ref OuterConeAngle, 0.1f, 0.0f, 90.0f, "%.1f°");ImGui.PopID();
}
}

View File

@@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse.UE4.Objects.Core.Math;
namespace FModel.Views.Snooper.Models;
public class Attachment
{
private string _modelName;
private string _attachedTo;
private readonly List<string> _attachedFor;
private Matrix4x4 _oldRelation;
public bool IsAttached => _attachedTo.Length > 0;
public bool IsAttachment => _attachedFor.Count > 0;
public string Icon => IsAttachment ? "link_has" : IsAttached ? "link_on" : "link_off";
public string Tooltip => IsAttachment ? $"Is Attachment For:\n{string.Join("\n", _attachedFor)}" : IsAttached ? $"Is Attached To {_attachedTo}" : "Not Attached To Any Socket Nor Attachment For Any Model";
public Attachment(string modelName)
{
_modelName = modelName;
_attachedTo = string.Empty;
_attachedFor = new List<string>();
}
public void Attach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Add(info);
_attachedTo = $"'{socket.Name}' from '{attachedTo.Name}'{(!socket.BoneName.IsNone ? $" at '{socket.BoneName}'" : "")}";
attachedTo.Attachments.AddAttachment(_modelName);
// reset PRS to 0 so it's attached to the actual position (can be transformed relative to the socket later by the user)
_oldRelation = transform.Relation;
transform.Position = FVector.ZeroVector;
transform.Rotation = FQuat.Identity;
transform.Scale = FVector.OneVector;
}
public void Detach(IRenderableModel attachedTo, Transform transform, Socket socket, SocketAttachementInfo info)
{
socket.AttachedModels.Remove(info);
SafeDetach(attachedTo, transform);
}
public void SafeDetach(IRenderableModel attachedTo, Transform transform)
{
_attachedTo = string.Empty;
attachedTo.Attachments.RemoveAttachment(_modelName);
transform.Relation = _oldRelation;
}
public void AddAttachment(string modelName) => _attachedFor.Add($"'{modelName}'");
public void RemoveAttachment(string modelName) => _attachedFor.Remove($"'{modelName}'");
}

View File

@@ -1,228 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.PhysicsEngine;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class Collision : IDisposable
{
private const int Slices = 16;
private const int Stacks = 8;
private const float SectorStep = 2 * MathF.PI / Slices;
private const float StackStep = MathF.PI / Stacks;
private readonly int[] _indexData;
private readonly FVector[] _vertexData;
private readonly Transform _transform;
public readonly string LoweredBoneName;
private int _handle;
private BufferObject<int> _ebo { get; set; }
private BufferObject<FVector> _vbo { get; set; }
private VertexArrayObject<FVector, int> _vao { get; set; }
private Collision()
{
_indexData = [];
_vertexData = [];
_transform = Transform.Identity;
}
private Collision(FName boneName) : this()
{
LoweredBoneName = boneName.Text.ToLower();
}
public Collision(FKConvexElem convexElems, FName boneName = default) : this(boneName)
{
_indexData = convexElems.IndexData;
_vertexData = convexElems.VertexData;
_transform = new Transform
{
Position = convexElems.Transform.Translation * Constants.SCALE_DOWN_RATIO,
Rotation = convexElems.Transform.Rotation,
Scale = convexElems.Transform.Scale3D
};
}
public Collision(FKSphereElem sphereElem, FName boneName = default) : this(boneName)
{
_vertexData = new FVector[(Slices + 1) * (Stacks + 1)];
for (var i = 0; i <= Stacks; i++)
{
var stackAngle = MathF.PI / 2 - i * StackStep;
var xy = MathF.Cos(stackAngle);
var z = MathF.Sin(stackAngle);
for (var j = 0; j <= Slices; j++)
{
var sectorAngle = j * SectorStep;
var x = xy * MathF.Cos(sectorAngle);
var y = xy * MathF.Sin(sectorAngle);
_vertexData[i * (Slices + 1) + j] = new FVector(x, y, z);
}
}
_indexData = new int[Stacks * Slices * 6];
for (var i = 0; i < Stacks; i++)
{
for (var j = 0; j < Slices; j++)
{
var a = i * (Slices + 1) + j;
var b = a + Slices + 1;
_indexData[(i * Slices + j) * 6 + 0] = a;
_indexData[(i * Slices + j) * 6 + 1] = b;
_indexData[(i * Slices + j) * 6 + 2] = a + 1;
_indexData[(i * Slices + j) * 6 + 3] = b;
_indexData[(i * Slices + j) * 6 + 4] = b + 1;
_indexData[(i * Slices + j) * 6 + 5] = a + 1;
}
}
_transform = new Transform
{
Position = sphereElem.Center * Constants.SCALE_DOWN_RATIO,
Scale = new FVector(sphereElem.Radius)
};
}
public Collision(FKBoxElem boxElem, FName boneName = default) : this(boneName)
{
_vertexData =
[
new FVector(-boxElem.X, -boxElem.Y, -boxElem.Z),
new FVector(boxElem.X, -boxElem.Y, -boxElem.Z),
new FVector(boxElem.X, boxElem.Y, -boxElem.Z),
new FVector(-boxElem.X, boxElem.Y, -boxElem.Z),
new FVector(-boxElem.X, -boxElem.Y, boxElem.Z),
new FVector(boxElem.X, -boxElem.Y, boxElem.Z),
new FVector(boxElem.X, boxElem.Y, boxElem.Z),
new FVector(-boxElem.X, boxElem.Y, boxElem.Z)
];
_indexData =
[
0, 1, 2, 2, 3, 0,
1, 5, 6, 6, 2, 1,
5, 4, 7, 7, 6, 5,
4, 0, 3, 3, 7, 4,
3, 2, 6, 6, 7, 3,
4, 5, 1, 1, 0, 4
];
_transform = new Transform
{
Position = boxElem.Center * Constants.SCALE_DOWN_RATIO,
Rotation = boxElem.Rotation.Quaternion(),
Scale = new FVector(.5f)
};
}
public Collision(FKSphylElem sphylElem, FName boneName = default)
: this(sphylElem.Length, [sphylElem.Radius, sphylElem.Radius], sphylElem.Center, sphylElem.Rotation, boneName) {}
public Collision(FKTaperedCapsuleElem taperedCapsuleElem, FName boneName = default)
: this(taperedCapsuleElem.Length, [taperedCapsuleElem.Radius1, taperedCapsuleElem.Radius0], taperedCapsuleElem.Center, taperedCapsuleElem.Rotation, boneName) {}
private Collision(float length, float[] radius, FVector center = default, FRotator rotator = default, FName boneName = default) : this(boneName)
{
int vLength = 0;
int half = Slices / 2;
int k2 = (Slices + 1) * (Stacks + 1);
_vertexData = new FVector[k2 + Slices + 1];
for(int i = 0; i < 2; ++i)
{
float h = -length / 2.0f + i * length;
int start = i == 0 ? Stacks / 2 : 0;
int end = i == 0 ? Stacks : Stacks / 2;
for(int j = start; j <= end; ++j)
{
var stackAngle = MathF.PI / 2 - j * StackStep;
var xy = radius[i] * MathF.Cos(stackAngle);
var z = radius[i] * MathF.Sin(stackAngle) + h;
for(int k = 0; k <= Slices; ++k)
{
var sectorAngle = k * SectorStep;
var x = xy * MathF.Cos(sectorAngle);
var y = xy * MathF.Sin(sectorAngle);
_vertexData[vLength++] = new FVector(x, y, z);
}
}
}
var indices = new List<int>();
AddIndicesForSlices(indices, ref k2);
indices.AddRange(new[] {0, k2, k2, k2 - half, half, half});
AddIndicesForStacks(indices);
half /= 2;
indices.AddRange(new[] {half, k2 - half * 3, k2 - half * 3, half * 3, k2 - half, k2 - half});
AddIndicesForStacks(indices, Stacks / 2);
_indexData = indices.ToArray();
_transform = new Transform
{
Position = center * Constants.SCALE_DOWN_RATIO,
Rotation = rotator.Quaternion()
};
}
private void AddIndicesForSlices(List<int> indices, ref int k2)
{
for(int k1 = 0; k1 < Slices; ++k1, ++k2)
{
indices.AddRange(new[] {k1, k1 + 1, k1 + 1, k2, k2 + 1, k2 + 1});
}
}
private void AddIndicesForStacks(List<int> indices, int start = 0)
{
for (int k1 = start; k1 < Stacks * Slices + Slices; k1 += Slices + 1)
{
if (k1 == Stacks / 2 * (Slices + 1) + start) continue;
indices.AddRange(new[] {k1, k1 + Slices + 1, k1 + Slices + 1, k1 + Slices / 2, k1 + Slices / 2 + Slices + 1, k1 + Slices / 2 + Slices + 1});
}
}
public void Setup()
{
_handle = GL.CreateProgram();
_ebo = new BufferObject<int>(_indexData, BufferTarget.ElementArrayBuffer);
_vbo = new BufferObject<FVector>(_vertexData, BufferTarget.ArrayBuffer);
_vao = new VertexArrayObject<FVector, int>(_vbo, _ebo);
_vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 1, 0);
_vao.Unbind();
}
public void Render(Shader shader, Matrix4x4 boneMatrix)
{
shader.SetUniform("uCollisionMatrix", _transform.Matrix * boneMatrix);
_vao.Bind();
if (_indexData.Length > 0)
{
GL.DrawElements(PrimitiveType.Triangles, _ebo.Size, DrawElementsType.UnsignedInt, 0);
}
else
{
GL.DrawArrays(PrimitiveType.Points, 0, _vbo.Size);
}
_vao.Unbind();
}
public void Dispose()
{
_ebo?.Dispose();
_vbo?.Dispose();
_vao?.Dispose();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,14 +0,0 @@
namespace FModel.Views.Snooper.Models;
public enum EAttribute
{
Index,
Position,
Normals,
Tangent,
UVs,
Layer,
Colors,
BonesId,
BonesWeight
}

View File

@@ -1,71 +0,0 @@
using System;
using System.Numerics;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class Grid : IDisposable
{
private int _handle;
private BufferObject<uint> _ebo;
private BufferObject<float> _vbo;
private VertexArrayObject<float, uint> _vao;
private Shader _shader;
public readonly uint[] Indices = { 0, 1, 2, 3, 4, 5 };
public readonly float[] Vertices = {
1f, 1f, 0f,
-1f, -1f, 0f,
-1f, 1f, 0f,
-1f, -1f, 0f,
1f, 1f, 0f,
1f, -1f, 0
};
public Grid() {}
public void Setup()
{
_handle = GL.CreateProgram();
_ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
_vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
_vao = new VertexArrayObject<float, uint>(_vbo, _ebo);
_shader = new Shader("grid");
_vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 3, 0); // position
}
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix, float near, float far)
{
GL.Disable(EnableCap.CullFace);
GL.Disable(EnableCap.DepthTest);
_vao.Bind();
_shader.Use();
_shader.SetUniform("view", viewMatrix);
_shader.SetUniform("proj", projMatrix);
_shader.SetUniform("uNear", near);
_shader.SetUniform("uFar", far);
GL.DrawArrays(PrimitiveType.Triangles, 0, Indices.Length);
GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.CullFace);
}
public void Dispose()
{
_ebo?.Dispose();
_vbo?.Dispose();
_vao?.Dispose();
_shader?.Dispose();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,61 +0,0 @@
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 : IExportableThing, IDisposable
{
protected int Handle { get; set; }
protected BufferObject<uint> Ebo { get; set; }
protected BufferObject<float> Vbo { get; set; }
protected BufferObject<Matrix4x4> MatrixVbo { get; set; }
protected VertexArrayObject<float, uint> Vao { get; set; }
public string Path { get; }
public string Name { get; }
public string Type { get; }
public int UvCount { get; }
public uint[] Indices { get; protected set; }
public float[] Vertices { get; protected set; }
public Section[] Sections { get; protected set; }
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

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Objects.Core.Math;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class Morph : IDisposable
{
private int _handle;
public static readonly int VertexSize = 6; // Position + Tangent
public readonly string Name;
public readonly float[] Vertices;
public Morph(float[] vertices, int vertexSize, UMorphTarget morphTarget)
{
Name = morphTarget.Name;
Vertices = new float[vertices.Length / vertexSize * VertexSize];
bool TryFindVertex(uint index, out FVector positionDelta, out FVector tangentDelta)
{
foreach (var vertex in morphTarget.MorphLODModels[0].Vertices)
{
if (vertex.SourceIdx == index)
{
positionDelta = vertex.PositionDelta;
tangentDelta = vertex.TangentZDelta;
return true;
}
}
positionDelta = FVector.ZeroVector;
tangentDelta = FVector.ZeroVector;
return false;
}
for (int i = 0; i < vertices.Length; i += vertexSize)
{
var count = 0;
var baseIndex = i / vertexSize * VertexSize;
if (TryFindVertex((uint) vertices[i + 0], out var positionDelta, out var tangentDelta))
{
Vertices[baseIndex + count++] = vertices[i + 1] + positionDelta.X * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vertices[i + 2] + positionDelta.Z * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vertices[i + 3] + positionDelta.Y * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vertices[i + 7] + tangentDelta.X;
Vertices[baseIndex + count++] = vertices[i + 8] + tangentDelta.Z;
Vertices[baseIndex + count++] = vertices[i + 9] + tangentDelta.Y;
}
else
{
Vertices[baseIndex + count++] = vertices[i + 1];
Vertices[baseIndex + count++] = vertices[i + 2];
Vertices[baseIndex + count++] = vertices[i + 3];
Vertices[baseIndex + count++] = vertices[i + 7];
Vertices[baseIndex + count++] = vertices[i + 8];
Vertices[baseIndex + count++] = vertices[i + 9];
}
}
}
public Morph(float[] vertices, Dictionary<uint, int> dict, UMorphTarget morphTarget, uint index = 0)
{
Name = morphTarget.Name;
Vertices = new float[vertices.Length];
Array.Copy(vertices, Vertices, vertices.Length);
foreach (var vert in morphTarget.MorphLODModels[index].Vertices)
{
var count = 0;
if (dict.TryGetValue(vert.SourceIdx, out var baseIndex))
{
Vertices[baseIndex + count++] += vert.PositionDelta.X * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] += vert.PositionDelta.Z * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] += vert.PositionDelta.Y * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] += vert.TangentZDelta.X;
Vertices[baseIndex + count++] += vert.TangentZDelta.Z;
Vertices[baseIndex + count++] += vert.TangentZDelta.Y;
}
}
}
public void Setup()
{
_handle = GL.CreateProgram();
}
public void Dispose()
{
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Numerics;
using FModel.Views.Snooper.Shading;
namespace FModel.Views.Snooper.Models;
public class Section
{
public readonly int MaterialIndex;
public readonly int FacesCount;
public readonly int FirstFaceIndex;
public readonly IntPtr FirstFaceIndexPtr;
public readonly Vector3 Color;
public bool Show;
public Section(int index, int facesCount, int firstFaceIndex)
{
MaterialIndex = Math.Max(0, index);
FacesCount = facesCount;
FirstFaceIndex = firstFaceIndex;
FirstFaceIndexPtr = new IntPtr(FirstFaceIndex * sizeof(uint));
Color = Constants.COLOR_PALETTE[MaterialIndex % Constants.PALETTE_LENGTH];
}
public void SetupMaterial(Material material)
{
material.IsUsed = true;
Show = !material.Parameters.IsNull && !material.Parameters.IsTranslucent;
}
}

View File

@@ -1,203 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.PhysicsEngine;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Views.Snooper.Animations;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class SkeletalModel : UModel<SkinnedMeshVertex>
{
private BufferObject<float> _morphVbo;
public readonly Skeleton Skeleton;
public readonly List<Morph> Morphs;
public bool HasMorphTargets => Morphs.Count > 0;
public float MorphTime;
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.Bounds * Constants.SCALE_DOWN_RATIO;
Skeleton = new Skeleton(export.ReferenceSkeleton);
var sockets = new List<FPackageIndex>();
sockets.AddRange(export.Sockets);
if (export.Skeleton.TryLoad(out USkeleton skeleton))
{
Skeleton.Name = skeleton.Name;
Skeleton.Guid = skeleton.Guid;
// Skeleton.Merge(skeleton.ReferenceSkeleton);
sockets.AddRange(skeleton.Sockets);
}
for (int i = 0; i < sockets.Count; i++)
{
if (sockets[i].Load<USkeletalMeshSocket>() is not { } socket) continue;
Sockets.Add(new Socket(socket));
}
if (export.PhysicsAsset.TryLoad(out UPhysicsAsset physicsAsset))
{
foreach (var skeletalBodySetup in physicsAsset.SkeletalBodySetups)
{
if (!skeletalBodySetup.TryLoad(out USkeletalBodySetup bodySetup) || bodySetup.AggGeom == null) continue;
foreach (var convexElem in bodySetup.AggGeom.ConvexElems)
{
Collisions.Add(new Collision(convexElem, bodySetup.BoneName));
}
foreach (var sphereElem in bodySetup.AggGeom.SphereElems)
{
Collisions.Add(new Collision(sphereElem, bodySetup.BoneName));
}
foreach (var boxElem in bodySetup.AggGeom.BoxElems)
{
Collisions.Add(new Collision(boxElem, bodySetup.BoneName));
}
foreach (var sphylElem in bodySetup.AggGeom.SphylElems)
{
Collisions.Add(new Collision(sphylElem, bodySetup.BoneName));
}
foreach (var taperedCapsuleElem in bodySetup.AggGeom.TaperedCapsuleElems)
{
Collisions.Add(new Collision(taperedCapsuleElem, bodySetup.BoneName));
}
}
}
Morphs = [];
if (export.MorphTargets.Length == 0) return;
export.PopulateMorphTargetVerticesData();
var verticesCount = Vertices.Length / VertexSize;
var cachedVertices = new float[verticesCount * Morph.VertexSize];
var vertexLookup = new Dictionary<uint, int>(verticesCount);
for (int i = 0; i < Vertices.Length; i += VertexSize)
{
var count = 0;
var baseIndex = i / VertexSize * Morph.VertexSize;
vertexLookup[(uint) Vertices[i]] = baseIndex;
{
cachedVertices[baseIndex + count++] = Vertices[i + 1];
cachedVertices[baseIndex + count++] = Vertices[i + 2];
cachedVertices[baseIndex + count++] = Vertices[i + 3];
cachedVertices[baseIndex + count++] = Vertices[i + 7];
cachedVertices[baseIndex + count++] = Vertices[i + 8];
cachedVertices[baseIndex + count++] = Vertices[i + 9];
}
}
foreach (var morph in export.MorphTargets)
{
if (!morph.TryLoad(out UMorphTarget morphTarget) || morphTarget.MorphLODModels.Length <= skeletalMesh.LODs[LodLevel].SourceLodIndex ||
morphTarget.MorphLODModels[skeletalMesh.LODs[LodLevel].SourceLodIndex].Vertices.Length < 1)
continue;
Morphs.Add(new Morph(cachedVertices, vertexLookup, morphTarget, skeletalMesh.LODs[LodLevel].SourceLodIndex));
}
}
public SkeletalModel(USkeleton export, FBox box) : base(export)
{
Indices = [];
Materials = [];
Vertices = [];
Sections = [];
AddInstance(Transform.Identity);
Box = box * Constants.SCALE_DOWN_RATIO;
Morphs = [];
Skeleton = new Skeleton(export.ReferenceSkeleton);
Skeleton.Name = export.Name;
Skeleton.Guid = export.Guid;
for (int i = 0; i < export.Sockets.Length; i++)
{
if (export.Sockets[i].Load<USkeletalMeshSocket>() is not { } socket) continue;
Sockets.Add(new Socket(socket));
}
IsVisible = true;
}
public override void Setup(Options options)
{
base.Setup(options);
Skeleton.Setup();
if (!HasMorphTargets) return;
for (int morph = 0; morph < Morphs.Count; morph++)
{
Morphs[morph].Setup();
if (morph == 0)
_morphVbo = new BufferObject<float>(Morphs[morph].Vertices, BufferTarget.ArrayBuffer);
}
Vao.Bind();
Vao.VertexAttributePointer(13, 3, VertexAttribPointerType.Float, Morph.VertexSize, 0); // morph position
Vao.VertexAttributePointer(14, 3, VertexAttribPointerType.Float, Morph.VertexSize, 0); // morph tangent
Vao.Unbind();
}
public override void RenderCollision(Shader shader)
{
base.RenderCollision(shader);
GL.Disable(EnableCap.DepthTest);
GL.Disable(EnableCap.CullFace);
GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Line);
foreach (var collision in Collisions)
{
var boneMatrix = Matrix4x4.Identity;
if (Skeleton.BonesByLoweredName.TryGetValue(collision.LoweredBoneName, out var bone))
boneMatrix = Skeleton.GetBoneMatrix(bone);
collision.Render(shader, boneMatrix);
}
GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
GL.Enable(EnableCap.CullFace);
GL.Enable(EnableCap.DepthTest);
}
public void Render(Shader shader)
{
shader.SetUniform("uMorphTime", MorphTime);
shader.SetUniform("uIsSpline", false);
Skeleton.Render(shader);
}
public void RenderBones(Shader shader)
{
shader.SetUniform("uInstanceMatrix", GetTransform().Matrix);
Skeleton.RenderBones();
}
public void UpdateMorph(int index)
{
_morphVbo.Update(Morphs[index].Vertices);
}
public override void Dispose()
{
Skeleton?.Dispose();
if (HasMorphTargets) _morphVbo.Dispose();
foreach (var morph in Morphs)
{
morph?.Dispose();
}
Morphs.Clear();
base.Dispose();
}
}

View File

@@ -1,115 +0,0 @@
using System;
using System.Numerics;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class Skybox : IDisposable
{
private int _handle;
private BufferObject<uint> _ebo;
private BufferObject<float> _vbo;
private VertexArrayObject<float, uint> _vao;
private string[] _textures = { "px", "nx", "py", "ny", "pz", "nz" };
private Texture _cubeMap;
private Shader _shader;
public readonly uint[] Indices = { 0, 1, 3, 1, 2, 3 };
public readonly float[] Vertices = {
//X Y Z
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f
};
public Skybox() {}
public void Setup()
{
_handle = GL.CreateProgram();
_ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
_vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
_vao = new VertexArrayObject<float, uint>(_vbo, _ebo);
_cubeMap = new Texture(_textures);
_shader = new Shader("skybox");
_vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 3, 0); // position
}
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix)
{
GL.Disable(EnableCap.CullFace);
GL.DepthFunc(DepthFunction.Lequal);
_vao.Bind();
_shader.Use();
viewMatrix.M41 = 0;
viewMatrix.M42 = 0;
viewMatrix.M43 = 0;
_shader.SetUniform("uView", viewMatrix);
_shader.SetUniform("uProjection", projMatrix);
_cubeMap.Bind(TextureUnit.Texture0);
_shader.SetUniform("cubemap", 0);
GL.DrawArrays(PrimitiveType.Triangles, 0, 36);
GL.DepthFunc(DepthFunction.Less);
GL.Enable(EnableCap.CullFace);
}
public void Dispose()
{
_ebo?.Dispose();
_vbo?.Dispose();
_vao?.Dispose();
_shader?.Dispose();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.Objects.UObject;
namespace FModel.Views.Snooper.Models;
public struct SocketAttachementInfo
{
public FGuid Guid;
public int Instance;
}
public class Socket : IDisposable
{
public readonly string Name;
public readonly FName BoneName;
public readonly Transform Transform;
public readonly bool IsVirtual;
public readonly List<SocketAttachementInfo> AttachedModels;
public bool IsDaron => AttachedModels.Count > 0;
private Socket()
{
Transform = Transform.Identity;
AttachedModels = new List<SocketAttachementInfo>();
}
public Socket(string name, FName boneName, Transform transform, bool isVirtual) : this()
{
Name = name;
BoneName = boneName;
Transform = transform;
IsVirtual = isVirtual;
}
public Socket(UStaticMeshSocket socket) : this()
{
Name = socket.SocketName.Text;
Transform.Rotation = socket.RelativeRotation.Quaternion();
Transform.Position = socket.RelativeLocation * Constants.SCALE_DOWN_RATIO;
Transform.Scale = socket.RelativeScale;
}
public Socket(USkeletalMeshSocket socket) : this()
{
Name = socket.SocketName.Text;
BoneName = socket.BoneName;
Transform.Rotation = socket.RelativeRotation.Quaternion();
Transform.Position = socket.RelativeLocation * Constants.SCALE_DOWN_RATIO;
Transform.Scale = socket.RelativeScale;
}
public void Dispose()
{
AttachedModels.Clear();
}
}

View File

@@ -1,106 +0,0 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports.Component.SplineMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Objects.Core.Math;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class SplineModel : StaticModel
{
[StructLayout(LayoutKind.Sequential)]
public struct GpuParams
{
public int ForwardAxis;
public float SplineBoundaryMin;
public float SplineBoundaryMax;
public bool bSmoothInterpRollScale;
public FVector MeshOrigin;
public int _padding0;
public FVector MeshBoxExtent;
public int _padding1;
public FVector StartPos;
public float StartRoll;
public FVector StartTangent;
public int _padding2;
public FVector2D StartScale;
public FVector2D StartOffset;
public FVector EndPos;
public float EndRoll;
public FVector EndTangent;
public int _padding3;
public FVector2D EndScale;
public FVector2D EndOffset;
public FVector SplineUpDir;
public int _padding4;
public GpuParams(USplineMeshComponent splineMesh)
{
ForwardAxis = (int)splineMesh.ForwardAxis;
SplineBoundaryMin = splineMesh.SplineBoundaryMin;
SplineBoundaryMax = splineMesh.SplineBoundaryMax;
bSmoothInterpRollScale = splineMesh.bSmoothInterpRollScale;
var b = splineMesh.GetLoadedStaticMesh()?.RenderData?.Bounds ?? new FBoxSphereBounds();
MeshOrigin = b.Origin * Constants.SCALE_DOWN_RATIO;
MeshBoxExtent = b.BoxExtent * Constants.SCALE_DOWN_RATIO;
var p = splineMesh.SplineParams;
StartPos = p.StartPos * Constants.SCALE_DOWN_RATIO;
StartRoll = p.StartRoll;
StartTangent = p.StartTangent * Constants.SCALE_DOWN_RATIO;
StartScale = p.StartScale;
StartOffset = p.StartOffset;
EndPos = p.EndPos * Constants.SCALE_DOWN_RATIO;
EndRoll = p.EndRoll;
EndTangent = p.EndTangent * Constants.SCALE_DOWN_RATIO;
EndScale = p.EndScale;
EndOffset = p.EndOffset;
SplineUpDir = splineMesh.SplineUpDir;
}
}
private readonly List<GpuParams> _splineParams;
private BufferObject<GpuParams> _ssbo;
public SplineModel(UStaticMesh export, StaticMeshDto staticMesh, USplineMeshComponent splineMesh, Transform transform = null) : base(export, staticMesh, transform)
{
_splineParams = [new GpuParams(splineMesh)];
Type = "SplineMesh";
IsVisible = true;
IsTwoSided = true;
}
public void AddComponent(USplineMeshComponent splineMesh)
{
_splineParams.Add(new GpuParams(splineMesh));
}
public override void Setup(Options options)
{
base.Setup(options);
_ssbo = new BufferObject<GpuParams>(_splineParams.ToArray(), BufferTarget.ShaderStorageBuffer);
}
public void Render(Shader shader)
{
shader.SetUniform("uIsSpline", true);
_ssbo.BindBufferBase(3);
}
public override void Dispose()
{
base.Dispose();
_ssbo?.Dispose();
}
}

View File

@@ -1,159 +0,0 @@
using System;
using System.Numerics;
using CUE4Parse_Conversion.Dto;
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 CUE4Parse.UE4.Objects.PhysicsEngine;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class StaticModel : UModel<MeshVertex>
{
public StaticModel(UMaterialInterface unrealMaterial, StaticMeshDto staticMesh) : base(unrealMaterial)
{
var lod = staticMesh.LODs[LodLevel];
Indices = new uint[lod.Indices.Length];
for (int i = 0; i < Indices.Length; i++)
{
Indices[i] = lod.Indices[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.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;
Vertices[baseIndex + count++] = vert.Position.Y * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vert.Normal.X;
Vertices[baseIndex + count++] = vert.Normal.Z;
Vertices[baseIndex + count++] = vert.Normal.Y;
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++] = .5f;
}
Materials = new Material[1];
Materials[0] = new Material(unrealMaterial) { IsUsed = true };
Sections = new Section[1];
Sections[0] = new Section(0, Indices.Length, 0);
AddInstance(Transform.Identity);
Box = staticMesh.Bounds * 1.5f * Constants.SCALE_DOWN_RATIO;
}
public StaticModel(UPaperSprite paperSprite, UTexture2D texture) : base(paperSprite)
{
Indices = new uint[paperSprite.BakedRenderData.Length];
for (int i = 0; i < Indices.Length; i++)
{
Indices[i] = (uint) i;
}
Vertices = new float[paperSprite.BakedRenderData.Length * VertexSize];
for (int i = 0; i < paperSprite.BakedRenderData.Length; i++)
{
var count = 0;
var baseIndex = i * VertexSize;
var vert = paperSprite.BakedRenderData[i];
var u = vert.Z;
var v = vert.W;
Vertices[baseIndex + count++] = i;
Vertices[baseIndex + count++] = vert.X * paperSprite.PixelsPerUnrealUnit * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vert.Y * paperSprite.PixelsPerUnrealUnit * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = 0;
Vertices[baseIndex + count++] = u;
Vertices[baseIndex + count++] = v;
Vertices[baseIndex + count++] = .5f;
}
Materials = new Material[1];
if (paperSprite.DefaultMaterial?.TryLoad(out UMaterialInstance unrealMaterial) ?? false)
{
Materials[0] = new Material(unrealMaterial);
}
else
{
Materials[0] = new Material();
}
Materials[0].Parameters.Textures[CMaterialParams2.FallbackDiffuse] = texture;
Materials[0].IsUsed = true;
Sections = new Section[1];
Sections[0] = new Section(0, Indices.Length, 0);
AddInstance(Transform.Identity);
var backward = new FVector(0, Math.Max(paperSprite.BakedSourceDimension.X, paperSprite.BakedSourceDimension.Y) / 2, 0);
Box = new FBox(-backward, backward) * Constants.SCALE_DOWN_RATIO;
}
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)
{
foreach (var convexElem in bodySetup.AggGeom.ConvexElems)
{
Collisions.Add(new Collision(convexElem));
}
foreach (var sphereElem in bodySetup.AggGeom.SphereElems)
{
Collisions.Add(new Collision(sphereElem));
}
foreach (var boxElem in bodySetup.AggGeom.BoxElems)
{
Collisions.Add(new Collision(boxElem));
}
foreach (var sphylElem in bodySetup.AggGeom.SphylElems)
{
Collisions.Add(new Collision(sphylElem));
}
foreach (var taperedCapsuleElem in bodySetup.AggGeom.TaperedCapsuleElems)
{
Collisions.Add(new Collision(taperedCapsuleElem));
}
}
Box = staticMesh.Bounds * Constants.SCALE_DOWN_RATIO;
foreach (var s in export.Sockets ?? [])
{
if (s.Load<UStaticMeshSocket>() is not { } socket) continue;
Sockets.Add(new Socket(socket));
}
}
public override void RenderCollision(Shader shader)
{
base.RenderCollision(shader);
GL.Disable(EnableCap.CullFace);
GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Line);
foreach (var collision in Collisions)
{
collision.Render(shader, Matrix4x4.Identity);
}
GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
GL.Enable(EnableCap.CullFace);
}
}

View File

@@ -1,416 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Dto;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.UObject;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Shading;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Models;
public class VertexAttribute
{
public int Size;
public VertexAttribPointerType Type;
public bool Enabled;
}
public abstract class UModel<TVertex> : IRenderableModel where TVertex : struct, IMeshVertex
{
protected const int LodLevel = 0;
private readonly UObject _export;
private readonly List<VertexAttribute> _vertexAttributes =
[
new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Int, Enabled = false }, // VertexIndex
new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = true }, // Position
new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = false }, // Normal
new VertexAttribute { Size = 3, Type = VertexAttribPointerType.Float, Enabled = false }, // Tangent
new VertexAttribute { Size = 2, Type = VertexAttribPointerType.Float, Enabled = false }, // UV
new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Float, Enabled = false }, // TextureLayer
new VertexAttribute { Size = 1, Type = VertexAttribPointerType.Float, Enabled = false }, // Colors
new VertexAttribute { Size = 4, Type = VertexAttribPointerType.Float, Enabled = false }, // BoneIds
new VertexAttribute { Size = 4, Type = VertexAttribPointerType.Float, Enabled = false } // BoneWeights
];
public int Handle { get; set; }
public BufferObject<uint> Ebo { get; set; }
public BufferObject<float> Vbo { get; set; }
public BufferObject<Matrix4x4> MatrixVbo { get; set; }
public VertexArrayObject<float, uint> Vao { get; set; }
public string Path { get; }
public string Name { get; }
public string Type { get; protected set; }
public int UvCount { get; }
public uint[] Indices { get; set; }
public float[] Vertices { get; set; }
public Section[] Sections { get; set; }
public List<Transform> Transforms { get; }
public Attachment Attachments { get; }
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;
public bool HasSockets => Sockets.Count > 0;
public bool HasCollisions => Collisions.Count > 0;
public int TransformsCount => Transforms.Count;
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; }
protected UModel()
{
_export = null;
UvCount = 1;
Box = new FBox(new FVector(-2f), new FVector(2f));
Sockets = new List<Socket>();
Collisions = new List<Collision>();
Transforms = new List<Transform>();
}
protected UModel(UObject export)
{
_export = export;
Path = _export.GetPathName();
Name = export.Name;
Type = export.ExportType;
UvCount = 1;
Box = new FBox(new FVector(-2f), new FVector(2f));
Sockets = new List<Socket>();
Collisions = new List<Collision>();
Transforms = new List<Transform>();
Attachments = new Attachment(Name);
_vertexAttributes[(int) EAttribute.Index].Enabled =
_vertexAttributes[(int) EAttribute.Normals].Enabled =
_vertexAttributes[(int) EAttribute.Tangent].Enabled =
_vertexAttributes[(int) EAttribute.UVs].Enabled =
_vertexAttributes[(int) EAttribute.Layer].Enabled = true;
}
protected UModel(UObject export, MeshLodDto<TVertex> lod, IReadOnlyList<FPackageIndex> materials, IReadOnlyList<TVertex> vertices, int numLods, Transform transform = null) : this(export)
{
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.Length];
for (int i = 0; i < Indices.Length; i++)
{
Indices[i] = lod.Indices[i];
}
Materials = new Material[materials.Count];
for (int m = 0; m < Materials.Length; m++)
{
if ((materials[m]?.TryLoad(out var material) ?? false) && material is UMaterialInterface unrealMaterial)
Materials[m] = new Material(unrealMaterial); else Materials[m] = new Material();
}
_vertexAttributes[(int) EAttribute.Colors].Enabled = lod.VertexColors is { Length: > 0 };
_vertexAttributes[(int) EAttribute.BonesId].Enabled =
_vertexAttributes[(int) EAttribute.BonesWeight].Enabled = vertices is SkinnedMeshVertex[];
Vertices = new float[vertices.Count * VertexSize];
for (int i = 0; i < vertices.Count; i++)
{
var count = 0;
var baseIndex = i * VertexSize;
var vert = 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;
Vertices[baseIndex + count++] = vert.Position.Y * Constants.SCALE_DOWN_RATIO;
Vertices[baseIndex + count++] = vert.Normal.X;
Vertices[baseIndex + count++] = vert.Normal.Z;
Vertices[baseIndex + count++] = vert.Normal.Y;
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.ExtraUvs[0][i].U - 1 : .5f;
if (HasVertexColors)
{
Vertices[baseIndex + count++] = lod.VertexColors![0].Colors[i].ToPackedARGB();
}
if (vert is SkinnedMeshVertex skelVert)
{
int max = skelVert.Influences.Length;
for (int j = 0; j < 8; j++)
{
var boneID = j < max ? skelVert.Influences[j].Bone : (ushort) 0;
var weight = j < max ? skelVert.Influences[j].RawWeight : (ushort) 0;
// Pack bone ID and weight
Vertices[baseIndex + count++] = (boneID << 16) | weight;
}
}
}
Sections = new Section[lod.Sections.Length];
for (var s = 0; s < Sections.Length; 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]);
}
AddInstance(transform ?? Transform.Identity);
}
public virtual void Setup(Options options)
{
Handle = GL.CreateProgram();
Ebo = new BufferObject<uint>(Indices, BufferTarget.ElementArrayBuffer);
Vbo = new BufferObject<float>(Vertices, BufferTarget.ArrayBuffer);
Vao = new VertexArrayObject<float, uint>(Vbo, Ebo);
var offset = 0;
var broken = GL.GetInteger(GetPName.MaxTextureCoords) == 0;
for (int i = 0; i < _vertexAttributes.Count; i++)
{
var attribute = _vertexAttributes[i];
if (!attribute.Enabled) continue;
if (i != 5 || !broken)
{
Vao.VertexAttributePointer((uint) i, attribute.Size, attribute.Type, VertexSize, offset);
}
offset += attribute.Size;
}
SetupInstances(); // instanced models transform
// setup all used materials for use in different UV channels
for (var i = 0; i < Materials.Length; i++)
{
if (!Materials[i].IsUsed) continue;
Materials[i].Setup(options, broken ? 1 : UvCount);
}
foreach (var collision in Collisions)
{
collision.Setup();
}
if (options.Models.Count == 1 && Sections.All(x => !x.Show)) // visible if alone and invisible
{
IsVisible = true;
foreach (var section in Sections)
{
section.Show = true;
}
}
else if (!IsVisible) // default: visible if one section is visible
{
foreach (var section in Sections)
{
if (section.Show)
{
IsVisible = true;
break;
}
}
}
else foreach (var section in Sections) // force visibility
{
section.Show = true;
}
IsSetup = true;
}
public virtual void Render(Shader shader, Texture checker = null, bool outline = false)
{
if (outline) GL.Disable(EnableCap.DepthTest);
if (IsTwoSided) GL.Disable(EnableCap.CullFace);
if (IsSelected)
{
GL.Enable(EnableCap.StencilTest);
GL.StencilFunc(outline ? StencilFunction.Notequal : StencilFunction.Always, 1, 0xFF);
}
if (this is SkeletalModel skeletalModel) skeletalModel.Render(shader);
else if (this is SplineModel splineModel) splineModel.Render(shader);
else
{
shader.SetUniform("uIsAnimated", false);
shader.SetUniform("uIsSpline", false);
}
if (!outline)
{
shader.SetUniform("uUvCount", UvCount);
shader.SetUniform("uOpacity", ShowCollisions && IsSelected ? 0.75f : 1f);
shader.SetUniform("uHasVertexColors", HasVertexColors);
}
Vao.Bind();
GL.PolygonMode(TriangleFace.FrontAndBack, ShowWireframe ? PolygonMode.Line : PolygonMode.Fill);
foreach (var section in Sections)
{
if (!section.Show) continue;
if (!outline)
{
if (checker != null)
{
shader.SetUniform("uParameters.Diffuse[0].Sampler", 0);
checker.Bind(TextureUnit.Texture0);
}
else
{
shader.SetUniform("uSectionColor", section.Color);
Materials[section.MaterialIndex].Render(shader);
}
}
GL.DrawElementsInstanced(PrimitiveType.Triangles, section.FacesCount, DrawElementsType.UnsignedInt, section.FirstFaceIndexPtr, TransformsCount);
}
GL.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
Vao.Unbind();
if (IsSelected)
{
GL.StencilFunc(StencilFunction.Always, 0, 0xFF);
GL.Disable(EnableCap.StencilTest);
}
if (IsTwoSided) GL.Enable(EnableCap.CullFace);
if (outline) GL.Enable(EnableCap.DepthTest);
}
public void PickingRender(Shader shader)
{
if (IsTwoSided) GL.Disable(EnableCap.CullFace);
if (this is SkeletalModel skeletalModel) skeletalModel.Render(shader);
if (this is SplineModel splineModel) splineModel.Render(shader);
else
{
shader.SetUniform("uIsAnimated", false);
shader.SetUniform("uIsSpline", false);
}
Vao.Bind();
foreach (var section in Sections)
{
if (!section.Show) continue;
GL.DrawElementsInstanced(PrimitiveType.Triangles, section.FacesCount, DrawElementsType.UnsignedInt, section.FirstFaceIndexPtr, TransformsCount);
}
Vao.Unbind();
if (IsTwoSided) GL.Enable(EnableCap.CullFace);
}
public virtual void RenderCollision(Shader shader)
{
shader.SetUniform("uInstanceMatrix", GetTransform().Matrix);
shader.SetUniform("uScaleDown", Constants.SCALE_DOWN_RATIO);
}
public void Update(Options options)
{
MatrixVbo.Bind();
for (int instance = 0; instance < TransformsCount; instance++)
{
MatrixVbo.Update(instance, Transforms[instance].Matrix);
}
MatrixVbo.Unbind();
var worldMatrix = GetTransform().Matrix;
foreach (var socket in Sockets)
{
if (!socket.IsDaron) continue;
var boneMatrix = Matrix4x4.Identity;
if (this is SkeletalModel skeletalModel && skeletalModel.Skeleton.BonesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var bone))
boneMatrix = skeletalModel.Skeleton.GetBoneMatrix(bone);
var socketRelation = boneMatrix * worldMatrix;
foreach (var info in socket.AttachedModels)
{
if (!options.TryGetModel(info.Guid, out var attachedModel))
continue;
attachedModel.Transforms[info.Instance].Relation = socket.Transform.LocalMatrix * socketRelation;
attachedModel.Update(options);
}
}
}
public void AddInstance(Transform transform)
{
SelectedInstance = TransformsCount;
Transforms.Add(transform);
}
public void SetupInstances()
{
MatrixVbo = new BufferObject<Matrix4x4>(TransformsCount, BufferTarget.ArrayBuffer);
for (int instance = 0; instance < TransformsCount; instance++)
{
Transforms[instance].Save();
MatrixVbo.Update(instance, Transforms[instance].Matrix);
}
Vao.BindInstancing(); // VertexAttributePointer
}
public Transform GetTransform() => Transforms[SelectedInstance];
public Matrix4x4 GetSocketTransform(int index)
{
var socket = Sockets[index];
var worldMatrix = GetTransform().Matrix;
var boneMatrix = Matrix4x4.Identity;
if (this is SkeletalModel skeletalModel && skeletalModel.Skeleton.BonesByLoweredName.TryGetValue(socket.BoneName.Text.ToLower(), out var bone))
boneMatrix = skeletalModel.Skeleton.GetBoneMatrix(bone);
var socketRelation = boneMatrix * worldMatrix;
return socket.Transform.LocalMatrix * socketRelation;
}
public void AddToExportSession(ExportSession session)
{
session.Add(_export);
}
public virtual void Dispose()
{
Ebo?.Dispose();
Vbo?.Dispose();
MatrixVbo?.Dispose();
Vao?.Dispose();
foreach (var socket in Sockets)
{
socket?.Dispose();
}
Sockets.Clear();
foreach (var collision in Collisions)
{
collision?.Dispose();
}
Collisions.Clear();
GL.DeleteProgram(Handle);
}
}

View File

@@ -1,285 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Settings;
using FModel.Views.Snooper.Animations;
using FModel.Views.Snooper.Lights;
using FModel.Views.Snooper.Models;
using FModel.Views.Snooper.Shading;
using SkiaSharp;
namespace FModel.Views.Snooper;
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 readonly Dictionary<FGuid, IRenderableModel> Models;
public readonly Dictionary<FGuid, Texture> Textures;
public readonly List<Light> Lights;
public readonly TimeTracker Tracker;
public readonly List<Animation> Animations;
public readonly Dictionary<string, Texture> Icons;
private readonly string _game;
public Options()
{
Models = new Dictionary<FGuid, IRenderableModel>();
Textures = new Dictionary<FGuid, Texture>();
Lights = new List<Light>();
Tracker = new TimeTracker();
Animations = new List<Animation>();
Icons = new Dictionary<string, Texture>
{
["material"] = new ("materialicon"),
["square"] = new ("square"),
["square_off"] = new ("square_off"),
["cube"] = new ("cube"),
["cube_off"] = new ("cube_off"),
["light"] = new ("light"),
["light_off"] = new ("light_off"),
["noimage"] = new ("T_Placeholder_Item_Image"),
["checker"] = new ("checker"),
["pointlight"] = new ("pointlight"),
["spotlight"] = new ("spotlight"),
["link_on"] = new ("link_on"),
["link_off"] = new ("link_off"),
["link_has"] = new ("link_has"),
["tl_play"] = new ("tl_play"),
["tl_pause"] = new ("tl_pause"),
["tl_rewind"] = new ("tl_rewind"),
["tl_forward"] = new ("tl_forward"),
["tl_previous"] = new ("tl_previous"),
["tl_next"] = new ("tl_next"),
};
_game = Services.ApplicationService.ApplicationView.CUE4Parse.Provider.ProjectName.ToUpper();
SelectModel(Guid.Empty);
}
public void SetupModelsAndLights()
{
foreach (var model in Models.Values)
{
if (model.IsSetup) continue;
model.Setup(this);
}
foreach (var light in Lights)
{
if (light.IsSetup) continue;
light.Setup();
}
}
public void SelectModel(FGuid guid)
{
// unselect old
if (TryGetModel(out var model))
model.IsSelected = false;
// select new
if (!TryGetModel(guid, out model))
SelectedModel = Guid.Empty;
else
{
model.IsSelected = true;
SelectedModel = guid;
}
SelectedSection = 0;
SelectedMorph = 0;
}
public void SelectAnimation(int animation)
{
SelectedAnimation = animation;
}
public void RemoveModel(FGuid guid)
{
if (!TryGetModel(guid, out var m) || m is null)
return;
DetachAndRemoveModels(m, true);
m.Dispose();
Models.Remove(guid);
}
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 null)
continue;
var t = m.GetTransform();
if (m.IsProp)
{
m.Attachments.SafeDetach(model, t);
RemoveModel(info.Guid);
}
else if (detach) m.Attachments.SafeDetach(model, t);
}
if (socket.IsVirtual)
{
socket.Dispose();
model.Sockets.Remove(socket);
}
}
}
public void AddAnimation(Animation animation)
{
Animations.Add(animation);
}
public void RemoveAnimations()
{
Tracker.Reset();
SelectedAnimation = 0;
foreach (var animation in Animations)
{
foreach (var guid in animation.AttachedModels)
{
if (!TryGetModel(guid, out var model) || model is not SkeletalModel animatedModel)
continue;
animatedModel.Skeleton.ResetAnimatedData(true);
DetachAndRemoveModels(animatedModel, false);
}
animation.Dispose();
}
foreach (var kvp in Models)
if (kvp.Value.IsProp)
RemoveModel(kvp.Key);
Animations.Clear();
}
public void SelectSection(int index)
{
SelectedSection = index;
}
public void SelectMorph(int index, SkeletalModel model)
{
SelectedMorph = index;
model.UpdateMorph(SelectedMorph);
}
public bool TryGetTexture(UTexture o, bool fix, out Texture texture)
{
var guid = o.LightingGuid;
if (Textures.TryGetValue(guid, out texture)) return texture != null;
var bitmap = o switch
{
UTexture2D texture2D => texture2D.Decode(UserSettings.Default.PreviewMaxTextureSize, UserSettings.Default.CurrentDir.TexturePlatform),
UTexture2DArray texture2DArray => texture2DArray.DecodeTextureArray(UserSettings.Default.CurrentDir.TexturePlatform)?.FirstOrDefault(),
_ => o.Decode(UserSettings.Default.CurrentDir.TexturePlatform)
};
if (bitmap is not null)
{
texture = new Texture(bitmap.ToSkBitmap(), o);
if (fix) TextureHelper.FixChannels(_game, texture);
Textures[guid] = texture;
}
return texture != null;
}
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)
{
if (TryGetModel(guid, out var model))
{
return TryGetSection(model, out section);
}
section = null;
return false;
}
public bool TryGetSection(IRenderableModel model, out Section section)
{
if (SelectedSection >= 0 && SelectedSection < model.Sections.Length)
section = model.Sections[SelectedSection]; else section = null;
return section != null;
}
public void SwapMaterial(bool value)
{
Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsOverwritingMaterial = value;
}
public void AnimateMesh(bool value)
{
Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation = value;
}
/// <summary>
/// Skip emissive for specific games, cause of excessive use in their materials
/// </summary>
public bool SkipEmissive()
{
return _game switch
{
"LIESOFP" or "CODEVEIN2" or "HIGHONLIFE2" or "MORTALSHELL2" => true,
_ => false,
};
}
public void ResetModelsLightsAnimations()
{
foreach (var model in Models.Values)
{
model.Dispose();
}
Models.Clear();
Lights.Clear();
Tracker.Reset();
foreach (var animation in Animations)
{
animation.Dispose();
}
Animations.Clear();
}
public void Dispose()
{
ResetModelsLightsAnimations();
foreach (var texture in Textures.Values)
{
texture.Dispose();
}
Textures.Clear();
foreach (var texture in Icons.Values)
{
texture.Dispose();
}
Icons.Clear();
}
}

View File

@@ -1,816 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
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.SkeletalMesh;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.UObject;
using CUE4Parse.Utils;
using FModel.Creator;
using FModel.Settings;
using FModel.Views.Snooper.Animations;
using FModel.Views.Snooper.Buffers;
using FModel.Views.Snooper.Lights;
using FModel.Views.Snooper.Models;
using FModel.Views.Snooper.Shading;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace FModel.Views.Snooper;
public enum VertexColor
{
Default,
Sections,
Colors,
Normals,
TextureCoordinates
}
public class Renderer : IDisposable
{
private readonly Skybox _skybox;
private readonly Grid _grid;
private Shader _shader;
private Shader _outline;
private Shader _light;
private Shader _bone;
private Shader _collision;
private bool _saveCameraMode;
public bool ShowSkybox;
public bool ShowGrid;
public bool ShowLights;
public bool AnimateWithRotationOnly;
public bool IsSkeletonTreeOpen;
public VertexColor Color;
public Camera CameraOp { get; }
public PickingTexture Picking { get; }
public Options Options { get; }
public Renderer(int width, int height)
{
_skybox = new Skybox();
_grid = new Grid();
CameraOp = new Camera();
Picking = new PickingTexture(width, height);
Options = new Options();
ShowSkybox = UserSettings.Default.ShowSkybox;
ShowGrid = UserSettings.Default.ShowGrid;
AnimateWithRotationOnly = UserSettings.Default.AnimateWithRotationOnly;
Color = VertexColor.Default;
}
public void Load(CancellationToken cancellationToken, UObject dummy, Lazy<UObject> export)
{
ShowLights = false;
Color = VertexColor.Default;
_saveCameraMode = dummy is not UWorld and not UBlueprintGeneratedClass;
switch (dummy)
{
case UStaticMesh when export.Value is UStaticMesh st:
LoadStaticMesh(st, UserSettings.Default.NaniteMeshExportFormat);
break;
case USkeletalMesh when export.Value is USkeletalMesh sk:
LoadSkeletalMesh(sk);
break;
case USkeleton when export.Value is USkeleton skel:
LoadSkeleton(skel);
break;
case UMaterialInstance when export.Value is UMaterialInstance mi:
LoadMaterialInstance(mi);
break;
case UWorld when export.Value is UWorld wd:
LoadWorld(cancellationToken, wd, Transform.Identity);
break;
case UBlueprintGeneratedClass when export.Value is UBlueprintGeneratedClass bp:
LoadJunoWorld(cancellationToken, bp, Transform.Identity);
Color = VertexColor.Colors;
break;
case UPaperSprite when export.Value is UPaperSprite ps:
LoadPaperSprite(ps);
break;
}
CameraOp.Mode = _saveCameraMode ? UserSettings.Default.CameraMode : Camera.WorldMode.FlyCam;
SetupCamera();
}
public void Swap(UMaterialInstance unrealMaterial)
{
if (!Options.TryGetModel(out var model) || !Options.TryGetSection(model, out var section)) return;
model.Materials[section.MaterialIndex].SwapMaterial(unrealMaterial);
Application.Current.Dispatcher.Invoke(() => model.Materials[section.MaterialIndex].Setup(Options, model.UvCount));
}
public void Animate(UObject anim)
{
if (!Services.ApplicationService.ApplicationView.CUE4Parse.ModelIsWaitingAnimation)
{
if (anim is UAnimSequenceBase animBase)
{
/*if (Options.TryGetModel(out var selected) &&
selected is SkeletalModel { IsVisible: true } skeletalModel &&
skeletalModel.Skeleton.Guid == animBase.SkeletonGuid)
{
// do nothing, selected model has the correct skeleton for this animation
}
else */if (animBase.Skeleton.TryLoad(out USkeleton skeleton))
{
LoadSkeleton(skeleton);
}
}
else return; // should never end here
}
Animate(anim, Options.SelectedModel);
}
private void Animate(UObject anim, FGuid guid)
{
if (anim is not UAnimSequenceBase animBase || !animBase.Skeleton.TryLoad(out USkeleton skeleton) ||
!Options.TryGetModel(guid, out var m) || m is not SkeletalModel model)
return;
var animSet = animBase switch
{
UAnimSequence animSequence => skeleton.ConvertAnims(animSequence),
UAnimMontage animMontage => skeleton.ConvertAnims(animMontage),
UAnimComposite animComposite => skeleton.ConvertAnims(animComposite),
_ => throw new ArgumentException("Unknown animation type")
};
var animation = new Animation(anim, animSet, guid);
model.Skeleton.Animate(animSet);
Options.AddAnimation(animation);
foreach (var notifyEvent in animBase.Notifies ?? [])
{
if (notifyEvent.NotifyStateClass is null ||
!notifyEvent.NotifyStateClass.TryLoad(out UObject notifyClass) ||
!notifyClass.TryGetValue(out UObject export, "SkeletalMeshProp", "StaticMeshProp", "Mesh", "SkeletalMeshTemplate"))
continue;
var t = Transform.Identity;
if (notifyClass.TryGetValue(out FTransform offset, "Offset"))
{
t.Rotation = offset.Rotation;
t.Position = offset.Translation * Constants.SCALE_DOWN_RATIO;
t.Scale = offset.Scale3D;
}
IRenderableModel addedModel = null;
switch (export)
{
case UStaticMesh st:
{
guid = st.LightingGuid;
if (Options.TryGetModel(guid, out addedModel))
{
addedModel.AddInstance(t);
}
else if (st.TryConvert(out var mesh, EMeshQuality.Highest))
{
addedModel = new StaticModel(st, mesh, t);
Options.Models[guid] = addedModel;
}
break;
}
case USkeletalMesh sk:
{
guid = Guid.NewGuid();
if (!Options.Models.ContainsKey(guid) && sk.TryConvert(out var mesh, EMeshQuality.Highest))
{
addedModel = new SkeletalModel(sk, mesh, t);
Options.Models[guid] = addedModel;
}
break;
}
}
if (addedModel == null)
throw new ArgumentException("Unknown model type");
addedModel.IsProp = true;
if (notifyClass.TryGetValue(out UObject skeletalMeshPropAnimation, "SkeletalMeshPropAnimation", "Animation", "AnimToPlay"))
Animate(skeletalMeshPropAnimation, guid);
if (notifyClass.TryGetValue(out FName socketName, "SocketName"))
{
t = Transform.Identity;
if (notifyClass.TryGetValue(out FVector location, "LocationOffset", "Location"))
t.Position = location * Constants.SCALE_DOWN_RATIO;
if (notifyClass.TryGetValue(out FRotator rotation, "RotationOffset", "Rotation"))
t.Rotation = rotation.Quaternion();
if (notifyClass.TryGetValue(out FVector scale, "Scale"))
t.Scale = scale;
var s = new Socket($"ANIM_{addedModel.Name}", socketName, t, true);
model.Sockets.Add(s);
addedModel.Attachments.Attach(model, addedModel.GetTransform(), s,
new SocketAttachementInfo { Guid = guid, Instance = addedModel.SelectedInstance });
}
}
Options.Tracker.IsPaused = false;
Options.Tracker.SafeSetMaxElapsedTime(animation.TotalElapsedTime);
}
public void Setup()
{
_skybox.Setup();
_grid.Setup();
_shader = new Shader();
_outline = new Shader("outline");
_light = new Shader("light");
_bone = new Shader("bone");
_collision = new Shader("collision", "bone");
Picking.Setup();
Options.SetupModelsAndLights();
}
public void Render()
{
var viewMatrix = CameraOp.GetViewMatrix();
var projMatrix = CameraOp.GetProjectionMatrix();
if (ShowSkybox) _skybox.Render(viewMatrix, projMatrix);
if (ShowGrid) _grid.Render(viewMatrix, projMatrix, CameraOp.Near, CameraOp.Far);
_shader.Render(viewMatrix, CameraOp.Position, projMatrix);
for (int i = 0; i < 5; i++)
_shader.SetUniform($"bVertexColors[{i}]", i == (int) Color);
// render model pass
foreach (var model in Options.Models.Values)
{
if (!model.IsVisible) continue;
model.Render(_shader, Color == VertexColor.TextureCoordinates ? Options.Icons["checker"] : null);
}
{ // light pass
var uNumLights = Math.Min(Options.Lights.Count, 100);
_shader.SetUniform("uNumLights", ShowLights ? uNumLights : 0);
if (ShowLights)
for (int i = 0; i < uNumLights; i++)
Options.Lights[i].Render(i, _shader);
_light.Render(viewMatrix, projMatrix);
for (int i = 0; i < uNumLights; i++)
Options.Lights[i].Render(_light);
}
// debug + outline pass
if (Options.TryGetModel(out var selected) && selected.IsVisible)
{
if (IsSkeletonTreeOpen && selected is SkeletalModel skeletalModel)
{
_bone.Render(viewMatrix, projMatrix);
skeletalModel.RenderBones(_bone);
}
else if (selected.ShowCollisions)
{
_collision.Render(viewMatrix, projMatrix);
selected.RenderCollision(_collision);
}
_outline.Render(viewMatrix, CameraOp.Position, projMatrix);
selected.Render(_outline, Color == VertexColor.TextureCoordinates ? Options.Icons["checker"] : null, true);
}
// picking pass (dedicated FBO, binding to 0 afterward)
Picking.Render(viewMatrix, projMatrix, Options.Models);
}
public void Update(Snooper wnd, float deltaSeconds)
{
if (Options.Animations.Count > 0) Options.Tracker.Update(deltaSeconds);
foreach (var animation in Options.Animations)
{
animation.TimeCalculation(Options.Tracker.ElapsedTime);
foreach (var guid in animation.AttachedModels)
{
if (!Options.TryGetModel(guid, out var m) || m is not SkeletalModel skeletalModel) continue;
skeletalModel.Skeleton.UpdateAnimationMatrices(animation, AnimateWithRotationOnly);
}
}
{
foreach (var model in Options.Models.Values)
{
model.Update(Options);
}
if (IsSkeletonTreeOpen && Options.TryGetModel(out var selected) && selected is SkeletalModel { IsVisible: true } skeletalModel)
{
skeletalModel.Skeleton.UpdateVertices();
}
}
CameraOp.Modify(wnd.KeyboardState, deltaSeconds);
if (wnd.KeyboardState.IsKeyPressed(Keys.Z) &&
Options.TryGetModel(out var selectedModel) &&
selectedModel is SkeletalModel)
{
Options.RemoveAnimations();
Options.AnimateMesh(true);
wnd.WindowShouldClose(true, false);
}
if (wnd.KeyboardState.IsKeyPressed(Keys.Space))
Options.Tracker.IsPaused = !Options.Tracker.IsPaused;
if (wnd.KeyboardState.IsKeyPressed(Keys.Delete))
Options.RemoveModel(Options.SelectedModel);
if (wnd.KeyboardState.IsKeyPressed(Keys.H))
wnd.WindowShouldClose(true, false);
if (wnd.KeyboardState.IsKeyPressed(Keys.Escape))
wnd.WindowShouldClose(true, true);
}
private void LoadStaticMesh(UStaticMesh original, ENaniteMeshFormat naniteFormat = ENaniteMeshFormat.NoNanite)
{
var guid = original.LightingGuid;
if (Options.TryGetModel(guid, out var model))
{
model.AddInstance(Transform.Identity);
Application.Current.Dispatcher.Invoke(() => model.SetupInstances());
return;
}
if (!original.TryConvert(out var mesh, EMeshQuality.Highest, naniteFormat))
return;
Options.Models[guid] = new StaticModel(original, mesh);
Options.SelectModel(guid);
}
private void LoadSkeletalMesh(USkeletalMesh original)
{
var guid = new FGuid((uint) original.GetFullName().GetHashCode());
if (Options.Models.ContainsKey(guid) || !original.TryConvert(out var mesh, EMeshQuality.Highest)) return;
var skeletalModel = new SkeletalModel(original, mesh);
Options.Models[guid] = skeletalModel;
Options.SelectModel(guid);
}
private void LoadSkeleton(USkeleton original)
{
var guid = original.Guid;
if (Options.Models.ContainsKey(guid) || !original.TryConvert(out _, out var box)) return;
var fakeSkeletalModel = new SkeletalModel(original, box);
Options.Models[guid] = fakeSkeletalModel;
Options.SelectModel(guid);
IsSkeletonTreeOpen = true;
}
private void LoadMaterialInstance(UMaterialInstance original)
{
if (!Utils.TryLoadObject("Engine/Content/BasicShapes/Cube.Cube", out UStaticMesh editorCube))
return;
var guid = editorCube.LightingGuid;
if (Options.TryGetModel(guid, out var model))
{
model.Materials[0].SwapMaterial(original);
Application.Current.Dispatcher.Invoke(() => model.Materials[0].Setup(Options, model.UvCount));
return;
}
if (!editorCube.TryConvert(out var mesh, EMeshQuality.Highest))
return;
Options.Models[guid] = new StaticModel(original, mesh);
Options.SelectModel(guid);
}
private void LoadPaperSprite(UPaperSprite original)
{
if (!(original.BakedSourceTexture?.TryLoad(out UTexture2D texture) ?? false))
return;
var guid = texture.LightingGuid;
if (Options.TryGetModel(guid, out var model))
{
model.AddInstance(Transform.Identity);
Application.Current.Dispatcher.Invoke(() => model.SetupInstances());
return;
}
Options.Models[guid] = new StaticModel(original, texture);
Options.SelectModel(guid);
}
private void SetupCamera()
{
if (Options.TryGetModel(out var model))
CameraOp.Setup(model.Box);
}
private void LoadWorld(CancellationToken cancellationToken, UWorld original, Transform transform)
{
CameraOp.Setup(new FBox(FVector.ZeroVector, new FVector(0, 10, 10)));
if (original.PersistentLevel.Load<ULevel>() is not { } persistentLevel)
return;
if (persistentLevel.TryGetValue(out FSoftObjectPath runtimeCell, "WorldPartitionRuntimeCell") &&
runtimeCell.TryLoad(out UObject worldPartition))
{
var position = worldPartition.GetOrDefault("Position", FVector.ZeroVector) * Constants.SCALE_DOWN_RATIO;
var box = worldPartition.GetOrDefault("ContentBounds", new FBox(FVector.ZeroVector, FVector.OneVector));
box *= MathF.Pow(Constants.SCALE_DOWN_RATIO, 2);
CameraOp.Teleport(new Vector3(position.X, position.Z, position.Y), box, true);
}
var length = persistentLevel.Actors.Length;
for (var i = 0; i < length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
if (persistentLevel.Actors[i].Load() is not { } actor || actor.ExportType is "LODActor")
continue;
Services.ApplicationService.ApplicationView.Status.UpdateStatusLabel($"{original.Name} ... {i}/{length}");
WorldCamera(actor);
WorldLight(actor);
WorldMesh(actor, transform);
AdditionalWorlds(actor, transform.Matrix, cancellationToken);
}
Services.ApplicationService.ApplicationView.Status.UpdateStatusLabel($"{original.Name} ... {length}/{length}");
}
private void LoadJunoWorld(CancellationToken cancellationToken, UBlueprintGeneratedClass original, Transform transform)
{
CameraOp.Setup(new FBox(FVector.ZeroVector, new FVector(0, 10, 10)));
var length = 0;
FPackageIndex[] allNodes = [];
IPropertyHolder[] records = [];
if (original.TryGetValue(out FPackageIndex simpleConstructionScript, "SimpleConstructionScript") &&
simpleConstructionScript.TryLoad(out var scs) && scs.TryGetValue(out allNodes, "AllNodes"))
length = allNodes.Length;
else if (original.TryGetValue(out FPackageIndex inheritableComponentHandler, "InheritableComponentHandler") &&
inheritableComponentHandler.TryLoad(out var ich) && ich.TryGetValue(out records, "Records"))
length = records.Length;
for (var i = 0; i < length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
IPropertyHolder actor;
if (allNodes is {Length: > 0} && allNodes[i].TryLoad(out UObject node))
{
actor = node;
}
else if (records is {Length: > 0})
{
actor = records[i];
}
else continue;
Services.ApplicationService.ApplicationView.Status.UpdateStatusLabel($"{original.Name} ... {i}/{length}");
WorldMesh(actor, transform, true);
}
Services.ApplicationService.ApplicationView.Status.UpdateStatusLabel($"{original.Name} ... {length}/{length}");
if (Options.Models.Count == 1)
{
var (guid, model) = Options.Models.First();
Options.SelectModel(guid);
CameraOp.Setup(model.Box);
_saveCameraMode = true;
}
}
private void WorldCamera(UObject actor)
{
if (actor.ExportType != "LevelBounds" || !actor.TryGetValue(out FPackageIndex boxComponent, "BoxComponent") ||
boxComponent.Load() is not { } boxObject) return;
var direction = boxObject.GetOrDefault("RelativeLocation", FVector.ZeroVector) * Constants.SCALE_DOWN_RATIO;
var position = boxObject.GetOrDefault("RelativeScale3D", FVector.OneVector) / 2f * Constants.SCALE_DOWN_RATIO;
CameraOp.Setup(new FBox(direction, position));
}
private void WorldLight(UObject actor)
{
if (!actor.TryGetValue(out FPackageIndex lightComponent, "LightComponent") ||
lightComponent.Load() is not { } lightObject) return;
switch (actor.ExportType)
{
case "PointLight":
Options.Lights.Add(new PointLight(Options.Icons["pointlight"], lightObject));
break;
case "SpotLight":
Options.Lights.Add(new SpotLight(Options.Icons["spotlight"], lightObject));
break;
case "RectLight":
case "SkyLight":
case "DirectionalLight":
break;
}
}
private void WorldMesh(IPropertyHolder actor, Transform transform, bool forceShow = false)
{
if (actor.TryGetValue(out FPackageIndex[] instanceComponents, "InstanceComponents"))
{
foreach (var component in instanceComponents)
{
if (!component.TryLoad(out UStaticMeshComponent staticMeshComp) ||
!staticMeshComp.GetStaticMesh().TryLoad(out UStaticMesh m) || m.Materials.Length < 1)
continue;
var relation = CalculateTransform(staticMeshComp, transform);
if (staticMeshComp is UInstancedStaticMeshComponent { PerInstanceSMData.Length: > 0 } instancedStaticMeshComp)
{
foreach (var perInstanceData in instancedStaticMeshComp.PerInstanceSMData)
{
ProcessMesh(actor, instancedStaticMeshComp, m, new Transform
{
Relation = relation.Matrix,
Position = perInstanceData.TransformData.Translation * Constants.SCALE_DOWN_RATIO,
Rotation = perInstanceData.TransformData.Rotation,
Scale = perInstanceData.TransformData.Scale3D
});
}
}
else ProcessMesh(actor, staticMeshComp, m, relation);
}
}
else if (actor.TryGetValue(out FPackageIndex componentTemplate, "ComponentTemplate") &&
componentTemplate.TryLoad(out UObject compTemplate))
{
UGeometryCollection geometryCollection = null;
if (!compTemplate.TryGetValue(out UStaticMesh m, "StaticMesh") &&
compTemplate.TryGetValue(out FPackageIndex restCollection, "RestCollection") &&
restCollection.TryLoad(out geometryCollection) && geometryCollection.RootProxyData is { ProxyMeshes.Length: > 0 } rootProxyData)
{
rootProxyData.ProxyMeshes[0].TryLoad(out m);
}
if (m is { Materials.Length: > 0 })
{
OverrideJunoVertexColors(m, geometryCollection);
ProcessMesh(actor, compTemplate, m, CalculateTransform(compTemplate, transform), forceShow);
}
}
else if (actor.TryGetValue(out FPackageIndex staticMeshComponent, "StaticMeshComponent", "ComponentTemplate", "StaticMesh", "Mesh", "LightMesh", "SplineMesh") &&
staticMeshComponent.TryLoad(out UStaticMeshComponent staticMeshComp) &&
staticMeshComp.GetStaticMesh().TryLoad(out UStaticMesh m) && m.Materials.Length > 0)
{
ProcessMesh(actor, staticMeshComp, m, CalculateTransform(staticMeshComp, transform));
}
}
private void ProcessMesh(IPropertyHolder actor, UStaticMeshComponent staticMeshComp, UStaticMesh m, Transform transform)
{
OverrideVertexColors(staticMeshComp, m);
ProcessMesh(actor, staticMeshComp, m, transform, false);
}
private void ProcessMesh(IPropertyHolder actor, UObject staticMeshComp, UStaticMesh m, Transform transform, bool forceShow)
{
var bSpline = staticMeshComp is USplineMeshComponent;
var guid = m.LightingGuid;
if (Options.TryGetModel(guid, out var model))
{
model.AddInstance(transform);
if (bSpline && model is SplineModel splineModel)
splineModel.AddComponent((USplineMeshComponent)staticMeshComp);
}
else if (m.TryConvert(out var mesh, EMeshQuality.Highest, UserSettings.Default.NaniteMeshExportFormat))
{
model = bSpline ? new SplineModel(m, mesh, (USplineMeshComponent)staticMeshComp, transform) : new StaticModel(m, mesh, transform);
model.IsTwoSided = actor.GetOrDefault("bMirrored", staticMeshComp.GetOrDefault("bDisallowMeshPaintPerInstance", model.IsTwoSided));
if (actor.TryGetAllValues(out FPackageIndex[] textureData, "TextureData"))
{
var material = model.Materials.FirstOrDefault();
if (material is { IsUsed: true })
{
for (int j = 0; j < textureData.Length; j++)
{
if (textureData[j]?.Load() is not { } textureDataIdx)
continue;
if (textureDataIdx.TryGetValue(out FPackageIndex overrideMaterial, "OverrideMaterial") &&
overrideMaterial.TryLoad(out var oMaterial) && oMaterial is UMaterialInterface oUnrealMaterial)
material.SwapMaterial(oUnrealMaterial);
WorldTextureData(material, textureDataIdx, "Diffuse", j switch
{
0 => "Diffuse",
> 0 => $"Diffuse_Texture_{j + 1}",
_ => CMaterialParams2.FallbackDiffuse
});
WorldTextureData(material, textureDataIdx, "Normal", j switch
{
0 => "Normals",
> 0 => $"Normals_Texture_{j + 1}",
_ => CMaterialParams2.FallbackNormals
});
WorldTextureData(material, textureDataIdx, "Specular", j switch
{
0 => "SpecularMasks",
> 0 => $"SpecularMasks_{j + 1}",
_ => CMaterialParams2.FallbackNormals
});
}
}
}
if (staticMeshComp.TryGetValue(out FPackageIndex[] overrideMaterials, "OverrideMaterials"))
{
for (var j = 0; j < overrideMaterials.Length && j < model.Sections.Length; j++)
{
var matIndex = model.Sections[j].MaterialIndex;
if (matIndex < 0 || matIndex >= model.Materials.Length || matIndex >= overrideMaterials.Length ||
overrideMaterials[matIndex].Load() is not UMaterialInterface unrealMaterial) continue;
model.Materials[matIndex].SwapMaterial(unrealMaterial);
}
}
if (forceShow)
{
foreach (var section in model.Sections)
{
section.Show = true;
}
}
Options.Models[guid] = model;
}
if (actor.TryGetValue(out FPackageIndex treasureLight, "PointLight", "TreasureLight") &&
treasureLight.TryLoad(out var pl1) && pl1.Template.TryLoad(out var pl2))
{
Options.Lights.Add(new PointLight(guid, Options.Icons["pointlight"], pl1, pl2, transform));
}
if (actor.TryGetValue(out FPackageIndex spotLight, "SpotLight") &&
spotLight.TryLoad(out var sl1) && sl1.Template.TryLoad(out var sl2))
{
Options.Lights.Add(new SpotLight(guid, Options.Icons["spotlight"], sl1, sl2, transform));
}
}
private Transform CalculateTransform(IPropertyHolder staticMeshComp, Transform relation)
{
if (staticMeshComp.TryGetValue(out FPackageIndex ap, "AttachParent") && ap.TryLoad(out UObject component))
{
relation = CalculateTransform(component, relation);
}
return new Transform
{
Relation = relation.Matrix,
Position = staticMeshComp.GetOrDefault("RelativeLocation", FVector.ZeroVector) * Constants.SCALE_DOWN_RATIO,
Rotation = staticMeshComp.GetOrDefault("RelativeRotation", FRotator.ZeroRotator).Quaternion(),
Scale = staticMeshComp.GetOrDefault("RelativeScale3D", FVector.OneVector)
};
}
private void OverrideJunoVertexColors(UStaticMesh staticMesh, UGeometryCollection geometryCollection = null)
{
if (staticMesh.RenderData is not { LODs.Length: > 0 } || staticMesh.RenderData.LODs[0].ColorVertexBuffer == null)
return;
var dico = new Dictionary<byte, FColor>();
if (geometryCollection?.Materials is not { Length: > 0 })
{
var distinctReds = new HashSet<byte>();
for (int i = 0; i < staticMesh.RenderData.LODs[0].ColorVertexBuffer.Data.Length; i++)
{
ref var vertexColor = ref staticMesh.RenderData.LODs[0].ColorVertexBuffer.Data[i];
var indexAsByte = vertexColor.R;
if (indexAsByte == 255) indexAsByte = vertexColor.A;
distinctReds.Add(indexAsByte);
}
foreach (var indexAsByte in distinctReds)
{
var path = string.Concat("/JunoAtomAssets/Materials/MI_LegoStandard_", indexAsByte, ".MI_LegoStandard_", indexAsByte);
if (!Utils.TryLoadObject(path, out UMaterialInterface unrealMaterial))
continue;
var parameters = new CMaterialParams2();
unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly);
if (!parameters.TryGetLinearColor(out var color, "Color"))
color = FLinearColor.Gray;
dico[indexAsByte] = color.ToFColor(true);
}
}
else foreach (var material in geometryCollection.Materials)
{
if (!material.TryLoad(out UMaterialInterface unrealMaterial)) continue;
var parameters = new CMaterialParams2();
unrealMaterial.GetParams(parameters, EMaterialDepth.TopLayerOnly);
if (!byte.TryParse(material.Name.SubstringAfterLast("_"), out var indexAsByte))
indexAsByte = byte.MaxValue;
if (!parameters.TryGetLinearColor(out var color, "Color"))
color = FLinearColor.Gray;
dico[indexAsByte] = color.ToFColor(true);
}
for (int i = 0; i < staticMesh.RenderData.LODs[0].ColorVertexBuffer.Data.Length; i++)
{
ref var vertexColor = ref staticMesh.RenderData.LODs[0].ColorVertexBuffer.Data[i];
vertexColor = dico.TryGetValue(vertexColor.R, out var color) ? color : FColor.Gray;
}
}
private void OverrideVertexColors(UStaticMeshComponent staticMeshComp, UStaticMesh staticMesh)
{
if (staticMeshComp.LODData is not { Length: > 0 } || staticMesh.RenderData is not { LODs.Length: > 0 })
return;
for (var lod = 0; lod < staticMeshComp.LODData.Length; lod++)
{
var vertexColors = staticMeshComp.LODData[lod].OverrideVertexColors;
if (vertexColors == null) continue;
staticMesh.RenderData.LODs[lod].ColorVertexBuffer = vertexColors;
}
}
private void WorldTextureData(Material material, UObject textureData, string name, string key)
{
if (textureData.TryGetValue(out FPackageIndex package, name) && package.Load() is UTexture2D texture)
material.Parameters.Textures[key] = texture;
}
private void AdditionalWorlds(UObject actor, Matrix4x4 relation, CancellationToken cancellationToken)
{
if (!actor.TryGetValue(out FSoftObjectPath[] additionalWorlds, "AdditionalWorlds") ||
!actor.TryGetValue(out FPackageIndex staticMeshComponent, "StaticMeshComponent", "Mesh") ||
staticMeshComponent.Load() is not { } staticMeshComp)
return;
var transform = new Transform
{
Relation = relation,
Position = staticMeshComp.GetOrDefault("RelativeLocation", FVector.ZeroVector) * Constants.SCALE_DOWN_RATIO,
Rotation = staticMeshComp.GetOrDefault("RelativeRotation", FRotator.ZeroRotator).Quaternion()
};
for (int j = 0; j < additionalWorlds.Length; j++)
if (Utils.TryLoadObject(additionalWorlds[j].AssetPathName.Text, out UWorld w))
LoadWorld(cancellationToken, w, transform);
}
public void WindowResized(int width, int height)
{
CameraOp.AspectRatio = width / (float) height;
Picking.WindowResized(width, height);
}
public void Save()
{
Options.ResetModelsLightsAnimations();
Options.SelectModel(Guid.Empty);
Options.SwapMaterial(false);
Options.AnimateMesh(false);
if (_saveCameraMode) UserSettings.Default.CameraMode = CameraOp.Mode;
UserSettings.Default.ShowSkybox = ShowSkybox;
UserSettings.Default.ShowGrid = ShowGrid;
UserSettings.Default.AnimateWithRotationOnly = AnimateWithRotationOnly;
}
public void Dispose()
{
_skybox?.Dispose();
_grid?.Dispose();
_shader?.Dispose();
_outline?.Dispose();
_light?.Dispose();
_bone?.Dispose();
_collision?.Dispose();
Picking?.Dispose();
Options?.Dispose();
}
}

View File

@@ -1,413 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CUE4Parse.UE4.Assets.Exports.Material;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Extensions;
using FModel.Settings;
using FModel.Views.Snooper.Models;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Shading;
public class Material : IDisposable
{
private int _handle;
public readonly CMaterialParams2 Parameters;
public string Name;
public string Path;
public int SelectedChannel;
public int SelectedTexture;
public bool IsUsed;
public Texture[] Diffuse;
public Texture[] Normals;
public Texture[] SpecularMasks;
public Texture[] Emissive;
public Vector4[] DiffuseColor;
public Vector4[] EmissiveColor;
public Vector4 EmissiveRegion;
public AoParams Ao;
public bool HasAo;
public float RoughnessMin = 0f;
public float RoughnessMax = 1f;
public float EmissiveMult = 1f;
public Material()
{
Parameters = new CMaterialParams2();
Name = "";
Path = "None";
IsUsed = false;
Diffuse = Array.Empty<Texture>();
Normals = Array.Empty<Texture>();
SpecularMasks = Array.Empty<Texture>();
Emissive = Array.Empty<Texture>();
DiffuseColor = Array.Empty<Vector4>();
EmissiveColor = Array.Empty<Vector4>();
EmissiveRegion = new Vector4(0, 0, 1, 1);
}
public Material(UMaterialInterface unrealMaterial) : this()
{
SwapMaterial(unrealMaterial);
}
public void SwapMaterial(UMaterialInterface unrealMaterial)
{
Name = unrealMaterial.Name;
Path = unrealMaterial.GetPathName();
unrealMaterial.GetParams(Parameters, UserSettings.Default.MaterialExportFormat);
}
public void Setup(Options options, int uvCount)
{
_handle = GL.CreateProgram();
if (uvCount < 1 || Parameters.IsNull)
{
Diffuse = [new Texture(FLinearColor.Gray)];
Normals = [new Texture(new FLinearColor(0.5f, 0.5f, 1f, 1f))];
SpecularMasks = [new Texture(new FLinearColor(1f, 0.5f, 0.5f, 1f))];
Emissive = new Texture[1];
DiffuseColor = FillColors(1, Diffuse, CMaterialParams2.DiffuseColors, Vector4.One);
EmissiveColor = [Vector4.One];
}
else
{
{ // textures
Diffuse = FillTextures(options, uvCount, Parameters.HasTopDiffuse, CMaterialParams2.Diffuse, CMaterialParams2.FallbackDiffuse, true);
Normals = FillTextures(options, uvCount, Parameters.HasTopNormals, CMaterialParams2.Normals, CMaterialParams2.FallbackNormals);
SpecularMasks = FillTextures(options, uvCount, Parameters.HasTopSpecularMasks, CMaterialParams2.SpecularMasks, CMaterialParams2.FallbackSpecularMasks);
Emissive = FillTextures(options, uvCount, true, CMaterialParams2.Emissive, CMaterialParams2.FallbackEmissive);
}
{ // colors
DiffuseColor = FillColors(uvCount, Diffuse, CMaterialParams2.DiffuseColors, Vector4.One);
EmissiveColor = FillColors(uvCount, Emissive, CMaterialParams2.EmissiveColors, Vector4.One);
}
{ // ambient occlusion + color boost
if (Parameters.TryGetTexture2d(out var original, "M", "AEM", "AO") &&
!original.Name.Equals("T_BlackMask") && options.TryGetTexture(original, false, out var transformed))
{
HasAo = true;
Ao = new AoParams { Texture = transformed };
if (Parameters.TryGetLinearColor(out var l, "Skin Boost Color And Exponent"))
{
Ao.HasColorBoost = true;
Ao.ColorBoost = new Boost { Color = new Vector3(l.R, l.G, l.B), Exponent = l.A };
}
}
if (Parameters.TryGetScalar(out var roughnessMin, "RoughnessMin", "SpecRoughnessMin"))
RoughnessMin = roughnessMin;
if (Parameters.TryGetScalar(out var roughnessMax, "RoughnessMax", "SpecRoughnessMax"))
RoughnessMax = roughnessMax;
if (Parameters.TryGetScalar(out var roughness, "Rough", "Roughness", "Ro Multiplier", "RO_mul", "Roughness_Mult"))
{
var d = roughness / 2;
RoughnessMin = roughness - d;
RoughnessMax = roughness + d;
}
if (!options.SkipEmissive())
{
if (Parameters.TryGetScalar(out var emissiveMultScalar, "emissive mult", "Emissive_Mult", "EmissiveIntensity", "EmissionIntensity"))
EmissiveMult = emissiveMultScalar;
else if (Parameters.TryGetLinearColor(out var emissiveMultColor, "Emissive Multiplier", "EmissiveMultiplier"))
EmissiveMult = emissiveMultColor.R;
}
else
EmissiveMult = 0f;
if (Parameters.TryGetLinearColor(out var EmissiveUVs,
"EmissiveUVs_RG_UpperLeftCorner_BA_LowerRightCorner",
"Emissive Texture UVs RG_TopLeft BA_BottomRight",
"Emissive 2 UV Positioning (RG)UpperLeft (BA)LowerRight",
"EmissiveUVPositioning (RG)UpperLeft (BA)LowerRight",
"Emissive_CH", "EmissiveColor4LM", "Emissive Sphere Center"))
EmissiveRegion = new Vector4(EmissiveUVs.R, EmissiveUVs.G, EmissiveUVs.B, EmissiveUVs.A);
if ((Parameters.TryGetSwitch(out var swizzleRoughnessToGreen, "SwizzleRoughnessToGreen") && swizzleRoughnessToGreen) ||
Parameters.Textures.ContainsKey("SRM"))
{
foreach (var specMask in SpecularMasks)
{
specMask.SwizzleMask = new []
{
(int) PixelFormat.Red,
(int) PixelFormat.Blue,
(int) PixelFormat.Green,
(int) PixelFormat.Alpha
};
specMask.Swizzle();
}
}
}
}
}
/// <param name="options">just the cache object</param>
/// <param name="uvCount">number of item in the array</param>
/// <param name="top">has at least 1 clearly defined texture, else will go straight to fallback</param>
/// <param name="triggers">list of texture parameter names by uv channel</param>
/// <param name="fallback">fallback texture name to use if no top texture found</param>
/// <param name="first">if no top texture, no fallback texture, then use the first texture found</param>
private Texture[] FillTextures(Options options, int uvCount, bool top, string[][] triggers, string fallback, bool first = false)
{
UTexture original;
Texture transformed;
var fix = fallback == CMaterialParams2.FallbackSpecularMasks;
var textures = new Texture[uvCount];
if (top)
{
for (int i = 0; i < textures.Length; i++)
{
if (Parameters.TryGetTexture2d(out original, triggers[i]) && options.TryGetTexture(original, fix, out transformed))
textures[i] = transformed;
else if (i > 0 && textures[i - 1] != null)
textures[i] = textures[i - 1];
}
}
else if (Parameters.TryGetTexture2d(out original, fallback) && options.TryGetTexture(original, fix, out transformed))
{
for (int i = 0; i < textures.Length; i++)
textures[i] = transformed;
}
else if (first && Parameters.TryGetFirstTexture2d(out original) && options.TryGetTexture(original, fix, out transformed))
{
for (int i = 0; i < textures.Length; i++)
textures[i] = transformed;
}
return textures;
}
/// <param name="uvCount">number of item in the array</param>
/// <param name="textures">reference array</param>
/// <param name="triggers">list of color parameter names by uv channel</param>
/// <param name="fallback">fallback color to use if no trigger was found</param>
private Vector4[] FillColors(int uvCount, Texture[] textures, string[][] triggers, Vector4 fallback)
{
var colors = new Vector4[uvCount];
for (int i = 0; i < colors.Length; i++)
{
if (textures[i] == null) continue;
if (Parameters.TryGetLinearColor(out var color, triggers[i]) && color is { A: > 0 })
{
colors[i] = new Vector4(color.R, color.G, color.B, color.A);
}
else colors[i] = fallback;
}
return colors;
}
public void Render(Shader shader)
{
var unit = 0;
for (var i = 0; i < Diffuse.Length; i++)
{
shader.SetUniform($"uParameters.Diffuse[{i}].Sampler", unit);
shader.SetUniform($"uParameters.Diffuse[{i}].Color", DiffuseColor[i]);
Diffuse[i]?.Bind(TextureUnit.Texture0 + unit++);
}
for (var i = 0; i < Normals.Length; i++)
{
shader.SetUniform($"uParameters.Normals[{i}].Sampler", unit);
Normals[i]?.Bind(TextureUnit.Texture0 + unit++);
}
for (var i = 0; i < SpecularMasks.Length; i++)
{
shader.SetUniform($"uParameters.SpecularMasks[{i}].Sampler", unit);
SpecularMasks[i]?.Bind(TextureUnit.Texture0 + unit++);
}
for (var i = 0; i < Emissive.Length; i++)
{
shader.SetUniform($"uParameters.Emissive[{i}].Sampler", unit);
shader.SetUniform($"uParameters.Emissive[{i}].Color", EmissiveColor[i]);
Emissive[i]?.Bind(TextureUnit.Texture0 + unit++);
}
Ao.Texture?.Bind(TextureUnit.Texture31);
shader.SetUniform("uParameters.Ao.Sampler", 31);
shader.SetUniform("uParameters.Ao.HasColorBoost", Ao.HasColorBoost);
shader.SetUniform("uParameters.Ao.ColorBoost.Color", Ao.ColorBoost.Color);
shader.SetUniform("uParameters.Ao.ColorBoost.Exponent", Ao.ColorBoost.Exponent);
shader.SetUniform("uParameters.HasAo", HasAo);
shader.SetUniform("uParameters.EmissiveRegion", EmissiveRegion);
shader.SetUniform("uParameters.RoughnessMin", RoughnessMin);
shader.SetUniform("uParameters.RoughnessMax", RoughnessMax);
shader.SetUniform("uParameters.EmissiveMult", EmissiveMult);
}
private const string _mult = "x %.2f";
private const float _step = 0.01f;
private const float _zero = 0.000001f; // doesn't actually work if _infinite is used as max value /shrug
private const float _infinite = 0.0f;
private const ImGuiSliderFlags _clamp = ImGuiSliderFlags.AlwaysClamp;
public void ImGuiParameters()
{
if (ImGui.BeginTable("parameters", 2))
{
var id = 1;
SnimGui.Layout("Roughness Min");ImGui.PushID(id++);
ImGui.DragFloat("", ref RoughnessMin, _step, _zero, 1.0f, _mult, _clamp);
ImGui.PopID();SnimGui.Layout("Roughness Max");ImGui.PushID(id++);
ImGui.DragFloat("", ref RoughnessMax, _step, _zero, 1.0f, _mult, _clamp);
ImGui.PopID();SnimGui.Layout("Emissive Multiplier");ImGui.PushID(id++);
ImGui.DragFloat("", ref EmissiveMult, _step, _zero, _infinite, _mult, _clamp);
ImGui.PopID();
if (HasAo && Ao.HasColorBoost)
{
SnimGui.Layout("Color Boost");ImGui.PushID(id++);
ImGui.ColorEdit3("", ref Ao.ColorBoost.Color);ImGui.PopID();
SnimGui.Layout("Color Boost Exponent");ImGui.PushID(id++);
ImGui.DragFloat("", ref Ao.ColorBoost.Exponent, _step, _zero, _infinite, _mult, _clamp);
ImGui.PopID();
}
ImGui.EndTable();
}
}
public void ImGuiBaseProperties(string id)
{
if (ImGui.BeginTable(id, 2, ImGuiTableFlags.SizingStretchProp))
{
Layout("Blend", Parameters.BlendMode.GetDescription(), true, true);
Layout("Shading", Parameters.ShadingModel.GetDescription(), true, true);
ImGui.EndTable();
}
}
public void ImGuiDictionaries<T>(string id, Dictionary<string, T> dictionary, bool center = false, bool wrap = false)
{
if (ImGui.BeginTable(id, 2))
{
foreach ((string key, T value) in dictionary.Reverse())
{
Layout(key, value, center, wrap);
}
ImGui.EndTable();
}
}
public void ImGuiColors(Dictionary<string, FLinearColor> colors)
{
foreach ((string key, FLinearColor value) in colors.Reverse())
{
ImGui.ColorButton(key, new Vector4(value.R, value.G, value.B, value.A), ImGuiColorEditFlags.None, new Vector2(16));
ImGui.SameLine();ImGui.Text(key);SnimGui.TooltipCopy(key);
}
}
public bool ImGuiTextures(Dictionary<string, Texture> icons, IRenderableModel model)
{
if (ImGui.BeginTable("material_textures", 2))
{
SnimGui.Layout("Channel");ImGui.PushID(1); ImGui.BeginDisabled(model.UvCount < 2);
ImGui.DragInt("", ref SelectedChannel, _step, 0, model.UvCount - 1, "UV %i", ImGuiSliderFlags.AlwaysClamp);
ImGui.EndDisabled();ImGui.PopID();SnimGui.Layout("Type");ImGui.PushID(2);
ImGui.Combo("texture_type", ref SelectedTexture, "Diffuse\0Normals\0Specular\0Ambient Occlusion\0Emissive\0");
ImGui.PopID();
switch (SelectedTexture)
{
case 0 when DiffuseColor.Length > 0:
SnimGui.Layout("Color");ImGui.PushID(3);
ImGui.ColorEdit4("", ref DiffuseColor[SelectedChannel], ImGuiColorEditFlags.NoAlpha);
ImGui.PopID();
break;
case 4 when EmissiveColor.Length > 0:
SnimGui.Layout("Color");ImGui.PushID(3);
ImGui.ColorEdit4("", ref EmissiveColor[SelectedChannel], ImGuiColorEditFlags.NoAlpha);
ImGui.PopID();SnimGui.Layout("Region");ImGui.PushID(4);
ImGui.DragFloat4("", ref EmissiveRegion, _step, _zero, 1.0f, "%.2f", _clamp);
ImGui.PopID();
break;
}
ImGui.EndTable();
}
var texture = GetSelectedTexture() ?? icons["noimage"];
ImGui.Image(texture.GetPointer(),
new Vector2(ImGui.GetContentRegionAvail().X - ImGui.GetScrollX()),
Vector2.Zero, Vector2.One);
return ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left);
}
public Texture GetSelectedTexture()
{
return SelectedTexture switch
{
0 when Diffuse.Length > 0 => Diffuse[SelectedChannel],
1 when Normals.Length > 0 => Normals[SelectedChannel],
2 when SpecularMasks.Length > 0 => SpecularMasks[SelectedChannel],
3 => Ao.Texture,
4 when Emissive.Length > 0 => Emissive[SelectedChannel],
_ => null
};
}
private void Layout<T>(string key, T value, bool center = false, bool wrap = false)
{
SnimGui.Layout(key, true);
var text = $"{value:N}";
if (center) ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() - ImGui.CalcTextSize(text).X) / 2);
if (wrap) ImGui.TextWrapped(text); else ImGui.Text(text);
SnimGui.TooltipCopy(text);
}
public void Dispose()
{
for (int i = 0; i < Diffuse.Length; i++)
{
Diffuse[i]?.Dispose();
}
for (int i = 0; i < Normals.Length; i++)
{
Normals[i]?.Dispose();
}
for (int i = 0; i < SpecularMasks.Length; i++)
{
SpecularMasks[i]?.Dispose();
}
for (int i = 0; i < Emissive.Length; i++)
{
Emissive[i]?.Dispose();
}
Ao.Texture?.Dispose();
GL.DeleteProgram(_handle);
}
}
public struct AoParams
{
public Texture Texture;
public Boost ColorBoost;
public bool HasColorBoost;
}
public struct Boost
{
public Vector3 Color;
public float Exponent;
}

View File

@@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Reflection;
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Shading;
public class Shader : IDisposable
{
private readonly int _handle;
private readonly int _vHandle;
private readonly int _fHandle;
private readonly Dictionary<string, int> _uniformsLocation = new ();
public Shader() : this("default") {}
public Shader(string name1, string name2 = null)
{
_handle = GL.CreateProgram();
_vHandle = LoadShader(ShaderType.VertexShader, $"{name1}.vert");
_fHandle = LoadShader(ShaderType.FragmentShader, $"{name2 ?? name1}.frag");
Attach();
}
private void Attach()
{
GL.AttachShader(_handle, _vHandle);
GL.AttachShader(_handle, _fHandle);
GL.LinkProgram(_handle);
GL.GetProgram(_handle, GetProgramParameterName.LinkStatus, out var status);
if (status == 0)
{
throw new Exception($"Program failed to link with error: {GL.GetProgramInfoLog(_handle)}");
}
}
private void Detach()
{
GL.DetachShader(_handle, _vHandle);
GL.DetachShader(_handle, _fHandle);
GL.DeleteShader(_vHandle);
GL.DeleteShader(_fHandle);
}
private int LoadShader(ShaderType type, string file)
{
var executingAssembly = Assembly.GetExecutingAssembly();
var executingAssemblyName = executingAssembly.GetName().Name;
using var stream = executingAssembly.GetManifestResourceStream($"{executingAssemblyName}.Resources.{file}");
using var reader = new StreamReader(stream);
var handle = GL.CreateShader(type);
var content = reader.ReadToEnd();
if (file.Equals("default.frag") && GL.GetInteger(GetPName.MaxTextureCoords) == 0)
content = content.Replace("#define MAX_UV_COUNT 8", "#define MAX_UV_COUNT 1");
if (type == ShaderType.VertexShader && Array.IndexOf(["default.vert", "outline.vert", "picking.vert"], file) > -1)
{
using var splineStream = executingAssembly.GetManifestResourceStream($"{executingAssemblyName}.Resources.spline.vert");
using var splineReader = new StreamReader(splineStream);
content = splineReader.ReadToEnd() + Environment.NewLine + content.Replace("#version 460 core", "");
}
GL.ShaderSource(handle, content);
GL.CompileShader(handle);
string infoLog = GL.GetShaderInfoLog(handle);
if (!string.IsNullOrWhiteSpace(infoLog))
{
throw new Exception($"Error compiling shader of type {type}, failed with error {infoLog}");
}
return handle;
}
public void Use()
{
GL.UseProgram(_handle);
}
public void Render(Matrix4x4 viewMatrix, Vector3 viewPos, Matrix4x4 projMatrix)
{
Render(viewMatrix, projMatrix);
SetUniform("uViewPos", viewPos);
}
public void Render(Matrix4x4 viewMatrix, Matrix4x4 projMatrix)
{
Use();
SetUniform("uView", viewMatrix);
SetUniform("uProjection", projMatrix);
}
public void SetUniform(string name, int value)
{
GL.Uniform1(GetUniformLocation(name), value);
}
public unsafe void SetUniform(string name, Matrix4x4 value) => UniformMatrix4(name, (float*) &value);
public unsafe void UniformMatrix4(string name, float* value)
{
GL.UniformMatrix4(GetUniformLocation(name), 1, false, value);
}
public void SetUniform(string name, bool value) => SetUniform(name, Convert.ToUInt32(value));
public void SetUniform(string name, uint value)
{
GL.Uniform1(GetUniformLocation(name), value);
}
public void SetUniform(string name, float value)
{
GL.Uniform1(GetUniformLocation(name), value);
}
public void SetUniform(string name, Vector2 value) => SetUniform3(name, value.X, value.Y);
public void SetUniform3(string name, float x, float y)
{
GL.Uniform2(GetUniformLocation(name), x, y);
}
public void SetUniform(string name, Vector3 value) => SetUniform3(name, value.X, value.Y, value.Z);
public void SetUniform3(string name, float x, float y, float z)
{
GL.Uniform3(GetUniformLocation(name), x, y, z);
}
public void SetUniform(string name, Vector4 value) => SetUniform4(name, value.X, value.Y, value.Z, value.W);
public void SetUniform4(string name, float x, float y, float z, float w)
{
GL.Uniform4(GetUniformLocation(name), x, y, z, w);
}
private int GetUniformLocation(string name)
{
if (!_uniformsLocation.TryGetValue(name, out int location))
{
location = GL.GetUniformLocation(_handle, name);
_uniformsLocation.Add(name, location);
if (location == -1)
{
throw new Exception($"{name} uniform not found on shader.");
}
}
return location;
}
public void Dispose()
{
Detach();
GL.DeleteProgram(_handle);
}
}

View File

@@ -1,316 +0,0 @@
using System;
using System.Numerics;
using System.Windows;
using CUE4Parse_Conversion.Textures;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Objects.Core.Math;
using CUE4Parse.UE4.Objects.Core.Misc;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SkiaSharp;
namespace FModel.Views.Snooper.Shading;
public class Texture : IDisposable
{
private readonly int _handle;
private readonly TextureType _type;
private readonly TextureTarget _target;
public readonly string Type;
public readonly FGuid Guid;
public readonly string Name;
public readonly string Path;
public readonly EPixelFormat Format;
public readonly uint ImportedWidth;
public readonly uint ImportedHeight;
public int Width;
public int Height;
private const int DisabledChannel = (int)BlendingFactor.Zero;
private readonly bool[] _values = [true, true, true, true];
private readonly string[] _labels = ["R", "G", "B", "A"];
public int[] SwizzleMask =
[
(int) PixelFormat.Red,
(int) PixelFormat.Green,
(int) PixelFormat.Blue,
(int) PixelFormat.Alpha
];
private Texture(TextureType type)
{
_handle = GL.GenTexture();
_type = type;
_target = _type switch
{
TextureType.Cubemap => TextureTarget.TextureCubeMap,
TextureType.MsaaFramebuffer => TextureTarget.Texture2DMultisample,
_ => TextureTarget.Texture2D
};
Guid = new FGuid();
}
public Texture(uint width, uint height) : this(TextureType.MsaaFramebuffer)
{
Width = (int) width;
Height = (int) height;
Bind(TextureUnit.Texture0);
GL.TexImage2DMultisample(TextureTargetMultisample.Texture2DMultisample, Constants.SAMPLES_COUNT, PixelInternalFormat.Rgb, Width, Height, true);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, _target, _handle, 0);
}
public Texture(int width, int height) : this(TextureType.Framebuffer)
{
Width = width;
Height = height;
Bind(TextureUnit.Texture0);
GL.TexImage2D(_target, 0, PixelInternalFormat.Rgb, Width, Height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, IntPtr.Zero);
GL.TexParameter(_target, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureWrapS, (int) TextureWrapMode.ClampToEdge);
GL.TexParameter(_target, TextureParameterName.TextureWrapT, (int) TextureWrapMode.ClampToEdge);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, _target, _handle, 0);
}
public Texture(SKBitmap bitmap, UTexture texture2D) : this(TextureType.Normal)
{
Type = texture2D.ExportType;
Guid = texture2D.LightingGuid;
Name = texture2D.Name;
Path = texture2D.GetPathName();
Format = texture2D.Format;
Width = bitmap.Width;
Height = bitmap.Height;
Bind(TextureUnit.Texture0);
var internalFormat = bitmap.ColorType switch
{
SKColorType.Gray8 => PixelInternalFormat.R8,
_ => texture2D.SRGB ? PixelInternalFormat.Srgb : PixelInternalFormat.Rgb
};
var pixelFormat = bitmap.ColorType switch
{
SKColorType.Gray8 => PixelFormat.Red,
SKColorType.Bgra8888 => PixelFormat.Bgra,
_ => PixelFormat.Rgba
};
GL.TexImage2D(_target, 0, internalFormat, Width, Height, 0, pixelFormat, PixelType.UnsignedByte, bitmap.Bytes);
GL.TexParameter(_target, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(_target, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureBaseLevel, 0);
GL.TexParameter(_target, TextureParameterName.TextureMaxLevel, 8);
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
bitmap.Dispose();
}
public Texture(FLinearColor color) : this(TextureType.Normal)
{
Type = "LinearColor";
Name = color.Hex;
Width = 1;
Height = 1;
Bind(TextureUnit.Texture0);
GL.TexImage2D(_target, 0, PixelInternalFormat.Rgba, Width, Height, 0, PixelFormat.Rgba, PixelType.Float, ref color);
GL.TexParameter(_target, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(_target, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureBaseLevel, 0);
GL.TexParameter(_target, TextureParameterName.TextureMaxLevel, 8);
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
}
public Texture(string[] textures) : this(TextureType.Cubemap)
{
Bind(TextureUnit.Texture0);
for (int t = 0; t < textures.Length; t++)
{
ProcessPixels(textures[t], TextureTarget.TextureCubeMapPositiveX + t);
}
GL.TexParameter(_target, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(_target, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureWrapR, (int) TextureWrapMode.ClampToEdge);
GL.TexParameter(_target, TextureParameterName.TextureWrapS, (int) TextureWrapMode.ClampToEdge);
GL.TexParameter(_target, TextureParameterName.TextureWrapT, (int) TextureWrapMode.ClampToEdge);
GL.GenerateMipmap(GenerateMipmapTarget.TextureCubeMap);
}
public Texture(string texture) : this(TextureType.Normal)
{
Bind(TextureUnit.Texture0);
ProcessPixels(texture, _target);
GL.TexParameter(_target, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(_target, TextureParameterName.TextureWrapR, (int) TextureWrapMode.ClampToEdge);
GL.TexParameter(_target, TextureParameterName.TextureWrapS, (int) TextureWrapMode.ClampToEdge);
GL.TexParameter(_target, TextureParameterName.TextureWrapT, (int) TextureWrapMode.ClampToEdge);
}
private void ProcessPixels(string texture, TextureTarget target)
{
var info = Application.GetResourceStream(new Uri($"/FModel;component/Resources/{texture}.png", UriKind.Relative));
using var img = Image.Load<Rgba32>(info.Stream);
Width = img.Width;
Height = img.Height;
GL.TexImage2D(target, 0, PixelInternalFormat.Rgba8, Width, Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
img.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
GL.TexSubImage2D(target, 0, 0, y, accessor.Width, 1, PixelFormat.Rgba, PixelType.UnsignedByte, accessor.GetRowSpan(y).ToArray());
}
});
}
public void Bind(TextureUnit textureSlot)
{
GL.ActiveTexture(textureSlot);
Bind(_target);
}
public void Bind(TextureTarget target)
{
GL.BindTexture(target, _handle);
}
public void Bind()
{
GL.BindTexture(_target, _handle);
}
public void Swizzle()
{
Bind();
GL.TexParameter(_target, TextureParameterName.TextureSwizzleRgba, SwizzleMask);
}
public IntPtr GetPointer() => (IntPtr) _handle;
public void WindowResized(int width, int height)
{
Width = width;
Height = height;
Bind();
switch (_type)
{
case TextureType.MsaaFramebuffer:
GL.TexImage2DMultisample(TextureTargetMultisample.Texture2DMultisample, Constants.SAMPLES_COUNT, PixelInternalFormat.Rgb, Width, Height, true);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, _target, _handle, 0);
break;
case TextureType.Framebuffer:
GL.TexImage2D(_target, 0, PixelInternalFormat.Rgb, Width, Height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, IntPtr.Zero);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, _target, _handle, 0);
break;
default:
throw new NotSupportedException();
}
}
public void Dispose()
{
GL.DeleteTexture(_handle);
}
private Vector3 _scrolling = new (0.0f, 0.0f, 1.0f);
public void ImGuiTextureInspector()
{
if (ImGui.BeginTable("texture_inspector", 2, ImGuiTableFlags.SizingStretchProp))
{
SnimGui.NoFramePaddingOnY(() =>
{
SnimGui.Layout("Type");ImGui.Text($" : ({Format}) {Name}");
SnimGui.TooltipCopy("(?) Click to Copy Path", Path);
SnimGui.Layout("Guid");ImGui.Text($" : {Guid.ToString(EGuidFormats.UniqueObjectGuid)}");
SnimGui.Layout("Size");
ImGui.Text($" : {Width}x{Height}");
SnimGui.Layout("Swizzle");
for (int c = 0; c < SwizzleMask.Length; c++)
{
if (ImGui.Checkbox(_labels[c], ref _values[c]))
{
Bind();
GL.TexParameter(_target, TextureParameterName.TextureSwizzleR + c, _values[c] ? SwizzleMask[c] : DisabledChannel);
}
ImGui.SameLine();
}
ImGui.EndTable();
});
}
var io = ImGui.GetIO();
var canvasP0 = ImGui.GetCursorScreenPos();
var canvasSize = ImGui.GetContentRegionAvail();
if (canvasSize.X < 50.0f) canvasSize.X = 50.0f;
if (canvasSize.Y < 50.0f) canvasSize.Y = 50.0f;
var canvasP1 = canvasP0 + canvasSize;
var origin = new Vector2(canvasP0.X + _scrolling.X, canvasP0.Y + _scrolling.Y);
var absoluteMiddle = canvasSize / 2.0f;
ImGui.InvisibleButton("texture_inspector_canvas", canvasSize, ImGuiButtonFlags.MouseButtonLeft);
if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_scrolling.X += io.MouseDelta.X;
_scrolling.Y += io.MouseDelta.Y;
}
else if (ImGui.IsItemHovered() && io.MouseWheel != 0.0f)
{
var zoomFactor = 1.0f + io.MouseWheel * 0.1f;
var mousePosCanvas = io.MousePos - origin;
_scrolling.X -= (mousePosCanvas.X - absoluteMiddle.X) * (zoomFactor - 1);
_scrolling.Y -= (mousePosCanvas.Y - absoluteMiddle.Y) * (zoomFactor - 1);
_scrolling.Z *= zoomFactor;
origin = new Vector2(canvasP0.X + _scrolling.X, canvasP0.Y + _scrolling.Y);
}
var drawList = ImGui.GetWindowDrawList();
drawList.AddRectFilled(canvasP0, canvasP1, 0xFF242424);
drawList.PushClipRect(canvasP0, canvasP1, true);
{
var sensitivity = _scrolling.Z * 25.0f;
for (float x = _scrolling.X % sensitivity; x < canvasSize.X; x += sensitivity)
drawList.AddLine(canvasP0 with { X = canvasP0.X + x }, canvasP1 with { X = canvasP0.X + x }, 0x28C8C8C8);
for (float y = _scrolling.Y % sensitivity; y < canvasSize.Y; y += sensitivity)
drawList.AddLine(canvasP0 with { Y = canvasP0.Y + y }, canvasP1 with { Y = canvasP0.Y + y }, 0x28C8C8C8);
}
drawList.PopClipRect();
drawList.PushClipRect(canvasP0, canvasP1, true);
{
var relativeMiddle = origin + absoluteMiddle;
var ratio = Math.Min(canvasSize.X / Width, canvasSize.Y / Height) * 0.95f * _scrolling.Z;
var size = new Vector2(Width, Height) * ratio / 2f;
drawList.AddImage(GetPointer(), relativeMiddle - size, relativeMiddle + size);
drawList.AddRect(relativeMiddle - size, relativeMiddle + size, 0xFFFFFFFF);
}
drawList.PopClipRect();
}
}
public enum TextureType
{
Normal,
Cubemap,
Framebuffer,
MsaaFramebuffer
}

View File

@@ -1,73 +0,0 @@
using OpenTK.Graphics.OpenGL4;
namespace FModel.Views.Snooper.Shading;
public static class TextureHelper
{
/// <summary>
/// Red : Specular (not used anymore)
/// Green : Metallic
/// Blue : Roughness
/// </summary>
public static void FixChannels(string game, Texture texture)
{
switch (game)
{
// R: Whatever (AO / S / E / ...)
// G: Roughness
// B: Metallic
case "GAMEFACE":
case "HK_PROJECT":
case "COSMICSHAKE":
case "PHOENIX":
case "ATOMICHEART":
case "MULTIVERSUS":
case "BODYCAM":
case "TSLGAME":
{
texture.SwizzleMask =
[
(int) PixelFormat.Red,
(int) PixelFormat.Blue,
(int) PixelFormat.Green,
(int) PixelFormat.Alpha
];
break;
}
// R: Metallic
// G: Roughness
// B: Whatever (AO / S / E / ...)
case "SHOOTERGAME":
case "DIVINEKNOCKOUT":
case "MOONMAN":
case "SHF":
{
texture.SwizzleMask =
[
(int) PixelFormat.Blue,
(int) PixelFormat.Red,
(int) PixelFormat.Green,
(int) PixelFormat.Alpha
];
break;
}
// R: Roughness
// G: Metallic
// B: Whatever (AO / S / E / ...)
case "CCFF7R":
case "PJ033":
case "ABINFINITE":
{
texture.SwizzleMask =
[
(int) PixelFormat.Blue,
(int) PixelFormat.Green,
(int) PixelFormat.Red,
(int) PixelFormat.Alpha
];
break;
}
}
texture.Swizzle();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,234 +0,0 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using CUE4Parse.UE4.Assets.Exports;
using FModel.Views.Snooper.Buffers;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Common.Input;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using Application = System.Windows.Application;
namespace FModel.Views.Snooper;
public class Snooper : GameWindow
{
public readonly FramebufferObject Framebuffer;
public readonly Renderer Renderer;
private readonly SnimGui _gui;
private bool _init;
public Snooper(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) : base(gwSettings, nwSettings)
{
Framebuffer = new FramebufferObject(ClientSize);
Renderer = new Renderer(ClientSize.X, ClientSize.Y);
_gui = new SnimGui(ClientSize.X, ClientSize.Y);
_init = false;
}
public bool TryLoadExport(CancellationToken cancellationToken, UObject dummy, Lazy<UObject> export)
{
Renderer.Load(cancellationToken, dummy, export);
return Renderer.Options.Models.Count > 0;
}
public unsafe void WindowShouldClose(bool value, bool clear)
{
if (clear)
{
Renderer.CameraOp.Speed = 1f;
Renderer.Save();
}
GLFW.SetWindowShouldClose(WindowPtr, value); // start / stop game loop
IsVisible = !value;
}
public unsafe void WindowShouldFreeze(bool value)
{
GLFW.SetWindowShouldClose(WindowPtr, value); // start / stop game loop
IsVisible = true;
}
public override void Run()
{
Renderer.Options.SwapMaterial(false);
Renderer.Options.AnimateMesh(false);
Application.Current.Dispatcher.Invoke(delegate
{
WindowShouldClose(false, false);
base.Run();
});
}
private unsafe void LoadWindowIcon()
{
var info = Application.GetResourceStream(new Uri("/FModel;component/Resources/engine.png", UriKind.Relative));
using var img = SixLabors.ImageSharp.Image.Load<Rgba32>(info.Stream);
var memoryGroup = img.GetPixelMemoryGroup();
Memory<byte> array = new byte[memoryGroup.TotalLength * sizeof(Rgba32)];
var block = MemoryMarshal.Cast<byte, Rgba32>(array.Span);
foreach (var memory in memoryGroup)
{
memory.Span.CopyTo(block);
block = block[memory.Length..];
}
Icon = new WindowIcon(new OpenTK.Windowing.Common.Input.Image(img.Width, img.Height, array.ToArray()));
}
protected override void OnLoad()
{
if (_init)
{
Renderer.Options.SetupModelsAndLights();
return;
}
base.OnLoad();
CenterWindow();
LoadWindowIcon();
GL.ClearColor(OpenTK.Mathematics.Color4.Black);
GL.Enable(EnableCap.Blend);
GL.Enable(EnableCap.CullFace);
GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.Multisample);
GL.Enable(EnableCap.VertexProgramPointSize);
GL.StencilOp(StencilOp.Keep, StencilOp.Replace, StencilOp.Replace);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
Framebuffer.Setup();
Renderer.Setup();
_init = true;
}
private void ClearWhatHasBeenDrawn()
{
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
}
protected override void OnRenderFrame(FrameEventArgs args)
{
base.OnRenderFrame(args);
if (!IsVisible)
return;
ClearWhatHasBeenDrawn(); // clear window background
Framebuffer.Bind(); // switch to viewport background
ClearWhatHasBeenDrawn(); // clear viewport background
Renderer.Render(); // render everything
Framebuffer.BindMsaa();
Framebuffer.Bind(0); // switch to window background
_gui.Render(this); // render UI
SwapBuffers();
}
protected override void OnUpdateFrame(FrameEventArgs e)
{
base.OnUpdateFrame(e);
if (!IsVisible)
return;
var delta = (float) e.Time;
_gui.Controller.Update(this, delta);
Renderer.Update(this, delta);
}
protected override void OnTextInput(TextInputEventArgs e)
{
base.OnTextInput(e);
if (!IsVisible)
return;
_gui.Controller.PressChar((char) e.Unicode);
}
protected override void OnResize(ResizeEventArgs e)
{
base.OnResize(e);
GL.Viewport(0, 0, e.Width, e.Height);
Framebuffer.WindowResized(e.Width, e.Height);
Renderer.WindowResized(e.Width, e.Height);
_gui.Controller.WindowResized(e.Width, e.Height);
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
WindowShouldClose(true, true);
}
[DllImport("user32.dll")]
private static extern bool EnumDisplaySettings(
string deviceName, int modeNum, ref DEVMODE devMode);
[StructLayout(LayoutKind.Sequential)]
private struct DEVMODE
{
private const int CCHDEVICENAME = 0x20;
private const int CCHFORMNAME = 0x20;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public ScreenOrientation dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
public static int GetMaxRefreshFrequency()
{
var rf = 60;
var vDevMode = new DEVMODE();
var i = 0;
while (EnumDisplaySettings(null, i, ref vDevMode))
{
i++;
rf = Math.Max(rf, vDevMode.dmDisplayFrequency);
}
return rf;
}
}

View File

@@ -1,103 +0,0 @@
using System.Numerics;
using CUE4Parse.UE4.Objects.Core.Math;
using ImGuiNET;
namespace FModel.Views.Snooper;
public class Transform
{
public static Transform Identity
{
get => new ();
}
public Matrix4x4 Relation = Matrix4x4.Identity;
public FVector Position = FVector.ZeroVector;
public FQuat Rotation = FQuat.Identity;
public FVector Scale = FVector.OneVector;
private Matrix4x4? _saved;
public Matrix4x4 LocalMatrix => Matrix4x4.CreateScale(Scale.X, Scale.Z, Scale.Y) *
Matrix4x4.CreateFromQuaternion(Quaternion.Normalize(new Quaternion(Rotation.X, Rotation.Z, Rotation.Y, -Rotation.W))) *
Matrix4x4.CreateTranslation(Position.X, Position.Z, Position.Y);
public Matrix4x4 Matrix => LocalMatrix * Relation;
public void Save()
{
_saved = LocalMatrix;
}
public void ModifyLocal(Matrix4x4 matrix)
{
Matrix4x4.Decompose(matrix, out var scale, out var rotation, out var position);
Scale.X = scale.X;
Scale.Y = scale.Z;
Scale.Z = scale.Y;
Rotation.X = rotation.X;
Rotation.Y = rotation.Z;
Rotation.Z = rotation.Y;
Rotation.W = -rotation.W;
Position.X = position.X;
Position.Z = position.Y;
Position.Y = position.Z;
}
public void Reset()
{
if (!_saved.HasValue) return;
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();
}

1
Snooper Submodule

Submodule Snooper added at b6ce504513