mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
gl viewport
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user