From 614787789ec3c9c2efc4dcc5e3863a5489278d64 Mon Sep 17 00:00:00 2001 From: bird_egop Date: Fri, 5 Jun 2026 20:47:57 +0300 Subject: [PATCH] qol --- NResUI/ImGuiUI/ViewportPanel.cs | 78 ++++++++++++-- NResUI/Rendering/Viewport/Meshes/GpuMesh.cs | 2 +- .../Viewport/Meshes/PrimitiveMeshes.cs | 53 ++++++++++ .../Viewport/OpenGL/MsaaFramebuffer.cs | 2 +- NResUI/Rendering/Viewport/ViewportBounds.cs | 18 ++++ NResUI/Rendering/Viewport/ViewportCamera.cs | 89 ++++++++++++++-- .../Viewport/ViewportDebugSettings.cs | 10 ++ .../Viewport/ViewportInputController.cs | 46 +++++++- NResUI/Rendering/Viewport/ViewportRenderer.cs | 100 ++++++++++++++++-- NResUI/Rendering/Viewport/ViewportScene.cs | 72 +++++++++++++ .../Rendering/Viewport/ViewportSelection.cs | 18 ++-- 11 files changed, 446 insertions(+), 42 deletions(-) create mode 100644 NResUI/Rendering/Viewport/ViewportBounds.cs create mode 100644 NResUI/Rendering/Viewport/ViewportDebugSettings.cs diff --git a/NResUI/ImGuiUI/ViewportPanel.cs b/NResUI/ImGuiUI/ViewportPanel.cs index 64e1795..405d1fb 100644 --- a/NResUI/ImGuiUI/ViewportPanel.cs +++ b/NResUI/ImGuiUI/ViewportPanel.cs @@ -33,7 +33,8 @@ public sealed class ViewportPanel : IImGuiPanel } DrawSelectionStatus(); - DrawGridToggle(); + DrawViewportToolbar(); + DrawDebugControls(); var imageSize = ImGui.GetContentRegionAvail(); if (imageSize.X < 32 || imageSize.Y < 32) @@ -56,6 +57,7 @@ public sealed class ViewportPanel : IImGuiPanel new Vector2(1, 0)); var viewportHovered = ImGui.IsItemHovered(); + var viewportFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); var imageMin = ImGui.GetItemRectMin(); DrawViewportHelpOverlay(); @@ -64,6 +66,7 @@ public sealed class ViewportPanel : IImGuiPanel _scene, _camera, viewportHovered, + viewportFocused, imageMin, imageSize); @@ -79,15 +82,69 @@ public sealed class ViewportPanel : IImGuiPanel ImGui.TextDisabled("Selected: none"); } - private void DrawGridToggle() + private void DrawViewportToolbar() + { + if (ImGui.Button("Reset view")) + _camera.Reset(); + + ImGui.SameLine(); + + if (ImGui.Button("Frame selected")) + { + if (_scene.TryGetSelectedPieceWorldBounds(out var selectedBounds)) + _camera.FrameBounds(selectedBounds.Min, selectedBounds.Max); + } + + ImGui.SameLine(); + + if (ImGui.Button("Frame all")) + { + if (_scene.TryGetSceneWorldBounds(out var sceneBounds)) + _camera.FrameBounds(sceneBounds.Min, sceneBounds.Max); + else + _camera.Reset(); + } + } + + private void DrawDebugControls() { var grid = _scene.Grid; - if (grid == null) - return; + if (grid != null) + { + var isVisible = grid.IsVisible; + if (ImGui.Checkbox("Grid", ref isVisible)) + grid.IsVisible = isVisible; + } - var isVisible = grid.IsVisible; - if (ImGui.Checkbox("Show grid", ref isVisible)) - grid.IsVisible = isVisible; + ImGui.SameLine(); + + var showOriginAxes = _scene.Debug.ShowOriginAxes; + if (ImGui.Checkbox("Origin axes", ref showOriginAxes)) + _scene.Debug.ShowOriginAxes = showOriginAxes; + + ImGui.SameLine(); + + var showPieceOrigins = _scene.Debug.ShowPieceOrigins; + if (ImGui.Checkbox("Piece axes", ref showPieceOrigins)) + _scene.Debug.ShowPieceOrigins = showPieceOrigins; + + ImGui.SameLine(); + + var showSelectedBounds = _scene.Debug.ShowSelectedBounds; + if (ImGui.Checkbox("Selected bounds", ref showSelectedBounds)) + _scene.Debug.ShowSelectedBounds = showSelectedBounds; + + ImGui.SameLine(); + + var showSceneBounds = _scene.Debug.ShowSceneBounds; + if (ImGui.Checkbox("Scene bounds", ref showSceneBounds)) + _scene.Debug.ShowSceneBounds = showSceneBounds; + + ImGui.SameLine(); + + var wireframe = _scene.Debug.Wireframe; + if (ImGui.Checkbox("Wireframe", ref wireframe)) + _scene.Debug.Wireframe = wireframe; } private static void DrawViewportHelpOverlay() @@ -97,7 +154,12 @@ public sealed class ViewportPanel : IImGuiPanel var imageMin = ImGui.GetItemRectMin(); var imageMax = ImGui.GetItemRectMax(); - const string helpText = "LMB click: select / deselect\nMMB drag: rotate\nMouse wheel: zoom"; + const string helpText = + "LMB click: select / deselect\n" + + "MMB drag: rotate\n" + + "RMB drag or Alt+MMB: pan\n" + + "Mouse wheel: zoom\n" + + "F: frame selected Home: frame all"; var textSize = ImGui.CalcTextSize(helpText); var padding = new Vector2(8.0f, 6.0f); diff --git a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs index edd01ea..be68e25 100644 --- a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs +++ b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs @@ -28,7 +28,7 @@ public sealed class GpuMesh PrimitiveType = primitiveType; } - public void Draw() + public unsafe void Draw() { _gl.BindVertexArray(VertexArrayObject); _gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null); diff --git a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs index 10f0b20..91ee0e7 100644 --- a/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs +++ b/NResUI/Rendering/Viewport/Meshes/PrimitiveMeshes.cs @@ -132,6 +132,59 @@ public static unsafe class PrimitiveMeshes return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), PrimitiveType.Lines); } + + + public static GpuMesh CreateUnitWireBox(GL gl) + { + float[] vertices = + { + -1, -1, -1, 1.0f, 0.82f, 0.15f, + 1, -1, -1, 1.0f, 0.82f, 0.15f, + 1, 1, -1, 1.0f, 0.82f, 0.15f, + -1, 1, -1, 1.0f, 0.82f, 0.15f, + -1, -1, 1, 1.0f, 0.82f, 0.15f, + 1, -1, 1, 1.0f, 0.82f, 0.15f, + 1, 1, 1, 1.0f, 0.82f, 0.15f, + -1, 1, 1, 1.0f, 0.82f, 0.15f, + }; + + uint[] indices = + { + 0, 1, 1, 2, 2, 3, 3, 0, + 4, 5, 5, 6, 6, 7, 7, 4, + 0, 4, 1, 5, 2, 6, 3, 7, + }; + + return CreateIndexedMesh(gl, vertices, indices, PrimitiveType.Lines); + } + + public static GpuMesh CreateAxes(GL gl, float length = 1.0f) + { + if (length <= 0.0f) + throw new ArgumentOutOfRangeException(nameof(length)); + + float[] vertices = + { + 0, 0, 0, 1.0f, 0.25f, 0.25f, + length, 0, 0, 1.0f, 0.25f, 0.25f, + + 0, 0, 0, 0.25f, 1.0f, 0.25f, + 0, length, 0, 0.25f, 1.0f, 0.25f, + + 0, 0, 0, 0.25f, 0.45f, 1.0f, + 0, 0, length, 0.25f, 0.45f, 1.0f, + }; + + uint[] indices = + { + 0, 1, + 2, 3, + 4, 5, + }; + + return CreateIndexedMesh(gl, vertices, indices, PrimitiveType.Lines); + } + private static GpuMesh CreateIndexedMesh( GL gl, float[] vertices, diff --git a/NResUI/Rendering/Viewport/OpenGL/MsaaFramebuffer.cs b/NResUI/Rendering/Viewport/OpenGL/MsaaFramebuffer.cs index eb96adb..4d74605 100644 --- a/NResUI/Rendering/Viewport/OpenGL/MsaaFramebuffer.cs +++ b/NResUI/Rendering/Viewport/OpenGL/MsaaFramebuffer.cs @@ -25,7 +25,7 @@ public sealed class MsaaFramebuffer _samples = samples; } - public void EnsureSize(int width, int height) + public unsafe void EnsureSize(int width, int height) { width = Math.Max(1, width); height = Math.Max(1, height); diff --git a/NResUI/Rendering/Viewport/ViewportBounds.cs b/NResUI/Rendering/Viewport/ViewportBounds.cs new file mode 100644 index 0000000..07ab24a --- /dev/null +++ b/NResUI/Rendering/Viewport/ViewportBounds.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace NResUI.Rendering.Viewport; + +public readonly struct ViewportBounds +{ + public Vector3 Min { get; } + public Vector3 Max { get; } + + public ViewportBounds(Vector3 min, Vector3 max) + { + Min = min; + Max = max; + } + + public Vector3 Center => (Min + Max) * 0.5f; + public Vector3 Size => Max - Min; +} diff --git a/NResUI/Rendering/Viewport/ViewportCamera.cs b/NResUI/Rendering/Viewport/ViewportCamera.cs index f9c3de1..0376230 100644 --- a/NResUI/Rendering/Viewport/ViewportCamera.cs +++ b/NResUI/Rendering/Viewport/ViewportCamera.cs @@ -4,31 +4,104 @@ namespace NResUI.Rendering.Viewport; public sealed class ViewportCamera { + public const float FieldOfViewDegrees = 60.0f; + public float YawDegrees { get; set; } = 35.0f; public float PitchDegrees { get; set; } = 25.0f; public float Distance { get; set; } = 4.0f; + public Vector3 Target { get; set; } = Vector3.Zero; public Matrix4x4 GetSceneRotationMatrix() { - return Matrix4x4.CreateRotationY(ToRadians(YawDegrees)) * - Matrix4x4.CreateRotationX(ToRadians(PitchDegrees)); + // Kept as an explicit hook for future scene-wide transforms. + // Camera orbit is handled by GetViewMatrix(). + return Matrix4x4.Identity; } public Matrix4x4 GetViewMatrix() { - return Matrix4x4.CreateLookAt( - new Vector3(0.0f, 0.0f, Distance), - Vector3.Zero, - Vector3.UnitY); + return Matrix4x4.CreateLookAt(GetEyePosition(), Target, Vector3.UnitY); } public Matrix4x4 GetProjectionMatrix(float aspectRatio) { return Matrix4x4.CreatePerspectiveFieldOfView( - ToRadians(60.0f), + ToRadians(FieldOfViewDegrees), aspectRatio, 0.01f, - 100.0f); + 1000.0f); + } + + public Vector3 GetEyePosition() + { + return Target + GetOrbitDirection() * Distance; + } + + public Vector3 GetForwardDirection() + { + return Vector3.Normalize(Target - GetEyePosition()); + } + + public Vector3 GetRightDirection() + { + var forward = GetForwardDirection(); + var right = Vector3.Cross(forward, Vector3.UnitY); + + if (right.LengthSquared() < 1e-8f) + return Vector3.UnitX; + + return Vector3.Normalize(right); + } + + public Vector3 GetUpDirection() + { + var right = GetRightDirection(); + var forward = GetForwardDirection(); + return Vector3.Normalize(Vector3.Cross(right, forward)); + } + + public void Pan(Vector2 mouseDelta, Vector2 viewportSize) + { + if (viewportSize.Y <= 1.0f) + return; + + var worldUnitsPerPixel = 2.0f * Distance * MathF.Tan(ToRadians(FieldOfViewDegrees) * 0.5f) / viewportSize.Y; + var right = GetRightDirection(); + var up = GetUpDirection(); + + Target -= right * (mouseDelta.X * worldUnitsPerPixel); + Target += up * (mouseDelta.Y * worldUnitsPerPixel); + } + + public void Reset() + { + YawDegrees = 35.0f; + PitchDegrees = 25.0f; + Distance = 4.0f; + Target = Vector3.Zero; + } + + public void FrameBounds(Vector3 boundsMin, Vector3 boundsMax) + { + var center = (boundsMin + boundsMax) * 0.5f; + var extents = (boundsMax - boundsMin) * 0.5f; + var radius = MathF.Max(extents.Length(), 0.25f); + + Target = center; + Distance = Math.Clamp(radius * 2.8f, 1.5f, 200.0f); + } + + private Vector3 GetOrbitDirection() + { + var yaw = ToRadians(YawDegrees); + var pitch = ToRadians(PitchDegrees); + + var cosPitch = MathF.Cos(pitch); + + return Vector3.Normalize(new Vector3( + MathF.Sin(yaw) * cosPitch, + MathF.Sin(pitch), + MathF.Cos(yaw) * cosPitch)); } public static float ToRadians(float degrees) diff --git a/NResUI/Rendering/Viewport/ViewportDebugSettings.cs b/NResUI/Rendering/Viewport/ViewportDebugSettings.cs new file mode 100644 index 0000000..e435ccc --- /dev/null +++ b/NResUI/Rendering/Viewport/ViewportDebugSettings.cs @@ -0,0 +1,10 @@ +namespace NResUI.Rendering.Viewport; + +public sealed class ViewportDebugSettings +{ + public bool ShowOriginAxes { get; set; } = true; + public bool ShowPieceOrigins { get; set; } = true; + public bool ShowSelectedBounds { get; set; } = true; + public bool ShowSceneBounds { get; set; } + public bool Wireframe { get; set; } +} diff --git a/NResUI/Rendering/Viewport/ViewportInputController.cs b/NResUI/Rendering/Viewport/ViewportInputController.cs index 8e957a6..0e1ab70 100644 --- a/NResUI/Rendering/Viewport/ViewportInputController.cs +++ b/NResUI/Rendering/Viewport/ViewportInputController.cs @@ -9,33 +9,49 @@ public sealed class ViewportInputController private const float MouseZoomSpeed = 0.35f; private bool _isRotatingViewport; + private bool _isPanningViewport; public void Handle( ViewportScene scene, ViewportCamera camera, bool viewportHovered, + bool viewportFocused, Vector2 imageMin, Vector2 imageSize) { var io = ImGui.GetIO(); - if (viewportHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + var wantsPanWithMiddle = io.KeyAlt && ImGui.IsMouseClicked(ImGuiMouseButton.Middle); + var wantsPanWithRight = ImGui.IsMouseClicked(ImGuiMouseButton.Right); + + if (viewportHovered && wantsPanWithMiddle) + _isPanningViewport = true; + else if (viewportHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) _isRotatingViewport = true; + if (viewportHovered && wantsPanWithRight) + _isPanningViewport = true; + if (!ImGui.IsMouseDown(ImGuiMouseButton.Middle)) _isRotatingViewport = false; + if (!ImGui.IsMouseDown(ImGuiMouseButton.Right) && !(io.KeyAlt && ImGui.IsMouseDown(ImGuiMouseButton.Middle))) + _isPanningViewport = false; + if (_isRotatingViewport) { - camera.YawDegrees += io.MouseDelta.X * MouseRotationSpeed; + camera.YawDegrees += -io.MouseDelta.X * MouseRotationSpeed; camera.PitchDegrees += io.MouseDelta.Y * MouseRotationSpeed; camera.PitchDegrees = Math.Clamp(camera.PitchDegrees, -89.0f, 89.0f); } + if (_isPanningViewport) + camera.Pan(io.MouseDelta, imageSize); + if (viewportHovered && Math.Abs(io.MouseWheel) > float.Epsilon) { - camera.Distance -= io.MouseWheel * MouseZoomSpeed; - camera.Distance = Math.Clamp(camera.Distance, 1.5f, 40.0f); + camera.Distance -= io.MouseWheel * MouseZoomSpeed * MathF.Max(1.0f, camera.Distance * 0.15f); + camera.Distance = Math.Clamp(camera.Distance, 0.25f, 400.0f); } if (viewportHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) @@ -49,5 +65,27 @@ public sealed class ViewportInputController localMouse, imageSize); } + + if (viewportFocused || viewportHovered) + HandleKeyboardShortcuts(scene, camera); + } + + private static void HandleKeyboardShortcuts(ViewportScene scene, ViewportCamera camera) + { + if (ImGui.IsKeyPressed(ImGuiKey.F)) + { + if (scene.TryGetSelectedPieceWorldBounds(out var selectedBounds)) + camera.FrameBounds(selectedBounds.Min, selectedBounds.Max); + else if (scene.TryGetSceneWorldBounds(out var sceneBounds)) + camera.FrameBounds(sceneBounds.Min, sceneBounds.Max); + } + + if (ImGui.IsKeyPressed(ImGuiKey.Home)) + { + if (scene.TryGetSceneWorldBounds(out var sceneBounds)) + camera.FrameBounds(sceneBounds.Min, sceneBounds.Max); + else + camera.Reset(); + } } } diff --git a/NResUI/Rendering/Viewport/ViewportRenderer.cs b/NResUI/Rendering/Viewport/ViewportRenderer.cs index b7a2f12..cb10bdc 100644 --- a/NResUI/Rendering/Viewport/ViewportRenderer.cs +++ b/NResUI/Rendering/Viewport/ViewportRenderer.cs @@ -1,4 +1,5 @@ using System.Numerics; +using NResUI.Rendering.Viewport.Meshes; using NResUI.Rendering.Viewport.OpenGL; using Silk.NET.OpenGL; using Silk.NET.Windowing; @@ -14,6 +15,9 @@ public sealed class ViewportRenderer private ShaderProgram? _meshShader; private ShaderProgram? _outlineShader; + private GpuMesh? _unitWireBoxMesh; + private GpuMesh? _axesMesh; + private int _meshMvpLocation; private int _outlineMvpLocation; private int _outlineColorLocation; @@ -55,7 +59,9 @@ public sealed class ViewportRenderer DrawGrid(scene, sceneRotation, view, projection); DrawScene(scene, sceneRotation, view, projection); + DrawDebugOverlays(scene, sceneRotation, view, projection); + _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _framebuffer.Resolve(); _gl.Disable(EnableCap.CullFace); @@ -71,7 +77,6 @@ public sealed class ViewportRenderer return _framebuffer.ColorTexture; } - private void DrawGrid( ViewportScene scene, Matrix4x4 sceneRotation, @@ -90,6 +95,7 @@ public sealed class ViewportRenderer _gl.Disable(EnableCap.StencilTest); _gl.Disable(EnableCap.CullFace); + _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _meshShader.Use(); _meshShader.SetMatrix4(_meshMvpLocation, mvp); @@ -109,6 +115,9 @@ public sealed class ViewportRenderer _gl.Disable(EnableCap.StencilTest); _meshShader.Use(); + _gl.PolygonMode( + TriangleFace.FrontAndBack, + scene.Debug.Wireframe ? PolygonMode.Line : PolygonMode.Fill); foreach (var piece in scene.Pieces) { @@ -120,7 +129,10 @@ public sealed class ViewportRenderer var selectedPiece = scene.SelectedPiece; if (selectedPiece == null) + { + _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); return; + } _gl.Enable(EnableCap.StencilTest); _gl.StencilMask(0xFF); @@ -130,6 +142,7 @@ public sealed class ViewportRenderer _meshShader.Use(); DrawPiece(selectedPiece, sceneRotation, view, projection); + _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _gl.StencilFunc(StencilFunction.Notequal, 1, 0xFF); _gl.StencilMask(0x00); _gl.Disable(EnableCap.DepthTest); @@ -141,21 +154,64 @@ public sealed class ViewportRenderer _gl.Disable(EnableCap.StencilTest); } + private void DrawDebugOverlays( + ViewportScene scene, + Matrix4x4 sceneRotation, + Matrix4x4 view, + Matrix4x4 projection) + { + if (_meshShader == null || _unitWireBoxMesh == null || _axesMesh == null) + throw new InvalidOperationException("Viewport debug resources are not initialized."); + + _gl.Disable(EnableCap.StencilTest); + _gl.Disable(EnableCap.CullFace); + _gl.Disable(EnableCap.DepthTest); + _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); + _gl.LineWidth(2.0f); + + _meshShader.Use(); + + if (scene.Debug.ShowOriginAxes) + DrawMesh(_axesMesh, Matrix4x4.CreateScale(1.25f) * sceneRotation, view, projection); + + if (scene.Debug.ShowPieceOrigins) + { + foreach (var piece in scene.Pieces) + { + var axesModel = Matrix4x4.CreateScale(0.45f) * piece.LocalTransform * sceneRotation; + DrawMesh(_axesMesh, axesModel, view, projection); + } + } + + if (scene.Debug.ShowSelectedBounds && scene.SelectedPiece != null) + { + var selectedPiece = scene.SelectedPiece; + var boundsModel = BuildLocalBoundsModel(selectedPiece.BoundsMin, selectedPiece.BoundsMax) * + selectedPiece.LocalTransform * + sceneRotation; + + DrawMesh(_unitWireBoxMesh, boundsModel, view, projection); + } + + if (scene.Debug.ShowSceneBounds && scene.TryGetSceneWorldBounds(out var sceneBounds)) + { + var sceneBoundsModel = BuildLocalBoundsModel(sceneBounds.Min, sceneBounds.Max) * sceneRotation; + DrawMesh(_unitWireBoxMesh, sceneBoundsModel, view, projection); + } + + _gl.LineWidth(1.0f); + _gl.Enable(EnableCap.DepthTest); + _gl.Enable(EnableCap.CullFace); + } + private void DrawPiece( ViewportPiece piece, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { - if (_meshShader == null) - throw new InvalidOperationException("Viewport mesh shader is not initialized."); - var model = piece.LocalTransform * sceneRotation; - var mvp = model * view * projection; - - _meshShader.Use(); - _meshShader.SetMatrix4(_meshMvpLocation, mvp); - piece.Mesh.Draw(); + DrawMesh(piece.Mesh, model, view, projection); } private void DrawPieceOutline( @@ -179,12 +235,38 @@ public sealed class ViewportRenderer piece.Mesh.Draw(); } + private void DrawMesh( + GpuMesh mesh, + Matrix4x4 model, + Matrix4x4 view, + Matrix4x4 projection) + { + if (_meshShader == null) + throw new InvalidOperationException("Viewport mesh shader is not initialized."); + + var mvp = model * view * projection; + + _meshShader.Use(); + _meshShader.SetMatrix4(_meshMvpLocation, mvp); + mesh.Draw(); + } + + private static Matrix4x4 BuildLocalBoundsModel(Vector3 boundsMin, Vector3 boundsMax) + { + var center = (boundsMin + boundsMax) * 0.5f; + var size = boundsMax - boundsMin; + + return Matrix4x4.CreateScale(size * 0.5f) * Matrix4x4.CreateTranslation(center); + } + private void EnsureInitialized() { if (_initialized) return; CreateShaders(); + _unitWireBoxMesh = PrimitiveMeshes.CreateUnitWireBox(_gl); + _axesMesh = PrimitiveMeshes.CreateAxes(_gl); _initialized = true; } diff --git a/NResUI/Rendering/Viewport/ViewportScene.cs b/NResUI/Rendering/Viewport/ViewportScene.cs index 8101c6f..8fe8b57 100644 --- a/NResUI/Rendering/Viewport/ViewportScene.cs +++ b/NResUI/Rendering/Viewport/ViewportScene.cs @@ -1,3 +1,4 @@ +using System.Numerics; using NResUI.Rendering.Viewport.Meshes; namespace NResUI.Rendering.Viewport; @@ -10,6 +11,8 @@ public sealed class ViewportScene public ViewportGrid? Grid { get; set; } + public ViewportDebugSettings Debug { get; } = new(); + public int SelectedPieceId { get; set; } = -1; public ViewportPiece? SelectedPiece @@ -39,6 +42,43 @@ public sealed class ViewportScene SelectedPieceId = -1; } + public bool TryGetPieceWorldBounds(ViewportPiece piece, out ViewportBounds bounds) + { + return TryTransformBounds(piece.BoundsMin, piece.BoundsMax, piece.LocalTransform, out bounds); + } + + public bool TryGetSelectedPieceWorldBounds(out ViewportBounds bounds) + { + var selectedPiece = SelectedPiece; + if (selectedPiece == null) + { + bounds = default; + return false; + } + + return TryGetPieceWorldBounds(selectedPiece, out bounds); + } + + public bool TryGetSceneWorldBounds(out ViewportBounds bounds) + { + var hasBounds = false; + var min = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + + foreach (var piece in _pieces) + { + if (!TryGetPieceWorldBounds(piece, out var pieceBounds)) + continue; + + min = Vector3.Min(min, pieceBounds.Min); + max = Vector3.Max(max, pieceBounds.Max); + hasBounds = true; + } + + bounds = hasBounds ? new ViewportBounds(min, max) : default; + return hasBounds; + } + public static ViewportScene CreateDefaultCubeScene(GpuMesh cubeMesh, GpuMesh gridMesh) { var scene = new ViewportScene @@ -50,4 +90,36 @@ public sealed class ViewportScene return scene; } + + private static bool TryTransformBounds( + Vector3 localMin, + Vector3 localMax, + Matrix4x4 transform, + out ViewportBounds bounds) + { + var corners = new[] + { + new Vector3(localMin.X, localMin.Y, localMin.Z), + new Vector3(localMax.X, localMin.Y, localMin.Z), + new Vector3(localMin.X, localMax.Y, localMin.Z), + new Vector3(localMax.X, localMax.Y, localMin.Z), + new Vector3(localMin.X, localMin.Y, localMax.Z), + new Vector3(localMax.X, localMin.Y, localMax.Z), + new Vector3(localMin.X, localMax.Y, localMax.Z), + new Vector3(localMax.X, localMax.Y, localMax.Z), + }; + + var min = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + + foreach (var corner in corners) + { + var world = Vector3.Transform(corner, transform); + min = Vector3.Min(min, world); + max = Vector3.Max(max, world); + } + + bounds = new ViewportBounds(min, max); + return true; + } } diff --git a/NResUI/Rendering/Viewport/ViewportSelection.cs b/NResUI/Rendering/Viewport/ViewportSelection.cs index c4bfaad..cef6c43 100644 --- a/NResUI/Rendering/Viewport/ViewportSelection.cs +++ b/NResUI/Rendering/Viewport/ViewportSelection.cs @@ -14,28 +14,24 @@ public static class ViewportSelection return -1; var aspect = viewportSize.X / viewportSize.Y; - var fovRadians = ViewportCamera.ToRadians(60.0f); + var fovRadians = ViewportCamera.ToRadians(ViewportCamera.FieldOfViewDegrees); var tanHalfFov = MathF.Tan(fovRadians * 0.5f); var ndcX = (2.0f * localMouse.X / viewportSize.X) - 1.0f; var ndcY = 1.0f - (2.0f * localMouse.Y / viewportSize.Y); - var rayOriginWorld = new Vector3(0.0f, 0.0f, camera.Distance); - var rayDirectionWorld = Vector3.Normalize(new Vector3( - ndcX * aspect * tanHalfFov, - ndcY * tanHalfFov, - -1.0f)); - - var sceneRotation = camera.GetSceneRotationMatrix(); + var rayOriginWorld = camera.GetEyePosition(); + var rayDirectionWorld = Vector3.Normalize( + camera.GetForwardDirection() + + camera.GetRightDirection() * (ndcX * aspect * tanHalfFov) + + camera.GetUpDirection() * (ndcY * tanHalfFov)); var bestPieceId = -1; var bestDistance = float.PositiveInfinity; foreach (var piece in scene.Pieces) { - var model = piece.LocalTransform * sceneRotation; - - if (!Matrix4x4.Invert(model, out var inverseModel)) + if (!Matrix4x4.Invert(piece.LocalTransform, out var inverseModel)) continue; var rayOriginLocal = Vector3.Transform(rayOriginWorld, inverseModel);