Files
parkan-playground/NResUI/ImGuiUI/ViewportPanel.cs
T
2026-06-12 02:06:32 +03:00

418 lines
13 KiB
C#

using System.Numerics;
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
using NResUI.Rendering.Inspection;
using NResUI.Rendering.Import.Msh;
using NResUI.Rendering.Materials;
using NResUI.Rendering.Viewport;
using NativeFileDialogSharp;
using NResUI.Rendering.Viewport.Meshes;
using NResUI.Rendering.Viewport.Msh;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace NResUI.ImGuiUI;
public sealed class ViewportPanel : IImGuiPanel, IUpdateReceiver
{
private readonly ViewportRenderer _renderer;
private readonly ViewportScene _scene;
private readonly ViewportCamera _camera = new();
private readonly ViewportInputController _inputController = new();
private readonly IConfigProvider _configProvider;
private readonly MeshViewportViewModel _viewModel;
private ViewportMaterialSet _materialSet = ViewportMaterialSet.Empty;
public ViewportPanel(GL gl, IWindow window, IConfigProvider configProvider, MeshViewportViewModel viewModel)
{
_configProvider = configProvider;
_viewModel = viewModel;
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;
}
DrawModelControls();
DrawSelectionStatus();
DrawViewportToolbar();
DrawDebugControls();
DrawAnimationControls();
RebuildSceneIfNeeded();
SyncSelectionFromViewModel();
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 viewportFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var imageMin = ImGui.GetItemRectMin();
DrawViewportHelpOverlay();
_inputController.Handle(
_renderer,
_scene,
_camera,
viewportHovered,
viewportFocused,
imageMin,
imageSize);
if (_viewModel.RenderState.SelectedPieceId != _scene.SelectedPieceId)
_viewModel.RenderState.SelectedPieceId = _scene.SelectedPieceId;
ImGui.End();
}
public void OnUpdate(float delta)
{
var document = _viewModel.Document;
if (document?.MshModelGeometry == null)
return;
var state = _viewModel.RenderState;
if (!state.AnimationPlay)
return;
var maxAnimationTime = GetMaxAnimationTime(document);
if (maxAnimationTime <= 0.0f)
return;
state.AnimationSampleTime = WrapAnimationTime(
state.AnimationSampleTime + delta * state.AnimationSpeed,
maxAnimationTime);
state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample;
ApplyCurrentAnimationPose(document);
}
private void DrawModelControls()
{
if (ImGui.Button("Open MSH in viewport"))
{
var result = Dialog.FileOpen("msh");
if (result.IsOk)
LoadMsh(result.Path);
}
ImGui.SameLine();
if (ImGui.Button("Reset cube"))
{
var cubeMesh = PrimitiveMeshes.CreateCube(_renderer.Gl);
_scene.ReplacePieces([ViewportPiece.CreateUnitCube(0, "Cube", cubeMesh)]);
_viewModel.ClearDocument();
_camera.Reset();
}
if (_viewModel.Document != null)
ImGui.TextDisabled($"Model: {Path.GetFileName(_viewModel.Document.SourcePath)}");
if (_viewModel.LoadError != null)
ImGui.TextColored(new Vector4(1.0f, 0.35f, 0.25f, 1.0f), $"MSH load failed: {_viewModel.LoadError}");
}
private void LoadMsh(string path)
{
// Сначала строим CPU-документ для inspector, затем отдельно резолвим материалы и OpenGL textures.
var loadResult = MshMeshDocumentImporter.LoadFromFile(path);
if (!loadResult.IsSuccess)
{
_viewModel.SetError(loadResult.Error ?? "Unknown error.");
return;
}
_viewModel.SetDocument(loadResult.Document!);
_materialSet = ViewportMaterialResolver.LoadForMsh(
_renderer.Gl,
path,
_configProvider,
loadResult.Document!.Warnings is ICollection<string> mutableWarnings ? mutableWarnings : null);
RebuildSceneIfNeeded();
if (_scene.TryGetSceneWorldBounds(out var bounds))
_camera.FrameBounds(bounds.Min, bounds.Max);
else
_camera.Reset();
}
private void DrawSelectionStatus()
{
var selectedPiece = _scene.SelectedPiece;
if (selectedPiece != null)
{
ImGui.Text($"Selected: {selectedPiece.Name}");
var debugInfo = selectedPiece.DebugInfo;
if (debugInfo != null)
{
ImGui.TextDisabled(
$"{debugInfo.SourceKind} | parent {debugInfo.SourceParentIndex} | slot {debugInfo.GeometrySlotIndex} | " +
$"flags 0x{debugInfo.Msh01Flags:X4} | batches {debugInfo.BatchCount} | tris {debugInfo.TriangleCount}");
}
}
else
{
ImGui.TextDisabled("Selected: none");
}
}
private void RebuildSceneIfNeeded()
{
if (!_viewModel.NeedsSceneRebuild)
return;
var document = _viewModel.Document;
if (document == null)
{
_viewModel.MarkSceneRebuilt();
return;
}
// Перестройка нужна при смене state/LOD, hide/isolate и batch filter: все это меняет состав GPU-мешей.
var pieces = MshViewportMeshFactory.BuildViewportPieces(
_renderer.Gl,
document,
_viewModel.RenderState,
_materialSet);
_scene.ReplacePieces(pieces);
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
ApplyCurrentAnimationPose(document);
_viewModel.MarkSceneRebuilt();
}
private void ApplyCurrentAnimationPose(MeshDocument document)
{
var modelGeometry = document.MshModelGeometry;
if (modelGeometry == null)
return;
var renderState = _viewModel.RenderState;
var poses = MshRestPoseBuilder.BuildPose(
modelGeometry.Nodes,
modelGeometry.TransformKeyframes,
modelGeometry.AnimationMap,
renderState.AnimationPoseMode,
renderState.AnimationSampleTime);
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
foreach (var piece in _scene.Pieces)
{
if (piece.Id < 0 || piece.Id >= poses.Count)
continue;
piece.LocalTransform = poses[piece.Id].MeshSpaceTransform * mshToViewportTransform;
}
}
private void SyncSelectionFromViewModel()
{
if (_viewModel.Document == null)
return;
// Inspector может выбрать piece без клика по viewport; renderer должен подсветить тот же id.
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
}
private void DrawViewportToolbar()
{
if (ImGui.Button("Reset view"))
_camera.Reset();
ImGui.SameLine();
if (ImGui.Button("Frame selected"))
{
if (_scene.TryGetSelectedPieceWorldBounds(out var selectedBounds))
_camera.FrameBounds(selectedBounds.Min, selectedBounds.Max);
}
ImGui.SameLine();
if (ImGui.Button("Frame all"))
{
if (_scene.TryGetSceneWorldBounds(out var sceneBounds))
_camera.FrameBounds(sceneBounds.Min, sceneBounds.Max);
else
_camera.Reset();
}
}
private void DrawDebugControls()
{
var grid = _scene.Grid;
if (grid != null)
{
var isVisible = grid.IsVisible;
if (ImGui.Checkbox("Grid", ref isVisible))
grid.IsVisible = isVisible;
}
ImGui.SameLine();
var showOriginAxes = _scene.Debug.ShowOriginAxes;
if (ImGui.Checkbox("Origin axes", ref showOriginAxes))
_scene.Debug.ShowOriginAxes = showOriginAxes;
ImGui.SameLine();
var showPieceOrigins = _scene.Debug.ShowPieceOrigins;
if (ImGui.Checkbox("Piece axes", ref showPieceOrigins))
_scene.Debug.ShowPieceOrigins = showPieceOrigins;
ImGui.SameLine();
var showSelectedBounds = _scene.Debug.ShowSelectedBounds;
if (ImGui.Checkbox("Selected bounds", ref showSelectedBounds))
_scene.Debug.ShowSelectedBounds = showSelectedBounds;
ImGui.SameLine();
var showSceneBounds = _scene.Debug.ShowSceneBounds;
if (ImGui.Checkbox("Scene bounds", ref showSceneBounds))
_scene.Debug.ShowSceneBounds = showSceneBounds;
ImGui.SameLine();
var wireframe = _scene.Debug.Wireframe;
if (ImGui.Checkbox("Wireframe", ref wireframe))
_scene.Debug.Wireframe = wireframe;
}
private void DrawAnimationControls()
{
var document = _viewModel.Document;
var state = _viewModel.RenderState;
ImGui.SeparatorText("Animation");
var hasAnimationDocument = document?.MshModelGeometry != null;
var maxAnimationTime = hasAnimationDocument ? GetMaxAnimationTime(document!) : 0.0f;
if (!hasAnimationDocument)
ImGui.BeginDisabled();
if (ImGui.Button("Reset to default keyframe"))
{
state.AnimationPlay = false;
state.AnimationPoseMode = MshAnimationPoseMode.DefaultKeyframe;
if (document != null)
ApplyCurrentAnimationPose(document);
}
var animationSampleTime = state.AnimationSampleTime;
if (ImGui.SliderFloat("Anim time", ref animationSampleTime, 0.0f, maxAnimationTime))
{
state.AnimationSampleTime = animationSampleTime;
state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample;
if (document != null)
ApplyCurrentAnimationPose(document);
}
var play = state.AnimationPlay;
if (ImGui.Checkbox("Play", ref play))
{
state.AnimationPlay = play;
if (play)
state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample;
else if (document != null)
ApplyCurrentAnimationPose(document);
}
ImGui.SameLine();
var speed = state.AnimationSpeed;
if (ImGui.InputFloat("Speed", ref speed, 0.1f, 1.0f))
state.AnimationSpeed = speed;
if (!hasAnimationDocument)
ImGui.EndDisabled();
}
private static float GetMaxAnimationTime(MeshDocument document)
{
var maxAnimationTime = document.MshModelGeometry?.MaxAnimationTime ?? 0;
if (maxAnimationTime > 0)
return maxAnimationTime;
var keyframes = document.MshModelGeometry?.TransformKeyframes;
return keyframes == null || keyframes.Count == 0
? 0.0f
: MathF.Max(0.0f, keyframes.Max(x => x.Time));
}
private static float WrapAnimationTime(float time, float maxAnimationTime)
{
if (maxAnimationTime <= 0.0f)
return 0.0f;
var wrapped = time % maxAnimationTime;
return wrapped < 0.0f ? wrapped + maxAnimationTime : wrapped;
}
private static void DrawViewportHelpOverlay()
{
var drawList = ImGui.GetWindowDrawList();
var imageMin = ImGui.GetItemRectMin();
var imageMax = ImGui.GetItemRectMax();
const string helpText =
"LMB click: select / deselect\n" +
"MMB drag: rotate\n" +
"RMB drag or Alt+MMB: pan\n" +
"Mouse wheel: zoom\n" +
"F: frame selected Home: frame all";
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);
}
}