From 34c2a04649d9d84ff1d15a7006f46379efc851f0 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 08:36:47 +0200 Subject: [PATCH 1/6] First itteration --- GameWorld/ContentProject/Content/Content.mgcb | 12 + .../Content/Shaders/EdgeQuadShader.fx | 113 +++++ .../Content/Shaders/VertexPointShader.fx | 106 +++++ .../Commands/Edge/EdgeSelectionCommand.cs | 47 +++ .../Rendering/CommonShaderParameterBuilder.cs | 6 +- .../Rendering/RenderEngineComponent.cs | 2 +- .../Selection/EdgeSelectionState.cs | 70 ++++ .../Components/Selection/ISelectionState.cs | 1 + .../Selection/SelectionComponent.cs | 44 +- .../Components/Selection/SelectionManager.cs | 171 +++++++- .../Selection/VertexSelectionState.cs | 65 +-- .../Rendering/CommonShaderParameters.cs | 4 +- .../Rendering/EdgeQuadInstanceMesh.cs | 129 ++++++ .../RenderItems/EdgeQuadRenderItem.cs | 37 ++ .../Rendering/RenderItems/VertexRenderItem.cs | 15 +- .../Rendering/VertexInstanceMesh.cs | 174 +++----- .../GameWorld.Core/Services/ResourceLibary.cs | 6 +- .../Utility/IntersectionMath.cs | 249 +++++++++-- .../Selection/VertexRenderingOverhaulTests.cs | 390 ++++++++++++++++++ 19 files changed, 1437 insertions(+), 204 deletions(-) create mode 100644 GameWorld/ContentProject/Content/Shaders/EdgeQuadShader.fx create mode 100644 GameWorld/ContentProject/Content/Shaders/VertexPointShader.fx create mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Rendering/EdgeQuadInstanceMesh.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/EdgeQuadRenderItem.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs diff --git a/GameWorld/ContentProject/Content/Content.mgcb b/GameWorld/ContentProject/Content/Content.mgcb index ccd09d02c..2e5ae4c18 100644 --- a/GameWorld/ContentProject/Content/Content.mgcb +++ b/GameWorld/ContentProject/Content/Content.mgcb @@ -38,12 +38,24 @@ /processorParam:DebugMode=Auto /build:Shaders/GridShader.fx +#begin Shaders/EdgeQuadShader.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:Shaders/EdgeQuadShader.fx + #begin Shaders/InstancingShader.fx /importer:EffectImporter /processor:EffectProcessor /processorParam:DebugMode=Auto /build:Shaders/InstancingShader.fx +#begin Shaders/VertexPointShader.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:Shaders/VertexPointShader.fx + #begin Shaders/LineShader.fx /importer:EffectImporter /processor:EffectProcessor diff --git a/GameWorld/ContentProject/Content/Shaders/EdgeQuadShader.fx b/GameWorld/ContentProject/Content/Shaders/EdgeQuadShader.fx new file mode 100644 index 000000000..ec0cbf8d4 --- /dev/null +++ b/GameWorld/ContentProject/Content/Shaders/EdgeQuadShader.fx @@ -0,0 +1,113 @@ +float4x4 View; +float4x4 Projection; +float ViewportWidth; +float ViewportHeight; + +struct VSInput +{ + float3 Position : POSITION0; + float3 P0 : POSITION1; + float3 P1 : POSITION2; + float3 C0 : COLOR1; + float3 C1 : COLOR2; + float Width : BLENDWEIGHT0; +}; + +struct VSOutput +{ + float4 Position : SV_POSITION; + float3 Color : COLOR0; + float Edge : TEXCOORD0; +}; + +float2 WorldToScreen(float3 worldPos) +{ + float4 clip = mul(mul(float4(worldPos, 1), View), Projection); + float2 ndc = clip.xy / clip.w; + return float2((ndc.x * 0.5 + 0.5) * ViewportWidth, + (0.5 - ndc.y * 0.5) * ViewportHeight); +} + +float WorldToClipW(float3 worldPos) +{ + float4 clip = mul(mul(float4(worldPos, 1), View), Projection); + return clip.w; +} + +float4 ScreenToClip(float2 screen, float w) +{ + float2 ndc = float2(screen.x / ViewportWidth * 2 - 1, + 1 - screen.y / ViewportHeight * 2); + return float4(ndc * w, 0, w); +} + +VSOutput EdgeQuadVS(VSInput input) +{ + VSOutput output; + + float2 s0 = WorldToScreen(input.P0); + float2 s1 = WorldToScreen(input.P1); + + float2 dir = s1 - s0; + float len = length(dir); + + if (len < 0.001) + { + dir = float2(1, 0); + len = 0.001; + } + else + { + dir /= len; + } + + float2 perp = float2(-dir.y, dir.x); + + float baseWidth = 1.2; + float halfW = baseWidth * 0.5 + 0.5; + + float t = input.Position.x; + float side = input.Position.y; + + float2 screenPos = lerp(s0, s1, t) + perp * side * halfW; + + float w0 = WorldToClipW(input.P0); + float w1 = WorldToClipW(input.P1); + float w = lerp(w0, w1, t); + + float4 clipPos = ScreenToClip(screenPos, w); + + float bias = -0.00005; + float4 clipP0 = mul(mul(float4(input.P0, 1), View), Projection); + float4 clipP1 = mul(mul(float4(input.P1, 1), View), Projection); + float z0 = clipP0.z / clipP0.w; + float z1 = clipP1.z / clipP1.w; + float z = lerp(z0, z1, t) + bias; + + clipPos.z = z * w; + + output.Position = clipPos; + output.Color = lerp(input.C0, input.C1, t); + output.Edge = side * 2.0; + + return output; +} + +float4 EdgeQuadPS(VSOutput input) : SV_Target +{ + float dist = abs(input.Edge); + float alpha = 1.0 - smoothstep(0.6, 1.0, dist); + if (alpha < 0.01) + discard; + + return float4(input.Color, alpha); +} + +technique EdgeQuad +{ + pass P0 + { + VertexShader = compile vs_4_0 EdgeQuadVS(); + PixelShader = compile ps_4_0 EdgeQuadPS(); + } +}; diff --git a/GameWorld/ContentProject/Content/Shaders/VertexPointShader.fx b/GameWorld/ContentProject/Content/Shaders/VertexPointShader.fx new file mode 100644 index 000000000..622f97ad7 --- /dev/null +++ b/GameWorld/ContentProject/Content/Shaders/VertexPointShader.fx @@ -0,0 +1,106 @@ +// Vertex point shader for rendering edit mode vertices as camera-facing circular points. +// Based on Blender's overlay_edit_mesh_vert.glsl approach. +// Renders instanced quads as billboarded circles with Z-bias for selected vertices. + +#if OPENGL +#define SV_POSITION POSITION +#define VS_SHADERMODEL vs_3_0 +#define PS_SHADERMODEL ps_3_0 +#else +#define VS_SHADERMODEL vs_4_0_level_9_1 +#define PS_SHADERMODEL ps_4_0_level_9_1 +#endif + +float4x4 View; +float4x4 ViewProjection; +float3 CameraPosition; + +// Instance data: position, scale, color, and selection weight +struct VSInstanceInput +{ + float3 InstancePosition : POSITION1; + float InstanceScale : NORMAL1; + float3 InstanceColor : NORMAL2; + float InstanceWeight : NORMAL3; +}; + +struct VSInput +{ + float4 Position : POSITION0; + float2 TexCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 Position : SV_POSITION; + float2 TexCoord : TEXCOORD0; + float4 Color : COLOR0; + float Weight : TEXCOORD1; +}; + +// Vertex shader: billboard quad with screen-space size +VSOutput VertexPointVS(VSInput input, VSInstanceInput instance) +{ + VSOutput output = (VSOutput)0; + + // Get camera right and up vectors from view matrix for billboard orientation + float3 cameraRight = float3(View[0][0], View[1][0], View[2][0]); + float3 cameraUp = float3(View[0][1], View[1][1], View[2][1]); + + // Offset billboard center slightly toward camera (in world space) + // This prevents the quad from extending behind the surface and being half-clipped + float3 toCamera = normalize(CameraPosition - instance.InstancePosition); + float3 adjustedPos = instance.InstancePosition + toCamera * 0.02 * instance.InstanceScale; + + // Billboard offset from center + float3 offset = (input.Position.xyz * instance.InstanceScale); + + // Apply billboard rotation (already in camera space) + float3 worldPos = adjustedPos + + cameraRight * offset.x + + cameraUp * offset.y; + + // Transform to clip space + output.Position = mul(float4(worldPos, 1.0), ViewProjection); + output.TexCoord = input.TexCoord; + output.Color = float4(instance.InstanceColor, 1.0); + output.Weight = instance.InstanceWeight; + + // Z-bias for ALL vertices to render on top of mesh surface + // Blender: gl_Position.z -= ndc_offset_factor * vert_ndc_offset (applied to all) + output.Position.z -= 1e-6 * abs(output.Position.w); + + // Extra Z-bias for selected vertices (Blender: 5e-7 * abs(w) for selected/active) + if (instance.InstanceWeight > 0.5) + output.Position.z -= 5e-7 * abs(output.Position.w); + + return output; +} + +// Pixel shader: circle clipping with anti-aliasing +// Blender 3D viewport style: solid circle with AA edge, no outline ring +float4 VertexPointPS(VSOutput input) : COLOR0 +{ + // Distance from center (0.5, 0.5) in UV space + float2 center = float2(0.5, 0.5); + float dist = length(input.TexCoord - center); + + // Discard pixels outside the circle + if (dist > 0.5) + discard; + + // Anti-aliased outer edge using smoothstep + float alpha = smoothstep(0.5, 0.42, dist); + + // Solid circle with AA edge (Blender 3D viewport style - no outline ring) + return float4(input.Color.rgb, alpha); +} + +technique VertexPoint +{ + pass Pass0 + { + VertexShader = compile VS_SHADERMODEL VertexPointVS(); + PixelShader = compile PS_SHADERMODEL VertexPointPS(); + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs b/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs new file mode 100644 index 000000000..3bb0b7c40 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using GameWorld.Core.Components.Selection; + +namespace GameWorld.Core.Commands.Edge +{ + public class EdgeSelectionCommand : ICommand + { + private readonly SelectionManager _selectionManager; + private List<(int v0, int v1)> _edges; + private bool _isAdd; + private bool _isRemove; + private ISelectionState _oldState; + + public string HintText => "Edge selection"; + public bool IsMutation => false; + + public EdgeSelectionCommand(SelectionManager selectionManager) + { + _selectionManager = selectionManager; + } + + public void Configure(List<(int v0, int v1)> edges, bool isSelectionModification, bool removeSelection) + { + _edges = edges; + _isAdd = isSelectionModification; + _isRemove = removeSelection; + } + + public void Execute() + { + _oldState = _selectionManager.GetStateCopy(); + var state = _selectionManager.GetState(); + if (state == null) + return; + + if (!_isAdd && !_isRemove) + state.Clear(); + + state.ModifySelection(_edges, _isRemove); + } + + public void Undo() + { + _selectionManager.SetState(_oldState); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/CommonShaderParameterBuilder.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/CommonShaderParameterBuilder.cs index 45f41b11c..27159a50b 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/CommonShaderParameterBuilder.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/CommonShaderParameterBuilder.cs @@ -5,7 +5,7 @@ namespace GameWorld.Core.Components.Rendering { internal static class CommonShaderParameterBuilder { - public static CommonShaderParameters Build(ArcBallCamera camera, SceneRenderParametersStore sceneLightParameters) + public static CommonShaderParameters Build(ArcBallCamera camera, SceneRenderParametersStore sceneLightParameters, float viewportWidth, float viewportHeight) { // Light follows camera rotation for better model visibility float dirLightRotX = MathHelper.ToRadians(sceneLightParameters.DirLightRotationDegrees_X) + camera.Pitch; @@ -24,7 +24,9 @@ public static CommonShaderParameters Build(ArcBallCamera camera, SceneRenderPara sceneLightParameters.LightIntensityMult, [sceneLightParameters.FactionColour0, sceneLightParameters.FactionColour1, sceneLightParameters.FactionColour2], - sceneLightParameters.LightColour + sceneLightParameters.LightColour, + viewportHeight, + viewportWidth ); return commonShaderParameters; diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/RenderEngineComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/RenderEngineComponent.cs index 08027f50d..5dc1729b1 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/RenderEngineComponent.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Rendering/RenderEngineComponent.cs @@ -150,7 +150,7 @@ public override void Draw(GameTime gameTime) return; } - var commonShaderParameters = CommonShaderParameterBuilder.Build(_camera, _sceneLightParameters); + var commonShaderParameters = CommonShaderParameterBuilder.Build(_camera, _sceneLightParameters, screenWidth, screenHeight); var backgroundColour = ApplicationSettingsHelper.GetEnumAsColour(_applicationSettingsService.CurrentSettings.RenderEngineBackgroundColour); _normalRenderTarget = RenderTargetHelper.GetRenderTarget(device, _normalRenderTarget, imageUpScale, _graphicsResourceCreator); diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs new file mode 100644 index 000000000..e09b052ac --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using GameWorld.Core.SceneNodes; + +namespace GameWorld.Core.Components.Selection +{ + public class EdgeSelectionState : ISelectionState + { + public event SelectionStateChanged SelectionChanged; + + public GeometrySelectionMode Mode => GeometrySelectionMode.Edge; + public ISelectable RenderObject { get; set; } + + private readonly HashSet<(int v0, int v1)> _selectedEdges = new(); + + public IReadOnlyCollection<(int v0, int v1)> SelectedEdges => _selectedEdges; + + public void ModifySelection(IEnumerable<(int v0, int v1)> edges, bool onlyRemove) + { + if (onlyRemove) + { + foreach (var edge in edges) + _selectedEdges.Remove(edge); + } + else + { + foreach (var edge in edges) + _selectedEdges.Add(edge); + } + + SelectionChanged?.Invoke(this, true); + } + + public List GetSelectedVertexIndices() + { + var set = new HashSet(); + foreach (var (v0, v1) in _selectedEdges) + { + set.Add(v0); + set.Add(v1); + } + return set.ToList(); + } + + public ISelectionState Clone() + { + var clone = new EdgeSelectionState { RenderObject = RenderObject }; + foreach (var edge in _selectedEdges) + clone._selectedEdges.Add(edge); + return clone; + } + + public void Clear() + { + _selectedEdges.Clear(); + SelectionChanged?.Invoke(this, true); + } + + public int SelectionCount() => _selectedEdges.Count; + + public ISelectable GetSingleSelectedObject() => RenderObject; + + public List SelectedObjects() + { + if (RenderObject != null) + return new List { RenderObject }; + return new List(); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs index 72023831d..39808ccd9 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs @@ -8,6 +8,7 @@ public enum GeometrySelectionMode Object, Face, Vertex, + Edge, Bone }; diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs index b5f41c68c..2e3f51001 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs @@ -1,6 +1,7 @@ using System.Windows.Forms; using GameWorld.Core.Commands; using GameWorld.Core.Commands.Bone; +using GameWorld.Core.Commands.Edge; using GameWorld.Core.Commands.Face; using GameWorld.Core.Commands.Object; using GameWorld.Core.Commands.Vertex; @@ -140,6 +141,14 @@ void SelectFromRectangle(Rectangle screenRect, bool isSelectionModification, boo return; } } + else if (currentState.Mode == GeometrySelectionMode.Edge && currentState is EdgeSelectionState edgeState) + { + if (edgeState.RenderObject != null && IntersectionMath.IntersectEdges(unprojectedSelectionRect, edgeState.RenderObject.Geometry, edgeState.RenderObject.RenderMatrix, out var edges)) + { + _commandFactory.Create().Configure(x => x.Configure(edges, isSelectionModification, removeSelection)).BuildAndExecute(); + return; + } + } else if (currentState.Mode == GeometrySelectionMode.Bone && currentState is BoneSelectionState boneState) { if (boneState.RenderObject == null) @@ -192,13 +201,25 @@ void SelectFromPoint(Vector2 mousePosition, bool isSelectionModification, bool r if (currentState is VertexSelectionState vertexState) { - if (IntersectionMath.IntersectVertex(ray, vertexState.RenderObject.Geometry, _camera.Position, vertexState.RenderObject.RenderMatrix, out var selecteVert) != null) + var viewProjection = _camera.ViewMatrix * _camera.ProjectionMatrix; + var viewport = _deviceResolverComponent.Device.Viewport; + if (IntersectionMath.IntersectVertex(mousePosition, vertexState.RenderObject.Geometry, vertexState.RenderObject.RenderMatrix, + viewProjection, viewport.Width, viewport.Height, out var selecteVert) != null) { _commandFactory.Create().Configure(x => x.Configure(new List() { selecteVert }, isSelectionModification, removeSelection)).BuildAndExecute(); return; } } + if (currentState is EdgeSelectionState edgeState && edgeState.RenderObject != null) + { + if (IntersectionMath.IntersectEdge(ray, edgeState.RenderObject.Geometry, _camera.Position, edgeState.RenderObject.RenderMatrix, out var selectedEdge) != null) + { + _commandFactory.Create().Configure(x => x.Configure(new List<(int, int)>() { selectedEdge }, isSelectionModification, removeSelection)).BuildAndExecute(); + return; + } + } + // Pick object var selectedObject = _sceneManger.SelectObject(ray); if (selectedObject == null && isSelectionModification == false) @@ -255,6 +276,21 @@ public bool SetVertexSelectionMode() return false; } + public bool SetEdgeSelectionMode() + { + var selectionState = _selectionManager.GetState(); + if (_selectionManager.GetState().Mode != GeometrySelectionMode.Edge) + { + var selectedObject = selectionState.GetSingleSelectedObject(); + if (selectedObject != null) + { + _commandFactory.Create().Configure(x => x.Configure(selectedObject, GeometrySelectionMode.Edge)).BuildAndExecute(); + return true; + } + } + return false; + } + public bool SetBoneSelectionMode() { var selectionState = _selectionManager.GetState(); @@ -291,6 +327,12 @@ bool ChangeSelectionMode() return true; } + else if (_keyboardComponent.IsKeyReleased(Keys.F4)) + { + if (SetEdgeSelectionMode()) + return true; + } + else if (_keyboardComponent.IsKeyReleased(Keys.F9)) { if (SetBoneSelectionMode()) diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs index a74394de8..18c2a8b50 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using GameWorld.Core.Components.Rendering; using GameWorld.Core.Rendering; using GameWorld.Core.Rendering.Materials.Shaders; @@ -7,6 +8,7 @@ using GameWorld.Core.Services; using GameWorld.Core.Utility; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using Shared.Core.Events; namespace GameWorld.Core.Components.Selection @@ -25,11 +27,25 @@ public class SelectionManager : BaseComponent, IDisposable BasicShader _selectedFacesEffect; VertexInstanceMesh _vertexRenderer; + EdgeQuadInstanceMesh _edgeQuadRenderer; + EdgeQuadRenderItem _edgeQuadRenderItem; + VertexRenderItem _vertexRenderItem; float _vertexSelectionFalloff = 0; private readonly IScopedResourceLibrary _resourceLib; private readonly IDeviceResolver _deviceResolverComponent; private readonly IGraphicsResourceCreator _graphicsResourceCreator; + private (int v0, int v1)[] _cachedEdgeIndices; + private Rmv2MeshNode _cachedEdgeMesh; + private bool _edgeDataDirty = true; + + private Vector3 _samplePos0, _samplePos1; + private int _sampleIdx0 = 0; + private int _sampleIdx1 = 1; + + const int MaxRenderEdges = 50000; + private EdgeData[] _edgeDataCache = new EdgeData[MaxRenderEdges]; + public SelectionManager(IEventHub eventHub, RenderEngineComponent renderEngine, IScopedResourceLibrary resourceLib, IDeviceResolver deviceResolverComponent, IGraphicsResourceCreator graphicsResourceCreator) { _eventHub = eventHub; @@ -44,9 +60,12 @@ public override void Initialize() CreateSelectionSate(GeometrySelectionMode.Object, null, false); _vertexRenderer = new VertexInstanceMesh(_deviceResolverComponent, _resourceLib, _graphicsResourceCreator); + _edgeQuadRenderer = new EdgeQuadInstanceMesh(_deviceResolverComponent, _resourceLib, _graphicsResourceCreator); + _edgeQuadRenderItem = new EdgeQuadRenderItem { EdgeQuadRenderer = _edgeQuadRenderer }; + _vertexRenderItem = new VertexRenderItem { VertexRenderer = _vertexRenderer }; _wireframeEffect = new BasicShader(_deviceResolverComponent.Device, _graphicsResourceCreator); - _wireframeEffect.DiffuseColour = Vector3.Zero; + _wireframeEffect.DiffuseColour = new Vector3(0.0f, 0.0f, 0.0f); _selectedFacesEffect = new BasicShader(_deviceResolverComponent.Device, _graphicsResourceCreator); _selectedFacesEffect.DiffuseColour = new Vector3(1, 0, 0); @@ -75,6 +94,10 @@ public ISelectionState CreateSelectionSate(GeometrySelectionMode mode, ISelectab _currentState = new FaceSelectionState(); break; + //case GeometrySelectionMode.Edge: + // _currentState = new EdgeSelectionState(); + // break; + // case GeometrySelectionMode.Vertex: _currentState = new VertexSelectionState(selectedObj, _vertexSelectionFalloff); break; @@ -98,7 +121,12 @@ public ISelectionState CreateSelectionSate(GeometrySelectionMode mode, ISelectab public void SetState(ISelectionState state) { - _currentState.SelectionChanged -= SelectionManager_SelectionChanged; + if (state == null) + return; + + if (_currentState != null) + _currentState.SelectionChanged -= SelectionManager_SelectionChanged; + _currentState = state; _currentState.SelectionChanged += SelectionManager_SelectionChanged; SelectionManager_SelectionChanged(_currentState, true); @@ -106,6 +134,7 @@ public void SetState(ISelectionState state) private void SelectionManager_SelectionChanged(ISelectionState state, bool sendEvent) { + _edgeDataDirty = true; _eventHub.Publish(new SelectionChangedEvent { NewState = state }); } @@ -131,9 +160,74 @@ public override void Draw(GameTime gameTime) if (selectionState is VertexSelectionState selectionVertexState && selectionVertexState.RenderObject != null) { var vertexObject = selectionVertexState.RenderObject as Rmv2MeshNode; - _renderEngine.AddRenderItem(RenderBuckedId.Normal, new VertexRenderItem() { Node = vertexObject, ModelMatrix = vertexObject.RenderMatrix, SelectedVertices = selectionVertexState, VertexRenderer = _vertexRenderer }); - _renderEngine.AddRenderItem(RenderBuckedId.Wireframe, new GeometryRenderItem(vertexObject.Geometry, _wireframeEffect, vertexObject.RenderMatrix)); + var geo = vertexObject.Geometry; + + if (_cachedEdgeMesh != vertexObject) + { + _cachedEdgeMesh = vertexObject; + _cachedEdgeIndices = BuildEdgeIndexCache(geo); + _edgeDataDirty = true; + } + + if (selectionVertexState.SelectedVertices.Count >= 2) + { + _sampleIdx0 = selectionVertexState.SelectedVertices[0]; + _sampleIdx1 = selectionVertexState.SelectedVertices[1]; + } + else if (selectionVertexState.SelectedVertices.Count == 1) + { + _sampleIdx0 = selectionVertexState.SelectedVertices[0]; + _sampleIdx1 = _sampleIdx0 < geo.VertexCount() - 1 ? _sampleIdx0 + 1 : 0; + } + + if (!_edgeDataDirty && geo.VertexCount() >= 2) + { + var p0 = geo.GetVertexById(_sampleIdx0); + var p1 = geo.GetVertexById(_sampleIdx1); + if (p0 != _samplePos0 || p1 != _samplePos1) + _edgeDataDirty = true; + } + + if (_edgeDataDirty) + { + UpdateEdgeQuadData(vertexObject, selectionVertexState); + _edgeDataDirty = false; + + if (geo.VertexCount() >= 2) + { + _samplePos0 = geo.GetVertexById(_sampleIdx0); + _samplePos1 = geo.GetVertexById(_sampleIdx1); + } + } + + _renderEngine.AddRenderItem(RenderBuckedId.Normal, _edgeQuadRenderItem); + _vertexRenderItem.Node = vertexObject; + _vertexRenderItem.ModelMatrix = vertexObject.RenderMatrix; + _vertexRenderItem.SelectedVertices = selectionVertexState; + _renderEngine.AddRenderItem(RenderBuckedId.Normal, _vertexRenderItem); + } + else + { + _cachedEdgeMesh = null; + _edgeDataDirty = true; } + // + //if (selectionState is EdgeSelectionState selectionEdgeState && selectionEdgeState.RenderObject is Rmv2MeshNode edgeNode) + //{ + // _renderEngine.AddRenderItem(RenderBuckedId.Wireframe, new GeometryRenderItem(edgeNode.Geometry, _wireframeEffect, edgeNode.RenderMatrix)); + // var geometry = edgeNode.Geometry; + // var matrix = edgeNode.RenderMatrix; + // foreach (var edge in selectionEdgeState.SelectedEdges) + // { + // var p0 = Vector3.Transform(geometry.GetVertexById(edge.v0), matrix); + // var p1 = Vector3.Transform(geometry.GetVertexById(edge.v1), matrix); + // _renderEngine.AddRenderLines(new VertexPositionColor[] + // { + // new VertexPositionColor(p0, Color.Orange), + // new VertexPositionColor(p1, Color.Orange) + // }); + // } + //} if (selectionState is BoneSelectionState selectionBoneState && selectionBoneState.RenderObject != null) { @@ -149,9 +243,6 @@ public override void Draw(GameTime gameTime) var parentWorld = Matrix.Identity; foreach (var boneIdx in bones) { - //var currentBoneMatrix = boneMatrix * Matrix.CreateScale(ScaleMult); - //var parentBoneMatrix = Skeleton.GetAnimatedWorldTranform(parentIndex) * Matrix.CreateScale(ScaleMult); - //_lineRenderer.AddLine(Vector3.Transform(currentBoneMatrix.Translation, parentWorld), Vector3.Transform(parentBoneMatrix.Translation, parentWorld)); var bone = currentFrame.GetSkeletonAnimatedWorld(skeleton, boneIdx); bone.Decompose(out var _, out var _, out var trans); _renderEngine.AddRenderLines(LineHelper.CreateCube(Matrix.CreateScale(0.06f) * bone * renderMatrix * parentWorld, Color.Red)); @@ -187,6 +278,12 @@ public void Dispose() _vertexRenderer = null; } + if (_edgeQuadRenderer != null) + { + _edgeQuadRenderer.Dispose(); + _edgeQuadRenderer = null; + } + _currentState?.Clear(); _currentState = null; } @@ -198,6 +295,66 @@ public void UpdateVertexSelectionFallof(float newValue) if (vertexSelectionState != null) vertexSelectionState.UpdateWeights(_vertexSelectionFalloff); } + + public float VertexSelectionFalloff => _vertexSelectionFalloff; + + private static (int v0, int v1)[] BuildEdgeIndexCache(GameWorld.Core.Rendering.Geometry.MeshObject geo) + { + var processedEdges = new HashSet<(int, int)>(); + var result = new List<(int, int)>(); + + for (var i = 0; i < geo.IndexArray.Length; i += 3) + { + var i0 = geo.IndexArray[i]; + var i1 = geo.IndexArray[i + 1]; + var i2 = geo.IndexArray[i + 2]; + + var edges = new[] { + (Math.Min(i0, i1), Math.Max(i0, i1)), + (Math.Min(i1, i2), Math.Max(i1, i2)), + (Math.Min(i0, i2), Math.Max(i0, i2)) + }; + + foreach (var edge in edges) + { + if (processedEdges.Add(edge)) + result.Add(edge); + } + } + + return result.ToArray(); + } + + private void UpdateEdgeQuadData(Rmv2MeshNode meshNode, VertexSelectionState selectionState) + { + var geo = meshNode.Geometry; + var matrix = meshNode.RenderMatrix; + var weights = selectionState.VertexWeights; + + var wireColor = new Vector3(0.15f, 0.15f, 0.15f); + var selectColor = new Vector3(1.0f, 0.47f, 0.0f); + + var edgeCount = Math.Min(_cachedEdgeIndices.Length, MaxRenderEdges); + for (var i = 0; i < edgeCount; i++) + { + var (v0, v1) = _cachedEdgeIndices[i]; + var w0 = weights[v0]; + var w1 = weights[v1]; + + _edgeDataCache[i] = new EdgeData + { + P0 = Vector3.Transform(geo.GetVertexById(v0), matrix), + P1 = Vector3.Transform(geo.GetVertexById(v1), matrix), + C0 = Vector3.Lerp(wireColor, selectColor, w0), + C1 = Vector3.Lerp(wireColor, selectColor, w1), + Width = 0 + }; + } + + _edgeQuadRenderItem.Edges = _edgeDataCache; + _edgeQuadRenderItem.EdgeCount = edgeCount; + _edgeQuadRenderItem.MarkDirty(); + } } } diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/VertexSelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/VertexSelectionState.cs index a2b0178a1..34330a1d6 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/VertexSelectionState.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/VertexSelectionState.cs @@ -49,50 +49,59 @@ public void ModifySelection(IEnumerable newSelectionItems, bool onlyRemove) public void UpdateWeights(float distanceOffset) { _selectionDistanceFallof = distanceOffset; - var vertexList = RenderObject.Geometry.GetVertexList(); - var vertListLength = vertexList.Count; + var geo = RenderObject.Geometry; + var vertexArray = geo.VertexArray; + var vertCount = vertexArray.Length; - // Clear all - for (var currentVertIndex = 0; currentVertIndex < vertexList.Count; currentVertIndex++) - VertexWeights[currentVertIndex] = 0; + var selectedSet = new HashSet(SelectedVertices); - // Compute new - if (SelectedVertices.Count == 0 || SelectedVertices.Count == vertexList.Count || distanceOffset == 0) + for (var i = 0; i < vertCount; i++) + VertexWeights[i] = 0; + + if (SelectedVertices.Count == 0 || SelectedVertices.Count == vertCount || distanceOffset == 0) { foreach (var vert in SelectedVertices) VertexWeights[vert] = 1.0f; + return; } - else + + var selectedPositions = new Vector3[SelectedVertices.Count]; + for (int i = 0; i < SelectedVertices.Count; i++) { - var vertsInUse = SelectedVertices.Select(x => vertexList[x]); - for (var currentVertIndex = 0; currentVertIndex < vertexList.Count; currentVertIndex++) + var pos = vertexArray[SelectedVertices[i]].Position; + selectedPositions[i] = new Vector3(pos.X, pos.Y, pos.Z); + } + + for (var i = 0; i < vertCount; i++) + { + if (selectedSet.Contains(i)) + { + VertexWeights[i] = 1.0f; + } + else { - var currentVertPos = vertexList[currentVertIndex]; - if (SelectedVertices.Contains(currentVertIndex)) - { - VertexWeights[currentVertIndex] = 1.0f; - } - else - { - var dist = GetClosestVertexDist(currentVertPos, vertsInUse); - if (dist <= distanceOffset) - VertexWeights[currentVertIndex] = 1 - dist / distanceOffset; - } + var pos = vertexArray[i].Position; + var currentPos = new Vector3(pos.X, pos.Y, pos.Z); + var dist = GetClosestVertexDist(currentPos, selectedPositions); + if (dist <= distanceOffset) + VertexWeights[i] = 1 - dist / distanceOffset; } } } - - float GetClosestVertexDist(Vector3 currentPos, IEnumerable vertList) + float GetClosestVertexDist(Vector3 currentPos, Vector3[] selectedPositions) { var closest = float.MaxValue; - foreach (var vert in vertList) + for (int i = 0; i < selectedPositions.Length; i++) { - var dist = Vector3.Distance(vert, currentPos); - if (dist < closest) - closest = dist; + var dx = currentPos.X - selectedPositions[i].X; + var dy = currentPos.Y - selectedPositions[i].Y; + var dz = currentPos.Z - selectedPositions[i].Z; + var distSq = dx * dx + dy * dy + dz * dz; + if (distSq < closest) + closest = distSq; } - return closest; + return MathF.Sqrt(closest); } public List CurrentSelection() diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/CommonShaderParameters.cs b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/CommonShaderParameters.cs index bad1d33b1..dfa8c2d30 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/CommonShaderParameters.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/CommonShaderParameters.cs @@ -12,7 +12,9 @@ public record CommonShaderParameters( float DirLightRotationRadians_Y, float LightIntensityMult, Vector3[] FactionColours, - Vector3 LightColour + Vector3 LightColour, + float ViewportHeight = 0, + float ViewportWidth = 0 ); } diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/EdgeQuadInstanceMesh.cs b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/EdgeQuadInstanceMesh.cs new file mode 100644 index 000000000..50d4600d6 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/EdgeQuadInstanceMesh.cs @@ -0,0 +1,129 @@ +using System; +using System.Runtime.InteropServices; +using GameWorld.Core.Services; +using GameWorld.Core.Utility; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace GameWorld.Core.Rendering +{ + [StructLayout(LayoutKind.Sequential)] + public struct EdgeQuadInstanceData : IVertexType + { + public Vector3 P0; + public Vector3 P1; + public Vector3 C0; + public Vector3 C1; + public float Width; + + public static readonly VertexDeclaration VertexDeclaration = new( + new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 1), + new VertexElement(12, VertexElementFormat.Vector3, VertexElementUsage.Position, 2), + new VertexElement(24, VertexElementFormat.Vector3, VertexElementUsage.Color, 1), + new VertexElement(36, VertexElementFormat.Vector3, VertexElementUsage.Color, 2), + new VertexElement(48, VertexElementFormat.Single, VertexElementUsage.BlendWeight, 0)); + + VertexDeclaration IVertexType.VertexDeclaration => VertexDeclaration; + } + + public struct EdgeData + { + public Vector3 P0; + public Vector3 P1; + public Vector3 C0; + public Vector3 C1; + public float Width; + } + + public class EdgeQuadInstanceMesh : IDisposable + { + private const int MaxInstances = 50000; + private readonly GraphicsDevice _device; + private readonly Effect _effect; + private VertexBuffer _quadVb; + private IndexBuffer _quadIb; + private DynamicVertexBuffer _instanceVb; + private readonly EdgeQuadInstanceData[] _instanceData = new EdgeQuadInstanceData[MaxInstances]; + private int _instanceCount; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; + + public EdgeQuadInstanceMesh(IDeviceResolver deviceResolver, IScopedResourceLibrary resourceLib, IGraphicsResourceCreator graphicsResourceCreator) + { + _device = deviceResolver.Device; + _graphicsResourceCreator = graphicsResourceCreator; + _effect = resourceLib.GetStaticEffect(ShaderTypes.EdgeQuad); + BuildQuadGeometry(); + } + + void BuildQuadGeometry() + { + var verts = new VertexPosition[] + { + new(new Vector3(0, -0.5f, 0)), + new(new Vector3(0, 0.5f, 0)), + new(new Vector3(1, 0.5f, 0)), + new(new Vector3(1, -0.5f, 0)), + }; + + _quadVb = _graphicsResourceCreator.CreateVertexBuffer(VertexPosition.VertexDeclaration, 4, BufferUsage.WriteOnly); + _quadVb.SetData(verts); + + var indices = new short[] { 0, 1, 2, 0, 2, 3 }; + _quadIb = _graphicsResourceCreator.CreateIndexBuffer(typeof(short), 6, BufferUsage.WriteOnly); + _quadIb.SetData(indices); + + _instanceVb = _graphicsResourceCreator.CreateDynamicVertexBuffer(EdgeQuadInstanceData.VertexDeclaration, MaxInstances, BufferUsage.WriteOnly); + } + + public void Update(EdgeData[] edges, int count, CommonShaderParameters shaderParams) + { + _instanceCount = Math.Min(count, MaxInstances); + for (var i = 0; i < _instanceCount; i++) + { + _instanceData[i] = new EdgeQuadInstanceData + { + P0 = edges[i].P0, + P1 = edges[i].P1, + C0 = edges[i].C0, + C1 = edges[i].C1, + Width = edges[i].Width + }; + } + + if (_instanceCount > 0) + _instanceVb.SetData(_instanceData, 0, _instanceCount); + } + + public void Draw(CommonShaderParameters shaderParams, GraphicsDevice device) + { + if (_instanceCount == 0) return; + + _effect.Parameters["View"]?.SetValue(shaderParams.View); + _effect.Parameters["Projection"]?.SetValue(shaderParams.Projection); + _effect.Parameters["ViewportWidth"]?.SetValue(shaderParams.ViewportWidth); + _effect.Parameters["ViewportHeight"]?.SetValue(shaderParams.ViewportHeight); + + device.BlendState = BlendState.AlphaBlend; + + _device.SetVertexBuffers( + new VertexBufferBinding(_quadVb, 0, 0), + new VertexBufferBinding(_instanceVb, 0, 1)); + _device.Indices = _quadIb; + + foreach (var pass in _effect.CurrentTechnique.Passes) + { + pass.Apply(); + _device.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 2, _instanceCount); + } + + device.BlendState = BlendState.Opaque; + } + + public void Dispose() + { + _quadVb = _graphicsResourceCreator.DisposeTracked(_quadVb); + _quadIb = _graphicsResourceCreator.DisposeTracked(_quadIb); + _instanceVb = _graphicsResourceCreator.DisposeTracked(_instanceVb); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/EdgeQuadRenderItem.cs b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/EdgeQuadRenderItem.cs new file mode 100644 index 000000000..dd83a1302 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/EdgeQuadRenderItem.cs @@ -0,0 +1,37 @@ +using GameWorld.Core.Components.Rendering; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace GameWorld.Core.Rendering.RenderItems +{ + public class EdgeQuadRenderItem : IRenderItem + { + public EdgeQuadInstanceMesh EdgeQuadRenderer { get; set; } + public EdgeData[] Edges { get; set; } + public int EdgeCount { get; set; } + public Matrix ModelMatrix { get; set; } = Matrix.Identity; + + private bool _dirty = true; + private EdgeData[] _lastEdges; + + public void MarkDirty() => _dirty = true; + + public void Draw(GraphicsDevice device, CommonShaderParameters parameters, RenderingTechnique renderingTechnique) + { + if (renderingTechnique != RenderingTechnique.Normal) + return; + + if (Edges == null || EdgeQuadRenderer == null || EdgeCount == 0) + return; + + if (_dirty || _lastEdges != Edges) + { + EdgeQuadRenderer.Update(Edges, EdgeCount, parameters); + _lastEdges = Edges; + _dirty = false; + } + + EdgeQuadRenderer.Draw(parameters, device); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/VertexRenderItem.cs b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/VertexRenderItem.cs index 69badff89..dfd401fe4 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/VertexRenderItem.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/RenderItems/VertexRenderItem.cs @@ -14,13 +14,24 @@ public class VertexRenderItem : IRenderItem public Matrix ModelMatrix { get; set; } = Matrix.Identity; public VertexSelectionState SelectedVertices { get; set; } + const float CameraFovRadians = MathHelper.PiOver4; + public void Draw(GraphicsDevice device, CommonShaderParameters parameters, RenderingTechnique renderingTechnique) { if (renderingTechnique != RenderingTechnique.Normal) return; - VertexRenderer.Update(Node.Geometry, Node.RenderMatrix, Node.Orientation, parameters.CameraPosition, SelectedVertices); - VertexRenderer.Draw(parameters.View, parameters.Projection, device, new Vector3(0, 1, 0)); + var viewportHeight = parameters.ViewportHeight > 0 ? parameters.ViewportHeight : device.Viewport.Height; + + VertexRenderer.Update( + Node.Geometry, + Node.RenderMatrix, + parameters.CameraPosition, + CameraFovRadians, + viewportHeight, + SelectedVertices); + + VertexRenderer.Draw(parameters.View, parameters.Projection, parameters.CameraPosition, device); } } } diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/VertexInstanceMesh.cs b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/VertexInstanceMesh.cs index 734ee4875..79b30ed73 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Rendering/VertexInstanceMesh.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Rendering/VertexInstanceMesh.cs @@ -10,42 +10,28 @@ namespace GameWorld.Core.Rendering { [StructLayout(LayoutKind.Sequential)] - public struct InstanceDataOrientation : IVertexType + public struct VertexPointInstanceData : IVertexType { - public Vector3 instanceForward; - public Vector3 instanceUp; - public Vector3 instanceLeft; - public Vector3 instancePosition; + public Vector3 InstancePosition; + public float InstanceScale; + public Vector3 InstanceColor; + public float InstanceWeight; public static readonly VertexDeclaration VertexDeclaration; - static InstanceDataOrientation() + static VertexPointInstanceData() { var elements = new VertexElement[] - { - new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 1), // The usage index must match. - new VertexElement(sizeof(float) *3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 1), - new VertexElement(sizeof(float) *6, VertexElementFormat.Vector3, VertexElementUsage.Normal, 2), - new VertexElement(sizeof(float) *9, VertexElementFormat.Vector3, VertexElementUsage.Normal, 3), - new VertexElement(sizeof(float) *12, VertexElementFormat.Vector3, VertexElementUsage.Normal, 4), - //new VertexElement(48, VertexElementFormat.Single, VertexElementUsage.BlendWeight, 0) - //new VertexElement( offset in bytes, VertexElementFormat.Single, VertexElementUsage. option, shader element usage id number ) - }; + { + new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 1), + new VertexElement(sizeof(float) * 3, VertexElementFormat.Single, VertexElementUsage.Normal, 1), + new VertexElement(sizeof(float) * 4, VertexElementFormat.Vector3, VertexElementUsage.Normal, 2), + new VertexElement(sizeof(float) * 7, VertexElementFormat.Single, VertexElementUsage.Normal, 3), + }; VertexDeclaration = new VertexDeclaration(elements); } - VertexDeclaration IVertexType.VertexDeclaration - { - get { return VertexDeclaration; } - } - } - struct VertexMeshInstanceInfo - { - public Vector3 World0 { get; set; } - public Vector3 World1 { get; set; } - public Vector3 World2 { get; set; } - public Vector3 World3 { get; set; } - public Vector3 Colour { get; set; } - }; + VertexDeclaration IVertexType.VertexDeclaration => VertexDeclaration; + } public class VertexInstanceMesh : IDisposable { @@ -58,13 +44,17 @@ public class VertexInstanceMesh : IDisposable IndexBuffer _indexBuffer; VertexBufferBinding[] _bindings; - VertexMeshInstanceInfo[] _instanceTransform; + VertexPointInstanceData[] _instanceData; readonly int _maxInstanceCount = 50000; int _currentInstanceCount; - Vector3 _selectedColour = new(1, 0, 0); - Vector3 _deselectedColour = new (1, 1, 1); + Vector3 _selectedColour = new(1.0f, 0.47f, 0.0f); + Vector3 _deselectedColour = new(0.0f, 0.0f, 0.0f); + + public float VertexPixelSize { get; set; } = 5.5f; + public float SelectedSizeBoost { get; set; } = 2.0f; + public float SelectionThresholdMultiplier { get; set; } = 2.0f; public VertexInstanceMesh(IDeviceResolver deviceResolverComponent, IScopedResourceLibrary resourceLibrary, IGraphicsResourceCreator graphicsResourceCreator) { @@ -74,13 +64,12 @@ public VertexInstanceMesh(IDeviceResolver deviceResolverComponent, IScopedResour void Initialize(GraphicsDevice device, IScopedResourceLibrary resourceLib) { - _effect = resourceLib.GetStaticEffect(ShaderTypes.GeometryInstance); + _effect = resourceLib.GetStaticEffect(ShaderTypes.VertexPoint); - _instanceVertexDeclaration = InstanceDataOrientation.VertexDeclaration; + _instanceVertexDeclaration = VertexPointInstanceData.VertexDeclaration; GenerateGeometry(device); _instanceBuffer = _graphicsResourceCreator.CreateDynamicVertexBuffer(_instanceVertexDeclaration, _maxInstanceCount, BufferUsage.WriteOnly); - _instanceTransform = new VertexMeshInstanceInfo[_maxInstanceCount]; - GenerateInstanceInformation(_maxInstanceCount); + _instanceData = new VertexPointInstanceData[_maxInstanceCount]; _bindings = new VertexBufferBinding[2]; _bindings[0] = new VertexBufferBinding(_geometryBuffer); @@ -89,113 +78,66 @@ void Initialize(GraphicsDevice device, IScopedResourceLibrary resourceLib) void GenerateGeometry(GraphicsDevice device) { - var vertices = new VertexPosition[24]; - vertices[0].Position = new Vector3(-1, 1, -1); - vertices[1].Position = new Vector3(1, 1, -1); - vertices[2].Position = new Vector3(-1, 1, 1); - vertices[3].Position = new Vector3(1, 1, 1); - - vertices[4].Position = new Vector3(-1, -1, 1); - vertices[5].Position = new Vector3(1, -1, 1); - vertices[6].Position = new Vector3(-1, -1, -1); - vertices[7].Position = new Vector3(1, -1, -1); - - vertices[8].Position = new Vector3(-1, 1, -1); - vertices[9].Position = new Vector3(-1, 1, 1); - vertices[10].Position = new Vector3(-1, -1, -1); - vertices[11].Position = new Vector3(-1, -1, 1); - - vertices[12].Position = new Vector3(-1, 1, 1); - vertices[13].Position = new Vector3(1, 1, 1); - vertices[14].Position = new Vector3(-1, -1, 1); - vertices[15].Position = new Vector3(1, -1, 1); - - vertices[16].Position = new Vector3(1, 1, 1); - vertices[17].Position = new Vector3(1, 1, -1); - vertices[18].Position = new Vector3(1, -1, 1); - vertices[19].Position = new Vector3(1, -1, -1); - - vertices[20].Position = new Vector3(1, 1, -1); - vertices[21].Position = new Vector3(-1, 1, -1); - vertices[22].Position = new Vector3(1, -1, -1); - vertices[23].Position = new Vector3(-1, -1, -1); - - _geometryBuffer = _graphicsResourceCreator.CreateVertexBuffer(VertexPosition.VertexDeclaration, 24, BufferUsage.WriteOnly); + var vertices = new VertexPositionTexture[4]; + vertices[0] = new VertexPositionTexture(new Vector3(-0.5f, -0.5f, 0), new Vector2(0, 1)); + vertices[1] = new VertexPositionTexture(new Vector3(0.5f, -0.5f, 0), new Vector2(1, 1)); + vertices[2] = new VertexPositionTexture(new Vector3(-0.5f, 0.5f, 0), new Vector2(0, 0)); + vertices[3] = new VertexPositionTexture(new Vector3(0.5f, 0.5f, 0), new Vector2(1, 0)); + + _geometryBuffer = _graphicsResourceCreator.CreateVertexBuffer(VertexPositionTexture.VertexDeclaration, 4, BufferUsage.WriteOnly); _geometryBuffer.SetData(vertices); - var indices = new int[36]; + var indices = new int[6]; indices[0] = 0; indices[1] = 1; indices[2] = 2; indices[3] = 1; indices[4] = 3; indices[5] = 2; - indices[6] = 4; indices[7] = 5; indices[8] = 6; - indices[9] = 5; indices[10] = 7; indices[11] = 6; - - indices[12] = 8; indices[13] = 9; indices[14] = 10; - indices[15] = 9; indices[16] = 11; indices[17] = 10; - - indices[18] = 12; indices[19] = 13; indices[20] = 14; - indices[21] = 13; indices[22] = 15; indices[23] = 14; - - indices[24] = 16; indices[25] = 17; indices[26] = 18; - indices[27] = 17; indices[28] = 19; indices[29] = 18; - - indices[30] = 20; indices[31] = 21; indices[32] = 22; - indices[33] = 21; indices[34] = 23; indices[35] = 22; - - _indexBuffer = _graphicsResourceCreator.CreateIndexBuffer(typeof(int), 36, BufferUsage.WriteOnly); + _indexBuffer = _graphicsResourceCreator.CreateIndexBuffer(typeof(int), 6, BufferUsage.WriteOnly); _indexBuffer.SetData(indices); } - public void Update(MeshObject geo, Matrix modelMatrix, Quaternion objectRotation, Vector3 cameraPos, VertexSelectionState selectedVertexes) + public void Update(MeshObject geo, Matrix modelMatrix, Vector3 cameraPos, + float cameraFov, float viewportHeight, VertexSelectionState selectedVertexes) { - _currentInstanceCount = geo.VertexCount(); + _currentInstanceCount = Math.Min(geo.VertexCount(), _maxInstanceCount); + + float fovScale = 2.0f * MathF.Tan(cameraFov / 2.0f) / viewportHeight; + for (var i = 0; i < _currentInstanceCount && i < _maxInstanceCount; i++) { var vertPos = Vector3.Transform(geo.GetVertexById(i), modelMatrix); var distance = (cameraPos - vertPos).Length(); - var distanceScale = distance * 1.5f; - var world = Matrix.CreateScale(0.0025f * distanceScale) * Matrix.CreateFromQuaternion(objectRotation) * Matrix.CreateTranslation(vertPos); + var weight = selectedVertexes.VertexWeights[i]; + var color = Vector3.Lerp(_deselectedColour, _selectedColour, weight); - _instanceTransform[i].World0 = new Vector3(world[0, 0], world[0, 1], world[0, 2]); - _instanceTransform[i].World1 = new Vector3(world[1, 0], world[1, 1], world[1, 2]); - _instanceTransform[i].World2 = new Vector3(world[2, 0], world[2, 1], world[2, 2]); - _instanceTransform[i].World3 = new Vector3(world[3, 0], world[3, 1], world[3, 2]); - _instanceTransform[i].Colour = Vector3.Lerp(_deselectedColour, _selectedColour, selectedVertexes.VertexWeights[i]); + var effectivePixelSize = VertexPixelSize + weight * SelectedSizeBoost; + var worldScale = effectivePixelSize * distance * fovScale; + _instanceData[i].InstancePosition = vertPos; + _instanceData[i].InstanceScale = worldScale; + _instanceData[i].InstanceColor = color; + _instanceData[i].InstanceWeight = weight; } - _instanceBuffer.SetData(_instanceTransform, 0, Math.Min(_currentInstanceCount, _maxInstanceCount), SetDataOptions.None); - } - private void GenerateInstanceInformation(int count) - { - var rnd = new Random(); - - for (var i = 0; i < count; i++) - { - var world = Matrix.CreateScale((float)rnd.NextDouble() * 1) * - Matrix.CreateRotationZ((float)rnd.NextDouble()) * - Matrix.CreateTranslation((float)rnd.NextDouble() * 20, (float)rnd.NextDouble() * 20, (float)rnd.NextDouble() * 20); - - _instanceTransform[i].World0 = new Vector3(world[0, 0], world[0, 1], world[0, 2]); - _instanceTransform[i].World1 = new Vector3(world[1, 0], world[1, 1], world[1, 2]); - _instanceTransform[i].World2 = new Vector3(world[2, 0], world[2, 1], world[2, 2]); - _instanceTransform[i].World3 = new Vector3(world[3, 0], world[3, 1], world[3, 2]); - } - _instanceBuffer.SetData(_instanceTransform); + _instanceBuffer.SetData(_instanceData, 0, Math.Min(_currentInstanceCount, _maxInstanceCount), SetDataOptions.None); } - public void Draw(Matrix view, Matrix projection, GraphicsDevice device, Vector3 colour) + public void Draw(Matrix view, Matrix projection, Vector3 cameraPos, GraphicsDevice device) { - _effect.CurrentTechnique = _effect.Techniques["Instancing"]; - _effect.Parameters["WVP"].SetValue(view * projection); - _effect.Parameters["VertexColour"].SetValue(colour); + _effect.CurrentTechnique = _effect.Techniques["VertexPoint"]; + _effect.Parameters["View"].SetValue(view); + _effect.Parameters["ViewProjection"].SetValue(view * projection); + _effect.Parameters["CameraPosition"].SetValue(cameraPos); + + device.BlendState = BlendState.AlphaBlend; device.Indices = _indexBuffer; _effect.CurrentTechnique.Passes[0].Apply(); device.SetVertexBuffers(_bindings); - device.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 24, 0, 12, _currentInstanceCount); + device.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0, 4, 0, 2, _currentInstanceCount); + + device.BlendState = BlendState.Opaque; } public void Dispose() diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Services/ResourceLibary.cs b/GameWorld/GameWorldCore/GameWorld.Core/Services/ResourceLibary.cs index fdaf19620..b150dfd11 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Services/ResourceLibary.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Services/ResourceLibary.cs @@ -20,7 +20,9 @@ public enum ShaderTypes GeometryInstance, Glow, BloomFilter, - Grid + Grid, + VertexPoint, + EdgeQuad } public class ResourceLibrary @@ -62,6 +64,8 @@ public void Initialize(GraphicsDevice graphicsDevice, ContentManager content) LoadEffect("Shaders\\LineShader", ShaderTypes.Line); LoadEffect("Shaders\\GridShader", ShaderTypes.Grid); LoadEffect("Shaders\\InstancingShader", ShaderTypes.GeometryInstance); + LoadEffect("Shaders\\VertexPointShader", ShaderTypes.VertexPoint); + LoadEffect("Shaders\\EdgeQuadShader", ShaderTypes.EdgeQuad); _pbrDiffuse = _content.Load("textures\\phazer\\DiffuseAmbientLightCubeMap"); _pbrSpecular= _content.Load("textures\\phazer\\SpecularAmbientLightCubemap"); diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Utility/IntersectionMath.cs b/GameWorld/GameWorldCore/GameWorld.Core/Utility/IntersectionMath.cs index 31320645d..0f8f7b6f7 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Utility/IntersectionMath.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Utility/IntersectionMath.cs @@ -11,42 +11,53 @@ public static class IntersectionMath { public static float? IntersectObject(Ray ray, MeshObject geometry, Matrix matrix) { + var inverseTransform = Matrix.Invert(matrix); + var localRay = new Ray( + Vector3.Transform(ray.Position, inverseTransform), + Vector3.TransformNormal(ray.Direction, inverseTransform)); + if (localRay.Intersects(geometry.BoundingBox) == null) + return null; + var res = IntersectFace(ray, geometry, matrix, out var _); return res; } - public static float? IntersectVertex(Ray ray, MeshObject geometry, Vector3 cameraPos, Matrix matrix, out int selectedVertex) + public static float? IntersectVertex(Vector2 mouseScreenPos, MeshObject geometry, Matrix modelMatrix, + Matrix viewProjection, float viewportWidth, float viewportHeight, out int selectedVertex) { - var inverseTransform = Matrix.Invert(matrix); - ray.Position = Vector3.Transform(ray.Position, inverseTransform); - ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform); - cameraPos = Vector3.Transform(cameraPos, inverseTransform); - - var vertexList = geometry.GetVertexList(); - var bestDistance = float.MaxValue; selectedVertex = -1; - for (var i = 0; i < vertexList.Count; i++) + var bestDist = float.MaxValue; + + const float pixelThreshold = 25.0f; + + for (var i = 0; i < geometry.VertexArray.Length; i++) { - var distance = (cameraPos - vertexList[i]).Length(); - var distanceScale = 0.0025f * distance * 1.5f; + var worldPos = Vector3.Transform(geometry.GetVertexById(i), modelMatrix); + var clipPos = Vector4.Transform(new Vector4(worldPos, 1.0f), viewProjection); + + if (clipPos.W <= 0.0f) + continue; - var bb = new BoundingBox(new Vector3(distanceScale * -0.5f) + vertexList[i], new Vector3(distanceScale * 0.5f) + vertexList[i]); - var res = bb.Intersects(ray); ; - if (res != null) + var invW = 1.0f / clipPos.W; + var screenX = (clipPos.X * invW + 1.0f) * 0.5f * viewportWidth; + var screenY = (1.0f - clipPos.Y * invW) * 0.5f * viewportHeight; + + var dist = MathF.Abs(screenX - mouseScreenPos.X) + MathF.Abs(screenY - mouseScreenPos.Y); + + if (dist < bestDist) { - var dist = res.Value; - if (dist < bestDistance) - { - selectedVertex = i; - bestDistance = dist; - } + bestDist = dist; + selectedVertex = i; } } - if (selectedVertex == -1) + if (selectedVertex == -1 || bestDist > pixelThreshold) + { + selectedVertex = -1; return null; + } - return bestDistance; + return bestDist; } public static float? IntersectFace(Ray ray, MeshObject geometry, Matrix matrix, out int? face) @@ -57,6 +68,9 @@ public static class IntersectionMath ray.Position = Vector3.Transform(ray.Position, inverseTransform); ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform); + if (ray.Intersects(geometry.BoundingBox) == null) + return null; + var faceIndex = -1; var bestDistance = float.MaxValue; for (var i = 0; i < geometry.GetIndexCount(); i += 3) @@ -90,6 +104,10 @@ public static class IntersectionMath public static bool IntersectObject(BoundingFrustum boundingFrustum, MeshObject geometry, Matrix matrix) { + var transformedBox = TransformBoundingBox(geometry.BoundingBox, matrix); + if (boundingFrustum.Contains(transformedBox) == ContainmentType.Disjoint) + return false; + for (var i = 0; i < geometry.VertexCount(); i++) { if (boundingFrustum.Contains(Vector3.Transform(geometry.GetVertexById(i), matrix)) != ContainmentType.Disjoint) @@ -103,28 +121,30 @@ public static bool IntersectFaces(BoundingFrustum boundingFrustum, MeshObject ge { faces = new List(); - var indexList = geometry.GetIndexBuffer(); - var vertList = geometry.GetVertexList(); + var transformedBox = TransformBoundingBox(geometry.BoundingBox, matrix); + if (boundingFrustum.Contains(transformedBox) == ContainmentType.Disjoint) + return false; - var transformedVertList = new Vector3[vertList.Count]; - for (var i = 0; i < vertList.Count; i++) - transformedVertList[i] = Vector3.Transform(vertList[i], matrix); + var vertCount = geometry.VertexArray.Length; + var transformedVerts = new Vector3[vertCount]; + for (var i = 0; i < vertCount; i++) + transformedVerts[i] = Vector3.Transform(geometry.GetVertexById(i), matrix); - for (var i = 0; i < indexList.Count; i += 3) + for (var i = 0; i < geometry.IndexArray.Length; i += 3) { - var index0 = indexList[i + 0]; - var index1 = indexList[i + 1]; - var index2 = indexList[i + 2]; + var index0 = geometry.IndexArray[i + 0]; + var index1 = geometry.IndexArray[i + 1]; + var index2 = geometry.IndexArray[i + 2]; - if (boundingFrustum.Contains(transformedVertList[index0]) != ContainmentType.Disjoint) + if (boundingFrustum.Contains(transformedVerts[index0]) != ContainmentType.Disjoint) faces.Add(i); - else if (boundingFrustum.Contains(transformedVertList[index1]) != ContainmentType.Disjoint) + else if (boundingFrustum.Contains(transformedVerts[index1]) != ContainmentType.Disjoint) faces.Add(i); - else if (boundingFrustum.Contains(transformedVertList[index2]) != ContainmentType.Disjoint) + else if (boundingFrustum.Contains(transformedVerts[index2]) != ContainmentType.Disjoint) faces.Add(i); } - if (faces.Count() == 0) + if (faces.Count == 0) faces = null; return faces != null; } @@ -133,19 +153,108 @@ public static bool IntersectVertices(BoundingFrustum boundingFrustum, MeshObject { vertices = new List(); - for (var i = 0; i < geometry.GetIndexCount(); i++) + for (var i = 0; i < geometry.IndexArray.Length; i++) { - var index = geometry.GetIndex(i); - + var index = geometry.IndexArray[i]; if (boundingFrustum.Contains(Vector3.Transform(geometry.GetVertexById(index), matrix)) != ContainmentType.Disjoint) vertices.Add(index); } - vertices = vertices.Distinct().ToList(); - if (vertices.Count() == 0) + + if (vertices.Count == 0) vertices = null; + else + vertices = vertices.Distinct().ToList(); return vertices != null; } + public static float? IntersectEdge(Ray ray, MeshObject geometry, Vector3 cameraPos, Matrix matrix, out (int v0, int v1) selectedEdge) + { + selectedEdge = (-1, -1); + var inverseTransform = Matrix.Invert(matrix); + ray.Position = Vector3.Transform(ray.Position, inverseTransform); + ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform); + cameraPos = Vector3.Transform(cameraPos, inverseTransform); + + var bestDistance = float.MaxValue; + var edgeThreshold = 0.0025f; + + var processedEdges = new HashSet<(int, int)>(); + var indexBuffer = geometry.IndexArray; + + for (var i = 0; i < indexBuffer.Length; i += 3) + { + var i0 = indexBuffer[i]; + var i1 = indexBuffer[i + 1]; + var i2 = indexBuffer[i + 2]; + + var edges = new[] { (Math.Min(i0, i1), Math.Max(i0, i1)), (Math.Min(i1, i2), Math.Max(i1, i2)), (Math.Min(i0, i2), Math.Max(i0, i2)) }; + + foreach (var edge in edges) + { + if (processedEdges.Contains(edge)) + continue; + processedEdges.Add(edge); + + var p0 = geometry.GetVertexById(edge.Item1); + var p1 = geometry.GetVertexById(edge.Item2); + + var midPoint = (p0 + p1) * 0.5f; + var distToCamera = (cameraPos - midPoint).Length(); + var scaledThreshold = edgeThreshold * distToCamera * 1.5f; + + var dist = RayToLineSegmentDistance(ray, p0, p1); + if (dist < scaledThreshold && dist < bestDistance) + { + bestDistance = dist; + selectedEdge = edge; + } + } + } + + if (selectedEdge.Item1 == -1) + return null; + + return bestDistance; + } + + public static bool IntersectEdges(BoundingFrustum boundingFrustum, MeshObject geometry, Matrix matrix, out List<(int v0, int v1)> edges) + { + edges = new List<(int, int)>(); + var processedEdges = new HashSet<(int, int)>(); + var indexBuffer = geometry.IndexArray; + + var vertCount = geometry.VertexArray.Length; + var transformedVerts = new Vector3[vertCount]; + for (var i = 0; i < vertCount; i++) + transformedVerts[i] = Vector3.Transform(geometry.GetVertexById(i), matrix); + + for (var i = 0; i < indexBuffer.Length; i += 3) + { + var i0 = indexBuffer[i]; + var i1 = indexBuffer[i + 1]; + var i2 = indexBuffer[i + 2]; + + var edgeList = new[] { (Math.Min(i0, i1), Math.Max(i0, i1)), (Math.Min(i1, i2), Math.Max(i1, i2)), (Math.Min(i0, i2), Math.Max(i0, i2)) }; + + foreach (var edge in edgeList) + { + if (processedEdges.Contains(edge)) + continue; + processedEdges.Add(edge); + + if (boundingFrustum.Contains(transformedVerts[edge.Item1]) != ContainmentType.Disjoint && + boundingFrustum.Contains(transformedVerts[edge.Item2]) != ContainmentType.Disjoint) + { + edges.Add(edge); + } + } + } + + if (edges.Count == 0) + return false; + return true; + } + public static ushort FindClosestVertexIndex(MeshObject mesh, Vector3 point, out float distance) { var closestDist = float.PositiveInfinity; @@ -167,7 +276,6 @@ public static ushort FindClosestVertexIndex(MeshObject mesh, Vector3 point, out public static bool MollerTrumboreIntersection(Ray r, Vector3 vertex0, Vector3 vertex1, Vector3 vertex2, out float? distance) { - //Source : https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm const float EPSILON = 0.0000001f; Vector3 edge1, edge2, h, s, q; float a, f, u, v; @@ -178,7 +286,7 @@ public static bool MollerTrumboreIntersection(Ray r, Vector3 vertex0, Vector3 ve if (a > -EPSILON && a < EPSILON) { distance = null; - return false; // This ray is parallel to this triangle. + return false; } f = 1.0f / a; s = r.Position - vertex0; @@ -195,14 +303,13 @@ public static bool MollerTrumboreIntersection(Ray r, Vector3 vertex0, Vector3 ve distance = null; return false; } - // At this stage we can compute t to find out where the intersection point is on the line. var t = f * Vector3.Dot(edge2, q); - if (t > EPSILON) // ray intersection + if (t > EPSILON) { distance = t; return true; } - else // This means that there is a line intersection but not a ray intersection. + else { distance = null; return false; @@ -234,5 +341,57 @@ public static bool IntersectBones(BoundingFrustum boundingFrustum, Rmv2MeshNode bones = null; return bones != null; } + + public static BoundingBox TransformBoundingBox(BoundingBox box, Matrix matrix) + { + var corners = box.GetCorners(); + Vector3.Transform(corners, ref matrix, corners); + return BoundingBox.CreateFromPoints(corners); + } + + static float RayToLineSegmentDistance(Ray ray, Vector3 segStart, Vector3 segEnd) + { + var rayDir = ray.Direction; + var segDir = segEnd - segStart; + var segLength = segDir.Length(); + + if (segLength < 0.0001f) + { + var toPoint = segStart - ray.Position; + var projection = Vector3.Dot(toPoint, rayDir); + var closestOnRay = ray.Position + rayDir * projection; + return (closestOnRay - segStart).Length(); + } + + segDir /= segLength; + + var w0 = ray.Position - segStart; + var a = Vector3.Dot(rayDir, rayDir); + var b = Vector3.Dot(rayDir, segDir); + var c = Vector3.Dot(segDir, segDir); + var d = Vector3.Dot(rayDir, w0); + var e = Vector3.Dot(segDir, w0); + + var denom = a * c - b * b; + + float s, t; + if (denom < 0.0001f) + { + s = 0f; + t = d / b; + } + else + { + s = (b * e - c * d) / denom; + t = (a * e - b * d) / denom; + } + + t = MathHelper.Clamp(t, 0f, segLength); + + var rayPt = ray.Position + rayDir * s; + var segPt = segStart + segDir * t; + + return (rayPt - segPt).Length(); + } } } diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs new file mode 100644 index 000000000..925df1ba1 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs @@ -0,0 +1,390 @@ +using GameWorld.Core.Components.Selection; +using GameWorld.Core.Rendering; +using GameWorld.Core.Rendering.Geometry; +using GameWorld.Core.Test.TestUtility; +using GameWorld.Core.Utility; +using Microsoft.Xna.Framework; +using Moq; +using GameWorld.Core.SceneNodes; +using NUnit.Framework; + +namespace GameWorld.Core.Test.Selection +{ + [TestFixture] + public class VertexRenderingOverhaulTests + { + static MeshObject CreateTriangleMesh() + { + var contextFactory = new TestGeometryGraphicsContextFactory(); + var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); + mesh.VertexArray = new VertexPositionNormalTextureCustom[] + { + new() { Position = new Vector4(0, 0, 0, 1) }, + new() { Position = new Vector4(1, 0, 0, 1) }, + new() { Position = new Vector4(0, 1, 0, 1) }, + }; + mesh.IndexArray = new ushort[] { 0, 1, 2 }; + return mesh; + } + + static MeshObject CreateQuadMesh() + { + var contextFactory = new TestGeometryGraphicsContextFactory(); + var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); + mesh.VertexArray = new VertexPositionNormalTextureCustom[] + { + new() { Position = new Vector4(0, 0, 0, 1) }, + new() { Position = new Vector4(1, 0, 0, 1) }, + new() { Position = new Vector4(1, 1, 0, 1) }, + new() { Position = new Vector4(0, 1, 0, 1) }, + }; + mesh.IndexArray = new ushort[] { 0, 1, 2, 0, 2, 3 }; + return mesh; + } + + static ISelectable CreateSelectable(MeshObject mesh) + { + var mock = new Mock(); + mock.Setup(x => x.Geometry).Returns(mesh); + mock.Setup(x => x.RenderMatrix).Returns(Matrix.Identity); + mock.Setup(x => x.Name).Returns("TestMesh"); + return mock.Object; + } + + #region IntersectionMath - Screen-Space Vertex Picking + + [Test] + public void IntersectVertex_ScreenSpace_FindsClosestVertex() + { + var mesh = CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + // Project vertex 0 (0,0,0) to screen to find where it should be + var clipPos = Vector4.Transform(new Vector4(0, 0, 0, 1), viewProjection); + var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; + var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; + + var result = IntersectionMath.IntersectVertex( + new Vector2(screenX, screenY), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Not.Null); + Assert.That(selectedVertex, Is.EqualTo(0)); + } + + [Test] + public void IntersectVertex_ScreenSpace_ReturnsNull_WhenFarFromVertices() + { + var mesh = CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + // Click far from any vertex (corner of screen) + var result = IntersectionMath.IntersectVertex( + new Vector2(0, 0), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Null); + Assert.That(selectedVertex, Is.EqualTo(-1)); + } + + [Test] + public void IntersectVertex_ScreenSpace_SelectsCloserVertex() + { + var mesh = CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + // Project vertex 1 (1,0,0) to screen + var clipPos = Vector4.Transform(new Vector4(1, 0, 0, 1), viewProjection); + var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; + var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; + + var result = IntersectionMath.IntersectVertex( + new Vector2(screenX, screenY), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Not.Null); + Assert.That(selectedVertex, Is.EqualTo(1)); + } + + #endregion + + #region IntersectionMath - Edge Picking + + [Test] + public void IntersectEdge_FindsEdge() + { + var mesh = CreateTriangleMesh(); + var cameraPos = new Vector3(0.5f, 0, 2); + var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); + + var result = IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); + + Assert.That(result, Is.Not.Null); + Assert.That(selectedEdge.v0, Is.GreaterThanOrEqualTo(0)); + Assert.That(selectedEdge.v1, Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public void IntersectEdge_ReturnsNull_WhenRayIsFar() + { + var mesh = CreateTriangleMesh(); + var cameraPos = new Vector3(100, 100, 2); + var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); + + var result = IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); + + Assert.That(result, Is.Null); + Assert.That(selectedEdge.v0, Is.EqualTo(-1)); + } + + [Test] + public void IntersectEdge_OrdersEdgeVertices() + { + var mesh = CreateTriangleMesh(); + var cameraPos = new Vector3(0.5f, 0, 2); + var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); + + IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); + + if (selectedEdge.v0 >= 0) + Assert.That(selectedEdge.v0, Is.LessThanOrEqualTo(selectedEdge.v1)); + } + + [Test] + public void IntersectEdges_RectangleSelection_FindsEdgesInFrustum() + { + var mesh = CreateTriangleMesh(); + var frustum = new BoundingFrustum( + Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, 1.0f, 0.1f, 100f)); + + var found = IntersectionMath.IntersectEdges(frustum, mesh, Matrix.Identity, out var edges); + + Assert.That(found, Is.True); + Assert.That(edges.Count, Is.GreaterThan(0)); + } + + #endregion + + #region IntersectionMath - BoundingBox Helpers + + [Test] + public void TransformBoundingBox_AppliesTranslation() + { + var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); + var translation = Matrix.CreateTranslation(10, 0, 0); + + var result = IntersectionMath.TransformBoundingBox(box, translation); + + Assert.That(result.Min.X, Is.EqualTo(9).Within(0.001f)); + Assert.That(result.Max.X, Is.EqualTo(11).Within(0.001f)); + } + + [Test] + public void TransformBoundingBox_AppliesScale() + { + var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); + var scale = Matrix.CreateScale(2); + + var result = IntersectionMath.TransformBoundingBox(box, scale); + + Assert.That(result.Min.X, Is.EqualTo(-2).Within(0.001f)); + Assert.That(result.Max.X, Is.EqualTo(2).Within(0.001f)); + } + + #endregion + + #region EdgeSelectionState + + [Test] + public void EdgeSelectionState_ModifySelection_AddsEdges() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); + + Assert.That(state.SelectionCount(), Is.EqualTo(2)); + Assert.That(state.SelectedEdges, Does.Contain((0, 1))); + Assert.That(state.SelectedEdges, Does.Contain((1, 2))); + } + + [Test] + public void EdgeSelectionState_ModifySelection_RemovesEdges() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (1, 2), (0, 2) }, onlyRemove: false); + state.ModifySelection(new[] { (1, 2) }, onlyRemove: true); + + Assert.That(state.SelectionCount(), Is.EqualTo(2)); + Assert.That(state.SelectedEdges, Does.Not.Contain((1, 2))); + } + + [Test] + public void EdgeSelectionState_GetSelectedVertexIndices_ReturnsUniqueVertices() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); + + var vertices = state.GetSelectedVertexIndices(); + + Assert.That(vertices.Count, Is.EqualTo(3)); + Assert.That(vertices, Does.Contain(0)); + Assert.That(vertices, Does.Contain(1)); + Assert.That(vertices, Does.Contain(2)); + } + + [Test] + public void EdgeSelectionState_Clear_RemovesAll() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); + state.Clear(); + + Assert.That(state.SelectionCount(), Is.EqualTo(0)); + } + + [Test] + public void EdgeSelectionState_Clone_CreatesIndependentCopy() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); + + var clone = state.Clone() as EdgeSelectionState; + clone.ModifySelection(new[] { (2, 3) }, onlyRemove: false); + + Assert.That(state.SelectionCount(), Is.EqualTo(2)); + Assert.That(clone.SelectionCount(), Is.EqualTo(3)); + } + + [Test] + public void EdgeSelectionState_DeduplicatesEdges() + { + var state = new EdgeSelectionState(); + state.ModifySelection(new[] { (0, 1), (0, 1), (0, 1) }, onlyRemove: false); + + Assert.That(state.SelectionCount(), Is.EqualTo(1)); + } + + #endregion + + #region VertexSelectionState - Performance Improvements + + [Test] + public void VertexSelectionState_WeightsZeroFalloff_OnlySelectedAreWeighted() + { + var mesh = CreateTriangleMesh(); + var selectable = CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + + state.ModifySelection(new[] { 1 }, onlyRemove: false); + + Assert.That(state.VertexWeights[0], Is.EqualTo(0)); + Assert.That(state.VertexWeights[1], Is.EqualTo(1)); + Assert.That(state.VertexWeights[2], Is.EqualTo(0)); + } + + [Test] + public void VertexSelectionState_WeightsFalloff_NearbyVerticesGetWeight() + { + var mesh = CreateTriangleMesh(); + var selectable = CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 2.0f); + + state.ModifySelection(new[] { 0 }, onlyRemove: false); + + Assert.That(state.VertexWeights[0], Is.EqualTo(1.0f)); + Assert.That(state.VertexWeights[1], Is.GreaterThan(0)); + Assert.That(state.VertexWeights[1], Is.LessThan(1)); + Assert.That(state.VertexWeights[2], Is.GreaterThan(0)); + } + + [Test] + public void VertexSelectionState_ModifySelection_DeselectWorks() + { + var mesh = CreateTriangleMesh(); + var selectable = CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + + state.ModifySelection(new[] { 0, 1, 2 }, onlyRemove: false); + Assert.That(state.SelectionCount(), Is.EqualTo(3)); + + state.ModifySelection(new[] { 1 }, onlyRemove: true); + Assert.That(state.SelectionCount(), Is.EqualTo(2)); + Assert.That(state.VertexWeights[1], Is.EqualTo(0)); + } + + [Test] + public void VertexSelectionState_Clone_IndependentCopy() + { + var mesh = CreateTriangleMesh(); + var selectable = CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + state.ModifySelection(new[] { 0 }, onlyRemove: false); + + var clone = state.Clone() as VertexSelectionState; + clone.ModifySelection(new[] { 1 }, onlyRemove: false); + + Assert.That(state.SelectionCount(), Is.EqualTo(1)); + Assert.That(clone.SelectionCount(), Is.EqualTo(2)); + } + + [Test] + public void VertexSelectionState_EnsureSorted_RemovesDuplicatesAndSorts() + { + var mesh = CreateQuadMesh(); + var selectable = CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + state.SelectedVertices = new List { 3, 1, 1, 2 }; + state.EnsureSorted(); + + Assert.That(state.SelectedVertices, Is.EqualTo(new List { 1, 2, 3 })); + } + + #endregion + + #region IntersectionMath - Face/Object with BoundingBox Pre-check + + [Test] + public void IntersectObject_ReturnNull_WhenRayMissesBoundingBox() + { + var mesh = CreateTriangleMesh(); + // Set a valid bounding box + typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, + new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); + + // Ray parallel, far away + var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); + var result = IntersectionMath.IntersectObject(ray, mesh, Matrix.Identity); + + Assert.That(result, Is.Null); + } + + [Test] + public void IntersectFace_ReturnNull_WhenRayMissesBoundingBox() + { + var mesh = CreateTriangleMesh(); + typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, + new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); + + var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); + var result = IntersectionMath.IntersectFace(ray, mesh, Matrix.Identity, out var face); + + Assert.That(result, Is.Null); + Assert.That(face, Is.Null); + } + + #endregion + + #region GeometrySelectionMode Enum + + [Test] + public void GeometrySelectionMode_ContainsEdge() + { + Assert.That(Enum.IsDefined(typeof(GeometrySelectionMode), GeometrySelectionMode.Edge), Is.True); + } + + #endregion + } +} From 55cf458e72257914968d5359445e6024c5536f81 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 09:27:59 +0200 Subject: [PATCH 2/6] Removed the face selection stuff --- .../Commands/Edge/EdgeSelectionCommand.cs | 47 ------------- .../Selection/EdgeSelectionState.cs | 70 ------------------- .../Components/Selection/ISelectionState.cs | 4 +- .../Selection/ObjectSelectionState.cs | 6 +- .../Selection/SelectionComponent.cs | 39 ----------- .../Components/Selection/SelectionManager.cs | 57 +++------------ 6 files changed, 13 insertions(+), 210 deletions(-) delete mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs delete mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs b/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs deleted file mode 100644 index 3bb0b7c40..000000000 --- a/GameWorld/GameWorldCore/GameWorld.Core/Commands/Edge/EdgeSelectionCommand.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using GameWorld.Core.Components.Selection; - -namespace GameWorld.Core.Commands.Edge -{ - public class EdgeSelectionCommand : ICommand - { - private readonly SelectionManager _selectionManager; - private List<(int v0, int v1)> _edges; - private bool _isAdd; - private bool _isRemove; - private ISelectionState _oldState; - - public string HintText => "Edge selection"; - public bool IsMutation => false; - - public EdgeSelectionCommand(SelectionManager selectionManager) - { - _selectionManager = selectionManager; - } - - public void Configure(List<(int v0, int v1)> edges, bool isSelectionModification, bool removeSelection) - { - _edges = edges; - _isAdd = isSelectionModification; - _isRemove = removeSelection; - } - - public void Execute() - { - _oldState = _selectionManager.GetStateCopy(); - var state = _selectionManager.GetState(); - if (state == null) - return; - - if (!_isAdd && !_isRemove) - state.Clear(); - - state.ModifySelection(_edges, _isRemove); - } - - public void Undo() - { - _selectionManager.SetState(_oldState); - } - } -} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs deleted file mode 100644 index e09b052ac..000000000 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/EdgeSelectionState.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using GameWorld.Core.SceneNodes; - -namespace GameWorld.Core.Components.Selection -{ - public class EdgeSelectionState : ISelectionState - { - public event SelectionStateChanged SelectionChanged; - - public GeometrySelectionMode Mode => GeometrySelectionMode.Edge; - public ISelectable RenderObject { get; set; } - - private readonly HashSet<(int v0, int v1)> _selectedEdges = new(); - - public IReadOnlyCollection<(int v0, int v1)> SelectedEdges => _selectedEdges; - - public void ModifySelection(IEnumerable<(int v0, int v1)> edges, bool onlyRemove) - { - if (onlyRemove) - { - foreach (var edge in edges) - _selectedEdges.Remove(edge); - } - else - { - foreach (var edge in edges) - _selectedEdges.Add(edge); - } - - SelectionChanged?.Invoke(this, true); - } - - public List GetSelectedVertexIndices() - { - var set = new HashSet(); - foreach (var (v0, v1) in _selectedEdges) - { - set.Add(v0); - set.Add(v1); - } - return set.ToList(); - } - - public ISelectionState Clone() - { - var clone = new EdgeSelectionState { RenderObject = RenderObject }; - foreach (var edge in _selectedEdges) - clone._selectedEdges.Add(edge); - return clone; - } - - public void Clear() - { - _selectedEdges.Clear(); - SelectionChanged?.Invoke(this, true); - } - - public int SelectionCount() => _selectedEdges.Count; - - public ISelectable GetSingleSelectedObject() => RenderObject; - - public List SelectedObjects() - { - if (RenderObject != null) - return new List { RenderObject }; - return new List(); - } - } -} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs index 39808ccd9..8cb5011e1 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using GameWorld.Core.SceneNodes; +using GameWorld.Core.SceneNodes; namespace GameWorld.Core.Components.Selection { @@ -8,7 +7,6 @@ public enum GeometrySelectionMode Object, Face, Vertex, - Edge, Bone }; diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ObjectSelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ObjectSelectionState.cs index a4e4ef13f..bc767ddb9 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ObjectSelectionState.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ObjectSelectionState.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; -using System.Linq; -using GameWorld.Core.SceneNodes; +using GameWorld.Core.SceneNodes; namespace GameWorld.Core.Components.Selection { public class ObjectSelectionState : ISelectionState { - public event SelectionStateChanged SelectionChanged; + public event SelectionStateChanged? SelectionChanged; public GeometrySelectionMode Mode => GeometrySelectionMode.Object; List _selectionList { get; set; } = new List(); diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs index 2e3f51001..bb8f956ca 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs @@ -1,7 +1,6 @@ using System.Windows.Forms; using GameWorld.Core.Commands; using GameWorld.Core.Commands.Bone; -using GameWorld.Core.Commands.Edge; using GameWorld.Core.Commands.Face; using GameWorld.Core.Commands.Object; using GameWorld.Core.Commands.Vertex; @@ -141,14 +140,6 @@ void SelectFromRectangle(Rectangle screenRect, bool isSelectionModification, boo return; } } - else if (currentState.Mode == GeometrySelectionMode.Edge && currentState is EdgeSelectionState edgeState) - { - if (edgeState.RenderObject != null && IntersectionMath.IntersectEdges(unprojectedSelectionRect, edgeState.RenderObject.Geometry, edgeState.RenderObject.RenderMatrix, out var edges)) - { - _commandFactory.Create().Configure(x => x.Configure(edges, isSelectionModification, removeSelection)).BuildAndExecute(); - return; - } - } else if (currentState.Mode == GeometrySelectionMode.Bone && currentState is BoneSelectionState boneState) { if (boneState.RenderObject == null) @@ -211,15 +202,6 @@ void SelectFromPoint(Vector2 mousePosition, bool isSelectionModification, bool r } } - if (currentState is EdgeSelectionState edgeState && edgeState.RenderObject != null) - { - if (IntersectionMath.IntersectEdge(ray, edgeState.RenderObject.Geometry, _camera.Position, edgeState.RenderObject.RenderMatrix, out var selectedEdge) != null) - { - _commandFactory.Create().Configure(x => x.Configure(new List<(int, int)>() { selectedEdge }, isSelectionModification, removeSelection)).BuildAndExecute(); - return; - } - } - // Pick object var selectedObject = _sceneManger.SelectObject(ray); if (selectedObject == null && isSelectionModification == false) @@ -276,21 +258,6 @@ public bool SetVertexSelectionMode() return false; } - public bool SetEdgeSelectionMode() - { - var selectionState = _selectionManager.GetState(); - if (_selectionManager.GetState().Mode != GeometrySelectionMode.Edge) - { - var selectedObject = selectionState.GetSingleSelectedObject(); - if (selectedObject != null) - { - _commandFactory.Create().Configure(x => x.Configure(selectedObject, GeometrySelectionMode.Edge)).BuildAndExecute(); - return true; - } - } - return false; - } - public bool SetBoneSelectionMode() { var selectionState = _selectionManager.GetState(); @@ -327,12 +294,6 @@ bool ChangeSelectionMode() return true; } - else if (_keyboardComponent.IsKeyReleased(Keys.F4)) - { - if (SetEdgeSelectionMode()) - return true; - } - else if (_keyboardComponent.IsKeyReleased(Keys.F9)) { if (SetBoneSelectionMode()) diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs index 18c2a8b50..a157d2c04 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Components.Rendering; using GameWorld.Core.Rendering; using GameWorld.Core.Rendering.Materials.Shaders; using GameWorld.Core.Rendering.RenderItems; @@ -8,14 +6,13 @@ using GameWorld.Core.Services; using GameWorld.Core.Utility; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using Shared.Core.Events; namespace GameWorld.Core.Components.Selection { public class SelectionChangedEvent { - public ISelectionState NewState { get; internal set; } + public ISelectionState? NewState { get; internal set; } } public class SelectionManager : BaseComponent, IDisposable @@ -44,7 +41,7 @@ public class SelectionManager : BaseComponent, IDisposable private int _sampleIdx1 = 1; const int MaxRenderEdges = 50000; - private EdgeData[] _edgeDataCache = new EdgeData[MaxRenderEdges]; + private readonly EdgeData[] _edgeDataCache = new EdgeData[MaxRenderEdges]; public SelectionManager(IEventHub eventHub, RenderEngineComponent renderEngine, IScopedResourceLibrary resourceLib, IDeviceResolver deviceResolverComponent, IGraphicsResourceCreator graphicsResourceCreator) { @@ -84,31 +81,14 @@ public ISelectionState CreateSelectionSate(GeometrySelectionMode mode, ISelectab _currentState.SelectionChanged -= SelectionManager_SelectionChanged; } - switch (mode) + _currentState = mode switch { - case GeometrySelectionMode.Object: - _currentState = new ObjectSelectionState(); - break; - - case GeometrySelectionMode.Face: - _currentState = new FaceSelectionState(); - break; - - //case GeometrySelectionMode.Edge: - // _currentState = new EdgeSelectionState(); - // break; - // - case GeometrySelectionMode.Vertex: - _currentState = new VertexSelectionState(selectedObj, _vertexSelectionFalloff); - break; - case GeometrySelectionMode.Bone: - _currentState = new BoneSelectionState(selectedObj); - break; - - default: - throw new Exception(); - } - + GeometrySelectionMode.Object => new ObjectSelectionState(), + GeometrySelectionMode.Face => new FaceSelectionState(), + GeometrySelectionMode.Vertex => new VertexSelectionState(selectedObj, _vertexSelectionFalloff), + GeometrySelectionMode.Bone => new BoneSelectionState(selectedObj), + _ => throw new Exception(), + }; _currentState.SelectionChanged += SelectionManager_SelectionChanged; SelectionManager_SelectionChanged(_currentState, sendEvent); return _currentState; @@ -211,23 +191,6 @@ public override void Draw(GameTime gameTime) _cachedEdgeMesh = null; _edgeDataDirty = true; } - // - //if (selectionState is EdgeSelectionState selectionEdgeState && selectionEdgeState.RenderObject is Rmv2MeshNode edgeNode) - //{ - // _renderEngine.AddRenderItem(RenderBuckedId.Wireframe, new GeometryRenderItem(edgeNode.Geometry, _wireframeEffect, edgeNode.RenderMatrix)); - // var geometry = edgeNode.Geometry; - // var matrix = edgeNode.RenderMatrix; - // foreach (var edge in selectionEdgeState.SelectedEdges) - // { - // var p0 = Vector3.Transform(geometry.GetVertexById(edge.v0), matrix); - // var p1 = Vector3.Transform(geometry.GetVertexById(edge.v1), matrix); - // _renderEngine.AddRenderLines(new VertexPositionColor[] - // { - // new VertexPositionColor(p0, Color.Orange), - // new VertexPositionColor(p1, Color.Orange) - // }); - // } - //} if (selectionState is BoneSelectionState selectionBoneState && selectionBoneState.RenderObject != null) { From 2b2a7104e94e1775cd0013f90abe7de99cb9de55 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 09:45:51 +0200 Subject: [PATCH 3/6] code --- .../PrintTrackedGraphicsResourcesCommand.cs | 3 +- .../Components/SceneInformationComponent.cs | 52 +++++++++++++++++++ .../DependencyInjectionContainer.cs | 10 ++-- 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs diff --git a/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs index 32a278456..1da3af481 100644 --- a/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs +++ b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs @@ -1,5 +1,4 @@ -using System; -using System.Text; +using System.Text; using GameWorld.Core.Services; using Serilog; using Shared.Core.DependencyInjection; diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs new file mode 100644 index 000000000..d53920cf5 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs @@ -0,0 +1,52 @@ +using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Rendering.RenderItems; +using GameWorld.Core.SceneNodes; +using Microsoft.Xna.Framework; + +namespace GameWorld.Core.Components +{ + public class SceneInformationComponent : BaseComponent + { + private TimeSpan _timeElapsed; + private readonly RenderEngineComponent _renderEngineComponent; + private readonly SceneManager _sceneManager; + + // Cached scene statistics (updated once per second) + private int _objectCount; + private int _vertexCount; + private int _faceCount; + + public SceneInformationComponent(RenderEngineComponent renderEngineComponent, SceneManager sceneManager) + { + _renderEngineComponent = renderEngineComponent; + _sceneManager = sceneManager; + } + + public override void Update(GameTime gameTime) + { + _timeElapsed += gameTime.ElapsedGameTime; + if (_timeElapsed >= TimeSpan.FromSeconds(1)) + { + _timeElapsed -= TimeSpan.FromSeconds(1); + var meshNodes = SceneNodeHelper.GetChildrenOfType(_sceneManager.RootNode); + _objectCount = meshNodes.Count; + _vertexCount = 0; + _faceCount = 0; + foreach (var node in meshNodes) + { + if (node.Geometry != null) + { + _vertexCount += node.Geometry.VertexCount(); + _faceCount += node.Geometry.IndexArray.Length / 3; + } + } + } + } + + public override void Draw(GameTime gameTime) + { + var statsItem = new FontRenderItem(_renderEngineComponent, $"Objects: {_objectCount} Verts: {_vertexCount} Faces: {_faceCount}", new Vector2(5, 25), Color.LightGray); + _renderEngineComponent.AddRenderItem(RenderBuckedId.Font, statsItem); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/DependencyInjectionContainer.cs b/GameWorld/GameWorldCore/GameWorld.Core/DependencyInjectionContainer.cs index 133d87a11..a8467ed2c 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/DependencyInjectionContainer.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/DependencyInjectionContainer.cs @@ -1,4 +1,5 @@ -using GameWorld.Core.Commands; +using System.Diagnostics; +using GameWorld.Core.Commands; using GameWorld.Core.Commands.Bone; using GameWorld.Core.Commands.Bone.Clipboard; using GameWorld.Core.Commands.Face; @@ -108,7 +109,7 @@ void RegisterComponents(IServiceCollection serviceCollection) RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); - RegisterGameComponent(serviceCollection); + RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); @@ -121,7 +122,10 @@ void RegisterComponents(IServiceCollection serviceCollection) RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); RegisterGameComponent(serviceCollection); - + + if (Debugger.IsAttached) + RegisterGameComponent(serviceCollection); + //serviceCollection.AddScoped(x => x.GetRequiredService()); From 443d52fdc6466c2774d1f32718c5383d83fa2c6a Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 09:57:10 +0200 Subject: [PATCH 4/6] Code --- .../Selection/VertexRenderingOverhaulTests.cs | 390 ------------------ .../Selection/VertexSelectionStateTests.cs | 80 ++++ .../TestUtility/MeshTestHelper.cs | 49 +++ .../Utility/IntersectionMathTests.cs | 115 ++++++ 4 files changed, 244 insertions(+), 390 deletions(-) delete mode 100644 GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexSelectionStateTests.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.CoreTest/TestUtility/MeshTestHelper.cs create mode 100644 GameWorld/GameWorldCore/GameWorld.CoreTest/Utility/IntersectionMathTests.cs diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs deleted file mode 100644 index 925df1ba1..000000000 --- a/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexRenderingOverhaulTests.cs +++ /dev/null @@ -1,390 +0,0 @@ -using GameWorld.Core.Components.Selection; -using GameWorld.Core.Rendering; -using GameWorld.Core.Rendering.Geometry; -using GameWorld.Core.Test.TestUtility; -using GameWorld.Core.Utility; -using Microsoft.Xna.Framework; -using Moq; -using GameWorld.Core.SceneNodes; -using NUnit.Framework; - -namespace GameWorld.Core.Test.Selection -{ - [TestFixture] - public class VertexRenderingOverhaulTests - { - static MeshObject CreateTriangleMesh() - { - var contextFactory = new TestGeometryGraphicsContextFactory(); - var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); - mesh.VertexArray = new VertexPositionNormalTextureCustom[] - { - new() { Position = new Vector4(0, 0, 0, 1) }, - new() { Position = new Vector4(1, 0, 0, 1) }, - new() { Position = new Vector4(0, 1, 0, 1) }, - }; - mesh.IndexArray = new ushort[] { 0, 1, 2 }; - return mesh; - } - - static MeshObject CreateQuadMesh() - { - var contextFactory = new TestGeometryGraphicsContextFactory(); - var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); - mesh.VertexArray = new VertexPositionNormalTextureCustom[] - { - new() { Position = new Vector4(0, 0, 0, 1) }, - new() { Position = new Vector4(1, 0, 0, 1) }, - new() { Position = new Vector4(1, 1, 0, 1) }, - new() { Position = new Vector4(0, 1, 0, 1) }, - }; - mesh.IndexArray = new ushort[] { 0, 1, 2, 0, 2, 3 }; - return mesh; - } - - static ISelectable CreateSelectable(MeshObject mesh) - { - var mock = new Mock(); - mock.Setup(x => x.Geometry).Returns(mesh); - mock.Setup(x => x.RenderMatrix).Returns(Matrix.Identity); - mock.Setup(x => x.Name).Returns("TestMesh"); - return mock.Object; - } - - #region IntersectionMath - Screen-Space Vertex Picking - - [Test] - public void IntersectVertex_ScreenSpace_FindsClosestVertex() - { - var mesh = CreateTriangleMesh(); - var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * - Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); - - // Project vertex 0 (0,0,0) to screen to find where it should be - var clipPos = Vector4.Transform(new Vector4(0, 0, 0, 1), viewProjection); - var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; - var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; - - var result = IntersectionMath.IntersectVertex( - new Vector2(screenX, screenY), mesh, Matrix.Identity, - viewProjection, 800, 600, out var selectedVertex); - - Assert.That(result, Is.Not.Null); - Assert.That(selectedVertex, Is.EqualTo(0)); - } - - [Test] - public void IntersectVertex_ScreenSpace_ReturnsNull_WhenFarFromVertices() - { - var mesh = CreateTriangleMesh(); - var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * - Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); - - // Click far from any vertex (corner of screen) - var result = IntersectionMath.IntersectVertex( - new Vector2(0, 0), mesh, Matrix.Identity, - viewProjection, 800, 600, out var selectedVertex); - - Assert.That(result, Is.Null); - Assert.That(selectedVertex, Is.EqualTo(-1)); - } - - [Test] - public void IntersectVertex_ScreenSpace_SelectsCloserVertex() - { - var mesh = CreateTriangleMesh(); - var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * - Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); - - // Project vertex 1 (1,0,0) to screen - var clipPos = Vector4.Transform(new Vector4(1, 0, 0, 1), viewProjection); - var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; - var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; - - var result = IntersectionMath.IntersectVertex( - new Vector2(screenX, screenY), mesh, Matrix.Identity, - viewProjection, 800, 600, out var selectedVertex); - - Assert.That(result, Is.Not.Null); - Assert.That(selectedVertex, Is.EqualTo(1)); - } - - #endregion - - #region IntersectionMath - Edge Picking - - [Test] - public void IntersectEdge_FindsEdge() - { - var mesh = CreateTriangleMesh(); - var cameraPos = new Vector3(0.5f, 0, 2); - var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); - - var result = IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); - - Assert.That(result, Is.Not.Null); - Assert.That(selectedEdge.v0, Is.GreaterThanOrEqualTo(0)); - Assert.That(selectedEdge.v1, Is.GreaterThanOrEqualTo(0)); - } - - [Test] - public void IntersectEdge_ReturnsNull_WhenRayIsFar() - { - var mesh = CreateTriangleMesh(); - var cameraPos = new Vector3(100, 100, 2); - var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); - - var result = IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); - - Assert.That(result, Is.Null); - Assert.That(selectedEdge.v0, Is.EqualTo(-1)); - } - - [Test] - public void IntersectEdge_OrdersEdgeVertices() - { - var mesh = CreateTriangleMesh(); - var cameraPos = new Vector3(0.5f, 0, 2); - var ray = new Ray(cameraPos, new Vector3(0, 0, -1)); - - IntersectionMath.IntersectEdge(ray, mesh, cameraPos, Matrix.Identity, out var selectedEdge); - - if (selectedEdge.v0 >= 0) - Assert.That(selectedEdge.v0, Is.LessThanOrEqualTo(selectedEdge.v1)); - } - - [Test] - public void IntersectEdges_RectangleSelection_FindsEdgesInFrustum() - { - var mesh = CreateTriangleMesh(); - var frustum = new BoundingFrustum( - Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * - Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, 1.0f, 0.1f, 100f)); - - var found = IntersectionMath.IntersectEdges(frustum, mesh, Matrix.Identity, out var edges); - - Assert.That(found, Is.True); - Assert.That(edges.Count, Is.GreaterThan(0)); - } - - #endregion - - #region IntersectionMath - BoundingBox Helpers - - [Test] - public void TransformBoundingBox_AppliesTranslation() - { - var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); - var translation = Matrix.CreateTranslation(10, 0, 0); - - var result = IntersectionMath.TransformBoundingBox(box, translation); - - Assert.That(result.Min.X, Is.EqualTo(9).Within(0.001f)); - Assert.That(result.Max.X, Is.EqualTo(11).Within(0.001f)); - } - - [Test] - public void TransformBoundingBox_AppliesScale() - { - var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); - var scale = Matrix.CreateScale(2); - - var result = IntersectionMath.TransformBoundingBox(box, scale); - - Assert.That(result.Min.X, Is.EqualTo(-2).Within(0.001f)); - Assert.That(result.Max.X, Is.EqualTo(2).Within(0.001f)); - } - - #endregion - - #region EdgeSelectionState - - [Test] - public void EdgeSelectionState_ModifySelection_AddsEdges() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); - - Assert.That(state.SelectionCount(), Is.EqualTo(2)); - Assert.That(state.SelectedEdges, Does.Contain((0, 1))); - Assert.That(state.SelectedEdges, Does.Contain((1, 2))); - } - - [Test] - public void EdgeSelectionState_ModifySelection_RemovesEdges() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (1, 2), (0, 2) }, onlyRemove: false); - state.ModifySelection(new[] { (1, 2) }, onlyRemove: true); - - Assert.That(state.SelectionCount(), Is.EqualTo(2)); - Assert.That(state.SelectedEdges, Does.Not.Contain((1, 2))); - } - - [Test] - public void EdgeSelectionState_GetSelectedVertexIndices_ReturnsUniqueVertices() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); - - var vertices = state.GetSelectedVertexIndices(); - - Assert.That(vertices.Count, Is.EqualTo(3)); - Assert.That(vertices, Does.Contain(0)); - Assert.That(vertices, Does.Contain(1)); - Assert.That(vertices, Does.Contain(2)); - } - - [Test] - public void EdgeSelectionState_Clear_RemovesAll() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); - state.Clear(); - - Assert.That(state.SelectionCount(), Is.EqualTo(0)); - } - - [Test] - public void EdgeSelectionState_Clone_CreatesIndependentCopy() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (1, 2) }, onlyRemove: false); - - var clone = state.Clone() as EdgeSelectionState; - clone.ModifySelection(new[] { (2, 3) }, onlyRemove: false); - - Assert.That(state.SelectionCount(), Is.EqualTo(2)); - Assert.That(clone.SelectionCount(), Is.EqualTo(3)); - } - - [Test] - public void EdgeSelectionState_DeduplicatesEdges() - { - var state = new EdgeSelectionState(); - state.ModifySelection(new[] { (0, 1), (0, 1), (0, 1) }, onlyRemove: false); - - Assert.That(state.SelectionCount(), Is.EqualTo(1)); - } - - #endregion - - #region VertexSelectionState - Performance Improvements - - [Test] - public void VertexSelectionState_WeightsZeroFalloff_OnlySelectedAreWeighted() - { - var mesh = CreateTriangleMesh(); - var selectable = CreateSelectable(mesh); - var state = new VertexSelectionState(selectable, 0); - - state.ModifySelection(new[] { 1 }, onlyRemove: false); - - Assert.That(state.VertexWeights[0], Is.EqualTo(0)); - Assert.That(state.VertexWeights[1], Is.EqualTo(1)); - Assert.That(state.VertexWeights[2], Is.EqualTo(0)); - } - - [Test] - public void VertexSelectionState_WeightsFalloff_NearbyVerticesGetWeight() - { - var mesh = CreateTriangleMesh(); - var selectable = CreateSelectable(mesh); - var state = new VertexSelectionState(selectable, 2.0f); - - state.ModifySelection(new[] { 0 }, onlyRemove: false); - - Assert.That(state.VertexWeights[0], Is.EqualTo(1.0f)); - Assert.That(state.VertexWeights[1], Is.GreaterThan(0)); - Assert.That(state.VertexWeights[1], Is.LessThan(1)); - Assert.That(state.VertexWeights[2], Is.GreaterThan(0)); - } - - [Test] - public void VertexSelectionState_ModifySelection_DeselectWorks() - { - var mesh = CreateTriangleMesh(); - var selectable = CreateSelectable(mesh); - var state = new VertexSelectionState(selectable, 0); - - state.ModifySelection(new[] { 0, 1, 2 }, onlyRemove: false); - Assert.That(state.SelectionCount(), Is.EqualTo(3)); - - state.ModifySelection(new[] { 1 }, onlyRemove: true); - Assert.That(state.SelectionCount(), Is.EqualTo(2)); - Assert.That(state.VertexWeights[1], Is.EqualTo(0)); - } - - [Test] - public void VertexSelectionState_Clone_IndependentCopy() - { - var mesh = CreateTriangleMesh(); - var selectable = CreateSelectable(mesh); - var state = new VertexSelectionState(selectable, 0); - state.ModifySelection(new[] { 0 }, onlyRemove: false); - - var clone = state.Clone() as VertexSelectionState; - clone.ModifySelection(new[] { 1 }, onlyRemove: false); - - Assert.That(state.SelectionCount(), Is.EqualTo(1)); - Assert.That(clone.SelectionCount(), Is.EqualTo(2)); - } - - [Test] - public void VertexSelectionState_EnsureSorted_RemovesDuplicatesAndSorts() - { - var mesh = CreateQuadMesh(); - var selectable = CreateSelectable(mesh); - var state = new VertexSelectionState(selectable, 0); - state.SelectedVertices = new List { 3, 1, 1, 2 }; - state.EnsureSorted(); - - Assert.That(state.SelectedVertices, Is.EqualTo(new List { 1, 2, 3 })); - } - - #endregion - - #region IntersectionMath - Face/Object with BoundingBox Pre-check - - [Test] - public void IntersectObject_ReturnNull_WhenRayMissesBoundingBox() - { - var mesh = CreateTriangleMesh(); - // Set a valid bounding box - typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, - new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); - - // Ray parallel, far away - var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); - var result = IntersectionMath.IntersectObject(ray, mesh, Matrix.Identity); - - Assert.That(result, Is.Null); - } - - [Test] - public void IntersectFace_ReturnNull_WhenRayMissesBoundingBox() - { - var mesh = CreateTriangleMesh(); - typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, - new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); - - var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); - var result = IntersectionMath.IntersectFace(ray, mesh, Matrix.Identity, out var face); - - Assert.That(result, Is.Null); - Assert.That(face, Is.Null); - } - - #endregion - - #region GeometrySelectionMode Enum - - [Test] - public void GeometrySelectionMode_ContainsEdge() - { - Assert.That(Enum.IsDefined(typeof(GeometrySelectionMode), GeometrySelectionMode.Edge), Is.True); - } - - #endregion - } -} diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexSelectionStateTests.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexSelectionStateTests.cs new file mode 100644 index 000000000..4cc83908c --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.CoreTest/Selection/VertexSelectionStateTests.cs @@ -0,0 +1,80 @@ +using GameWorld.Core.Components.Selection; +using GameWorld.Core.Test.TestUtility; + +namespace GameWorld.Core.Test.Selection +{ + [TestFixture] + public class VertexSelectionStateTests + { + [Test] + public void WeightsZeroFalloff_OnlySelectedAreWeighted() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var selectable = MeshTestHelper.CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + + state.ModifySelection(new[] { 1 }, onlyRemove: false); + + Assert.That(state.VertexWeights[0], Is.EqualTo(0)); + Assert.That(state.VertexWeights[1], Is.EqualTo(1)); + Assert.That(state.VertexWeights[2], Is.EqualTo(0)); + } + + [Test] + public void WeightsFalloff_NearbyVerticesGetWeight() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var selectable = MeshTestHelper.CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 2.0f); + + state.ModifySelection(new[] { 0 }, onlyRemove: false); + + Assert.That(state.VertexWeights[0], Is.EqualTo(1.0f)); + Assert.That(state.VertexWeights[1], Is.GreaterThan(0)); + Assert.That(state.VertexWeights[1], Is.LessThan(1)); + Assert.That(state.VertexWeights[2], Is.GreaterThan(0)); + } + + [Test] + public void ModifySelection_DeselectWorks() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var selectable = MeshTestHelper.CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + + state.ModifySelection(new[] { 0, 1, 2 }, onlyRemove: false); + Assert.That(state.SelectionCount(), Is.EqualTo(3)); + + state.ModifySelection(new[] { 1 }, onlyRemove: true); + Assert.That(state.SelectionCount(), Is.EqualTo(2)); + Assert.That(state.VertexWeights[1], Is.EqualTo(0)); + } + + [Test] + public void Clone_IndependentCopy() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var selectable = MeshTestHelper.CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + state.ModifySelection(new[] { 0 }, onlyRemove: false); + + var clone = state.Clone() as VertexSelectionState; + clone.ModifySelection(new[] { 1 }, onlyRemove: false); + + Assert.That(state.SelectionCount(), Is.EqualTo(1)); + Assert.That(clone.SelectionCount(), Is.EqualTo(2)); + } + + [Test] + public void EnsureSorted_RemovesDuplicatesAndSorts() + { + var mesh = MeshTestHelper.CreateQuadMesh(); + var selectable = MeshTestHelper.CreateSelectable(mesh); + var state = new VertexSelectionState(selectable, 0); + state.SelectedVertices = new List { 3, 1, 1, 2 }; + state.EnsureSorted(); + + Assert.That(state.SelectedVertices, Is.EqualTo(new List { 1, 2, 3 })); + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/TestUtility/MeshTestHelper.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/TestUtility/MeshTestHelper.cs new file mode 100644 index 000000000..54eac1a7f --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.CoreTest/TestUtility/MeshTestHelper.cs @@ -0,0 +1,49 @@ +using GameWorld.Core.Rendering; +using GameWorld.Core.Rendering.Geometry; +using GameWorld.Core.SceneNodes; +using Microsoft.Xna.Framework; +using Moq; + +namespace GameWorld.Core.Test.TestUtility +{ + public static class MeshTestHelper + { + public static MeshObject CreateTriangleMesh() + { + var contextFactory = new TestGeometryGraphicsContextFactory(); + var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); + mesh.VertexArray = new VertexPositionNormalTextureCustom[] + { + new() { Position = new Vector4(0, 0, 0, 1) }, + new() { Position = new Vector4(1, 0, 0, 1) }, + new() { Position = new Vector4(0, 1, 0, 1) }, + }; + mesh.IndexArray = new ushort[] { 0, 1, 2 }; + return mesh; + } + + public static MeshObject CreateQuadMesh() + { + var contextFactory = new TestGeometryGraphicsContextFactory(); + var mesh = new MeshObject(contextFactory.Create(), "test_skeleton"); + mesh.VertexArray = new VertexPositionNormalTextureCustom[] + { + new() { Position = new Vector4(0, 0, 0, 1) }, + new() { Position = new Vector4(1, 0, 0, 1) }, + new() { Position = new Vector4(1, 1, 0, 1) }, + new() { Position = new Vector4(0, 1, 0, 1) }, + }; + mesh.IndexArray = new ushort[] { 0, 1, 2, 0, 2, 3 }; + return mesh; + } + + public static ISelectable CreateSelectable(MeshObject mesh) + { + var mock = new Mock(); + mock.Setup(x => x.Geometry).Returns(mesh); + mock.Setup(x => x.RenderMatrix).Returns(Matrix.Identity); + mock.Setup(x => x.Name).Returns("TestMesh"); + return mock.Object; + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.CoreTest/Utility/IntersectionMathTests.cs b/GameWorld/GameWorldCore/GameWorld.CoreTest/Utility/IntersectionMathTests.cs new file mode 100644 index 000000000..cae5ef357 --- /dev/null +++ b/GameWorld/GameWorldCore/GameWorld.CoreTest/Utility/IntersectionMathTests.cs @@ -0,0 +1,115 @@ +using GameWorld.Core.Rendering.Geometry; +using GameWorld.Core.Test.TestUtility; +using GameWorld.Core.Utility; +using Microsoft.Xna.Framework; + +namespace GameWorld.Core.Test.Utility +{ + [TestFixture] + public class IntersectionMathTests + { + [Test] + public void IntersectVertex_ScreenSpace_FindsClosestVertex() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + var clipPos = Vector4.Transform(new Vector4(0, 0, 0, 1), viewProjection); + var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; + var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; + + var result = IntersectionMath.IntersectVertex( + new Vector2(screenX, screenY), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Not.Null); + Assert.That(selectedVertex, Is.EqualTo(0)); + } + + [Test] + public void IntersectVertex_ScreenSpace_ReturnsNull_WhenFarFromVertices() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + var result = IntersectionMath.IntersectVertex( + new Vector2(0, 0), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Null); + Assert.That(selectedVertex, Is.EqualTo(-1)); + } + + [Test] + public void IntersectVertex_ScreenSpace_SelectsCloserVertex() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + var viewProjection = Matrix.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up) * + Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.0f, 0.1f, 100f); + + var clipPos = Vector4.Transform(new Vector4(1, 0, 0, 1), viewProjection); + var screenX = (clipPos.X / clipPos.W + 1) * 0.5f * 800; + var screenY = (1 - clipPos.Y / clipPos.W) * 0.5f * 600; + + var result = IntersectionMath.IntersectVertex( + new Vector2(screenX, screenY), mesh, Matrix.Identity, + viewProjection, 800, 600, out var selectedVertex); + + Assert.That(result, Is.Not.Null); + Assert.That(selectedVertex, Is.EqualTo(1)); + } + + [Test] + public void TransformBoundingBox_AppliesTranslation() + { + var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); + var translation = Matrix.CreateTranslation(10, 0, 0); + + var result = IntersectionMath.TransformBoundingBox(box, translation); + + Assert.That(result.Min.X, Is.EqualTo(9).Within(0.001f)); + Assert.That(result.Max.X, Is.EqualTo(11).Within(0.001f)); + } + + [Test] + public void TransformBoundingBox_AppliesScale() + { + var box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); + var scale = Matrix.CreateScale(2); + + var result = IntersectionMath.TransformBoundingBox(box, scale); + + Assert.That(result.Min.X, Is.EqualTo(-2).Within(0.001f)); + Assert.That(result.Max.X, Is.EqualTo(2).Within(0.001f)); + } + + [Test] + public void IntersectObject_ReturnNull_WhenRayMissesBoundingBox() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, + new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); + + var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); + var result = IntersectionMath.IntersectObject(ray, mesh, Matrix.Identity); + + Assert.That(result, Is.Null); + } + + [Test] + public void IntersectFace_ReturnNull_WhenRayMissesBoundingBox() + { + var mesh = MeshTestHelper.CreateTriangleMesh(); + typeof(MeshObject).GetProperty("BoundingBox")?.SetValue(mesh, + new BoundingBox(new Vector3(-0.1f, -0.1f, -0.1f), new Vector3(1.1f, 1.1f, 0.1f))); + + var ray = new Ray(new Vector3(100, 100, 5), new Vector3(0, 0, -1)); + var result = IntersectionMath.IntersectFace(ray, mesh, Matrix.Identity, out var face); + + Assert.That(result, Is.Null); + Assert.That(face, Is.Null); + } + } +} From d974cc42cb06d999a82ba87123e68a70ac86e2dc Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 10:33:57 +0200 Subject: [PATCH 5/6] Code --- .../Components/SceneInformationComponent.cs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs index d53920cf5..1fe5b8991 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs @@ -1,4 +1,5 @@ using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Components.Selection; using GameWorld.Core.Rendering.RenderItems; using GameWorld.Core.SceneNodes; using Microsoft.Xna.Framework; @@ -10,16 +11,18 @@ public class SceneInformationComponent : BaseComponent private TimeSpan _timeElapsed; private readonly RenderEngineComponent _renderEngineComponent; private readonly SceneManager _sceneManager; + private readonly SelectionManager _selectionManager; // Cached scene statistics (updated once per second) private int _objectCount; private int _vertexCount; private int _faceCount; - public SceneInformationComponent(RenderEngineComponent renderEngineComponent, SceneManager sceneManager) + public SceneInformationComponent(RenderEngineComponent renderEngineComponent, SceneManager sceneManager, SelectionManager selectionManager) { _renderEngineComponent = renderEngineComponent; _sceneManager = sceneManager; + _selectionManager = selectionManager; } public override void Update(GameTime gameTime) @@ -45,8 +48,57 @@ public override void Update(GameTime gameTime) public override void Draw(GameTime gameTime) { - var statsItem = new FontRenderItem(_renderEngineComponent, $"Objects: {_objectCount} Verts: {_vertexCount} Faces: {_faceCount}", new Vector2(5, 25), Color.LightGray); + var statsText = BuildStatsText(); + var statsItem = new FontRenderItem(_renderEngineComponent, statsText, new Vector2(5, 25), Color.LightGray); _renderEngineComponent.AddRenderItem(RenderBuckedId.Font, statsItem); } + + private string BuildStatsText() + { + var selectionState = _selectionManager.GetState(); + + if (selectionState is ObjectSelectionState objectState && objectState.SelectionCount() > 0) + { + var selectedObjects = objectState.CurrentSelection(); + var selectedVerts = 0; + var selectedFaces = 0; + + foreach (var selectable in selectedObjects) + { + if (selectable?.Geometry == null) + continue; + + selectedVerts += selectable.Geometry.VertexCount(); + selectedFaces += selectable.Geometry.IndexArray.Length / 3; + } + + return $"Selected Objects: {selectedObjects.Count} Verts: {selectedVerts} Faces: {selectedFaces}"; + } + + if (selectionState is FaceSelectionState faceState && faceState.SelectionCount() > 0 && faceState.RenderObject?.Geometry != null) + { + var geometry = faceState.RenderObject.Geometry; + var uniqueVerts = new HashSet(); + + foreach (var faceStartIndex in faceState.SelectedFaces) + { + if (faceStartIndex < 0 || faceStartIndex + 2 >= geometry.IndexArray.Length) + continue; + + uniqueVerts.Add(geometry.IndexArray[faceStartIndex]); + uniqueVerts.Add(geometry.IndexArray[faceStartIndex + 1]); + uniqueVerts.Add(geometry.IndexArray[faceStartIndex + 2]); + } + + return $"Selected Faces: {faceState.SelectedFaces.Count} Verts: {uniqueVerts.Count} Objects: 1"; + } + + if (selectionState is VertexSelectionState vertexState && vertexState.SelectionCount() > 0) + { + return $"Selected Vertices: {vertexState.SelectedVertices.Count} Objects: 1"; + } + + return $"Objects: {_objectCount} Verts: {_vertexCount} Faces: {_faceCount}"; + } } } From c7cf6f5a70f8dcf976f4b827566a173a78fd7616 Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Thu, 23 Apr 2026 11:32:41 +0200 Subject: [PATCH 6/6] Vertical menubar --- .../KitbasherEditor/Core/KitbasherView.xaml | 47 ++++++++++++++++++- .../Core/KitbasherView.xaml.cs | 21 +++++++++ .../Core/MenuBarViews/MenuBarViewModel.cs | 40 ++++++++++------ 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/Editors/Kitbashing/KitbasherEditor/Core/KitbasherView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/KitbasherView.xaml index a0cf3713a..6023c5ef3 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/KitbasherView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/KitbasherView.xaml @@ -9,6 +9,7 @@ xmlns:b="http://schemas.microsoft.com/xaml/behaviors" xmlns:behaviors="clr-namespace:Shared.Ui.Common.Behaviors;assembly=Shared.Ui" xmlns:loc="clr-namespace:Shared.Ui.Common;assembly=Shared.Ui" + xmlns:menusystem="clr-namespace:Shared.Ui.Common.MenuSystem;assembly=Shared.Ui" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" AllowDrop="True"> @@ -43,11 +44,53 @@ - + + + + + + + + + + + + + + + + + + + + + + + + - + + + MenuItems { get; set; } = new ObservableCollection(); public ObservableCollection CustomButtons { get; set; } = new ObservableCollection(); + public ObservableCollection SidebarButtons { get; set; } = new ObservableCollection(); public TransformToolViewModel TransformTool { get; set; } private readonly IUiCommandFactory _uiCommandFactory; @@ -43,6 +44,7 @@ public MenuBarViewModel(CommandExecutor commandExecutor, IEventHub eventHub, Men RegisterActions(); RegisterHotkeys(); CustomButtons = CreateButtons(); + SidebarButtons = CreateVerticalButtons(); MenuItems = CreateToolbarMenu(); eventHub.Register(this, OnUndoStackChanged); @@ -148,20 +150,6 @@ ObservableCollection CreateButtons() builder.CreateButton(IconLibrary.UndoIcon); builder.CreateButtonSeparator(); - // Gizmo buttons - builder.CreateGroupedButton("Gizmo", true, IconLibrary.Gizmo_CursorIcon); - builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_MoveIcon); - builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_RotateIcon); - builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_ScaleIcon); - builder.CreateButtonSeparator(); - - // Selection buttons - builder.CreateGroupedButton("SelectionMode", true, IconLibrary.Selection_Object_Icon); - builder.CreateGroupedButton("SelectionMode", false, IconLibrary.Selection_Face_Icon); - builder.CreateGroupedButton("SelectionMode", false, IconLibrary.Selection_Vertex_Icon); - builder.CreateButton(IconLibrary.ViewSelectedIcon); - builder.CreateButtonSeparator(); - // Object buttons builder.CreateButton(IconLibrary.DivideIntoSubMeshIcon, ButtonVisibilityRule.ObjectMode); builder.CreateButton(IconLibrary.MergeMeshIcon, ButtonVisibilityRule.ObjectMode); @@ -189,6 +177,27 @@ ObservableCollection CreateButtons() return builder.Build(); } + + ObservableCollection CreateVerticalButtons() + { + var builder = new ButtonBuilder(_uiCommands); + + // Gizmo buttons + builder.CreateGroupedButton("Gizmo", true, IconLibrary.Gizmo_CursorIcon); + builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_MoveIcon); + builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_RotateIcon); + builder.CreateGroupedButton("Gizmo", false, IconLibrary.Gizmo_ScaleIcon); + builder.CreateButtonSeparator(); + + // Selection buttons + builder.CreateGroupedButton("SelectionMode", true, IconLibrary.Selection_Object_Icon); + builder.CreateGroupedButton("SelectionMode", false, IconLibrary.Selection_Face_Icon); + builder.CreateGroupedButton("SelectionMode", false, IconLibrary.Selection_Vertex_Icon); + builder.CreateButton(IconLibrary.ViewSelectedIcon); + + return builder.Build(); + } + void RegisterUiCommand() where T : IKitbasherUiCommand { if (_uiCommands.ContainsKey(typeof(T))) @@ -244,6 +253,9 @@ void OnSelectionChanged(SelectionChangedEvent notification) foreach (var button in CustomButtons) _menuItemVisibilityRuleEngine.Validate(button); + foreach (var button in SidebarButtons) + _menuItemVisibilityRuleEngine.Validate(button); + // Validate if menu action is enabled foreach (var action in _uiCommands.Values) _menuItemVisibilityRuleEngine.Validate(action);