/// <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); }
/*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); }
/// <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++; } }
/// <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); }
/// <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); }