private static SMVertex GetSMVertexFromTriangle(MilkShape milkShape, List <MilkShapeBoneMap> milkShapeBoneMap, ms3d_triangle_t t, int triangleVertex) { // Only focusing on one of the three triangle vertices var triVertIdx = t.VertexIndices[triangleVertex]; return(new SMVertex { // Get position of vertex Position = milkShape.Vertices[triVertIdx].Position, // Get normal of vertex (stored with triangle in Milkshape for some reason) Normal = t.VertexNormals[triangleVertex], // Texture coordinate needs "reversal" since I "reversed" it earlier // Also the (unused?) Z coordinate always appears to reflect the vertex Z, although // I don't know if that's a requirement or a quirk, but either way... TexCoord = new Vector3(t.TextureCoordinates[triangleVertex].X, 1.0f - t.TextureCoordinates[triangleVertex].Y, milkShape.Vertices[triVertIdx].Position.Z), // This uses the bone map just in case the MilkShape indexes don't line up with // the intended indexes... BoneIndices = milkShape.Vertices[triVertIdx].BoneIdsAndWeights .Where(biw => biw.BoneId != -1) .Select(biw => milkShapeBoneMap.Where(bm => bm.MilkShapeJointIndex == biw.BoneId).Single().SMBoneIndex) .ToArray(), // Finally the MilkShape weights are stored as 0-100 byte-sized values, and we // must convert back (granted with loss) Weights = milkShape.Vertices[triVertIdx].BoneIdsAndWeights .Where(biw => biw.BoneId != -1) .Select(biw => biw.Weight / 100.0f).ToArray() }); }
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 Load(BinaryReader br) { var ms = new MilkShape(); var utility = new Utility(br, null, Endianness.Little); // Milkshape is tied to Win32, so definitely little endian // First comes the header (sizeof(ms3d_header_t) == 14) var magic = ReadString(utility, 10); if (magic != Magic) { throw new InvalidOperationException($"Milkshape Load: Bad magic, expected {Magic}, got {magic}"); } var version = utility.ReadU32(); if (version != 4) { throw new InvalidOperationException($"Milkshape Load: Unsupported version, expected 4, got {version}"); } // Then comes the number of vertices var nNumVertices = utility.ReadU16(); // Then come nNumVertices times ms3d_vertex_t structs (sizeof(ms3d_vertex_t) == 15) ms.Vertices = new List <ms3d_vertex_t>(nNumVertices); for (var v = 0; v < nNumVertices; v++) { var vertex = new ms3d_vertex_t(); vertex.Flags = (MilkshapeObjectFlags)utility.ReadByte(); // SELECTED | SELECTED2 | HIDDEN vertex.Position = Vector3.Read(utility); // NOTE: I'm merging the different specs / extended attributes here; will look confusing vertex.BoneIdsAndWeights[0].BoneId = utility.ReadSByte(); vertex.ReferenceCount = utility.ReadByte(); ms.Vertices.Add(vertex); } // Then comes the number of triangles var nNumTriangles = utility.ReadU16(); // 2 bytes // Then come nNumTriangles times ms3d_triangle_t structs (sizeof(ms3d_triangle_t) == 70) ms.Triangles = new List <ms3d_triangle_t>(nNumTriangles); for (var t = 0; t < nNumTriangles; t++) { ms.Triangles.Add(new ms3d_triangle_t { Flags = (MilkshapeObjectFlags)utility.ReadU16(), // SELECTED | SELECTED2 | HIDDEN VertexIndices = utility.ReadU16Ints(3), VertexNormals = new[] { Vector3.Read(utility), Vector3.Read(utility), Vector3.Read(utility) }, TextureCoordinates = ReadTextureCoordinates(utility), SmoothingGroup = utility.ReadByte(), // 1 - 32 GroupIndex = utility.ReadByte() // }); } // Then comes the number of groups var nNumGroups = utility.ReadU16(); // 2 bytes // Then come nNumGroups times groups (the sizeof a group is dynamic, because of triangleIndices is numtriangles long) ms.Groups = new List <ms3d_group_t>(nNumGroups); for (var g = 0; g < nNumGroups; g++) { var group = new ms3d_group_t(); group.Flags = (MilkshapeObjectFlags)utility.ReadByte(); // SELECTED | HIDDEN group.Name = ReadString(utility, 32); var numtriangles = utility.ReadU16(); group.TriangleIndices = utility.ReadU16Ints(numtriangles); group.MaterialIndex = utility.ReadSByte(); ms.Groups.Add(group); } // number of materials var nNumMaterials = utility.ReadU16(); // 2 bytes // Then come nNumMaterials times ms3d_material_t structs (sizeof(ms3d_material_t) == 361) ms.Materials = new List <ms3d_material_t>(nNumMaterials); for (var m = 0; m < nNumMaterials; m++) { ms.Materials.Add(new ms3d_material_t { Name = ReadString(utility, 32), Ambient = utility.ReadFloats(4), Diffuse = utility.ReadFloats(4), Specular = utility.ReadFloats(4), Emissive = utility.ReadFloats(4), Shininess = utility.ReadFloat(), Transparency = utility.ReadFloat(), Mode = utility.ReadSByte(), // NOTE: Examining a file written by MilkShape, I saw garbage beyond // the null terminator of these strings. Harmless, just FYI. Texture = ReadString(utility, 128), Alphamap = ReadString(utility, 128) }); } // save some keyframer data ms.fAnimationFPS = utility.ReadFloat(); ms.fCurrentTime = utility.ReadFloat(); ms.iTotalFrames = utility.ReadI32(); // number of joints var nNumJoints = utility.ReadU16(); // 2 bytes // Then come nNumJoints joints (the size of joints are dynamic, because each joint has a differnt count of keys ms.Joints = new List <ms3d_joint_t>(nNumJoints); for (var j = 0; j < nNumJoints; j++) { var joint = new ms3d_joint_t(); joint.Flags = (MilkshapeObjectFlags)utility.ReadByte(); joint.Name = ReadString(utility, 32); joint.ParentName = ReadString(utility, 32); joint.Rotation = Vector3.Read(utility); joint.Position = Vector3.Read(utility); var numKeyFramesRot = utility.ReadU16(); var numKeyFramesTrans = utility.ReadU16(); joint.KeyFramesRot = new ms3d_keyframe_rot_t[numKeyFramesRot]; for (var r = 0; r < numKeyFramesRot; r++) { joint.KeyFramesRot[r] = new ms3d_keyframe_rot_t { Time = utility.ReadFloat(), Rotation = Vector3.Read(utility) }; } joint.KeyFramesTrans = new ms3d_keyframe_pos_t[numKeyFramesTrans]; for (var t = 0; t < numKeyFramesTrans; t++) { joint.KeyFramesTrans[t] = new ms3d_keyframe_pos_t { Time = utility.ReadFloat(), Position = Vector3.Read(utility) }; } ms.Joints.Add(joint); } try { // subVersion specifying whether comment data exists var subVersion = utility.ReadU32(); if (subVersion == 1) { // Group comments ReadComments(utility, ms.Groups); // Material comments ReadComments(utility, ms.Materials); // Joint comments ReadComments(utility, ms.Joints); // Then comes the number of model comments, which is always 0 or 1 ReadComments(utility, new[] { ms }, true); } // subVersion specifying whether extended vertex data exists and to what extent subVersion = utility.ReadU32(); if (subVersion > 0) { // Then comes nNumVertices times ms3d_vertex_ex_t structs (sizeof(ms3d_vertex_ex_t) == 14) // NOTE: I'm merging extended vertex data spec stuff from this mess: // sbyte[] boneIds = new sbyte[3]; // index of joint or -1, if -1, then that weight is ignored, since subVersion 1 // byte[] weights = new byte[3]; // vertex weight ranging from 0 - 100, last weight is computed by 1.0 - sum(all weights), since subVersion 1 // // weight[0] is the weight for boneId in ms3d_vertex_t // // weight[1] is the weight for boneIds[0] // // weight[2] is the weight for boneIds[1] // // 1.0f - weight[0] - weight[1] - weight[2] is the weight for boneIds[2] // NOTE: "extra" depends in subVersion; 1 element if 2, 2 elements if 3 // uint[] extra = new uint[2]; // vertex extra, which can be used as color or anything else, since subVersion 3 for (var v = 0; v < nNumVertices; v++) { var vertex = ms.Vertices[v]; // These are ADDITIONAL bone Ids vertex.BoneIdsAndWeights[1].BoneId = utility.ReadSByte(); vertex.BoneIdsAndWeights[2].BoneId = utility.ReadSByte(); vertex.BoneIdsAndWeights[3].BoneId = utility.ReadSByte(); // These are WEIGHTS which were previously unavailable vertex.BoneIdsAndWeights[0].Weight = utility.ReadByte(); vertex.BoneIdsAndWeights[1].Weight = utility.ReadByte(); vertex.BoneIdsAndWeights[2].Weight = utility.ReadByte(); // Final bone weight is computed -- NOTE, spec says 1.0 - [...], but I think it meant 100 vertex.BoneIdsAndWeights[3].Weight = (byte)(100 - vertex.BoneIdsAndWeights[0].Weight - vertex.BoneIdsAndWeights[1].Weight - vertex.BoneIdsAndWeights[2].Weight); // How much "extra" data is here depends on subVersion... var extraCount = subVersion - 1; vertex.Extra = utility.ReadUInts(extraCount); } } // subVersion specifying whether joints have color subVersion = utility.ReadU32(); if (subVersion == 1) { for (var j = 0; j < nNumJoints; j++) { var joint = ms.Joints[j]; joint.Color = new ColorFloat { R = utility.ReadFloat(), G = utility.ReadFloat(), B = utility.ReadFloat(), A = 1.0f // Not stored }; } } // subVersion specifying whether model extended data exists subVersion = utility.ReadU32(); if (subVersion == 1) { ms.jointSize = utility.ReadFloat(); // joint size, since subVersion == 1 ms.transparencyMode = utility.ReadI32(); // 0 = simple, 1 = depth buffered with alpha ref, 2 = depth sorted triangles, since subVersion == 1 ms.alphaRef = utility.ReadFloat(); // alpha reference value for transparencyMode = 1, since subVersion == 1 } } catch (IndexOutOfRangeException) { // This is a dirty hack because any file that doesn't have the extended data // will throw IndexOutOfRangeException but I really should be doing EOF checks } return(ms); }
// Gets or generates an SMVertex for a vertex on a MilkShape triangle private static ushort GetLocalVertexForMSTriangleVertex(List <MilkShapeTempVertex> milkShapeTempVertices, MilkShape milkShape, List <MilkShapeBoneMap> milkShapeBoneMap, ms3d_triangle_t t, int triangleVertex) { // This will provide the "close enough" rating of texture coordinates // to decide that a vertex with the same MilkShape index as well as // "close enough" texture coordinates is the same overall vertex. const float texCoordEpsilon = 1.0f / 128.0f; // This is assuming that we're dealing with textures 128x128 and under, which is probably true // Get the texture coordinates used by this vertex of the triangle, // as it will be part of the consideration of "sameness" of other vertices var triangleVertexTexCoord = t.TextureCoordinates[triangleVertex]; // The primary consideration is MilkShape's own vertex index var triangleVertexIndex = t.VertexIndices[triangleVertex]; var resultVertex = milkShapeTempVertices .Where(tv => // Must come from same vertex in MilkShape's pool... tv.MilkShapeVertexIndex == triangleVertexIndex && // ... and be "close enough" with the texture coordinates Math.Abs(triangleVertexTexCoord.X - tv.SMVertex.TexCoord.X) < texCoordEpsilon && Math.Abs((1.0f - triangleVertexTexCoord.Y) - tv.SMVertex.TexCoord.Y) < texCoordEpsilon ).SingleOrDefault(); if (resultVertex == null) { // If we don't have one quite like this, then we need to create it! resultVertex = new MilkShapeTempVertex { MilkShapeVertexIndex = triangleVertexIndex, MeshLocalVertexIndex = milkShapeTempVertices.Count, SMVertex = GetSMVertexFromTriangle(milkShape, milkShapeBoneMap, t, triangleVertex) }; milkShapeTempVertices.Add(resultVertex); } return((ushort)resultVertex.MeshLocalVertexIndex); }
// 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); } }
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); }