gpu picking and qol

This commit is contained in:
bird_egop
2026-06-10 02:01:41 +03:00
parent fa05aba9ba
commit 4d0d1aca12
5 changed files with 237 additions and 108 deletions
+35
View File
@@ -14,6 +14,8 @@ public sealed class MeshInspectorPanel : IImGuiPanel
{ {
private readonly MeshViewportViewModel _viewModel; private readonly MeshViewportViewModel _viewModel;
private readonly HashSet<int> _expandedPieceDetails = []; private readonly HashSet<int> _expandedPieceDetails = [];
private readonly HashSet<int> _pieceIdsToAutoOpen = [];
private int _lastSyncedSelectedPieceId = -1;
private static readonly string[] ModelStateLabels = ["Обычное", "Collapsed", "Неизвестное 2"]; private static readonly string[] ModelStateLabels = ["Обычное", "Collapsed", "Неизвестное 2"];
private static readonly string[] LodLabels = ["LOD 0", "LOD -1", "LOD -2", "LOD -3", "LOD -4"]; private static readonly string[] LodLabels = ["LOD 0", "LOD -1", "LOD -2", "LOD -3", "LOD -4"];
@@ -116,6 +118,8 @@ public sealed class MeshInspectorPanel : IImGuiPanel
.GroupBy(x => x.ParentId) .GroupBy(x => x.ParentId)
.ToDictionary(x => x.Key, x => x.OrderBy(p => p.Id).ToList()); .ToDictionary(x => x.Key, x => x.OrderBy(p => p.Id).ToList());
SyncTreeExpansionToSelection(document);
// Parent -1 приходит из 0xFFFF в 0x01 и означает корневой piece. // Parent -1 приходит из 0xFFFF в 0x01 и означает корневой piece.
if (childrenByParent.TryGetValue(-1, out var roots)) if (childrenByParent.TryGetValue(-1, out var roots))
{ {
@@ -146,6 +150,9 @@ public sealed class MeshInspectorPanel : IImGuiPanel
var childMarker = childCount > 0 ? $" | дочерних: {childCount}" : ""; var childMarker = childCount > 0 ? $" | дочерних: {childCount}" : "";
var hiddenMarker = hidden ? " | скрыта" : ""; var hiddenMarker = hidden ? " | скрыта" : "";
var label = $"{piece.Name}{childMarker}{hiddenMarker}##piece_{piece.Id}"; var label = $"{piece.Name}{childMarker}{hiddenMarker}##piece_{piece.Id}";
if (_pieceIdsToAutoOpen.Remove(piece.Id))
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
var open = ImGui.TreeNodeEx(label, flags); var open = ImGui.TreeNodeEx(label, flags);
var nodeClicked = ImGui.IsItemClicked(); var nodeClicked = ImGui.IsItemClicked();
if (nodeClicked) if (nodeClicked)
@@ -194,6 +201,34 @@ public sealed class MeshInspectorPanel : IImGuiPanel
} }
} }
private void SyncTreeExpansionToSelection(MeshDocument document)
{
var selectedPieceId = _viewModel.RenderState.SelectedPieceId;
if (selectedPieceId == _lastSyncedSelectedPieceId)
return;
_lastSyncedSelectedPieceId = selectedPieceId;
_pieceIdsToAutoOpen.Clear();
if (selectedPieceId < 0)
return;
var piecesById = document.Pieces.ToDictionary(x => x.Id);
if (!piecesById.TryGetValue(selectedPieceId, out var piece))
return;
// ImGui не раскрывает родителей выбранного leaf автоматически, поэтому раскрываем только путь до него.
var visited = new HashSet<int>();
while (piece.ParentId != -1 && piecesById.TryGetValue(piece.ParentId, out var parent))
{
if (!visited.Add(parent.Id))
break;
_pieceIdsToAutoOpen.Add(parent.Id);
piece = parent;
}
}
private void DrawPieceDetailsToggle(MeshPieceInfo piece) private void DrawPieceDetailsToggle(MeshPieceInfo piece)
{ {
var detailsOpen = _expandedPieceDetails.Contains(piece.Id); var detailsOpen = _expandedPieceDetails.Contains(piece.Id);
+1
View File
@@ -76,6 +76,7 @@ public sealed class ViewportPanel : IImGuiPanel
DrawViewportHelpOverlay(); DrawViewportHelpOverlay();
_inputController.Handle( _inputController.Handle(
_renderer,
_scene, _scene,
_camera, _camera,
viewportHovered, viewportHovered,
@@ -12,6 +12,7 @@ public sealed class ViewportInputController
private bool _isPanningViewport; private bool _isPanningViewport;
public void Handle( public void Handle(
ViewportRenderer renderer,
ViewportScene scene, ViewportScene scene,
ViewportCamera camera, ViewportCamera camera,
bool viewportHovered, bool viewportHovered,
@@ -59,11 +60,12 @@ public sealed class ViewportInputController
var mouse = ImGui.GetMousePos(); var mouse = ImGui.GetMousePos();
var localMouse = mouse - imageMin; var localMouse = mouse - imageMin;
scene.SelectedPieceId = ViewportSelection.PickPiece( scene.SelectedPieceId = renderer.PickPiece(
Math.Max(1, (int)imageSize.X),
Math.Max(1, (int)imageSize.Y),
scene, scene,
camera, camera,
localMouse, localMouse);
imageSize);
} }
if (viewportFocused || viewportHovered) if (viewportFocused || viewportHovered)
@@ -15,16 +15,25 @@ public sealed class ViewportRenderer
public GL Gl => _gl; public GL Gl => _gl;
private ShaderProgram? _meshShader; private ShaderProgram? _meshShader;
private ShaderProgram? _pickingShader;
private GpuMesh? _unitWireBoxMesh; private GpuMesh? _unitWireBoxMesh;
private GpuMesh? _axesMesh; private GpuMesh? _axesMesh;
private uint _pickingFramebuffer;
private uint _pickingColorTexture;
private uint _pickingDepthRenderbuffer;
private int _pickingWidth;
private int _pickingHeight;
private int _modelLocation; private int _modelLocation;
private int _mvpLocation; private int _mvpLocation;
private int _lightDirectionLocation; private int _lightDirectionLocation;
private int _useTextureLocation; private int _useTextureLocation;
private int _texture0Location; private int _texture0Location;
private int _colorAddLocation; private int _colorAddLocation;
private int _pickingMvpLocation;
private int _pickingColorLocation;
private bool _initialized; private bool _initialized;
@@ -79,6 +88,58 @@ public sealed class ViewportRenderer
return _framebuffer.ColorTexture; return _framebuffer.ColorTexture;
} }
/// <summary>
/// Делает GPU picking: рендерит pieces в служебный framebuffer уникальными цветами и читает один пиксель.
/// Так выбор совпадает с видимой геометрией и не требует CPU-копии вершин.
/// </summary>
public unsafe int PickPiece(int width, int height, ViewportScene scene, ViewportCamera camera, Vector2 localMouse)
{
EnsureInitialized();
if (_pickingShader == null || width <= 1 || height <= 1)
return -1;
var x = Math.Clamp((int)localMouse.X, 0, width - 1);
var y = Math.Clamp(height - 1 - (int)localMouse.Y, 0, height - 1);
EnsurePickingFramebuffer(width, height);
try
{
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _pickingFramebuffer);
_gl.Viewport(0, 0, (uint)width, (uint)height);
_gl.Enable(EnableCap.DepthTest);
_gl.Disable(EnableCap.CullFace);
_gl.Disable(EnableCap.Blend);
_gl.Disable(EnableCap.StencilTest);
_gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
_gl.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
_gl.Clear((uint)(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit));
var aspect = width / (float)Math.Max(1, height);
var sceneRotation = camera.GetSceneRotationMatrix();
var view = camera.GetViewMatrix();
var projection = camera.GetProjectionMatrix(aspect);
_pickingShader.Use();
foreach (var piece in scene.Pieces)
DrawPieceForPicking(piece, sceneRotation, view, projection);
Span<byte> pixel = stackalloc byte[4];
fixed (byte* pixelPtr = pixel)
{
_gl.ReadPixels(x, y, 1, 1, PixelFormat.Rgba, PixelType.UnsignedByte, pixelPtr);
}
var encoded = pixel[0] | (pixel[1] << 8) | (pixel[2] << 16);
return encoded == 0 ? -1 : encoded - 1;
}
finally
{
RestoreDefaultRenderTargetState();
}
}
private void DrawGrid( private void DrawGrid(
ViewportScene scene, ViewportScene scene,
Matrix4x4 sceneRotation, Matrix4x4 sceneRotation,
@@ -187,6 +248,27 @@ public sealed class ViewportRenderer
DrawMesh(mesh, model, view, projection, colorAdd); DrawMesh(mesh, model, view, projection, colorAdd);
} }
private void DrawPieceForPicking(
ViewportPiece piece,
Matrix4x4 sceneRotation,
Matrix4x4 view,
Matrix4x4 projection)
{
if (_pickingShader == null)
throw new InvalidOperationException("Viewport picking shader is not initialized.");
var encodedColor = EncodePickingColor(piece.Id);
var model = piece.LocalTransform * sceneRotation;
var mvp = model * view * projection;
_pickingShader.SetMatrix4(_pickingMvpLocation, mvp);
_pickingShader.SetVector4(_pickingColorLocation, encodedColor);
// Рисуем теми же VAO, что и обычный viewport. Depth buffer сам выберет ближайшую piece под курсором.
foreach (var mesh in piece.Meshes)
mesh.Draw();
}
private void DrawMesh( private void DrawMesh(
GpuMesh mesh, GpuMesh mesh,
Matrix4x4 model, Matrix4x4 model,
@@ -231,6 +313,90 @@ public sealed class ViewportRenderer
return Matrix4x4.CreateScale(size * 0.5f) * Matrix4x4.CreateTranslation(center); return Matrix4x4.CreateScale(size * 0.5f) * Matrix4x4.CreateTranslation(center);
} }
private unsafe void EnsurePickingFramebuffer(int width, int height)
{
width = Math.Max(1, width);
height = Math.Max(1, height);
if (_pickingFramebuffer != 0 && _pickingWidth == width && _pickingHeight == height)
return;
_pickingWidth = width;
_pickingHeight = height;
if (_pickingFramebuffer == 0)
{
_pickingFramebuffer = _gl.GenFramebuffer();
_pickingColorTexture = _gl.GenTexture();
_pickingDepthRenderbuffer = _gl.GenRenderbuffer();
}
_gl.BindTexture(TextureTarget.Texture2D, _pickingColorTexture);
_gl.TexImage2D(
TextureTarget.Texture2D,
0,
InternalFormat.Rgba8,
(uint)width,
(uint)height,
0,
PixelFormat.Rgba,
PixelType.UnsignedByte,
null);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _pickingDepthRenderbuffer);
_gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer, InternalFormat.DepthComponent24, (uint)width, (uint)height);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _pickingFramebuffer);
_gl.FramebufferTexture2D(
FramebufferTarget.Framebuffer,
FramebufferAttachment.ColorAttachment0,
TextureTarget.Texture2D,
_pickingColorTexture,
0);
_gl.FramebufferRenderbuffer(
FramebufferTarget.Framebuffer,
FramebufferAttachment.DepthAttachment,
RenderbufferTarget.Renderbuffer,
_pickingDepthRenderbuffer);
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (status != GLEnum.FramebufferComplete)
throw new InvalidOperationException($"Viewport picking framebuffer is incomplete: {status}");
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
_gl.BindTexture(TextureTarget.Texture2D, 0);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
}
private void RestoreDefaultRenderTargetState()
{
_gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill);
_gl.Disable(EnableCap.DepthTest);
_gl.Disable(EnableCap.StencilTest);
_gl.UseProgram(0);
_gl.BindVertexArray(0);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
var framebufferSize = _window.FramebufferSize;
_gl.Viewport(0, 0, (uint)framebufferSize.X, (uint)framebufferSize.Y);
}
private static Vector4 EncodePickingColor(int pieceId)
{
var encoded = pieceId + 1;
if (encoded <= 0 || encoded > 0x00FFFFFF)
return Vector4.Zero;
var r = (encoded & 0x0000FF) / 255.0f;
var g = ((encoded >> 8) & 0x0000FF) / 255.0f;
var b = ((encoded >> 16) & 0x0000FF) / 255.0f;
return new Vector4(r, g, b, 1.0f);
}
private void EnsureInitialized() private void EnsureInitialized()
{ {
if (_initialized) if (_initialized)
@@ -304,7 +470,34 @@ public sealed class ViewportRenderer
} }
"""; """;
const string pickingVertexShaderSource = """
#version 330 core
layout (location = 0) in vec3 aPosition;
uniform mat4 uMvp;
void main()
{
gl_Position = uMvp * vec4(aPosition, 1.0);
}
""";
const string pickingFragmentShaderSource = """
#version 330 core
uniform vec4 uPickColor;
out vec4 FragColor;
void main()
{
FragColor = uPickColor;
}
""";
_meshShader = new ShaderProgram(_gl, meshVertexShaderSource, meshFragmentShaderSource, "Viewport mesh"); _meshShader = new ShaderProgram(_gl, meshVertexShaderSource, meshFragmentShaderSource, "Viewport mesh");
_pickingShader = new ShaderProgram(_gl, pickingVertexShaderSource, pickingFragmentShaderSource,
"Viewport picking");
_modelLocation = _meshShader.GetUniformLocation("uModel"); _modelLocation = _meshShader.GetUniformLocation("uModel");
_mvpLocation = _meshShader.GetUniformLocation("uMvp"); _mvpLocation = _meshShader.GetUniformLocation("uMvp");
@@ -312,5 +505,8 @@ public sealed class ViewportRenderer
_useTextureLocation = _meshShader.GetUniformLocation("uUseTexture"); _useTextureLocation = _meshShader.GetUniformLocation("uUseTexture");
_texture0Location = _meshShader.GetUniformLocation("uTexture0"); _texture0Location = _meshShader.GetUniformLocation("uTexture0");
_colorAddLocation = _meshShader.GetUniformLocation("uColorAdd"); _colorAddLocation = _meshShader.GetUniformLocation("uColorAdd");
_pickingMvpLocation = _pickingShader.GetUniformLocation("uMvp");
_pickingColorLocation = _pickingShader.GetUniformLocation("uPickColor");
} }
} }
@@ -1,105 +0,0 @@
using System.Numerics;
namespace NResUI.Rendering.Viewport;
public static class ViewportSelection
{
public static int PickPiece(
ViewportScene scene,
ViewportCamera camera,
Vector2 localMouse,
Vector2 viewportSize)
{
if (viewportSize.X <= 1.0f || viewportSize.Y <= 1.0f)
return -1;
var aspect = viewportSize.X / viewportSize.Y;
var fovRadians = ViewportCamera.ToRadians(ViewportCamera.FieldOfViewDegrees);
var tanHalfFov = MathF.Tan(fovRadians * 0.5f);
var ndcX = (2.0f * localMouse.X / viewportSize.X) - 1.0f;
var ndcY = 1.0f - (2.0f * localMouse.Y / viewportSize.Y);
var rayOriginWorld = camera.GetEyePosition();
var rayDirectionWorld = Vector3.Normalize(
camera.GetForwardDirection() +
camera.GetRightDirection() * (ndcX * aspect * tanHalfFov) +
camera.GetUpDirection() * (ndcY * tanHalfFov));
var bestPieceId = -1;
var bestDistance = float.PositiveInfinity;
foreach (var piece in scene.Pieces)
{
if (!Matrix4x4.Invert(piece.LocalTransform, out var inverseModel))
continue;
var rayOriginLocal = Vector3.Transform(rayOriginWorld, inverseModel);
var rayDirectionLocal = Vector3.Normalize(
Vector3.TransformNormal(rayDirectionWorld, inverseModel));
if (IntersectRayAabb(
rayOriginLocal,
rayDirectionLocal,
piece.BoundsMin,
piece.BoundsMax,
out var hitDistance) &&
hitDistance < bestDistance)
{
bestDistance = hitDistance;
bestPieceId = piece.Id;
}
}
return bestPieceId;
}
private static bool IntersectRayAabb(
Vector3 rayOrigin,
Vector3 rayDirection,
Vector3 boundsMin,
Vector3 boundsMax,
out float hitDistance)
{
hitDistance = 0.0f;
var tMin = 0.0f;
var tMax = float.PositiveInfinity;
if (!IntersectSlab(rayOrigin.X, rayDirection.X, boundsMin.X, boundsMax.X, ref tMin, ref tMax) ||
!IntersectSlab(rayOrigin.Y, rayDirection.Y, boundsMin.Y, boundsMax.Y, ref tMin, ref tMax) ||
!IntersectSlab(rayOrigin.Z, rayDirection.Z, boundsMin.Z, boundsMax.Z, ref tMin, ref tMax))
{
return false;
}
hitDistance = tMin;
return true;
}
private static bool IntersectSlab(
float origin,
float direction,
float min,
float max,
ref float tMin,
ref float tMax)
{
const float epsilon = 1e-6f;
if (MathF.Abs(direction) < epsilon)
return origin >= min && origin <= max;
var invDirection = 1.0f / direction;
var t1 = (min - origin) * invDirection;
var t2 = (max - origin) * invDirection;
if (t1 > t2)
(t1, t2) = (t2, t1);
tMin = MathF.Max(tMin, t1);
tMax = MathF.Min(tMax, t2);
return tMin <= tMax;
}
}