From 9b967341c2d05144ae6f737d97a0a88122c2331e Mon Sep 17 00:00:00 2001 From: 4sval Date: Thu, 25 Aug 2022 20:27:35 +0200 Subject: [PATCH 001/108] just ctrl s --- FModel/FModel.csproj | 3 ++ FModel/Helper.cs | 5 +++ FModel/MainWindow.xaml.cs | 5 +++ FModel/Views/Snooper/Camera.cs | 58 +++++++++++++++++++++++++++++ FModel/Views/Snooper/Viewer.cs | 67 ++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 FModel/Views/Snooper/Camera.cs create mode 100644 FModel/Views/Snooper/Viewer.cs diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index f052d827..9f143a85 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -121,6 +121,9 @@ + + + diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 1f4e55e4..93103474 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -100,4 +100,9 @@ public static class Helper var d = (Math.Abs(d1) + Math.Abs(d2) + 10) * 1.0e-15; return -d < n && d > n; } + + public static float DegreesToRadians(float degrees) + { + return MathF.PI / 180f * degrees; + } } diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index dd3e8854..f30583bd 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -11,6 +11,7 @@ using FModel.Settings; using FModel.ViewModels; using FModel.Views; using FModel.Views.Resources.Controls; +using FModel.Views.Snooper; using ICSharpCode.AvalonEdit.Editing; namespace FModel; @@ -52,6 +53,10 @@ public partial class MainWindow ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode); #endif + var v = new Viewer(); + v.Run(); + return; + switch (UserSettings.Default.AesReload) { case EAesReload.Always: diff --git a/FModel/Views/Snooper/Camera.cs b/FModel/Views/Snooper/Camera.cs new file mode 100644 index 00000000..69f71276 --- /dev/null +++ b/FModel/Views/Snooper/Camera.cs @@ -0,0 +1,58 @@ +using System; +using System.Numerics; + +namespace FModel.Views.Snooper; + +public class Camera +{ + public Vector3 Position { get; set; } + public Vector3 Front { get; set; } + + public Vector3 Up { get; private set; } + public float AspectRatio { get; set; } + + public float Yaw { get; set; } = -90f; + public float Pitch { get; set; } + + private float Zoom = 45f; + + public Camera(Vector3 position, Vector3 front, Vector3 up, float aspectRatio) + { + Position = position; + AspectRatio = aspectRatio; + Front = front; + Up = up; + } + + public 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, 45f); + } + + public void ModifyDirection(float xOffset, float yOffset) + { + Yaw += xOffset; + Pitch -= yOffset; + + //We don't want to be able to look behind us by going over our head or under our feet so make sure it stays within these bounds + Pitch = Math.Clamp(Pitch, -89f, 89f); + + var cameraDirection = Vector3.Zero; + cameraDirection.X = MathF.Cos(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); + cameraDirection.Y = MathF.Sin(Helper.DegreesToRadians(Pitch)); + cameraDirection.Z = MathF.Sin(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); + + Front = Vector3.Normalize(cameraDirection); + } + + public Matrix4x4 GetViewMatrix() + { + return Matrix4x4.CreateLookAt(Position, Position + Front, Up); + } + + public Matrix4x4 GetProjectionMatrix() + { + return Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, 0.1f, 100.0f); + } +} diff --git a/FModel/Views/Snooper/Viewer.cs b/FModel/Views/Snooper/Viewer.cs new file mode 100644 index 00000000..eced863f --- /dev/null +++ b/FModel/Views/Snooper/Viewer.cs @@ -0,0 +1,67 @@ +using System; +using System.Numerics; +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +namespace FModel.Views.Snooper; + +public class Viewer +{ + private IWindow _window; + private GL _gl; + private Camera _camera; + private IKeyboard _keyboard; + + public int Width { get; } + public int Height { get; } + + private const double _ratio = .75; + + public Viewer() + { + var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; + var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; + Width = Convert.ToInt32(x * _ratio); + Height = Convert.ToInt32(y * _ratio); + + var options = WindowOptions.Default; + options.Size = new Vector2D(Width, Height); + options.Title = "Snooper"; + _window = Window.Create(options); + + _window.Load += OnLoad; + _window.Closing += OnClose; + } + + public void Run() + { + _window.Run(); + } + + private void OnLoad() + { + var input = _window.CreateInput(); + _keyboard = input.Keyboards[0]; + _keyboard.KeyDown += KeyDown; + + _gl = GL.GetApi(_window); + + //Start a camera at position 3 on the Z axis, looking at position -1 on the Z axis + _camera = new Camera(Vector3.UnitZ * 6, Vector3.UnitZ * -1, Vector3.UnitY, Width / Height); + } + + private void OnClose() + { + + } + + private void KeyDown(IKeyboard keyboard, Key key, int arg3) + { + if (key == Key.Escape) + { + _window.Close(); + } + } +} From 2c0fee3fd362581bf599f6ee55ff074609836338 Mon Sep 17 00:00:00 2001 From: 4sval Date: Thu, 25 Aug 2022 23:38:34 +0200 Subject: [PATCH 002/108] proof of concept --- CUE4Parse | 2 +- FModel/Creator/Bases/FN/BaseCommunity.cs | 2 +- FModel/Creator/Bases/FN/BaseIcon.cs | 2 +- FModel/Creator/Bases/FN/BaseIconStats.cs | 4 +- FModel/FModel.sln | 6 - FModel/MainWindow.xaml.cs | 11 +- FModel/ViewModels/CUE4ParseViewModel.cs | 8 +- FModel/Views/Snooper/BufferObject.cs | 34 ++++ FModel/Views/Snooper/Shader.cs | 142 ++++++++++++++++ FModel/Views/Snooper/Snooper.cs | 193 ++++++++++++++++++++++ FModel/Views/Snooper/Texture.cs | 46 ++++++ FModel/Views/Snooper/Transform.cs | 15 ++ FModel/Views/Snooper/VertexArrayObject.cs | 36 ++++ FModel/Views/Snooper/Viewer.cs | 67 -------- 14 files changed, 480 insertions(+), 88 deletions(-) create mode 100644 FModel/Views/Snooper/BufferObject.cs create mode 100644 FModel/Views/Snooper/Shader.cs create mode 100644 FModel/Views/Snooper/Snooper.cs create mode 100644 FModel/Views/Snooper/Texture.cs create mode 100644 FModel/Views/Snooper/Transform.cs create mode 100644 FModel/Views/Snooper/VertexArrayObject.cs delete mode 100644 FModel/Views/Snooper/Viewer.cs diff --git a/CUE4Parse b/CUE4Parse index 67a3af8b..5f323ef9 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 67a3af8b005e20b14846e77db759382084b989df +Subproject commit 5f323ef90c2ee82eb6d7967783c42a3656ed401c diff --git a/FModel/Creator/Bases/FN/BaseCommunity.cs b/FModel/Creator/Bases/FN/BaseCommunity.cs index 1e0a7294..0967d38a 100644 --- a/FModel/Creator/Bases/FN/BaseCommunity.cs +++ b/FModel/Creator/Bases/FN/BaseCommunity.cs @@ -1,8 +1,8 @@ +using CUE4Parse.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Versions; -using CUE4Parse_Fortnite.Enums; using FModel.Extensions; using FModel.Framework; using FModel.Services; diff --git a/FModel/Creator/Bases/FN/BaseIcon.cs b/FModel/Creator/Bases/FN/BaseIcon.cs index 048e6a0e..15f792e6 100644 --- a/FModel/Creator/Bases/FN/BaseIcon.cs +++ b/FModel/Creator/Bases/FN/BaseIcon.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Windows; +using CUE4Parse.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.Material; @@ -12,7 +13,6 @@ using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse_Conversion.Textures; -using CUE4Parse_Fortnite.Enums; using FModel.Settings; using SkiaSharp; diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index a23e02fc..3a8e374f 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using CUE4Parse.FN.Enums; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Objects; @@ -7,7 +8,6 @@ using CUE4Parse.UE4.Objects.Core.i18N; using CUE4Parse.UE4.Objects.Engine.Curves; using CUE4Parse.UE4.Objects.GameplayTags; using CUE4Parse.UE4.Objects.UObject; -using CUE4Parse_Fortnite.Enums; using FModel.Extensions; using FModel.Framework; using SkiaSharp; @@ -288,4 +288,4 @@ public class IconStat var sliderWidth = (sliderRight - height * 2) * (floatValue / _maxValue); c.DrawRect(new SKRect(height * 2, y, Math.Min(height * 2 + sliderWidth, sliderRight), y + 5), _statPaint); } -} \ No newline at end of file +} diff --git a/FModel/FModel.sln b/FModel/FModel.sln index 7de93e7b..238d1b59 100644 --- a/FModel/FModel.sln +++ b/FModel/FModel.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FModel", "FModel.csproj", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse", "..\CUE4Parse\CUE4Parse\CUE4Parse.csproj", "{C4620341-BBB7-4384-AC7D-5082D3E0386E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Fortnite", "..\CUE4Parse\CUE4Parse-Fortnite\CUE4Parse-Fortnite.csproj", "{7765FB4C-B54D-427B-ABB6-1073687E56BD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUE4Parse-Conversion", "..\CUE4Parse\CUE4Parse-Conversion\CUE4Parse-Conversion.csproj", "{D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}" EndProject Global @@ -25,10 +23,6 @@ Global {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4620341-BBB7-4384-AC7D-5082D3E0386E}.Release|Any CPU.Build.0 = Release|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7765FB4C-B54D-427B-ABB6-1073687E56BD}.Release|Any CPU.Build.0 = Release|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0E1E8F7-F56D-469A-8E24-C2439B9FFD83}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index f30583bd..d2590bc8 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -11,7 +11,6 @@ using FModel.Settings; using FModel.ViewModels; using FModel.Views; using FModel.Views.Resources.Controls; -using FModel.Views.Snooper; using ICSharpCode.AvalonEdit.Editing; namespace FModel; @@ -53,10 +52,6 @@ public partial class MainWindow ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode); #endif - var v = new Viewer(); - v.Run(); - return; - switch (UserSettings.Default.AesReload) { case EAesReload.Always: @@ -80,6 +75,12 @@ public partial class MainWindow if (UserSettings.Default.DiscordRpc == EDiscordRpc.Always) _discordHandler.Initialize(_applicationView.CUE4Parse.Game); + +#if DEBUG + await _threadWorkerView.Begin(_ => + _applicationView.CUE4Parse.Extract( + "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_Garland_Merge.uasset")); +#endif } private void OnGridSplitterDoubleClick(object sender, MouseButtonEventArgs e) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 440a821e..4b9ac9a6 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -38,6 +38,7 @@ using FModel.Services; using FModel.Settings; using FModel.Views; using FModel.Views.Resources.Controls; +using FModel.Views.Snooper; using Newtonsoft.Json; using Serilog; using SkiaSharp; @@ -752,11 +753,8 @@ public class CUE4ParseViewModel : ViewModel export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): { - Application.Current.Dispatcher.Invoke(delegate - { - var modelViewer = Helper.GetWindow("Model Viewer", () => new ModelViewer().Show()); - modelViewer.Load(export); - }); + var snooper = new Snooper(export); + snooper.Run(); return true; } case UMaterialInstance m when ModelIsOverwritingMaterial: diff --git a/FModel/Views/Snooper/BufferObject.cs b/FModel/Views/Snooper/BufferObject.cs new file mode 100644 index 00000000..79fe7058 --- /dev/null +++ b/FModel/Views/Snooper/BufferObject.cs @@ -0,0 +1,34 @@ +using System; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class BufferObject : IDisposable where TDataType : unmanaged +{ + private uint _handle; + private BufferTargetARB _bufferType; + private GL _gl; + + public unsafe BufferObject(GL gl, Span data, BufferTargetARB bufferType) + { + _gl = gl; + _bufferType = bufferType; + + _handle = _gl.GenBuffer(); + Bind(); + fixed (void* d = data) + { + _gl.BufferData(bufferType, (nuint) (data.Length * sizeof(TDataType)), d, BufferUsageARB.StaticDraw); + } + } + + public void Bind() + { + _gl.BindBuffer(_bufferType, _handle); + } + + public void Dispose() + { + _gl.DeleteBuffer(_handle); + } +} diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs new file mode 100644 index 00000000..4cf96dc5 --- /dev/null +++ b/FModel/Views/Snooper/Shader.cs @@ -0,0 +1,142 @@ +using System; +using System.Numerics; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class Shader : IDisposable +{ + private uint _handle; + private GL _gl; + + private readonly string VertexShaderSource = @" +#version 330 core +layout (location = 0) in vec3 vPos; +layout (location = 1) in vec3 vNormal; +layout (location = 2) in vec2 vTexCoords; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; + +out vec3 fNormal; +out vec3 fPos; +out vec2 fTexCoords; + +void main() +{ + //Multiplying our uniform with the vertex position, the multiplication order here does matter. + gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); + //We want to know the fragment's position in World space, so we multiply ONLY by uModel and not uView or uProjection + fPos = vec3(uModel * vec4(vPos, 1.0)); + //The Normal needs to be in World space too, but needs to account for Scaling of the object + fNormal = mat3(transpose(inverse(uModel))) * vNormal; + //Pass the texture coordinates straight through to the fragment shader + fTexCoords = vTexCoords; +} + "; + + private readonly string FragmentShaderSource = @" +#version 330 core + +in vec3 fNormal; +in vec3 fPos; +in vec2 fTexCoords; + +uniform vec3 viewPos; + +out vec4 FragColor; + +void main() +{ + vec3 viewDirection = normalize(viewPos - fPos); + FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); +} + "; + + public Shader(GL gl) + { + _gl = gl; + + uint vertex = LoadShader(ShaderType.VertexShader, VertexShaderSource); + uint fragment = LoadShader(ShaderType.FragmentShader, FragmentShaderSource); + _handle = _gl.CreateProgram(); + _gl.AttachShader(_handle, vertex); + _gl.AttachShader(_handle, fragment); + _gl.LinkProgram(_handle); + _gl.GetProgram(_handle, GLEnum.LinkStatus, out var status); + if (status == 0) + { + throw new Exception($"Program failed to link with error: {_gl.GetProgramInfoLog(_handle)}"); + } + _gl.DetachShader(_handle, vertex); + _gl.DetachShader(_handle, fragment); + _gl.DeleteShader(vertex); + _gl.DeleteShader(fragment); + } + + public void Use() + { + _gl.UseProgram(_handle); + } + + public void SetUniform(string name, int value) + { + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.Uniform1(location, value); + } + + public unsafe void SetUniform(string name, Matrix4x4 value) + { + //A new overload has been created for setting a uniform so we can use the transform in our shader. + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.UniformMatrix4(location, 1, false, (float*) &value); + } + + public void SetUniform(string name, float value) + { + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.Uniform1(location, value); + } + + public void SetUniform(string name, Vector3 value) + { + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.Uniform3(location, value.X, value.Y, value.Z); + } + + public void Dispose() + { + _gl.DeleteProgram(_handle); + } + + private uint LoadShader(ShaderType type, string content) + { + uint handle = _gl.CreateShader(type); + _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; + } +} diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs new file mode 100644 index 00000000..323c33ec --- /dev/null +++ b/FModel/Views/Snooper/Snooper.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using System.Numerics; +using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.StaticMesh; +using CUE4Parse_Conversion.Meshes; +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +namespace FModel.Views.Snooper; + +public class Snooper +{ + private IWindow _window; + private GL _gl; + private Camera _camera; + private IKeyboard _keyboard; + private Vector2 _previousMousePosition; + + private BufferObject _ebo; + private BufferObject _vbo; + private VertexArrayObject _vao; + private Shader _shader; + + private uint[] _indices; + private float[] _vertices; + + public int Width { get; } + public int Height { get; } + + private const double _ratio = .75; + private const int _vertexSize = 8; // just so we don't have to do .Length + private const uint _faceSize = 3; // just so we don't have to do .Length + private readonly uint[] _facesIndex = { 1, 0, 2 }; + + public Snooper(UObject export) + { + var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; + var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; + Width = Convert.ToInt32(x * _ratio); + Height = Convert.ToInt32(y * _ratio); + + var options = WindowOptions.Default; + options.Size = new Vector2D(Width, Height); + options.Title = "Snooper"; + _window = Window.Create(options); + + _window.Load += OnLoad; + _window.Update += OnUpdate; + _window.Render += OnRender; + _window.Closing += OnClose; + + switch (export) + { + case UStaticMesh st when st.TryConvert(out var mesh): + { + _indices = new uint[mesh.LODs[0].Sections.Value.Sum(section => section.NumFaces * _faceSize)]; + _vertices = new float[_indices.Length * _vertexSize]; + foreach (var section in mesh.LODs[0].Sections.Value) + { + for (uint face = 0; face < section.NumFaces; face++) + { + foreach (var f in _facesIndex) + { + var index = section.FirstIndex + face * _faceSize + f; + var indice = mesh.LODs[0].Indices.Value[index]; + + var vert = mesh.LODs[0].Verts[indice]; + _vertices[index * _vertexSize] = vert.Position.X * 0.01f; + _vertices[index * _vertexSize + 1] = vert.Position.Z * 0.01f; + _vertices[index * _vertexSize + 2] = vert.Position.Y * 0.01f; + _vertices[index * _vertexSize + 3] = vert.Normal.X; + _vertices[index * _vertexSize + 4] = vert.Normal.Z; + _vertices[index * _vertexSize + 5] = vert.Normal.Y; + _vertices[index * _vertexSize + 6] = vert.UV.U; + _vertices[index * _vertexSize + 7] = vert.UV.V; + + _indices[index] = face * _faceSize + f; + } + } + } + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(export)); + } + } + + public void Run() + { + _window.Run(); + } + + private void OnLoad() + { + var input = _window.CreateInput(); + _keyboard = input.Keyboards[0]; + _keyboard.KeyDown += KeyDown; + foreach (var mouse in input.Mice) + { + mouse.Cursor.CursorMode = CursorMode.Raw; + mouse.MouseMove += OnMouseMove; + mouse.Scroll += OnMouseWheel; + } + + _gl = GL.GetApi(_window); + + _ebo = new BufferObject(_gl, _indices, BufferTargetARB.ElementArrayBuffer); + _vbo = new BufferObject(_gl, _vertices, BufferTargetARB.ArrayBuffer); + _vao = new VertexArrayObject(_gl, _vbo, _ebo); + + _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, _vertexSize, 0); // position + _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normals + _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv + + _shader = new Shader(_gl); + + _camera = new Camera(Vector3.UnitZ * 6, Vector3.UnitZ * -1, Vector3.UnitY, Width / Height); + } + + private unsafe void OnRender(double deltaTime) + { + _gl.Clear((uint) ClearBufferMask.ColorBufferBit); + + _vao.Bind(); + _shader.Use(); + + _shader.SetUniform("uModel", Matrix4x4.Identity); + _shader.SetUniform("uView", _camera.GetViewMatrix()); + _shader.SetUniform("uProjection", _camera.GetProjectionMatrix()); + // _shader.SetUniform("viewPos", _camera.Position); + + _gl.DrawElements(PrimitiveType.Triangles, (uint) _indices.Length, DrawElementsType.UnsignedInt, null); + } + + private void OnUpdate(double deltaTime) + { + var moveSpeed = 5f * (float) deltaTime; + if (_keyboard.IsKeyPressed(Key.W)) + { + _camera.Position += moveSpeed * _camera.Front; + } + if (_keyboard.IsKeyPressed(Key.S)) + { + _camera.Position -= moveSpeed * _camera.Front; + } + if (_keyboard.IsKeyPressed(Key.A)) + { + _camera.Position -= Vector3.Normalize(Vector3.Cross(_camera.Front, _camera.Up)) * moveSpeed; + } + if (_keyboard.IsKeyPressed(Key.D)) + { + _camera.Position += Vector3.Normalize(Vector3.Cross(_camera.Front, _camera.Up)) * moveSpeed; + } + } + + private void OnMouseMove(IMouse mouse, Vector2 position) + { + const float lookSensitivity = 0.1f; + if (_previousMousePosition == default) { _previousMousePosition = position; } + else + { + var xOffset = (position.X - _previousMousePosition.X) * lookSensitivity; + var yOffset = (position.Y - _previousMousePosition.Y) * lookSensitivity; + _previousMousePosition = position; + + _camera.ModifyDirection(xOffset, yOffset); + } + } + + private void OnMouseWheel(IMouse mouse, ScrollWheel scrollWheel) + { + _camera.ModifyZoom(scrollWheel.Y); + } + + private void OnClose() + { + _ebo.Dispose(); + _vbo.Dispose(); + _vao.Dispose(); + _shader.Dispose(); + } + + private void KeyDown(IKeyboard keyboard, Key key, int arg3) + { + if (key == Key.Escape) + { + _window.Close(); + } + } +} diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs new file mode 100644 index 00000000..ed843e73 --- /dev/null +++ b/FModel/Views/Snooper/Texture.cs @@ -0,0 +1,46 @@ +using Silk.NET.OpenGL; +using System; + +namespace FModel.Views.Snooper; + +public class Texture : IDisposable +{ + private uint _handle; + private GL _gl; + + public unsafe Texture(GL gl, Span data, uint width, uint height) + { + _gl = gl; + + _handle = _gl.GenTexture(); + Bind(); + + fixed (void* d = &data[0]) + { + _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgba, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, d); + SetParameters(); + } + } + + private void SetParameters() + { + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.LinearMipmapLinear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, 8); + _gl.GenerateMipmap(TextureTarget.Texture2D); + } + + public void Bind(TextureUnit textureSlot = TextureUnit.Texture0) + { + _gl.ActiveTexture(textureSlot); + _gl.BindTexture(TextureTarget.Texture2D, _handle); + } + + public void Dispose() + { + _gl.DeleteTexture(_handle); + } +} diff --git a/FModel/Views/Snooper/Transform.cs b/FModel/Views/Snooper/Transform.cs new file mode 100644 index 00000000..563fb14d --- /dev/null +++ b/FModel/Views/Snooper/Transform.cs @@ -0,0 +1,15 @@ +using System.Numerics; + +namespace FModel.Views.Snooper; + +public class Transform +{ + public Vector3 Position { get; set; } = new Vector3(0, 0, 0); + + public float Scale { get; set; } = 1f; + + public Quaternion Rotation { get; set; } = Quaternion.Identity; + + //Note: The order here does matter. + public Matrix4x4 ViewMatrix => Matrix4x4.Identity * Matrix4x4.CreateFromQuaternion(Rotation) * Matrix4x4.CreateScale(Scale) * Matrix4x4.CreateTranslation(Position); +} diff --git a/FModel/Views/Snooper/VertexArrayObject.cs b/FModel/Views/Snooper/VertexArrayObject.cs new file mode 100644 index 00000000..17068915 --- /dev/null +++ b/FModel/Views/Snooper/VertexArrayObject.cs @@ -0,0 +1,36 @@ +using System; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class VertexArrayObject : IDisposable where TVertexType : unmanaged where TIndexType : unmanaged +{ + private uint _handle; + private GL _gl; + + public VertexArrayObject(GL gl, BufferObject vbo, BufferObject ebo) + { + _gl = gl; + + _handle = _gl.GenVertexArray(); + Bind(); + vbo.Bind(); + ebo.Bind(); + } + + public unsafe void VertexAttributePointer(uint index, int count, VertexAttribPointerType type, uint vertexSize, int offSet) + { + _gl.VertexAttribPointer(index, count, type, false, vertexSize * (uint) sizeof(TVertexType), (void*) (offSet * sizeof(TVertexType))); + _gl.EnableVertexAttribArray(index); + } + + public void Bind() + { + _gl.BindVertexArray(_handle); + } + + public void Dispose() + { + _gl.DeleteVertexArray(_handle); + } +} diff --git a/FModel/Views/Snooper/Viewer.cs b/FModel/Views/Snooper/Viewer.cs deleted file mode 100644 index eced863f..00000000 --- a/FModel/Views/Snooper/Viewer.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Numerics; -using Silk.NET.Input; -using Silk.NET.Maths; -using Silk.NET.OpenGL; -using Silk.NET.Windowing; - -namespace FModel.Views.Snooper; - -public class Viewer -{ - private IWindow _window; - private GL _gl; - private Camera _camera; - private IKeyboard _keyboard; - - public int Width { get; } - public int Height { get; } - - private const double _ratio = .75; - - public Viewer() - { - var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; - var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; - Width = Convert.ToInt32(x * _ratio); - Height = Convert.ToInt32(y * _ratio); - - var options = WindowOptions.Default; - options.Size = new Vector2D(Width, Height); - options.Title = "Snooper"; - _window = Window.Create(options); - - _window.Load += OnLoad; - _window.Closing += OnClose; - } - - public void Run() - { - _window.Run(); - } - - private void OnLoad() - { - var input = _window.CreateInput(); - _keyboard = input.Keyboards[0]; - _keyboard.KeyDown += KeyDown; - - _gl = GL.GetApi(_window); - - //Start a camera at position 3 on the Z axis, looking at position -1 on the Z axis - _camera = new Camera(Vector3.UnitZ * 6, Vector3.UnitZ * -1, Vector3.UnitY, Width / Height); - } - - private void OnClose() - { - - } - - private void KeyDown(IKeyboard keyboard, Key key, int arg3) - { - if (key == Key.Escape) - { - _window.Close(); - } - } -} From 7d775a8dd4c80a8c1da3055e9e69f1debc20df0e Mon Sep 17 00:00:00 2001 From: 4sval Date: Fri, 26 Aug 2022 16:10:42 +0200 Subject: [PATCH 003/108] something's wrong --- FModel/MainWindow.xaml.cs | 2 +- FModel/Views/Snooper/Shader.cs | 16 +++++++---- FModel/Views/Snooper/Snooper.cs | 49 ++++++++++++++++++++++++++++----- FModel/Views/Snooper/Texture.cs | 6 ++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index d2590bc8..bf377057 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_Garland_Merge.uasset")); + "FortniteGame/Content/Environments/Props/Winter/Meshes/S_Sled.uasset")); #endif } diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 4cf96dc5..5c39376e 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -25,13 +25,9 @@ out vec2 fTexCoords; void main() { - //Multiplying our uniform with the vertex position, the multiplication order here does matter. gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); - //We want to know the fragment's position in World space, so we multiply ONLY by uModel and not uView or uProjection fPos = vec3(uModel * vec4(vPos, 1.0)); - //The Normal needs to be in World space too, but needs to account for Scaling of the object fNormal = mat3(transpose(inverse(uModel))) * vNormal; - //Pass the texture coordinates straight through to the fragment shader fTexCoords = vTexCoords; } "; @@ -43,14 +39,22 @@ in vec3 fNormal; in vec3 fPos; in vec2 fTexCoords; +struct Material { + sampler2D albedo; + sampler2D normal; + sampler2D specular; + sampler2D metallic; + sampler2D emissive; +}; + +uniform Material material; uniform vec3 viewPos; out vec4 FragColor; void main() { - vec3 viewDirection = normalize(viewPos - fPos); - FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); + FragColor = texture(material.albedo, fTexCoords); } "; diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 323c33ec..d7ebecb7 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -2,8 +2,11 @@ using System.Linq; using System.Numerics; using CUE4Parse.UE4.Assets.Exports; +using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.StaticMesh; +using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes; +using CUE4Parse_Conversion.Textures; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; @@ -23,6 +26,10 @@ public class Snooper private BufferObject _vbo; private VertexArrayObject _vao; private Shader _shader; + private CMaterialParams _params; + + private Texture _albedoMap; + private Texture _normalMap; private uint[] _indices; private float[] _vertices; @@ -30,20 +37,21 @@ public class Snooper public int Width { get; } public int Height { get; } - private const double _ratio = .75; - private const int _vertexSize = 8; // just so we don't have to do .Length + private const int _vertexSize = 8; // Position + Normals + UV private const uint _faceSize = 3; // just so we don't have to do .Length private readonly uint[] _facesIndex = { 1, 0, 2 }; public Snooper(UObject export) { + const double ratio = .6; var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; - Width = Convert.ToInt32(x * _ratio); - Height = Convert.ToInt32(y * _ratio); + Width = Convert.ToInt32(x * ratio); + Height = Convert.ToInt32(y * ratio); var options = WindowOptions.Default; options.Size = new Vector2D(Width, Height); + options.Position = new Vector2D(Width, Height); options.Title = "Snooper"; _window = Window.Create(options); @@ -64,7 +72,8 @@ public class Snooper { foreach (var f in _facesIndex) { - var index = section.FirstIndex + face * _faceSize + f; + var i = face * _faceSize + f; + var index = section.FirstIndex + i; var indice = mesh.LODs[0].Indices.Value[index]; var vert = mesh.LODs[0].Verts[indice]; @@ -77,9 +86,15 @@ public class Snooper _vertices[index * _vertexSize + 6] = vert.UV.U; _vertices[index * _vertexSize + 7] = vert.UV.V; - _indices[index] = face * _faceSize + f; + _indices[index] = i; } } + + _params = new CMaterialParams(); + if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) + { + unrealMaterial.GetParams(_params); + } } break; } @@ -118,6 +133,12 @@ public class Snooper _shader = new Shader(_gl); _camera = new Camera(Vector3.UnitZ * 6, Vector3.UnitZ * -1, Vector3.UnitY, Width / Height); + + if (_params.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + { + TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); + _albedoMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } } private unsafe void OnRender(double deltaTime) @@ -127,17 +148,23 @@ public class Snooper _vao.Bind(); _shader.Use(); + _albedoMap.Bind(TextureUnit.Texture0); + _shader.SetUniform("uModel", Matrix4x4.Identity); _shader.SetUniform("uView", _camera.GetViewMatrix()); _shader.SetUniform("uProjection", _camera.GetProjectionMatrix()); // _shader.SetUniform("viewPos", _camera.Position); + //Configure the materials variables. + _shader.SetUniform("material.albedo", 0); + _gl.DrawElements(PrimitiveType.Triangles, (uint) _indices.Length, DrawElementsType.UnsignedInt, null); } private void OnUpdate(double deltaTime) { - var moveSpeed = 5f * (float) deltaTime; + var speed = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 5f : 2.5f; + var moveSpeed = speed * (float) deltaTime; if (_keyboard.IsKeyPressed(Key.W)) { _camera.Position += moveSpeed * _camera.Front; @@ -154,6 +181,14 @@ public class Snooper { _camera.Position += Vector3.Normalize(Vector3.Cross(_camera.Front, _camera.Up)) * moveSpeed; } + if (_keyboard.IsKeyPressed(Key.E)) + { + _camera.Position += moveSpeed * _camera.Up; + } + if (_keyboard.IsKeyPressed(Key.Q)) + { + _camera.Position -= moveSpeed * _camera.Up; + } } private void OnMouseMove(IMouse mouse, Vector2 position) diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index ed843e73..d8d00013 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -8,12 +8,12 @@ public class Texture : IDisposable private uint _handle; private GL _gl; - public unsafe Texture(GL gl, Span data, uint width, uint height) + public unsafe Texture(GL gl, byte[] data, uint width, uint height) { _gl = gl; _handle = _gl.GenTexture(); - Bind(); + Bind(TextureUnit.Texture0); fixed (void* d = &data[0]) { @@ -33,7 +33,7 @@ public class Texture : IDisposable _gl.GenerateMipmap(TextureTarget.Texture2D); } - public void Bind(TextureUnit textureSlot = TextureUnit.Texture0) + public void Bind(TextureUnit textureSlot) { _gl.ActiveTexture(textureSlot); _gl.BindTexture(TextureTarget.Texture2D, _handle); From 0014b6f4ed1ea408cea70cdd4668c40c7281ec1c Mon Sep 17 00:00:00 2001 From: 4sval Date: Sat, 27 Aug 2022 00:42:42 +0200 Subject: [PATCH 004/108] good cam position + fixed depth mip mapping + trying to understand shit --- CUE4Parse | 2 +- FModel/App.xaml.cs | 4 + FModel/Constants.cs | 2 + FModel/Helper.cs | 5 ++ FModel/MainWindow.xaml.cs | 2 +- FModel/Views/Snooper/Camera.cs | 38 +++++---- FModel/Views/Snooper/Mesh.cs | 128 ++++++++++++++++++++++++++++++ FModel/Views/Snooper/Shader.cs | 1 - FModel/Views/Snooper/Snooper.cs | 133 ++++++++++---------------------- 9 files changed, 205 insertions(+), 110 deletions(-) create mode 100644 FModel/Views/Snooper/Mesh.cs diff --git a/CUE4Parse b/CUE4Parse index 5f323ef9..aa5c9a48 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 5f323ef90c2ee82eb6d7967783c42a3656ed401c +Subproject commit aa5c9a485e236b1788cc484e4bb22ab8ff202573 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index b09146f7..f889e0f4 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -89,9 +89,13 @@ public partial class App Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, "Logs")); Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")); +#if DEBUG + Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).CreateLogger(); +#else Log.Logger = new LoggerConfiguration().WriteTo.Console(theme: AnsiConsoleTheme.Literate).WriteTo.File( Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.txt"), outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger(); +#endif Log.Information("Version {Version}", Constants.APP_VERSION); Log.Information("{OS}", GetOperatingSystemProductName()); diff --git a/FModel/Constants.cs b/FModel/Constants.cs index 9ea96e56..6deb29c7 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -9,6 +9,8 @@ public static class Constants public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000"; public static readonly FGuid ZERO_GUID = new(0U); + public const float SCALE_DOWN_RATIO = 0.01F; + public const string WHITE = "#DAE5F2"; public const string GRAY = "#BBBBBB"; public const string RED = "#E06C75"; diff --git a/FModel/Helper.cs b/FModel/Helper.cs index 93103474..a44a3d09 100644 --- a/FModel/Helper.cs +++ b/FModel/Helper.cs @@ -105,4 +105,9 @@ public static class Helper { return MathF.PI / 180f * degrees; } + + public static float RadiansToDegrees(float radians) + { + return radians* 180f / MathF.PI; + } } diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index bf377057..88c73acc 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "FortniteGame/Content/Environments/Props/Winter/Meshes/S_Sled.uasset")); + "/Game/Gadgets/Assets/VinderTech_GliderChute/Glider_Mark_II/Meshes/Glider_Mark_II.uasset")); #endif } diff --git a/FModel/Views/Snooper/Camera.cs b/FModel/Views/Snooper/Camera.cs index 69f71276..e41094f0 100644 --- a/FModel/Views/Snooper/Camera.cs +++ b/FModel/Views/Snooper/Camera.cs @@ -6,22 +6,24 @@ namespace FModel.Views.Snooper; public class Camera { public Vector3 Position { get; set; } - public Vector3 Front { get; set; } - - public Vector3 Up { get; private set; } - public float AspectRatio { get; set; } + public Vector3 Direction { get; private set; } + public Vector3 Up = Vector3.UnitY; + public float AspectRatio { get; } public float Yaw { get; set; } = -90f; public float Pitch { get; set; } + public float Zoom = 45f; - private float Zoom = 45f; - - public Camera(Vector3 position, Vector3 front, Vector3 up, float aspectRatio) + public Camera(Vector3 position, Vector3 direction, float aspectRatio = 16f / 9f) { Position = position; + Direction = direction; AspectRatio = aspectRatio; - Front = front; - Up = up; + + // trigonometric math to calculate the cam's yaw/pitch based on position and direction to look + var yaw = MathF.Atan((-Position.X - Direction.X) / (Position.Z - Direction.Z)); + var pitch = MathF.Atan((Position.Y - Direction.Y) / (Position.Z - Direction.Z)); + ModifyDirection(Helper.RadiansToDegrees(yaw), Helper.RadiansToDegrees(pitch)); } public void ModifyZoom(float zoomAmount) @@ -38,21 +40,25 @@ public class Camera //We don't want to be able to look behind us by going over our head or under our feet so make sure it stays within these bounds Pitch = Math.Clamp(Pitch, -89f, 89f); - var cameraDirection = Vector3.Zero; - cameraDirection.X = MathF.Cos(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); - cameraDirection.Y = MathF.Sin(Helper.DegreesToRadians(Pitch)); - cameraDirection.Z = MathF.Sin(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); - - Front = Vector3.Normalize(cameraDirection); + Direction = Vector3.Normalize(CalculateDirection()); } public Matrix4x4 GetViewMatrix() { - return Matrix4x4.CreateLookAt(Position, Position + Front, Up); + return Matrix4x4.CreateLookAt(Position, Position + Direction, Up); } public Matrix4x4 GetProjectionMatrix() { return Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, 0.1f, 100.0f); } + + private Vector3 CalculateDirection() + { + var direction = Vector3.Zero; + direction.X = MathF.Cos(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); + direction.Y = MathF.Sin(Helper.DegreesToRadians(Pitch)); + direction.Z = MathF.Sin(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); + return direction; + } } diff --git a/FModel/Views/Snooper/Mesh.cs b/FModel/Views/Snooper/Mesh.cs new file mode 100644 index 00000000..70869128 --- /dev/null +++ b/FModel/Views/Snooper/Mesh.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Numerics; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse_Conversion.Meshes.PSK; +using CUE4Parse_Conversion.Textures; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class Mesh : IDisposable +{ + private uint _handle; + private GL _gl; + + private BufferObject _ebo; + private BufferObject _vbo; + private VertexArrayObject _vao; + + private Shader _shader; + private Texture[] _albedoMap; + // private Texture _normalMap; + + private const int _vertexSize = 8; // Position + Normals + UV + private const uint _faceSize = 3; // just so we don't have to do .Length + private readonly uint[] _facesIndex = { 1, 0, 2 }; + + public uint[] Indices; + public float[] Vertices; + public CMaterialParams[] Params; + + public Mesh(CBaseMeshLod lod, CMeshVertex[] vertices) + { + var sections = lod.Sections.Value; + Params = new CMaterialParams[sections.Length]; + Indices = new uint[sections.Sum(section => section.NumFaces * _faceSize)]; + Vertices = new float[Indices.Length * _vertexSize]; + + for (var s = 0; s < sections.Length; s++) + { + var section = sections[s]; + for (uint face = 0; face < section.NumFaces; face++) + { + foreach (var f in _facesIndex) + { + var i = face * _faceSize + f; + var index = section.FirstIndex + i; + var indice = lod.Indices.Value[index]; + + var vert = vertices[indice]; + Vertices[index * _vertexSize] = vert.Position.X * Constants.SCALE_DOWN_RATIO; + Vertices[index * _vertexSize + 1] = vert.Position.Z * Constants.SCALE_DOWN_RATIO; + Vertices[index * _vertexSize + 2] = vert.Position.Y * Constants.SCALE_DOWN_RATIO; + Vertices[index * _vertexSize + 3] = vert.Normal.X; + Vertices[index * _vertexSize + 4] = vert.Normal.Z; + Vertices[index * _vertexSize + 5] = vert.Normal.Y; + Vertices[index * _vertexSize + 6] = vert.UV.U; + Vertices[index * _vertexSize + 7] = vert.UV.V; + + Indices[index] = (uint) index; + } + } + + Params[s] = new CMaterialParams(); + if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) + { + unrealMaterial.GetParams(Params[s]); + } + } + } + + public void Setup(GL gl) + { + _gl = gl; + + _handle = _gl.CreateProgram(); + + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); + _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); + _vao = new VertexArrayObject(_gl, _vbo, _ebo); + + _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, _vertexSize, 0); // position + _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normals + _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv + + _shader = new Shader(_gl); + + _albedoMap = new Texture[Params.Length]; + for (int i = 0; i < _albedoMap.Length; i++) + { + if (Params[i].Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + { + TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); + _albedoMap[i] = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + } + } + + public unsafe void Bind(Camera camera) + { + _vao.Bind(); + + _shader.Use(); + + _shader.SetUniform("uModel", Matrix4x4.Identity); + _shader.SetUniform("uView", camera.GetViewMatrix()); + _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); + // _shader.SetUniform("viewPos", _camera.Position); + + for (int i = 0; i < _albedoMap.Length; i++) + { + _shader.SetUniform("material.albedo", i); + _albedoMap[i].Bind(TextureUnit.Texture0 + i); + } + + _gl.DrawElements(PrimitiveType.Triangles, (uint) Indices.Length, DrawElementsType.UnsignedInt, null); + } + + public void Dispose() + { + _ebo.Dispose(); + _vbo.Dispose(); + _vao.Dispose(); + _shader.Dispose(); + _gl.DeleteProgram(_handle); + } +} diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 5c39376e..8aee622e 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -48,7 +48,6 @@ struct Material { }; uniform Material material; -uniform vec3 viewPos; out vec4 FragColor; diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index d7ebecb7..14e8bcbe 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -1,12 +1,10 @@ using System; -using System.Linq; using System.Numerics; using CUE4Parse.UE4.Assets.Exports; -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_Conversion.Meshes; -using CUE4Parse_Conversion.Textures; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; @@ -22,25 +20,11 @@ public class Snooper private IKeyboard _keyboard; private Vector2 _previousMousePosition; - private BufferObject _ebo; - private BufferObject _vbo; - private VertexArrayObject _vao; - private Shader _shader; - private CMaterialParams _params; - - private Texture _albedoMap; - private Texture _normalMap; - - private uint[] _indices; - private float[] _vertices; + private Mesh[] _meshes; public int Width { get; } public int Height { get; } - private const int _vertexSize = 8; // Position + Normals + UV - private const uint _faceSize = 3; // just so we don't have to do .Length - private readonly uint[] _facesIndex = { 1, 0, 2 }; - public Snooper(UObject export) { const double ratio = .6; @@ -60,42 +44,19 @@ public class Snooper _window.Render += OnRender; _window.Closing += OnClose; + _meshes = new Mesh[1]; switch (export) { case UStaticMesh st when st.TryConvert(out var mesh): { - _indices = new uint[mesh.LODs[0].Sections.Value.Sum(section => section.NumFaces * _faceSize)]; - _vertices = new float[_indices.Length * _vertexSize]; - foreach (var section in mesh.LODs[0].Sections.Value) - { - for (uint face = 0; face < section.NumFaces; face++) - { - foreach (var f in _facesIndex) - { - var i = face * _faceSize + f; - var index = section.FirstIndex + i; - var indice = mesh.LODs[0].Indices.Value[index]; - - var vert = mesh.LODs[0].Verts[indice]; - _vertices[index * _vertexSize] = vert.Position.X * 0.01f; - _vertices[index * _vertexSize + 1] = vert.Position.Z * 0.01f; - _vertices[index * _vertexSize + 2] = vert.Position.Y * 0.01f; - _vertices[index * _vertexSize + 3] = vert.Normal.X; - _vertices[index * _vertexSize + 4] = vert.Normal.Z; - _vertices[index * _vertexSize + 5] = vert.Normal.Y; - _vertices[index * _vertexSize + 6] = vert.UV.U; - _vertices[index * _vertexSize + 7] = vert.UV.V; - - _indices[index] = i; - } - } - - _params = new CMaterialParams(); - if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) - { - unrealMaterial.GetParams(_params); - } - } + _meshes[0] = new Mesh(mesh.LODs[0], mesh.LODs[0].Verts); + SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); + break; + } + case USkeletalMesh sk when sk.TryConvert(out var mesh): + { + _meshes[0] = new Mesh(mesh.LODs[0], mesh.LODs[0].Verts); + SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } default: @@ -108,6 +69,17 @@ public class Snooper _window.Run(); } + private void SetupCamera(FBox box) + { + // X Yaw Gauche Droite + // Y Pitch Haut Bas + // Z Avant Arrière + + var center = box.GetCenter(); + var position = new Vector3(0f, center.Z, box.Max.Y * 3); + _camera = new Camera(position, center); + } + private void OnLoad() { var input = _window.CreateInput(); @@ -121,65 +93,44 @@ public class Snooper } _gl = GL.GetApi(_window); + _gl.Enable(EnableCap.DepthTest); - _ebo = new BufferObject(_gl, _indices, BufferTargetARB.ElementArrayBuffer); - _vbo = new BufferObject(_gl, _vertices, BufferTargetARB.ArrayBuffer); - _vao = new VertexArrayObject(_gl, _vbo, _ebo); - - _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, _vertexSize, 0); // position - _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normals - _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv - - _shader = new Shader(_gl); - - _camera = new Camera(Vector3.UnitZ * 6, Vector3.UnitZ * -1, Vector3.UnitY, Width / Height); - - if (_params.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + foreach (var mesh in _meshes) { - TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); - _albedoMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + mesh.Setup(_gl); } } - private unsafe void OnRender(double deltaTime) + private void OnRender(double deltaTime) { - _gl.Clear((uint) ClearBufferMask.ColorBufferBit); + _gl.ClearColor(0.149f, 0.149f, 0.188f, 1.0f); + _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); - _vao.Bind(); - _shader.Use(); - - _albedoMap.Bind(TextureUnit.Texture0); - - _shader.SetUniform("uModel", Matrix4x4.Identity); - _shader.SetUniform("uView", _camera.GetViewMatrix()); - _shader.SetUniform("uProjection", _camera.GetProjectionMatrix()); - // _shader.SetUniform("viewPos", _camera.Position); - - //Configure the materials variables. - _shader.SetUniform("material.albedo", 0); - - _gl.DrawElements(PrimitiveType.Triangles, (uint) _indices.Length, DrawElementsType.UnsignedInt, null); + foreach (var mesh in _meshes) + { + mesh.Bind(_camera); + } } private void OnUpdate(double deltaTime) { - var speed = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 5f : 2.5f; + var speed = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 2.5f : 1f; var moveSpeed = speed * (float) deltaTime; if (_keyboard.IsKeyPressed(Key.W)) { - _camera.Position += moveSpeed * _camera.Front; + _camera.Position += moveSpeed * _camera.Direction; } if (_keyboard.IsKeyPressed(Key.S)) { - _camera.Position -= moveSpeed * _camera.Front; + _camera.Position -= moveSpeed * _camera.Direction; } if (_keyboard.IsKeyPressed(Key.A)) { - _camera.Position -= Vector3.Normalize(Vector3.Cross(_camera.Front, _camera.Up)) * moveSpeed; + _camera.Position -= Vector3.Normalize(Vector3.Cross(_camera.Direction, _camera.Up)) * moveSpeed; } if (_keyboard.IsKeyPressed(Key.D)) { - _camera.Position += Vector3.Normalize(Vector3.Cross(_camera.Front, _camera.Up)) * moveSpeed; + _camera.Position += Vector3.Normalize(Vector3.Cross(_camera.Direction, _camera.Up)) * moveSpeed; } if (_keyboard.IsKeyPressed(Key.E)) { @@ -212,10 +163,10 @@ public class Snooper private void OnClose() { - _ebo.Dispose(); - _vbo.Dispose(); - _vao.Dispose(); - _shader.Dispose(); + foreach (var mesh in _meshes) + { + mesh.Dispose(); + } } private void KeyDown(IKeyboard keyboard, Key key, int arg3) From 2cf40a022c4f944f669a3faf1b2a2ce9e19dbef0 Mon Sep 17 00:00:00 2001 From: 4sval Date: Sat, 27 Aug 2022 17:50:05 +0200 Subject: [PATCH 005/108] I have no idea how I made multiple materials work --- FModel/FModel.csproj | 1 - FModel/MainWindow.xaml.cs | 2 +- FModel/Views/Snooper/Camera.cs | 32 +++++++----- FModel/Views/Snooper/{Mesh.cs => Model.cs} | 56 +++++++++------------ FModel/Views/Snooper/Section.cs | 57 ++++++++++++++++++++++ FModel/Views/Snooper/Snooper.cs | 25 ++++------ FModel/Views/Snooper/Texture.cs | 2 - 7 files changed, 111 insertions(+), 64 deletions(-) rename FModel/Views/Snooper/{Mesh.cs => Model.cs} (67%) create mode 100644 FModel/Views/Snooper/Section.cs diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 9f143a85..80dd9cc6 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -130,7 +130,6 @@ - diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 88c73acc..563c3b27 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "/Game/Gadgets/Assets/VinderTech_GliderChute/Glider_Mark_II/Meshes/Glider_Mark_II.uasset")); + "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_ChristmasTree_Llama.uasset")); #endif } diff --git a/FModel/Views/Snooper/Camera.cs b/FModel/Views/Snooper/Camera.cs index e41094f0..9dbaf5d8 100644 --- a/FModel/Views/Snooper/Camera.cs +++ b/FModel/Views/Snooper/Camera.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using Serilog; namespace FModel.Views.Snooper; @@ -9,16 +10,15 @@ public class Camera public Vector3 Direction { get; private set; } public Vector3 Up = Vector3.UnitY; - public float AspectRatio { get; } public float Yaw { get; set; } = -90f; - public float Pitch { get; set; } - public float Zoom = 45f; + public float Pitch { get; set; } = 0f; + public float Zoom { get; set; } = 45f; + public float AspectRatio => 16f / 9f; - public Camera(Vector3 position, Vector3 direction, float aspectRatio = 16f / 9f) + public Camera(Vector3 position, Vector3 direction) { Position = position; Direction = direction; - AspectRatio = aspectRatio; // trigonometric math to calculate the cam's yaw/pitch based on position and direction to look var yaw = MathF.Atan((-Position.X - Direction.X) / (Position.Z - Direction.Z)); @@ -40,7 +40,7 @@ public class Camera //We don't want to be able to look behind us by going over our head or under our feet so make sure it stays within these bounds Pitch = Math.Clamp(Pitch, -89f, 89f); - Direction = Vector3.Normalize(CalculateDirection()); + CalculateDirection(); } public Matrix4x4 GetViewMatrix() @@ -53,12 +53,22 @@ public class Camera return Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, 0.1f, 100.0f); } - private Vector3 CalculateDirection() + public void CalculateDirection() { var direction = Vector3.Zero; - direction.X = MathF.Cos(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); - direction.Y = MathF.Sin(Helper.DegreesToRadians(Pitch)); - direction.Z = MathF.Sin(Helper.DegreesToRadians(Yaw)) * MathF.Cos(Helper.DegreesToRadians(Pitch)); - return direction; + var yaw = Helper.DegreesToRadians(Yaw); + var pitch = Helper.DegreesToRadians(Pitch); + direction.X = MathF.Cos(yaw) * MathF.Cos(pitch); + direction.Y = MathF.Sin(pitch); + direction.Z = MathF.Sin(yaw) * MathF.Cos(pitch); + Direction = Vector3.Normalize(direction); + } + + private void Loge() + { + Log.Logger.Information("Position {Position}", Position); + Log.Logger.Information("Direction {Direction}", Direction); + Log.Logger.Information("Yaw {Yaw}", Yaw); + Log.Logger.Information("Pitch {Pitch}", Pitch); } } diff --git a/FModel/Views/Snooper/Mesh.cs b/FModel/Views/Snooper/Model.cs similarity index 67% rename from FModel/Views/Snooper/Mesh.cs rename to FModel/Views/Snooper/Model.cs index 70869128..9755d97a 100644 --- a/FModel/Views/Snooper/Mesh.cs +++ b/FModel/Views/Snooper/Model.cs @@ -1,15 +1,12 @@ using System; using System.Linq; using System.Numerics; -using CUE4Parse.UE4.Assets.Exports.Material; -using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; -using CUE4Parse_Conversion.Textures; using Silk.NET.OpenGL; namespace FModel.Views.Snooper; -public class Mesh : IDisposable +public class Model : IDisposable { private uint _handle; private GL _gl; @@ -18,28 +15,27 @@ public class Mesh : IDisposable private BufferObject _vbo; private VertexArrayObject _vao; - private Shader _shader; - private Texture[] _albedoMap; - // private Texture _normalMap; - private const int _vertexSize = 8; // Position + Normals + UV private const uint _faceSize = 3; // just so we don't have to do .Length private readonly uint[] _facesIndex = { 1, 0, 2 }; + private Shader _shader; + public uint[] Indices; public float[] Vertices; - public CMaterialParams[] Params; + public Section[] Sections; - public Mesh(CBaseMeshLod lod, CMeshVertex[] vertices) + public Model(CBaseMeshLod lod, CMeshVertex[] vertices) { var sections = lod.Sections.Value; - Params = new CMaterialParams[sections.Length]; + Sections = new Section[sections.Length]; Indices = new uint[sections.Sum(section => section.NumFaces * _faceSize)]; Vertices = new float[Indices.Length * _vertexSize]; for (var s = 0; s < sections.Length; s++) { var section = sections[s]; + Sections[s] = new Section((uint) section.NumFaces * _faceSize, section.FirstIndex, section); for (uint face = 0; face < section.NumFaces; face++) { foreach (var f in _facesIndex) @@ -58,15 +54,9 @@ public class Mesh : IDisposable Vertices[index * _vertexSize + 6] = vert.UV.U; Vertices[index * _vertexSize + 7] = vert.UV.V; - Indices[index] = (uint) index; + Indices[index] = i; } } - - Params[s] = new CMaterialParams(); - if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) - { - unrealMaterial.GetParams(Params[s]); - } } } @@ -76,6 +66,8 @@ public class Mesh : IDisposable _handle = _gl.CreateProgram(); + _shader = new Shader(_gl); + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); _vao = new VertexArrayObject(_gl, _vbo, _ebo); @@ -84,20 +76,13 @@ public class Mesh : IDisposable _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normals _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv - _shader = new Shader(_gl); - - _albedoMap = new Texture[Params.Length]; - for (int i = 0; i < _albedoMap.Length; i++) + for (int section = 0; section < Sections.Length; section++) { - if (Params[i].Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) - { - TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); - _albedoMap[i] = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); - } + Sections[section].Setup(_gl); } } - public unsafe void Bind(Camera camera) + public void Bind(Camera camera) { _vao.Bind(); @@ -106,15 +91,14 @@ public class Mesh : IDisposable _shader.SetUniform("uModel", Matrix4x4.Identity); _shader.SetUniform("uView", camera.GetViewMatrix()); _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); - // _shader.SetUniform("viewPos", _camera.Position); + // _shader.SetUniform("viewPos", camera.Position); - for (int i = 0; i < _albedoMap.Length; i++) + for (int section = 0; section < Sections.Length; section++) { - _shader.SetUniform("material.albedo", i); - _albedoMap[i].Bind(TextureUnit.Texture0 + i); - } + Sections[section].Bind(_shader); - _gl.DrawElements(PrimitiveType.Triangles, (uint) Indices.Length, DrawElementsType.UnsignedInt, null); + _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); + } } public void Dispose() @@ -123,6 +107,10 @@ public class Mesh : IDisposable _vbo.Dispose(); _vao.Dispose(); _shader.Dispose(); + for (int section = 0; section < Sections.Length; section++) + { + Sections[section].Dispose(); + } _gl.DeleteProgram(_handle); } } diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs new file mode 100644 index 00000000..95200a34 --- /dev/null +++ b/FModel/Views/Snooper/Section.cs @@ -0,0 +1,57 @@ +using System; +using CUE4Parse.UE4.Assets.Exports.Material; +using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse_Conversion.Meshes.PSK; +using CUE4Parse_Conversion.Textures; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class Section : IDisposable +{ + private uint _handle; + private GL _gl; + + private Texture _albedoMap; + // private Texture _normalMap; + + public uint FacesCount; + public int FirstFaceIndex; + public CMaterialParams Params; + + public Section(uint facesCount, int firstFaceIndex, CMeshSection section) + { + FacesCount = facesCount; + FirstFaceIndex = firstFaceIndex; + Params = new CMaterialParams(); + if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) + { + unrealMaterial.GetParams(Params); + } + } + + public void Setup(GL gl) + { + _gl = gl; + + _handle = _gl.CreateProgram(); + + if (Params.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + { + TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); + _albedoMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + } + + public void Bind(Shader shader) + { + shader.SetUniform("material.albedo", 0); + _albedoMap.Bind(TextureUnit.Texture0); + } + + public void Dispose() + { + _albedoMap.Dispose(); + _gl.DeleteProgram(_handle); + } +} diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 14e8bcbe..c8bf7f60 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -20,7 +20,7 @@ public class Snooper private IKeyboard _keyboard; private Vector2 _previousMousePosition; - private Mesh[] _meshes; + private Model[] _models; public int Width { get; } public int Height { get; } @@ -35,7 +35,6 @@ public class Snooper var options = WindowOptions.Default; options.Size = new Vector2D(Width, Height); - options.Position = new Vector2D(Width, Height); options.Title = "Snooper"; _window = Window.Create(options); @@ -44,18 +43,18 @@ public class Snooper _window.Render += OnRender; _window.Closing += OnClose; - _meshes = new Mesh[1]; + _models = new Model[1]; switch (export) { case UStaticMesh st when st.TryConvert(out var mesh): { - _meshes[0] = new Mesh(mesh.LODs[0], mesh.LODs[0].Verts); + _models[0] = new Model(mesh.LODs[0], mesh.LODs[0].Verts); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } case USkeletalMesh sk when sk.TryConvert(out var mesh): { - _meshes[0] = new Mesh(mesh.LODs[0], mesh.LODs[0].Verts); + _models[0] = new Model(mesh.LODs[0], mesh.LODs[0].Verts); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } @@ -71,10 +70,6 @@ public class Snooper private void SetupCamera(FBox box) { - // X Yaw Gauche Droite - // Y Pitch Haut Bas - // Z Avant Arrière - var center = box.GetCenter(); var position = new Vector3(0f, center.Z, box.Max.Y * 3); _camera = new Camera(position, center); @@ -95,9 +90,9 @@ public class Snooper _gl = GL.GetApi(_window); _gl.Enable(EnableCap.DepthTest); - foreach (var mesh in _meshes) + foreach (var model in _models) { - mesh.Setup(_gl); + model.Setup(_gl); } } @@ -106,9 +101,9 @@ public class Snooper _gl.ClearColor(0.149f, 0.149f, 0.188f, 1.0f); _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); - foreach (var mesh in _meshes) + foreach (var model in _models) { - mesh.Bind(_camera); + model.Bind(_camera); } } @@ -163,9 +158,9 @@ public class Snooper private void OnClose() { - foreach (var mesh in _meshes) + foreach (var model in _models) { - mesh.Dispose(); + model.Dispose(); } } diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index d8d00013..7f9fd236 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -24,8 +24,6 @@ public class Texture : IDisposable private void SetParameters() { - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.LinearMipmapLinear); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); From 4edfdabcd089b663cde384807d552f55b393f0ba Mon Sep 17 00:00:00 2001 From: 4sval Date: Sat, 27 Aug 2022 18:37:33 +0200 Subject: [PATCH 006/108] dynamic camera speed --- CUE4Parse | 2 +- FModel/Views/Snooper/Camera.cs | 4 +++- FModel/Views/Snooper/Section.cs | 15 +++++++++------ FModel/Views/Snooper/Snooper.cs | 6 +++--- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index aa5c9a48..c3db95eb 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit aa5c9a485e236b1788cc484e4bb22ab8ff202573 +Subproject commit c3db95ebc3800207eb3fc7dc66470178c037ff51 diff --git a/FModel/Views/Snooper/Camera.cs b/FModel/Views/Snooper/Camera.cs index 9dbaf5d8..0409ff1b 100644 --- a/FModel/Views/Snooper/Camera.cs +++ b/FModel/Views/Snooper/Camera.cs @@ -13,12 +13,14 @@ public class Camera public float Yaw { get; set; } = -90f; public float Pitch { get; set; } = 0f; public float Zoom { get; set; } = 45f; + public float Speed { get; set; } = 1f; public float AspectRatio => 16f / 9f; - public Camera(Vector3 position, Vector3 direction) + public Camera(Vector3 position, Vector3 direction, float speed) { Position = position; Direction = direction; + Speed = speed; // trigonometric math to calculate the cam's yaw/pitch based on position and direction to look var yaw = MathF.Atan((-Position.X - Direction.X) / (Position.Z - Direction.Z)); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 95200a34..c3624ffc 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -17,16 +17,16 @@ public class Section : IDisposable public uint FacesCount; public int FirstFaceIndex; - public CMaterialParams Params; + public CMaterialParams Parameters; public Section(uint facesCount, int firstFaceIndex, CMeshSection section) { FacesCount = facesCount; FirstFaceIndex = firstFaceIndex; - Params = new CMaterialParams(); + Parameters = new CMaterialParams(); if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) { - unrealMaterial.GetParams(Params); + unrealMaterial.GetParams(Parameters); } } @@ -36,7 +36,7 @@ public class Section : IDisposable _handle = _gl.CreateProgram(); - if (Params.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) { TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); _albedoMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); @@ -45,13 +45,16 @@ public class Section : IDisposable public void Bind(Shader shader) { + if (Parameters.IsNull) + return; + shader.SetUniform("material.albedo", 0); - _albedoMap.Bind(TextureUnit.Texture0); + _albedoMap?.Bind(TextureUnit.Texture0); } public void Dispose() { - _albedoMap.Dispose(); + _albedoMap?.Dispose(); _gl.DeleteProgram(_handle); } } diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index c8bf7f60..dc4483c6 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -72,7 +72,7 @@ public class Snooper { var center = box.GetCenter(); var position = new Vector3(0f, center.Z, box.Max.Y * 3); - _camera = new Camera(position, center); + _camera = new Camera(position, center, box.Max.Max() / 2f); } private void OnLoad() @@ -109,8 +109,8 @@ public class Snooper private void OnUpdate(double deltaTime) { - var speed = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 2.5f : 1f; - var moveSpeed = speed * (float) deltaTime; + var multiplier = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 2f : 1f; + var moveSpeed = _camera.Speed * multiplier * (float) deltaTime; if (_keyboard.IsKeyPressed(Key.W)) { _camera.Position += moveSpeed * _camera.Direction; From dde7688f2e8c2e37302fabbf5f01930d7a16bf06 Mon Sep 17 00:00:00 2001 From: 4sval Date: Sun, 28 Aug 2022 00:04:28 +0200 Subject: [PATCH 007/108] really bad shader --- CUE4Parse | 2 +- FModel/ViewModels/ModelViewerViewModel.cs | 20 +-------- FModel/Views/Snooper/Model.cs | 24 +++++++++-- FModel/Views/Snooper/Section.cs | 52 +++++++++++++++++++---- FModel/Views/Snooper/Shader.cs | 41 +++++++++++++++--- 5 files changed, 102 insertions(+), 37 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index c3db95eb..5725d3c3 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit c3db95ebc3800207eb3fc7dc66470178c037ff51 +Subproject commit 5725d3c30ec311870e5832e6869af73ea40aab79 diff --git a/FModel/ViewModels/ModelViewerViewModel.cs b/FModel/ViewModels/ModelViewerViewModel.cs index 6aada8bf..34018f77 100644 --- a/FModel/ViewModels/ModelViewerViewModel.cs +++ b/FModel/ViewModels/ModelViewerViewModel.cs @@ -444,24 +444,8 @@ public class ModelViewerViewModel : ViewModel if (parameters.Specular is UTexture2D specular) { var mip = specular.GetFirstMip(); - byte[] data; - SKColorType colorType; - switch (UserSettings.Default.OverridedPlatform) - { - case ETexturePlatform.Playstation: - PlaystationDecoder.DecodeTexturePlaystation(mip, specular.Format, specular.isNormalMap, - out data, out colorType); - break; - case ETexturePlatform.NintendoSwitch: - NintendoSwitchDecoder.DecodeTextureNSW(mip, specular.Format, specular.isNormalMap, - out data, out colorType); - break; - default: - TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap, - out data, out colorType); - break; - } - + var platform = UserSettings.Default.OverridedPlatform; + TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap, platform, out var data, out var colorType); switch (_game) { diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index 9755d97a..f123c33e 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -15,7 +15,7 @@ public class Model : IDisposable private BufferObject _vbo; private VertexArrayObject _vao; - private const int _vertexSize = 8; // Position + Normals + UV + private const int _vertexSize = 8; // Position + Normal + UV private const uint _faceSize = 3; // just so we don't have to do .Length private readonly uint[] _facesIndex = { 1, 0, 2 }; @@ -73,7 +73,7 @@ public class Model : IDisposable _vao = new VertexArrayObject(_gl, _vbo, _ebo); _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, _vertexSize, 0); // position - _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normals + _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normal _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv for (int section = 0; section < Sections.Length; section++) @@ -91,11 +91,27 @@ public class Model : IDisposable _shader.SetUniform("uModel", Matrix4x4.Identity); _shader.SetUniform("uView", camera.GetViewMatrix()); _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); - // _shader.SetUniform("viewPos", camera.Position); + _shader.SetUniform("viewPos", camera.Position); + + _shader.SetUniform("material.diffuse", 0); + _shader.SetUniform("material.normal", 1); + _shader.SetUniform("material.specular", 2); + // _shader.SetUniform("material.metallic", 3); + // _shader.SetUniform("material.emission", 4); + _shader.SetUniform("material.shininess", 32f); + + var lightColor = Vector3.One; + var diffuseColor = lightColor * new Vector3(0.5f); + var ambientColor = diffuseColor * new Vector3(0.2f); + + _shader.SetUniform("light.ambient", ambientColor); + _shader.SetUniform("light.diffuse", diffuseColor); // darkened + _shader.SetUniform("light.specular", Vector3.One); + _shader.SetUniform("light.position", camera.Position); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(_shader); + Sections[section].Bind(); _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index c3624ffc..f85dbcfb 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -3,6 +3,7 @@ using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; using CUE4Parse_Conversion.Textures; +using FModel.Settings; using Silk.NET.OpenGL; namespace FModel.Views.Snooper; @@ -12,8 +13,11 @@ public class Section : IDisposable private uint _handle; private GL _gl; - private Texture _albedoMap; - // private Texture _normalMap; + private Texture _diffuseMap; + private Texture _normalMap; + private Texture _specularMap; + // private Texture _metallicMap; + private Texture _emissionMap; public uint FacesCount; public int FirstFaceIndex; @@ -36,25 +40,55 @@ public class Section : IDisposable _handle = _gl.CreateProgram(); - if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse && diffuse.GetFirstMip() is { } mip) + var platform = UserSettings.Default.OverridedPlatform; + if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse) { - TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, out var data, out _); - _albedoMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + var mip = diffuse.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, platform, out var data, out _); + _diffuseMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.Normal is UTexture2D { IsVirtual: false } normal) + { + var mip = normal.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, normal.Format, normal.isNormalMap, platform, out var data, out _); + _normalMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.Specular is UTexture2D { IsVirtual: false } specular) + { + var mip = specular.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap, platform, out var data, out _); + _specularMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.HasTopEmissiveTexture && + Parameters.EmissiveColor is { A: > 0 } emissiveColor && + Parameters.Emissive is UTexture2D { IsVirtual: false } emissive) + { + var mip = emissive.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, emissive.Format, emissive.isNormalMap, platform, out var data, out _); + _emissionMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); } } - public void Bind(Shader shader) + public void Bind() { if (Parameters.IsNull) return; - shader.SetUniform("material.albedo", 0); - _albedoMap?.Bind(TextureUnit.Texture0); + _diffuseMap?.Bind(TextureUnit.Texture0); + _normalMap?.Bind(TextureUnit.Texture1); + _specularMap?.Bind(TextureUnit.Texture2); + _emissionMap?.Bind(TextureUnit.Texture4); } public void Dispose() { - _albedoMap?.Dispose(); + _diffuseMap?.Dispose(); + _normalMap?.Dispose(); + _specularMap?.Dispose(); + _emissionMap?.Dispose(); _gl.DeleteProgram(_handle); } } diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 8aee622e..403f4b8e 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -19,13 +19,14 @@ uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProjection; -out vec3 fNormal; out vec3 fPos; +out vec3 fNormal; out vec2 fTexCoords; void main() { gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); + fPos = vec3(uModel * vec4(vPos, 1.0)); fNormal = mat3(transpose(inverse(uModel))) * vNormal; fTexCoords = vTexCoords; @@ -35,25 +36,55 @@ void main() private readonly string FragmentShaderSource = @" #version 330 core -in vec3 fNormal; in vec3 fPos; +in vec3 fNormal; in vec2 fTexCoords; struct Material { - sampler2D albedo; + sampler2D diffuse; sampler2D normal; sampler2D specular; sampler2D metallic; - sampler2D emissive; + sampler2D emission; + + float shininess; +}; + +struct Light { + vec3 position; + vec3 ambient; + vec3 diffuse; + vec3 specular; }; uniform Material material; +uniform Light light; +uniform vec3 viewPos; out vec4 FragColor; void main() { - FragColor = texture(material.albedo, fTexCoords); + // ambient + vec3 ambient = light.ambient * vec3(texture(material.diffuse, fTexCoords)); + + // diffuse + vec3 norm = texture(material.normal, fTexCoords).rgb; + norm = normalize(norm * 2.0 - 1.0); + vec3 lightDir = normalize(light.position - fPos); + float diff = max(dot(norm, lightDir), 0.0f); + vec3 diffuseMap = vec3(texture(material.diffuse, fTexCoords)); + vec3 diffuse = light.diffuse * diff * diffuseMap; + + // specular + vec3 viewDir = normalize(viewPos - fPos); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess); + vec3 specularMap = vec3(texture(material.specular, fTexCoords)); + vec3 specular = light.specular * spec * specularMap; + + vec3 result = ambient + diffuse + specular; + FragColor = vec4(result, 1.0); } "; From 80e8f3f02df8779f454d0daac4196635a0f46ef4 Mon Sep 17 00:00:00 2001 From: 4sval Date: Sun, 28 Aug 2022 17:48:05 +0200 Subject: [PATCH 008/108] infinite grid skid but it was pain in the ass --- FModel/FModel.csproj | 8 +++ FModel/Resources/grid.frag | 63 +++++++++++++++++++ FModel/Resources/grid.vert | 36 +++++++++++ FModel/Resources/shader.frag | 52 ++++++++++++++++ FModel/Resources/shader.vert | 22 +++++++ FModel/Views/Snooper/Grid.cs | 70 +++++++++++++++++++++ FModel/Views/Snooper/Shader.cs | 107 ++++++-------------------------- FModel/Views/Snooper/Snooper.cs | 9 +++ 8 files changed, 278 insertions(+), 89 deletions(-) create mode 100644 FModel/Resources/grid.frag create mode 100644 FModel/Resources/grid.vert create mode 100644 FModel/Resources/shader.frag create mode 100644 FModel/Resources/shader.vert create mode 100644 FModel/Views/Snooper/Grid.cs diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 80dd9cc6..c226e803 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -93,6 +93,10 @@ + + + + @@ -102,6 +106,10 @@ + + + + diff --git a/FModel/Resources/grid.frag b/FModel/Resources/grid.frag new file mode 100644 index 00000000..b981fa45 --- /dev/null +++ b/FModel/Resources/grid.frag @@ -0,0 +1,63 @@ +#version 430 core + +// --------------------- IN --------------------- +in OUT_IN_VARIABLES { + vec3 nearPoint; + vec3 farPoint; + mat4 proj; + mat4 view; + float near; + float far; +} inVar; + +// --------------------- OUT -------------------- +out vec4 FragColor; + +// ------------------- UNIFORM ------------------ +uniform vec3 uCamDir; + +vec4 grid(vec3 fragPos, float scale) { + vec2 coord = fragPos.xz * scale; + vec2 derivative = fwidth(coord); + vec2 grid = abs(fract(coord - 0.5) - 0.5) / derivative; + float line = min(grid.x, grid.y); + float minimumz = min(derivative.y, 1) * 0.1; + float minimumx = min(derivative.x, 1) * 0.1; + vec4 color = vec4(0.149, 0.149, 0.188, 1.0 - min(line, 1.0)); + if(abs(fragPos.x) < minimumx) + color.z = 1.0; + if(abs(fragPos.z) < minimumz) + color.x = 1.0; + return color; +} + +float computeDepth(vec3 pos) { + vec4 clip_space_pos = inVar.proj * inVar.view * vec4(pos.xyz, 1.0); + float clip_space_depth = clip_space_pos.z / clip_space_pos.w; + + float far = gl_DepthRange.far; + float near = gl_DepthRange.near; + + float depth = (((far-near) * clip_space_depth) + near + far) / 2.0; + + return depth; +} + +float computeLinearDepth(vec3 pos) { + vec4 clip_space_pos = inVar.proj * inVar.view * vec4(pos.xyz, 1.0); + float clip_space_depth = (clip_space_pos.z / clip_space_pos.w) * 2.0 - 1.0; + float linearDepth = (2.0 * inVar.near * inVar.far) / (inVar.far + inVar.near - clip_space_depth * (inVar.far - inVar.near)); + return linearDepth / inVar.far; +} +void main() { + float t = -inVar.nearPoint.y / (inVar.farPoint.y - inVar.nearPoint.y); + vec3 fragPos3D = inVar.nearPoint + t * (inVar.farPoint - inVar.nearPoint); + + gl_FragDepth = computeDepth(fragPos3D); + + float linearDepth = computeLinearDepth(fragPos3D); + float fading = max(0, (0.5 - linearDepth)); + + FragColor = (grid(fragPos3D, 10) + grid(fragPos3D, 1)) * float(t > 0); + FragColor.a *= fading; +} diff --git a/FModel/Resources/grid.vert b/FModel/Resources/grid.vert new file mode 100644 index 00000000..91b6985b --- /dev/null +++ b/FModel/Resources/grid.vert @@ -0,0 +1,36 @@ +#version 430 core + +layout (location = 0) in vec3 vPos; + +// --------------------- OUT --------------------- +out OUT_IN_VARIABLES { + vec3 nearPoint; + vec3 farPoint; + mat4 proj; + mat4 view; + float near; + float far; +} outVar; + +uniform mat4 proj; +uniform mat4 view; +uniform float uNear; +uniform float uFar; + +vec3 UnprojectPoint(vec2 xy, float z) { + mat4 viewInv = inverse(view); + mat4 projInv = inverse(proj); + vec4 unprojectedPoint = viewInv * projInv * vec4(xy, z, 1.0); + return unprojectedPoint.xyz / unprojectedPoint.w; +} + +void main() +{ + outVar.near = uNear; + outVar.far = uFar; + outVar.proj = proj; + outVar.view = view; + outVar.nearPoint = UnprojectPoint(vPos.xy, -1.0).xyz; + outVar.farPoint = UnprojectPoint(vPos.xy, 1.0).xyz; + gl_Position = vec4(vPos, 1.0f); +} diff --git a/FModel/Resources/shader.frag b/FModel/Resources/shader.frag new file mode 100644 index 00000000..be50fdcf --- /dev/null +++ b/FModel/Resources/shader.frag @@ -0,0 +1,52 @@ +#version 330 core + +in vec3 fPos; +in vec3 fNormal; +in vec2 fTexCoords; + +struct Material { + sampler2D diffuse; + sampler2D normal; + sampler2D specular; + sampler2D metallic; + sampler2D emission; + + float shininess; +}; + +struct Light { + vec3 position; + vec3 ambient; + vec3 diffuse; + vec3 specular; +}; + +uniform Material material; +uniform Light light; +uniform vec3 viewPos; + +out vec4 FragColor; + +void main() +{ + // ambient + vec3 ambient = light.ambient * vec3(texture(material.diffuse, fTexCoords)); + + // diffuse + vec3 norm = texture(material.normal, fTexCoords).rgb; + norm = normalize(norm * 2.0 - 1.0); + vec3 lightDir = normalize(light.position - fPos); + float diff = max(dot(norm, lightDir), 0.0f); + vec3 diffuseMap = vec3(texture(material.diffuse, fTexCoords)); + vec3 diffuse = light.diffuse * diff * diffuseMap; + + // specular + vec3 viewDir = normalize(viewPos - fPos); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess); + vec3 specularMap = vec3(texture(material.specular, fTexCoords)); + vec3 specular = light.specular * spec * specularMap; + + vec3 result = ambient + diffuse + specular; + FragColor = vec4(result, 1.0); +} diff --git a/FModel/Resources/shader.vert b/FModel/Resources/shader.vert new file mode 100644 index 00000000..d2b4ab93 --- /dev/null +++ b/FModel/Resources/shader.vert @@ -0,0 +1,22 @@ +#version 330 core + +layout (location = 0) in vec3 vPos; +layout (location = 1) in vec3 vNormal; +layout (location = 2) in vec2 vTexCoords; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; + +out vec3 fPos; +out vec3 fNormal; +out vec2 fTexCoords; + +void main() +{ + gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); + + fPos = vec3(uModel * vec4(vPos, 1.0)); + fNormal = mat3(transpose(inverse(uModel))) * vNormal; + fTexCoords = vTexCoords; +} diff --git a/FModel/Views/Snooper/Grid.cs b/FModel/Views/Snooper/Grid.cs new file mode 100644 index 00000000..9ef3788b --- /dev/null +++ b/FModel/Views/Snooper/Grid.cs @@ -0,0 +1,70 @@ +using System; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class Grid : IDisposable +{ + private uint _handle; + private GL _gl; + + private BufferObject _ebo; + private BufferObject _vbo; + private VertexArrayObject _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(GL gl) + { + _gl = gl; + + _handle = _gl.CreateProgram(); + + _shader = new Shader(_gl, "grid.vert", "grid.frag"); + + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); + _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); + _vao = new VertexArrayObject(_gl, _vbo, _ebo); + + _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 3, 0); // position + } + + public void Bind(Camera camera) + { + _gl.DepthMask(false); + + _vao.Bind(); + + _shader.Use(); + + _shader.SetUniform("view", camera.GetViewMatrix()); + _shader.SetUniform("proj", camera.GetProjectionMatrix()); + _shader.SetUniform("uNear", -0.01f); + _shader.SetUniform("uFar", 100f); + + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint) Indices.Length); + + _gl.DepthMask(true); + } + + public void Dispose() + { + _ebo.Dispose(); + _vbo.Dispose(); + _vao.Dispose(); + _shader.Dispose(); + _gl.DeleteProgram(_handle); + } +} diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 403f4b8e..879b9eec 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -1,5 +1,7 @@ using System; +using System.IO; using System.Numerics; +using System.Reflection; using Silk.NET.OpenGL; namespace FModel.Views.Snooper; @@ -9,104 +11,28 @@ public class Shader : IDisposable private uint _handle; private GL _gl; - private readonly string VertexShaderSource = @" -#version 330 core -layout (location = 0) in vec3 vPos; -layout (location = 1) in vec3 vNormal; -layout (location = 2) in vec2 vTexCoords; + public Shader(GL gl) : this(gl, "shader.vert", "shader.frag") {} -uniform mat4 uModel; -uniform mat4 uView; -uniform mat4 uProjection; - -out vec3 fPos; -out vec3 fNormal; -out vec2 fTexCoords; - -void main() -{ - gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); - - fPos = vec3(uModel * vec4(vPos, 1.0)); - fNormal = mat3(transpose(inverse(uModel))) * vNormal; - fTexCoords = vTexCoords; -} - "; - - private readonly string FragmentShaderSource = @" -#version 330 core - -in vec3 fPos; -in vec3 fNormal; -in vec2 fTexCoords; - -struct Material { - sampler2D diffuse; - sampler2D normal; - sampler2D specular; - sampler2D metallic; - sampler2D emission; - - float shininess; -}; - -struct Light { - vec3 position; - vec3 ambient; - vec3 diffuse; - vec3 specular; -}; - -uniform Material material; -uniform Light light; -uniform vec3 viewPos; - -out vec4 FragColor; - -void main() -{ - // ambient - vec3 ambient = light.ambient * vec3(texture(material.diffuse, fTexCoords)); - - // diffuse - vec3 norm = texture(material.normal, fTexCoords).rgb; - norm = normalize(norm * 2.0 - 1.0); - vec3 lightDir = normalize(light.position - fPos); - float diff = max(dot(norm, lightDir), 0.0f); - vec3 diffuseMap = vec3(texture(material.diffuse, fTexCoords)); - vec3 diffuse = light.diffuse * diff * diffuseMap; - - // specular - vec3 viewDir = normalize(viewPos - fPos); - vec3 reflectDir = reflect(-lightDir, norm); - float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess); - vec3 specularMap = vec3(texture(material.specular, fTexCoords)); - vec3 specular = light.specular * spec * specularMap; - - vec3 result = ambient + diffuse + specular; - FragColor = vec4(result, 1.0); -} - "; - - public Shader(GL gl) + public Shader(GL gl, string vertex, string fragment) { _gl = gl; - uint vertex = LoadShader(ShaderType.VertexShader, VertexShaderSource); - uint fragment = LoadShader(ShaderType.FragmentShader, FragmentShaderSource); _handle = _gl.CreateProgram(); - _gl.AttachShader(_handle, vertex); - _gl.AttachShader(_handle, fragment); + + uint v = LoadShader(ShaderType.VertexShader, vertex); + uint f = LoadShader(ShaderType.FragmentShader, fragment); + _gl.AttachShader(_handle, v); + _gl.AttachShader(_handle, f); _gl.LinkProgram(_handle); _gl.GetProgram(_handle, GLEnum.LinkStatus, out var status); if (status == 0) { throw new Exception($"Program failed to link with error: {_gl.GetProgramInfoLog(_handle)}"); } - _gl.DetachShader(_handle, vertex); - _gl.DetachShader(_handle, fragment); - _gl.DeleteShader(vertex); - _gl.DeleteShader(fragment); + _gl.DetachShader(_handle, v); + _gl.DetachShader(_handle, f); + _gl.DeleteShader(v); + _gl.DeleteShader(f); } public void Use() @@ -160,10 +86,13 @@ void main() _gl.DeleteProgram(_handle); } - private uint LoadShader(ShaderType type, string content) + private uint LoadShader(ShaderType type, string file) { + var executingAssembly = Assembly.GetExecutingAssembly(); + using var stream = executingAssembly.GetManifestResourceStream($"{executingAssembly.GetName().Name}.Resources.{file}"); + using var reader = new StreamReader(stream); uint handle = _gl.CreateShader(type); - _gl.ShaderSource(handle, content); + _gl.ShaderSource(handle, reader.ReadToEnd()); _gl.CompileShader(handle); string infoLog = _gl.GetShaderInfoLog(handle); if (!string.IsNullOrWhiteSpace(infoLog)) diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index dc4483c6..14e5dc90 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -21,6 +21,7 @@ public class Snooper private Vector2 _previousMousePosition; private Model[] _models; + private Grid _grid; public int Width { get; } public int Height { get; } @@ -43,6 +44,7 @@ public class Snooper _window.Render += OnRender; _window.Closing += OnClose; + _grid = new Grid(); _models = new Model[1]; switch (export) { @@ -88,7 +90,11 @@ public class Snooper } _gl = GL.GetApi(_window); + _gl.Enable(EnableCap.Blend); _gl.Enable(EnableCap.DepthTest); + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + + _grid.Setup(_gl); foreach (var model in _models) { @@ -101,6 +107,8 @@ public class Snooper _gl.ClearColor(0.149f, 0.149f, 0.188f, 1.0f); _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); + _grid.Bind(_camera); + foreach (var model in _models) { model.Bind(_camera); @@ -158,6 +166,7 @@ public class Snooper private void OnClose() { + _grid.Dispose(); foreach (var model in _models) { model.Dispose(); From 9f3d9b33416bc007cf747b987d19af3705220017 Mon Sep 17 00:00:00 2001 From: 4sval Date: Sun, 28 Aug 2022 18:15:15 +0200 Subject: [PATCH 009/108] hello imgui --- FModel/FModel.csproj | 1 + FModel/Views/Snooper/Snooper.cs | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index c226e803..9b6e0217 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -131,6 +131,7 @@ + diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 14e5dc90..6a932aac 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -8,6 +8,7 @@ using CUE4Parse_Conversion.Meshes; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; using Silk.NET.Windowing; namespace FModel.Views.Snooper; @@ -15,6 +16,8 @@ namespace FModel.Views.Snooper; public class Snooper { private IWindow _window; + private IInputContext _input; + private ImGuiController _controller; private GL _gl; private Camera _camera; private IKeyboard _keyboard; @@ -43,6 +46,7 @@ public class Snooper _window.Update += OnUpdate; _window.Render += OnRender; _window.Closing += OnClose; + _window.FramebufferResize += OnFramebufferResize; _grid = new Grid(); _models = new Model[1]; @@ -79,10 +83,10 @@ public class Snooper private void OnLoad() { - var input = _window.CreateInput(); - _keyboard = input.Keyboards[0]; + _input = _window.CreateInput(); + _keyboard = _input.Keyboards[0]; _keyboard.KeyDown += KeyDown; - foreach (var mouse in input.Mice) + foreach (var mouse in _input.Mice) { mouse.Cursor.CursorMode = CursorMode.Raw; mouse.MouseMove += OnMouseMove; @@ -94,6 +98,8 @@ public class Snooper _gl.Enable(EnableCap.DepthTest); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + _controller = new ImGuiController(_gl, _window, _input); + _grid.Setup(_gl); foreach (var model in _models) @@ -102,8 +108,15 @@ public class Snooper } } + private void OnFramebufferResize(Vector2D size) + { + _gl.Viewport(size); + } + private void OnRender(double deltaTime) { + _controller.Update((float) deltaTime); + _gl.ClearColor(0.149f, 0.149f, 0.188f, 1.0f); _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); @@ -113,6 +126,10 @@ public class Snooper { model.Bind(_camera); } + + ImGuiNET.ImGui.ShowAboutWindow(); + + _controller.Render(); } private void OnUpdate(double deltaTime) @@ -171,6 +188,9 @@ public class Snooper { model.Dispose(); } + _input.Dispose(); + _controller.Dispose(); + _gl.Dispose(); } private void KeyDown(IKeyboard keyboard, Key key, int arg3) From f7c1a764f7c987b37d96676becb9a8f6210fca99 Mon Sep 17 00:00:00 2001 From: 4sval Date: Sun, 28 Aug 2022 23:43:20 +0200 Subject: [PATCH 010/108] basic layout --- FModel/Views/Snooper/Model.cs | 17 +++++-- FModel/Views/Snooper/Section.cs | 27 ++++++++-- FModel/Views/Snooper/Snooper.cs | 90 +++++++++++++++++++++++++++++---- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index f123c33e..f5795953 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Numerics; using CUE4Parse_Conversion.Meshes.PSK; +using ImGuiNET; using Silk.NET.OpenGL; namespace FModel.Views.Snooper; @@ -21,12 +22,15 @@ public class Model : IDisposable private Shader _shader; + public string Name; public uint[] Indices; public float[] Vertices; public Section[] Sections; - public Model(CBaseMeshLod lod, CMeshVertex[] vertices) + public Model(string name, CBaseMeshLod lod, CMeshVertex[] vertices) { + Name = name; + var sections = lod.Sections.Value; Sections = new Section[sections.Length]; Indices = new uint[sections.Sum(section => section.NumFaces * _faceSize)]; @@ -35,7 +39,7 @@ public class Model : IDisposable for (var s = 0; s < sections.Length; s++) { var section = sections[s]; - Sections[s] = new Section((uint) section.NumFaces * _faceSize, section.FirstIndex, section); + Sections[s] = new Section(section.MaterialName, (uint) section.NumFaces * _faceSize, section.FirstIndex, section); for (uint face = 0; face < section.NumFaces; face++) { foreach (var f in _facesIndex) @@ -109,12 +113,15 @@ public class Model : IDisposable _shader.SetUniform("light.specular", Vector3.One); _shader.SetUniform("light.position", camera.Position); + ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); + ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Material"); + ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(); - - _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); + Sections[section].Bind(section); } + ImGui.EndTable(); } public void Dispose() diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index f85dbcfb..584d21d7 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -4,6 +4,7 @@ using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; using CUE4Parse_Conversion.Textures; using FModel.Settings; +using ImGuiNET; using Silk.NET.OpenGL; namespace FModel.Views.Snooper; @@ -19,12 +20,14 @@ public class Section : IDisposable // private Texture _metallicMap; private Texture _emissionMap; + public string Name; public uint FacesCount; public int FirstFaceIndex; public CMaterialParams Parameters; - public Section(uint facesCount, int firstFaceIndex, CMeshSection section) + public Section(string name, uint facesCount, int firstFaceIndex, CMeshSection section) { + Name = name; FacesCount = facesCount; FirstFaceIndex = firstFaceIndex; Parameters = new CMaterialParams(); @@ -72,11 +75,27 @@ public class Section : IDisposable } } - public void Bind() + public void Bind(int index) { - if (Parameters.IsNull) - return; + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text(index.ToString()); + ImGui.TableSetColumnIndex(1); + ImGui.Text(Name); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text($"Faces: {FacesCount}"); + ImGui.Text($"Face First Index: {FirstFaceIndex}"); + ImGui.Separator(); + ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); + ImGui.EndTooltip(); + } + + _gl.DrawArrays(PrimitiveType.Triangles, FirstFaceIndex, FacesCount); + + if (Parameters.IsNull) return; _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); _specularMap?.Bind(TextureUnit.Texture2); diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 6a932aac..6e08f056 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -5,6 +5,7 @@ using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse_Conversion.Meshes; +using ImGuiNET; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; @@ -31,7 +32,7 @@ public class Snooper public Snooper(UObject export) { - const double ratio = .6; + const double ratio = .7; var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; Width = Convert.ToInt32(x * ratio); @@ -54,13 +55,13 @@ public class Snooper { case UStaticMesh st when st.TryConvert(out var mesh): { - _models[0] = new Model(mesh.LODs[0], mesh.LODs[0].Verts); + _models[0] = new Model(st.Name, mesh.LODs[0], mesh.LODs[0].Verts); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } case USkeletalMesh sk when sk.TryConvert(out var mesh): { - _models[0] = new Model(mesh.LODs[0], mesh.LODs[0].Verts); + _models[0] = new Model(sk.Name, mesh.LODs[0], mesh.LODs[0].Verts); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } @@ -88,9 +89,9 @@ public class Snooper _keyboard.KeyDown += KeyDown; foreach (var mouse in _input.Mice) { - mouse.Cursor.CursorMode = CursorMode.Raw; - mouse.MouseMove += OnMouseMove; - mouse.Scroll += OnMouseWheel; + // mouse.Cursor.CursorMode = CursorMode.Raw; + // mouse.MouseMove += OnMouseMove; + // mouse.Scroll += OnMouseWheel; } _gl = GL.GetApi(_window); @@ -122,12 +123,23 @@ public class Snooper _grid.Bind(_camera); + var padding = Theme(); + ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoBackground); + ImGui.SetWindowSize(new Vector2(Width / 4f, Height)); + ImGui.SetWindowPos(new Vector2(0)); + foreach (var model in _models) { + ImGui.Text($"Entity: {model.Name}"); model.Bind(_camera); + ImGui.Separator(); } - ImGuiNET.ImGui.ShowAboutWindow(); + float framerate = ImGui.GetIO().Framerate; + string f = $"FPS: {framerate:0.#} ({1000.0f / framerate:0.##} ms)"; + ImGui.SetCursorPosY(ImGui.GetWindowHeight() - padding.Y - ImGui.CalcTextSize(f).Y); + ImGui.Text(f); + ImGui.End(); _controller.Render(); } @@ -195,9 +207,69 @@ public class Snooper private void KeyDown(IKeyboard keyboard, Key key, int arg3) { - if (key == Key.Escape) + switch (key) { - _window.Close(); + case Key.Escape: + _window.Close(); + break; } } + + private Vector2 Theme() + { + var style = ImGui.GetStyle(); + style.FrameRounding = 4.0f; + style.GrabRounding = 4.0f; + + style.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); + style.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); + style.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + style.Colors[(int) ImGuiCol.ChildBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + style.Colors[(int) ImGuiCol.PopupBg] = new Vector4(0.08f, 0.08f, 0.08f, 0.94f); + style.Colors[(int) ImGuiCol.Border] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + style.Colors[(int) ImGuiCol.BorderShadow] = new Vector4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + style.Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.12f, 0.20f, 0.28f, 1.00f); + style.Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.09f, 0.12f, 0.14f, 1.00f); + style.Colors[(int) ImGuiCol.TitleBg] = new Vector4(0.09f, 0.12f, 0.14f, 0.65f); + style.Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + style.Colors[(int) ImGuiCol.TitleBgCollapsed] = new Vector4(0.00f, 0.00f, 0.00f, 0.51f); + style.Colors[(int) ImGuiCol.MenuBarBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + style.Colors[(int) ImGuiCol.ScrollbarBg] = new Vector4(0.02f, 0.02f, 0.02f, 0.39f); + style.Colors[(int) ImGuiCol.ScrollbarGrab] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + style.Colors[(int) ImGuiCol.ScrollbarGrabHovered] = new Vector4(0.18f, 0.22f, 0.25f, 1.00f); + style.Colors[(int) ImGuiCol.ScrollbarGrabActive] = new Vector4(0.09f, 0.21f, 0.31f, 1.00f); + style.Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + style.Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + style.Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.37f, 0.61f, 1.00f, 1.00f); + style.Colors[(int) ImGuiCol.Button] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + style.Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + style.Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.06f, 0.53f, 0.98f, 1.00f); + style.Colors[(int) ImGuiCol.Header] = new Vector4(0.20f, 0.25f, 0.29f, 0.55f); + style.Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + style.Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + style.Colors[(int) ImGuiCol.Separator] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + style.Colors[(int) ImGuiCol.SeparatorHovered] = new Vector4(0.10f, 0.40f, 0.75f, 0.78f); + style.Colors[(int) ImGuiCol.SeparatorActive] = new Vector4(0.10f, 0.40f, 0.75f, 1.00f); + style.Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.26f, 0.59f, 0.98f, 0.25f); + style.Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.67f); + style.Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.26f, 0.59f, 0.98f, 0.95f); + style.Colors[(int) ImGuiCol.Tab] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + style.Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + style.Colors[(int) ImGuiCol.TabActive] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + style.Colors[(int) ImGuiCol.TabUnfocused] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + style.Colors[(int) ImGuiCol.TabUnfocusedActive] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + style.Colors[(int) ImGuiCol.PlotLines] = new Vector4(0.61f, 0.61f, 0.61f, 1.00f); + style.Colors[(int) ImGuiCol.PlotLinesHovered] = new Vector4(1.00f, 0.43f, 0.35f, 1.00f); + style.Colors[(int) ImGuiCol.PlotHistogram] = new Vector4(0.90f, 0.70f, 0.00f, 1.00f); + style.Colors[(int) ImGuiCol.PlotHistogramHovered] = new Vector4(1.00f, 0.60f, 0.00f, 1.00f); + style.Colors[(int) ImGuiCol.TextSelectedBg] = new Vector4(0.26f, 0.59f, 0.98f, 0.35f); + style.Colors[(int) ImGuiCol.DragDropTarget] = new Vector4(1.00f, 1.00f, 0.00f, 0.90f); + style.Colors[(int) ImGuiCol.NavHighlight] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + style.Colors[(int) ImGuiCol.NavWindowingHighlight] = new Vector4(1.00f, 1.00f, 1.00f, 0.70f); + style.Colors[(int) ImGuiCol.NavWindowingDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.20f); + style.Colors[(int) ImGuiCol.ModalWindowDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.35f); + + return style.WindowPadding; + } } From 0b01400493672ecabcdd6a16aaecafb6907fc737 Mon Sep 17 00:00:00 2001 From: 4sval Date: Mon, 29 Aug 2022 01:10:57 +0200 Subject: [PATCH 011/108] hmm --- FModel/Views/Snooper/Model.cs | 8 +++++-- FModel/Views/Snooper/Section.cs | 8 ++++--- FModel/Views/Snooper/Snooper.cs | 42 +++++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index f5795953..9dfdc00a 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -88,6 +88,8 @@ public class Model : IDisposable public void Bind(Camera camera) { + ImGui.Text($"Entity: {Name}"); + _vao.Bind(); _shader.Use(); @@ -115,13 +117,15 @@ public class Model : IDisposable ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Material"); + ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(section); + Sections[section].Bind(section, Indices.Length); } ImGui.EndTable(); + + ImGui.Separator(); } public void Dispose() diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 584d21d7..b43de0a0 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; @@ -75,7 +76,7 @@ public class Section : IDisposable } } - public void Bind(int index) + public void Bind(int index, float indices) { ImGui.TableNextRow(); @@ -86,10 +87,11 @@ public class Section : IDisposable if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - ImGui.Text($"Faces: {FacesCount}"); - ImGui.Text($"Face First Index: {FirstFaceIndex}"); + ImGui.Text($"Faces: {FacesCount} ({Math.Floor(FacesCount / indices * 100f)}%%)"); + ImGui.Text($"First Face: {FirstFaceIndex}"); ImGui.Separator(); ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); + ImGui.Text($"Normal: ({Parameters.Normal?.ExportType}) {Parameters.Normal?.Name}"); ImGui.EndTooltip(); } diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 6e08f056..fdaf5133 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -22,6 +22,7 @@ public class Snooper private GL _gl; private Camera _camera; private IKeyboard _keyboard; + private IMouse _mouse; private Vector2 _previousMousePosition; private Model[] _models; @@ -40,6 +41,8 @@ public class Snooper var options = WindowOptions.Default; options.Size = new Vector2D(Width, Height); + options.WindowBorder = WindowBorder.Hidden; + options.Position = new Vector2D(5, 5); // this doesn't fucking work WTF options.Title = "Snooper"; _window = Window.Create(options); @@ -87,12 +90,11 @@ public class Snooper _input = _window.CreateInput(); _keyboard = _input.Keyboards[0]; _keyboard.KeyDown += KeyDown; - foreach (var mouse in _input.Mice) - { - // mouse.Cursor.CursorMode = CursorMode.Raw; - // mouse.MouseMove += OnMouseMove; - // mouse.Scroll += OnMouseWheel; - } + _mouse = _input.Mice[0]; + _mouse.MouseDown += OnMouseDown; + _mouse.MouseUp += OnMouseUp; + _mouse.MouseMove += OnMouseMove; + _mouse.Scroll += OnMouseWheel; _gl = GL.GetApi(_window); _gl.Enable(EnableCap.Blend); @@ -130,9 +132,7 @@ public class Snooper foreach (var model in _models) { - ImGui.Text($"Entity: {model.Name}"); model.Bind(_camera); - ImGui.Separator(); } float framerate = ImGui.GetIO().Framerate; @@ -174,17 +174,33 @@ public class Snooper } } + private void OnMouseDown(IMouse mouse, MouseButton button) + { + if (button != MouseButton.Left) return; + mouse.Cursor.CursorMode = CursorMode.Raw; + } + + private void OnMouseUp(IMouse mouse, MouseButton button) + { + if (button != MouseButton.Left) return; + mouse.Cursor.CursorMode = CursorMode.Normal; + } + private void OnMouseMove(IMouse mouse, Vector2 position) { - const float lookSensitivity = 0.1f; if (_previousMousePosition == default) { _previousMousePosition = position; } else { - var xOffset = (position.X - _previousMousePosition.X) * lookSensitivity; - var yOffset = (position.Y - _previousMousePosition.Y) * lookSensitivity; - _previousMousePosition = position; + if (mouse.Cursor.CursorMode == CursorMode.Raw) + { + const float lookSensitivity = 0.1f; + var xOffset = (position.X - _previousMousePosition.X) * lookSensitivity; + var yOffset = (position.Y - _previousMousePosition.Y) * lookSensitivity; - _camera.ModifyDirection(xOffset, yOffset); + _camera.ModifyDirection(xOffset, yOffset); + } + + _previousMousePosition = position; } } From 8597e491e0bb4d92bc4cdf8a09f99650007f25eb Mon Sep 17 00:00:00 2001 From: 4sval Date: Mon, 29 Aug 2022 01:20:28 +0200 Subject: [PATCH 012/108] get back here --- FModel/Views/Snooper/Model.cs | 1 + FModel/Views/Snooper/Section.cs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index 9dfdc00a..6ab314c1 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -122,6 +122,7 @@ public class Model : IDisposable for (int section = 0; section < Sections.Length; section++) { Sections[section].Bind(section, Indices.Length); + _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index b43de0a0..3a3e0d61 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -95,8 +95,6 @@ public class Section : IDisposable ImGui.EndTooltip(); } - _gl.DrawArrays(PrimitiveType.Triangles, FirstFaceIndex, FacesCount); - if (Parameters.IsNull) return; _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); From b2002121abdf26f9badb5c54367b8d898e54eae8 Mon Sep 17 00:00:00 2001 From: 4sval Date: Mon, 29 Aug 2022 02:07:56 +0200 Subject: [PATCH 013/108] fixed material index + wrong gl + draw distance + grid fading --- FModel/Views/Snooper/Camera.cs | 10 +++++++--- FModel/Views/Snooper/Grid.cs | 4 ++-- FModel/Views/Snooper/Model.cs | 4 ++-- FModel/Views/Snooper/Section.cs | 12 +++++++----- FModel/Views/Snooper/Snooper.cs | 3 ++- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/FModel/Views/Snooper/Camera.cs b/FModel/Views/Snooper/Camera.cs index 0409ff1b..6d68683f 100644 --- a/FModel/Views/Snooper/Camera.cs +++ b/FModel/Views/Snooper/Camera.cs @@ -13,13 +13,17 @@ public class Camera public float Yaw { get; set; } = -90f; public float Pitch { get; set; } = 0f; public float Zoom { get; set; } = 45f; - public float Speed { get; set; } = 1f; + public float Speed { get; } = 1f; + public float Near { get; } = 0.01f; + public float Far { get; } = 100f; public float AspectRatio => 16f / 9f; - public Camera(Vector3 position, Vector3 direction, float speed) + public Camera(Vector3 position, Vector3 direction, float near, float far, float speed) { Position = position; Direction = direction; + Near = near; + Far = far; Speed = speed; // trigonometric math to calculate the cam's yaw/pitch based on position and direction to look @@ -52,7 +56,7 @@ public class Camera public Matrix4x4 GetProjectionMatrix() { - return Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, 0.1f, 100.0f); + return Matrix4x4.CreatePerspectiveFieldOfView(Helper.DegreesToRadians(Zoom), AspectRatio, Near, Far); } public void CalculateDirection() diff --git a/FModel/Views/Snooper/Grid.cs b/FModel/Views/Snooper/Grid.cs index 9ef3788b..0e6a2e3c 100644 --- a/FModel/Views/Snooper/Grid.cs +++ b/FModel/Views/Snooper/Grid.cs @@ -51,8 +51,8 @@ public class Grid : IDisposable _shader.SetUniform("view", camera.GetViewMatrix()); _shader.SetUniform("proj", camera.GetProjectionMatrix()); - _shader.SetUniform("uNear", -0.01f); - _shader.SetUniform("uFar", 100f); + _shader.SetUniform("uNear", camera.Near); + _shader.SetUniform("uFar", camera.Far); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint) Indices.Length); diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index 6ab314c1..a342d817 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -39,7 +39,7 @@ public class Model : IDisposable for (var s = 0; s < sections.Length; s++) { var section = sections[s]; - Sections[s] = new Section(section.MaterialName, (uint) section.NumFaces * _faceSize, section.FirstIndex, section); + Sections[s] = new Section(section.MaterialName, section.MaterialIndex, (uint) section.NumFaces * _faceSize, section.FirstIndex, section); for (uint face = 0; face < section.NumFaces; face++) { foreach (var f in _facesIndex) @@ -121,7 +121,7 @@ public class Model : IDisposable ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(section, Indices.Length); + Sections[section].Bind(Indices.Length); _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 3a3e0d61..b2806f6a 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -1,5 +1,4 @@ using System; -using System.Numerics; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; @@ -22,18 +21,21 @@ public class Section : IDisposable private Texture _emissionMap; public string Name; + public int Index; public uint FacesCount; public int FirstFaceIndex; public CMaterialParams Parameters; - public Section(string name, uint facesCount, int firstFaceIndex, CMeshSection section) + public Section(string name, int index, uint facesCount, int firstFaceIndex, CMeshSection section) { Name = name; + Index = index; FacesCount = facesCount; FirstFaceIndex = firstFaceIndex; Parameters = new CMaterialParams(); if (section.Material != null && section.Material.TryLoad(out var material) && material is UMaterialInterface unrealMaterial) { + Name = unrealMaterial.Name; unrealMaterial.GetParams(Parameters); } } @@ -76,18 +78,18 @@ public class Section : IDisposable } } - public void Bind(int index, float indices) + public void Bind(float indices) { ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); - ImGui.Text(index.ToString()); + ImGui.Text(Index.ToString()); ImGui.TableSetColumnIndex(1); ImGui.Text(Name); if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - ImGui.Text($"Faces: {FacesCount} ({Math.Floor(FacesCount / indices * 100f)}%%)"); + ImGui.Text($"Faces: {FacesCount} ({Math.Round(FacesCount / indices * 100f, 2)}%%)"); ImGui.Text($"First Face: {FirstFaceIndex}"); ImGui.Separator(); ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index fdaf5133..50c83c07 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -80,9 +80,10 @@ public class Snooper private void SetupCamera(FBox box) { + var far = box.Max.Max(); var center = box.GetCenter(); var position = new Vector3(0f, center.Z, box.Max.Y * 3); - _camera = new Camera(position, center, box.Max.Max() / 2f); + _camera = new Camera(position, center, 0.01f, far * 50f, far / 2f); } private void OnLoad() From 0c5ede60af2e4d037848ce178f9a8a93570eda2e Mon Sep 17 00:00:00 2001 From: 4sval Date: Mon, 29 Aug 2022 19:48:31 +0200 Subject: [PATCH 014/108] fuck you skybox --- FModel/Extensions/ImGuiExtensions.cs | 84 +++++++++++++++++++ FModel/FModel.csproj | 11 ++- FModel/Resources/grid.frag | 2 +- FModel/Resources/nx.png | Bin 0 -> 12380 bytes FModel/Resources/ny.png | Bin 0 -> 12681 bytes FModel/Resources/nz.png | Bin 0 -> 20239 bytes FModel/Resources/px.png | Bin 0 -> 15181 bytes FModel/Resources/py.png | Bin 0 -> 19207 bytes FModel/Resources/pz.png | Bin 0 -> 13160 bytes FModel/Resources/skybox.frag | 12 +++ FModel/Resources/skybox.vert | 16 ++++ FModel/Views/Snooper/Model.cs | 8 +- FModel/Views/Snooper/Section.cs | 10 +-- FModel/Views/Snooper/Skybox.cs | 115 ++++++++++++++++++++++++++ FModel/Views/Snooper/Snooper.cs | 117 ++++++++++----------------- FModel/Views/Snooper/Texture.cs | 75 ++++++++++++++--- 16 files changed, 352 insertions(+), 98 deletions(-) create mode 100644 FModel/Extensions/ImGuiExtensions.cs create mode 100644 FModel/Resources/nx.png create mode 100644 FModel/Resources/ny.png create mode 100644 FModel/Resources/nz.png create mode 100644 FModel/Resources/px.png create mode 100644 FModel/Resources/py.png create mode 100644 FModel/Resources/pz.png create mode 100644 FModel/Resources/skybox.frag create mode 100644 FModel/Resources/skybox.vert create mode 100644 FModel/Views/Snooper/Skybox.cs diff --git a/FModel/Extensions/ImGuiExtensions.cs b/FModel/Extensions/ImGuiExtensions.cs new file mode 100644 index 00000000..88f749d6 --- /dev/null +++ b/FModel/Extensions/ImGuiExtensions.cs @@ -0,0 +1,84 @@ +using System.Numerics; +using ImGuiNET; + +namespace FModel.Extensions; + +public static class ImGuiExtensions +{ + public const float PADDING = 5.0f; + public static ImGuiStylePtr STYLE = ImGui.GetStyle(); + + public static void DrawFPS() + { + const ImGuiWindowFlags flags = + ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoNav; + var viewport = ImGui.GetMainViewport(); + var work_pos = viewport.WorkPos; + var work_size = viewport.WorkSize; + + ImGui.SetNextWindowPos(new Vector2(work_pos.X + PADDING, work_pos.Y + work_size.Y - PADDING), ImGuiCond.Always, new Vector2(0, 1)); + if (ImGui.Begin("FPS Overlay", flags)) + { + float framerate = ImGui.GetIO().Framerate; + ImGui.Text($"FPS: {framerate:0} ({1000.0f / framerate:0.##} ms)"); + } + ImGui.End(); + } + + public static void Theme() + { + STYLE.FrameRounding = 4.0f; + STYLE.GrabRounding = 4.0f; + + STYLE.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); + STYLE.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.11f, 0.15f, 0.17f, 0.35f); + STYLE.Colors[(int) ImGuiCol.ChildBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + STYLE.Colors[(int) ImGuiCol.PopupBg] = new Vector4(0.08f, 0.08f, 0.08f, 0.94f); + STYLE.Colors[(int) ImGuiCol.Border] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + STYLE.Colors[(int) ImGuiCol.BorderShadow] = new Vector4(0.00f, 0.00f, 0.00f, 0.00f); + STYLE.Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + STYLE.Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.12f, 0.20f, 0.28f, 1.00f); + STYLE.Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.09f, 0.12f, 0.14f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TitleBg] = new Vector4(0.09f, 0.12f, 0.14f, 0.65f); + STYLE.Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TitleBgCollapsed] = new Vector4(0.00f, 0.00f, 0.00f, 0.51f); + STYLE.Colors[(int) ImGuiCol.MenuBarBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ScrollbarBg] = new Vector4(0.02f, 0.02f, 0.02f, 0.39f); + STYLE.Colors[(int) ImGuiCol.ScrollbarGrab] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ScrollbarGrabHovered] = new Vector4(0.18f, 0.22f, 0.25f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ScrollbarGrabActive] = new Vector4(0.09f, 0.21f, 0.31f, 1.00f); + STYLE.Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.37f, 0.61f, 1.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.Button] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.06f, 0.53f, 0.98f, 1.00f); + STYLE.Colors[(int) ImGuiCol.Header] = new Vector4(0.20f, 0.25f, 0.29f, 0.55f); + STYLE.Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + STYLE.Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + STYLE.Colors[(int) ImGuiCol.Separator] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + STYLE.Colors[(int) ImGuiCol.SeparatorHovered] = new Vector4(0.10f, 0.40f, 0.75f, 0.78f); + STYLE.Colors[(int) ImGuiCol.SeparatorActive] = new Vector4(0.10f, 0.40f, 0.75f, 1.00f); + STYLE.Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.26f, 0.59f, 0.98f, 0.25f); + STYLE.Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.67f); + STYLE.Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.26f, 0.59f, 0.98f, 0.95f); + STYLE.Colors[(int) ImGuiCol.Tab] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + STYLE.Colors[(int) ImGuiCol.TabActive] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TabUnfocused] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TabUnfocusedActive] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + STYLE.Colors[(int) ImGuiCol.PlotLines] = new Vector4(0.61f, 0.61f, 0.61f, 1.00f); + STYLE.Colors[(int) ImGuiCol.PlotLinesHovered] = new Vector4(1.00f, 0.43f, 0.35f, 1.00f); + STYLE.Colors[(int) ImGuiCol.PlotHistogram] = new Vector4(0.90f, 0.70f, 0.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.PlotHistogramHovered] = new Vector4(1.00f, 0.60f, 0.00f, 1.00f); + STYLE.Colors[(int) ImGuiCol.TextSelectedBg] = new Vector4(0.26f, 0.59f, 0.98f, 0.35f); + STYLE.Colors[(int) ImGuiCol.DragDropTarget] = new Vector4(1.00f, 1.00f, 0.00f, 0.90f); + STYLE.Colors[(int) ImGuiCol.NavHighlight] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + STYLE.Colors[(int) ImGuiCol.NavWindowingHighlight] = new Vector4(1.00f, 1.00f, 1.00f, 0.70f); + STYLE.Colors[(int) ImGuiCol.NavWindowingDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.20f); + STYLE.Colors[(int) ImGuiCol.ModalWindowDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.35f); + } +} diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 9b6e0217..01d7d569 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -97,6 +97,8 @@ + + @@ -110,6 +112,8 @@ + + @@ -132,7 +136,6 @@ - @@ -202,6 +205,12 @@ + + + + + + diff --git a/FModel/Resources/grid.frag b/FModel/Resources/grid.frag index b981fa45..7fc9f0d2 100644 --- a/FModel/Resources/grid.frag +++ b/FModel/Resources/grid.frag @@ -23,7 +23,7 @@ vec4 grid(vec3 fragPos, float scale) { float line = min(grid.x, grid.y); float minimumz = min(derivative.y, 1) * 0.1; float minimumx = min(derivative.x, 1) * 0.1; - vec4 color = vec4(0.149, 0.149, 0.188, 1.0 - min(line, 1.0)); + vec4 color = vec4(0.102, 0.102, 0.129, 1.0 - min(line, 1.0)); if(abs(fragPos.x) < minimumx) color.z = 1.0; if(abs(fragPos.z) < minimumz) diff --git a/FModel/Resources/nx.png b/FModel/Resources/nx.png new file mode 100644 index 0000000000000000000000000000000000000000..a6896403c2ff1360298641460da97edcd9adb764 GIT binary patch literal 12380 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 literal 0 HcmV?d00001 diff --git a/FModel/Resources/ny.png b/FModel/Resources/ny.png new file mode 100644 index 0000000000000000000000000000000000000000..73349e3cc0174d342aeea6ec2d7a5c350d63c43b GIT binary patch literal 12681 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 literal 0 HcmV?d00001 diff --git a/FModel/Resources/nz.png b/FModel/Resources/nz.png new file mode 100644 index 0000000000000000000000000000000000000000..8e6b10155479b8bd266628b55b313478fea6995b GIT binary patch literal 20239 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 literal 0 HcmV?d00001 diff --git a/FModel/Resources/pz.png b/FModel/Resources/pz.png new file mode 100644 index 0000000000000000000000000000000000000000..7f825fbb961ac25c68cc607d074e278e103e43c2 GIT binary patch literal 13160 zcmXAwXE+?++s9|uvP+cRMMMd!ccKIlW%U|l)#yZ`L=8dI)e_N(5`9HqqDJo`YDA0P zHX^_1q7&`$zn)h!^J305=RW7$?em>?dOB(pWDH~g08nVCD`NlvbaM*=VBnj}1YBO^ ze@~tmHASG}JJUJG=vodfXS`fVYVBIux*Sw$Z6|$ksXfp?IQV+-!c%x@agjB3%6VR*LBMLE zKK6yTpjDe6XQlCW(@P;=fBzS|5fur;C5$!644h^?cU32aY{fSCd@U%`1Z*qI_z#O) z9pvLHAkTgq&%0e)w)wr1&i1_+`6FhmHLY7b@%yEXShKHOZWGNiU3w|~AEO_}^YaaE z>*M4U<^e6fubMpk{Wrhsb<)3JA-w8gYW5g{w2Fy%Z#Q_USp1P!yGOyr^PjtB{0HD- zn;i5(z8?9`_nmXI5UG2U`OF8og@uLDMGbJ)h`oyOSN=_CF6^EX(at8U24tykRk0B> z|5CQ$@SjM3BzZ+_^c*wD4}o&!Zx>D@bw$7rMGfL)+VGlJrBS1~TTSV})b9?zqm84@ z=_uy#m-C*qOy&$Pz=saFH%J3bh|y?Ti>)JvL5Ys{wly^w=Y|j_FLaGX=9_8%U=2m% zJeeV9SFSpwNZKKELv63_58m~H`ROH_Aj_>?vz&6 z|Elc1JND1V$A=fh(XC1ZrYvoaz)X~GdDvmqJIlR{Xp62a9docGnv?e^)Cbg!Oh`Yw zWFlpLB#;vy>ty?Lga68pz7C^)petAbA{F(bb2a!`2@XJM<5m>Fcf<8K#=4=mq5t@n zg|i(9yYmohDu^ z^&hFWg0{7jI}90z%!Q4VC?em$;MYUYa*8Cfcw4ZKE}wr2QPofyqzV~{tHj)@V;(el zWP#SfqFPt3c!!gk@_B1$o79e{6VWnKWiPq2fEeb2eg_w0%I8dE_<5VH{l#Q7KEU#a zF>>v_u3R-TGEbP)`rzei5KEI2duLE2m4kJ9#!J=eR=)I9b(}%KfyEYg2pC*&B@AdX zfmAVl&wkXqXkLnel27wiAc1KdQ93W1CVC;i`L}banz|!_iiFrd^NRJWIeWh2QnwS? zXl2d`<6Myv{f{&ld$Qx5Y3#x*zxLb-eE3mWAlCdyH+{4Jw-%|LPeADdp&|6E=_?CL znaz9DD0G0vGP}Mdg{VUF1Jgq$Vo39u!A>!WlbzbF{C)><7@??Nb5-9-f+U zXnHP#M7Ldw0jIIsD()Y%jL> z7zTe+qUX*Cna`oo+WYr(w9Go5ob;AVV_N{x$)QM%+AkI@)D!!vUQHdJp4;hjd|(rN z$(4GOZuc9JVMv0ub)phoN>4*5)Yx^Tc@W}oI&Hw+Lrx0Vyf)Y5#(d4iH1t66E}uiz zdZF-@o!BfLZlASKX$yBAf5(Y_76UGKA0K8>_g&`Su|7RY z3NOxgT6X7{7ARd-K6X~|WCH`x} zv5tkw&qY8h%2na5Kcw6%2*#zq?87kjn#DO7p{At&0^|-}op&vcvG4{xF|P*G5bqPF z)WI5H=Xq6J+Vze58cads4lA1j)WCJ@=!gEQ0d};nZ*dN)#}q!GdTAx1f>QXRlMIt4Q3woXG>`z072WdcdML%DL0ez%xY*EKq&f3vmzQ<*8J2 zxW!}$NL5KFH=giJa}h0hhUa?MiZSb6rOKO3+BLB%2;Xfr?J3ml9dm3w6(RF3n33vfuta1?1YSNo{Yc-Wn4}*|MM}_dr!AtWGt!WrJ~2xHzTvSFh)?MBP>mzZf#a`j+Ivuu@li(ujnnCezV% z)ok9m1nH;uKS+$-<2j1jU}61Y^z2<82b_N9DAy|ZGUZoi@b$Ts3h4%k27?ARGn0`X z^Z~$JsDm3NYv3m`cQb&Bk@sKzeAi65gQPG7qtNAw#ZGQxGFD2Z-%VCp44)L*o!nkc zDoH_y%MU{OH^u(tH026h#2VmpIYf_?W3|lYy>;p;64ZTmXTg1`^<*R+V^Q0hUiPD z_V3CVe%`6v&u*u!<$k8)J0yCt93ggm+HZqP7*`3eX(Lb3bey1i7yU)mo_YM^KsGcX zhn~roB!=KP{v31C#~yk33E`dkXx{UbEmFZ*)&8w&be{JA)+GuMcTEDWPlB)2d<1pF z#u??C9?S}jj~ISAO?ZBBIH6l=rAy&HS->V7#O=~3iQ77MGL$_P{FN#e{U#sBnx~!n zTbioNq8FqAoVDKSSkY$=F*57;@s?jHSNa+a5HF|S&n3}Tur7RF3QB_f`fy9i>XY60 z(h8RIB5IXZR=h15o&Bc_1WdKYnZl4#*$9(hF8iHj<_Lr zni>plvFgh`ort_g09f0=Wwv5f-wmnbVXUtf&`XJYu0PpG(aT9&kJVT$JAwq+0cxyQ z;^oi2@umiAvc@k?GmAE?nQx1pfS>fmOP!7}2`e}~G9_jpt`c4S2UdP~kt8l$jt zL{r*^O)z?~D=(PDAl~a0FFoOkT8j@B^(c7lJc?ud?15&EyYV>syZhXAN_3O|(NP1f z(O%+nB#a1D5K`1ojTejwTWX&vc%$HFcz0GcJ2I)OMP!_|%l0wfdpo4IWdB?C8;7nP z{A__Bc<&sncQn-?W%=`DsmcomKMl&U@X7FZwR|N_tmkx(fw%x=D>fj_F=)&!+HYmy zTo_3e7dOsT{SkC7d>-T}x35u#-Z``1ST$7x;(jIo>rHEK+SLdVGd=s{O$VEw!N;|- zdyzq~#ylieibd~*fCObfYYJ7Y?3=^N z-t_c)qGBwCHHLj|R>JqeZH#OjxxADyOLsZ4+x&JON9BqW3&*OCoVFo=*))~NAM8q% z&kIJ{9MPJ@l%kh>-+c|r{ ze4xLFh__O%54sluD8w8rO(Zz6jJLr}Zrem|!}0N@xqbS`NOexn-#DEVm!aw!2IuD- z*)S{|HFZbKvXx@cyX$0S$o@Y8ojbOokO3?iWwkboOEWD?;USVz851W$D{*R;((aP+z5ToCcszfX{pGe>Sh)ImUI z#!nDrdpG5nto1;HvOHGZI=75j1P0qPX3rbZixUg!dyDV!B`Lt3c{=sw=BbIj`5ZF{hz30lup!R;~sh)%o?qzU35 z4<5Xgf*igaxzoPnTHQfj}+gnwL-UDo*!YhO6 zIja=F$oy`!OxrpW!WtqR(4|JqR<`R>P%~Q0??mrqVCV)hO7UYu`~3xppixVIqhr~$ zrqYaJZ;WKS;K}c5gykm5-xlPcjM0I_9@87eYPaOrL-hVW@?7?Ap9j41)unm-f22!D zKgGhWIWG0{cK-#v78!>AcnqV5!oluTl@Emd5!SDHW}fODmw1va*9#;eAp@=#3@{E6 zbZh&hgaqXwtjcA6$@8!ajt+hL6DLsaBzXAg2lM1^^YNIOk9mD^=$A;O2bUgOr;g_~ zC4mMs{-+i0%PNuIl*`LVcvb9FMy|0azjpp91~jSTDA~!0mDS%aT3^pDQZI|!)^G2z z4>?8Z24@h>u;E!?N$^a5YGBN58xentP45n8%MM1DcC#W7e>M3f0drYq88`#E!`j}= zS|!mtthZM96%-CQ@?}+kMFxq^!pnjO=*zD3!KjAT+xKefGaz^*iUQH7u;JnJu}i0` zSbB5hzi;P@ZxP8B%8_v4E%R{YKGfb;k z7-Fds772vVkc=oNGhN`#=7eqdhDVuuVIb9X569ACufs=>ubj9%EkFrPSRM*CnaVLcTKcyKK;4-Bkd!ASpBs{(0&Yx^b_ zc^M1~%kyG)=9pv|92j^DF1S%QJnP^Rd_*p?p>K_tK$PV`2iU6%7_60Zj;^V+Xk}Qo} zAg=sN1+1)lOS#VI33LVOb3Bgm_;oX^@YBIye=VH1a&+EwbkY#Pyqw4VT3yjR<$Rh0 z%`_YJ>(F5-bAgTlbo^eZ&1U1#fE{lZz{{e_yvRwqZ|3L0m!*jd1K<+Jz$Wu6MM-@T z%{@d}a9JcJIOa|8BM3m9$ajiAmg+q&qu*7sKJ$R1+n_2!EQOLYcY2qym>~}2M>{H* z0P(3{@Vi~r7?ZnSqw{t$M^CtbwTQ`8ixfci+455f@c5;Zs;Q3)W^Mip<(_Uuivj6v zbUL{UxKWpcT{pZA`iNHG(WU~{@JoA94plY~`v){do>lT21xObZCMxo0S1FAy#{sAK zwYlk!jc1a+#ckC$FYHQ#w?5C0W=hb_;;EC&$1B&XeG~MB1Y zcJcj~7bU*@c>f^`)z>0ktOgm)To7EV;5|gb;L7A2Tucwpam7f56C>Wq@8t^raH(N{ z)|&?L;*Jy8s9Cy&sBR4s{tpxExo3hf^aMD-kk_Bgw>tJ!?@;wCU`6G$l#D9w^8B?W z#r89|d{Hr`1fPmoshCYNcVqQQ;NTU2kBpNiUkPehSCl%uD&yU>`7<4Z*f zd!Q5y8-sMv&lsjcK^sH0g(>KzC^&1wfWn_qv0AmS;hj_-xBjqyB2&J0D0a346KD3k zg-CPgkrAGRg#oqfr>z$VP{yw(42{njEvv(TThQ8O4=;6GahE*o-u1oJYO1Or?o%?2 zzmpm67K;c_94sNelXl^NHbV-AQc{-N6pS91VMeYcpey4HarVke^=)!Bu~kZ75%7Yi z+~KEJOZiIhzQ>U~UMXxe#y|KC$?}XHRua%}Ax`Z*hh%H5iqudLC721~YYS?>VpoZ@ z9jm4;Cn;F%pW^5$^u|kY9;DV#(9$2iQ3U`#!CCtZ0^`G2rDO=uvmnsl zFLg$Un1*myqH7A+q+gDASP(c72gk&uw{7FQkRhG4f5x)&$#FW?dfPMFW(pWa1Ak?} z?wi(uFPvbbuBG(m8_2;^8F0sGJ};b=fw{v8Qe=BsR@ykaB%)-P#Y@`hZ&hX8y>;>& zaM2FVP9_8)P!;RMRYeW5lZ>lH>xQ020+Ai*4P9a(VL-nF3j&}oRLdNl?unyLWd>2QpLaHS-882=m%CUNk^eQi zH<4)T1JU8=6hCpO+w@?U96>%-L^Fdn7+se?X9!LC)n`@iR72Hgezu5fE4{E#!!9NK ztm+l4goD6&*B+}t+s#a{`CdL+2_$<&K!w$xiWb)^ROhZZ>CRcqrLQ~O`nj`z?b%0m z^loZ`!op2Xc$7HI4AFD^f&l#>us}X2Y^~HlLC;A(|M}qrxl;$Kr^wiD228iVd`lJ| zWMfkI1X@hQO&7eTqgQt!W?^F~%U}6Mp^vb1lz{`O$s}FP#e+T;q&;-{;F#2h!No&3 z7w}={H2?kmmKQHx`1-W=D*yrITu0q09C}m%Z5Zz@qsfU6DVB+Rpz{Cd1>f+cE#Fvm zx7bO(JN8}#2a`!7XRO<#T<`4MoZko6WHJXr(Bur&6%2OoL6a_`(`pBdn_SmFR2U7JAprHaH%=-Gh>(pvn4mkSd%AS+yO#W%{`5-vW? zMN)Hp-_K#jg7#7E{0{;OEA|T|q)@vly#LLMoE6CVFSP8VqeScF@dy_!dq7K7TuMy& zU{N2k+3<`1;dtIUmAs^J|*lG1)cq-yH72$@Mkjk zNC$t+Zv6?*O}wKRW`baiG=5j9*qTY-_x7i1)|nnVmIK|qkZ$Xc!+cxTF&{-Rnk15F z=qXnQQvM@3)EmRmFjZ({+w5JQf+2!?#=J^XWbx^lxx6iqmRtP1W7gs4__@-G3vo8A zMe5?nW$cs;8<;RIuVUs@74WHcdPVVvP`MK~xQDS#43dzJcBf(E>I@R&d2zF?9G1OX ztVv4?jcf_2bFo&qOnmlZu7rx0nncPMGg6(9JI}`R&G;$qi+*s8I7i|O;lDMYXVoPuzBM{0lATM8{-62LE1MuW~II zqI?8ldzVwCCpl0(cK2b&2-xWNz`ciEf@-tc1Bt@A5B@XrQINq;{U%33-OF3}Ro2f& znL$ReIo%|SisXWCfrBRD$mgTZR8`-_+xnabq!Y2DTv+}d3;Wz6D1jG68{k~`FqT{D z>-zs?v3dDPY zU(uA)6pj!T`24t|t$1taoF&Wz6#~9!vMTg_M1c``6R%Y#*!rJ%Wp^2V1yyg%<8wgv zds<&|7m32k&334P31O9_SoP~fN{z8phHpw8bISKHx5}daJmm(fhg8UgLDY@?1w@=F zQ3G%pTce0A8H1cAk~jbcvfT1pQ><*Q$8T(fCd0lhNa?lpQ$-iDDzat<1T%mHRLY3H z9=~7O;Dr*R+;l^;MqlcMoG$n-4_hv4G*BFoHW)HF1sH zC>oG<_%4`(L|25wIS`r;l3DY$JIPqj{pt7i$N(ULz;A z%FN~aC#dp+iX;mgYB=_%85^41PXpi&mx8KW1ij*K`1x#=V@C^s3*pjRCsh;a!JjkB zbacyq8S}D{Y(_rtX0cA@gq{OkeDEDGp6Jd@%lpR5(_itW4O+#)b_ury9-O}hqb^?y zmzwZGBge!Bh4&7P+i!DPN0PDI(Qgs;S5}~bj2E+TO+fDqWo`OQ|J9Ou3)xyM+pTPU zb3j;oa1WVgw0BqhU!6~o7Gi0I%7}{dZN*;AVQ@pnv2)Lu z{_1@WaNKbaRDUDTV!a{sX6F%hX;l>hbJ{}S=Dy+s1xgs|eoE~aE*XIxr(Rt0f7mTL ztKP0<>#TQCKr)pR|IGhn&io4ZfH;fjn^e$_-oiT!ox;=6a#^(RW$c-mB33HcT|bmy zo50f2X1HGS4FcQbcw5e9${E7Ny~Ydv21jl8v?}mYsAu|jqzmb!B(vfajvE1h`u6{{ z0FFE`4EW*%#1ZQ9=Na`40Z;){fU>l|V^w!PSc$!YRo$2Yt?Zlt)5rhGXT@qOh3Sn( z5|#e*`I#QtEo+LtRP5vI|ENI8Zh~II?XJ7tq>U6Wdnt>4_)Qvb+sR{o z;%;M}t20I3xLnqzU=1Q;@DZ%4&sTypVTJ2HTKOzVGhwZ|uYbqbUYCyebM<%2yDL7w zVw!pQ6F02(x+;h*wkIQZVq_4wRr3=|;*Ck+T{!CqxvCFdp146idIkDd^+k3K4x?N# zM`{!s>hT_d-XU?9(AH24$n;mwHV3C9>r&yQ95l0Ml}3MAWZNJ@pBG#Kx`sz1PTY?w zEk*=?%OP$+hhBCPRqD%rmmM2w6)Xwa(*;z?#^YPxG`3)D7N&+yK|iTHvhWQ;pk${8 zGHxAmup#ebo!ke|gy0|aNUpN4LzH$r+b9;=21^f?)DJ%*V)wdZ)D2WGIh3V3t$fsQWX95cp@K$ zD#p@$=gtjaGY%;@WyM1lXI@F?5KG1Zk;bksm<#ru@C^L7!;x)|mnxrgP{o~RRalss z#IK%4%1eM378iM+=)OX(yrxe+MV<6>fH3Kj%^rUA&6lag?>b&_yVgP+cyei|$5Hny z{(TpBzmJXc68%(fIsk(hx;Gwo4d(xIvU{*^s|=iN*Sn-GU(Lc$!f|Zq^&rDa+s21N zH*BBi^C#VfP?*4T=Y$%uGuP6qWR7v1_?|)+7Qo9RHx)Kl>wm{mubo{VoRz~mM?0FD z@)iNoz!RJ5F_Y4`(l=biPyYvf$7$+0tmm9H?p|vTd&Qqx|4ikKJc-e_EgA%_++$SFFK0ESp&!2Ghu&qBQyE2jyk#Y1kq3F42ywd@V%sZug=6i^g_X(A;W&C zr8)zYn|ay@otlYpp&bPGIBE3%sg#!rCh~TGtE(#q^;)gzt?u)VfIGP`y>3@cT8MTj zg-)xl`S=_B9w?Wj%CSMQrxF@xK1E;IP*7USlzYR<{x=g;LvMed*q|ZA_u#)+Hf#)V zgBg!1C&H#YU&r4}I0&=A3D8`jOum=){Cc@2E^)BAg)s>lyNx*Xq} zXgKUJCuZxZg{<7D8rUUonkMh#G@SmbWA8W_YNDt+)Ut7dYRzdz5!zHUdN`knGa`x= z7>fV#NA>OuIkTf+fB!O^Fe!??^DH10k}xtjBR1X-3u|Ec;Ig~arfJkdvf64uhCA8gwyzuG<4c#Cq>wFKHqr3?%)E$CN?sGXK(Wjs_e)q z+mgww6P-A01d5K{bMn4yfAV^IzbY&ljr{jAbXATnI!FT;h&?!yNkS`de6}i7+F;JW z!mwKG52mCNF@|EAtnLn>sW^JyOx}H4qAD@=WXRV=ZDWd>&x!3;E6$%0Txn;9lz(p` zyXmW*@I-BZ&VKgWl3Qzp_DM_N`F6u3M`O5ikK0XYXF~(46c5Z*eyzu}m3Ln&u!E1S zx`GY3F4ei_pY@c^F5qEX9T3ZNqEBW6Dih}Vde&xN`?xcZB56JP^r<`Ve~hO1AQ5%Q zplChUt54=p_1|Mz>pg^%8pJ)*vzFK_?7Zq$AE*vdM92%`Oq!N<)H2CQN*EdXrC6eX zwI*R9g|pZ3g)lU_(P%8nfCYL;Y>YLNWMu+z~r3a^OgIARM&j#y&u0I%$j*J$ImMMrRBZ4vF`5o<0(#nB}`v2k{ysoJL~_f;|C%Anq)A z3*H|jnid!nUB0?gSw5o0i+zG}U@@qC5LKLFBL#6J>pAKJ zs&X$8La1Lg!~A$yyE*_fh7G8mL8Syp8MQ|tfEv?YV%g~gNnq3|O;JFmqajW>a`JH& zvVb&vu{~DnqYM6WHz( zUtYRhyJ!E3%1V;aN_2rv4T6Is(24?ATneVxWwE_;){5o6`326zYy!uPElm^{1I2 zhID?pUx7@O|b|B{&y){$0D%I z!TmH*#xk#><+C;>H2CTuw-mC15I#Ff@({R(Hypd$B(U-mK*aZiV1axiIL;Gi+>2T+ zm^V_f)s2tLy>nPYLJL|e$&$Vr(6&CQ#Mt;%SNu>ke}|X%9ZDDqe}qJRrI(^6Y?0hs zbo|Y&;^~CTijZNMN|n|0i>3MT7rjfEao5vbp$RU7rzsV}P(;z^{z=MjhJJqpgK6#F zs4v7HLlb0wLBR!$#$w^~P$}2QteYUDie!4qpcWwf+eFkzN=bFRRg+}Xh)DAr9Gl%7 zsG0*q-zw7F=*ObWd=5egl4s2A|Nd=OPOf3k9Ax{^g=}rW#Rj>t z(L|CWDtbd=9I?;7NU^A{u4m_J&5E*Hb7_P&{-yaB$p$7JU82Cr@obp}3s=Up*MZ+8 zfo>#G4Nta7vRZToK{n@`R@WQji?sLHldyadIA?p94b2@W7*>=MMA9zy4AH;RsOHpC z1b?ZEDFN874YYCcg;@H#5roiiU&DBRhm|gEMCT(Wwb@TCIVR&O2ffw(v$TiEWGxs@ zPmTpX&Y-F&Z~d6iZ^+BTY-?s~N;S7!mv$Jk%9qAhBdS$q%E$O&GSb!JHJ%m;Oo z-C9e$&G#umGcrCy4-~SIrcDUFP>%~#fIJN-y_`Xj=gn> z>|sQ?(##4!t_WqUUd{wyl7D;9LZ>Ayp1Sy^=KjD`Yp>7WQ&gQ~y`F*_)d}wNeLKQAOm$r`N#jPHK4*^D>YPygR?+eTBaP*)Vq7V|pD7kZnHAq;_gp6*PQ8uOrm?pU zf4|aMp$Pttqs4M85MXwkODXGWPR&HEMotPT$(E9NF0EU4y{i(XXN z^44FURsvRp$ZRC)Z;f--{4^qd7&OF&Ks$X%T%)Rp4+v(WUJzhwFKMr9C|ZIjknB*~ zP?e$o7lJ1jwDEVJn2}c^Pc?ga=ccAoB~Fm!Xv&yx3!C{IzK53YlIA=(69UJI{l8uk zQ=BWVzr9ii3xo%ik1?LUpm~ry;s9Z2s@yy$0z(*jhoM{4b_QQ~PU=m^!p8?8U&ACA-#k6}gw$!x zqk2s@_}}}ZDY2PHi5U!yp0oT%ONGG;F7OCLclKlJ3Em$4fn@x}7-n1@Txwp}EQ2Sp zue~V=o!=EzDmFP?e@q@x)5VGls;2tU6g@-GE${GOPrM~7F?CUeBpm!e0dbEPlGN%S zLuj5x7H554f5q%I@;FK zi;(N3m?bq>siri@wvYLT^e2qSmA79(|L-Y9{ mcu{Z4PSrLxuO|rCpiaBHnkjmq>ziL^01Xu# _ebo; + private BufferObject _vbo; + private VertexArrayObject _vao; + + private string[] _textures = { "nx", "ny", "nz", "px", "py", "pz" }; + + 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(GL gl) + { + _gl = gl; + + _handle = _gl.CreateProgram(); + + _cubeMap = new Texture(_gl, _textures); + _shader = new Shader(_gl, "skybox.vert", "skybox.frag"); + + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); + _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); + _vao = new VertexArrayObject(_gl, _vbo, _ebo); + + _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 3, 0); // position + } + + public void Bind(Camera camera) + { + _gl.DepthFunc(DepthFunction.Lequal); + + _vao.Bind(); + + _cubeMap.Bind(TextureUnit.Texture0); + _shader.Use(); + + var view = camera.GetViewMatrix(); + view.M41 = 0; + view.M42 = 0; + view.M43 = 0; + _shader.SetUniform("uView", view); + _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); + + _shader.SetUniform("cubemap", 0); + + _gl.DrawArrays(PrimitiveType.Triangles, 0, 36); + + _gl.DepthFunc(DepthFunction.Less); + } + + public void Dispose() + { + _ebo.Dispose(); + _vbo.Dispose(); + _vao.Dispose(); + _shader.Dispose(); + _gl.DeleteProgram(_handle); + } +} diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 50c83c07..1c18458f 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -5,6 +5,7 @@ using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse_Conversion.Meshes; +using FModel.Extensions; using ImGuiNET; using Silk.NET.Input; using Silk.NET.Maths; @@ -17,7 +18,6 @@ namespace FModel.Views.Snooper; public class Snooper { private IWindow _window; - private IInputContext _input; private ImGuiController _controller; private GL _gl; private Camera _camera; @@ -25,8 +25,9 @@ public class Snooper private IMouse _mouse; private Vector2 _previousMousePosition; - private Model[] _models; + // private Skybox _skybox; private Grid _grid; + private Model[] _models; public int Width { get; } public int Height { get; } @@ -42,8 +43,8 @@ public class Snooper var options = WindowOptions.Default; options.Size = new Vector2D(Width, Height); options.WindowBorder = WindowBorder.Hidden; - options.Position = new Vector2D(5, 5); // this doesn't fucking work WTF options.Title = "Snooper"; + options.Samples = 4; _window = Window.Create(options); _window.Load += OnLoad; @@ -52,6 +53,7 @@ public class Snooper _window.Closing += OnClose; _window.FramebufferResize += OnFramebufferResize; + // _skybox = new Skybox(); _grid = new Grid(); _models = new Model[1]; switch (export) @@ -88,10 +90,12 @@ public class Snooper private void OnLoad() { - _input = _window.CreateInput(); - _keyboard = _input.Keyboards[0]; + _window.Center(); + + var input = _window.CreateInput(); + _keyboard = input.Keyboards[0]; _keyboard.KeyDown += KeyDown; - _mouse = _input.Mice[0]; + _mouse = input.Mice[0]; _mouse.MouseDown += OnMouseDown; _mouse.MouseUp += OnMouseUp; _mouse.MouseMove += OnMouseMove; @@ -100,10 +104,12 @@ public class Snooper _gl = GL.GetApi(_window); _gl.Enable(EnableCap.Blend); _gl.Enable(EnableCap.DepthTest); + _gl.Enable(EnableCap.Multisample); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - _controller = new ImGuiController(_gl, _window, _input); + _controller = new ImGuiController(_gl, _window, input); + // _skybox.Setup(_gl); _grid.Setup(_gl); foreach (var model in _models) @@ -121,27 +127,43 @@ public class Snooper { _controller.Update((float) deltaTime); - _gl.ClearColor(0.149f, 0.149f, 0.188f, 1.0f); + _gl.ClearColor(0.102f, 0.102f, 0.129f, 1.0f); _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); + // _skybox.Bind(_camera); _grid.Bind(_camera); - var padding = Theme(); - ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoBackground); - ImGui.SetWindowSize(new Vector2(Width / 4f, Height)); - ImGui.SetWindowPos(new Vector2(0)); + ImGuiExtensions.Theme(); + if (ImGui.BeginMainMenuBar()) + { + if (ImGui.BeginMenu("Edit")) + { + if (ImGui.MenuItem("Undo", "CTRL+Z")) {} + if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item + ImGui.Separator(); + if (ImGui.MenuItem("Cut", "CTRL+X")) {} + if (ImGui.MenuItem("Copy", "CTRL+C")) {} + if (ImGui.MenuItem("Paste", "CTRL+V")) {} + ImGui.EndMenu(); + } + + const string text = "ESC to Exit..."; + ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); + ImGui.TextColored(ImGuiExtensions.STYLE.Colors[(int) ImGuiCol.TextDisabled], text); + + ImGui.EndMainMenuBar(); + } + + ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoSavedSettings); foreach (var model in _models) { model.Bind(_camera); } - - float framerate = ImGui.GetIO().Framerate; - string f = $"FPS: {framerate:0.#} ({1000.0f / framerate:0.##} ms)"; - ImGui.SetCursorPosY(ImGui.GetWindowHeight() - padding.Y - ImGui.CalcTextSize(f).Y); - ImGui.Text(f); ImGui.End(); + ImGuiExtensions.DrawFPS(); + _controller.Render(); } @@ -213,12 +235,13 @@ public class Snooper private void OnClose() { _grid.Dispose(); + // _skybox.Dispose(); foreach (var model in _models) { model.Dispose(); } - _input.Dispose(); _controller.Dispose(); + _window.Dispose(); _gl.Dispose(); } @@ -231,62 +254,4 @@ public class Snooper break; } } - - private Vector2 Theme() - { - var style = ImGui.GetStyle(); - style.FrameRounding = 4.0f; - style.GrabRounding = 4.0f; - - style.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); - style.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); - style.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - style.Colors[(int) ImGuiCol.ChildBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); - style.Colors[(int) ImGuiCol.PopupBg] = new Vector4(0.08f, 0.08f, 0.08f, 0.94f); - style.Colors[(int) ImGuiCol.Border] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); - style.Colors[(int) ImGuiCol.BorderShadow] = new Vector4(0.00f, 0.00f, 0.00f, 0.00f); - style.Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - style.Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.12f, 0.20f, 0.28f, 1.00f); - style.Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.09f, 0.12f, 0.14f, 1.00f); - style.Colors[(int) ImGuiCol.TitleBg] = new Vector4(0.09f, 0.12f, 0.14f, 0.65f); - style.Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); - style.Colors[(int) ImGuiCol.TitleBgCollapsed] = new Vector4(0.00f, 0.00f, 0.00f, 0.51f); - style.Colors[(int) ImGuiCol.MenuBarBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); - style.Colors[(int) ImGuiCol.ScrollbarBg] = new Vector4(0.02f, 0.02f, 0.02f, 0.39f); - style.Colors[(int) ImGuiCol.ScrollbarGrab] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - style.Colors[(int) ImGuiCol.ScrollbarGrabHovered] = new Vector4(0.18f, 0.22f, 0.25f, 1.00f); - style.Colors[(int) ImGuiCol.ScrollbarGrabActive] = new Vector4(0.09f, 0.21f, 0.31f, 1.00f); - style.Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - style.Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - style.Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.37f, 0.61f, 1.00f, 1.00f); - style.Colors[(int) ImGuiCol.Button] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - style.Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - style.Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.06f, 0.53f, 0.98f, 1.00f); - style.Colors[(int) ImGuiCol.Header] = new Vector4(0.20f, 0.25f, 0.29f, 0.55f); - style.Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); - style.Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); - style.Colors[(int) ImGuiCol.Separator] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - style.Colors[(int) ImGuiCol.SeparatorHovered] = new Vector4(0.10f, 0.40f, 0.75f, 0.78f); - style.Colors[(int) ImGuiCol.SeparatorActive] = new Vector4(0.10f, 0.40f, 0.75f, 1.00f); - style.Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.26f, 0.59f, 0.98f, 0.25f); - style.Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.67f); - style.Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.26f, 0.59f, 0.98f, 0.95f); - style.Colors[(int) ImGuiCol.Tab] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - style.Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); - style.Colors[(int) ImGuiCol.TabActive] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - style.Colors[(int) ImGuiCol.TabUnfocused] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - style.Colors[(int) ImGuiCol.TabUnfocusedActive] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - style.Colors[(int) ImGuiCol.PlotLines] = new Vector4(0.61f, 0.61f, 0.61f, 1.00f); - style.Colors[(int) ImGuiCol.PlotLinesHovered] = new Vector4(1.00f, 0.43f, 0.35f, 1.00f); - style.Colors[(int) ImGuiCol.PlotHistogram] = new Vector4(0.90f, 0.70f, 0.00f, 1.00f); - style.Colors[(int) ImGuiCol.PlotHistogramHovered] = new Vector4(1.00f, 0.60f, 0.00f, 1.00f); - style.Colors[(int) ImGuiCol.TextSelectedBg] = new Vector4(0.26f, 0.59f, 0.98f, 0.35f); - style.Colors[(int) ImGuiCol.DragDropTarget] = new Vector4(1.00f, 1.00f, 0.00f, 0.90f); - style.Colors[(int) ImGuiCol.NavHighlight] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); - style.Colors[(int) ImGuiCol.NavWindowingHighlight] = new Vector4(1.00f, 1.00f, 1.00f, 0.70f); - style.Colors[(int) ImGuiCol.NavWindowingDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.20f); - style.Colors[(int) ImGuiCol.ModalWindowDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.35f); - - return style.WindowPadding; - } } diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index 7f9fd236..fc7c48e0 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -1,5 +1,7 @@ using Silk.NET.OpenGL; using System; +using System.IO; +using System.Windows; namespace FModel.Views.Snooper; @@ -8,33 +10,77 @@ public class Texture : IDisposable private uint _handle; private GL _gl; - public unsafe Texture(GL gl, byte[] data, uint width, uint height) + private TextureType _type; + + public Texture(GL gl, TextureType type) { _gl = gl; - _handle = _gl.GenTexture(); - Bind(TextureUnit.Texture0); + _type = type; + Bind(TextureUnit.Texture0); + } + + public unsafe Texture(GL gl, uint width, uint height) : this(gl, TextureType.Framebuffer) + { + _gl = gl; + _handle = _gl.GenTexture(); + + _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgb, width, height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, null); + + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Nearest); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); + + _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _handle, 0); + } + + public unsafe Texture(GL gl, byte[] data, uint width, uint height) : this(gl, TextureType.Normal) + { fixed (void* d = &data[0]) { _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgba, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, d); - SetParameters(); + + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.LinearMipmapLinear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, 8); + + _gl.GenerateMipmap(TextureTarget.Texture2D); } } - private void SetParameters() + public unsafe Texture(GL gl, string[] textures) : this(gl, TextureType.Cubemap) { - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.LinearMipmapLinear); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, 8); - _gl.GenerateMipmap(TextureTarget.Texture2D); + for (int t = 0; t < textures.Length; t++) + { + var info = Application.GetResourceStream(new Uri($"/FModel;component/Resources/{textures[t]}.png", UriKind.Relative)); + var stream = new MemoryStream(); + info.Stream.CopyTo(stream); + + fixed (void* d = &stream.ToArray()[0]) + { + _gl.TexImage2D(TextureTarget.TextureCubeMapPositiveX + t, 0, (int) InternalFormat.Rgb, 256, 256, 0, PixelFormat.Rgb, PixelType.UnsignedByte, d); + } + } + + _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMinFilter, (int) GLEnum.Linear); + _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); + _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapR, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); } public void Bind(TextureUnit textureSlot) { _gl.ActiveTexture(textureSlot); - _gl.BindTexture(TextureTarget.Texture2D, _handle); + var target = _type switch + { + TextureType.Cubemap => TextureTarget.TextureCubeMap, + _ => TextureTarget.Texture2D + }; + _gl.BindTexture(target, _handle); } public void Dispose() @@ -42,3 +88,10 @@ public class Texture : IDisposable _gl.DeleteTexture(_handle); } } + +public enum TextureType +{ + Normal, + Cubemap, + Framebuffer +} From 9b5bbd976d79fdb26d2421599cde52f1bcd63d1a Mon Sep 17 00:00:00 2001 From: 4sval Date: Mon, 29 Aug 2022 23:06:47 +0200 Subject: [PATCH 015/108] skybox --- FModel/FModel.csproj | 1 + FModel/Resources/nx.png | Bin 12380 -> 3319 bytes FModel/Resources/ny.png | Bin 12681 -> 2667 bytes FModel/Resources/nz.png | Bin 20239 -> 3474 bytes FModel/Resources/px.png | Bin 15181 -> 3335 bytes FModel/Resources/py.png | Bin 19207 -> 2954 bytes FModel/Resources/pz.png | Bin 13160 -> 3840 bytes FModel/Views/Snooper/Skybox.cs | 2 +- FModel/Views/Snooper/Snooper.cs | 41 +++++++++++++++++++++++++------- FModel/Views/Snooper/Texture.cs | 19 ++++++++++----- 10 files changed, 47 insertions(+), 16 deletions(-) diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 01d7d569..c11e65eb 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -136,6 +136,7 @@ + diff --git a/FModel/Resources/nx.png b/FModel/Resources/nx.png index a6896403c2ff1360298641460da97edcd9adb764..febc439eecd5abcbc66d076780bdc4913b94b3fb 100644 GIT binary patch literal 3319 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 diff --git a/FModel/Resources/ny.png b/FModel/Resources/ny.png index 73349e3cc0174d342aeea6ec2d7a5c350d63c43b..5b8af2cd49fa905c287472402d7e3e2b92868f64 100644 GIT binary patch literal 2667 zcmV-x3Y7JUP)00001b5ch_0Itp) z=>Pxwc(w7uD=^Y-`d}KW_uUc({t7rw+C0#^obvDuVZsV zy4_|~#n!D?ivdIb00R49Wm`BX(dT91&Lz^kRUYz5BLGiUud6xrr9bPX3AwnR{9o^s zoA8X)nf)`q*D<{NyQrpq@or z?D_;5*T4ZQ~14s1<=VfZ{tYSaI~i zN-}cj`(j6mLw91W7?oT&NIhi$_`&i%0jT#g5e#g%zO@|HpN%^aE6&W$>;-+hKx``i zW_#$v16Qy@ zcwPvAmGyshe`JtlYvhTa7L2c9^w3ROmDX23hcoD7irx6?DHK`rBzY`Wk_MJSYd4~~ zGP!9ivdD=y=gJsDTz<>bia84Z2ob;D-nr9p#lZ#E32-OuxepBRBN^iA#sIt!x`Zl? z%}uq^l3Unlehi$Sn(+XD(*R6%i7w-og+WehLZ#O$Q_W<#ATr8u+o?|I;MQ^!f;08+ zD-41U2!K1!ugjZox+jIhC?OCSOkG^0(C|4TZUdVmF6P%klw{Pi!JUtzJf4|Co--7p zrFwGzV~4S?mV-U(EAv1Xb-yoV@F3499JNNWhOw++6}-$xP>jCd%nW zM&V8cc51I{c|Q_BCK)$`L>`xwP(%xyS&u7Sh0^_$^u(1}_9LsEj}(D$p9Dq#pmGOy z;k=*)C#Ywu{hMA7_g`0OWK2fa06bXEtS;h+C!)?NxEH# zzK>myL~8`mg#vTkns*wOKWWmu_kIQ-8(+1!i@kSjBpR9_!eH_8xeYC1r`ySG98Plv zM@um&E%%9$V2soOaQmspW_iy;smCTVRQiQc+cx&|+^+K&+qI8v>~iKM9iH)W#oKI7 zoW6b_^kiCZD+ea)sdlzXi{t8};{%|QhHaM$ES3(9fPM%VLn@mDX5Ic36DNLiLYpBI z0Gc{-@=Y}}05bt1XwXZkSz`P(_HmtO?$)#OaJC2K&A}y&Ot`Z6lr~$a`0e(ZdFIdI zv0a}?B5Nt%g8)uuIo2bt>lC3pr2=2*DavbZE}9h4Ae0-iQbPX^fY5#u4p2IKrkGF7 zxRe%jasgQ|6q{oxowbx$&c!G0_X<-1_&K~XaiwUf?>gB&&MKoYD}YF?R3F!$gz06A ze-kQ2?r?IK6oURl|7Q28IF`_7{t%9)>KG|cVJkIB`Xfck*1@E74uA%OSf~1BdCgE6 zrH0a$W+l=yfQH5{a5koaK5F8VU~=8?x<2AiUW4H`-Tl>Pr~`$Ol}DuOor0h@#L{>{ zldSQ%PMY4OTBz%ZZHMY{@8Zwmx*-DQo=IhSq-^S8K85vo6ia0+$V831WdO#~_M0MOMeXkHVrPE4fF2uKqr&MDtR z_#V%Mw3dZj@ntC?i+kj)?f3etsOxRy2T z?v|O}kMaH2yLsbLpxP7nF~&a7TLuA7c|{^kQz;mpmy3!+;g65nvC>y1DD9y#>%rwVu0}Oak0nquS&N(zp z24X$3NR2lDm-zp{}Mp)dK8l|G<36(xxs%Rm8fEy9rH}Eo#;wL1~2p0${$SdBdhNO(HS= zMgH4$+7jb6i7bx^wWa%%1DJ5-kWKJFvsocut zw*$~2C2_96{A|jVoEIYewr1Pi}eSfIsl{A%m)`z zSZ~8*?JoONz+mFGP{?eaE9lAk=pbOR`On*7)B)p6@%)6LuPde}JpmA#jwlqF=lr0R z^$fsQZh^!rF*k1kqZw4@vZ>ZQV^JLrv$ttMVC$hnQOYfV{Qs5PSEi*GTEbYYuBLzi z8y?zgIF+%!S1SPMhd78u96exWk>&KGZggP_!JDWG9zM06TTqvvSj(=guX~kffp~E{ Z#(&W|PpOrjq9yPyA07*naRCr$PUE7u$Hx3-l|Np^mHgS@Zi=BL;?L7(=io&H(0MR{~#Cgc^w7^R@ ztAGN?mM;JK&wpN=|Gs|x>U}h?Gd3?TFQl0muGXqN9Jtx?Ou`4)VsN=!TKXBC;NJA7 zp8a}#{@ZI#UN`$O2YP*d70*Opxm+%n|Ni&CV0D>=$2Lq4?a134RL`fq!$!R7u3GrO zzN}yub%%FGj9a&I6y9cXciiDiJz=CGx3+5?a?6a>c30|o(2W`~n4K?Go+&}FkeGN#WXeE-h4T`)bNpDYuy1OT~2m>6IRc@aZ& ztIfQK(I9~s&@eXpL7*_odzbs0Y$2IongQoMX9XID!p7{MyvSt0FE1~zudjgw8g~)` z>q&BeaGnfYS~_6{dLA201K$066{Iq1v9|FNiE*9bz^w`t>ShKLy&PahJi>X`#(f?r z7iO{(1Q^7VLBLe%VXll*0{y9RWmJIx2S`kWW}!jILuPgPQMytNW>(K^LlS_L_3NE_ zYCbD|yjgC^Q%Y*4q z%JDwCFyYPOPZ%Y}l=$sik^?%K23H=+pkS#Oth|*3V&?Yd=-*Fo7z7`|2c8Bb;zRM{ zu|lNRW{}|>|2?#}RDl>2cG(ChFx|dM;+vfq199G_kqkTs%=}m*e$8U_zeguPL}`yc zJ1OJ?lmrUba^L-jLwIY?H{>mOoP~)~wyPuTH_1>)3Ctvz&y0D919>x_WKaeA?z`_a zGMFo*C+1#Ooe(r z2Y6KRmW?@RVbB)THg-ayS&SIA!pP2TCB6|+g;;>JXBsU4P;;#Vo%_NuvC}~DQjbC+!oCo!Gxm?z} z?xM$LKLy0=93YhP`aFd>uHcEFSr{np1UQHTA!E2dM4glP?J_rcDOM7%05Ke$((Lq^qDnNCi{D_p7q`?mQ$F##lLV(ze10G2<$Gou6 zvk$O?{t(%rK{@M9^-QAnh(5`owG*v148I#ocwbZHn>Hcnan2fNw2#pM+Yd?@RUm+; zr!5f8Ij8WKG7iDi+gmtXXh8b%O&yiRG*b>*Lj+Ej^`|&t(9-B33Dzn?FKUVdJCUIv zX|fQ^+6$~~HV+ZpWw6Nvrg@mBVgroh)TiL(vhh5nvYJWoejc1+3n`jOkdbB@QrU)C z`z{c-^us{e8z^j>4Fx#I1%wH6XfJuVC%((Kak*tsCK@o}J4DQ8lqdv>Y?JU7TF`lB zCmO6@zkUrN&g2GJPp=bV6Q<#&*k}dXntZz*g)niPa_2iBKCZ0^DIU$d<(b%R9BakI zyxAQZm~BH!;83vMFRlxvKD~c6;wpiJEFn2?5Q0Peg$8BGp6$62lyB0N-fzY4C|#t34(FCc|6_6V0i55tB~f;r5|V=H;j1?->RPQ(NHafxgE;H{(F4lly*PbD*UA&z_vy5kH3p zfis~`j>V*;$3Y*LeNuTs=)doXTx*n3ZO8ixI-UbJ0tgV#-{;6^#C*3b+=78-QHy)ZX&=n$Y} zKiDkg?ANXi0lJ``l7kypd<=nas)4jn2enFM>u z0M^r+=`thM88CI(v|!~8w-k=nMI^XAGwk4TwP!9(__hEUBJMeWSqoYrMY?CwEakSl z7{c_kgMNmAJN+|lR{+iVv{Qls_n0jSXs=9UHaGKH((#LdYy$8_Atp->VMpR7Gz$$5 z^~%iX41&9CvIiWp<7y5ttnh#?r~=IP#m|~O+f^VSgh;hRn$3s4Sr}#dIU1lx_n!qQ zux|_q0I6PDavTTDG}jD7_rq{vBh2*Ho)Ai@oe&?MLD(>iFo!z+qdRZJz38fo2Iq4C zBmkrknw3x^0s!Z&vB=kjY6bol^fL^};*6YFw%haV_i1px7Ls3Z`}lM7YlaY}5SQ=2 z|9-CJ_{XNdPlbh4amfP*iAB{ zA(YpJO3}Kao9ds$B56iKt+zLF(4)=D9AI)^=C>tDK6ptm$pO(kD1$QJm&g!cN8g{r zkQBWBkpwW4Me?yQ9LU<3o?g7w9x5E10bgl3l3*=GOjt8H0As_8at3Mz(M7JSFk>cD z>-pJQ6{0~F-|H=F-|wM+)%P0QAUE*hLwp#nkQFd7f#CoHxX>mvAi>ww4>{bMn{{!( zbkg(gfD1SAZ8lul0!mV5a%>!hNFhG-_aSbMQ%4NPc$|ed!guwKEVDGga4%q*+#q8T zgNOaEzY9l+qY&haFCitE446b)XciifSnTSD9PX*ib~upLMPY@$w|x|odA&2cq+&qF_{z%fDQ>Y3^o7(fe8Y72hAvht^l3O&j=O9hH?KS zLzpet9r*6FJpzJn-sf=~U<#m&4d8$mTiU8XPl$eoAplk12%sO%lW@SR8ZiP%#*kDQ zH2bzdS)YX9+cyXuYB)sL!;Zl^dJ1+sgEDPXlL@sDWZc>40?iz&p#s)PlfG66{S5+XG; zC@0@p#PIg1IrBi|CqP<~L*v4z?j-d0CLeYQpk4d861w~>CC*H6Q%K?`M&hGpR+}^P zz0I}x;8r0AOal|*eDQ}7`n=680(e08LID#WfKUmSIW(vpu4X{M49)bDwP_hQL}YeW zp?x@F7Y9;8C`W)$@lOMwdA#*AukT2sbxAg}h^Nv&Ed((AXb9p>1Iz(TfbiGyLfj97 z_5b5`NDc)10NRU(YzYC%taOVF(}`I7LX%DO96I6z!5Ag<*5!N2asVU-AS>{G0wYWS z);{HNXm2Mt1bR6GCYO9CN{~aC>%G%93!rnxTHlUep+NpB40%&L2S|voN1ueq*y-F`(Qr@_3ZdC)19>wUIA{+GBnNgxk^VZwagSjTc=$TR zl_Vws#-myP&t$Y=OqlDjf3gP}#nMO#7O_|if0aGA(ntt9<7z9j`82bQ1 z{LDYYbT%{qEMj1t((45@7n)?zf5H&iDVg>=G$?bxgm`#S6M)Zvua+YMV8!o!8on?| zQXVisuSJsf9KPfKko|0bBfxbJRcl@Wc9z z(5pL~$k+jWDh@DM^lX^6r>XziWOkT#a$8;tZE_%61#=Fg-nfE0PZmq0|mRvu|MHChRsu+uDQv zX}YlaJ2iL)0&Hdi#zvTi>0I4x8I%bK*$fziwE*QplY?0F9%=9l93VAP<$VA_hJh8& zrv>q_WE0ORcUHH_Iq0fmJo$EDE>!dfP*2YMrQb6OSNp z`S9U`xz%MNTVHG>K-rB!c6ajNa|qCdr>=g&2yog!N94yX z1Rzu4Ie*Q8pkyQn;JpXTB)cI2cCy%=w(Z*e99*6=N=QNy$7VlFTO0uj1jt1LK!6qW z1pEt=ZPZ`HvLnfoha)3#Z#mOI!6}KF~l=x;!Al9BCMhJ8U zPdgfkAyViF!@J;nU@Vzu!t~Q(!#MJtd>~5z@0T(}a{T)GIv(Tf-w;S2K70_bM4Gr4 zX&Q!+;-zK6ZSc?tzU|0N*bO3KoN-;A{M4h0Nz>h zmT#r|@gr)7a;?LI2;hYprVJ{S4=(!%6?7-S`t(;(#G^m_a@^YcTxD+P1uk=-+%OBW zu7QTLoC=u9MER-Kc8*Er2s54lfy)rj!X;~ezDusnaLyCI zl21aAWJ0;OtQPVp!Ehr?1FjJO%brF8c&l3`KuRvxuvKeCVko5pai8QoYtGvn`ZNwa zsP?~q;;u#jv0-czK*t1A6nUi~uMl(u0eB9O5INuLZ;qI{FCmv}MAfabOvnZSM&%d2 zBIWwEC_n!ABk=L_;evN!yu`Of|AgR3Ek9{YnsuQkyhm@Koq4`(C^(1!WE!w;3$O$j z&su+pB6&!zmi~H?WCcVD1f(%()<}Tbh_zN?+F9pq-u37tm1=`=TebzX`Zx1|TNP;B z6%z`yzb7NWK|-Gg0clB^y)F71C)q-P`84?6c7i}n{oCcyu@A7KpFb9Xa{zOZ3TXgX z>=X_dW5%Wt`Ze)$&?I^6f+xUtvO@rULXPqGECF-|3@8DF{x<@URN(>5TarWXr?X{2 z^@ap^NEK!TnEIX(IiBp9R7rXIvO3}c!B{djWnh7Z`Iegp?jO9SpXfQkEY zk)eba&Bd~%!U9$ZDL^pBj7=B3nEYOh^(NCR&>-6+6$(rlGagh;1qB2MlVh=vKoJ2n zhf7KAOM4G#wH9w2T68*Zs(x6Fv_fPF%aLk3G1QX2p|`c z1Q}_T%_|OKP64S$H`xa_llXf0F5uoxf(zMjR{Hb02>WIRwTw+lijWI?v(gx6H57)e7(X3$!ra}VWGJA#MFQ#C>zv8$hq8wR`bGc- zBAW?GAvpA^=Z-X(|5kBm_voi!n}RDHwmuaE%CV?5c$fW+$n-rxzF-lHh#;w7(aI z8P!r1VG#L^0HGcunf4DMfc^@gH59C0R4<@oB8imO3{shj4X(qO3)w{g{uW3)y_H!x z8gvPvmhXqFOCUf10l^vpvOFnZdNyG|iEWr3$er4^0(5QMUK(VD(EgV2<>#M&HjhH^ zuZHPl;B5&&Vli8j3A+sq3Zbb6%JW+lXpqG^Qx&b`08dK*u-Dd}Vs2uWZ2pgo0P9*b z(jY|-2KQcF=y@Z+IRxObi{VT5Lz_y_ABlRPOJ@aLb$Sf2C`;jYgM9Y}zX81IMxA*mPwFgA^z_|3c8=KvWW zyrV(P@wj(JfDnj`L8|z}81u;HE#DlzEfd)10F(Rf)I(hY=+cmuG~mh_@oQyJ56Ug& zNCwt?U6q72Y1V?gr|N{KL28d|$MN5fLyIa9b1;r!LY4qb%F6(Vv8l;%&@(wGY4%ju zZJbP*2BZ)&^u7O!!o&FbA0WV7FmF$Qi~!-`2!O~HG$ep}&B9ng4wuqqjj(&Fu1F4$ zabPDEmJtBI7W|C>0KV^<0Q39pPW##jk^@YsM^uE0ifBZD8Ucn>zCIhR1L4efZ(5^xd4|hIXfJe_`0$B2$WV3}3G^XkkUnh~8UUtOQodTNo1RpDd=sY41Sr(9ZdZg*YyAfG5CR+o(-| z2gg4@ok{Q}0XFmCmIy%NfHZ44{>c!DN0@2J-_GVheDeYm$R@)qYvlP zs?i{P6t!-9TakWU>lSkwDFRH~o1*AW1{?u4j|R1;0pY6e8-Sdorf0$_r7+i0(@c%n zVw_?^XzZoDm?1_6e7V>WAXNy`nQlP>k!KAyWeJbP&4G>+Km0AU0Jp1?5YxPN(q(=z zNjuqfYC#VK=*ok$2{HDpAaf9R9)cq@2;D!~LoHT>Xv>ZscBmg<3aQ!t$=zSAUa%qx z3;$+Ld^-^O%P+r>{=T*yMn(e@d3Gt<)x@@N|w zKtcj+rU5(wn1WA;gH7r3w25zcuDv2=9g64;PIHu=;zT^$$%0yro*NmnBMwgC|7) zK9Rz^X26}bDUg9_;@N;r@ZVA23k4E5v?C;H8EZ(d^HK{ljB%}<9=csBHh6AnSS-$OVAY0yZ6 zO(A59A}xV7(_e-OH3F3NPpBoj2+#<-ovwD;lCaL2HPQedhD)cylK?mkAb(8Tiw~p< z!tc!-gyZKye+zJ1I?3Xjv}b4#+GOJZ$ONuj`t{de=Oddr?)v**`qSf75Tp1DcrApE zER3@DCSo=_*=$aNc;769IJsd=I(z)RaW(-?Q$g#Rk;o>^)^*A@$s#Fh&t&acoR#O0 z9jMWMlLqk;G9k@|p8*54N#dF^OJ7ED?DUILUy7d~RZV)VAY zUAg|*h<;s}Db$fx4uE*u8fZv>o%bZo#qK3W7Ef9GvCOqr0wVjJl#fU- z&O5~xM%kMkl5xPz1b+SX*VQ+#&y;yDOrUu*_k;~d9%^-=(3Jv+WOY%CYTt+KQ}IVU zlTB8H%}oQ60A40=l|ZZG$Nqlv3*<~)E--=rBwrjmCjls+xzI!tZ>N)0;iNn?Zye&- z>nd!>M+r|3X~{$pfK)*sNdR&`i+Cn;2qS~%M|o`VmW@7pv#ac!dC-?Joz?vw2gog9 zmjHzWp{A5i9TpNW4dqHfv}Bu*5FyPr@p^>s-^X;hhtR>VJ@d}qYvqZ4SrHl%NT?u; zV=d)VxaXrmxGx3}7Gg9NNf=G#AtRhg2&v5;Hk*gj;|)?A@OA^T+<1L`1##vkfdmJ# z8)4;rVvsSDDkbaV$!=TFTt;5GVQ zEeJ4fc(pN+QQ!bGuDPfwXr_#XAWT6@2dGiVFL!cULy`l$^dB=fac^Jz?YG}FcKMWG zl|xz@rLK4g(48=TF3c!ICk$K|n@t?2hA7;^X~<3u#L$4}fX;LUD;^lK62PqX@E22X z2e1Y%pZDA;0+6sH&G>yTzC(PC^@_z#iK4%qZTmBmeu=;8IwlSF!U?Ft-T9n^@`z^#e$Z^LKnsC>vD^5w6o*w&n67KEp zpGPWbNt%&fwzoHO&=bsXOz`}=7Chczxa)O60O2FE2c)rHVzdn)z!B(Yun4j2V#wPEg~N;6?z?$FGkV91S8j#mHlDK;$qvK+64x1W17_G!6|& z#B}vz4ku2?c?G4(2lDtnd22ikvO>syC2~Z7a3f4G1W5eso`4BK-M^{DJsj}YLnMU; zeA$QZ%ERR*hP>vh;NG01DL)#gO&)nzM`R-aI14=<3XTThDLTLz(&aPmA9!?9L5bBO z0l}CtHUOiIFj~7Bvs&GG<3L^CBqyfSNrMpq0L0DxuUcBHy*Q2}!oHmV3?dnuP)9Ka zp#*QlB_FkV2kqj6nH7kO;LnGc@cZw-VDI}gB@N`GJt4#5H4hP z+><4~9_|^SKW9}gE`>B;3@*R_{(GtfYCR96d06*Hle|OZV)1jnM2sgsw9QO)-_YUH*CpsAoW#Qq9_y-z;xK= z*-z5a+c^e~Fbo=@6J^Mg6+cg|d|IHt^Va;@Hic%B7)%f0*$1J!?h!yZ=Jna(cLN5R zSNFeSaqX7O3czt79ZX1ddxZNBop%d83f^=R_oMkiRnc)fpS@iGsx!*%-&a2|*e`_P8fmZ-Ecf{^TAv|#( zZW4QL9=|{%HXi`cy^ly$lXd^^4*ff*ZQlb<5kMQ0NigOBzfE?zTrM9!eq50n3Lasg z)%hxiJnina3JW_pAn>Q1H}wf<&cKne36Zqf$!uQRqX?(BZ;xkqo*i|Mm1n>?5 zWmVG@KOQl3F1s&*vJoHz)(-l^&Pm+t^t02JoL;*b*ed`$u{F=~gTXffcp_&_a8p>< z#scxY+$W_GQ$bp5dEN>!;P7mA+GY{Mj5VLhiUYo3c1ow&V})bNDv|g;e|KH<3Cd|VO@rr!U=I18teoma{gtsnGMY{KVQt*pG=tp@+^>tU5xJ# z>*1jZuMlJ+JA^x_Om-pwIYgK5PjMz?Tmv628LdIat8a=P%BO&IzPtA9Wd$Z-?hycI z0{F9M!S3y*7{;g4p4LdH3INnleauV?$<_V(A;HHI6Xnq;JOdM6m7^#(fTOK zEi=={la#<{(Cgak{>!~-SY#oXwKpHsn>+g*dNBC|!+|RSLI*!FYHD{z=Xl!)A{u~v zAVhO0fy#IddoE|aDIBMay}Hc~=4Kca%<w5g_EiguaPU zuhI_!!s_?|?uQxp-7%oHL(VWty&#!*q%xL_4Tkkj59b^fG8r}TI_4j`{Op$SEi)bw z01zPLhS)>U0fFZXI5KUDB8*_M@C7LI+ij_=G=<2NaXA*P^Ve`>4hU|>7SFYtx9=s& zIDs}5(LDl)WBtH+3oM#4X1%DfyGp5+9Dq?N1p+YL7P3+X(~OOWxkB5u$@|(iCLRCH z-!BJ&M#EbYEL6VMCo5#d^9{F(h5rDX13(L5Jrg3efd15}2NHAGjO?Tl_g$`CDG_oG=H#^r*eR5|XOj_tyx(x0;rl z^&hXFa_Hc*YDQ?0M?GOYO7pnibn~m{ z-s~#8DQv*+k?|b3#L)Wy8igwaJ*to|6$ms7%>%IRsUxOPvk&NO52^ALv&^PV zc~)U>Wiey&CzTI~fU`cyaNhug2wegiQ?>U2fP4>=0TaaDuCUNN9?HGfj+s2Q;C*$I zGStk5Wmk$D9@H)7=Ze=gTrh!A^lY-utI0BC(EaOsxf7ta6(GWe_Zu+aYgfEi5i}V9 zJ%D`AVprIg5nLc%=$QBGKbsge;a-kln+?O1z~1`(`}Z^NWzcmV>o;DBCGT^wp4$RJ)1@MP8fo#62075(j3 zqf;Sx#6?gzVloY(q{Ud{(@?JPSPZ-JMg;MP{Akm)XQNz*MLtky(Qh7M7%R1Yw7O^h zBj!xRygPk*!bpNYefq?gK83=;%HHHgxGAPlu1STRG!6~`#0&vKN67QdbGjiqLmq~c z6rM$3k(_`y>$a|}Q1jl~Fu45BbQm5GTJOa^3i0XFCw${DMVN& zGX&nO08kQ_j9FoH0VZY?plZ6Hne7LAbmqJemPu|IRHKmm25B5Iw4?P{C$1MF0Fxq{ z1mJ}e$vV91sYqpZ1;9<>UOcPvMi|KEMRF|OKQ>e6IeBk4UY`kQ|$>eax*wcivpOeE|V|p(l1x@Vi3(}(Om_rm6l+wOy zUuPwH+jQ9EsXI_nHN}tM=bXQn30xz92E9Jfu--hw0ii?YpWdNSZ%@q(5e(=<8Kc!s zD6Rk+=5s%j00l~fG+W&VzdxX12TsL-0>9WVOZ^E5Y3_*K<~+)vxbWh6Xsx!#6^0>< z_zCx?K}Com0MO;w_owm%CO0TUIYn9q{RKBan`A}5iSIGVFzXd8^13p%e*bzCa`N+9 z2A$8v+}R~j?(HOu=4L?fq6A}XI%7}GoAC0Cv%25Rn?bAy``_F+E6X0Hq44x!6Fv^` zca3QbNNAGbP5@J&d%j%2ddhTxD*jI(jqul^m^jY5|Ij`ZKRa2eZd(or7)HWX11jpo zF^mje6I}ho&kYGW6ajE=`qOaqf}&vh1gc#?P-H&dE`ctje);l681sKV zi2J{_z8PfhY4IO_{Na@^J!c$Nau|k$NiY)%yd)Sy9Jqf5`q=4ap`3N$JWQFCml<6^ zc>-N?%=5k;H!hcphP&B6lM`pt;@iu_f&iwN(Uo|yBW4<`3m|sEOmO2+V4^m>=q-}D zr~q4lGof5*3S256|J6MM{o_d;@@wHhioC)ui&0XCkY+msz@diWCvfB*^FzqaY}mXt6Uw4k zX|9M_gvBQ#^x+6_<$f4$5@0xW6VRRxN!2>1BM*H%cpK9Lfrt`%IZcfe<_XNAg0TSL zLg$SQc)pQ(40LEJ6O(Uv>%f#KfVU?1W+z@6pc4c0%8Xt2eSnvjm(QO+iwS9`PP+MZ z?ZYqC3g15l6;ksfGerg$P5^)|e`EwEecnh{_O7U~=wDEJ&#K*#%2M1`gJx}j&)z2hGa1s^ zduj(<#|27n+=)n`c?8f`Gmk=Z0;}Q*OQQO4#5&bm3|TkAbe=jI=meMKiI5Os^zcH# z4gpNn6yhl~$SR){Jwmab3R$(@HnRfVoae)eF74OXp4Sa8Gd_qPL=qw8dKz?;LB(_h z;!PhvY_zvtyYfPpgy~H_X>0vJ$9J8xx-g8&+(gPUepGKR8S$+l<-`&xE|u3i5}j)WQi)kQHwSt16QRkvfDWgQV7j; z@dAri5}kojtE1P~*BNalA#WRDGmv>&Xg&h1d`x7U`(l6+bKF$KFn7UBI0A*kiyr`V zUMcaVRp5+;Ry36l9}v8Q#3bQOu8P#wn~a$O3lI;R&!0bwjV-eZG|kq0dXiyq;PdCt zcQUN|0G<{S4+6Xl0Nnh%tI&Caun@N0(LbSMW<340G0WH>MgjRwI$++}3x6lvvv#5& z?0ClJ-Ys(@W*ETgDJ@L@ZH5{XeAxZ# zPiG?Kk%94=A=5(rv$LbDGq;rhx&pJZMN4Wk2LKmMX$0~ITmThiQ&IB?>q4)uuMY$e zI%j>#qEa+5s-dRCgJt4#tg)W!N*uY&yzxfK7-VwWA8HeF?E~vjH1zcHI!YuOWi)JVYFPbjd z%mi@dH1xsiK?qk@2#Nl73a@j~6t)JZXg%ufX+LUlw24Kdb$wZJq;aA$Tap0KW* z%ItZTLG{`QKka?>m6`8`Uf=v0B9mhP$~tI(;NbatOa`El02#bg1j{f|#ADQAPtS-e zaP-pr$jhP4>sn&z>~^KDF6OLQ*N=ic3p7K-H3H0trn{S`gMd0>4p0cmx?sYb7svVtl3J1J8Z!Oc;N2g`F4qW;z%Jp`1QW0vQ z^;z8sU~UDNbs>FhF7>^LBApNwz18D-Aja2C_3zdDJuLE4M_ZcpA20rp8&96>HZzv+ zqF}EXG(a8G`)JB>efUQ(MBfgR1_-PRpqr9uK6V{RZbj9(5jJt7-7}WvUT_fNK_FY> z#@w!AszzRj2sIB7KRjN;QBrXxto;M&57b&?T8stHI*^y_ddbQ9Bt?)};k%~1pa5!W zA-M@K&I0KNr=7Itw!O9jTe^s(ag)_Wo?hb9Uk2r-&A14@Bbt{(Aw3v(&ggz7hG6>P z3aRE8Sqz;M6X~*OK0Yb diff --git a/FModel/Resources/nz.png b/FModel/Resources/nz.png index 8e6b10155479b8bd266628b55b313478fea6995b..fcab8840b9ce49420cec0883e65654b90dd6aef2 100644 GIT binary patch literal 3474 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 A+yDRo literal 20239 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 A;Q#;t literal 19207 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 diff --git a/FModel/Resources/pz.png b/FModel/Resources/pz.png index 7f825fbb961ac25c68cc607d074e278e103e43c2..8fece6be1b598a4adaa3193b4665e7bb35aeab10 100644 GIT binary patch literal 3840 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 mcu{Z4PSrLxuO|rCpiaBHnkjmq>ziL^01Xu# _vbo; private VertexArrayObject _vao; - private string[] _textures = { "nx", "ny", "nz", "px", "py", "pz" }; + private string[] _textures = { "px", "nx", "py", "ny", "pz", "nz" }; private Texture _cubeMap; private Shader _shader; diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 1c18458f..6c4bd389 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -1,5 +1,7 @@ using System; using System.Numerics; +using System.Runtime.InteropServices; +using System.Windows; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.SkeletalMesh; using CUE4Parse.UE4.Assets.Exports.StaticMesh; @@ -7,11 +9,15 @@ using CUE4Parse.UE4.Objects.Core.Math; using CUE4Parse_Conversion.Meshes; using FModel.Extensions; using ImGuiNET; +using Silk.NET.Core; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.OpenGL.Extensions.ImGui; using Silk.NET.Windowing; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; namespace FModel.Views.Snooper; @@ -24,8 +30,9 @@ public class Snooper private IKeyboard _keyboard; private IMouse _mouse; private Vector2 _previousMousePosition; + private RawImage _icon; - // private Skybox _skybox; + private Skybox _skybox; private Grid _grid; private Model[] _models; @@ -35,8 +42,8 @@ public class Snooper public Snooper(UObject export) { const double ratio = .7; - var x = System.Windows.SystemParameters.MaximizedPrimaryScreenWidth; - var y = System.Windows.SystemParameters.MaximizedPrimaryScreenHeight; + var x = SystemParameters.MaximizedPrimaryScreenWidth; + var y = SystemParameters.MaximizedPrimaryScreenHeight; Width = Convert.ToInt32(x * ratio); Height = Convert.ToInt32(y * ratio); @@ -45,7 +52,22 @@ public class Snooper options.WindowBorder = WindowBorder.Hidden; options.Title = "Snooper"; options.Samples = 4; - _window = Window.Create(options); + _window = Silk.NET.Windowing.Window.Create(options); + + unsafe + { + var info = Application.GetResourceStream(new Uri("/FModel;component/Resources/materialicon.png", UriKind.Relative)); + using var image = Image.Load(info.Stream); + var memoryGroup = image.GetPixelMemoryGroup(); + Memory array = new byte[memoryGroup.TotalLength * sizeof(Rgba32)]; + var block = MemoryMarshal.Cast(array.Span); + foreach (var memory in memoryGroup) + { + memory.Span.CopyTo(block); + block = block.Slice(memory.Length); + } + _icon = new RawImage(image.Width, image.Height, array); + } _window.Load += OnLoad; _window.Update += OnUpdate; @@ -53,7 +75,7 @@ public class Snooper _window.Closing += OnClose; _window.FramebufferResize += OnFramebufferResize; - // _skybox = new Skybox(); + _skybox = new Skybox(); _grid = new Grid(); _models = new Model[1]; switch (export) @@ -90,6 +112,7 @@ public class Snooper private void OnLoad() { + _window.SetWindowIcon(ref _icon); _window.Center(); var input = _window.CreateInput(); @@ -109,7 +132,7 @@ public class Snooper _controller = new ImGuiController(_gl, _window, input); - // _skybox.Setup(_gl); + _skybox.Setup(_gl); _grid.Setup(_gl); foreach (var model in _models) @@ -130,7 +153,7 @@ public class Snooper _gl.ClearColor(0.102f, 0.102f, 0.129f, 1.0f); _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); - // _skybox.Bind(_camera); + _skybox.Bind(_camera); _grid.Bind(_camera); ImGuiExtensions.Theme(); @@ -148,7 +171,7 @@ public class Snooper ImGui.EndMenu(); } - const string text = "ESC to Exit..."; + const string text = "Press ESC to Exit..."; ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); ImGui.TextColored(ImGuiExtensions.STYLE.Colors[(int) ImGuiCol.TextDisabled], text); @@ -235,7 +258,7 @@ public class Snooper private void OnClose() { _grid.Dispose(); - // _skybox.Dispose(); + _skybox.Dispose(); foreach (var model in _models) { model.Dispose(); diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index fc7c48e0..5478433c 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -1,7 +1,8 @@ using Silk.NET.OpenGL; using System; -using System.IO; using System.Windows; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace FModel.Views.Snooper; @@ -56,13 +57,19 @@ public class Texture : IDisposable for (int t = 0; t < textures.Length; t++) { var info = Application.GetResourceStream(new Uri($"/FModel;component/Resources/{textures[t]}.png", UriKind.Relative)); - var stream = new MemoryStream(); - info.Stream.CopyTo(stream); + using var img = Image.Load(info.Stream); + _gl.TexImage2D(TextureTarget.TextureCubeMapPositiveX + t, 0, InternalFormat.Rgba8, (uint) img.Width, (uint) img.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, null); - fixed (void* d = &stream.ToArray()[0]) + img.ProcessPixelRows(accessor => { - _gl.TexImage2D(TextureTarget.TextureCubeMapPositiveX + t, 0, (int) InternalFormat.Rgb, 256, 256, 0, PixelFormat.Rgb, PixelType.UnsignedByte, d); - } + for (int y = 0; y < accessor.Height; y++) + { + fixed (void* data = accessor.GetRowSpan(y)) + { + gl.TexSubImage2D(TextureTarget.TextureCubeMapPositiveX + t, 0, 0, y, (uint) accessor.Width, 1, PixelFormat.Rgba, PixelType.UnsignedByte, data); + } + } + }); } _gl.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMinFilter, (int) GLEnum.Linear); From cfc1a31fc7c15e866e7040fe7e9de99b71be51cc Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 00:58:07 +0200 Subject: [PATCH 016/108] there are a lot of new problems it crashes because of Avalon sometimes, ImGui isn't stable sometimes, icon display name is not centered anymore, still mouse and keyboard somtime freeze --- FModel/Extensions/ImGuiExtensions.cs | 23 ++++++++++ FModel/FModel.csproj | 2 +- FModel/MainWindow.xaml.cs | 2 +- FModel/Resources/shader.frag | 34 ++++++++++----- FModel/ViewModels/CUE4ParseViewModel.cs | 4 +- FModel/Views/Snooper/Model.cs | 30 +------------ FModel/Views/Snooper/Section.cs | 52 ++++++++++++++++++++-- FModel/Views/Snooper/Shader.cs | 20 +++++++++ FModel/Views/Snooper/Snooper.cs | 57 +++++++++---------------- 9 files changed, 142 insertions(+), 82 deletions(-) diff --git a/FModel/Extensions/ImGuiExtensions.cs b/FModel/Extensions/ImGuiExtensions.cs index 88f749d6..0b293077 100644 --- a/FModel/Extensions/ImGuiExtensions.cs +++ b/FModel/Extensions/ImGuiExtensions.cs @@ -8,6 +8,29 @@ public static class ImGuiExtensions public const float PADDING = 5.0f; public static ImGuiStylePtr STYLE = ImGui.GetStyle(); + public static void DrawNavbar() + { + if (ImGui.BeginMainMenuBar()) + { + if (ImGui.BeginMenu("Edit")) + { + if (ImGui.MenuItem("Undo", "CTRL+Z")) {} + if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item + ImGui.Separator(); + if (ImGui.MenuItem("Cut", "CTRL+X")) {} + if (ImGui.MenuItem("Copy", "CTRL+C")) {} + if (ImGui.MenuItem("Paste", "CTRL+V")) {} + ImGui.EndMenu(); + } + + const string text = "Press ESC to Exit..."; + ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); + ImGui.TextColored(new Vector4(0.36f, 0.42f, 0.47f, 1.00f), text); // ImGuiCol.TextDisabled + + ImGui.EndMainMenuBar(); + } + } + public static void DrawFPS() { const ImGuiWindowFlags flags = diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index c11e65eb..fd4883d0 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -137,7 +137,7 @@ - + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 563c3b27..cb5099c6 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_ChristmasTree_Llama.uasset")); + "/Game/Characters/Player/Female/Medium/Bodies/F_MED_Neon_Cat_Tech/Meshes/F_MED_Neon_Cat_Tech.uasset")); #endif } diff --git a/FModel/Resources/shader.frag b/FModel/Resources/shader.frag index be50fdcf..1fa18d57 100644 --- a/FModel/Resources/shader.frag +++ b/FModel/Resources/shader.frag @@ -5,12 +5,16 @@ in vec3 fNormal; in vec2 fTexCoords; struct Material { - sampler2D diffuse; - sampler2D normal; - sampler2D specular; - sampler2D metallic; - sampler2D emission; + sampler2D diffuseMap; + sampler2D normalMap; + sampler2D specularMap; + sampler2D metallicMap; + sampler2D emissionMap; + bool swap; + + vec4 diffuseColor; + vec4 emissionColor; float shininess; }; @@ -29,24 +33,34 @@ out vec4 FragColor; void main() { + if (material.swap) + { + FragColor = material.diffuseColor; + return; + } + // ambient - vec3 ambient = light.ambient * vec3(texture(material.diffuse, fTexCoords)); + vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, fTexCoords)); // diffuse - vec3 norm = texture(material.normal, fTexCoords).rgb; + vec3 norm = texture(material.normalMap, fTexCoords).rgb; norm = normalize(norm * 2.0 - 1.0); vec3 lightDir = normalize(light.position - fPos); float diff = max(dot(norm, lightDir), 0.0f); - vec3 diffuseMap = vec3(texture(material.diffuse, fTexCoords)); + vec3 diffuseMap = vec3(texture(material.diffuseMap, fTexCoords)); vec3 diffuse = light.diffuse * diff * diffuseMap; // specular vec3 viewDir = normalize(viewPos - fPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess); - vec3 specularMap = vec3(texture(material.specular, fTexCoords)); + vec3 specularMap = vec3(texture(material.specularMap, fTexCoords)); vec3 specular = light.specular * spec * specularMap; - vec3 result = ambient + diffuse + specular; + // emission + vec3 emissionMap = vec3(texture(material.emissionMap, fTexCoords)); + vec3 emission = material.emissionColor.rgb * emissionMap; + + vec3 result = ambient + diffuse + specular + emission; FragColor = vec4(result, 1.0); } diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 4b9ac9a6..e413dc4e 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -753,8 +753,8 @@ public class CUE4ParseViewModel : ViewModel export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): { - var snooper = new Snooper(export); - snooper.Run(); + var snooper = new Snooper(); + snooper.Run(export); return true; } case UMaterialInstance m when ModelIsOverwritingMaterial: diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index ffa32773..8a032683 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -20,8 +20,6 @@ public class Model : IDisposable private const uint _faceSize = 3; // just so we don't have to do .Length private readonly uint[] _facesIndex = { 1, 0, 2 }; - private Shader _shader; - public readonly string Name; public readonly uint[] Indices; public readonly float[] Vertices; @@ -70,8 +68,6 @@ public class Model : IDisposable _handle = _gl.CreateProgram(); - _shader = new Shader(_gl); - _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); _vao = new VertexArrayObject(_gl, _vbo, _ebo); @@ -92,36 +88,13 @@ public class Model : IDisposable _vao.Bind(); - _shader.Use(); - - _shader.SetUniform("uModel", Matrix4x4.Identity); - _shader.SetUniform("uView", camera.GetViewMatrix()); - _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); - _shader.SetUniform("viewPos", camera.Position); - - _shader.SetUniform("material.diffuse", 0); - _shader.SetUniform("material.normal", 1); - _shader.SetUniform("material.specular", 2); - // _shader.SetUniform("material.metallic", 3); - // _shader.SetUniform("material.emission", 4); - _shader.SetUniform("material.shininess", 32f); - - var lightColor = Vector3.One; - var diffuseColor = lightColor * new Vector3(0.5f); - var ambientColor = diffuseColor * new Vector3(0.2f); - - _shader.SetUniform("light.ambient", ambientColor); - _shader.SetUniform("light.diffuse", diffuseColor); // darkened - _shader.SetUniform("light.specular", Vector3.One); - _shader.SetUniform("light.position", camera.Position); - ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(Indices.Length); + Sections[section].Bind(camera, Indices.Length); _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); @@ -134,7 +107,6 @@ public class Model : IDisposable _ebo.Dispose(); _vbo.Dispose(); _vao.Dispose(); - _shader.Dispose(); for (int section = 0; section < Sections.Length; section++) { Sections[section].Dispose(); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 57991d88..a1097bf7 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; @@ -20,6 +21,11 @@ public class Section : IDisposable // private Texture _metallicMap; private Texture _emissionMap; + private Vector4 _diffuseColor; + private Vector4 _emissionColor; + + private Shader _shader; + public readonly string Name; public readonly int Index; public readonly uint FacesCount; @@ -46,8 +52,20 @@ public class Section : IDisposable _handle = _gl.CreateProgram(); + _shader = new Shader(_gl); + + if (Parameters.IsNull) + { + _diffuseColor = new Vector4(1, 0, 0, 1); + return; + } + var platform = UserSettings.Default.OverridedPlatform; - if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse) + if (!Parameters.HasTopDiffuseTexture && Parameters.DiffuseColor is { A: > 0 } diffuseColor) + { + _diffuseColor = new Vector4(diffuseColor.R, diffuseColor.G, diffuseColor.B, diffuseColor.A); + } + else if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse) { var mip = diffuse.GetFirstMip(); TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, platform, out var data, out _); @@ -75,10 +93,11 @@ public class Section : IDisposable var mip = emissive.GetFirstMip(); TextureDecoder.DecodeTexture(mip, emissive.Format, emissive.isNormalMap, platform, out var data, out _); _emissionMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + _emissionColor = new Vector4(emissiveColor.R, emissiveColor.G, emissiveColor.B, emissiveColor.A); } } - public void Bind(float indices) + public void Bind(Camera camera, float indices) { ImGui.TableNextRow(); @@ -97,15 +116,42 @@ public class Section : IDisposable ImGui.EndTooltip(); } - if (Parameters.IsNull) return; + _shader.Use(); + + _shader.SetUniform("uModel", Matrix4x4.Identity); + _shader.SetUniform("uView", camera.GetViewMatrix()); + _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); + _shader.SetUniform("viewPos", camera.Position); + + _shader.SetUniform("material.diffuseMap", 0); + _shader.SetUniform("material.normalMap", 1); + _shader.SetUniform("material.specularMap", 2); + // _shader.SetUniform("material.metallicMap", 3); + _shader.SetUniform("material.emissionMap", 4); + _shader.SetUniform("material.shininess", 32f); + + _shader.SetUniform("material.swap", Convert.ToUInt32(_diffuseColor != Vector4.Zero)); + _shader.SetUniform("material.diffuseColor", _diffuseColor); + _shader.SetUniform("material.emissionColor", _emissionColor); + _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); _specularMap?.Bind(TextureUnit.Texture2); _emissionMap?.Bind(TextureUnit.Texture4); + + var lightColor = Vector3.One; + var diffuseColor = lightColor * new Vector3(0.5f); + var ambientColor = diffuseColor * new Vector3(0.2f); + + _shader.SetUniform("light.ambient", ambientColor); + _shader.SetUniform("light.diffuse", diffuseColor); // darkened + _shader.SetUniform("light.specular", Vector3.One); + _shader.SetUniform("light.position", camera.Position); } public void Dispose() { + _shader.Dispose(); _diffuseMap?.Dispose(); _normalMap?.Dispose(); _specularMap?.Dispose(); diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 879b9eec..5ac485a2 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -61,6 +61,16 @@ public class Shader : IDisposable _gl.UniformMatrix4(location, 1, false, (float*) &value); } + public void SetUniform(string name, uint value) + { + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.Uniform1(location, value); + } + public void SetUniform(string name, float value) { int location = _gl.GetUniformLocation(_handle, name); @@ -81,6 +91,16 @@ public class Shader : IDisposable _gl.Uniform3(location, value.X, value.Y, value.Z); } + public void SetUniform(string name, Vector4 value) + { + int location = _gl.GetUniformLocation(_handle, name); + if (location == -1) + { + throw new Exception($"{name} uniform not found on shader."); + } + _gl.Uniform4(location, value.X, value.Y, value.Z, value.W); + } + public void Dispose() { _gl.DeleteProgram(_handle); diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 6c4bd389..f9646acb 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; using System.Windows; @@ -32,23 +33,23 @@ public class Snooper private Vector2 _previousMousePosition; private RawImage _icon; - private Skybox _skybox; - private Grid _grid; - private Model[] _models; + private readonly Skybox _skybox; + private readonly Grid _grid; + private readonly List _models; - public int Width { get; } - public int Height { get; } + private readonly int _width; + private readonly int _height; - public Snooper(UObject export) + public Snooper() { const double ratio = .7; var x = SystemParameters.MaximizedPrimaryScreenWidth; var y = SystemParameters.MaximizedPrimaryScreenHeight; - Width = Convert.ToInt32(x * ratio); - Height = Convert.ToInt32(y * ratio); + _width = Convert.ToInt32(x * ratio); + _height = Convert.ToInt32(y * ratio); var options = WindowOptions.Default; - options.Size = new Vector2D(Width, Height); + options.Size = new Vector2D(_width, _height); options.WindowBorder = WindowBorder.Hidden; options.Title = "Snooper"; options.Samples = 4; @@ -77,28 +78,29 @@ public class Snooper _skybox = new Skybox(); _grid = new Grid(); - _models = new Model[1]; + _models = new List(); + } + + public void Run(UObject export) + { switch (export) { case UStaticMesh st when st.TryConvert(out var mesh): { - _models[0] = new Model(st.Name, mesh.LODs[0], mesh.LODs[0].Verts); + _models.Add(new Model(st.Name, mesh.LODs[0], mesh.LODs[0].Verts)); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } case USkeletalMesh sk when sk.TryConvert(out var mesh): { - _models[0] = new Model(sk.Name, mesh.LODs[0], mesh.LODs[0].Verts); + _models.Add(new Model(sk.Name, mesh.LODs[0], mesh.LODs[0].Verts)); SetupCamera(mesh.BoundingBox *= Constants.SCALE_DOWN_RATIO); break; } default: throw new ArgumentOutOfRangeException(nameof(export)); } - } - public void Run() - { _window.Run(); } @@ -132,6 +134,8 @@ public class Snooper _controller = new ImGuiController(_gl, _window, input); + ImGuiExtensions.Theme(); + _skybox.Setup(_gl); _grid.Setup(_gl); @@ -156,27 +160,7 @@ public class Snooper _skybox.Bind(_camera); _grid.Bind(_camera); - ImGuiExtensions.Theme(); - - if (ImGui.BeginMainMenuBar()) - { - if (ImGui.BeginMenu("Edit")) - { - if (ImGui.MenuItem("Undo", "CTRL+Z")) {} - if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item - ImGui.Separator(); - if (ImGui.MenuItem("Cut", "CTRL+X")) {} - if (ImGui.MenuItem("Copy", "CTRL+C")) {} - if (ImGui.MenuItem("Paste", "CTRL+V")) {} - ImGui.EndMenu(); - } - - const string text = "Press ESC to Exit..."; - ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); - ImGui.TextColored(ImGuiExtensions.STYLE.Colors[(int) ImGuiCol.TextDisabled], text); - - ImGui.EndMainMenuBar(); - } + ImGuiExtensions.DrawNavbar(); ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoSavedSettings); foreach (var model in _models) @@ -263,6 +247,7 @@ public class Snooper { model.Dispose(); } + _models.Clear(); _controller.Dispose(); _window.Dispose(); _gl.Dispose(); From 7b9e53c931477cd3e019174e8a45dfe0879a7473 Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 01:03:56 +0200 Subject: [PATCH 017/108] get out --- FModel/FModel.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index fd4883d0..c11e65eb 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -137,7 +137,7 @@ - + From b947ac3e967084c2f3ba405c9e498bc407b3f912 Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 17:14:32 +0200 Subject: [PATCH 018/108] improved shader --- CUE4Parse | 2 +- FModel/Resources/shader.frag | 73 +++++++---- FModel/Views/Snooper/Model.cs | 3 +- FModel/Views/Snooper/Section.cs | 224 +++++++++++++++++++++++++------- FModel/Views/Snooper/Shader.cs | 2 + FModel/Views/Snooper/Snooper.cs | 2 +- FModel/Views/Snooper/Texture.cs | 2 + 7 files changed, 232 insertions(+), 76 deletions(-) diff --git a/CUE4Parse b/CUE4Parse index 5725d3c3..194224af 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 5725d3c30ec311870e5832e6869af73ea40aab79 +Subproject commit 194224aff89fa4512edac7557e7a1859b878f3a8 diff --git a/FModel/Resources/shader.frag b/FModel/Resources/shader.frag index 1fa18d57..129ac22b 100644 --- a/FModel/Resources/shader.frag +++ b/FModel/Resources/shader.frag @@ -7,14 +7,16 @@ in vec2 fTexCoords; struct Material { sampler2D diffuseMap; sampler2D normalMap; - sampler2D specularMap; - sampler2D metallicMap; sampler2D emissionMap; - bool swap; + bool useSpecularMap; + sampler2D specularMap; + bool hasDiffuseColor; vec4 diffuseColor; + vec4 emissionColor; + float shininess; }; @@ -31,36 +33,55 @@ uniform vec3 viewPos; out vec4 FragColor; +vec3 getNormalFromMap() +{ + vec3 tangentNormal = texture(material.normalMap, fTexCoords).xyz * 2.0 - 1.0; + + vec3 q1 = dFdx(fPos); + vec3 q2 = dFdy(fPos); + vec2 st1 = dFdx(fTexCoords); + vec2 st2 = dFdy(fTexCoords); + + vec3 n = normalize(fNormal); + vec3 t = normalize(q1*st2.t - q2*st1.t); + vec3 b = -normalize(cross(n, t)); + mat3 tbn = mat3(t, b, n); + + return normalize(tbn * tangentNormal); +} + void main() { - if (material.swap) + if (material.hasDiffuseColor) { FragColor = material.diffuseColor; - return; } + else + { + vec3 n_normal_map = getNormalFromMap(); + vec3 n_light_direction = normalize(light.position - fPos); + vec3 result = light.ambient * vec3(texture(material.diffuseMap, fTexCoords)); - // ambient - vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, fTexCoords)); + // diffuse + float diff = max(dot(n_normal_map, n_light_direction), 0.0f); + vec4 diffuse_map = texture(material.diffuseMap, fTexCoords); + if(diffuse_map.a < 0.1f) discard; + result += light.diffuse * diff * diffuse_map.rgb; - // diffuse - vec3 norm = texture(material.normalMap, fTexCoords).rgb; - norm = normalize(norm * 2.0 - 1.0); - vec3 lightDir = normalize(light.position - fPos); - float diff = max(dot(norm, lightDir), 0.0f); - vec3 diffuseMap = vec3(texture(material.diffuseMap, fTexCoords)); - vec3 diffuse = light.diffuse * diff * diffuseMap; + // specular + if (material.useSpecularMap) + { + vec3 n_view_direction = normalize(viewPos - fPos); + vec3 reflect_direction = reflect(-n_light_direction, n_normal_map); + float spec = pow(max(dot(n_view_direction, reflect_direction), 0.0f), material.shininess); + vec3 specular_map = vec3(texture(material.specularMap, fTexCoords)); + result += light.specular * spec * specular_map.b; + } - // specular - vec3 viewDir = normalize(viewPos - fPos); - vec3 reflectDir = reflect(-lightDir, norm); - float spec = pow(max(dot(viewDir, reflectDir), 0.0f), material.shininess); - vec3 specularMap = vec3(texture(material.specularMap, fTexCoords)); - vec3 specular = light.specular * spec * specularMap; + // emission + vec3 emission_map = vec3(texture(material.emissionMap, fTexCoords)); + result += material.emissionColor.rgb * emission_map; - // emission - vec3 emissionMap = vec3(texture(material.emissionMap, fTexCoords)); - vec3 emission = material.emissionColor.rgb * emissionMap; - - vec3 result = ambient + diffuse + specular + emission; - FragColor = vec4(result, 1.0); + FragColor = vec4(result, 1.0); + } } diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index 8a032683..dee34493 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -90,11 +90,12 @@ public class Model : IDisposable ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthFixed); ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { Sections[section].Bind(camera, Indices.Length); + // if (!Sections[section].Show) continue; _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index a1097bf7..6bf40667 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -4,6 +4,7 @@ using CUE4Parse.UE4.Assets.Exports.Material; using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse_Conversion.Meshes.PSK; using CUE4Parse_Conversion.Textures; +using FModel.Services; using FModel.Settings; using ImGuiNET; using Silk.NET.OpenGL; @@ -14,15 +15,17 @@ public class Section : IDisposable { private uint _handle; private GL _gl; + private FGame _game; private Texture _diffuseMap; private Texture _normalMap; private Texture _specularMap; - // private Texture _metallicMap; private Texture _emissionMap; - private Vector4 _diffuseColor; - private Vector4 _emissionColor; + private bool _hasDiffuseColor; + private bool _hasSpecularMap; + private Vector4 _diffuseColor = Vector4.Zero; + private Vector4 _emissionColor = Vector4.Zero; private Shader _shader; @@ -32,6 +35,13 @@ public class Section : IDisposable public readonly int FirstFaceIndex; public readonly CMaterialParams Parameters; + private bool _show = true; + public bool Show + { + get => _show; + set => _show = value; + } + public Section(string name, int index, uint facesCount, int firstFaceIndex, CMeshSection section) { Name = name; @@ -44,6 +54,8 @@ public class Section : IDisposable Name = unrealMaterial.Name; unrealMaterial.GetParams(Parameters); } + + _game = ApplicationService.ApplicationView.CUE4Parse.Game; } public void Setup(GL gl) @@ -57,43 +69,152 @@ public class Section : IDisposable if (Parameters.IsNull) { _diffuseColor = new Vector4(1, 0, 0, 1); - return; + } + else + { + var platform = UserSettings.Default.OverridedPlatform; + if (!Parameters.HasTopDiffuseTexture && Parameters.DiffuseColor is { A: > 0 } diffuseColor) + { + _diffuseColor = new Vector4(diffuseColor.R, diffuseColor.G, diffuseColor.B, diffuseColor.A); + } + else if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse) + { + var mip = diffuse.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, platform, out var data, out _); + _diffuseMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.Normal is UTexture2D { IsVirtual: false } normal) + { + var mip = normal.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, normal.Format, normal.isNormalMap, platform, out var data, out _); + _normalMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.Specular is UTexture2D { IsVirtual: false } specular) + { + var mip = specular.GetFirstMip(); + SwapSpecular(specular, mip, platform, out var data); + _specularMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + } + + if (Parameters.HasTopEmissiveTexture && + Parameters.EmissiveColor is { A: > 0 } emissiveColor && + Parameters.Emissive is UTexture2D { IsVirtual: false } emissive) + { + var mip = emissive.GetFirstMip(); + TextureDecoder.DecodeTexture(mip, emissive.Format, emissive.isNormalMap, platform, out var data, out _); + _emissionMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); + _emissionColor = new Vector4(emissiveColor.R, emissiveColor.G, emissiveColor.B, emissiveColor.A); + } } - var platform = UserSettings.Default.OverridedPlatform; - if (!Parameters.HasTopDiffuseTexture && Parameters.DiffuseColor is { A: > 0 } diffuseColor) - { - _diffuseColor = new Vector4(diffuseColor.R, diffuseColor.G, diffuseColor.B, diffuseColor.A); - } - else if (Parameters.Diffuse is UTexture2D { IsVirtual: false } diffuse) - { - var mip = diffuse.GetFirstMip(); - TextureDecoder.DecodeTexture(mip, diffuse.Format, diffuse.isNormalMap, platform, out var data, out _); - _diffuseMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); - } + _hasSpecularMap = _specularMap != null; + _hasDiffuseColor = _diffuseColor != Vector4.Zero; + _show = !Parameters.IsNull && !Parameters.IsTransparent; + } - if (Parameters.Normal is UTexture2D { IsVirtual: false } normal) - { - var mip = normal.GetFirstMip(); - TextureDecoder.DecodeTexture(mip, normal.Format, normal.isNormalMap, platform, out var data, out _); - _normalMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); - } + /// + /// goal is to put + /// Metallic on Blue + /// Roughness on Green + /// Ambient Occlusion on Red + /// + private void SwapSpecular(UTexture2D specular, FTexture2DMipMap mip, ETexturePlatform platform, out byte[] data) + { + TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap, platform, out data, out _); - if (Parameters.Specular is UTexture2D { IsVirtual: false } specular) + switch (_game) { - var mip = specular.GetFirstMip(); - TextureDecoder.DecodeTexture(mip, specular.Format, specular.isNormalMap, platform, out var data, out _); - _specularMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); - } + case FGame.FortniteGame: + { + // Fortnite's Specular Texture Channels + // R Specular + // G Metallic + // B Roughness + unsafe + { + var offset = 0; + fixed (byte* d = data) + { + for (var i = 0; i < mip.SizeX * mip.SizeY; i++) + { + d[offset] = 0; + (d[offset + 1], d[offset + 2]) = (d[offset + 2], d[offset + 1]); // swap G and B + offset += 4; + } + } + } - if (Parameters.HasTopEmissiveTexture && - Parameters.EmissiveColor is { A: > 0 } emissiveColor && - Parameters.Emissive is UTexture2D { IsVirtual: false } emissive) - { - var mip = emissive.GetFirstMip(); - TextureDecoder.DecodeTexture(mip, emissive.Format, emissive.isNormalMap, platform, out var data, out _); - _emissionMap = new Texture(_gl, data, (uint) mip.SizeX, (uint) mip.SizeY); - _emissionColor = new Vector4(emissiveColor.R, emissiveColor.G, emissiveColor.B, emissiveColor.A); + Parameters.RoughnessValue = 1; + Parameters.MetallicValue = 1; + break; + } + case FGame.ShooterGame: + { + var packedPBRType = specular.Name[(specular.Name.LastIndexOf('_') + 1)..]; + switch (packedPBRType) + { + case "MRAE": // R: Metallic, G: AO (0-127) & Emissive (128-255), B: Roughness (Character PBR) + unsafe + { + var offset = 0; + fixed (byte* d = data) + { + for (var i = 0; i < mip.SizeX * mip.SizeY; i++) + { + (d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B + (d[offset], d[offset + 1]) = (d[offset + 1], d[offset]); // swap R and G + offset += 4; + } + } + } + + break; + case "MRAS": // R: Metallic, B: Roughness, B: AO, A: Specular (Legacy PBR) + case "MRA": // R: Metallic, B: Roughness, B: AO (Environment PBR) + case "MRS": // R: Metallic, G: Roughness, B: Specular (Weapon PBR) + unsafe + { + var offset = 0; + fixed (byte* d = data) + { + for (var i = 0; i < mip.SizeX * mip.SizeY; i++) + { + (d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B + offset += 4; + } + } + } + + break; + } + + Parameters.RoughnessValue = 1; + Parameters.MetallicValue = 1; + break; + } + case FGame.Gameface: + { + // GTA's Specular Texture Channels + // R Metallic + // G Roughness + // B Specular + unsafe + { + var offset = 0; + fixed (byte* d = data) + { + for (var i = 0; i < mip.SizeX * mip.SizeY; i++) + { + (d[offset], d[offset + 2]) = (d[offset + 2], d[offset]); // swap R and B + offset += 4; + } + } + } + + break; + } } } @@ -111,8 +232,19 @@ public class Section : IDisposable ImGui.Text($"Faces: {FacesCount} ({Math.Round(FacesCount / indices * 100f, 2)}%%)"); ImGui.Text($"First Face: {FirstFaceIndex}"); ImGui.Separator(); - ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); - ImGui.Text($"Normal: ({Parameters.Normal?.ExportType}) {Parameters.Normal?.Name}"); + if (_hasDiffuseColor) + { + ImGui.ColorEdit4("Diffuse Color", ref _diffuseColor, ImGuiColorEditFlags.NoInputs); + } + else + { + ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); + ImGui.Text($"Normal: ({Parameters.Normal?.ExportType}) {Parameters.Normal?.Name}"); + ImGui.Text($"Specular: ({Parameters.Specular?.ExportType}) {Parameters.Specular?.Name}"); + if (Parameters.HasTopEmissiveTexture) + ImGui.Text($"Emissive: ({Parameters.Emissive?.ExportType}) {Parameters.Emissive?.Name}"); + ImGui.Separator(); + } ImGui.EndTooltip(); } @@ -126,26 +258,24 @@ public class Section : IDisposable _shader.SetUniform("material.diffuseMap", 0); _shader.SetUniform("material.normalMap", 1); _shader.SetUniform("material.specularMap", 2); - // _shader.SetUniform("material.metallicMap", 3); - _shader.SetUniform("material.emissionMap", 4); - _shader.SetUniform("material.shininess", 32f); + _shader.SetUniform("material.useSpecularMap", _hasSpecularMap); - _shader.SetUniform("material.swap", Convert.ToUInt32(_diffuseColor != Vector4.Zero)); + _shader.SetUniform("material.hasDiffuseColor", _hasDiffuseColor); _shader.SetUniform("material.diffuseColor", _diffuseColor); + + _shader.SetUniform("material.emissionMap", 4); _shader.SetUniform("material.emissionColor", _emissionColor); + _shader.SetUniform("material.shininess", Parameters.MetallicValue); + _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); _specularMap?.Bind(TextureUnit.Texture2); _emissionMap?.Bind(TextureUnit.Texture4); - var lightColor = Vector3.One; - var diffuseColor = lightColor * new Vector3(0.5f); - var ambientColor = diffuseColor * new Vector3(0.2f); - - _shader.SetUniform("light.ambient", ambientColor); - _shader.SetUniform("light.diffuse", diffuseColor); // darkened - _shader.SetUniform("light.specular", Vector3.One); + _shader.SetUniform("light.ambient", new Vector3(0.2f)); + _shader.SetUniform("light.diffuse", new Vector3(0.75f)); + _shader.SetUniform("light.specular", new Vector3(0.5f)); _shader.SetUniform("light.position", camera.Position); } diff --git a/FModel/Views/Snooper/Shader.cs b/FModel/Views/Snooper/Shader.cs index 5ac485a2..cd540d43 100644 --- a/FModel/Views/Snooper/Shader.cs +++ b/FModel/Views/Snooper/Shader.cs @@ -61,6 +61,8 @@ public class Shader : IDisposable _gl.UniformMatrix4(location, 1, false, (float*) &value); } + public void SetUniform(string name, bool value) => SetUniform(name, Convert.ToUInt32(value)); + public void SetUniform(string name, uint value) { int location = _gl.GetUniformLocation(_handle, name); diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index f9646acb..05401e7c 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -134,7 +134,7 @@ public class Snooper _controller = new ImGuiController(_gl, _window, input); - ImGuiExtensions.Theme(); + // ImGuiExtensions.Theme(); _skybox.Setup(_gl); _grid.Setup(_gl); diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index 5478433c..8a1e60d4 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -90,6 +90,8 @@ public class Texture : IDisposable _gl.BindTexture(target, _handle); } + public IntPtr GetPointer() => (IntPtr) _handle; + public void Dispose() { _gl.DeleteTexture(_handle); From 009ad9dc55bb33583dfcd0a5d58eafe3aaa72950 Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 17:53:30 +0200 Subject: [PATCH 019/108] fixed light without normal map --- FModel/Views/Snooper/Section.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 6bf40667..78177d8a 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -22,11 +22,15 @@ public class Section : IDisposable private Texture _specularMap; private Texture _emissionMap; - private bool _hasDiffuseColor; private bool _hasSpecularMap; + private bool _hasDiffuseColor; private Vector4 _diffuseColor = Vector4.Zero; private Vector4 _emissionColor = Vector4.Zero; + private Vector3 _ambientLight; + private Vector3 _diffuseLight; + private Vector3 _specularLight; + private Shader _shader; public readonly string Name; @@ -109,6 +113,10 @@ public class Section : IDisposable } } + // diffuse light is based on normal map, so increase ambient if no normal map + _ambientLight = new Vector3(_normalMap == null ? 1.0f : 0.2f); + _diffuseLight = new Vector3(0.75f); + _specularLight = new Vector3(0.5f); _hasSpecularMap = _specularMap != null; _hasDiffuseColor = _diffuseColor != Vector4.Zero; _show = !Parameters.IsNull && !Parameters.IsTransparent; @@ -273,9 +281,9 @@ public class Section : IDisposable _specularMap?.Bind(TextureUnit.Texture2); _emissionMap?.Bind(TextureUnit.Texture4); - _shader.SetUniform("light.ambient", new Vector3(0.2f)); - _shader.SetUniform("light.diffuse", new Vector3(0.75f)); - _shader.SetUniform("light.specular", new Vector3(0.5f)); + _shader.SetUniform("light.ambient", _ambientLight); + _shader.SetUniform("light.diffuse", _diffuseLight); + _shader.SetUniform("light.specular", _specularLight); _shader.SetUniform("light.position", camera.Position); } From 37b5989378b3089530de917a945b1871a8859d5b Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 19:40:27 +0200 Subject: [PATCH 020/108] display vertex colors --- FModel/Resources/shader.frag | 8 +++++- FModel/Resources/shader.vert | 3 ++ FModel/Views/Snooper/Model.cs | 34 ++++++++++++++++++++-- FModel/Views/Snooper/Section.cs | 50 +++++++++++++-------------------- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/FModel/Resources/shader.frag b/FModel/Resources/shader.frag index 129ac22b..d2fea11d 100644 --- a/FModel/Resources/shader.frag +++ b/FModel/Resources/shader.frag @@ -3,6 +3,7 @@ in vec3 fPos; in vec3 fNormal; in vec2 fTexCoords; +in vec4 fColor; struct Material { sampler2D diffuseMap; @@ -30,6 +31,7 @@ struct Light { uniform Material material; uniform Light light; uniform vec3 viewPos; +uniform bool display_vertex_colors; out vec4 FragColor; @@ -52,7 +54,11 @@ vec3 getNormalFromMap() void main() { - if (material.hasDiffuseColor) + if (display_vertex_colors) + { + FragColor = fColor; + } + else if (material.hasDiffuseColor) { FragColor = material.diffuseColor; } diff --git a/FModel/Resources/shader.vert b/FModel/Resources/shader.vert index d2b4ab93..28918dd4 100644 --- a/FModel/Resources/shader.vert +++ b/FModel/Resources/shader.vert @@ -3,6 +3,7 @@ layout (location = 0) in vec3 vPos; layout (location = 1) in vec3 vNormal; layout (location = 2) in vec2 vTexCoords; +layout (location = 3) in vec4 vColor; uniform mat4 uModel; uniform mat4 uView; @@ -11,6 +12,7 @@ uniform mat4 uProjection; out vec3 fPos; out vec3 fNormal; out vec2 fTexCoords; +out vec4 fColor; void main() { @@ -19,4 +21,5 @@ void main() fPos = vec3(uModel * vec4(vPos, 1.0)); fNormal = mat3(transpose(inverse(uModel))) * vNormal; fTexCoords = vTexCoords; + fColor = vColor; } diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index dee34493..e0d9e327 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -12,22 +12,29 @@ public class Model : IDisposable private uint _handle; private GL _gl; + private Shader _shader; + private BufferObject _ebo; private BufferObject _vbo; private VertexArrayObject _vao; - private const int _vertexSize = 8; // Position + Normal + UV + private uint _vertexSize = 8; // Position + Normal + UV private const uint _faceSize = 3; // just so we don't have to do .Length private readonly uint[] _facesIndex = { 1, 0, 2 }; public readonly string Name; + public readonly bool HasVertexColors; public readonly uint[] Indices; public readonly float[] Vertices; public readonly Section[] Sections; + private bool _display_vertex_colors; + public Model(string name, CBaseMeshLod lod, CMeshVertex[] vertices) { Name = name; + HasVertexColors = lod.VertexColors != null; + if (HasVertexColors) _vertexSize += 4; // + Color var sections = lod.Sections.Value; Sections = new Section[sections.Length]; @@ -56,6 +63,15 @@ public class Model : IDisposable Vertices[index * _vertexSize + 6] = vert.UV.U; Vertices[index * _vertexSize + 7] = vert.UV.V; + if (HasVertexColors) + { + var color = lod.VertexColors[indice]; + Vertices[index * _vertexSize + 8] = color.R; + Vertices[index * _vertexSize + 9] = color.G; + Vertices[index * _vertexSize + 10] = color.B; + Vertices[index * _vertexSize + 11] = color.A; + } + Indices[index] = i; } } @@ -68,6 +84,8 @@ public class Model : IDisposable _handle = _gl.CreateProgram(); + _shader = new Shader(_gl); + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); _vao = new VertexArrayObject(_gl, _vbo, _ebo); @@ -75,6 +93,7 @@ public class Model : IDisposable _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, _vertexSize, 0); // position _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, _vertexSize, 3); // normal _vao.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, _vertexSize, 6); // uv + _vao.VertexAttributePointer(3, 4, VertexAttribPointerType.Float, _vertexSize, 8); // color for (int section = 0; section < Sections.Length; section++) { @@ -85,16 +104,26 @@ public class Model : IDisposable public void Bind(Camera camera) { ImGui.Text($"Entity: {Name}"); + if (HasVertexColors) + ImGui.Checkbox("Display Vertex Colors", ref _display_vertex_colors); _vao.Bind(); + _shader.Use(); + + _shader.SetUniform("uModel", Matrix4x4.Identity); + _shader.SetUniform("uView", camera.GetViewMatrix()); + _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); + _shader.SetUniform("viewPos", camera.Position); + _shader.SetUniform("display_vertex_colors", _display_vertex_colors); + ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthFixed); ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(camera, Indices.Length); + Sections[section].Bind(_shader, camera, Indices.Length); // if (!Sections[section].Show) continue; _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } @@ -105,6 +134,7 @@ public class Model : IDisposable public void Dispose() { + _shader.Dispose(); _ebo.Dispose(); _vbo.Dispose(); _vao.Dispose(); diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 78177d8a..6653ad59 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -31,8 +31,6 @@ public class Section : IDisposable private Vector3 _diffuseLight; private Vector3 _specularLight; - private Shader _shader; - public readonly string Name; public readonly int Index; public readonly uint FacesCount; @@ -68,8 +66,6 @@ public class Section : IDisposable _handle = _gl.CreateProgram(); - _shader = new Shader(_gl); - if (Parameters.IsNull) { _diffuseColor = new Vector4(1, 0, 0, 1); @@ -226,7 +222,7 @@ public class Section : IDisposable } } - public void Bind(Camera camera, float indices) + public void Bind(Shader shader, Camera camera, float indices) { ImGui.TableNextRow(); @@ -256,40 +252,32 @@ public class Section : IDisposable ImGui.EndTooltip(); } - _shader.Use(); - - _shader.SetUniform("uModel", Matrix4x4.Identity); - _shader.SetUniform("uView", camera.GetViewMatrix()); - _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); - _shader.SetUniform("viewPos", camera.Position); - - _shader.SetUniform("material.diffuseMap", 0); - _shader.SetUniform("material.normalMap", 1); - _shader.SetUniform("material.specularMap", 2); - _shader.SetUniform("material.useSpecularMap", _hasSpecularMap); - - _shader.SetUniform("material.hasDiffuseColor", _hasDiffuseColor); - _shader.SetUniform("material.diffuseColor", _diffuseColor); - - _shader.SetUniform("material.emissionMap", 4); - _shader.SetUniform("material.emissionColor", _emissionColor); - - _shader.SetUniform("material.shininess", Parameters.MetallicValue); - + shader.SetUniform("material.diffuseMap", 0); + shader.SetUniform("material.normalMap", 1); + shader.SetUniform("material.specularMap", 2); + shader.SetUniform("material.emissionMap", 3); _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); _specularMap?.Bind(TextureUnit.Texture2); - _emissionMap?.Bind(TextureUnit.Texture4); + _emissionMap?.Bind(TextureUnit.Texture3); - _shader.SetUniform("light.ambient", _ambientLight); - _shader.SetUniform("light.diffuse", _diffuseLight); - _shader.SetUniform("light.specular", _specularLight); - _shader.SetUniform("light.position", camera.Position); + shader.SetUniform("material.useSpecularMap", _hasSpecularMap); + + shader.SetUniform("material.hasDiffuseColor", _hasDiffuseColor); + shader.SetUniform("material.diffuseColor", _diffuseColor); + + shader.SetUniform("material.emissionColor", _emissionColor); + + shader.SetUniform("material.shininess", Parameters.MetallicValue); + + shader.SetUniform("light.ambient", _ambientLight); + shader.SetUniform("light.diffuse", _diffuseLight); + shader.SetUniform("light.specular", _specularLight); + shader.SetUniform("light.position", camera.Position); } public void Dispose() { - _shader.Dispose(); _diffuseMap?.Dispose(); _normalMap?.Dispose(); _specularMap?.Dispose(); From 6ba553e89025785094b36761735c67ac92866486 Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 21:11:39 +0200 Subject: [PATCH 021/108] less shader call --- FModel/Views/Snooper/Model.cs | 9 ++++++++- FModel/Views/Snooper/Section.cs | 10 +++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index e0d9e327..5463d881 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -117,18 +117,25 @@ public class Model : IDisposable _shader.SetUniform("viewPos", camera.Position); _shader.SetUniform("display_vertex_colors", _display_vertex_colors); + _shader.SetUniform("material.diffuseMap", 0); + _shader.SetUniform("material.normalMap", 1); + _shader.SetUniform("material.specularMap", 2); + _shader.SetUniform("material.emissionMap", 3); + ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthFixed); ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(_shader, camera, Indices.Length); + Sections[section].Bind(_shader, Indices.Length); // if (!Sections[section].Show) continue; _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); + _shader.SetUniform("light.position", camera.Position); + ImGui.Separator(); } diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 6653ad59..995ccf52 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -15,7 +15,6 @@ public class Section : IDisposable { private uint _handle; private GL _gl; - private FGame _game; private Texture _diffuseMap; private Texture _normalMap; @@ -31,6 +30,8 @@ public class Section : IDisposable private Vector3 _diffuseLight; private Vector3 _specularLight; + private readonly FGame _game; + public readonly string Name; public readonly int Index; public readonly uint FacesCount; @@ -222,7 +223,7 @@ public class Section : IDisposable } } - public void Bind(Shader shader, Camera camera, float indices) + public void Bind(Shader shader, float indices) { ImGui.TableNextRow(); @@ -252,10 +253,6 @@ public class Section : IDisposable ImGui.EndTooltip(); } - shader.SetUniform("material.diffuseMap", 0); - shader.SetUniform("material.normalMap", 1); - shader.SetUniform("material.specularMap", 2); - shader.SetUniform("material.emissionMap", 3); _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); _specularMap?.Bind(TextureUnit.Texture2); @@ -273,7 +270,6 @@ public class Section : IDisposable shader.SetUniform("light.ambient", _ambientLight); shader.SetUniform("light.diffuse", _diffuseLight); shader.SetUniform("light.specular", _specularLight); - shader.SetUniform("light.position", camera.Position); } public void Dispose() From 628570e84d3ca5f89fb8e47d2de07be6743f6681 Mon Sep 17 00:00:00 2001 From: 4sval Date: Tue, 30 Aug 2022 21:46:47 +0200 Subject: [PATCH 022/108] even less --- FModel/Views/Snooper/Model.cs | 24 +++--------------------- FModel/Views/Snooper/Snooper.cs | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index 5463d881..c6befbc6 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -12,8 +12,6 @@ public class Model : IDisposable private uint _handle; private GL _gl; - private Shader _shader; - private BufferObject _ebo; private BufferObject _vbo; private VertexArrayObject _vao; @@ -84,8 +82,6 @@ public class Model : IDisposable _handle = _gl.CreateProgram(); - _shader = new Shader(_gl); - _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); _vao = new VertexArrayObject(_gl, _vbo, _ebo); @@ -101,7 +97,7 @@ public class Model : IDisposable } } - public void Bind(Camera camera) + public void Bind(Shader shader) { ImGui.Text($"Entity: {Name}"); if (HasVertexColors) @@ -109,18 +105,7 @@ public class Model : IDisposable _vao.Bind(); - _shader.Use(); - - _shader.SetUniform("uModel", Matrix4x4.Identity); - _shader.SetUniform("uView", camera.GetViewMatrix()); - _shader.SetUniform("uProjection", camera.GetProjectionMatrix()); - _shader.SetUniform("viewPos", camera.Position); - _shader.SetUniform("display_vertex_colors", _display_vertex_colors); - - _shader.SetUniform("material.diffuseMap", 0); - _shader.SetUniform("material.normalMap", 1); - _shader.SetUniform("material.specularMap", 2); - _shader.SetUniform("material.emissionMap", 3); + shader.SetUniform("display_vertex_colors", _display_vertex_colors); ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); @@ -128,20 +113,17 @@ public class Model : IDisposable ImGui.TableHeadersRow(); for (int section = 0; section < Sections.Length; section++) { - Sections[section].Bind(_shader, Indices.Length); + Sections[section].Bind(shader, Indices.Length); // if (!Sections[section].Show) continue; _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } ImGui.EndTable(); - _shader.SetUniform("light.position", camera.Position); - ImGui.Separator(); } public void Dispose() { - _shader.Dispose(); _ebo.Dispose(); _vbo.Dispose(); _vao.Dispose(); diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 05401e7c..e32960a8 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -37,6 +37,8 @@ public class Snooper private readonly Grid _grid; private readonly List _models; + private Shader _shader; + private readonly int _width; private readonly int _height; @@ -139,6 +141,7 @@ public class Snooper _skybox.Setup(_gl); _grid.Setup(_gl); + _shader = new Shader(_gl); foreach (var model in _models) { model.Setup(_gl); @@ -160,17 +163,29 @@ public class Snooper _skybox.Bind(_camera); _grid.Bind(_camera); - ImGuiExtensions.DrawNavbar(); + _shader.Use(); + _shader.SetUniform("uModel", Matrix4x4.Identity); + _shader.SetUniform("uView", _camera.GetViewMatrix()); + _shader.SetUniform("uProjection", _camera.GetProjectionMatrix()); + _shader.SetUniform("viewPos", _camera.Position); + + _shader.SetUniform("material.diffuseMap", 0); + _shader.SetUniform("material.normalMap", 1); + _shader.SetUniform("material.specularMap", 2); + _shader.SetUniform("material.emissionMap", 3); + + ImGuiExtensions.DrawNavbar(); ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoSavedSettings); foreach (var model in _models) { - model.Bind(_camera); + model.Bind(_shader); } ImGui.End(); - ImGuiExtensions.DrawFPS(); + _shader.SetUniform("light.position", _camera.Position); + _controller.Render(); } @@ -243,6 +258,7 @@ public class Snooper { _grid.Dispose(); _skybox.Dispose(); + _shader.Dispose(); foreach (var model in _models) { model.Dispose(); From 5b08fc400d6e27469612fe8b9b9aacd6d3e545d0 Mon Sep 17 00:00:00 2001 From: 4sval Date: Wed, 31 Aug 2022 20:04:59 +0200 Subject: [PATCH 023/108] basically just a non msaa framebuffer --- FModel/Constants.cs | 1 + FModel/Extensions/ImGuiExtensions.cs | 187 +++++++++++++++------ FModel/FModel.csproj | 4 + FModel/Resources/framebuffer.frag | 12 ++ FModel/Resources/framebuffer.vert | 12 ++ FModel/Views/Snooper/FramebufferObject.cs | 104 ++++++++++++ FModel/Views/Snooper/Model.cs | 19 +-- FModel/Views/Snooper/RenderbufferObject.cs | 34 ++++ FModel/Views/Snooper/Section.cs | 70 ++++---- FModel/Views/Snooper/Snooper.cs | 104 +++++------- FModel/Views/Snooper/Texture.cs | 18 +- 11 files changed, 405 insertions(+), 160 deletions(-) create mode 100644 FModel/Resources/framebuffer.frag create mode 100644 FModel/Resources/framebuffer.vert create mode 100644 FModel/Views/Snooper/FramebufferObject.cs create mode 100644 FModel/Views/Snooper/RenderbufferObject.cs diff --git a/FModel/Constants.cs b/FModel/Constants.cs index 6deb29c7..0b6d0157 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -10,6 +10,7 @@ public static class Constants public static readonly FGuid ZERO_GUID = new(0U); public const float SCALE_DOWN_RATIO = 0.01F; + public const uint SAMPLES_COUNT = 4; public const string WHITE = "#DAE5F2"; public const string GRAY = "#BBBBBB"; diff --git a/FModel/Extensions/ImGuiExtensions.cs b/FModel/Extensions/ImGuiExtensions.cs index 0b293077..f317e889 100644 --- a/FModel/Extensions/ImGuiExtensions.cs +++ b/FModel/Extensions/ImGuiExtensions.cs @@ -1,12 +1,30 @@ using System.Numerics; +using FModel.Views.Snooper; using ImGuiNET; +using Silk.NET.Input; +using Silk.NET.Maths; namespace FModel.Extensions; public static class ImGuiExtensions { - public const float PADDING = 5.0f; - public static ImGuiStylePtr STYLE = ImGui.GetStyle(); + public const float PADDING = 2.5f; + + private static bool _viewportFocus; + + public static void DrawDockSpace(Vector2D size) + { + const ImGuiWindowFlags flags = + ImGuiWindowFlags.MenuBar | ImGuiWindowFlags.NoDocking | + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus; + + ImGui.SetNextWindowPos(new Vector2(0, 0)); + ImGui.SetNextWindowSize(new Vector2(size.X, size.Y)); + ImGui.Begin("Snooper", flags); + ImGui.DockSpace(ImGui.GetID("Snooper")); + } public static void DrawNavbar() { @@ -50,58 +68,123 @@ public static class ImGuiExtensions ImGui.End(); } + public static void DrawViewport(FramebufferObject framebuffer, Camera camera, IMouse mouse) + { + const float lookSensitivity = 0.1f; + const ImGuiWindowFlags flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; + + ImGui.Begin("Viewport", flags); + + var largest = ImGui.GetContentRegionAvail(); + largest.X -= ImGui.GetScrollX(); + largest.Y -= ImGui.GetScrollY(); + + var width = largest.X; + var height = width / camera.AspectRatio; + if (height > largest.Y) + { + height = largest.Y; + width = height * camera.AspectRatio; + } + + var pos = new Vector2(largest.X / 2f - width / 2f + ImGui.GetCursorPosX(), largest.Y / 2f - height / 2f + ImGui.GetCursorPosY()); + ImGui.SetCursorPos(pos); + ImGui.ImageButton(framebuffer.GetPointer(), new Vector2(width, height), new Vector2(0, 1), new Vector2(1, 0), 0); + + // it took me 5 hours to make it work, don't change any of the following code + // basically the Raw cursor doesn't actually freeze the mouse position + // so for ImGui, the IsItemHovered will be false if mouse leave, even in Raw mode + var io = ImGui.GetIO(); + if (ImGui.IsItemHovered()) + { + camera.ModifyZoom(io.MouseWheel); + + // if left button down while mouse is hover viewport + if (ImGui.IsMouseDown(ImGuiMouseButton.Left) && !_viewportFocus) + _viewportFocus = true; + } + + // this can't be inside IsItemHovered! read it as + // if left mouse button was pressed while hovering the viewport + // move camera until left mouse button is released + // no matter where mouse position end up + if (ImGui.IsMouseDragging(ImGuiMouseButton.Left, lookSensitivity) && _viewportFocus) + { + var delta = io.MouseDelta * lookSensitivity; + camera.ModifyDirection(delta.X, delta.Y); + mouse.Cursor.CursorMode = CursorMode.Raw; + } + + // if left button up and mouse was in viewport + if (ImGui.IsMouseReleased(ImGuiMouseButton.Left) && _viewportFocus) + { + _viewportFocus = false; + mouse.Cursor.CursorMode = CursorMode.Normal; + } + + ImGui.End(); + } + public static void Theme() { - STYLE.FrameRounding = 4.0f; - STYLE.GrabRounding = 4.0f; + var io = ImGui.GetIO(); + io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + io.ConfigWindowsMoveFromTitleBarOnly = true; + io.ConfigWindowsResizeFromEdges = false; - STYLE.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); - STYLE.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.11f, 0.15f, 0.17f, 0.35f); - STYLE.Colors[(int) ImGuiCol.ChildBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); - STYLE.Colors[(int) ImGuiCol.PopupBg] = new Vector4(0.08f, 0.08f, 0.08f, 0.94f); - STYLE.Colors[(int) ImGuiCol.Border] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); - STYLE.Colors[(int) ImGuiCol.BorderShadow] = new Vector4(0.00f, 0.00f, 0.00f, 0.00f); - STYLE.Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - STYLE.Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.12f, 0.20f, 0.28f, 1.00f); - STYLE.Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.09f, 0.12f, 0.14f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TitleBg] = new Vector4(0.09f, 0.12f, 0.14f, 0.65f); - STYLE.Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TitleBgCollapsed] = new Vector4(0.00f, 0.00f, 0.00f, 0.51f); - STYLE.Colors[(int) ImGuiCol.MenuBarBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ScrollbarBg] = new Vector4(0.02f, 0.02f, 0.02f, 0.39f); - STYLE.Colors[(int) ImGuiCol.ScrollbarGrab] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ScrollbarGrabHovered] = new Vector4(0.18f, 0.22f, 0.25f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ScrollbarGrabActive] = new Vector4(0.09f, 0.21f, 0.31f, 1.00f); - STYLE.Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.37f, 0.61f, 1.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.Button] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.06f, 0.53f, 0.98f, 1.00f); - STYLE.Colors[(int) ImGuiCol.Header] = new Vector4(0.20f, 0.25f, 0.29f, 0.55f); - STYLE.Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); - STYLE.Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); - STYLE.Colors[(int) ImGuiCol.Separator] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - STYLE.Colors[(int) ImGuiCol.SeparatorHovered] = new Vector4(0.10f, 0.40f, 0.75f, 0.78f); - STYLE.Colors[(int) ImGuiCol.SeparatorActive] = new Vector4(0.10f, 0.40f, 0.75f, 1.00f); - STYLE.Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.26f, 0.59f, 0.98f, 0.25f); - STYLE.Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.67f); - STYLE.Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.26f, 0.59f, 0.98f, 0.95f); - STYLE.Colors[(int) ImGuiCol.Tab] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); - STYLE.Colors[(int) ImGuiCol.TabActive] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TabUnfocused] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TabUnfocusedActive] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); - STYLE.Colors[(int) ImGuiCol.PlotLines] = new Vector4(0.61f, 0.61f, 0.61f, 1.00f); - STYLE.Colors[(int) ImGuiCol.PlotLinesHovered] = new Vector4(1.00f, 0.43f, 0.35f, 1.00f); - STYLE.Colors[(int) ImGuiCol.PlotHistogram] = new Vector4(0.90f, 0.70f, 0.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.PlotHistogramHovered] = new Vector4(1.00f, 0.60f, 0.00f, 1.00f); - STYLE.Colors[(int) ImGuiCol.TextSelectedBg] = new Vector4(0.26f, 0.59f, 0.98f, 0.35f); - STYLE.Colors[(int) ImGuiCol.DragDropTarget] = new Vector4(1.00f, 1.00f, 0.00f, 0.90f); - STYLE.Colors[(int) ImGuiCol.NavHighlight] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); - STYLE.Colors[(int) ImGuiCol.NavWindowingHighlight] = new Vector4(1.00f, 1.00f, 1.00f, 0.70f); - STYLE.Colors[(int) ImGuiCol.NavWindowingDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.20f); - STYLE.Colors[(int) ImGuiCol.ModalWindowDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.35f); + var style = ImGui.GetStyle(); + style.FrameRounding = 2.0f; + style.GrabRounding = 2.0f; + style.WindowPadding = Vector2.Zero; + + // style.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); + // style.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); + // style.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.149f, 0.149f, 0.188f, 0.35f); + // style.Colors[(int) ImGuiCol.ChildBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + // style.Colors[(int) ImGuiCol.PopupBg] = new Vector4(0.08f, 0.08f, 0.08f, 0.94f); + // style.Colors[(int) ImGuiCol.Border] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + // style.Colors[(int) ImGuiCol.BorderShadow] = new Vector4(0.00f, 0.00f, 0.00f, 0.00f); + // style.Colors[(int) ImGuiCol.FrameBg] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + // style.Colors[(int) ImGuiCol.FrameBgHovered] = new Vector4(0.12f, 0.20f, 0.28f, 1.00f); + // style.Colors[(int) ImGuiCol.FrameBgActive] = new Vector4(0.09f, 0.12f, 0.14f, 1.00f); + // style.Colors[(int) ImGuiCol.TitleBg] = new Vector4(0.09f, 0.12f, 0.14f, 0.65f); + // style.Colors[(int) ImGuiCol.TitleBgActive] = new Vector4(0.08f, 0.10f, 0.12f, 1.00f); + // style.Colors[(int) ImGuiCol.TitleBgCollapsed] = new Vector4(0.00f, 0.00f, 0.00f, 0.51f); + // style.Colors[(int) ImGuiCol.MenuBarBg] = new Vector4(0.15f, 0.18f, 0.22f, 1.00f); + // style.Colors[(int) ImGuiCol.ScrollbarBg] = new Vector4(0.02f, 0.02f, 0.02f, 0.39f); + // style.Colors[(int) ImGuiCol.ScrollbarGrab] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + // style.Colors[(int) ImGuiCol.ScrollbarGrabHovered] = new Vector4(0.18f, 0.22f, 0.25f, 1.00f); + // style.Colors[(int) ImGuiCol.ScrollbarGrabActive] = new Vector4(0.09f, 0.21f, 0.31f, 1.00f); + // style.Colors[(int) ImGuiCol.CheckMark] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + // style.Colors[(int) ImGuiCol.SliderGrab] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + // style.Colors[(int) ImGuiCol.SliderGrabActive] = new Vector4(0.37f, 0.61f, 1.00f, 1.00f); + // style.Colors[(int) ImGuiCol.Button] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + // style.Colors[(int) ImGuiCol.ButtonHovered] = new Vector4(0.28f, 0.56f, 1.00f, 1.00f); + // style.Colors[(int) ImGuiCol.ButtonActive] = new Vector4(0.06f, 0.53f, 0.98f, 1.00f); + // style.Colors[(int) ImGuiCol.Header] = new Vector4(0.20f, 0.25f, 0.29f, 0.55f); + // style.Colors[(int) ImGuiCol.HeaderHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + // style.Colors[(int) ImGuiCol.HeaderActive] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + // style.Colors[(int) ImGuiCol.Separator] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + // style.Colors[(int) ImGuiCol.SeparatorHovered] = new Vector4(0.10f, 0.40f, 0.75f, 0.78f); + // style.Colors[(int) ImGuiCol.SeparatorActive] = new Vector4(0.10f, 0.40f, 0.75f, 1.00f); + // style.Colors[(int) ImGuiCol.ResizeGrip] = new Vector4(0.26f, 0.59f, 0.98f, 0.25f); + // style.Colors[(int) ImGuiCol.ResizeGripHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.67f); + // style.Colors[(int) ImGuiCol.ResizeGripActive] = new Vector4(0.26f, 0.59f, 0.98f, 0.95f); + // style.Colors[(int) ImGuiCol.Tab] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + // style.Colors[(int) ImGuiCol.TabHovered] = new Vector4(0.26f, 0.59f, 0.98f, 0.80f); + // style.Colors[(int) ImGuiCol.TabActive] = new Vector4(0.20f, 0.25f, 0.29f, 1.00f); + // style.Colors[(int) ImGuiCol.TabUnfocused] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + // style.Colors[(int) ImGuiCol.TabUnfocusedActive] = new Vector4(0.11f, 0.15f, 0.17f, 1.00f); + // style.Colors[(int) ImGuiCol.PlotLines] = new Vector4(0.61f, 0.61f, 0.61f, 1.00f); + // style.Colors[(int) ImGuiCol.PlotLinesHovered] = new Vector4(1.00f, 0.43f, 0.35f, 1.00f); + // style.Colors[(int) ImGuiCol.PlotHistogram] = new Vector4(0.90f, 0.70f, 0.00f, 1.00f); + // style.Colors[(int) ImGuiCol.PlotHistogramHovered] = new Vector4(1.00f, 0.60f, 0.00f, 1.00f); + // style.Colors[(int) ImGuiCol.TextSelectedBg] = new Vector4(0.26f, 0.59f, 0.98f, 0.35f); + // style.Colors[(int) ImGuiCol.DragDropTarget] = new Vector4(1.00f, 1.00f, 0.00f, 0.90f); + // style.Colors[(int) ImGuiCol.NavHighlight] = new Vector4(0.26f, 0.59f, 0.98f, 1.00f); + // style.Colors[(int) ImGuiCol.NavWindowingHighlight] = new Vector4(1.00f, 1.00f, 1.00f, 0.70f); + // style.Colors[(int) ImGuiCol.NavWindowingDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.20f); + // style.Colors[(int) ImGuiCol.ModalWindowDimBg] = new Vector4(0.80f, 0.80f, 0.80f, 0.35f); } } diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index c11e65eb..1dcb7ee1 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -99,6 +99,8 @@ + + @@ -114,6 +116,8 @@ + + diff --git a/FModel/Resources/framebuffer.frag b/FModel/Resources/framebuffer.frag new file mode 100644 index 00000000..a039b285 --- /dev/null +++ b/FModel/Resources/framebuffer.frag @@ -0,0 +1,12 @@ +#version 330 core + +in vec2 fTexCoords; + +uniform sampler2D screenTexture; + +out vec4 FragColor; + +void main() +{ + FragColor = texture(screenTexture, fTexCoords); +} diff --git a/FModel/Resources/framebuffer.vert b/FModel/Resources/framebuffer.vert new file mode 100644 index 00000000..3d332f17 --- /dev/null +++ b/FModel/Resources/framebuffer.vert @@ -0,0 +1,12 @@ +#version 330 core + +layout (location = 0) in vec2 vPos; +layout (location = 1) in vec2 vTexCoords; + +out vec2 fTexCoords; + +void main() +{ + gl_Position = vec4(vPos.x, vPos.y, 0.0, 1.0); + fTexCoords = vTexCoords; +} diff --git a/FModel/Views/Snooper/FramebufferObject.cs b/FModel/Views/Snooper/FramebufferObject.cs new file mode 100644 index 00000000..90852810 --- /dev/null +++ b/FModel/Views/Snooper/FramebufferObject.cs @@ -0,0 +1,104 @@ +using System; +using Silk.NET.Maths; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class FramebufferObject : IDisposable +{ + private uint _handle; + private GL _gl; + + private readonly int _width; + private readonly int _height; + private readonly RenderbufferObject _renderbuffer; + + private BufferObject _ebo; + private BufferObject _vbo; + private VertexArrayObject _vao; + + private Shader _shader; + private Texture _texture; + + 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(Vector2D size) + { + _width = size.X; + _height = size.Y; + _renderbuffer = new RenderbufferObject((uint) _width, (uint) _height); + } + + public void Setup(GL gl) + { + _gl = gl; + _handle = _gl.GenFramebuffer(); + Bind(); + + _texture = new Texture(_gl, (uint) _width, (uint) _height); + + _renderbuffer.Setup(gl); + + _shader = new Shader(_gl, "framebuffer.vert", "framebuffer.frag"); + _shader.Use(); + _shader.SetUniform("screenTexture", 0); + + _ebo = new BufferObject(_gl, Indices, BufferTargetARB.ElementArrayBuffer); + _vbo = new BufferObject(_gl, Vertices, BufferTargetARB.ArrayBuffer); + _vao = new VertexArrayObject(_gl, _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 != GLEnum.FramebufferComplete) + { + throw new Exception($"Framebuffer failed to bind with error: {_gl.GetProgramInfoLog(_handle)}"); + } + } + + public void Bind() + { + _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _handle); + } + + public void UnBind() + { + _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0); + } + + public void BindStuff() + { + _gl.DepthMask(false); + + _shader.Use(); + _vao.Bind(); + + _texture.Bind(TextureUnit.Texture0); + + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint) Indices.Length); + + _gl.DepthMask(true); + } + + public IntPtr GetPointer() => _texture.GetPointer(); + + public void Dispose() + { + _vao.Dispose(); + _shader.Dispose(); + _texture.Dispose(); + _renderbuffer.Dispose(); + _gl.DeleteFramebuffer(_handle); + } +} diff --git a/FModel/Views/Snooper/Model.cs b/FModel/Views/Snooper/Model.cs index c6befbc6..b63f329a 100644 --- a/FModel/Views/Snooper/Model.cs +++ b/FModel/Views/Snooper/Model.cs @@ -99,25 +99,15 @@ public class Model : IDisposable public void Bind(Shader shader) { - ImGui.Text($"Entity: {Name}"); - if (HasVertexColors) - ImGui.Checkbox("Display Vertex Colors", ref _display_vertex_colors); - _vao.Bind(); shader.SetUniform("display_vertex_colors", _display_vertex_colors); - ImGui.BeginTable("Sections", 2, ImGuiTableFlags.RowBg); - ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Material", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableHeadersRow(); + DrawImGui(); for (int section = 0; section < Sections.Length; section++) { Sections[section].Bind(shader, Indices.Length); - // if (!Sections[section].Show) continue; - _gl.DrawArrays(PrimitiveType.Triangles, Sections[section].FirstFaceIndex, Sections[section].FacesCount); } - ImGui.EndTable(); ImGui.Separator(); } @@ -133,4 +123,11 @@ public class Model : IDisposable } _gl.DeleteProgram(_handle); } + + private void DrawImGui() + { + ImGui.Text($"Entity: {Name}"); + if (HasVertexColors) + ImGui.Checkbox("Display Vertex Colors", ref _display_vertex_colors); + } } diff --git a/FModel/Views/Snooper/RenderbufferObject.cs b/FModel/Views/Snooper/RenderbufferObject.cs new file mode 100644 index 00000000..bf1fee56 --- /dev/null +++ b/FModel/Views/Snooper/RenderbufferObject.cs @@ -0,0 +1,34 @@ +using System; +using Silk.NET.OpenGL; + +namespace FModel.Views.Snooper; + +public class RenderbufferObject : IDisposable +{ + private uint _handle; + private GL _gl; + + private readonly uint _width; + private readonly uint _height; + + public RenderbufferObject(uint width, uint height) + { + _width = width; + _height = height; + } + + public void Setup(GL gl) + { + _gl = gl; + _handle = _gl.GenRenderbuffer(); + + _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _handle); + _gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer, InternalFormat.Depth24Stencil8, _width, _height); + _gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthStencilAttachment, RenderbufferTarget.Renderbuffer, _handle); + } + + public void Dispose() + { + _gl.DeleteRenderbuffer(_handle); + } +} diff --git a/FModel/Views/Snooper/Section.cs b/FModel/Views/Snooper/Section.cs index 995ccf52..760d38de 100644 --- a/FModel/Views/Snooper/Section.cs +++ b/FModel/Views/Snooper/Section.cs @@ -39,11 +39,7 @@ public class Section : IDisposable public readonly CMaterialParams Parameters; private bool _show = true; - public bool Show - { - get => _show; - set => _show = value; - } + private bool _wireframe; public Section(string name, int index, uint facesCount, int firstFaceIndex, CMeshSection section) { @@ -225,33 +221,35 @@ public class Section : IDisposable public void Bind(Shader shader, float indices) { - ImGui.TableNextRow(); + // ImGui.TableNextRow(); + // + // ImGui.TableSetColumnIndex(0); + // ImGui.Text(Index.ToString()); + // ImGui.TableSetColumnIndex(1); + // ImGui.Text(Name); + // if (ImGui.IsItemHovered()) + // { + // ImGui.BeginTooltip(); + // ImGui.Text($"Faces: {FacesCount} ({Math.Round(FacesCount / indices * 100f, 2)}%%)"); + // ImGui.Text($"First Face: {FirstFaceIndex}"); + // ImGui.Separator(); + // if (_hasDiffuseColor) + // { + // ImGui.ColorEdit4("Diffuse Color", ref _diffuseColor, ImGuiColorEditFlags.NoInputs); + // } + // else + // { + // ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); + // ImGui.Text($"Normal: ({Parameters.Normal?.ExportType}) {Parameters.Normal?.Name}"); + // ImGui.Text($"Specular: ({Parameters.Specular?.ExportType}) {Parameters.Specular?.Name}"); + // if (Parameters.HasTopEmissiveTexture) + // ImGui.Text($"Emissive: ({Parameters.Emissive?.ExportType}) {Parameters.Emissive?.Name}"); + // ImGui.Separator(); + // } + // ImGui.EndTooltip(); + // } - ImGui.TableSetColumnIndex(0); - ImGui.Text(Index.ToString()); - ImGui.TableSetColumnIndex(1); - ImGui.Text(Name); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text($"Faces: {FacesCount} ({Math.Round(FacesCount / indices * 100f, 2)}%%)"); - ImGui.Text($"First Face: {FirstFaceIndex}"); - ImGui.Separator(); - if (_hasDiffuseColor) - { - ImGui.ColorEdit4("Diffuse Color", ref _diffuseColor, ImGuiColorEditFlags.NoInputs); - } - else - { - ImGui.Text($"Diffuse: ({Parameters.Diffuse?.ExportType}) {Parameters.Diffuse?.Name}"); - ImGui.Text($"Normal: ({Parameters.Normal?.ExportType}) {Parameters.Normal?.Name}"); - ImGui.Text($"Specular: ({Parameters.Specular?.ExportType}) {Parameters.Specular?.Name}"); - if (Parameters.HasTopEmissiveTexture) - ImGui.Text($"Emissive: ({Parameters.Emissive?.ExportType}) {Parameters.Emissive?.Name}"); - ImGui.Separator(); - } - ImGui.EndTooltip(); - } + // DrawImGui(); _diffuseMap?.Bind(TextureUnit.Texture0); _normalMap?.Bind(TextureUnit.Texture1); @@ -270,6 +268,9 @@ public class Section : IDisposable shader.SetUniform("light.ambient", _ambientLight); shader.SetUniform("light.diffuse", _diffuseLight); shader.SetUniform("light.specular", _specularLight); + + _gl.PolygonMode(MaterialFace.Front, _wireframe ? PolygonMode.Line : PolygonMode.Fill); + if (_show) _gl.DrawArrays(PrimitiveType.Triangles, FirstFaceIndex, FacesCount); } public void Dispose() @@ -280,4 +281,11 @@ public class Section : IDisposable _emissionMap?.Dispose(); _gl.DeleteProgram(_handle); } + + private void DrawImGui() + { + ImGui.Checkbox($"Show {Name}", ref _show); + ImGui.Checkbox($"Wireframe {Name}", ref _wireframe); + ImGui.Separator(); + } } diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index e32960a8..6c0ccd08 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -30,31 +30,28 @@ public class Snooper private Camera _camera; private IKeyboard _keyboard; private IMouse _mouse; - private Vector2 _previousMousePosition; private RawImage _icon; + private readonly FramebufferObject _framebuffer; private readonly Skybox _skybox; private readonly Grid _grid; private readonly List _models; private Shader _shader; - private readonly int _width; - private readonly int _height; + private Vector2D _size; public Snooper() { const double ratio = .7; var x = SystemParameters.MaximizedPrimaryScreenWidth; var y = SystemParameters.MaximizedPrimaryScreenHeight; - _width = Convert.ToInt32(x * ratio); - _height = Convert.ToInt32(y * ratio); var options = WindowOptions.Default; - options.Size = new Vector2D(_width, _height); + options.Size = _size = new Vector2D(Convert.ToInt32(x * ratio), Convert.ToInt32(y * ratio)); options.WindowBorder = WindowBorder.Hidden; options.Title = "Snooper"; - options.Samples = 4; + options.Samples = (int) Constants.SAMPLES_COUNT; _window = Silk.NET.Windowing.Window.Create(options); unsafe @@ -76,8 +73,13 @@ public class Snooper _window.Update += OnUpdate; _window.Render += OnRender; _window.Closing += OnClose; - _window.FramebufferResize += OnFramebufferResize; + _window.FramebufferResize += delegate(Vector2D vector2D) + { + _gl.Viewport(vector2D); + _size = vector2D; + }; + _framebuffer = new FramebufferObject(_size); _skybox = new Skybox(); _grid = new Grid(); _models = new List(); @@ -123,21 +125,15 @@ public class Snooper _keyboard = input.Keyboards[0]; _keyboard.KeyDown += KeyDown; _mouse = input.Mice[0]; - _mouse.MouseDown += OnMouseDown; - _mouse.MouseUp += OnMouseUp; - _mouse.MouseMove += OnMouseMove; - _mouse.Scroll += OnMouseWheel; _gl = GL.GetApi(_window); - _gl.Enable(EnableCap.Blend); - _gl.Enable(EnableCap.DepthTest); _gl.Enable(EnableCap.Multisample); - _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); _controller = new ImGuiController(_gl, _window, input); - // ImGuiExtensions.Theme(); + ImGuiExtensions.Theme(); + _framebuffer.Setup(_gl); _skybox.Setup(_gl); _grid.Setup(_gl); @@ -148,18 +144,24 @@ public class Snooper } } - private void OnFramebufferResize(Vector2D size) - { - _gl.Viewport(size); - } - + /// + /// friendly reminder this is called each frame + /// don't do crazy things inside + /// private void OnRender(double deltaTime) { _controller.Update((float) deltaTime); - _gl.ClearColor(0.102f, 0.102f, 0.129f, 1.0f); - _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); + ClearWhatHasBeenDrawn(); // in main window + _framebuffer.Bind(); // switch to dedicated window + ClearWhatHasBeenDrawn(); // in dedicated window + + ImGuiExtensions.DrawDockSpace(_size); + ImGuiExtensions.DrawNavbar(); + ImGui.ShowDemoWindow(); + + _framebuffer.BindStuff(); _skybox.Bind(_camera); _grid.Bind(_camera); @@ -175,18 +177,30 @@ public class Snooper _shader.SetUniform("material.specularMap", 2); _shader.SetUniform("material.emissionMap", 3); - ImGuiExtensions.DrawNavbar(); - ImGui.Begin("ImGui.NET", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoSavedSettings); + _shader.SetUniform("light.position", _camera.Position); + + ImGui.Begin("ImGui.NET"); foreach (var model in _models) { model.Bind(_shader); } ImGui.End(); + + ImGuiExtensions.DrawViewport(_framebuffer, _camera, _mouse); ImGuiExtensions.DrawFPS(); - _shader.SetUniform("light.position", _camera.Position); + _framebuffer.UnBind(); // switch back to main window + _controller.Render(); // render ImGui in main window + } - _controller.Render(); + private void ClearWhatHasBeenDrawn() + { + _gl.Enable(EnableCap.Blend); + _gl.Enable(EnableCap.DepthTest); + _gl.ClearColor(1.0f, 0.102f, 0.129f, 1.0f); + _gl.Clear((uint) ClearBufferMask.ColorBufferBit | (uint) ClearBufferMask.DepthBufferBit); + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + _gl.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill); } private void OnUpdate(double deltaTime) @@ -219,43 +233,9 @@ public class Snooper } } - private void OnMouseDown(IMouse mouse, MouseButton button) - { - if (button != MouseButton.Left) return; - mouse.Cursor.CursorMode = CursorMode.Raw; - } - - private void OnMouseUp(IMouse mouse, MouseButton button) - { - if (button != MouseButton.Left) return; - mouse.Cursor.CursorMode = CursorMode.Normal; - } - - private void OnMouseMove(IMouse mouse, Vector2 position) - { - if (_previousMousePosition == default) { _previousMousePosition = position; } - else - { - if (mouse.Cursor.CursorMode == CursorMode.Raw) - { - const float lookSensitivity = 0.1f; - var xOffset = (position.X - _previousMousePosition.X) * lookSensitivity; - var yOffset = (position.Y - _previousMousePosition.Y) * lookSensitivity; - - _camera.ModifyDirection(xOffset, yOffset); - } - - _previousMousePosition = position; - } - } - - private void OnMouseWheel(IMouse mouse, ScrollWheel scrollWheel) - { - _camera.ModifyZoom(scrollWheel.Y); - } - private void OnClose() { + _framebuffer.Dispose(); _grid.Dispose(); _skybox.Dispose(); _shader.Dispose(); diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index 8a1e60d4..8e00e124 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -22,11 +22,20 @@ public class Texture : IDisposable Bind(TextureUnit.Texture0); } + // public Texture(GL gl, uint width, uint height) : this(gl, TextureType.MsaaFramebuffer) + // { + // _gl.TexImage2DMultisample(TextureTarget.Texture2DMultisample, Constants.SAMPLES_COUNT, InternalFormat.Rgb, width, height, Silk.NET.OpenGL.Boolean.True); + // + // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); + // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Nearest); + // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); + // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); + // + // _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2DMultisample, _handle, 0); + // } + public unsafe Texture(GL gl, uint width, uint height) : this(gl, TextureType.Framebuffer) { - _gl = gl; - _handle = _gl.GenTexture(); - _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgb, width, height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, null); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); @@ -102,5 +111,6 @@ public enum TextureType { Normal, Cubemap, - Framebuffer + Framebuffer, + MsaaFramebuffer } From fb330e33dae734cdb8cc61f335cd80d5f15bf5a5 Mon Sep 17 00:00:00 2001 From: 4sval Date: Wed, 31 Aug 2022 23:04:15 +0200 Subject: [PATCH 024/108] msaa enabled --- FModel/MainWindow.xaml.cs | 2 +- FModel/Views/Snooper/FramebufferObject.cs | 47 +++++++++++++++------- FModel/Views/Snooper/RenderbufferObject.cs | 2 +- FModel/Views/Snooper/Snooper.cs | 7 ++-- FModel/Views/Snooper/Texture.cs | 33 +++++++-------- 5 files changed, 56 insertions(+), 35 deletions(-) diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index cb5099c6..563c3b27 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "/Game/Characters/Player/Female/Medium/Bodies/F_MED_Neon_Cat_Tech/Meshes/F_MED_Neon_Cat_Tech.uasset")); + "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_ChristmasTree_Llama.uasset")); #endif } diff --git a/FModel/Views/Snooper/FramebufferObject.cs b/FModel/Views/Snooper/FramebufferObject.cs index 90852810..947a7fc6 100644 --- a/FModel/Views/Snooper/FramebufferObject.cs +++ b/FModel/Views/Snooper/FramebufferObject.cs @@ -6,7 +6,8 @@ namespace FModel.Views.Snooper; public class FramebufferObject : IDisposable { - private uint _handle; + private uint _framebufferHandle; + private uint _postProcessingHandle; private GL _gl; private readonly int _width; @@ -18,7 +19,8 @@ public class FramebufferObject : IDisposable private VertexArrayObject _vao; private Shader _shader; - private Texture _texture; + private Texture _framebufferTexture; + private Texture _postProcessingTexture; public readonly uint[] Indices = { 0, 1, 2, 3, 4, 5 }; public readonly float[] Vertices = { @@ -42,10 +44,11 @@ public class FramebufferObject : IDisposable public void Setup(GL gl) { _gl = gl; - _handle = _gl.GenFramebuffer(); - Bind(); - _texture = new Texture(_gl, (uint) _width, (uint) _height); + _framebufferHandle = _gl.GenFramebuffer(); + Bind(_framebufferHandle); + + _framebufferTexture = new Texture(_gl, (uint) _width, (uint) _height); _renderbuffer.Setup(gl); @@ -63,18 +66,32 @@ public class FramebufferObject : IDisposable var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer); if (status != GLEnum.FramebufferComplete) { - throw new Exception($"Framebuffer failed to bind with error: {_gl.GetProgramInfoLog(_handle)}"); + throw new Exception($"Framebuffer failed to bind with error: {_gl.GetProgramInfoLog(_framebufferHandle)}"); + } + + _postProcessingHandle = _gl.GenFramebuffer(); + Bind(_postProcessingHandle); + + _postProcessingTexture = new Texture(_gl, _width, _height); + + status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer); + if (status != GLEnum.FramebufferComplete) + { + throw new Exception($"Post-Processing framebuffer failed to bind with error: {_gl.GetProgramInfoLog(_postProcessingHandle)}"); } } - public void Bind() + public void Bind() => Bind(_framebufferHandle); + public void Bind(uint handle) { - _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _handle); + _gl.BindFramebuffer(FramebufferTarget.Framebuffer, handle); } - public void UnBind() + public void BindMsaa() { - _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0); + _gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, _framebufferHandle); + _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _postProcessingHandle); + _gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height, ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Nearest); } public void BindStuff() @@ -84,21 +101,23 @@ public class FramebufferObject : IDisposable _shader.Use(); _vao.Bind(); - _texture.Bind(TextureUnit.Texture0); + _postProcessingTexture.Bind(TextureUnit.Texture0); _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint) Indices.Length); _gl.DepthMask(true); } - public IntPtr GetPointer() => _texture.GetPointer(); + public IntPtr GetPointer() => _postProcessingTexture.GetPointer(); public void Dispose() { _vao.Dispose(); _shader.Dispose(); - _texture.Dispose(); + _framebufferTexture.Dispose(); + _postProcessingTexture.Dispose(); _renderbuffer.Dispose(); - _gl.DeleteFramebuffer(_handle); + _gl.DeleteFramebuffer(_framebufferHandle); + _gl.DeleteFramebuffer(_postProcessingHandle); } } diff --git a/FModel/Views/Snooper/RenderbufferObject.cs b/FModel/Views/Snooper/RenderbufferObject.cs index bf1fee56..bc789e4e 100644 --- a/FModel/Views/Snooper/RenderbufferObject.cs +++ b/FModel/Views/Snooper/RenderbufferObject.cs @@ -23,7 +23,7 @@ public class RenderbufferObject : IDisposable _handle = _gl.GenRenderbuffer(); _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _handle); - _gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer, InternalFormat.Depth24Stencil8, _width, _height); + _gl.RenderbufferStorageMultisample(RenderbufferTarget.Renderbuffer, Constants.SAMPLES_COUNT, InternalFormat.Depth24Stencil8, _width, _height); _gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthStencilAttachment, RenderbufferTarget.Renderbuffer, _handle); } diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 6c0ccd08..ee8c1158 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -51,7 +51,6 @@ public class Snooper options.Size = _size = new Vector2D(Convert.ToInt32(x * ratio), Convert.ToInt32(y * ratio)); options.WindowBorder = WindowBorder.Hidden; options.Title = "Snooper"; - options.Samples = (int) Constants.SAMPLES_COUNT; _window = Silk.NET.Windowing.Window.Create(options); unsafe @@ -161,7 +160,6 @@ public class Snooper ImGuiExtensions.DrawNavbar(); ImGui.ShowDemoWindow(); - _framebuffer.BindStuff(); _skybox.Bind(_camera); _grid.Bind(_camera); @@ -189,7 +187,10 @@ public class Snooper ImGuiExtensions.DrawViewport(_framebuffer, _camera, _mouse); ImGuiExtensions.DrawFPS(); - _framebuffer.UnBind(); // switch back to main window + _framebuffer.BindMsaa(); + _framebuffer.Bind(0); // switch back to main window + _framebuffer.BindStuff(); + _controller.Render(); // render ImGui in main window } diff --git a/FModel/Views/Snooper/Texture.cs b/FModel/Views/Snooper/Texture.cs index 8e00e124..1e7840c6 100644 --- a/FModel/Views/Snooper/Texture.cs +++ b/FModel/Views/Snooper/Texture.cs @@ -22,24 +22,24 @@ public class Texture : IDisposable Bind(TextureUnit.Texture0); } - // public Texture(GL gl, uint width, uint height) : this(gl, TextureType.MsaaFramebuffer) - // { - // _gl.TexImage2DMultisample(TextureTarget.Texture2DMultisample, Constants.SAMPLES_COUNT, InternalFormat.Rgb, width, height, Silk.NET.OpenGL.Boolean.True); - // - // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); - // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Nearest); - // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); - // _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); - // - // _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2DMultisample, _handle, 0); - // } - - public unsafe Texture(GL gl, uint width, uint height) : this(gl, TextureType.Framebuffer) + public Texture(GL gl, uint width, uint height) : this(gl, TextureType.MsaaFramebuffer) { - _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgb, width, height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, null); + _gl.TexImage2DMultisample(TextureTarget.Texture2DMultisample, Constants.SAMPLES_COUNT, InternalFormat.Rgb, width, height, Silk.NET.OpenGL.Boolean.True); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); - _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Nearest); + _gl.TexParameter(TextureTarget.Texture2DMultisample, TextureParameterName.TextureMinFilter, (int) GLEnum.Nearest); + _gl.TexParameter(TextureTarget.Texture2DMultisample, TextureParameterName.TextureMagFilter, (int) GLEnum.Nearest); + _gl.TexParameter(TextureTarget.Texture2DMultisample, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2DMultisample, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); + + _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2DMultisample, _handle, 0); + } + + public unsafe Texture(GL gl, int width, int height) : this(gl, TextureType.Framebuffer) + { + _gl.TexImage2D(TextureTarget.Texture2D, 0, (int) InternalFormat.Rgb, (uint) width, (uint) height, 0, PixelFormat.Rgb, PixelType.UnsignedByte, null); + + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) GLEnum.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) GLEnum.Linear); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) GLEnum.ClampToEdge); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) GLEnum.ClampToEdge); @@ -94,6 +94,7 @@ public class Texture : IDisposable var target = _type switch { TextureType.Cubemap => TextureTarget.TextureCubeMap, + TextureType.MsaaFramebuffer => TextureTarget.Texture2DMultisample, _ => TextureTarget.Texture2D }; _gl.BindTexture(target, _handle); From bb7eaf3e1530bc81ea58ad2416e69f9f128ca847 Mon Sep 17 00:00:00 2001 From: 4sval Date: Wed, 31 Aug 2022 23:47:28 +0200 Subject: [PATCH 025/108] frozen events workaround --- FModel/ViewModels/CUE4ParseViewModel.cs | 6 ++++-- FModel/Views/Snooper/Snooper.cs | 27 +++---------------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index e413dc4e..15567f29 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -69,6 +69,9 @@ public class CUE4ParseViewModel : ViewModel set => SetProperty(ref _modelIsOverwritingMaterial, value); } + public Snooper SnooperViewer => _snooper ??= new Snooper(); + private Snooper _snooper; + public AbstractVfsFileProvider Provider { get; } public GameDirectoryViewModel GameDirectory { get; } public AssetsFolderViewModel AssetsFolder { get; } @@ -753,8 +756,7 @@ public class CUE4ParseViewModel : ViewModel export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): { - var snooper = new Snooper(); - snooper.Run(export); + SnooperViewer.Run(export); return true; } case UMaterialInstance m when ModelIsOverwritingMaterial: diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index ee8c1158..7eb351d1 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -24,7 +24,7 @@ namespace FModel.Views.Snooper; public class Snooper { - private IWindow _window; + private readonly IWindow _window; private ImGuiController _controller; private GL _gl; private Camera _camera; @@ -122,7 +122,6 @@ public class Snooper var input = _window.CreateInput(); _keyboard = input.Keyboards[0]; - _keyboard.KeyDown += KeyDown; _mouse = input.Mice[0]; _gl = GL.GetApi(_window); @@ -209,29 +208,19 @@ public class Snooper var multiplier = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 2f : 1f; var moveSpeed = _camera.Speed * multiplier * (float) deltaTime; if (_keyboard.IsKeyPressed(Key.W)) - { _camera.Position += moveSpeed * _camera.Direction; - } if (_keyboard.IsKeyPressed(Key.S)) - { _camera.Position -= moveSpeed * _camera.Direction; - } if (_keyboard.IsKeyPressed(Key.A)) - { _camera.Position -= Vector3.Normalize(Vector3.Cross(_camera.Direction, _camera.Up)) * moveSpeed; - } if (_keyboard.IsKeyPressed(Key.D)) - { _camera.Position += Vector3.Normalize(Vector3.Cross(_camera.Direction, _camera.Up)) * moveSpeed; - } if (_keyboard.IsKeyPressed(Key.E)) - { _camera.Position += moveSpeed * _camera.Up; - } if (_keyboard.IsKeyPressed(Key.Q)) - { _camera.Position -= moveSpeed * _camera.Up; - } + if (_keyboard.IsKeyPressed(Key.Escape)) + _window.Close(); } private void OnClose() @@ -249,14 +238,4 @@ public class Snooper _window.Dispose(); _gl.Dispose(); } - - private void KeyDown(IKeyboard keyboard, Key key, int arg3) - { - switch (key) - { - case Key.Escape: - _window.Close(); - break; - } - } } From 4194052eed4cec196851b917a0f20e50420e4da6 Mon Sep 17 00:00:00 2001 From: 4sval Date: Thu, 1 Sep 2022 20:53:05 +0200 Subject: [PATCH 026/108] ctrl s --- FModel/Extensions/ImGuiExtensions.cs | 35 ++++++++++++++-------------- FModel/Settings/UserSettings.cs | 2 +- FModel/Views/Snooper/Snooper.cs | 6 ++++- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/FModel/Extensions/ImGuiExtensions.cs b/FModel/Extensions/ImGuiExtensions.cs index f317e889..c6ab3b33 100644 --- a/FModel/Extensions/ImGuiExtensions.cs +++ b/FModel/Extensions/ImGuiExtensions.cs @@ -23,30 +23,29 @@ public static class ImGuiExtensions ImGui.SetNextWindowPos(new Vector2(0, 0)); ImGui.SetNextWindowSize(new Vector2(size.X, size.Y)); ImGui.Begin("Snooper", flags); - ImGui.DockSpace(ImGui.GetID("Snooper")); + ImGui.DockSpace(0); } public static void DrawNavbar() { - if (ImGui.BeginMainMenuBar()) + if (!ImGui.BeginMainMenuBar()) return; + + if (ImGui.BeginMenu("Edit")) { - if (ImGui.BeginMenu("Edit")) - { - if (ImGui.MenuItem("Undo", "CTRL+Z")) {} - if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item - ImGui.Separator(); - if (ImGui.MenuItem("Cut", "CTRL+X")) {} - if (ImGui.MenuItem("Copy", "CTRL+C")) {} - if (ImGui.MenuItem("Paste", "CTRL+V")) {} - ImGui.EndMenu(); - } - - const string text = "Press ESC to Exit..."; - ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); - ImGui.TextColored(new Vector4(0.36f, 0.42f, 0.47f, 1.00f), text); // ImGuiCol.TextDisabled - - ImGui.EndMainMenuBar(); + if (ImGui.MenuItem("Undo", "CTRL+Z")) {} + if (ImGui.MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item + ImGui.Separator(); + if (ImGui.MenuItem("Cut", "CTRL+X")) {} + if (ImGui.MenuItem("Copy", "CTRL+C")) {} + if (ImGui.MenuItem("Paste", "CTRL+V")) {} + ImGui.EndMenu(); } + + const string text = "Press ESC to Exit..."; + ImGui.SetCursorPosX(ImGui.GetWindowViewport().WorkSize.X - ImGui.CalcTextSize(text).X - 5); + ImGui.TextColored(new Vector4(0.36f, 0.42f, 0.47f, 1.00f), text); // ImGuiCol.TextDisabled + + ImGui.EndMainMenuBar(); } public static void DrawFPS() diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 1292021c..f1d0bef9 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -140,7 +140,7 @@ namespace FModel.Settings set => SetProperty(ref _isLoggerExpanded, value); } - private GridLength _avalonImageSize = GridLength.Auto; + private GridLength _avalonImageSize = new (200, GridUnitType.Pixel); public GridLength AvalonImageSize { get => _avalonImageSize; diff --git a/FModel/Views/Snooper/Snooper.cs b/FModel/Views/Snooper/Snooper.cs index 7eb351d1..0b7cf1d6 100644 --- a/FModel/Views/Snooper/Snooper.cs +++ b/FModel/Views/Snooper/Snooper.cs @@ -127,7 +127,10 @@ public class Snooper _gl = GL.GetApi(_window); _gl.Enable(EnableCap.Multisample); - _controller = new ImGuiController(_gl, _window, input); + // we don't need to bother with file check + // but keep an eye on font size for different monitors + var fontConfig = new ImGuiFontConfig("C:\\Windows\\Fonts\\segoeui.ttf", 16); + _controller = new ImGuiController(_gl, _window, input, fontConfig); ImGuiExtensions.Theme(); @@ -205,6 +208,7 @@ public class Snooper private void OnUpdate(double deltaTime) { + if (ImGui.GetIO().WantTextInput) return; var multiplier = _keyboard.IsKeyPressed(Key.ShiftLeft) ? 2f : 1f; var moveSpeed = _camera.Speed * multiplier * (float) deltaTime; if (_keyboard.IsKeyPressed(Key.W)) From 129f128f900a4042ce5b563a15d4fbe5452dde82 Mon Sep 17 00:00:00 2001 From: 4sval Date: Fri, 2 Sep 2022 20:38:27 +0200 Subject: [PATCH 027/108] transforms --- CUE4Parse | 2 +- FModel/Extensions/ImGuiExtensions.cs | 47 ++++------ FModel/FModel.csproj | 8 +- FModel/MainWindow.xaml.cs | 2 +- .../Resources/{shader.frag => default.frag} | 0 .../Resources/{shader.vert => default.vert} | 0 FModel/Views/SettingsView.xaml | 13 +-- FModel/Views/Snooper/Camera.cs | 9 -- FModel/Views/Snooper/FramebufferObject.cs | 2 +- FModel/Views/Snooper/Grid.cs | 4 +- FModel/Views/Snooper/Model.cs | 93 ++++++++++++++++--- FModel/Views/Snooper/Section.cs | 17 ++-- FModel/Views/Snooper/Shader.cs | 8 +- FModel/Views/Snooper/Skybox.cs | 6 +- FModel/Views/Snooper/Snooper.cs | 35 +++---- FModel/Views/Snooper/Transform.cs | 21 +++-- 16 files changed, 156 insertions(+), 111 deletions(-) rename FModel/Resources/{shader.frag => default.frag} (100%) rename FModel/Resources/{shader.vert => default.vert} (100%) diff --git a/CUE4Parse b/CUE4Parse index 194224af..19dfffa9 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 194224aff89fa4512edac7557e7a1859b878f3a8 +Subproject commit 19dfffa9e55d0951e7da75789e65c8c4e6e62a3e diff --git a/FModel/Extensions/ImGuiExtensions.cs b/FModel/Extensions/ImGuiExtensions.cs index c6ab3b33..f49c63cf 100644 --- a/FModel/Extensions/ImGuiExtensions.cs +++ b/FModel/Extensions/ImGuiExtensions.cs @@ -8,7 +8,7 @@ namespace FModel.Extensions; public static class ImGuiExtensions { - public const float PADDING = 2.5f; + public const uint DockspaceId = 1337; private static bool _viewportFocus; @@ -23,13 +23,19 @@ public static class ImGuiExtensions ImGui.SetNextWindowPos(new Vector2(0, 0)); ImGui.SetNextWindowSize(new Vector2(size.X, size.Y)); ImGui.Begin("Snooper", flags); - ImGui.DockSpace(0); + ImGui.DockSpace(DockspaceId); } public static void DrawNavbar() { if (!ImGui.BeginMainMenuBar()) return; + if (ImGui.BeginMenu("Window")) + { + ImGui.MenuItem("Append", "CTRL+A"); + ImGui.MenuItem("Close", "ESC"); + ImGui.EndMenu(); + } if (ImGui.BeginMenu("Edit")) { if (ImGui.MenuItem("Undo", "CTRL+Z")) {} @@ -48,29 +54,12 @@ public static class ImGuiExtensions ImGui.EndMainMenuBar(); } - public static void DrawFPS() - { - const ImGuiWindowFlags flags = - ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoNav; - var viewport = ImGui.GetMainViewport(); - var work_pos = viewport.WorkPos; - var work_size = viewport.WorkSize; - - ImGui.SetNextWindowPos(new Vector2(work_pos.X + PADDING, work_pos.Y + work_size.Y - PADDING), ImGuiCond.Always, new Vector2(0, 1)); - if (ImGui.Begin("FPS Overlay", flags)) - { - float framerate = ImGui.GetIO().Framerate; - ImGui.Text($"FPS: {framerate:0} ({1000.0f / framerate:0.##} ms)"); - } - ImGui.End(); - } - public static void DrawViewport(FramebufferObject framebuffer, Camera camera, IMouse mouse) { const float lookSensitivity = 0.1f; - const ImGuiWindowFlags flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; + const ImGuiWindowFlags flags = + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse | + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.AlwaysUseWindowPadding; ImGui.Begin("Viewport", flags); @@ -87,8 +76,9 @@ public static class ImGuiExtensions } var pos = new Vector2(largest.X / 2f - width / 2f + ImGui.GetCursorPosX(), largest.Y / 2f - height / 2f + ImGui.GetCursorPosY()); + var size = new Vector2(width, height); ImGui.SetCursorPos(pos); - ImGui.ImageButton(framebuffer.GetPointer(), new Vector2(width, height), new Vector2(0, 1), new Vector2(1, 0), 0); + ImGui.ImageButton(framebuffer.GetPointer(), size, new Vector2(0, 1), new Vector2(1, 0), 0); // it took me 5 hours to make it work, don't change any of the following code // basically the Raw cursor doesn't actually freeze the mouse position @@ -121,6 +111,12 @@ public static class ImGuiExtensions mouse.Cursor.CursorMode = CursorMode.Normal; } + const float padding = 5f; + float framerate = ImGui.GetIO().Framerate; + var text = $"FPS: {framerate:0} ({1000.0f / framerate:0.##} ms)"; + ImGui.SetCursorPos(new Vector2(pos.X + padding, pos.Y + size.Y - ImGui.CalcTextSize(text).Y - padding)); + ImGui.Text(text); + ImGui.End(); } @@ -130,13 +126,10 @@ public static class ImGuiExtensions io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; io.ConfigWindowsMoveFromTitleBarOnly = true; - io.ConfigWindowsResizeFromEdges = false; + io.ConfigDockingWithShift = true; var style = ImGui.GetStyle(); - style.FrameRounding = 2.0f; - style.GrabRounding = 2.0f; style.WindowPadding = Vector2.Zero; - // style.Colors[(int) ImGuiCol.Text] = new Vector4(0.95f, 0.96f, 0.98f, 1.00f); // style.Colors[(int) ImGuiCol.TextDisabled] = new Vector4(0.36f, 0.42f, 0.47f, 1.00f); // style.Colors[(int) ImGuiCol.WindowBg] = new Vector4(0.149f, 0.149f, 0.188f, 0.35f); diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 1dcb7ee1..ce1ddf6b 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -93,8 +93,8 @@ - - + + @@ -110,8 +110,8 @@ - - + + diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 563c3b27..f58f8d56 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -79,7 +79,7 @@ public partial class MainWindow #if DEBUG await _threadWorkerView.Begin(_ => _applicationView.CUE4Parse.Extract( - "FortniteGame/Content/Environments/Props/Winter/Meshes/SM_ChristmasTree_Llama.uasset")); + "Engine/Content/BasicShapes/Cube.uasset")); #endif } diff --git a/FModel/Resources/shader.frag b/FModel/Resources/default.frag similarity index 100% rename from FModel/Resources/shader.frag rename to FModel/Resources/default.frag diff --git a/FModel/Resources/shader.vert b/FModel/Resources/default.vert similarity index 100% rename from FModel/Resources/shader.vert rename to FModel/Resources/default.vert diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index a0b22557..fd3b2703 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -189,18 +189,7 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FModel/Views/ModelViewer.xaml.cs b/FModel/Views/ModelViewer.xaml.cs deleted file mode 100644 index 07d6a2bd..00000000 --- a/FModel/Views/ModelViewer.xaml.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.ComponentModel; -using System.Windows; -using System.Windows.Input; -using AdonisUI.Controls; -using CUE4Parse.UE4.Assets.Exports; -using CUE4Parse.UE4.Assets.Exports.Material; -using FModel.Services; -using FModel.ViewModels; -using HelixToolkit.Wpf.SharpDX; -using MessageBox = AdonisUI.Controls.MessageBox; -using MessageBoxImage = AdonisUI.Controls.MessageBoxImage; - -namespace FModel.Views; - -public partial class ModelViewer -{ - private bool _messageShown; - private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; - - public ModelViewer() - { - DataContext = _applicationView; - InitializeComponent(); - } - - public void Load(UObject export) => _applicationView.ModelViewer.LoadExport(export); - public void Overwrite(UMaterialInstance materialInstance) - { - if (_applicationView.ModelViewer.TryOverwriteMaterial(materialInstance)) - { - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = false; - } - else - { - MessageBox.Show(new MessageBoxModel - { - Text = "An attempt to load a material failed.", - Caption = "Error", - Icon = MessageBoxImage.Error, - Buttons = MessageBoxButtons.OkCancel(), - IsSoundEnabled = false - }); - } - } - - private void OnClosing(object sender, CancelEventArgs e) - { - _applicationView.ModelViewer.Clear(); - _applicationView.ModelViewer.AppendMode = false; - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = false; - MyAntiCrashGroup.ItemsSource = null; // <3 - } - - private async void OnWindowKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.W: - _applicationView.ModelViewer.WirefreameToggle(); - break; - case Key.H: - _applicationView.ModelViewer.RenderingToggle(); - break; - case Key.D: - _applicationView.ModelViewer.DiffuseOnlyToggle(); - break; - case Key.M: - _applicationView.ModelViewer.MaterialColorToggle(); - break; - case Key.Decimal: - _applicationView.ModelViewer.FocusOnSelectedMesh(); - break; - case Key.S when Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift): - _applicationView.ModelViewer.SaveAsScene(); - break; - case Key.S when Keyboard.Modifiers.HasFlag(ModifierKeys.Control): - await _applicationView.ModelViewer.SaveLoadedModels(); - break; - } - } - - private void OnMouse3DDown(object sender, MouseDown3DEventArgs e) - { - if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) || e.HitTestResult.ModelHit is not CustomMeshGeometryModel3D m) return; - _applicationView.ModelViewer.SelectedModel.SelectedGeometry = m; - MaterialsListName.ScrollIntoView(m); - } - - private void OnFocusClick(object sender, RoutedEventArgs e) - => _applicationView.ModelViewer.FocusOnSelectedMesh(); - - private void OnCopyClick(object sender, RoutedEventArgs e) - => _applicationView.ModelViewer.CopySelectedMaterialName(); - - private async void Save(object sender, RoutedEventArgs e) - => await _applicationView.ModelViewer.SaveLoadedModels(); - - private void OnOverwriteMaterialClick(object sender, RoutedEventArgs e) - { - _applicationView.CUE4Parse.ModelIsOverwritingMaterial = true; - if (!_messageShown) - { - MessageBox.Show(new MessageBoxModel - { - Text = "Simply extract a material once FModel will be brought to the foreground. This message will be shown once per Model Viewer's lifetime, close it to begin.", - Caption = "How To Overwrite Material?", - Icon = MessageBoxImage.Information, - IsSoundEnabled = false - }); - _messageShown = true; - } - - MainWindow.YesWeCats.Activate(); - } -} \ No newline at end of file diff --git a/FModel/Views/Resources/Converters/BoolToFillModeConverter.cs b/FModel/Views/Resources/Converters/BoolToFillModeConverter.cs deleted file mode 100644 index 3a85a8c7..00000000 --- a/FModel/Views/Resources/Converters/BoolToFillModeConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using SharpDX.Direct3D11; - -namespace FModel.Views.Resources.Converters; - -public class BoolToFillModeConverter : IValueConverter -{ - public static readonly BoolToFillModeConverter Instance = new(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return value switch - { - FillMode.Solid => true, - _ => false - }; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return value switch - { - true => FillMode.Solid, - _ => FillMode.Wireframe - }; - } -} \ No newline at end of file diff --git a/FModel/Views/Resources/Converters/DateTimeToStringConverter.cs b/FModel/Views/Resources/Converters/DateTimeToStringConverter.cs deleted file mode 100644 index 1d525940..00000000 --- a/FModel/Views/Resources/Converters/DateTimeToStringConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; - -namespace FModel.Views.Resources.Converters; - -public class DateTimeToStringConverter : IValueConverter -{ - public static readonly DateTimeToStringConverter Instance = new(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return value is not DateTime dateTime ? value : $"{dateTime.ToLongDateString()}, {dateTime.ToShortTimeString()}"; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/FModel/Views/Resources/Converters/TagToColorConverter.cs b/FModel/Views/Resources/Converters/TagToColorConverter.cs deleted file mode 100644 index ae0d4cfd..00000000 --- a/FModel/Views/Resources/Converters/TagToColorConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; -using HelixToolkit.Wpf.SharpDX; - -namespace FModel.Views.Resources.Converters; - -public class TagToColorConverter : IValueConverter -{ - public static readonly TagToColorConverter Instance = new(); - - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is not PBRMaterial material) - return new SolidColorBrush(Colors.Red); - - return new SolidColorBrush(Color.FromScRgb( - material.AlbedoColor.Alpha, material.AlbedoColor.Red, - material.AlbedoColor.Green, material.AlbedoColor.Blue)); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 984f2454..3dd838e2 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -4,7 +4,6 @@ xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:controls="clr-namespace:FModel.Views.Resources.Controls" xmlns:soundOut="clr-namespace:CSCore.SoundOut;assembly=CSCore" - xmlns:sharpDx="clr-namespace:SharpDX.Direct3D11;assembly=SharpDX.Direct3D11" xmlns:audioControls="clr-namespace:FModel.Views.Resources.Controls.Aup" xmlns:converters="clr-namespace:FModel.Views.Resources.Converters" xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit" @@ -638,91 +637,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/FModel/MainWindow.xaml.cs b/FModel/MainWindow.xaml.cs index 1e782c99..ed12174e 100644 --- a/FModel/MainWindow.xaml.cs +++ b/FModel/MainWindow.xaml.cs @@ -95,10 +95,10 @@ public partial class MainWindow if (_threadWorkerView.CanBeCanceled && e.Key == Key.Escape) { - _applicationView.Status = EStatusKind.Stopping; + _applicationView.Status.SetStatus(EStatusKind.Stopping); _threadWorkerView.Cancel(); } - else if (_applicationView.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) + else if (_applicationView.Status.IsReady && e.Key == Key.F && Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) OnSearchViewClick(null, null); else if (e.Key == Key.Left && _applicationView.CUE4Parse.TabControl.SelectedTab.HasImage) _applicationView.CUE4Parse.TabControl.SelectedTab.GoPreviousImage(); @@ -230,14 +230,14 @@ public partial class MainWindow private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e) { - if (!_applicationView.IsReady || sender is not ListBox listBox) return; + if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return; UserSettings.Default.LoadingMode = ELoadingMode.Multiple; _applicationView.LoadingModes.LoadCommand.Execute(listBox.SelectedItems); } private async void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (!_applicationView.IsReady || sender is not ListBox listBox) return; + if (!_applicationView.Status.IsReady || sender is not ListBox listBox) return; switch (e.Key) { diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index 7be5c4b1..1689364d 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -46,7 +46,7 @@ namespace FModel.Settings return false; endpoint = endpoints[(int) type]; - return endpoint.IsValid; + return endpoint.Overwrite || endpoint.IsValid; } private bool _showChangelog = true; diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 7438c6a6..9c3a1c2d 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -22,7 +22,6 @@ namespace FModel.ViewModels; public class ApplicationViewModel : ViewModel { private EBuildKind _build; - public EBuildKind Build { get => _build; @@ -33,24 +32,11 @@ public class ApplicationViewModel : ViewModel } } - private bool _isReady; - - public bool IsReady - { - get => _isReady; - private set => SetProperty(ref _isReady, value); - } - - private EStatusKind _status; - - public EStatusKind Status + private FStatus _status; + public FStatus Status { get => _status; - set - { - SetProperty(ref _status, value); - IsReady = Status != EStatusKind.Loading && Status != EStatusKind.Stopping; - } + set => SetProperty(ref _status, value); } public RightClickMenuCommand RightClickMenuCommand => _rightClickMenuCommand ??= new RightClickMenuCommand(this); @@ -76,7 +62,7 @@ public class ApplicationViewModel : ViewModel public ApplicationViewModel() { - Status = EStatusKind.Loading; + Status = new FStatus(); #if DEBUG Build = EBuildKind.Debug; #elif RELEASE @@ -93,7 +79,7 @@ public class ApplicationViewModel : ViewModel AesManager = new AesManagerViewModel(CUE4Parse); MapViewer = new MapViewerViewModel(CUE4Parse); AudioPlayer = new AudioPlayerViewModel(); - Status = EStatusKind.Ready; + Status.SetStatus(EStatusKind.Ready); } public void AvoidEmptyGameDirectoryAndSetEGame(bool bAlreadyLaunched) diff --git a/FModel/ViewModels/CUE4ParseViewModel.cs b/FModel/ViewModels/CUE4ParseViewModel.cs index 2203df3f..793b10dc 100644 --- a/FModel/ViewModels/CUE4ParseViewModel.cs +++ b/FModel/ViewModels/CUE4ParseViewModel.cs @@ -56,7 +56,6 @@ public class CUE4ParseViewModel : ViewModel RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private FGame _game; - public FGame Game { get => _game; @@ -458,7 +457,7 @@ public class CUE4ParseViewModel : ViewModel cancellationToken.ThrowIfCancellationRequested(); try { - Extract(asset.FullPath, TabControl.HasNoTabs); + Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs); } catch { @@ -489,7 +488,7 @@ public class CUE4ParseViewModel : ViewModel cancellationToken.ThrowIfCancellationRequested(); try { - Extract(asset.FullPath, TabControl.HasNoTabs, true); + Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs, true); } catch { @@ -506,11 +505,11 @@ public class CUE4ParseViewModel : ViewModel { Thread.Sleep(10); cancellationToken.ThrowIfCancellationRequested(); - Extract(asset.FullPath, TabControl.HasNoTabs); + Extract(cancellationToken, asset.FullPath, TabControl.HasNoTabs); } } - public void Extract(string fullPath, bool addNewTab = false, bool bulkSave = false) + public void Extract(CancellationToken cancellationToken, string fullPath, bool addNewTab = false, bool bulkSave = false) { Log.Information("User DOUBLE-CLICKED to extract '{FullPath}'", fullPath); @@ -543,7 +542,7 @@ public class CUE4ParseViewModel : ViewModel foreach (var e in exports) { - if (CheckExport(e)) + if (CheckExport(cancellationToken, e)) break; } @@ -704,7 +703,7 @@ public class CUE4ParseViewModel : ViewModel } } - public void ExtractAndScroll(string fullPath, string objectName) + public void ExtractAndScroll(CancellationToken cancellationToken, string fullPath, string objectName) { Log.Information("User CTRL-CLICKED to extract '{FullPath}'", fullPath); TabControl.AddTab(fullPath.SubstringAfterLast('/'), fullPath.SubstringBeforeLast('/')); @@ -716,12 +715,12 @@ public class CUE4ParseViewModel : ViewModel foreach (var e in exports) { - if (CheckExport(e)) + if (CheckExport(cancellationToken, e)) break; } } - private bool CheckExport(UObject export) // return true once you wanna stop searching for exports + private bool CheckExport(CancellationToken cancellationToken, UObject export) // return true once you wanna stop searching for exports { switch (export) { @@ -758,7 +757,7 @@ public class CUE4ParseViewModel : ViewModel export.Owner.Name.EndsWith($"/RenderSwitch_Materials/{export.Name}", StringComparison.OrdinalIgnoreCase) || export.Owner.Name.EndsWith($"/MI_BPTile/{export.Name}", StringComparison.OrdinalIgnoreCase))): { - SnooperViewer.Run(export); + SnooperViewer.Run(cancellationToken, export); return true; } case UMaterialInstance m when ModelIsOverwritingMaterial: diff --git a/FModel/ViewModels/Commands/LoadCommand.cs b/FModel/ViewModels/Commands/LoadCommand.cs index e8bc4f7d..3e26ccc4 100644 --- a/FModel/ViewModels/Commands/LoadCommand.cs +++ b/FModel/ViewModels/Commands/LoadCommand.cs @@ -105,8 +105,7 @@ public class LoadCommand : ViewModelCommand private void FilterDirectoryFilesToDisplay(CancellationToken cancellationToken, IEnumerable directoryFiles) { HashSet filter; - if (directoryFiles == null) - filter = null; + if (directoryFiles == null) filter = null; else { filter = new HashSet(); diff --git a/FModel/ViewModels/Commands/RightClickMenuCommand.cs b/FModel/ViewModels/Commands/RightClickMenuCommand.cs index ff41ba8e..0e2f6dcc 100644 --- a/FModel/ViewModels/Commands/RightClickMenuCommand.cs +++ b/FModel/ViewModels/Commands/RightClickMenuCommand.cs @@ -29,7 +29,7 @@ public class RightClickMenuCommand : ViewModelCommand foreach (var asset in assetItems) { cancellationToken.ThrowIfCancellationRequested(); - contextViewModel.CUE4Parse.Extract(asset.FullPath, true); + contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath, true); } break; @@ -45,7 +45,7 @@ public class RightClickMenuCommand : ViewModelCommand foreach (var asset in assetItems) { cancellationToken.ThrowIfCancellationRequested(); - contextViewModel.CUE4Parse.Extract(asset.FullPath); + contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath); contextViewModel.CUE4Parse.TabControl.SelectedTab.SaveProperty(false); } @@ -54,7 +54,7 @@ public class RightClickMenuCommand : ViewModelCommand foreach (var asset in assetItems) { cancellationToken.ThrowIfCancellationRequested(); - contextViewModel.CUE4Parse.Extract(asset.FullPath); + contextViewModel.CUE4Parse.Extract(cancellationToken, asset.FullPath); contextViewModel.CUE4Parse.TabControl.SelectedTab.SaveImage(false); } @@ -62,4 +62,4 @@ public class RightClickMenuCommand : ViewModelCommand } }); } -} \ No newline at end of file +} diff --git a/FModel/ViewModels/ThreadWorkerViewModel.cs b/FModel/ViewModels/ThreadWorkerViewModel.cs index 2f8fc49f..dc6ad853 100644 --- a/FModel/ViewModels/ThreadWorkerViewModel.cs +++ b/FModel/ViewModels/ThreadWorkerViewModel.cs @@ -53,7 +53,7 @@ public class ThreadWorkerViewModel : ViewModel public async Task Begin(Action action) { - if (!_applicationView.IsReady) + if (!_applicationView.Status.IsReady) { SignalOperationInProgress(); return; @@ -79,7 +79,7 @@ public class ThreadWorkerViewModel : ViewModel { if (_jobs.Count > 0) { - _applicationView.Status = EStatusKind.Loading; + _applicationView.Status.SetStatus(EStatusKind.Loading); await foreach (var job in _jobs) { try @@ -89,7 +89,7 @@ public class ThreadWorkerViewModel : ViewModel } catch (OperationCanceledException) { - _applicationView.Status = EStatusKind.Stopped; + _applicationView.Status.SetStatus(EStatusKind.Stopped); CurrentCancellationTokenSource = null; // kill token OperationCancelled = true; OperationCancelled = false; @@ -97,7 +97,7 @@ public class ThreadWorkerViewModel : ViewModel } catch (Exception e) { - _applicationView.Status = EStatusKind.Failed; + _applicationView.Status.SetStatus(EStatusKind.Failed); CurrentCancellationTokenSource = null; // kill token Log.Error("{Exception}", e); @@ -134,7 +134,7 @@ public class ThreadWorkerViewModel : ViewModel } } - _applicationView.Status = EStatusKind.Completed; + _applicationView.Status.SetStatus(EStatusKind.Completed); CurrentCancellationTokenSource = null; // kill token } } diff --git a/FModel/Views/MapViewer.xaml b/FModel/Views/MapViewer.xaml index 34b57e04..54de3a50 100644 --- a/FModel/Views/MapViewer.xaml +++ b/FModel/Views/MapViewer.xaml @@ -24,41 +24,41 @@ + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> - + + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> - + + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> + DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Views.MapViewer}}}" IsEnabled="{Binding Status.IsReady}" /> @@ -108,7 +108,7 @@ -