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/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);
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/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/SceneInformationComponent.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs
new file mode 100644
index 000000000..1fe5b8991
--- /dev/null
+++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/SceneInformationComponent.cs
@@ -0,0 +1,104 @@
+using GameWorld.Core.Components.Rendering;
+using GameWorld.Core.Components.Selection;
+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;
+ 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, SelectionManager selectionManager)
+ {
+ _renderEngineComponent = renderEngineComponent;
+ _sceneManager = sceneManager;
+ _selectionManager = selectionManager;
+ }
+
+ 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 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}";
+ }
+ }
+}
diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/ISelectionState.cs
index 72023831d..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
{
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 b5f41c68c..bb8f956ca 100644
--- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs
+++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionComponent.cs
@@ -192,7 +192,10 @@ 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;
diff --git a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs
index a74394de8..a157d2c04 100644
--- a/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs
+++ b/GameWorld/GameWorldCore/GameWorld.Core/Components/Selection/SelectionManager.cs
@@ -1,5 +1,4 @@
-using System;
-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;
@@ -13,7 +12,7 @@ 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
@@ -25,11 +24,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 readonly EdgeData[] _edgeDataCache = new EdgeData[MaxRenderEdges];
+
public SelectionManager(IEventHub eventHub, RenderEngineComponent renderEngine, IScopedResourceLibrary resourceLib, IDeviceResolver deviceResolverComponent, IGraphicsResourceCreator graphicsResourceCreator)
{
_eventHub = eventHub;
@@ -44,9 +57,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);
@@ -65,27 +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.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;
@@ -98,7 +101,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 +114,7 @@ public void SetState(ISelectionState state)
private void SelectionManager_SelectionChanged(ISelectionState state, bool sendEvent)
{
+ _edgeDataDirty = true;
_eventHub.Publish(new SelectionChangedEvent { NewState = state });
}
@@ -131,8 +140,56 @@ 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 BoneSelectionState selectionBoneState && selectionBoneState.RenderObject != null)
@@ -149,9 +206,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 +241,12 @@ public void Dispose()
_vertexRenderer = null;
}
+ if (_edgeQuadRenderer != null)
+ {
+ _edgeQuadRenderer.Dispose();
+ _edgeQuadRenderer = null;
+ }
+
_currentState?.Clear();
_currentState = null;
}
@@ -198,6 +258,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/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());
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/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);
+ }
+ }
+}