mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
418 lines
13 KiB
C#
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);
|
|
}
|
|
}
|