/// <summary> /// Accessor for the full unified MeshGroup level Vertex list. /// </summary> /// <param name="id"></param> /// <returns></returns> public TTVertex GetVertexAt(int id) { if (Parts.Count == 0) { return(null); } var startingOffset = 0; TTMeshPart part = Parts[0]; foreach (var p in Parts) { if (startingOffset + p.Vertices.Count < id) { startingOffset += p.Vertices.Count; } else { part = p; break; } } var realId = id - startingOffset; return(part.Vertices[realId]); }
/*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> /// 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); }