From 5fcf28c1f2852430a10e83049b386ed8fc165bf5 Mon Sep 17 00:00:00 2001 From: osdanova Date: Sat, 29 Jun 2024 17:51:05 +0200 Subject: [PATCH] Kh2ObjectEditor - Export model and animations --- OpenKh.AssimpUtils/Kh2MdlxAssimp.cs | 156 +++++++++++++++++- OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj | 1 + .../Motions/ModuleMotions_Control.xaml | 1 + .../Motions/ModuleMotions_Control.xaml.cs | 44 +++++ .../Modules/Motions/ModuleMotions_VM.cs | 5 +- .../Views/Main_Window.xaml | 1 + .../Views/Main_Window.xaml.cs | 58 +++++++ 7 files changed, 262 insertions(+), 4 deletions(-) diff --git a/OpenKh.AssimpUtils/Kh2MdlxAssimp.cs b/OpenKh.AssimpUtils/Kh2MdlxAssimp.cs index d7a7882ab..d672083a3 100644 --- a/OpenKh.AssimpUtils/Kh2MdlxAssimp.cs +++ b/OpenKh.AssimpUtils/Kh2MdlxAssimp.cs @@ -2,8 +2,9 @@ using OpenKh.Kh2; using OpenKh.Kh2.Models; using OpenKh.Kh2.Models.VIF; +using OpenKh.Kh2Anim.Mset; +using OpenKh.Kh2Anim.Mset.Interfaces; using System.Numerics; -using OpenKh.Engine.Monogame.Helpers; namespace OpenKh.AssimpUtils { @@ -120,7 +121,7 @@ public static Assimp.Scene getAssimpScene(MdlxParser mParser) parentNode.Children.Add(boneNode); } - MatrixRecursivity.ComputeMatrices(ref matricesToReverse, mParser); + OpenKh.Engine.Monogame.Helpers.MatrixRecursivity.ComputeMatrices(ref matricesToReverse, mParser); foreach (Assimp.Mesh mesh in scene.Meshes) { @@ -242,6 +243,8 @@ public static Assimp.Scene getAssimpScene(ModelSkeletal model) } // BONES (Node hierarchy) + //Assimp.Node armatureNode = new Assimp.Node("Armature"); + //scene.RootNode.Children.Add(armatureNode); foreach (ModelCommon.Bone bone in model.Bones) { string boneName = "Bone" + bone.Index.ToString("D4"); @@ -250,6 +253,7 @@ public static Assimp.Scene getAssimpScene(ModelSkeletal model) Assimp.Node parentNode; if (bone.ParentIndex == -1) { + //parentNode = armatureNode; parentNode = scene.RootNode; } else @@ -363,10 +367,158 @@ public static VifMesh getVifMeshFromAssimp(Assimp.Mesh mesh, Matrix4x4[] boneMat return vifMesh; } + public static void AddAnimation(Assimp.Scene assimpScene, Bar mdlxBar, AnimationBinary animation) + { + // Set basic data + Assimp.Animation assimpAnimation = new Assimp.Animation(); + assimpAnimation.Name = "EXPORT"; + assimpAnimation.DurationInTicks = animation.MotionFile.InterpolatedMotionHeader.FrameCount; + assimpAnimation.TicksPerSecond = animation.MotionFile.InterpolatedMotionHeader.FrameData.FramesPerSecond; + assimpScene.Animations.Add(assimpAnimation); + + HashSet keyframeTimes = animation.MotionFile.KeyTimes.ToHashSet(); + + // Get absolute transformation matrices of the bones + Dictionary frameMatrices = getMatricesForKeyFrames(mdlxBar, animation, keyframeTimes); + + // Prepare channels per bone + Dictionary animationChannelsPerBone = new Dictionary(); + for (int i = 0; i < animation.MotionFile.InterpolatedMotionHeader.BoneCount; i++) + { + Assimp.NodeAnimationChannel nodeAnimChannel = new Assimp.NodeAnimationChannel(); + nodeAnimChannel.NodeName = "Bone" + i.ToString("D4"); + animationChannelsPerBone.Add(i, nodeAnimChannel); + assimpAnimation.NodeAnimationChannels.Add(nodeAnimChannel); + } + + // Get bone data + List modelBones = new List(); + foreach (Bar.Entry barEntry in mdlxBar) + { + if(barEntry.Type == Bar.EntryType.Model) + { + ModelSkeletal modelFile = ModelSkeletal.Read(barEntry.Stream); + modelBones = modelFile.Bones; + break; + } + } + + // Set channels + foreach (float keyTime in frameMatrices.Keys) // Frame + { + for (int j = 0; j < frameMatrices[keyTime].Length; j++) // Bone + { + Assimp.NodeAnimationChannel channel = animationChannelsPerBone[j]; + + Matrix4x4 currentFrameBoneMatrix = frameMatrices[keyTime][j]; + + // Transform to local + if (modelBones[j].ParentIndex != -1) + { + Matrix4x4.Invert(frameMatrices[keyTime][modelBones[j].ParentIndex], out Matrix4x4 invertedParent); + currentFrameBoneMatrix *= invertedParent; + } + + Assimp.Matrix4x4 assimpMatrix = AssimpGeneric.ToAssimp(currentFrameBoneMatrix); + assimpMatrix.Decompose(out Assimp.Vector3D scaling, out Assimp.Quaternion rotation, out Assimp.Vector3D translation); + + Assimp.VectorKey positionKey = new Assimp.VectorKey(keyTime / assimpAnimation.TicksPerSecond, translation); + Assimp.VectorKey scalingKey = new Assimp.VectorKey(keyTime / assimpAnimation.TicksPerSecond, new Assimp.Vector3D(RoundFloat(scaling.X), RoundFloat(scaling.Y), RoundFloat(scaling.Z))); + //Assimp.VectorKey scalingKey = new Assimp.VectorKey(keyTime / assimpAnimation.TicksPerSecond, scaling); + Assimp.QuaternionKey rotationKey = new Assimp.QuaternionKey(keyTime / assimpAnimation.TicksPerSecond, rotation); + + // Ignore duplicates + if(channel.PositionKeys.Count > 0) + { + Assimp.VectorKey previousPositionKey = channel.PositionKeys[channel.PositionKeys.Count - 1]; + Assimp.VectorKey previousScalingKey = channel.ScalingKeys[channel.ScalingKeys.Count - 1]; + Assimp.QuaternionKey previousRotationKey = channel.RotationKeys[channel.RotationKeys.Count - 1]; + + if (!positionKey.Equals(previousPositionKey)) + { + channel.PositionKeys.Add(positionKey); + } + if (!scalingKey.Equals(previousScalingKey)) + { + channel.ScalingKeys.Add(scalingKey); + } + if (!rotationKey.Equals(previousRotationKey)) + { + channel.RotationKeys.Add(rotationKey); + } + } + else + { + channel.PositionKeys.Add(positionKey); + channel.ScalingKeys.Add(scalingKey); + channel.RotationKeys.Add(rotationKey); + } + } + } + + if(assimpScene.RootNode.FindNode("Armature") != null) + { + assimpAnimation.NodeAnimationChannels.Add(getArmatureChannel(keyframeTimes.ToArray()[0] / assimpAnimation.TicksPerSecond, assimpAnimation.DurationInTicks, animation.MotionFile.InterpolatedMotionHeader.FrameData.FramesPerSecond)); + } + } + /**************** * UTILITIES ****************/ private static Vector3 ToVector3(Vector4 pos) => new Vector3(pos.X, pos.Y, pos.Z); + private static float RoundFloat(float value) + { + float reminder = value % 1; + if (reminder > 0.999999 && reminder < 0.999999999999) + { + return value - reminder + 1; + } + else if (reminder > 0 && reminder < 0.00001) + { + return value - reminder; + } + return value; + } + + private static Assimp.NodeAnimationChannel getArmatureChannel(double startFrame, double endFrame, double framesPerSecond) + { + Assimp.NodeAnimationChannel armatureChannel = new Assimp.NodeAnimationChannel(); + armatureChannel.NodeName = "Armature"; + + armatureChannel.PositionKeys.Add(new Assimp.VectorKey(startFrame / framesPerSecond, new Assimp.Vector3D(0,0,0))); + armatureChannel.ScalingKeys.Add(new Assimp.VectorKey(startFrame / framesPerSecond, new Assimp.Vector3D(1, 1, 1))); + armatureChannel.RotationKeys.Add(new Assimp.QuaternionKey(startFrame / framesPerSecond, new Assimp.Quaternion(1, 0, 0, 0))); + + armatureChannel.PositionKeys.Add(new Assimp.VectorKey(endFrame / framesPerSecond, new Assimp.Vector3D(0, 0, 0))); + armatureChannel.ScalingKeys.Add(new Assimp.VectorKey(endFrame / framesPerSecond, new Assimp.Vector3D(1, 1, 1))); + armatureChannel.RotationKeys.Add(new Assimp.QuaternionKey(endFrame / framesPerSecond, new Assimp.Quaternion(1, 0, 0, 0))); + + return armatureChannel; + } + + // Generates the absolute transformation matrices for each bone for the given frames + // Makes use of IAnimMatricesProvider to generate the matrices + private static Dictionary getMatricesForKeyFrames (Bar mdlxBar, AnimationBinary animation, HashSet keyframeTimes) + { + // Mdlx as stream is required + MemoryStream modelStream = new MemoryStream(); + Bar.Write(modelStream, mdlxBar); + modelStream.Position = 0; + + // Calculate matrices + Dictionary frameMatrices = new Dictionary(); + Bar anbBarFile = Bar.Read(animation.toStream()); + foreach (float keyTime in keyframeTimes) + { + // I have no idea why this needs to be done for every frame but otherwise it won't work properly + AnbIndir currentAnb = new AnbIndir(anbBarFile); + IAnimMatricesProvider AnimMatricesProvider = currentAnb.GetAnimProvider(modelStream); + frameMatrices.Add(keyTime, AnimMatricesProvider.ProvideMatrices(keyTime)); + modelStream.Position = 0; + } + + return frameMatrices; + } } } diff --git a/OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj b/OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj index cf3159153..526aeb709 100644 --- a/OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj +++ b/OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj @@ -15,6 +15,7 @@ + diff --git a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml index dcc0cefc6..1ae93133e 100644 --- a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml +++ b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml @@ -60,6 +60,7 @@ + diff --git a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml.cs b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml.cs index a342e5dce..c0f01068f 100644 --- a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml.cs +++ b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_Control.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.Win32; +using OpenKh.AssimpUtils; using OpenKh.Kh2; using OpenKh.Tools.Kh2ObjectEditor.Classes; using OpenKh.Tools.Kh2ObjectEditor.Modules.Motions; @@ -129,6 +130,49 @@ public void Motion_Replace(object sender, RoutedEventArgs e) ThisVM.Motion_Replace(item.Index); } } + public void Motion_Export(object sender, RoutedEventArgs e) + { + if (MotionList.SelectedItem == null || MdlxService.Instance.ModelFile == null) + { + return; + } + + MotionSelector_Wrapper item = (MotionSelector_Wrapper)MotionList.SelectedItem; + AnimationBinary animation; + using (MemoryStream memStream = new MemoryStream(item.LinkedSubfile)) + { + animation = new AnimationBinary(memStream); + } + + Kh2.Models.ModelSkeletal model = null; + foreach(Bar.Entry barEntry in MdlxService.Instance.MdlxBar) + { + if(barEntry.Type == Bar.EntryType.Model) + { + model = Kh2.Models.ModelSkeletal.Read(barEntry.Stream); + barEntry.Stream.Position = 0; + } + } + Assimp.Scene scene = Kh2MdlxAssimp.getAssimpScene(model); + Kh2MdlxAssimp.AddAnimation(scene, MdlxService.Instance.MdlxBar, animation); + + System.Windows.Forms.SaveFileDialog sfd; + sfd = new System.Windows.Forms.SaveFileDialog(); + sfd.Title = "Export animated model"; + sfd.FileName = MdlxService.Instance.MdlxPath + "." + AssimpGeneric.GetFormatFileExtension(AssimpGeneric.FileFormat.fbx); + sfd.ShowDialog(); + if (sfd.FileName != "") + { + string dirPath = Path.GetDirectoryName(sfd.FileName); + + if (!Directory.Exists(dirPath)) + return; + + dirPath += "\\"; + + AssimpGeneric.ExportScene(scene, AssimpGeneric.FileFormat.fbx, sfd.FileName); + } + } public void Motion_Import(object sender, RoutedEventArgs e) { if (MotionList.SelectedItem == null) { diff --git a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_VM.cs b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_VM.cs index 0291577d3..3f94b836e 100644 --- a/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_VM.cs +++ b/OpenKh.Tools.Kh2ObjectEditor/Modules/Motions/ModuleMotions_VM.cs @@ -95,6 +95,7 @@ public void Motion_Copy(int index) public void Motion_Replace(int index) { MsetService.Instance.MsetBinarc.Subfiles.Add(ClipboardService.Instance.FetchMotion()); + MsetService.Instance.MsetBinarc.Entries[index].Type = BinaryArchive.EntryType.Anb; MsetService.Instance.MsetBinarc.Entries[index].Name = "COPY"; MsetService.Instance.MsetBinarc.Entries[index].Link = MsetService.Instance.MsetBinarc.Subfiles.Count - 1; @@ -132,14 +133,14 @@ public void Motion_Import(int index, string animationPath) // Get as stream MemoryStream motionStream = (MemoryStream)ipm.toStream(); - + // Insert to mset int subfileIndex = MsetService.Instance.MsetBinarc.Entries[index].Link; MemoryStream replaceMotionStream = new MemoryStream(MsetService.Instance.MsetBinarc.Subfiles[subfileIndex]); AnimationBinary msetEntry = new AnimationBinary(replaceMotionStream); msetEntry.MotionFile = new Motion.InterpolatedMotion(motionStream); MsetService.Instance.MsetBinarc.Subfiles[subfileIndex] = ((MemoryStream)msetEntry.toStream()).ToArray(); - + loadMotions(); } public void Motion_ImportRC(int index, string msetPath) diff --git a/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml b/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml index 5467e7972..296d8bba5 100644 --- a/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml +++ b/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml @@ -30,6 +30,7 @@ + diff --git a/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml.cs b/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml.cs index 8629b8a10..17bf88506 100644 --- a/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml.cs +++ b/OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml.cs @@ -1,10 +1,14 @@ using Microsoft.Win32; +using OpenKh.AssimpUtils; +using OpenKh.Kh2; +using OpenKh.Tools.Common.Wpf; using OpenKh.Tools.Kh2ObjectEditor.Services; using OpenKh.Tools.Kh2ObjectEditor.Utils; using OpenKh.Tools.Kh2ObjectEditor.ViewModel; using System.IO; using System.Linq; using System.Windows; +using System.Windows.Media.Imaging; namespace OpenKh.Tools.Kh2ObjectEditor.Views { @@ -155,6 +159,60 @@ private void Menu_OpenMdlx(object sender, RoutedEventArgs e) } } + private void Menu_ExportModel(object sender, RoutedEventArgs e) + { + if(MdlxService.Instance.ModelFile != null) + { + Kh2.Models.ModelSkeletal model = null; + foreach (Bar.Entry barEntry in MdlxService.Instance.MdlxBar) + { + if (barEntry.Type == Bar.EntryType.Model) + { + model = Kh2.Models.ModelSkeletal.Read(barEntry.Stream); + barEntry.Stream.Position = 0; + } + } + Assimp.Scene scene = Kh2MdlxAssimp.getAssimpScene(model); + + System.Windows.Forms.SaveFileDialog sfd; + sfd = new System.Windows.Forms.SaveFileDialog(); + sfd.Title = "Export model"; + sfd.FileName = MdlxService.Instance.MdlxPath + "." + AssimpGeneric.GetFormatFileExtension(AssimpGeneric.FileFormat.fbx); + sfd.ShowDialog(); + if (sfd.FileName != "") + { + string dirPath = Path.GetDirectoryName(sfd.FileName); + + if (!Directory.Exists(dirPath)) + return; + + dirPath += "\\"; + + AssimpGeneric.ExportScene(scene, AssimpGeneric.FileFormat.fbx, sfd.FileName); + exportTextures(dirPath); + } + } + } + + public void exportTextures(string filePath) + { + for (int i = 0; i < MdlxService.Instance.TextureFile.Images.Count; i++) + { + ModelTexture.Texture texture = MdlxService.Instance.TextureFile.Images[i]; + BitmapSource bitmapImage = texture.GetBimapSource(); + + string fullPath = filePath + "Texture" + i.ToString("D4"); + string finalPath = fullPath; + int repeat = 0; + while (File.Exists(finalPath)) + { + repeat++; + finalPath = fullPath + " (" + repeat + ")"; + } + + AssimpGeneric.ExportBitmapSourceAsPng(bitmapImage, fullPath); + } + } } }