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 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); } }