/// <summary>
        /// Creates and populates a TTModel object from a raw XivMdl.
        /// </summary>
        public static TTModel FromRaw(Model rawMdl, Action <bool, string> loggingFunction = null)
        {
            if (rawMdl == null)
            {
                return(null);
            }

            if (loggingFunction == null)
            {
                loggingFunction = ModelModifiers.NoOp;
            }

            TTModel ttModel = new TTModel();

            ModelModifiers.MergeGeometryData(ttModel, rawMdl, loggingFunction);
            ModelModifiers.MergeAttributeData(ttModel, rawMdl, loggingFunction);
            ModelModifiers.MergeMaterialData(ttModel, rawMdl, loggingFunction);
            try
            {
                ModelModifiers.MergeShapeData(ttModel, rawMdl, loggingFunction);
            }
            catch (Exception ex)
            {
                loggingFunction(true, "Unable to load shape data: " + ex.Message);
                ModelModifiers.ClearShapeData(ttModel, loggingFunction);
            }

            return(ttModel);
        }
Exemple #2
0
        /*private static float[] NewIdentityMatrix()
         * {
         *      var arr = new float [16];
         *      arr[0] = 1f;
         *      arr[1] = 0f;
         *      arr[2] = 0f;
         *      arr[3] = 0f;
         *
         *      arr[4] = 0f;
         *      arr[5] = 1f;
         *      arr[6] = 0f;
         *      arr[7] = 0f;
         *
         *      arr[8] = 0f;
         *      arr[9] = 0f;
         *      arr[10] = 1f;
         *      arr[11] = 0f;
         *
         *      arr[12] = 0f;
         *      arr[13] = 0f;
         *      arr[14] = 0f;
         *      arr[15] = 1f;
         *      return arr;
         * }
         *
         * public Dictionary<string, SkeletonData> ResolveBoneHeirarchy(List<XivDependencyRootInfo> roots = null, XivRace race = XivRace.All_Races, List<string> bones = null, Action<bool, string> loggingFunction = null)
         * {
         *      if (roots == null || roots.Count == 0)
         *      {
         *              if (!IsInternal)
         *              {
         *                      throw new Exception("Cannot dynamically resolve bone heirarchy for external model.");
         *              }
         *
         *
         *              // We can use the raw function here since we know this is a valid internal model file.
         *              XivDependencyRootInfo root = XivDependencyGraph.ExtractRootInfo(Source);
         *
         *              if (race == XivRace.All_Races)
         *              {
         *                      race = IOUtil.GetRaceFromPath(Source);
         *              }
         *
         *
         *              // Just our one dynamically found root.
         *              roots = new List<XivDependencyRootInfo>() { root };
         *      }
         *
         *      return TTModel.ResolveBoneHeirarchyRaw(roots, race, bones, loggingFunction);
         * }
         *
         * /// <summary>
         * /// Creates a row-by-row Matrix from a column order float set.
         * /// </summary>
         * /// <param name="data"></param>
         * /// <returns></returns>
         * private static float[] RowsFromColumns(float[] data)
         * {
         *      var formatted = new float[16];
         *
         *      formatted[0] = data[0];
         *      formatted[1] = data[4];
         *      formatted[2] = data[8];
         *      formatted[3] = data[12];
         *
         *
         *      formatted[4] = data[1];
         *      formatted[5] = data[5];
         *      formatted[6] = data[9];
         *      formatted[7] = data[13];
         *
         *      formatted[8] = data[2];
         *      formatted[9] = data[6];
         *      formatted[10] = data[10];
         *      formatted[11] = data[14];
         *
         *      formatted[12] = data[3];
         *      formatted[13] = data[7];
         *      formatted[14] = data[11];
         *      formatted[15] = data[15];
         *
         *      return formatted;
         * }
         *
         * /// <summary>
         * /// Resolves the full bone heirarchy necessary to animate this TTModel.
         * /// Used when saving the file to DB.  (Or potentially animating it)
         * /// </summary>
         * /// <returns></returns>
         * public static Dictionary<string, SkeletonData> ResolveBoneHeirarchyRaw(List<XivDependencyRootInfo> roots, XivRace race, List<string> bones = null, Action<bool, string> loggingFunction = null)
         * {
         *      if (loggingFunction == null)
         *      {
         *              loggingFunction = ModelModifiers.NoOp;
         *      }
         *
         *      var fullSkel = new Dictionary<string, SkeletonData>();
         *      var skelDict = new Dictionary<string, SkeletonData>();
         *
         *      bool parsedBase = false;
         *      var baseSkeletonPath = "";
         *      var extraSkeletonPath = "";
         *
         *      foreach (var root in roots)
         *      {
         *              // Do we need to get the base skel still?
         *              string[] skeletonData;
         *              if (!parsedBase)
         *              {
         *                      try
         *                      {
         *                              baseSkeletonPath = Sklb.GetBaseSkeletonFile(root, race);
         *                              skeletonData = File.ReadAllLines(baseSkeletonPath);
         *
         *                              // Parse both skeleton files, starting with the base file.
         *                              foreach (var b in skeletonData)
         *                              {
         *                                      if (b == "") continue;
         *                                      var j = JsonConvert.DeserializeObject<SkeletonData>(b);
         *                                      j.PoseMatrix = RowsFromColumns(j.PoseMatrix);
         *                                      fullSkel.Add(j.BoneName, j);
         *                              }
         *
         *                      } catch(Exception ex)
         *                      {
         *                              // If we failed to resolve the bones for some reason, log the error message and use a blank skel.
         *                              loggingFunction(true, "Error Parsing Skeleton ("+ baseSkeletonPath.ToString() +"):" + ex.Message);
         *                      }
         *                      parsedBase = true;
         *              }
         *
         *
         *              extraSkeletonPath = Sklb.GetExtraSkeletonFile(root, race);
         *              // Did this root have an extra skeleton in use?
         *              if (!String.IsNullOrEmpty(extraSkeletonPath))
         *              {
         *                      try
         *                      {
         *                              // If it did, add its bones to the resulting skeleton.
         *                              Dictionary<int, int> exTranslationTable = new Dictionary<int, int>();
         *                              skeletonData = File.ReadAllLines(extraSkeletonPath);
         *                              foreach (var b in skeletonData)
         *                              {
         *                                      if (b == "") continue;
         *                                      var j = JsonConvert.DeserializeObject<SkeletonData>(b);
         *                                      j.PoseMatrix = RowsFromColumns(j.PoseMatrix);
         *
         *                                      if (fullSkel.ContainsKey(j.BoneName))
         *                                      {
         *                                              // This is a parent level reference to a base bone.
         *                                              exTranslationTable.Add(j.BoneNumber, fullSkel[j.BoneName].BoneNumber);
         *                                      }
         *                                      else if (exTranslationTable.ContainsKey(j.BoneParent))
         *                                      {
         *                                              // Run it through the translation to match up with the base skeleton.
         *                                              j.BoneParent = exTranslationTable[j.BoneParent];
         *
         *                                              // And generate its own new bone number
         *                                              var originalNumber = j.BoneNumber;
         *                                              j.BoneNumber = fullSkel.Select(x => x.Value.BoneNumber).Max() + 1;
         *
         *                                              fullSkel.Add(j.BoneName, j);
         *                                              exTranslationTable.Add(originalNumber, j.BoneNumber);
         *                                      } else
         *                                      {
         *                                              // This is a root bone in the EX skeleton that has no parent element in the base skeleton.
         *                                              // Just stick it onto the root bone.
         *                                              j.BoneParent = fullSkel["n_root"].BoneNumber;
         *
         *                                              // And generate its own new bone number
         *                                              var originalNumber = j.BoneNumber;
         *                                              j.BoneNumber = fullSkel.Select(x => x.Value.BoneNumber).Max() + 1;
         *
         *                                              fullSkel.Add(j.BoneName, j);
         *                                              exTranslationTable.Add(originalNumber, j.BoneNumber);
         *
         *                                      }
         *                              }
         *                      } catch(Exception ex)
         *                      {
         *                              // If we failed to resolve the bones for some reason, log the error message and use a blank skel.
         *                              loggingFunction(true, "Error Parsing Extra Skeleton (" + extraSkeletonPath.ToString() + "):" + ex.Message);
         *                      }
         *              }
         *      }
         *
         *
         *      // If no bones were specified, include all of them.
         *      if(bones == null)
         *      {
         *              bones = new List<string>();
         *              foreach(var e in fullSkel)
         *              {
         *                      bones.Add(e.Value.BoneName);
         *              }
         *
         *              bones = bones.Distinct().ToList();
         *      }
         *
         *
         *      var badBoneId = 900;
         *      foreach (var s in bones)
         *      {
         *              var fixedBone = Regex.Replace(s, "[0-9]+$", string.Empty);
         *
         *              if (fullSkel.ContainsKey(fixedBone))
         *              {
         *                      var skel = fullSkel[fixedBone];
         *
         *                      if (skel.BoneParent == -1 && !skelDict.ContainsKey(skel.BoneName))
         *                      {
         *                              skelDict.Add(skel.BoneName, skel);
         *                      }
         *
         *                      while (skel.BoneParent != -1)
         *                      {
         *                              if (!skelDict.ContainsKey(skel.BoneName))
         *                              {
         *                                      skelDict.Add(skel.BoneName, skel);
         *                              }
         *                              skel = fullSkel.First(x => x.Value.BoneNumber == skel.BoneParent).Value;
         *
         *                              if (skel.BoneParent == -1 && !skelDict.ContainsKey(skel.BoneName))
         *                              {
         *                                      skelDict.Add(skel.BoneName, skel);
         *                              }
         *                      }
         *              }
         *              else
         *              {
         *                      // Create a fake bone for this, rather than strictly crashing out.
         *                      var skel = new SkeletonData();
         *                      skel.BoneName = s;
         *                      skel.BoneNumber = badBoneId;
         *                      badBoneId++;
         *                      skel.BoneParent = 0;
         *                      skel.InversePoseMatrix = NewIdentityMatrix();
         *                      skel.PoseMatrix = NewIdentityMatrix();
         *
         *                      skelDict.Add(s, skel);
         *                      loggingFunction(true, $"The base game skeleton did not contain bone {s}. It has been parented to the root bone.");
         *              }
         *      }
         *
         *      return skelDict;
         * }*/


        /// <summary>
        /// Performs a basic sanity check on an incoming TTModel
        /// Returns true if there were no errors or errors that were resolvable.
        /// Returns false if the model was deemed insane.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="loggingFunction"></param>
        /// <returns></returns>
        public static bool SanityCheck(TTModel model, Action <bool, string> loggingFunction = null)
        {
            if (loggingFunction == null)
            {
                loggingFunction = ModelModifiers.NoOp;
            }
            loggingFunction(false, "Validating model sanity...");

            bool hasWeights = model.HasWeights;

            if (model.MeshGroups.Count == 0)
            {
                loggingFunction(true, "Model has no data. - Model must have at least one valid Mesh Group.");
                return(false);
            }

            var mIdx = 0;

            foreach (var m in model.MeshGroups)
            {
                if (m.Parts.Count == 0)
                {
                    var part = new TTMeshPart();
                    part.Name = "Part 0";
                    m.Parts.Add(part);
                }

                // Meshes in animated models must have at least one bone in their bone set in order to not generate a crash.
                if (hasWeights && m.Bones.Count == 0)
                {
                    m.Bones.Add("n_root");
                }
                mIdx++;
            }

            return(true);
        }
