using System; using System.Drawing; using System.IO; using System.Collections.Generic; using System.Linq; using Assimp; using OpenTK; using Toolbox.Library.Rendering; using System.Windows.Forms; using Toolbox.Library.Animations; using Toolbox.Library.Forms; namespace Toolbox.Library { public class AssimpSaver { private List<string> ExtractedTextures = new List<string>(); public List<string> BoneNames = new List<string>(); STProgressBar progressBar; public void SaveFromModel(STGenericModel model, string FileName, List<STGenericTexture> Textures, STSkeleton skeleton = null, List<int> NodeArray = null) { SaveFromModel(model.Objects.ToList(), model.Materials.ToList(), FileName, Textures, skeleton, NodeArray); } public void SaveFromModel(List<STGenericObject> Meshes, List<STGenericMaterial> Materials, string FileName, List<STGenericTexture> Textures, STSkeleton skeleton = null, List<int> NodeArray = null) { ExtractedTextures.Clear(); Scene scene = new Scene(); scene.RootNode = new Node("RootNode"); progressBar = new STProgressBar(); progressBar.Task = "Exporting Skeleton..."; progressBar.Value = 0; progressBar.StartPosition = FormStartPosition.CenterScreen; progressBar.Show(); progressBar.Refresh(); SaveSkeleton(skeleton, scene.RootNode); SaveMaterials(scene, Materials, FileName, Textures); progressBar.Task = "Exporting Meshes..."; progressBar.Value = 50; SaveMeshes(scene, Meshes, skeleton, FileName, NodeArray); progressBar.Task = "Saving File..."; progressBar.Value = 80; SaveScene(FileName, scene, Meshes); progressBar.Value = 100; progressBar.Close(); progressBar.Dispose(); } private void SaveScene(string FileName, Scene scene, List<STGenericObject> Meshes) { using (var v = new AssimpContext()) { string ext = System.IO.Path.GetExtension(FileName); string formatID = "collada"; if (ext == ".obj") formatID = "obj"; if (ext == ".3ds") formatID = "3ds"; if (ext == ".dae") formatID = "collada"; if (ext == ".ply") formatID = "ply"; bool ExportSuccessScene = v.ExportFile(scene, FileName, formatID, PostProcessSteps.FlipUVs); if (ExportSuccessScene) { if (ext == ".dae") WriteExtraSkinningInfo(FileName, scene, Meshes); MessageBox.Show($"Exported {FileName} Successfuly!"); } else MessageBox.Show($"Failed to export {FileName}!"); } } private void SaveMeshes(Scene scene, List<STGenericObject> Meshes, STSkeleton skeleton, string FileName, List<int> NodeArray) { int MeshIndex = 0; foreach (var obj in Meshes) { var mesh = SaveMesh((STGenericObject)obj, scene, MeshIndex++, skeleton, NodeArray); scene.Meshes.Add(mesh); } Node geomNode = new Node(Path.GetFileNameWithoutExtension(FileName), scene.RootNode); for (int ob = 0; ob < scene.MeshCount; ob++) { geomNode.MeshIndices.Add(ob); // if (!scene.Meshes[ob].HasBones) } scene.RootNode.Children.Add(geomNode); } private Mesh SaveMesh(STGenericObject genericObj, Scene scene, int index, STSkeleton skeleton, List<int> NodeArray) { //Assimp is weird so use mesh_# for the name. We'll change it back after save Mesh mesh = new Mesh($"mesh_{ index }", PrimitiveType.Triangle); if (genericObj.MaterialIndex < scene.MaterialCount && genericObj.MaterialIndex > 0) mesh.MaterialIndex = genericObj.MaterialIndex; else mesh.MaterialIndex = 0; List<Vector3D> textureCoords0 = new List<Vector3D>(); List<Vector3D> textureCoords1 = new List<Vector3D>(); List<Vector3D> textureCoords2 = new List<Vector3D>(); List<Color4D> vertexColors = new List<Color4D>(); int vertexID = 0; foreach (Vertex v in genericObj.vertices) { mesh.Vertices.Add(new Vector3D(v.pos.X, v.pos.Y, v.pos.Z)); mesh.Normals.Add(new Vector3D(v.nrm.X, v.nrm.Y, v.nrm.Z)); textureCoords0.Add(new Vector3D(v.uv0.X, v.uv0.Y, 0)); textureCoords1.Add(new Vector3D(v.uv1.X, v.uv1.Y, 0)); textureCoords2.Add(new Vector3D(v.uv2.X, v.uv2.Y, 0)); vertexColors.Add(new Color4D(v.col.X, v.col.Y, v.col.Z, v.col.W)); if (skeleton != null) { for (int j = 0; j < v.boneIds.Count; j++) { if (j < genericObj.VertexSkinCount) { STBone STbone = null; if (NodeArray != null) { //Get the bone via the node array and bone index from the vertex STbone = skeleton.bones[NodeArray[v.boneIds[j]]]; } else STbone = skeleton.bones[v.boneIds[j]]; //Find the index of a bone. If it doesn't exist then we add it int boneInd = mesh.Bones.FindIndex(x => x.Name == STbone.Text); if (boneInd == -1) { var matrices = Toolbox.Library.IO.MatrixExenstion.CalculateInverseMatrix(STbone); //Set the inverse matrix Matrix4x4 transform = matrices.inverse.FromNumerics(); //Create a new assimp bone Bone bone = new Bone(); bone.Name = STbone.Text; bone.OffsetMatrix = STbone.invert.ToMatrix4x4(); mesh.Bones.Add(bone); BoneNames.Add(bone.Name); boneInd = mesh.Bones.IndexOf(bone); //Set the index of the bone for the vertex weight } int MinWeightAmount = 0; //Check if the max amount of weights is higher than the current bone id if (v.boneWeights.Count > j && v.boneWeights[j] > MinWeightAmount) { if (v.boneWeights[j] <= 1) mesh.Bones[boneInd].VertexWeights.Add(new VertexWeight(vertexID, v.boneWeights[j])); else mesh.Bones[boneInd].VertexWeights.Add(new VertexWeight(vertexID, 1)); } else if (v.boneWeights.Count == 0 || v.boneWeights[j] > MinWeightAmount) mesh.Bones[boneInd].VertexWeights.Add(new VertexWeight(vertexID, 1)); } } } vertexID++; } if (genericObj.lodMeshes.Count != 0) { List<int> faces = genericObj.lodMeshes[genericObj.DisplayLODIndex].faces; for (int f = 0; f < faces.Count; f++) mesh.Faces.Add(new Face(new int[] { faces[f++], faces[f++], faces[f] })); } if (genericObj.PolygonGroups.Count != 0) { for (int p = 0; p < genericObj.PolygonGroups.Count; p++) { var polygonGroup = genericObj.PolygonGroups[p]; for (int f = 0; f < polygonGroup.faces.Count; f++) if (f < polygonGroup.faces.Count - 2) mesh.Faces.Add(new Face(new int[] { polygonGroup.faces[f++], polygonGroup.faces[f++], polygonGroup.faces[f] })); } } mesh.TextureCoordinateChannels.SetValue(textureCoords0, 0); mesh.TextureCoordinateChannels.SetValue(textureCoords1, 1); mesh.TextureCoordinateChannels.SetValue(textureCoords2, 2); mesh.VertexColorChannels.SetValue(vertexColors, 0); return mesh; } //Extra skin data based on https://github.com/Sage-of-Mirrors/SuperBMD/blob/ce1061e9b5f57de112f1d12f6459b938594664a0/SuperBMDLib/source/Model.cs#L193 //Todo this doesn't quite work yet //Need to adjust all mesh name IDs so they are correct private void WriteExtraSkinningInfo(string FileName, Scene outScene, List<STGenericObject> Meshes) { StreamWriter test = new StreamWriter(FileName + ".tmp"); StreamReader dae = File.OpenText(FileName); int geomIndex = 0; while (!dae.EndOfStream) { string line = dae.ReadLine(); /* if (line == " <library_visual_scenes>") { AddControllerLibrary(outScene, test); test.WriteLine(line); test.Flush(); } else if (line.Contains("<node")) { // test.WriteLine(line); // test.Flush(); string[] testLn = line.Split('\"'); string name = testLn[3]; string jointLine = line.Replace(">", $" sid=\"{ name }\" type=\"JOINT\">"); test.WriteLine(jointLine); test.Flush(); } else if (line.Contains("</visual_scene>")) { foreach (Mesh mesh in outScene.Meshes) { test.WriteLine($" <node id=\"{ mesh.Name }\" name=\"{ mesh.Name }\" type=\"NODE\">"); test.WriteLine($" <instance_controller url=\"#{ mesh.Name }-skin\">"); test.WriteLine(" <skeleton>#skeleton_root</skeleton>"); test.WriteLine(" <bind_material>"); test.WriteLine(" <technique_common>"); test.WriteLine($" <instance_material symbol=\"theresonlyone\" target=\"#m{ mesh.MaterialIndex }mat\" />"); test.WriteLine(" </technique_common>"); test.WriteLine(" </bind_material>"); test.WriteLine(" </instance_controller>"); test.WriteLine(" </node>"); test.Flush(); } test.WriteLine(line); test.Flush(); }*/ if (line.Contains("<geometry")) { string RealMeshName = Meshes[geomIndex].Text; test.WriteLine($" <geometry id=\"meshId{ geomIndex }\" name=\"{ RealMeshName }\" > "); test.Flush(); geomIndex++; } else { test.WriteLine(line); test.Flush(); } /* else if (line.Contains("<matrix")) { string matLine = line.Replace("<matrix>", "<matrix sid=\"matrix\">"); test.WriteLine(matLine); test.Flush(); }*/ } test.Close(); dae.Close(); File.Copy(FileName + ".tmp", FileName, true); File.Delete(FileName + ".tmp"); } private void AddControllerLibrary(Scene scene, StreamWriter writer) { writer.WriteLine(" <library_controllers>"); for (int i = 0; i < scene.MeshCount; i++) { Mesh curMesh = scene.Meshes[i]; curMesh.Name = curMesh.Name.Replace('_', '-'); writer.WriteLine($" <controller id=\"{ curMesh.Name }-skin\" name=\"{ curMesh.Name }Skin\">"); writer.WriteLine($" <skin source=\"#meshId{ i }\">"); WriteBindShapeMatrixToStream(writer); WriteJointNameArrayToStream(curMesh, writer); WriteInverseBindMatricesToStream(curMesh, writer); WriteSkinWeightsToStream(curMesh, writer); writer.WriteLine(" <joints>"); writer.WriteLine($" <input semantic=\"JOINT\" source=\"#{ curMesh.Name }-skin-joints-array\"></input>"); writer.WriteLine($" <input semantic=\"INV_BIND_MATRIX\" source=\"#{ curMesh.Name }-skin-bind_poses-array\"></input>"); writer.WriteLine(" </joints>"); writer.Flush(); WriteVertexWeightsToStream(curMesh, writer); writer.WriteLine(" </skin>"); writer.WriteLine(" </controller>"); writer.Flush(); } writer.WriteLine(" </library_controllers>"); writer.Flush(); } private void WriteJointNameArrayToStream(Mesh mesh, StreamWriter writer) { writer.WriteLine($" <source id =\"{ mesh.Name }-skin-joints-array\">"); writer.WriteLine($" <Name_array id=\"{ mesh.Name }-skin-joints-array\" count=\"{ mesh.Bones.Count }\">"); writer.Write(" "); foreach (Bone bone in mesh.Bones) { writer.Write($"{ bone.Name }"); if (bone != mesh.Bones.Last()) writer.Write(' '); else writer.Write('\n'); writer.Flush(); } writer.WriteLine(" </Name_array>"); writer.Flush(); writer.WriteLine(" <technique_common>"); writer.WriteLine($" <accessor source=\"#{ mesh.Name }-skin-joints-array\" count=\"{ mesh.Bones.Count }\" stride=\"1\">"); writer.WriteLine(" <param name=\"JOINT\" type=\"Name\"></param>"); writer.WriteLine(" </accessor>"); writer.WriteLine(" </technique_common>"); writer.WriteLine(" </source>"); writer.Flush(); } private void WriteInverseBindMatricesToStream(Mesh mesh, StreamWriter writer) { writer.WriteLine($" <source id =\"{ mesh.Name }-skin-bind_poses-array\">"); writer.WriteLine($" <float_array id=\"{ mesh.Name }-skin-bind_poses-array\" count=\"{ mesh.Bones.Count * 16 }\">"); foreach (Bone bone in mesh.Bones) { Matrix4x4 ibm = bone.OffsetMatrix; ibm.Transpose(); writer.WriteLine($" {ibm.A1.ToString("F")} {ibm.A2.ToString("F")} {ibm.A3.ToString("F")} {ibm.A4.ToString("F")}"); writer.WriteLine($" {ibm.B1.ToString("F")} {ibm.B2.ToString("F")} {ibm.B3.ToString("F")} {ibm.B4.ToString("F")}"); writer.WriteLine($" {ibm.C1.ToString("F")} {ibm.C2.ToString("F")} {ibm.C3.ToString("F")} {ibm.C4.ToString("F")}"); writer.WriteLine($" {ibm.D1.ToString("F")} {ibm.D2.ToString("F")} {ibm.D3.ToString("F")} {ibm.D4.ToString("F")}"); if (bone != mesh.Bones.Last()) writer.WriteLine(""); } writer.WriteLine(" </float_array>"); writer.Flush(); writer.WriteLine(" <technique_common>"); writer.WriteLine($" <accessor source=\"#{ mesh.Name }-skin-bind_poses-array\" count=\"{ mesh.Bones.Count }\" stride=\"16\">"); writer.WriteLine(" <param name=\"TRANSFORM\" type=\"float4x4\"></param>"); writer.WriteLine(" </accessor>"); writer.WriteLine(" </technique_common>"); writer.WriteLine(" </source>"); writer.Flush(); } private void WriteSkinWeightsToStream(Mesh mesh, StreamWriter writer) { int totalWeightCount = 0; foreach (Bone bone in mesh.Bones) { totalWeightCount += bone.VertexWeightCount; } writer.WriteLine($" <source id =\"{ mesh.Name }-skin-weights-array\">"); writer.WriteLine($" <float_array id=\"{ mesh.Name }-skin-weights-array\" count=\"{ totalWeightCount }\">"); writer.Write(" "); foreach (Bone bone in mesh.Bones) { foreach (VertexWeight weight in bone.VertexWeights) { writer.Write($"{ weight.Weight } "); } if (bone == mesh.Bones.Last()) writer.WriteLine(); } writer.WriteLine(" </float_array>"); writer.Flush(); writer.WriteLine(" <technique_common>"); writer.WriteLine($" <accessor source=\"#{ mesh.Name }-skin-weights-array\" count=\"{ totalWeightCount }\" stride=\"1\">"); writer.WriteLine(" <param name=\"WEIGHT\" type=\"float\"></param>"); writer.WriteLine(" </accessor>"); writer.WriteLine(" </technique_common>"); writer.WriteLine(" </source>"); writer.Flush(); } private class RiggedWeight { public List<float> Weights { get; private set; } public List<int> BoneIndices { get; private set; } public int WeightCount { get; private set; } public RiggedWeight() { Weights = new List<float>(); BoneIndices = new List<int>(); } public void AddWeight(float weight, int boneIndex) { Weights.Add(weight); BoneIndices.Add(boneIndex); WeightCount++; } } private void WriteVertexWeightsToStream(Mesh mesh, StreamWriter writer) { List<float> weights = new List<float>(); Dictionary<int, RiggedWeight> vertIDWeights = new Dictionary<int, RiggedWeight>(); foreach (Bone bone in mesh.Bones) { foreach (VertexWeight weight in bone.VertexWeights) { weights.Add(weight.Weight); if (!vertIDWeights.ContainsKey(weight.VertexID)) vertIDWeights.Add(weight.VertexID, new RiggedWeight()); vertIDWeights[weight.VertexID].AddWeight(weight.Weight, mesh.Bones.IndexOf(bone)); } } writer.WriteLine($" <vertex_weights count=\"{ vertIDWeights.Count }\">"); writer.WriteLine($" <input semantic=\"JOINT\" source=\"#{ mesh.Name }-skin-joints-array\" offset=\"0\"></input>"); writer.WriteLine($" <input semantic=\"WEIGHT\" source=\"#{ mesh.Name }-skin-weights-array\" offset=\"1\"></input>"); writer.WriteLine(" <vcount>"); writer.Write(" "); for (int i = 0; i < vertIDWeights.Count; i++) writer.Write($"{ vertIDWeights[i].WeightCount } "); writer.WriteLine("\n </vcount>"); writer.WriteLine(" <v>"); writer.Write(" "); for (int i = 0; i < vertIDWeights.Count; i++) { RiggedWeight curWeight = vertIDWeights[i]; for (int j = 0; j < curWeight.WeightCount; j++) { writer.Write($"{ curWeight.BoneIndices[j] } { weights.IndexOf(curWeight.Weights[j]) } "); } } writer.WriteLine("\n </v>"); writer.WriteLine($" </vertex_weights>"); } private void WriteBindShapeMatrixToStream(StreamWriter writer) { writer.WriteLine(" <bind_shape_matrix>"); writer.WriteLine(" 1 0 0 0"); writer.WriteLine(" 0 1 0 0"); writer.WriteLine(" 0 0 1 0"); writer.WriteLine(" 0 0 0 1"); writer.WriteLine(" </bind_shape_matrix>"); writer.Flush(); } private void SaveMaterials(Scene scene, List<STGenericMaterial> Materials, string FileName, List<STGenericTexture> Textures) { string TextureExtension = ".png"; string TexturePath = System.IO.Path.GetDirectoryName(FileName); for (int i = 0; i < Textures.Count; i++) { string path = System.IO.Path.Combine(TexturePath, Textures[i].Text + TextureExtension); if (!ExtractedTextures.Contains(path)) { ExtractedTextures.Add(path); progressBar.Task = $"Exporting Texture {Textures[i].Text}"; progressBar.Value = ((i * 100) / Textures.Count); progressBar.Refresh(); var bitmap = Textures[i].GetBitmap(); bitmap.Save(path); bitmap.Dispose(); GC.Collect(); } } if (Materials.Count == 0) { Material material = new Material(); material.Name = "New Material"; scene.Materials.Add(material); return; } foreach (var mat in Materials) { var genericMat = (STGenericMaterial)mat; Material material = new Material(); material.Name = genericMat.Text; foreach (var tex in genericMat.TextureMaps) { int index = Textures.FindIndex(r => r.Text.Equals(tex.Name)); string path = System.IO.Path.Combine(TexturePath, tex.Name + TextureExtension); if (!File.Exists(path)) continue; TextureSlot slot2 = new TextureSlot(path, ConvertToAssimpTextureType(tex.Type), 0, TextureMapping.FromUV, 0, 1.0f, Assimp.TextureOperation.Add, ConvertToAssimpWrapType(tex.WrapModeS), ConvertToAssimpWrapType(tex.WrapModeT), 0); material.AddMaterialTexture(ref slot2); } scene.Materials.Add(material); } } private static Assimp.TextureWrapMode ConvertToAssimpWrapType(STTextureWrapMode type) { switch (type) { case STTextureWrapMode.Repeat: return TextureWrapMode.Wrap; case STTextureWrapMode.Mirror: return TextureWrapMode.Mirror; case STTextureWrapMode.Clamp: return TextureWrapMode.Clamp; default: return TextureWrapMode.Wrap; } } private static Assimp.TextureType ConvertToAssimpTextureType(STGenericMatTexture.TextureType type) { switch (type) { case STGenericMatTexture.TextureType.Diffuse: return TextureType.Diffuse; case STGenericMatTexture.TextureType.AO: return TextureType.Ambient; case STGenericMatTexture.TextureType.Normal: return TextureType.Normals; case STGenericMatTexture.TextureType.Light: return TextureType.Lightmap; case STGenericMatTexture.TextureType.Emission: return TextureType.Emissive; case STGenericMatTexture.TextureType.Specular: return TextureType.Specular; default: return TextureType.Unknown; } } public void SaveFromObject(STGenericObject genericObject, string FileName) { Scene scene = new Scene(); scene.RootNode = new Node("Root"); var mesh = SaveMesh(genericObject, scene, 0, null, null); mesh.MaterialIndex = 0; scene.Meshes.Add(mesh); Material material = new Material(); material.Name = "NewMaterial"; scene.Materials.Add(material); SaveScene(FileName, scene, new List<STGenericObject>() { genericObject }); } private void SaveSkeleton(STSkeleton skeleton, Node parentNode) { Node root = new Node("skeleton_root"); parentNode.Children.Add(root); Console.WriteLine($"bones {skeleton.bones.Count}"); if (skeleton.bones.Count > 0) { foreach (var bone in skeleton.bones) { //Get each root bone and find children if (bone.parentIndex == -1) { Node boneNode = new Node(bone.Text); boneNode.Transform = AssimpHelper.GetBoneMatrix(bone); root.Children.Add(boneNode); foreach (STBone child in bone.GetChildren()) SaveBones(boneNode, child, skeleton); } } } } private void SaveBones(Node parentBone, STBone bone, STSkeleton skeleton) { Node boneNode = new Node(bone.Text); parentBone.Children.Add(boneNode); boneNode.Transform = AssimpHelper.GetBoneMatrix(bone); foreach (STBone child in bone.GetChildren()) SaveBones(boneNode, child, skeleton); } } }