// 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);
            }
        }
コード例 #2
0
        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);
        }