mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
animations and QOL
This commit is contained in:
@@ -105,6 +105,8 @@ public static class Msh0x01
|
|||||||
ushort FallbackKey0x08,
|
ushort FallbackKey0x08,
|
||||||
ushort[] Msh02SlotIndicesByStateAndLOD)
|
ushort[] Msh02SlotIndicesByStateAndLOD)
|
||||||
{
|
{
|
||||||
|
public ushort DefaultKeyframeIndex0x08 => FallbackKey0x08;
|
||||||
|
|
||||||
public ushort ResolveSlotIndex(int state, int lod = 0)
|
public ushort ResolveSlotIndex(int state, int lod = 0)
|
||||||
{
|
{
|
||||||
// State и LOD выбираются для конкретного узла/piece, не для всего файла сразу.
|
// State и LOD выбираются для конкретного узла/piece, не для всего файла сразу.
|
||||||
|
|||||||
+9
-9
@@ -5,7 +5,7 @@ namespace MshLib;
|
|||||||
|
|
||||||
public static class Msh0x08
|
public static class Msh0x08
|
||||||
{
|
{
|
||||||
public static List<AnimationDescriptor> ReadComponent(FileStream mshFs, NResArchive archive)
|
public static List<MshTransformKeyframe> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
{
|
{
|
||||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "08 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "08 00 00 00");
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ public static class Msh0x08
|
|||||||
|
|
||||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
|
|
||||||
var descriptors = new List<AnimationDescriptor>();
|
var descriptors = new List<MshTransformKeyframe>();
|
||||||
|
|
||||||
for (var i = 0; i < entry.ElementCount; i++)
|
for (var i = 0; i < entry.ElementCount; i++)
|
||||||
{
|
{
|
||||||
descriptors.Add(new AnimationDescriptor(
|
descriptors.Add(new MshTransformKeyframe(
|
||||||
new Vector3(mshFs.ReadFloatLittleEndian(),
|
new Vector3(mshFs.ReadFloatLittleEndian(),
|
||||||
mshFs.ReadFloatLittleEndian(),
|
mshFs.ReadFloatLittleEndian(),
|
||||||
mshFs.ReadFloatLittleEndian()),
|
mshFs.ReadFloatLittleEndian()),
|
||||||
@@ -36,10 +36,10 @@ public static class Msh0x08
|
|||||||
|
|
||||||
return descriptors;
|
return descriptors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AnimationDescriptor(
|
|
||||||
Vector3 Position,
|
|
||||||
float Time,
|
|
||||||
UShortQuaternion Rotation
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record MshTransformKeyframe(
|
||||||
|
Vector3 Position,
|
||||||
|
float Time,
|
||||||
|
UShortQuaternion Rotation
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
using NResLib;
|
||||||
|
|
||||||
|
namespace MshLib;
|
||||||
|
|
||||||
|
public static class Msh0x13
|
||||||
|
{
|
||||||
|
public static Msh0x13Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
|
{
|
||||||
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "13 00 00 00");
|
||||||
|
|
||||||
|
if (entry is null)
|
||||||
|
{
|
||||||
|
throw new Exception("Archive doesn't contain animation map component (0x13)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.ElementSize < 2)
|
||||||
|
{
|
||||||
|
throw new Exception("Animation map component (0x13) element size is too small");
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new byte[entry.FileLength];
|
||||||
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
|
var elementCount = checked((int)entry.ElementCount);
|
||||||
|
var entries = new List<MshAnimationMapEntry>(elementCount);
|
||||||
|
var span = data.AsSpan();
|
||||||
|
for (var i = 0; i < elementCount; i++)
|
||||||
|
{
|
||||||
|
var elementOffset = i * entry.ElementSize;
|
||||||
|
entries.Add(new MshAnimationMapEntry(
|
||||||
|
BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(elementOffset, 2))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Msh0x13Component(entry.ElementSize, entries)
|
||||||
|
{
|
||||||
|
MaxAnimationTime = entry.Magic1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct MshAnimationMapEntry(
|
||||||
|
ushort KeyframeIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
public sealed record Msh0x13Component(
|
||||||
|
int ElementSize,
|
||||||
|
List<MshAnimationMapEntry> Entries
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public int MaxAnimationTime { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace MshLib;
|
||||||
|
|
||||||
|
public static class MshAnimationSampler
|
||||||
|
{
|
||||||
|
public static MshSampledPieceTransform SamplePieceAnimationAtTime(
|
||||||
|
Msh0x01.Node node,
|
||||||
|
IReadOnlyList<MshTransformKeyframe> keyframes,
|
||||||
|
IReadOnlyList<MshAnimationMapEntry> animationMap,
|
||||||
|
float sampleTime)
|
||||||
|
{
|
||||||
|
var defaultKeyframeIndex = node.DefaultKeyframeIndex0x08;
|
||||||
|
if (defaultKeyframeIndex == ushort.MaxValue || defaultKeyframeIndex >= keyframes.Count)
|
||||||
|
return MshSampledPieceTransform.Identity;
|
||||||
|
|
||||||
|
var frameIndex = (int)MathF.Round(sampleTime - 0.5f, MidpointRounding.AwayFromZero);
|
||||||
|
if (frameIndex < 0 ||
|
||||||
|
node.AnimMapStart0x13 == ushort.MaxValue ||
|
||||||
|
node.AnimMapStart0x13 + frameIndex >= animationMap.Count)
|
||||||
|
{
|
||||||
|
return FromKeyframe(keyframes[defaultKeyframeIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyframeIndex = animationMap[node.AnimMapStart0x13 + frameIndex].KeyframeIndex;
|
||||||
|
if (keyframeIndex >= defaultKeyframeIndex ||
|
||||||
|
keyframeIndex >= keyframes.Count ||
|
||||||
|
keyframeIndex + 1 >= keyframes.Count)
|
||||||
|
{
|
||||||
|
return FromKeyframe(keyframes[defaultKeyframeIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var key0 = keyframes[keyframeIndex];
|
||||||
|
var key1 = keyframes[keyframeIndex + 1];
|
||||||
|
|
||||||
|
if (sampleTime == key0.Time)
|
||||||
|
return FromKeyframe(key0);
|
||||||
|
|
||||||
|
if (sampleTime == key1.Time)
|
||||||
|
return FromKeyframe(key1);
|
||||||
|
|
||||||
|
var duration = key1.Time - key0.Time;
|
||||||
|
if (MathF.Abs(duration) < 1e-6f)
|
||||||
|
return FromKeyframe(key0);
|
||||||
|
|
||||||
|
var t = (sampleTime - key0.Time) / duration;
|
||||||
|
return new MshSampledPieceTransform(
|
||||||
|
Vector3.Lerp(ToNumericsVector3(key0.Position), ToNumericsVector3(key1.Position), t),
|
||||||
|
Quaternion.Slerp(ToNumericsQuaternion(key0.Rotation), ToNumericsQuaternion(key1.Rotation), t));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Matrix4x4 ToMatrix(MshSampledPieceTransform transform)
|
||||||
|
{
|
||||||
|
var matrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
|
||||||
|
matrix.Translation = transform.Position;
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MshSampledPieceTransform FromKeyframe(MshTransformKeyframe keyframe)
|
||||||
|
{
|
||||||
|
return new MshSampledPieceTransform(
|
||||||
|
ToNumericsVector3(keyframe.Position),
|
||||||
|
ToNumericsQuaternion(keyframe.Rotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3 ToNumericsVector3(Common.Vector3 vector)
|
||||||
|
{
|
||||||
|
return new Vector3(vector.X, vector.Y, vector.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Quaternion ToNumericsQuaternion(Common.UShortQuaternion packedQuaternion)
|
||||||
|
{
|
||||||
|
var q = new Quaternion(
|
||||||
|
packedQuaternion.X / 32767f,
|
||||||
|
packedQuaternion.Y / 32767f,
|
||||||
|
packedQuaternion.Z / 32767f,
|
||||||
|
packedQuaternion.W / 32767f);
|
||||||
|
|
||||||
|
if (q.LengthSquared() < 1e-8f)
|
||||||
|
return Quaternion.Identity;
|
||||||
|
|
||||||
|
// MSH stores mesh-to-parent rotation; viewport composition uses the inverse.
|
||||||
|
return Quaternion.Conjugate(Quaternion.Normalize(q));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct MshSampledPieceTransform(
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Rotation)
|
||||||
|
{
|
||||||
|
public static MshSampledPieceTransform Identity { get; } = new(Vector3.Zero, Quaternion.Identity);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ public class App
|
|||||||
public ImFontPtr OpenSansFont;
|
public ImFontPtr OpenSansFont;
|
||||||
|
|
||||||
private List<IImGuiPanel> _imGuiPanels = [];
|
private List<IImGuiPanel> _imGuiPanels = [];
|
||||||
|
private List<IUpdateReceiver> _updateReceivers = [];
|
||||||
private Later _later = new();
|
private Later _later = new();
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
@@ -59,6 +60,11 @@ public class App
|
|||||||
serviceCollection.AddSingleton(type);
|
serviceCollection.AddSingleton(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var type in Utils.GetAssignableTypes<IUpdateReceiver>())
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton(type);
|
||||||
|
}
|
||||||
|
|
||||||
serviceCollection.AddSingleton(openGl);
|
serviceCollection.AddSingleton(openGl);
|
||||||
serviceCollection.AddSingleton(window);
|
serviceCollection.AddSingleton(window);
|
||||||
|
|
||||||
@@ -78,6 +84,10 @@ public class App
|
|||||||
_imGuiPanels = Utils.GetAssignableTypes<IImGuiPanel>()
|
_imGuiPanels = Utils.GetAssignableTypes<IImGuiPanel>()
|
||||||
.Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!)
|
.Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
_updateReceivers = Utils.GetAssignableTypes<IUpdateReceiver>()
|
||||||
|
.Select(t => (serviceProvider.GetService(t) as IUpdateReceiver)!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (var type in Utils.GetAssignableTypes<ILaunchReceiver>())
|
foreach (var type in Utils.GetAssignableTypes<ILaunchReceiver>())
|
||||||
{
|
{
|
||||||
@@ -159,6 +169,8 @@ public class App
|
|||||||
|
|
||||||
public void Update(double delta)
|
public void Update(double delta)
|
||||||
{
|
{
|
||||||
|
foreach (var updateReceiver in _updateReceivers)
|
||||||
|
updateReceiver.OnUpdate((float)delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnKeyPressed(Key key)
|
public void OnKeyPressed(Key key)
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ using System.Numerics;
|
|||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using NResUI.Abstractions;
|
using NResUI.Abstractions;
|
||||||
using NResUI.Models;
|
using NResUI.Models;
|
||||||
|
using NResUI.Rendering.Inspection;
|
||||||
using NResUI.Rendering.Import.Msh;
|
using NResUI.Rendering.Import.Msh;
|
||||||
using NResUI.Rendering.Materials;
|
using NResUI.Rendering.Materials;
|
||||||
using NResUI.Rendering.Viewport;
|
using NResUI.Rendering.Viewport;
|
||||||
using NativeFileDialogSharp;
|
using NativeFileDialogSharp;
|
||||||
using NResUI.Rendering.Viewport.Meshes;
|
using NResUI.Rendering.Viewport.Meshes;
|
||||||
|
using NResUI.Rendering.Viewport.Msh;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
|
||||||
namespace NResUI.ImGuiUI;
|
namespace NResUI.ImGuiUI;
|
||||||
|
|
||||||
public sealed class ViewportPanel : IImGuiPanel
|
public sealed class ViewportPanel : IImGuiPanel, IUpdateReceiver
|
||||||
{
|
{
|
||||||
private readonly ViewportRenderer _renderer;
|
private readonly ViewportRenderer _renderer;
|
||||||
private readonly ViewportScene _scene;
|
private readonly ViewportScene _scene;
|
||||||
@@ -46,6 +48,7 @@ public sealed class ViewportPanel : IImGuiPanel
|
|||||||
DrawSelectionStatus();
|
DrawSelectionStatus();
|
||||||
DrawViewportToolbar();
|
DrawViewportToolbar();
|
||||||
DrawDebugControls();
|
DrawDebugControls();
|
||||||
|
DrawAnimationControls();
|
||||||
RebuildSceneIfNeeded();
|
RebuildSceneIfNeeded();
|
||||||
SyncSelectionFromViewModel();
|
SyncSelectionFromViewModel();
|
||||||
|
|
||||||
@@ -90,6 +93,27 @@ public sealed class ViewportPanel : IImGuiPanel
|
|||||||
ImGui.End();
|
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()
|
private void DrawModelControls()
|
||||||
{
|
{
|
||||||
@@ -183,9 +207,34 @@ public sealed class ViewportPanel : IImGuiPanel
|
|||||||
|
|
||||||
_scene.ReplacePieces(pieces);
|
_scene.ReplacePieces(pieces);
|
||||||
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
|
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
|
||||||
|
ApplyCurrentAnimationPose(document);
|
||||||
_viewModel.MarkSceneRebuilt();
|
_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()
|
private void SyncSelectionFromViewModel()
|
||||||
{
|
{
|
||||||
if (_viewModel.Document == null)
|
if (_viewModel.Document == null)
|
||||||
@@ -260,6 +309,75 @@ public sealed class ViewportPanel : IImGuiPanel
|
|||||||
_scene.Debug.Wireframe = 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()
|
private static void DrawViewportHelpOverlay()
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ public static class MshMeshDocumentImporter
|
|||||||
var indices = Msh0x06.ReadComponent(fs, archive);
|
var indices = Msh0x06.ReadComponent(fs, archive);
|
||||||
var batches = Msh0x0D.ReadComponent(fs, archive);
|
var batches = Msh0x0D.ReadComponent(fs, archive);
|
||||||
var animationDescriptors = Msh0x08.ReadComponent(fs, archive);
|
var animationDescriptors = Msh0x08.ReadComponent(fs, archive);
|
||||||
|
var animationMapComponent = TryReadAnimationMap(fs, archive, warnings);
|
||||||
|
var animationMap = animationMapComponent?.Entries ?? [];
|
||||||
|
var maxAnimationTime = animationMapComponent?.MaxAnimationTime ?? InferMaxAnimationTime(animationDescriptors);
|
||||||
var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
|
var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
|
||||||
var names = TryReadNames(fs, archive, warnings);
|
var names = TryReadNames(fs, archive, warnings);
|
||||||
|
|
||||||
@@ -121,7 +124,10 @@ public static class MshMeshDocumentImporter
|
|||||||
Normals = normals,
|
Normals = normals,
|
||||||
Uvs = uvs,
|
Uvs = uvs,
|
||||||
Indices = indices,
|
Indices = indices,
|
||||||
Batches = batches
|
Batches = batches,
|
||||||
|
TransformKeyframes = animationDescriptors,
|
||||||
|
AnimationMap = animationMap,
|
||||||
|
MaxAnimationTime = maxAnimationTime
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -228,6 +234,27 @@ public static class MshMeshDocumentImporter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Msh0x13Component? TryReadAnimationMap(FileStream fs, NResArchive archive, ICollection<string> warnings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Msh0x13.ReadComponent(fs, archive);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
warnings.Add($"MSH 0x13 animation map is unavailable: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int InferMaxAnimationTime(IReadOnlyList<MshTransformKeyframe> keyframes)
|
||||||
|
{
|
||||||
|
if (keyframes.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return (int)MathF.Ceiling(MathF.Max(0.0f, keyframes.Max(x => x.Time)));
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
|
private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
|
||||||
{
|
{
|
||||||
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
|
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using NResUI.Rendering.Inspection;
|
|||||||
using NResUI.Rendering.Materials;
|
using NResUI.Rendering.Materials;
|
||||||
using NResUI.Rendering.Viewport;
|
using NResUI.Rendering.Viewport;
|
||||||
using NResUI.Rendering.Viewport.Meshes;
|
using NResUI.Rendering.Viewport.Meshes;
|
||||||
|
using NResUI.Rendering.Viewport.Msh;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace NResUI.Rendering.Import.Msh;
|
namespace NResUI.Rendering.Import.Msh;
|
||||||
@@ -26,18 +27,25 @@ public static class MshViewportMeshFactory
|
|||||||
var result = new List<ViewportPiece>();
|
var result = new List<ViewportPiece>();
|
||||||
var modelGeometry = document.MshModelGeometry;
|
var modelGeometry = document.MshModelGeometry;
|
||||||
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
|
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
|
||||||
|
var poses = MshRestPoseBuilder.BuildPose(
|
||||||
|
modelGeometry.Nodes,
|
||||||
|
modelGeometry.TransformKeyframes,
|
||||||
|
modelGeometry.AnimationMap,
|
||||||
|
renderState.AnimationPoseMode,
|
||||||
|
renderState.AnimationSampleTime);
|
||||||
|
|
||||||
foreach (var pieceInfo in document.Pieces)
|
foreach (var pieceInfo in document.Pieces)
|
||||||
{
|
{
|
||||||
if (!renderState.IsPieceVisible(pieceInfo.Id))
|
if (!renderState.IsPieceVisible(pieceInfo.Id))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var pieceTransform = poses[pieceInfo.Id].MeshSpaceTransform;
|
||||||
var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState);
|
var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState);
|
||||||
var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod);
|
var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod);
|
||||||
if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count)
|
if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count)
|
||||||
{
|
{
|
||||||
if (renderState.ShowEmptyPieces)
|
if (renderState.ShowEmptyPieces)
|
||||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "No geometry slot for selected state/LOD");
|
AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "No geometry slot for selected state/LOD");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +53,7 @@ public static class MshViewportMeshFactory
|
|||||||
if (!slot.HasBatches)
|
if (!slot.HasBatches)
|
||||||
{
|
{
|
||||||
if (renderState.ShowEmptyPieces)
|
if (renderState.ShowEmptyPieces)
|
||||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot has no batches", slotIndex);
|
AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "Geometry slot has no batches", slotIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +68,7 @@ public static class MshViewportMeshFactory
|
|||||||
if (meshBuildResult == null)
|
if (meshBuildResult == null)
|
||||||
{
|
{
|
||||||
if (renderState.ShowEmptyPieces)
|
if (renderState.ShowEmptyPieces)
|
||||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot produced no valid triangles", slotIndex);
|
AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "Geometry slot produced no valid triangles", slotIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +76,7 @@ public static class MshViewportMeshFactory
|
|||||||
id: pieceInfo.Id,
|
id: pieceInfo.Id,
|
||||||
name: pieceInfo.Name,
|
name: pieceInfo.Name,
|
||||||
meshes: meshBuildResult.Meshes,
|
meshes: meshBuildResult.Meshes,
|
||||||
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform,
|
localTransform: pieceTransform * mshToViewportTransform,
|
||||||
boundsMin: meshBuildResult.BoundsMin,
|
boundsMin: meshBuildResult.BoundsMin,
|
||||||
boundsMax: meshBuildResult.BoundsMax,
|
boundsMax: meshBuildResult.BoundsMax,
|
||||||
debugInfo: new ViewportPieceDebugInfo
|
debugInfo: new ViewportPieceDebugInfo
|
||||||
@@ -121,6 +129,7 @@ public static class MshViewportMeshFactory
|
|||||||
GL gl,
|
GL gl,
|
||||||
List<ViewportPiece> pieces,
|
List<ViewportPiece> pieces,
|
||||||
MeshPieceInfo pieceInfo,
|
MeshPieceInfo pieceInfo,
|
||||||
|
Matrix4x4 pieceTransform,
|
||||||
Matrix4x4 mshToViewportTransform,
|
Matrix4x4 mshToViewportTransform,
|
||||||
string reason,
|
string reason,
|
||||||
int geometrySlotIndex = -1)
|
int geometrySlotIndex = -1)
|
||||||
@@ -131,7 +140,7 @@ public static class MshViewportMeshFactory
|
|||||||
id: pieceInfo.Id,
|
id: pieceInfo.Id,
|
||||||
name: pieceInfo.Name,
|
name: pieceInfo.Name,
|
||||||
mesh: marker,
|
mesh: marker,
|
||||||
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform,
|
localTransform: pieceTransform * mshToViewportTransform,
|
||||||
boundsMin: new Vector3(-0.35f, -0.35f, -0.35f),
|
boundsMin: new Vector3(-0.35f, -0.35f, -0.35f),
|
||||||
boundsMax: new Vector3(0.35f, 0.35f, 0.35f),
|
boundsMax: new Vector3(0.35f, 0.35f, 0.35f),
|
||||||
debugInfo: new ViewportPieceDebugInfo
|
debugInfo: new ViewportPieceDebugInfo
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace NResUI.Rendering.Inspection;
|
namespace NResUI.Rendering.Inspection;
|
||||||
|
|
||||||
|
using NResUI.Rendering.Viewport.Msh;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Состояние просмотра меша. State/LOD задаются только для конкретных pieces,
|
/// Состояние просмотра меша. State/LOD задаются только для конкретных pieces,
|
||||||
/// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла.
|
/// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла.
|
||||||
@@ -15,6 +17,10 @@ public sealed class MeshRenderState
|
|||||||
public HashSet<int> HiddenPieceIds { get; } = [];
|
public HashSet<int> HiddenPieceIds { get; } = [];
|
||||||
public Dictionary<int, int> PieceStateOverrides { get; } = [];
|
public Dictionary<int, int> PieceStateOverrides { get; } = [];
|
||||||
public Dictionary<int, int> PieceLodOverrides { get; } = [];
|
public Dictionary<int, int> PieceLodOverrides { get; } = [];
|
||||||
|
public MshAnimationPoseMode AnimationPoseMode { get; set; } = MshAnimationPoseMode.DefaultKeyframe;
|
||||||
|
public float AnimationSampleTime { get; set; }
|
||||||
|
public float AnimationSpeed { get; set; } = 1.0f;
|
||||||
|
public bool AnimationPlay { get; set; }
|
||||||
|
|
||||||
public void ClearSelection()
|
public void ClearSelection()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,4 +27,13 @@ public sealed class MshModelGeometry
|
|||||||
|
|
||||||
/// <summary>Render batches 0x0D: диапазоны индексов, material id и render flags.</summary>
|
/// <summary>Render batches 0x0D: диапазоны индексов, material id и render flags.</summary>
|
||||||
public required IReadOnlyList<Msh0x0D.Batch> Batches { get; init; }
|
public required IReadOnlyList<Msh0x0D.Batch> Batches { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Piece transform keyframes from MSH 0x08.</summary>
|
||||||
|
public required IReadOnlyList<MshTransformKeyframe> TransformKeyframes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Animation frame to 0x08 keyframe map from MSH 0x13. Optional in some files.</summary>
|
||||||
|
public required IReadOnlyList<MshAnimationMapEntry> AnimationMap { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Animation frame count/window from MSH 0x13 NRes metadata.</summary>
|
||||||
|
public required int MaxAnimationTime { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ using NResUI.Rendering.Viewport;
|
|||||||
|
|
||||||
namespace NResUI.Rendering.Viewport.Meshes;
|
namespace NResUI.Rendering.Viewport.Meshes;
|
||||||
|
|
||||||
public sealed class GpuMesh
|
public sealed class GpuMesh : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public uint VertexArrayObject { get; }
|
public uint VertexArrayObject { get; }
|
||||||
public uint VertexBufferObject { get; }
|
public uint VertexBufferObject { get; }
|
||||||
@@ -34,7 +35,21 @@ public sealed class GpuMesh
|
|||||||
|
|
||||||
public unsafe void Draw()
|
public unsafe void Draw()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
_gl.BindVertexArray(VertexArrayObject);
|
_gl.BindVertexArray(VertexArrayObject);
|
||||||
_gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null);
|
_gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_gl.DeleteBuffer(ElementBufferObject);
|
||||||
|
_gl.DeleteBuffer(VertexBufferObject);
|
||||||
|
_gl.DeleteVertexArray(VertexArrayObject);
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,22 @@ public static class MshRestPoseBuilder
|
|||||||
/// Собирает bind/rest pose для pieces из fallback keyframes 0x08.
|
/// Собирает bind/rest pose для pieces из fallback keyframes 0x08.
|
||||||
/// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform.
|
/// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<MshPieceRestPose> BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, List<Msh0x08.AnimationDescriptor> animationDescriptors)
|
public static IReadOnlyList<MshPieceRestPose> BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, IReadOnlyList<MshTransformKeyframe> animationDescriptors)
|
||||||
|
{
|
||||||
|
return BuildPose(
|
||||||
|
nodesComponent,
|
||||||
|
animationDescriptors,
|
||||||
|
[],
|
||||||
|
MshAnimationPoseMode.DefaultKeyframe,
|
||||||
|
0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<MshPieceRestPose> BuildPose(
|
||||||
|
Msh0x01.Msh0x01Component nodesComponent,
|
||||||
|
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
|
||||||
|
IReadOnlyList<MshAnimationMapEntry> animationMap,
|
||||||
|
MshAnimationPoseMode poseMode,
|
||||||
|
float sampleTime)
|
||||||
{
|
{
|
||||||
var nodeList = nodesComponent.Nodes;
|
var nodeList = nodesComponent.Nodes;
|
||||||
var animationList = animationDescriptors;
|
var animationList = animationDescriptors;
|
||||||
@@ -22,7 +37,7 @@ public static class MshRestPoseBuilder
|
|||||||
var state = new byte[nodeList.Count];
|
var state = new byte[nodeList.Count];
|
||||||
|
|
||||||
for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++)
|
for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++)
|
||||||
BuildNodePose(nodeIndex, nodeList, animationList, poses, state);
|
BuildNodePose(nodeIndex, nodeList, animationList, animationMap, poses, state, poseMode, sampleTime);
|
||||||
|
|
||||||
return poses;
|
return poses;
|
||||||
}
|
}
|
||||||
@@ -30,9 +45,12 @@ public static class MshRestPoseBuilder
|
|||||||
private static Matrix4x4 BuildNodePose(
|
private static Matrix4x4 BuildNodePose(
|
||||||
int nodeIndex,
|
int nodeIndex,
|
||||||
List<Msh0x01.Node> nodes,
|
List<Msh0x01.Node> nodes,
|
||||||
List<Msh0x08.AnimationDescriptor> animationDescriptors,
|
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
|
||||||
|
IReadOnlyList<MshAnimationMapEntry> animationMap,
|
||||||
MshPieceRestPose[] poses,
|
MshPieceRestPose[] poses,
|
||||||
byte[] state)
|
byte[] state,
|
||||||
|
MshAnimationPoseMode poseMode,
|
||||||
|
float sampleTime)
|
||||||
{
|
{
|
||||||
if (state[nodeIndex] == 2)
|
if (state[nodeIndex] == 2)
|
||||||
return poses[nodeIndex].MeshSpaceTransform;
|
return poses[nodeIndex].MeshSpaceTransform;
|
||||||
@@ -49,11 +67,10 @@ public static class MshRestPoseBuilder
|
|||||||
var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node);
|
var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node);
|
||||||
var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count;
|
var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count;
|
||||||
|
|
||||||
// var localTransform = Matrix4x4.Identity;
|
|
||||||
Matrix4x4 localTransform;
|
Matrix4x4 localTransform;
|
||||||
if (hasFallbackPose)
|
if (hasFallbackPose)
|
||||||
{
|
{
|
||||||
localTransform = BuildTransformFromAnimationDescriptor(animationDescriptors[fallbackKeyframeIndex]!);
|
localTransform = BuildLocalTransform(node, animationDescriptors, animationMap, poseMode, sampleTime);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -69,7 +86,7 @@ public static class MshRestPoseBuilder
|
|||||||
var meshSpaceTransform = localTransform;
|
var meshSpaceTransform = localTransform;
|
||||||
if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex)
|
if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex)
|
||||||
{
|
{
|
||||||
var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, poses, state);
|
var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, animationMap, poses, state, poseMode, sampleTime);
|
||||||
meshSpaceTransform = localTransform * parentTransform;
|
meshSpaceTransform = localTransform * parentTransform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +102,43 @@ public static class MshRestPoseBuilder
|
|||||||
return meshSpaceTransform;
|
return meshSpaceTransform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Matrix4x4 BuildLocalTransform(
|
||||||
|
Msh0x01.Node node,
|
||||||
|
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
|
||||||
|
IReadOnlyList<MshAnimationMapEntry> animationMap,
|
||||||
|
MshAnimationPoseMode poseMode,
|
||||||
|
float sampleTime)
|
||||||
|
{
|
||||||
|
if (poseMode == MshAnimationPoseMode.AnimationSample)
|
||||||
|
{
|
||||||
|
return MshAnimationSampler.ToMatrix(MshAnimationSampler.SamplePieceAnimationAtTime(
|
||||||
|
node,
|
||||||
|
animationDescriptors,
|
||||||
|
animationMap,
|
||||||
|
sampleTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildTransformFromAnimationDescriptor(animationDescriptors[node.DefaultKeyframeIndex0x08]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidSampleStream(
|
||||||
|
Msh0x01.Node node,
|
||||||
|
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
|
||||||
|
IReadOnlyList<MshAnimationMapEntry> animationMap,
|
||||||
|
float sampleTime)
|
||||||
|
{
|
||||||
|
if (animationMap.Count == 0 || node.AnimMapStart0x13 == ushort.MaxValue)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var frameIndex = (int)MathF.Round(sampleTime - 0.5f, MidpointRounding.AwayFromZero);
|
||||||
|
if (frameIndex < 0 || node.AnimMapStart0x13 + frameIndex >= animationMap.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var keyframeIndex = animationMap[node.AnimMapStart0x13 + frameIndex].KeyframeIndex;
|
||||||
|
return keyframeIndex < node.DefaultKeyframeIndex0x08 &&
|
||||||
|
keyframeIndex + 1 < animationDescriptors.Count;
|
||||||
|
}
|
||||||
|
|
||||||
private static int GetParentIndex(Msh0x01.Node node)
|
private static int GetParentIndex(Msh0x01.Node node)
|
||||||
{
|
{
|
||||||
return node.ParentIndexOrLink == ushort.MaxValue ? -1 : node.ParentIndexOrLink;
|
return node.ParentIndexOrLink == ushort.MaxValue ? -1 : node.ParentIndexOrLink;
|
||||||
@@ -103,14 +157,9 @@ public static class MshRestPoseBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Matrix4x4 BuildTransformFromAnimationDescriptor(Msh0x08.AnimationDescriptor descriptor)
|
private static Matrix4x4 BuildTransformFromAnimationDescriptor(MshTransformKeyframe descriptor)
|
||||||
{
|
{
|
||||||
var position = ToSystemVector3(descriptor.Position);
|
return MshAnimationSampler.ToMatrix(MshAnimationSampler.FromKeyframe(descriptor));
|
||||||
var rotation = ToSystemQuaternion(descriptor.Rotation);
|
|
||||||
|
|
||||||
var matrix = Matrix4x4.CreateFromQuaternion(rotation);
|
|
||||||
matrix.Translation = position;
|
|
||||||
return matrix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 ToSystemVector3(dynamic vector)
|
private static Vector3 ToSystemVector3(dynamic vector)
|
||||||
@@ -143,6 +192,12 @@ public static class MshRestPoseBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum MshAnimationPoseMode
|
||||||
|
{
|
||||||
|
DefaultKeyframe,
|
||||||
|
AnimationSample
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct MshPieceRestPose(
|
public readonly record struct MshPieceRestPose(
|
||||||
int NodeIndex,
|
int NodeIndex,
|
||||||
int ParentIndex,
|
int ParentIndex,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using NResUI.Rendering.Viewport.Meshes;
|
|||||||
|
|
||||||
namespace NResUI.Rendering.Viewport;
|
namespace NResUI.Rendering.Viewport;
|
||||||
|
|
||||||
public sealed class ViewportPiece
|
public sealed class ViewportPiece : IDisposable
|
||||||
{
|
{
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
@@ -75,4 +75,10 @@ public sealed class ViewportPiece
|
|||||||
TriangleCount = 12
|
TriangleCount = 12
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var mesh in Meshes)
|
||||||
|
mesh.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ public sealed class ViewportScene
|
|||||||
|
|
||||||
public void ClearPieces()
|
public void ClearPieces()
|
||||||
{
|
{
|
||||||
|
DisposePieces();
|
||||||
_pieces.Clear();
|
_pieces.Clear();
|
||||||
ClearSelection();
|
ClearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReplacePieces(IEnumerable<ViewportPiece> pieces)
|
public void ReplacePieces(IEnumerable<ViewportPiece> pieces)
|
||||||
{
|
{
|
||||||
|
DisposePieces();
|
||||||
_pieces.Clear();
|
_pieces.Clear();
|
||||||
_pieces.AddRange(pieces);
|
_pieces.AddRange(pieces);
|
||||||
ClearSelection();
|
ClearSelection();
|
||||||
@@ -104,6 +106,12 @@ public sealed class ViewportScene
|
|||||||
return scene;
|
return scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DisposePieces()
|
||||||
|
{
|
||||||
|
foreach (var piece in _pieces)
|
||||||
|
piece.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryTransformBounds(
|
private static bool TryTransformBounds(
|
||||||
Vector3 localMin,
|
Vector3 localMin,
|
||||||
Vector3 localMax,
|
Vector3 localMax,
|
||||||
|
|||||||
Reference in New Issue
Block a user