Exemple #3
0
        /// <summary>
        /// Checks the model for common valid-but-unusual states that users often end up in by accident, providing
        /// a warning message for each one, if the conditions are met.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="loggingFunction"></param>
        public static void CheckCommonUserErrors(TTModel model, Action <bool, string> loggingFunction = null)
        {
            if (loggingFunction == null)
            {
                loggingFunction = ModelModifiers.NoOp;
            }
            loggingFunction(false, "Checking for unusual data...");

            if (model.Materials.Count > 4)
            {
                loggingFunction(true, "Model has more than four active materials.  The following materials will be ignored in game: ");
                var idx = 0;
                foreach (var m in model.Materials)
                {
                    if (idx >= 4)
                    {
                        loggingFunction(true, "Material: " + m);
                    }
                    idx++;
                }
            }

            int mIdx = 0;

            foreach (var m in model.MeshGroups)
            {
                int pIdx = 0;
                foreach (var p in m.Parts)
                {
                    if (p.Vertices.Count == 0)
                    {
                        continue;
                    }

                    bool anyAlpha     = false;
                    bool anyColor     = false;
                    bool anyWeirdUV1s = false;
                    bool anyWeirdUV2s = false;

                    foreach (var v in p.Vertices)
                    {
                        anyAlpha     = anyAlpha || (v.VertexColor[3] > 0);
                        anyColor     = anyColor || (v.VertexColor[0] > 0 || v.VertexColor[1] > 0 || v.VertexColor[2] > 0);
                        anyWeirdUV1s = anyWeirdUV1s || (v.UV1.X > 2 || v.UV1.X < -2 || v.UV1.Y > 2 || v.UV1.Y < -2);
                        anyWeirdUV2s = anyWeirdUV2s || (v.UV2.X > 2 || v.UV2.X < -2 || v.UV2.Y > 2 || v.UV2.Y < -2);
                    }

                    if (!anyAlpha)
                    {
                        loggingFunction(true, "Mesh: " + mIdx + " Part: " + pIdx + " has a fully black Vertex Alpha channel.  This will render the part invisible in-game.  Was this intended?");
                    }

                    if (!anyColor)
                    {
                        loggingFunction(true, "Mesh: " + mIdx + " Part: " + pIdx + " has a fully black Vertex Color channel.  This can have unexpected results on in-game rendering.  Was this intended?");
                    }

                    if (anyWeirdUV1s)
                    {
                        loggingFunction(true, "Mesh: " + mIdx + " Part: " + pIdx + " has unusual UV1 data.  This can have unexpected results on texture placement.  Was this inteneded?");
                    }

                    if (anyWeirdUV2s)
                    {
                        loggingFunction(true, "Mesh: " + mIdx + " Part: " + pIdx + " has unusual UV2 data.  This can have unexpected results on decal placement or opacity.  Was this inteneded?");
                    }

                    pIdx++;
                }
                mIdx++;
            }
        }
