Skip to content

Commit

Permalink
Merge pull request #1071 from osdanova/master
Browse files Browse the repository at this point in the history
Kh2ObjectEditor - Export model and animations
  • Loading branch information
Oathseeker authored Jul 1, 2024
2 parents 495fdd9 + 5fcf28c commit c7fb58f
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 4 deletions.
156 changes: 154 additions & 2 deletions OpenKh.AssimpUtils/Kh2MdlxAssimp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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");
Expand All @@ -250,6 +253,7 @@ public static Assimp.Scene getAssimpScene(ModelSkeletal model)
Assimp.Node parentNode;
if (bone.ParentIndex == -1)
{
//parentNode = armatureNode;
parentNode = scene.RootNode;
}
else
Expand Down Expand Up @@ -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<float> keyframeTimes = animation.MotionFile.KeyTimes.ToHashSet();

// Get absolute transformation matrices of the bones
Dictionary<float, Matrix4x4[]> frameMatrices = getMatricesForKeyFrames(mdlxBar, animation, keyframeTimes);

// Prepare channels per bone
Dictionary<int, Assimp.NodeAnimationChannel> animationChannelsPerBone = new Dictionary<int, Assimp.NodeAnimationChannel>();
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<ModelCommon.Bone> modelBones = new List<ModelCommon.Bone>();
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<float, Matrix4x4[]> getMatricesForKeyFrames (Bar mdlxBar, AnimationBinary animation, HashSet<float> keyframeTimes)
{
// Mdlx as stream is required
MemoryStream modelStream = new MemoryStream();
Bar.Write(modelStream, mdlxBar);
modelStream.Position = 0;

// Calculate matrices
Dictionary<float, Matrix4x4[]> frameMatrices = new Dictionary<float, Matrix4x4[]>();
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;
}
}
}
1 change: 1 addition & 0 deletions OpenKh.AssimpUtils/OpenKh.AssimpUtils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ProjectReference Include="..\OpenKh.Engine.MonoGame\OpenKh.Engine.MonoGame.csproj" />
<ProjectReference Include="..\OpenKh.Engine\OpenKh.Engine.csproj" />
<ProjectReference Include="..\OpenKh.Kh1\OpenKh.Kh1.csproj" />
<ProjectReference Include="..\OpenKh.Kh2AnimEmu\OpenKh.Kh2AnimEmu.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<Separator/>
<MenuItem Header="Copy motion" Click="Motion_Copy"/>
<MenuItem Header="Replace with copied motion" Click="Motion_Replace"/>
<MenuItem Header="Export FBX animation" Click="Motion_Export"/>
<MenuItem Header="Replace with imported FBX animation" Click="Motion_Import"/>
<Separator/>
<MenuItem Header="Export RC as mset" Click="RC_Export"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Win32;
using OpenKh.AssimpUtils;
using OpenKh.Kh2;
using OpenKh.Tools.Kh2ObjectEditor.Classes;
using OpenKh.Tools.Kh2ObjectEditor.Modules.Motions;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<MenuItem Header="Overwrite Apdx" Click="Menu_Overwrite_Apdx"/>
</MenuItem>
</MenuItem>
<MenuItem Header="Export Model" Click="Menu_ExportModel"/>
</Menu>

<Grid AllowDrop="True" Drop="Window_Drop">
Expand Down
58 changes: 58 additions & 0 deletions OpenKh.Tools.Kh2ObjectEditor/Views/Main_Window.xaml.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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);
}
}
}
}

0 comments on commit c7fb58f

Please sign in to comment.