// NOTE: SimplifiedModel (based on the origin BCRES) is passed in because we can't // guarantee that the model edited file has ALL of the data it originally contained, // just the same as the SimplifiedModel isn't enough by itself and still needs the // origin BCRES to generate all other data! public static void FromMilkShape(SimplifiedModel simplifiedModel, MilkShape milkShape) { // Take the MilkShape stored bones and remap them to the CGFX bones. var milkShapeBoneMap = simplifiedModel.Bones .Select(b => new { b.Name, MilkShapeJoint = milkShape.Joints.Where(j => j.Name == b.Name), SMBone = b }) .Select(b => new MilkShapeBoneMap { Name = b.Name, MilkShapeJointIndex = (b.MilkShapeJoint.Count() == 1) ? milkShape.Joints.IndexOf(b.MilkShapeJoint.Single()) : -1, SMBoneIndex = Array.IndexOf(simplifiedModel.Bones, b.SMBone) }) .ToList(); var missingBones = milkShapeBoneMap.Where(msbm => msbm.SMBoneIndex == -1); if (missingBones.Any()) { throw new KeyNotFoundException($"The following required bones are missing from or ambiguous in the MilkShape file: {string.Join(", ", missingBones.Select(b => b.Name))}"); } // We need to figure out the matching MilkShape groups to the original meshes. // There must be a group for each mesh, identified by the given name of "meshN" for (var m = 0; m < simplifiedModel.Meshes.Length; m++) { var mesh = simplifiedModel.Meshes[m]; // Since it's still useful to HAVE groups (for showing/hiding mainly), I'm allowing // MilkShape groups to be named in ways that will group them together as such: // mesh0 -- Basic name of mesh index 0 // mesh0-top -- Also should be part of mesh index 0, but is the "top" geometry of something etc. // // ... so in this example, both "mesh0" and "mesh0-top" will be merged together into // Mesh Index 0, so you can still use groups in a useful way and reference back the // eventual CGFX mesh it's actually supposed to be a part of... var meshName = $"mesh{m}"; var milkShapeGroupMatches = milkShape.Groups.Where(g => g.Name.StartsWith(meshName)); if (!milkShapeGroupMatches.Any()) { throw new KeyNotFoundException($"Required MilkShape group {meshName} not found"); } // CGFX stores vertices per mesh, but the vertices were all lumped together for MilkShape. // To reverse the process, we need to find out what vertices were used by all triangles in // this milkShapeGroup and that becomes our list of vertices for this mesh. Of course, we // still need to have a MilkShape vertex -> CGFX mesh vertex map to translate the triangles. // TODO -- Groups in MilkShape define a MaterialIndex, which isn't currently used, but if // it were, having different materials across the merged groups wouldn't work. This will // call the user out if they do that and eventually someday it might really matter. if (milkShapeGroupMatches.Select(g => g.MaterialIndex).Distinct().Count() > 1) { throw new InvalidOperationException($"Groups {string.Join(", ", milkShapeGroupMatches.Select(g => g.Name))} are to be merged into {meshName} but they have different materials assigned to them! This is not supported."); } var milkShapeGroupsTriangleIndices = milkShapeGroupMatches.SelectMany(g => g.TriangleIndices); // Triangles in this group var triangles = milkShapeGroupsTriangleIndices .Select(ti => milkShape.Triangles[ti]) .ToList(); // The "easy" concept is that we just take the triangles and selected distinct vertexes // out of them based on the MilkShape vertex index... // Unfortunately for us, MilkShape does texture coordinates PER TRIANGLE VERTEX, // not per vertex, which means to PROPERLY represent it in the mesh we need to "split" // vertices that would be otherwise common so they can hold the unique texture coordinates. var SMTriangles = new List <SMTriangle>(); var vertices = new List <MilkShapeTempVertex>(); foreach (var triangle in triangles) { // Get vertices for the triangle's three vertices SMTriangles.Add(new SMTriangle { v1 = GetLocalVertexForMSTriangleVertex(vertices, milkShape, milkShapeBoneMap, triangle, 0), v2 = GetLocalVertexForMSTriangleVertex(vertices, milkShape, milkShapeBoneMap, triangle, 1), v3 = GetLocalVertexForMSTriangleVertex(vertices, milkShape, milkShapeBoneMap, triangle, 2) }); } // Add the generated triangles back in mesh.Triangles.Clear(); mesh.Triangles.AddRange(SMTriangles); // As a benefit, the "vertices" collection now contains the set of vertices required to // reconstruct the mesh! var SMVertices = vertices.OrderBy(v => v.MeshLocalVertexIndex).Select(v => v.SMVertex).ToList(); // If the model has a skeleton... then WHOA there! Before we throw them back into the // collection, they need to be un-transformed back to the neutral bone position!! var nativeMeshUseColorVerts = mesh.Vertices.Where(v => !ReferenceEquals(v.Color, null)).Any(); foreach (var vertex in SMVertices) { vertex.Position = TransformPositionByBone(vertex, vertex.BoneIndices, simplifiedModel.Bones, true); // Also, while we're here, if the original model supported vertex Color attributes // (which MilkShape does NOT), we'll just patch in an all-white color. It's not the // best but I don't have a lot of option with this format... if (nativeMeshUseColorVerts) { vertex.Color = new Vector4(1, 1, 1, 1); } } mesh.Vertices.Clear(); mesh.Vertices.AddRange(SMVertices); } }
private static void ExportImportCGX(OperationInfo opInfo, string[] args) { // The base, input, and output files var baseFile = args[1]; var inFile = args[args.Length == 4 ? 2 : 1]; var outFile = args[args.Length == 4 ? 3 : 2]; var inFileExt = Path.GetExtension(inFile).ToLower(); var outFileExt = Path.GetExtension(outFile).ToLower(); // As we must have a BCRES for "backing", it will be loaded either way // before we do anything else. CGFX cgfxBase; SimplifiedModel simplifiedModel; using (var br = new BinaryReader(File.Open(baseFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) { // Load the BCRES (CGFX) cgfxBase = CGFX.Load(br); // Create a "SimplifiedModel" out of the base CGFX simplifiedModel = new SimplifiedModel(cgfxBase); } try { Console.WriteLine($"Converting {inFile} to {outFile}..."); // If we're converting FROM a BCRES TO another file type... if (opInfo.Operation == Operations.ExportFromCGFX) { // Convert it and save it to the requested file type using (var bw = new BinaryWriter(File.Open(outFile, FileMode.Create))) { if (outFileExt == ".ms3d") { var milkShape = MilkShapeConverter.ToMilkShape(simplifiedModel); milkShape.Save(bw); } else { throw new NotImplementedException($"Unsupported Destination filetype {outFileExt}"); } } // Dump textures if (simplifiedModel.Textures != null) { var dumpTextureDir = Path.GetDirectoryName(outFile); foreach (var texture in simplifiedModel.Textures) { Console.WriteLine($"Exporting texture {texture.Name}..."); texture.TextureBitmap.Save(Path.Combine(dumpTextureDir, texture.Name + ".png")); } } Console.WriteLine(); Console.WriteLine("Done."); Console.WriteLine(); Console.WriteLine("Note, if there are any textures you do NOT want to modify, you can delete the respective PNG files"); Console.WriteLine("and they'll be skipped on import."); Console.WriteLine(); Console.WriteLine(); } else { // Converting from a model file TO a BCRES... if (baseFile == outFile) { // The only reason I'm actually blocking this is lack of trust in that if // there's a bug that causes data loss, it may compound over subsequent // writes. So until I trust this code more, we'll force an "unmodified" base // to be used. (Of course, I can't KNOW the base is "unmodified"...) throw new InvalidOperationException("Currently writing to the base BCRES/CGFX file is prohibited. May lift this restriction in the future."); } using (var br = new BinaryReader(File.Open(inFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) using (var bw = new BinaryWriter(File.Open(outFile, FileMode.Create))) { if (inFileExt == ".ms3d") { var milkShape = MilkShape.Load(br); MilkShapeConverter.FromMilkShape(simplifiedModel, milkShape); } else { throw new NotImplementedException($"Unsupported Source filetype {inFileExt}"); } // Import textures var dumpTextureDir = Path.GetDirectoryName(inFile); // Find WHAT textures we have (user has option to not include any or all) if (simplifiedModel.Textures != null) { var importTextures = simplifiedModel.Textures .Select(t => new { Filename = Path.Combine(dumpTextureDir, t.Name + ".png"), Texture = t }) .Where(t => File.Exists(t.Filename)) .Select(t => new { t.Texture.Name, TextureBitmap = Image.FromFile(t.Filename) }) .ToList(); foreach (var texture in importTextures) { Console.WriteLine($"Importing texture {texture.Name}..."); // Corresponding texture in SimplifiedModel var smTexture = simplifiedModel.Textures.Where(t => t.Name == texture.Name).Single(); smTexture.TextureBitmap = (Bitmap)texture.TextureBitmap; } } simplifiedModel.RecomputeVertexNormals(); simplifiedModel.ApplyChanges(); cgfxBase.Save(bw); } Console.WriteLine(); Console.WriteLine("Done."); Console.WriteLine(); } } catch { if (File.Exists(outFile)) { File.Delete(outFile); } throw; } }
public static MilkShape ToMilkShape(SimplifiedModel sm) { var milkShape = new MilkShape(); // Convert the skeleton var bones = sm.Bones.Select(b => new ms3d_joint_t { Name = b.Name, ParentName = b.ParentName, Rotation = b.Rotation, Position = b.Translation, KeyFramesRot = new ms3d_keyframe_rot_t[0], KeyFramesTrans = new ms3d_keyframe_pos_t[0] }); var allVertices = new List <ms3d_vertex_t>(); var allTriangles = new List <ms3d_triangle_t>(); var allMaterials = new List <ms3d_material_t>(); var allGroups = new List <ms3d_group_t>(); foreach (var mesh in sm.Meshes) { // Get current vertex offset as we're accumulating them var vertexMeshOffset = allVertices.Count; // Get current triangle offset as we're grouping them var triangleOffset = allTriangles.Count; // Vertices belonging to this mesh var vertices = mesh.Vertices .Select(v => new ms3d_vertex_t { Position = TransformPositionByBone(v, v.BoneIndices, sm.Bones, false), BoneIdsAndWeights = GetBoneIndiciesAndWeights(v.BoneIndices, v.Weights), }); allVertices.AddRange(vertices); // Triangles belonging to this mesh var triangles = mesh.Triangles .Select(t => new ms3d_triangle_t { VertexIndices = new ushort[] { (ushort)(vertexMeshOffset + t.v1), (ushort)(vertexMeshOffset + t.v2), (ushort)(vertexMeshOffset + t.v3) }, VertexNormals = new Vector3[] { mesh.Vertices[t.v1].Normal, mesh.Vertices[t.v2].Normal, mesh.Vertices[t.v3].Normal }, TextureCoordinates = new Vector2[] { // NOTE: Textures are "upside-down", so this reverses them... make sure to undo that when saving... new Vector2(mesh.Vertices[t.v1].TexCoord.X, 1.0f - mesh.Vertices[t.v1].TexCoord.Y), new Vector2(mesh.Vertices[t.v2].TexCoord.X, 1.0f - mesh.Vertices[t.v2].TexCoord.Y), new Vector2(mesh.Vertices[t.v3].TexCoord.X, 1.0f - mesh.Vertices[t.v3].TexCoord.Y) }, GroupIndex = (byte)allGroups.Count }); allTriangles.AddRange(triangles); // Generate materials from the meshes sbyte materialIndex = -1; if (mesh.Texture != null) { materialIndex = (sbyte)allMaterials.Count; allMaterials.Add(new ms3d_material_t { Name = mesh.Texture.Name, Texture = mesh.Texture.Name + ".png" }); } // We're going to make the "mesh" into a "group" in MilkShape speak var group = new ms3d_group_t { Name = $"mesh{allGroups.Count}", TriangleIndices = Enumerable.Range(triangleOffset, triangles.Count()).Select(i => (ushort)i).ToArray(), MaterialIndex = materialIndex }; allGroups.Add(group); } milkShape.Vertices.AddRange(allVertices); milkShape.Triangles.AddRange(allTriangles); milkShape.Groups.AddRange(allGroups); milkShape.Materials.AddRange(allMaterials); milkShape.Joints.AddRange(bones); return(milkShape); }