Exemple #4
0
        /// <summary>
        /// Loads a TTModel file from a given SQLite3 DB filepath.
        /// </summary>
        public static TTModel FromDb(FileInfo file, Action <bool, string> loggingFunction = null)
        {
            if (loggingFunction == null)
            {
                loggingFunction = ModelModifiers.NoOp;
            }

            string  connectionString = "Data Source=" + file + ";";
            TTModel model            = new TTModel();

            model.Source = file.FullName;

            // Spawn a DB connection to do the raw queries.
            using (SqliteConnection db = new SqliteConnection(connectionString))
            {
                db.Open();

                // Load Mesh Groups
                string query = "select * from meshes order by mesh asc;";
                using (SqliteCommand cmd = new SqliteCommand(query, db))
                {
                    using (CacheReader reader = new CacheReader(cmd.ExecuteReader()))
                    {
                        while (reader.NextRow())
                        {
                            int meshNum = reader.GetInt32("mesh");

                            // Spawn mesh groups as needed.
                            while (model.MeshGroups.Count <= meshNum)
                            {
                                model.MeshGroups.Add(new TTMeshGroup());
                            }

                            model.MeshGroups[meshNum].Name = reader.GetString("name");
                        }
                    }
                }

                // Load Mesh Parts
                query = "select * from parts order by mesh asc, part asc;";
                using (SqliteCommand cmd = new SqliteCommand(query, db))
                {
                    using (CacheReader reader = new CacheReader(cmd.ExecuteReader()))
                    {
                        while (reader.NextRow())
                        {
                            int meshNum = reader.GetInt32("mesh");
                            int partNum = reader.GetInt32("part");

                            // Spawn mesh groups if needed.
                            while (model.MeshGroups.Count <= meshNum)
                            {
                                model.MeshGroups.Add(new TTMeshGroup());
                            }

                            // Spawn parts as needed.
                            while (model.MeshGroups[meshNum].Parts.Count <= partNum)
                            {
                                model.MeshGroups[meshNum].Parts.Add(new TTMeshPart());
                            }

                            model.MeshGroups[meshNum].Parts[partNum].Name = reader.GetString("name");
                        }
                    }
                }

                // Load Bones
                query = "select * from bones where mesh >= 0 order by mesh asc, bone_id asc;";
                using (SqliteCommand cmd = new SqliteCommand(query, db))
                {
                    using (CacheReader reader = new CacheReader(cmd.ExecuteReader()))
                    {
                        while (reader.NextRow())
                        {
                            int meshId = reader.GetInt32("mesh");
                            model.MeshGroups[meshId].Bones.Add(reader.GetString("name"));
                        }
                    }
                }
            }

            // Loop for each part, to populate their internal data structures.
            for (int mId = 0; mId < model.MeshGroups.Count; mId++)
            {
                TTMeshGroup m = model.MeshGroups[mId];
                for (int pId = 0; pId < m.Parts.Count; pId++)
                {
                    TTMeshPart p = m.Parts[pId];
                    WhereClause where = new WhereClause();
                    WhereClause mWhere = new WhereClause();
                    mWhere.Column = "mesh";
                    mWhere.Value  = mId;
                    WhereClause pWhere = new WhereClause();
                    pWhere.Column = "part";
                    pWhere.Value  = pId;

                    where.Inner.Add(mWhere);
                    where.Inner.Add(pWhere);

                    // Load Vertices
                    // The reader handles coalescing the null types for us.
                    p.Vertices = BuildListFromTable(connectionString, "vertices", where, (reader) =>
                    {
                        TTVertex vertex = new TTVertex();

                        // Positions
                        vertex.Position.X = reader.GetFloat("position_x");
                        vertex.Position.Y = reader.GetFloat("position_y");
                        vertex.Position.Z = reader.GetFloat("position_z");

                        // Normals
                        vertex.Normal.X = reader.GetFloat("normal_x");
                        vertex.Normal.Y = reader.GetFloat("normal_y");
                        vertex.Normal.Z = reader.GetFloat("normal_z");

                        // Vertex Colors - Vertex color is RGBA
                        vertex.VertexColor[0] = (byte)Math.Round(reader.GetFloat("color_r") * 255);
                        vertex.VertexColor[1] = (byte)Math.Round(reader.GetFloat("color_g") * 255);
                        vertex.VertexColor[2] = (byte)Math.Round(reader.GetFloat("color_b") * 255);
                        vertex.VertexColor[3] = (byte)Math.Round(reader.GetFloat("color_a") * 255);

                        // UV Coordinates
                        vertex.UV1.X = reader.GetFloat("uv_1_u");
                        vertex.UV1.Y = reader.GetFloat("uv_1_v");
                        vertex.UV2.X = reader.GetFloat("uv_2_u");
                        vertex.UV2.Y = reader.GetFloat("uv_2_v");

                        // Bone Ids
                        vertex.BoneIds[0] = (byte)reader.GetByte("bone_1_id");
                        vertex.BoneIds[1] = (byte)reader.GetByte("bone_2_id");
                        vertex.BoneIds[2] = (byte)reader.GetByte("bone_3_id");
                        vertex.BoneIds[3] = (byte)reader.GetByte("bone_4_id");

                        // Weights
                        vertex.Weights[0] = (byte)Math.Round(reader.GetFloat("bone_1_weight") * 255);
                        vertex.Weights[1] = (byte)Math.Round(reader.GetFloat("bone_2_weight") * 255);
                        vertex.Weights[2] = (byte)Math.Round(reader.GetFloat("bone_3_weight") * 255);
                        vertex.Weights[3] = (byte)Math.Round(reader.GetFloat("bone_4_weight") * 255);

                        return(vertex);
                    });

                    p.TriangleIndices = BuildListFromTable(connectionString, "indices", where, (reader) =>
                    {
                        try
                        {
                            return(reader.GetInt32("vertex_id"));
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                    });
                }
            }

            // Spawn a DB connection to do the raw queries.
            using (SqliteConnection db = new SqliteConnection(connectionString))
            {
                db.Open();

                // Load Shape Verts
                string query = "select * from shape_vertices order by shape asc, mesh asc, part asc, vertex_id asc;";
                using (SqliteCommand cmd = new SqliteCommand(query, db))
                {
                    using (CacheReader reader = new CacheReader(cmd.ExecuteReader()))
                    {
                        while (reader.NextRow())
                        {
                            string shapeName = reader.GetString("shape");
                            int    meshNum   = reader.GetInt32("mesh");
                            int    partNum   = reader.GetInt32("part");
                            int    vertexId  = reader.GetInt32("vertex_id");

                            TTMeshPart part = model.MeshGroups[meshNum].Parts[partNum];

                            // Copy the original vertex and update position.
                            TTVertex vertex = (TTVertex)part.Vertices[vertexId].Clone();
                            vertex.Position.X = reader.GetFloat("position_x");
                            vertex.Position.Y = reader.GetFloat("position_y");
                            vertex.Position.Z = reader.GetFloat("position_z");

                            TTVertex repVert = part.Vertices[vertexId];
                            if (repVert.Position.Equals(vertex.Position))
                            {
                                // Skip morphology which doesn't actually change anything.
                                continue;
                            }

                            if (!part.ShapeParts.ContainsKey(shapeName))
                            {
                                TTShapePart shpPt = new TTShapePart();
                                shpPt.Name = shapeName;
                                part.ShapeParts.Add(shapeName, shpPt);
                            }

                            part.ShapeParts[shapeName].VertexReplacements.Add(vertexId, part.ShapeParts[shapeName].Vertices.Count);
                            part.ShapeParts[shapeName].Vertices.Add(vertex);
                        }
                    }
                }
            }

            // Convert the model to FFXIV's internal weirdness.
            ModelModifiers.MakeImportReady(model, loggingFunction);
            return(model);
        }
Exemple #5
0
        /// <summary>
        /// Saves the TTModel to a .DB file for use with external importers/exporters.
        /// </summary>
        public static void ToDb(TTModel model, FileInfo file, bool useAllBones, Action <bool, string> loggingFunction = null)
        {
            if (loggingFunction == null)
            {
                loggingFunction = ModelModifiers.NoOp;
            }

            file.Delete();
            string directory = file.DirectoryName;

            ModelModifiers.MakeExportReady(model, loggingFunction);

            string connectionString = "Data Source=" + file + ";";

            try
            {
                List <string> bones = useAllBones ? null : model.Bones;

                // TODO: Get bones for skeletons
                Console.WriteLine("WARNING: NO SKELETONS");
                Dictionary <string, SkeletonData> boneDict = new Dictionary <string, SkeletonData>();               ////model.ResolveBoneHeirarchy(null, TTModel.XivRace.All_Races, bones, loggingFunction);

                const string creationScript = "CreateImportDB.sql";

                // Spawn a DB connection to do the raw queries.
                // Using statements help ensure we don't accidentally leave any connections open and lock the file handle.
                using (SqliteConnection db = new SqliteConnection(connectionString))
                {
                    db.Open();

                    // Create the DB
                    string[] lines  = File.ReadAllLines("Resources\\SQL\\" + creationScript);
                    string   sqlCmd = string.Join("\n", lines);

                    using (SqliteCommand cmd = new SqliteCommand(sqlCmd, db))
                    {
                        cmd.ExecuteScalar();
                    }

                    // Write the Data.
                    using (SqliteTransaction transaction = db.BeginTransaction())
                    {
                        // Metadata.
                        string query = @"insert into meta (key, value) values ($key, $value)";
                        using (SqliteCommand cmd = new SqliteCommand(query, db))
                        {
                            // FFXIV stores stuff in Meters.
                            cmd.Parameters.AddWithValue("key", "unit");
                            cmd.Parameters.AddWithValue("value", "meter");
                            cmd.ExecuteScalar();

                            // Application that created the db.
                            cmd.Parameters.AddWithValue("key", "application");
                            cmd.Parameters.AddWithValue("value", "FfxivResourceConverter");
                            cmd.ExecuteScalar();

                            cmd.Parameters.AddWithValue("key", "version");
                            cmd.Parameters.AddWithValue("value", typeof(TTModelDb).Assembly.GetName().Version);
                            cmd.ExecuteScalar();

                            // Axis information
                            cmd.Parameters.AddWithValue("key", "up");
                            cmd.Parameters.AddWithValue("value", "y");
                            cmd.ExecuteScalar();

                            cmd.Parameters.AddWithValue("key", "front");
                            cmd.Parameters.AddWithValue("value", "z");
                            cmd.ExecuteScalar();

                            cmd.Parameters.AddWithValue("key", "handedness");
                            cmd.Parameters.AddWithValue("value", "r");
                            cmd.ExecuteScalar();

                            // FFXIV stores stuff in Meters.
                            cmd.Parameters.AddWithValue("key", "root_name");
                            cmd.Parameters.AddWithValue("value", Path.GetFileNameWithoutExtension(model.Source));
                            cmd.ExecuteScalar();
                        }

                        // Skeleton
                        query = @"insert into skeleton (name, parent, matrix_0, matrix_1, matrix_2, matrix_3, matrix_4, matrix_5, matrix_6, matrix_7, matrix_8, matrix_9, matrix_10, matrix_11, matrix_12, matrix_13, matrix_14, matrix_15) 
											 values ($name, $parent, $matrix_0, $matrix_1, $matrix_2, $matrix_3, $matrix_4, $matrix_5, $matrix_6, $matrix_7, $matrix_8, $matrix_9, $matrix_10, $matrix_11, $matrix_12, $matrix_13, $matrix_14, $matrix_15);"                                            ;

                        using (SqliteCommand cmd = new SqliteCommand(query, db))
                        {
                            foreach (KeyValuePair <string, SkeletonData> b in boneDict)
                            {
                                KeyValuePair <string, SkeletonData> parent = boneDict.FirstOrDefault(x => x.Value.BoneNumber == b.Value.BoneParent);
                                string parentName = parent.Key == null ? null : parent.Key;
                                cmd.Parameters.AddWithValue("name", b.Value.BoneName);
                                cmd.Parameters.AddWithValue("parent", parentName);

                                for (int i = 0; i < 16; i++)
                                {
                                    cmd.Parameters.AddWithValue("matrix_" + i.ToString(), b.Value.PoseMatrix[i]);
                                }

                                cmd.ExecuteScalar();
                            }
                        }

                        int           modelIdx = 0;
                        List <string> models   = new List <string>()
                        {
                            Path.GetFileNameWithoutExtension(model.Source)
                        };
                        foreach (string mdl in models)
                        {
                            query = @"insert into models (model, name) values ($model, $name);";
                            using (SqliteCommand cmd = new SqliteCommand(query, db))
                            {
                                cmd.Parameters.AddWithValue("model", modelIdx);
                                cmd.Parameters.AddWithValue("name", mdl);
                                cmd.ExecuteScalar();
                            }

                            modelIdx++;
                        }

                        int matIdx = 0;
                        foreach (string material in model.Materials)
                        {
                            // Materials
                            query = @"insert into materials (material_id, name, diffuse, normal, specular, opacity, emissive) values ($material_id, $name, $diffuse, $normal, $specular, $opacity, $emissive);";
                            using (SqliteCommand cmd = new SqliteCommand(query, db))
                            {
                                string mtrl_prefix = directory + "\\" + Path.GetFileNameWithoutExtension(material.Substring(1)) + "_";
                                string mtrl_suffix = ".png";
                                string name        = material;
                                try
                                {
                                    name = Path.GetFileNameWithoutExtension(material);
                                }
                                catch
                                {
                                }

                                cmd.Parameters.AddWithValue("material_id", matIdx);
                                cmd.Parameters.AddWithValue("name", name);
                                cmd.Parameters.AddWithValue("diffuse", mtrl_prefix + "d" + mtrl_suffix);
                                cmd.Parameters.AddWithValue("normal", mtrl_prefix + "n" + mtrl_suffix);
                                cmd.Parameters.AddWithValue("specular", mtrl_prefix + "s" + mtrl_suffix);
                                cmd.Parameters.AddWithValue("emissive", mtrl_prefix + "e" + mtrl_suffix);
                                cmd.Parameters.AddWithValue("opacity", mtrl_prefix + "o" + mtrl_suffix);
                                cmd.ExecuteScalar();
                            }

                            matIdx++;
                        }

                        int meshIdx = 0;
                        foreach (TTMeshGroup m in model.MeshGroups)
                        {
                            // Bones
                            query = @"insert into bones (mesh, bone_id, name) values ($mesh, $bone_id, $name);";
                            int bIdx = 0;
                            foreach (string b in m.Bones)
                            {
                                using (SqliteCommand cmd = new SqliteCommand(query, db))
                                {
                                    cmd.Parameters.AddWithValue("name", b);
                                    cmd.Parameters.AddWithValue("bone_id", bIdx);
                                    cmd.Parameters.AddWithValue("parent_id", null);
                                    cmd.Parameters.AddWithValue("mesh", meshIdx);
                                    cmd.ExecuteScalar();
                                }

                                bIdx++;
                            }

                            // Groups
                            query = @"insert into meshes (mesh, name, material_id, model) values ($mesh, $name, $material_id, $model);";
                            using (SqliteCommand cmd = new SqliteCommand(query, db))
                            {
                                cmd.Parameters.AddWithValue("name", m.Name);
                                cmd.Parameters.AddWithValue("mesh", meshIdx);

                                // This is always 0 for now.  Support element for Liinko's work on multi-model export.
                                cmd.Parameters.AddWithValue("model", 0);
                                cmd.Parameters.AddWithValue("material_id", model.GetMaterialIndex(meshIdx));
                                cmd.ExecuteScalar();
                            }

                            // Parts
                            int partIdx = 0;
                            foreach (TTMeshPart p in m.Parts)
                            {
                                // Parts
                                query = @"insert into parts (mesh, part, name) values ($mesh, $part, $name);";
                                using (SqliteCommand cmd = new SqliteCommand(query, db))
                                {
                                    cmd.Parameters.AddWithValue("name", p.Name);
                                    cmd.Parameters.AddWithValue("part", partIdx);
                                    cmd.Parameters.AddWithValue("mesh", meshIdx);
                                    cmd.ExecuteScalar();
                                }

                                // Vertices
                                int vIdx = 0;
                                foreach (TTVertex v in p.Vertices)
                                {
                                    query = @"insert into vertices ( mesh,  part,  vertex_id,  position_x,  position_y,  position_z,  normal_x,  normal_y,  normal_z,  color_r,  color_g,  color_b,  color_a,  uv_1_u,  uv_1_v,  uv_2_u,  uv_2_v,  bone_1_id,  bone_1_weight,  bone_2_id,  bone_2_weight,  bone_3_id,  bone_3_weight,  bone_4_id,  bone_4_weight) 
														values ($mesh, $part, $vertex_id, $position_x, $position_y, $position_z, $normal_x, $normal_y, $normal_z, $color_r, $color_g, $color_b, $color_a, $uv_1_u, $uv_1_v, $uv_2_u, $uv_2_v, $bone_1_id, $bone_1_weight, $bone_2_id, $bone_2_weight, $bone_3_id, $bone_3_weight, $bone_4_id, $bone_4_weight);"                                                        ;
                                    using (SqliteCommand cmd = new SqliteCommand(query, db))
                                    {
                                        cmd.Parameters.AddWithValue("part", partIdx);
                                        cmd.Parameters.AddWithValue("mesh", meshIdx);
                                        cmd.Parameters.AddWithValue("vertex_id", vIdx);

                                        cmd.Parameters.AddWithValue("position_x", v.Position.X);
                                        cmd.Parameters.AddWithValue("position_y", v.Position.Y);
                                        cmd.Parameters.AddWithValue("position_z", v.Position.Z);

                                        cmd.Parameters.AddWithValue("normal_x", v.Normal.X);
                                        cmd.Parameters.AddWithValue("normal_y", v.Normal.Y);
                                        cmd.Parameters.AddWithValue("normal_z", v.Normal.Z);

                                        cmd.Parameters.AddWithValue("color_r", v.VertexColor[0] / 255f);
                                        cmd.Parameters.AddWithValue("color_g", v.VertexColor[1] / 255f);
                                        cmd.Parameters.AddWithValue("color_b", v.VertexColor[2] / 255f);
                                        cmd.Parameters.AddWithValue("color_a", v.VertexColor[3] / 255f);

                                        cmd.Parameters.AddWithValue("uv_1_u", v.UV1.X);
                                        cmd.Parameters.AddWithValue("uv_1_v", v.UV1.Y);
                                        cmd.Parameters.AddWithValue("uv_2_u", v.UV2.X);
                                        cmd.Parameters.AddWithValue("uv_2_v", v.UV2.Y);

                                        cmd.Parameters.AddWithValue("bone_1_id", v.BoneIds[0]);
                                        cmd.Parameters.AddWithValue("bone_1_weight", v.Weights[0] / 255f);

                                        cmd.Parameters.AddWithValue("bone_2_id", v.BoneIds[1]);
                                        cmd.Parameters.AddWithValue("bone_2_weight", v.Weights[1] / 255f);

                                        cmd.Parameters.AddWithValue("bone_3_id", v.BoneIds[2]);
                                        cmd.Parameters.AddWithValue("bone_3_weight", v.Weights[2] / 255f);

                                        cmd.Parameters.AddWithValue("bone_4_id", v.BoneIds[3]);
                                        cmd.Parameters.AddWithValue("bone_4_weight", v.Weights[3] / 255f);

                                        cmd.ExecuteScalar();
                                        vIdx++;
                                    }
                                }

                                // Indices
                                for (int i = 0; i < p.TriangleIndices.Count; i++)
                                {
                                    query = @"insert into indices (mesh, part, index_id, vertex_id) values ($mesh, $part, $index_id, $vertex_id);";
                                    using (SqliteCommand cmd = new SqliteCommand(query, db))
                                    {
                                        cmd.Parameters.AddWithValue("part", partIdx);
                                        cmd.Parameters.AddWithValue("mesh", meshIdx);
                                        cmd.Parameters.AddWithValue("index_id", i);
                                        cmd.Parameters.AddWithValue("vertex_id", p.TriangleIndices[i]);
                                        cmd.ExecuteScalar();
                                    }
                                }

                                // Shape Parts
                                foreach (KeyValuePair <string, TTShapePart> shpKv in p.ShapeParts)
                                {
                                    if (!shpKv.Key.StartsWith("shp_"))
                                    {
                                        continue;
                                    }
                                    TTShapePart shp = shpKv.Value;

                                    query = @"insert into shape_vertices ( mesh,  part,  shape,  vertex_id,  position_x,  position_y,  position_z) 
																   values($mesh, $part, $shape, $vertex_id, $position_x, $position_y, $position_z);"                                                                ;
                                    using (SqliteCommand cmd = new SqliteCommand(query, db))
                                    {
                                        foreach (KeyValuePair <int, int> vKv in shp.VertexReplacements)
                                        {
                                            TTVertex v = shp.Vertices[vKv.Value];
                                            cmd.Parameters.AddWithValue("part", partIdx);
                                            cmd.Parameters.AddWithValue("mesh", meshIdx);
                                            cmd.Parameters.AddWithValue("shape", shpKv.Key);
                                            cmd.Parameters.AddWithValue("vertex_id", vKv.Key);

                                            cmd.Parameters.AddWithValue("position_x", v.Position.X);
                                            cmd.Parameters.AddWithValue("position_y", v.Position.Y);
                                            cmd.Parameters.AddWithValue("position_z", v.Position.Z);

                                            cmd.ExecuteScalar();
                                            vIdx++;
                                        }
                                    }
                                }

                                partIdx++;
                            }

                            meshIdx++;
                        }

                        transaction.Commit();
                    }
                }
            }
            catch (Exception)
            {
                ModelModifiers.MakeImportReady(model, loggingFunction);
                throw;
            }

            // Undo the export ready at the start.
            ModelModifiers.MakeImportReady(model, loggingFunction);
        }