gl viewport

This commit is contained in:
bird_egop
2026-06-05 20:33:21 +03:00
parent 97dc14c36c
commit 195e6d71bd
13 changed files with 1140 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
using System.Numerics;
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Rendering.Viewport;
using NResUI.Rendering.Viewport.Meshes;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace NResUI.ImGuiUI;
public sealed class ViewportPanel : IImGuiPanel
{
private readonly ViewportRenderer _renderer;
private readonly ViewportScene _scene;
private readonly ViewportCamera _camera = new();
private readonly ViewportInputController _inputController = new();
public ViewportPanel(GL gl, IWindow window)
{
var cubeMesh = PrimitiveMeshes.CreateCube(gl);
var gridMesh = PrimitiveMeshes.CreateWorldGrid(gl);
_scene = ViewportScene.CreateDefaultCubeScene(cubeMesh, gridMesh);
_renderer = new ViewportRenderer(gl, window);
}
public void OnImGuiRender()
{
if (!ImGui.Begin("Viewport"))
{
ImGui.End();
return;
}
DrawSelectionStatus();
DrawGridToggle();
var imageSize = ImGui.GetContentRegionAvail();
if (imageSize.X < 32 || imageSize.Y < 32)
{
ImGui.TextDisabled("Viewport is too small.");
ImGui.End();
return;
}
var textureId = _renderer.Render(
width: Math.Max(1, (int)imageSize.X),
height: Math.Max(1, (int)imageSize.Y),
scene: _scene,
camera: _camera);
ImGui.Image(
(IntPtr)textureId,
imageSize,
new Vector2(0, 1),
new Vector2(1, 0));
var viewportHovered = ImGui.IsItemHovered();
var imageMin = ImGui.GetItemRectMin();
DrawViewportHelpOverlay();
_inputController.Handle(
_scene,
_camera,
viewportHovered,
imageMin,
imageSize);
ImGui.End();
}
private void DrawSelectionStatus()
{
var selectedPiece = _scene.SelectedPiece;
if (selectedPiece != null)
ImGui.Text($"Selected: {selectedPiece.Name}");
else
ImGui.TextDisabled("Selected: none");
}
private void DrawGridToggle()
{
var grid = _scene.Grid;
if (grid == null)
return;
var isVisible = grid.IsVisible;
if (ImGui.Checkbox("Show grid", ref isVisible))
grid.IsVisible = isVisible;
}
private static void DrawViewportHelpOverlay()
{
var drawList = ImGui.GetWindowDrawList();
var imageMin = ImGui.GetItemRectMin();
var imageMax = ImGui.GetItemRectMax();
const string helpText = "LMB click: select / deselect\nMMB drag: rotate\nMouse wheel: zoom";
var textSize = ImGui.CalcTextSize(helpText);
var padding = new Vector2(8.0f, 6.0f);
var boxMin = new Vector2(
imageMin.X + 8.0f,
imageMax.Y - textSize.Y - padding.Y * 2.0f - 8.0f);
var boxMax = new Vector2(
boxMin.X + textSize.X + padding.X * 2.0f,
boxMin.Y + textSize.Y + padding.Y * 2.0f);
drawList.AddRectFilled(
boxMin,
boxMax,
ImGui.GetColorU32(new Vector4(0.0f, 0.0f, 0.0f, 0.45f)),
4.0f);
drawList.AddText(
boxMin + padding,
ImGui.GetColorU32(new Vector4(1.0f, 1.0f, 1.0f, 0.9f)),
helpText);
}
}
+1
View File
@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType Condition="'$(OS)' == 'Windows_NT'">WinExe</OutputType> <OutputType Condition="'$(OS)' == 'Windows_NT'">WinExe</OutputType>
<OutputType Condition="'$(OS)' != 'Windows_NT'">Exe</OutputType> <OutputType Condition="'$(OS)' != 'Windows_NT'">Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -0,0 +1,36 @@
using Silk.NET.OpenGL;
namespace NResUI.Rendering.Viewport.Meshes;
public sealed class GpuMesh
{
private readonly GL _gl;
public uint VertexArrayObject { get; }
public uint VertexBufferObject { get; }
public uint ElementBufferObject { get; }
public uint IndexCount { get; }
public PrimitiveType PrimitiveType { get; }
public GpuMesh(
GL gl,
uint vertexArrayObject,
uint vertexBufferObject,
uint elementBufferObject,
uint indexCount,
PrimitiveType primitiveType = PrimitiveType.Triangles)
{
_gl = gl;
VertexArrayObject = vertexArrayObject;
VertexBufferObject = vertexBufferObject;
ElementBufferObject = elementBufferObject;
IndexCount = indexCount;
PrimitiveType = primitiveType;
}
public void Draw()
{
_gl.BindVertexArray(VertexArrayObject);
_gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null);
}
}
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using Silk.NET.OpenGL;
namespace NResUI.Rendering.Viewport.Meshes;
public static unsafe class PrimitiveMeshes
{
public static GpuMesh CreateCube(GL gl)
{
// position.xyz, color.rgb
float[] vertices =
{
// front
-1, -1, 1, 1, 0, 0,
1, -1, 1, 0, 1, 0,
1, 1, 1, 0, 0, 1,
-1, 1, 1, 1, 1, 0,
// back
-1, -1, -1, 1, 0, 1,
1, -1, -1, 0, 1, 1,
1, 1, -1, 1, 1, 1,
-1, 1, -1, 0.5f, 0.5f, 0.5f,
};
uint[] indices =
{
// front
0, 1, 2,
2, 3, 0,
// right
1, 5, 6,
6, 2, 1,
// back
5, 4, 7,
7, 6, 5,
// left
4, 0, 3,
3, 7, 4,
// top
3, 2, 6,
6, 7, 3,
// bottom
4, 5, 1,
1, 0, 4,
};
return CreateIndexedMesh(gl, vertices, indices, PrimitiveType.Triangles);
}
public static GpuMesh CreateWorldGrid(
GL gl,
int halfExtent = 20,
int minorStep = 1,
int majorStep = 5)
{
if (halfExtent <= 0)
throw new ArgumentOutOfRangeException(nameof(halfExtent));
if (minorStep <= 0)
throw new ArgumentOutOfRangeException(nameof(minorStep));
if (majorStep <= 0)
throw new ArgumentOutOfRangeException(nameof(majorStep));
var vertices = new List<float>();
var indices = new List<uint>();
void AddVertex(float x, float y, float z, float r, float g, float b)
{
vertices.Add(x);
vertices.Add(y);
vertices.Add(z);
vertices.Add(r);
vertices.Add(g);
vertices.Add(b);
}
void AddLine(
float x0, float y0, float z0,
float x1, float y1, float z1,
float r, float g, float b)
{
var startIndex = (uint)(vertices.Count / 6);
AddVertex(x0, y0, z0, r, g, b);
AddVertex(x1, y1, z1, r, g, b);
indices.Add(startIndex);
indices.Add(startIndex + 1);
}
const float y = 0.0f;
for (var i = -halfExtent; i <= halfExtent; i += minorStep)
{
var isAxis = i == 0;
var isMajor = i % majorStep == 0;
var brightness = isMajor ? 0.38f : 0.24f;
var r = brightness;
var g = brightness;
var b = brightness;
// Lines parallel to X. The Z axis itself is blue.
if (isAxis)
{
AddLine(-halfExtent, y, i, halfExtent, y, i, 0.25f, 0.45f, 1.0f);
}
else
{
AddLine(-halfExtent, y, i, halfExtent, y, i, r, g, b);
}
// Lines parallel to Z. The X axis itself is red.
if (isAxis)
{
AddLine(i, y, -halfExtent, i, y, halfExtent, 1.0f, 0.25f, 0.25f);
}
else
{
AddLine(i, y, -halfExtent, i, y, halfExtent, r, g, b);
}
}
return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), PrimitiveType.Lines);
}
private static GpuMesh CreateIndexedMesh(
GL gl,
float[] vertices,
uint[] indices,
PrimitiveType primitiveType)
{
var vao = gl.GenVertexArray();
var vbo = gl.GenBuffer();
var ebo = gl.GenBuffer();
gl.BindVertexArray(vao);
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
fixed (float* vertexPtr = vertices)
{
gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint)(vertices.Length * sizeof(float)),
vertexPtr,
BufferUsageARB.StaticDraw);
}
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
fixed (uint* indexPtr = indices)
{
gl.BufferData(
BufferTargetARB.ElementArrayBuffer,
(nuint)(indices.Length * sizeof(uint)),
indexPtr,
BufferUsageARB.StaticDraw);
}
const uint stride = 6 * sizeof(float);
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(
0,
3,
VertexAttribPointerType.Float,
false,
stride,
null);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(
1,
3,
VertexAttribPointerType.Float,
false,
stride,
(void*)(3 * sizeof(float)));
gl.BindVertexArray(0);
return new GpuMesh(gl, vao, vbo, ebo, (uint)indices.Length, primitiveType);
}
}
@@ -0,0 +1,150 @@
using Silk.NET.OpenGL;
namespace NResUI.Rendering.Viewport.OpenGL;
public sealed class MsaaFramebuffer
{
private readonly GL _gl;
private readonly uint _samples;
private uint _msaaFramebuffer;
private uint _msaaColorRenderbuffer;
private uint _msaaDepthStencilRenderbuffer;
private uint _resolveFramebuffer;
private uint _resolveColorTexture;
private int _width;
private int _height;
public uint ColorTexture => _resolveColorTexture;
public MsaaFramebuffer(GL gl, uint samples = 4)
{
_gl = gl;
_samples = samples;
}
public void EnsureSize(int width, int height)
{
width = Math.Max(1, width);
height = Math.Max(1, height);
if (_msaaFramebuffer != 0 && _width == width && _height == height)
return;
_width = width;
_height = height;
if (_msaaFramebuffer == 0)
{
_msaaFramebuffer = _gl.GenFramebuffer();
_msaaColorRenderbuffer = _gl.GenRenderbuffer();
_msaaDepthStencilRenderbuffer = _gl.GenRenderbuffer();
_resolveFramebuffer = _gl.GenFramebuffer();
_resolveColorTexture = _gl.GenTexture();
}
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _msaaColorRenderbuffer);
_gl.RenderbufferStorageMultisample(
RenderbufferTarget.Renderbuffer,
_samples,
InternalFormat.Rgba8,
(uint)width,
(uint)height);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _msaaDepthStencilRenderbuffer);
_gl.RenderbufferStorageMultisample(
RenderbufferTarget.Renderbuffer,
_samples,
InternalFormat.Depth24Stencil8,
(uint)width,
(uint)height);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _msaaFramebuffer);
_gl.FramebufferRenderbuffer(
FramebufferTarget.Framebuffer,
FramebufferAttachment.ColorAttachment0,
RenderbufferTarget.Renderbuffer,
_msaaColorRenderbuffer);
_gl.FramebufferRenderbuffer(
FramebufferTarget.Framebuffer,
FramebufferAttachment.DepthStencilAttachment,
RenderbufferTarget.Renderbuffer,
_msaaDepthStencilRenderbuffer);
var msaaStatus = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (msaaStatus != GLEnum.FramebufferComplete)
throw new InvalidOperationException($"MSAA viewport framebuffer is incomplete: {msaaStatus}");
_gl.BindTexture(TextureTarget.Texture2D, _resolveColorTexture);
_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.Linear);
_gl.TexParameter(
TextureTarget.Texture2D,
TextureParameterName.TextureMagFilter,
(int)TextureMagFilter.Linear);
_gl.TexParameter(
TextureTarget.Texture2D,
TextureParameterName.TextureWrapS,
(int)TextureWrapMode.ClampToEdge);
_gl.TexParameter(
TextureTarget.Texture2D,
TextureParameterName.TextureWrapT,
(int)TextureWrapMode.ClampToEdge);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _resolveFramebuffer);
_gl.FramebufferTexture2D(
FramebufferTarget.Framebuffer,
FramebufferAttachment.ColorAttachment0,
TextureTarget.Texture2D,
_resolveColorTexture,
0);
var resolveStatus = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
if (resolveStatus != GLEnum.FramebufferComplete)
throw new InvalidOperationException($"Resolve viewport framebuffer is incomplete: {resolveStatus}");
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
_gl.BindTexture(TextureTarget.Texture2D, 0);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
}
public void BindForRender()
{
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _msaaFramebuffer);
_gl.Viewport(0, 0, (uint)_width, (uint)_height);
}
public void Resolve()
{
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, _msaaFramebuffer);
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _resolveFramebuffer);
_gl.BlitFramebuffer(
0, 0, _width, _height,
0, 0, _width, _height,
ClearBufferMask.ColorBufferBit,
BlitFramebufferFilter.Nearest);
}
}
@@ -0,0 +1,72 @@
using System.Numerics;
using Silk.NET.OpenGL;
namespace NResUI.Rendering.Viewport.OpenGL;
public sealed unsafe class ShaderProgram
{
private readonly GL _gl;
public uint Handle { get; }
public ShaderProgram(GL gl, string vertexShaderSource, string fragmentShaderSource, string debugName)
{
_gl = gl;
var vertexShader = CompileShader(ShaderType.VertexShader, vertexShaderSource, debugName);
var fragmentShader = CompileShader(ShaderType.FragmentShader, fragmentShaderSource, debugName);
Handle = _gl.CreateProgram();
_gl.AttachShader(Handle, vertexShader);
_gl.AttachShader(Handle, fragmentShader);
_gl.LinkProgram(Handle);
_gl.GetProgram(Handle, ProgramPropertyARB.LinkStatus, out var linked);
if (linked == 0)
{
var log = _gl.GetProgramInfoLog(Handle);
throw new InvalidOperationException($"{debugName} shader link failed: {log}");
}
_gl.DetachShader(Handle, vertexShader);
_gl.DetachShader(Handle, fragmentShader);
_gl.DeleteShader(vertexShader);
_gl.DeleteShader(fragmentShader);
}
public void Use()
{
_gl.UseProgram(Handle);
}
public int GetUniformLocation(string name)
{
return _gl.GetUniformLocation(Handle, name);
}
public void SetMatrix4(int location, Matrix4x4 matrix)
{
_gl.UniformMatrix4(location, 1, false, (float*)&matrix);
}
public void SetVector4(int location, Vector4 value)
{
_gl.Uniform4(location, value.X, value.Y, value.Z, value.W);
}
private uint CompileShader(ShaderType type, string source, string debugName)
{
var shader = _gl.CreateShader(type);
_gl.ShaderSource(shader, source);
_gl.CompileShader(shader);
_gl.GetShader(shader, ShaderParameterName.CompileStatus, out var compiled);
if (compiled == 0)
{
var log = _gl.GetShaderInfoLog(shader);
throw new InvalidOperationException($"{debugName} {type} compile failed: {log}");
}
return shader;
}
}
@@ -0,0 +1,38 @@
using System.Numerics;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportCamera
{
public float YawDegrees { get; set; } = 35.0f;
public float PitchDegrees { get; set; } = 25.0f;
public float Distance { get; set; } = 4.0f;
public Matrix4x4 GetSceneRotationMatrix()
{
return Matrix4x4.CreateRotationY(ToRadians(YawDegrees)) *
Matrix4x4.CreateRotationX(ToRadians(PitchDegrees));
}
public Matrix4x4 GetViewMatrix()
{
return Matrix4x4.CreateLookAt(
new Vector3(0.0f, 0.0f, Distance),
Vector3.Zero,
Vector3.UnitY);
}
public Matrix4x4 GetProjectionMatrix(float aspectRatio)
{
return Matrix4x4.CreatePerspectiveFieldOfView(
ToRadians(60.0f),
aspectRatio,
0.01f,
100.0f);
}
public static float ToRadians(float degrees)
{
return degrees * MathF.PI / 180.0f;
}
}
+16
View File
@@ -0,0 +1,16 @@
using System.Numerics;
using NResUI.Rendering.Viewport.Meshes;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportGrid
{
public GpuMesh Mesh { get; }
public bool IsVisible { get; set; } = true;
public Matrix4x4 LocalTransform { get; set; } = Matrix4x4.Identity;
public ViewportGrid(GpuMesh mesh)
{
Mesh = mesh;
}
}
@@ -0,0 +1,53 @@
using System.Numerics;
using ImGuiNET;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportInputController
{
private const float MouseRotationSpeed = 0.25f;
private const float MouseZoomSpeed = 0.35f;
private bool _isRotatingViewport;
public void Handle(
ViewportScene scene,
ViewportCamera camera,
bool viewportHovered,
Vector2 imageMin,
Vector2 imageSize)
{
var io = ImGui.GetIO();
if (viewportHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Middle))
_isRotatingViewport = true;
if (!ImGui.IsMouseDown(ImGuiMouseButton.Middle))
_isRotatingViewport = false;
if (_isRotatingViewport)
{
camera.YawDegrees += io.MouseDelta.X * MouseRotationSpeed;
camera.PitchDegrees += io.MouseDelta.Y * MouseRotationSpeed;
camera.PitchDegrees = Math.Clamp(camera.PitchDegrees, -89.0f, 89.0f);
}
if (viewportHovered && Math.Abs(io.MouseWheel) > float.Epsilon)
{
camera.Distance -= io.MouseWheel * MouseZoomSpeed;
camera.Distance = Math.Clamp(camera.Distance, 1.5f, 40.0f);
}
if (viewportHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
var mouse = ImGui.GetMousePos();
var localMouse = mouse - imageMin;
scene.SelectedPieceId = ViewportSelection.PickPiece(
scene,
camera,
localMouse,
imageSize);
}
}
}
@@ -0,0 +1,43 @@
using System.Numerics;
using NResUI.Rendering.Viewport.Meshes;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportPiece
{
public int Id { get; }
public string Name { get; }
public GpuMesh Mesh { get; }
public Matrix4x4 LocalTransform { get; set; }
public Vector3 BoundsMin { get; }
public Vector3 BoundsMax { get; }
public ViewportPiece(
int id,
string name,
GpuMesh mesh,
Matrix4x4 localTransform,
Vector3 boundsMin,
Vector3 boundsMax)
{
Id = id;
Name = name;
Mesh = mesh;
LocalTransform = localTransform;
BoundsMin = boundsMin;
BoundsMax = boundsMax;
}
public static ViewportPiece CreateUnitCube(int id, string name, GpuMesh mesh)
{
return new ViewportPiece(
id,
name,
mesh,
Matrix4x4.Identity,
new Vector3(-1.0f, -1.0f, -1.0f),
new Vector3(1.0f, 1.0f, 1.0f));
}
}
@@ -0,0 +1,254 @@
using System.Numerics;
using NResUI.Rendering.Viewport.OpenGL;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportRenderer
{
private readonly GL _gl;
private readonly IWindow _window;
private readonly MsaaFramebuffer _framebuffer;
private ShaderProgram? _meshShader;
private ShaderProgram? _outlineShader;
private int _meshMvpLocation;
private int _outlineMvpLocation;
private int _outlineColorLocation;
private bool _initialized;
public ViewportRenderer(GL gl, IWindow window)
{
_gl = gl;
_window = window;
_framebuffer = new MsaaFramebuffer(gl, samples: 4);
}
public uint Render(int width, int height, ViewportScene scene, ViewportCamera camera)
{
EnsureInitialized();
width = Math.Max(1, width);
height = Math.Max(1, height);
_framebuffer.EnsureSize(width, height);
_framebuffer.BindForRender();
_gl.Enable(EnableCap.DepthTest);
_gl.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
_gl.ClearColor(0.12f, 0.13f, 0.15f, 1.0f);
_gl.ClearStencil(0);
_gl.Clear((uint)(
ClearBufferMask.ColorBufferBit |
ClearBufferMask.DepthBufferBit |
ClearBufferMask.StencilBufferBit));
var aspect = width / (float)Math.Max(1, height);
var sceneRotation = camera.GetSceneRotationMatrix();
var view = camera.GetViewMatrix();
var projection = camera.GetProjectionMatrix(aspect);
DrawGrid(scene, sceneRotation, view, projection);
DrawScene(scene, sceneRotation, view, projection);
_framebuffer.Resolve();
_gl.Disable(EnableCap.CullFace);
_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);
return _framebuffer.ColorTexture;
}
private void DrawGrid(
ViewportScene scene,
Matrix4x4 sceneRotation,
Matrix4x4 view,
Matrix4x4 projection)
{
if (_meshShader == null)
throw new InvalidOperationException("Viewport mesh shader is not initialized.");
var grid = scene.Grid;
if (grid == null || !grid.IsVisible)
return;
var model = grid.LocalTransform * sceneRotation;
var mvp = model * view * projection;
_gl.Disable(EnableCap.StencilTest);
_gl.Disable(EnableCap.CullFace);
_meshShader.Use();
_meshShader.SetMatrix4(_meshMvpLocation, mvp);
grid.Mesh.Draw();
_gl.Enable(EnableCap.CullFace);
}
private void DrawScene(
ViewportScene scene,
Matrix4x4 sceneRotation,
Matrix4x4 view,
Matrix4x4 projection)
{
if (_meshShader == null || _outlineShader == null)
throw new InvalidOperationException("Viewport renderer is not initialized.");
_gl.Disable(EnableCap.StencilTest);
_meshShader.Use();
foreach (var piece in scene.Pieces)
{
if (piece.Id == scene.SelectedPieceId)
continue;
DrawPiece(piece, sceneRotation, view, projection);
}
var selectedPiece = scene.SelectedPiece;
if (selectedPiece == null)
return;
_gl.Enable(EnableCap.StencilTest);
_gl.StencilMask(0xFF);
_gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
_meshShader.Use();
DrawPiece(selectedPiece, sceneRotation, view, projection);
_gl.StencilFunc(StencilFunction.Notequal, 1, 0xFF);
_gl.StencilMask(0x00);
_gl.Disable(EnableCap.DepthTest);
DrawPieceOutline(selectedPiece, sceneRotation, view, projection);
_gl.Enable(EnableCap.DepthTest);
_gl.StencilMask(0xFF);
_gl.Disable(EnableCap.StencilTest);
}
private void DrawPiece(
ViewportPiece piece,
Matrix4x4 sceneRotation,
Matrix4x4 view,
Matrix4x4 projection)
{
if (_meshShader == null)
throw new InvalidOperationException("Viewport mesh shader is not initialized.");
var model = piece.LocalTransform * sceneRotation;
var mvp = model * view * projection;
_meshShader.Use();
_meshShader.SetMatrix4(_meshMvpLocation, mvp);
piece.Mesh.Draw();
}
private void DrawPieceOutline(
ViewportPiece piece,
Matrix4x4 sceneRotation,
Matrix4x4 view,
Matrix4x4 projection)
{
if (_outlineShader == null)
throw new InvalidOperationException("Viewport outline shader is not initialized.");
const float outlineScale = 1.06f;
var model = Matrix4x4.CreateScale(outlineScale) * piece.LocalTransform * sceneRotation;
var mvp = model * view * projection;
_outlineShader.Use();
_outlineShader.SetMatrix4(_outlineMvpLocation, mvp);
_outlineShader.SetVector4(_outlineColorLocation, new Vector4(1.0f, 0.82f, 0.15f, 1.0f));
piece.Mesh.Draw();
}
private void EnsureInitialized()
{
if (_initialized)
return;
CreateShaders();
_initialized = true;
}
private void CreateShaders()
{
const string meshVertexShaderSource = """
#version 330 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec3 aColor;
uniform mat4 uMvp;
out vec3 vColor;
void main()
{
vColor = aColor;
gl_Position = uMvp * vec4(aPosition, 1.0);
}
""";
const string meshFragmentShaderSource = """
#version 330 core
in vec3 vColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(vColor, 1.0);
}
""";
const string outlineVertexShaderSource = """
#version 330 core
layout (location = 0) in vec3 aPosition;
uniform mat4 uMvp;
void main()
{
gl_Position = uMvp * vec4(aPosition, 1.0);
}
""";
const string outlineFragmentShaderSource = """
#version 330 core
uniform vec4 uColor;
out vec4 FragColor;
void main()
{
FragColor = uColor;
}
""";
_meshShader = new ShaderProgram(_gl, meshVertexShaderSource, meshFragmentShaderSource, "Viewport mesh");
_outlineShader = new ShaderProgram(_gl, outlineVertexShaderSource, outlineFragmentShaderSource, "Viewport outline");
_meshMvpLocation = _meshShader.GetUniformLocation("uMvp");
_outlineMvpLocation = _outlineShader.GetUniformLocation("uMvp");
_outlineColorLocation = _outlineShader.GetUniformLocation("uColor");
}
}
@@ -0,0 +1,53 @@
using NResUI.Rendering.Viewport.Meshes;
namespace NResUI.Rendering.Viewport;
public sealed class ViewportScene
{
private readonly List<ViewportPiece> _pieces = new();
public IReadOnlyList<ViewportPiece> Pieces => _pieces;
public ViewportGrid? Grid { get; set; }
public int SelectedPieceId { get; set; } = -1;
public ViewportPiece? SelectedPiece
{
get
{
if (SelectedPieceId < 0)
return null;
foreach (var piece in _pieces)
{
if (piece.Id == SelectedPieceId)
return piece;
}
return null;
}
}
public void AddPiece(ViewportPiece piece)
{
_pieces.Add(piece);
}
public void ClearSelection()
{
SelectedPieceId = -1;
}
public static ViewportScene CreateDefaultCubeScene(GpuMesh cubeMesh, GpuMesh gridMesh)
{
var scene = new ViewportScene
{
Grid = new ViewportGrid(gridMesh)
};
scene.AddPiece(ViewportPiece.CreateUnitCube(0, "Cube", cubeMesh));
return scene;
}
}
@@ -0,0 +1,109 @@
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(60.0f);
var tanHalfFov = MathF.Tan(fovRadians * 0.5f);
var ndcX = (2.0f * localMouse.X / viewportSize.X) - 1.0f;
var ndcY = 1.0f - (2.0f * localMouse.Y / viewportSize.Y);
var rayOriginWorld = new Vector3(0.0f, 0.0f, camera.Distance);
var rayDirectionWorld = Vector3.Normalize(new Vector3(
ndcX * aspect * tanHalfFov,
ndcY * tanHalfFov,
-1.0f));
var sceneRotation = camera.GetSceneRotationMatrix();
var bestPieceId = -1;
var bestDistance = float.PositiveInfinity;
foreach (var piece in scene.Pieces)
{
var model = piece.LocalTransform * sceneRotation;
if (!Matrix4x4.Invert(model, 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;
}
}