using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; namespace Microsoft.Xna.Framework.Content.Pipeline.Graphics { public static class MeshHelper { static bool IsFinite(float v) { return !float.IsInfinity(v) && !float.IsNaN(v); } static bool IsFinite(this Vector3 v) { return IsFinite(v.X) && IsFinite(v.Y) && IsFinite(v.Z); } /// /// Generates vertex normals by accumulation of triangle face normals. /// /// The mesh which will recieve the normals. /// Overwrite or skip over geometry with existing normals. /// /// This calls to do the work. /// public static void CalculateNormals(MeshContent mesh, bool overwriteExistingNormals) { foreach (var geom in mesh.Geometry) CalculateNormals(geom, overwriteExistingNormals); } /// /// Generates vertex normals by accumulation of triangle face normals. /// /// The geometry which will recieve the normals. /// Overwrite or skip over geometry with existing normals. /// /// We use a "Mean Weighted Equally" method generate vertex normals from triangle /// face normals. If normal cannot be calculated from the geometry we set it to zero. /// public static void CalculateNormals(GeometryContent geom, bool overwriteExistingNormals) { VertexChannel channel; // Look for an existing normals channel. if (!geom.Vertices.Channels.Contains(VertexChannelNames.Normal())) { // We don't have existing normals, so add a new channel. channel = geom.Vertices.Channels.Add(VertexChannelNames.Normal(), null); } else { // If we're not supposed to overwrite the existing // normals then we're done here. if (!overwriteExistingNormals) return; channel = geom.Vertices.Channels.Get(VertexChannelNames.Normal()); } var positionIndices = geom.Vertices.PositionIndices; Debug.Assert(positionIndices.Count == channel.Count, "The position and channel sizes were different!"); // Accumulate all the triangle face normals for each vertex. var normals = new Vector3[positionIndices.Count]; for (var i = 0; i < geom.Indices.Count; i += 3) { var ia = geom.Indices[i + 0]; var ib = geom.Indices[i + 1]; var ic = geom.Indices[i + 2]; var aa = geom.Vertices.Positions[ia]; var bb = geom.Vertices.Positions[ib]; var cc = geom.Vertices.Positions[ic]; var faceNormal = Vector3.Cross(cc - bb, bb - aa); var len = faceNormal.Length(); if (len > 0.0f) { faceNormal = faceNormal / len; // We are using the "Mean Weighted Equally" method where each // face has an equal weight in the final normal calculation. // // We could maybe switch to "Mean Weighted by Angle" which is said // to look best in most cases, but is more expensive to calculate. // // There is also an idea of weighting by triangle area, but IMO the // triangle area doesn't always have a direct relationship to the // shape of a mesh. // // For more ideas see: // // "A Comparison of Algorithms for Vertex Normal Computation" // by Shuangshuang Jin, Robert R. Lewis, David West. // normals[positionIndices[ia]] += faceNormal; normals[positionIndices[ib]] += faceNormal; normals[positionIndices[ic]] += faceNormal; } } // Normalize the gathered vertex normals. for (var i = 0; i < normals.Length; i++) { var normal = normals[i]; var len = normal.Length(); if (len > 0.0f) normals[i] = normal / len; else { // TODO: It would be nice to be able to log this to // the pipeline so that it can be fixed in the model. // TODO: We could maybe void this by a better algorithm // above for generating the normals. // We have a zero length normal. You can argue that putting // anything here is better than nothing, but by leaving it to // zero it allows the caller to detect this and react to it. normals[i] = Vector3.Zero; } } // Set the new normals on the vertex channel. for (var i = 0; i < channel.Count; i++) channel[i] = normals[geom.Indices[i]]; } /// /// Generate the tangents and binormals (tangent frames) for each vertex in the mesh. /// /// The mesh which will have add tangent and binormal channels added. /// The Vector2 texture coordinate channel used to generate tangent frames. /// /// public static void CalculateTangentFrames(MeshContent mesh, string textureCoordinateChannelName, string tangentChannelName, string binormalChannelName) { foreach (var geom in mesh.Geometry) CalculateTangentFrames(geom, textureCoordinateChannelName, tangentChannelName, binormalChannelName); } public static void CalculateTangentFrames(GeometryContent geom, string textureCoordinateChannelName, string tangentChannelName, string binormalChannelName) { var verts = geom.Vertices; var indices = geom.Indices; var channels = geom.Vertices.Channels; var normals = channels.Get(VertexChannelNames.Normal(0)); var uvs = channels.Get(textureCoordinateChannelName); Vector3[] tangents, bitangents; CalculateTangentFrames(verts.Positions, indices, normals, uvs, out tangents, out bitangents); // All the indices are 1:1 with the others, so we // can just add the new channels in place. if (!string.IsNullOrEmpty(tangentChannelName)) channels.Add(tangentChannelName, tangents); if (!string.IsNullOrEmpty(binormalChannelName)) channels.Add(binormalChannelName, bitangents); } public static void CalculateTangentFrames(IList positions, IList indices, IList normals, IList textureCoords, out Vector3[] tangents, out Vector3[] bitangents) { // Lengyel, Eric. “Computing Tangent Space Basis Vectors for an Arbitrary Mesh”. // Terathon Software 3D Graphics Library, 2001. // http://www.terathon.com/code/tangent.html // Hegde, Siddharth. "Messing with Tangent Space". Gamasutra, 2007. // http://www.gamasutra.com/view/feature/129939/messing_with_tangent_space.php var numVerts = positions.Count; var numIndices = indices.Count; var tan1 = new Vector3[numVerts]; var tan2 = new Vector3[numVerts]; for (var index = 0; index < numIndices; index += 3) { var i1 = indices[index + 0]; var i2 = indices[index + 1]; var i3 = indices[index + 2]; var w1 = textureCoords[i1]; var w2 = textureCoords[i2]; var w3 = textureCoords[i3]; var s1 = w2.X - w1.X; var s2 = w3.X - w1.X; var t1 = w2.Y - w1.Y; var t2 = w3.Y - w1.Y; var denom = s1 * t2 - s2 * t1; if (Math.Abs(denom) < float.Epsilon) { // The triangle UVs are zero sized one dimension. // // So we cannot calculate the vertex tangents for this // one trangle, but maybe it can with other trangles. continue; } var r = 1.0f / denom; Debug.Assert(IsFinite(r), "Bad r!"); var v1 = positions[i1]; var v2 = positions[i2]; var v3 = positions[i3]; var x1 = v2.X - v1.X; var x2 = v3.X - v1.X; var y1 = v2.Y - v1.Y; var y2 = v3.Y - v1.Y; var z1 = v2.Z - v1.Z; var z2 = v3.Z - v1.Z; var sdir = new Vector3() { X = (t2 * x1 - t1 * x2) * r, Y = (t2 * y1 - t1 * y2) * r, Z = (t2 * z1 - t1 * z2) * r, }; var tdir = new Vector3() { X = (s1 * x2 - s2 * x1) * r, Y = (s1 * y2 - s2 * y1) * r, Z = (s1 * z2 - s2 * z1) * r, }; tan1[i1] += sdir; Debug.Assert(tan1[i1].IsFinite(), "Bad tan1[i1]!"); tan1[i2] += sdir; Debug.Assert(tan1[i2].IsFinite(), "Bad tan1[i2]!"); tan1[i3] += sdir; Debug.Assert(tan1[i3].IsFinite(), "Bad tan1[i3]!"); tan2[i1] += tdir; Debug.Assert(tan2[i1].IsFinite(), "Bad tan2[i1]!"); tan2[i2] += tdir; Debug.Assert(tan2[i2].IsFinite(), "Bad tan2[i2]!"); tan2[i3] += tdir; Debug.Assert(tan2[i3].IsFinite(), "Bad tan2[i3]!"); } tangents = new Vector3[numVerts]; bitangents = new Vector3[numVerts]; // At this point we have all the vectors accumulated, but we need to average // them all out. So we loop through all the final verts and do a Gram-Schmidt // orthonormalize, then make sure they're all unit length. for (var i = 0; i < numVerts; i++) { var n = normals[i]; Debug.Assert(n.IsFinite(), "Bad normal!"); Debug.Assert(n.Length() >= 0.9999f, "Bad normal!"); var t = tan1[i]; if (t.LengthSquared() < float.Epsilon) { // TODO: Ideally we could spit out a warning to the // content logging here! // We couldn't find a good tanget for this vertex. // // Rather than set them to zero which could produce // errors in other parts of the pipeline, we just take // a guess at something that may look ok. t = Vector3.Cross(n, Vector3.UnitX); if (t.LengthSquared() < float.Epsilon) t = Vector3.Cross(n, Vector3.UnitY); tangents[i] = Vector3.Normalize(t); bitangents[i] = Vector3.Cross(n, tangents[i]); continue; } // Gram-Schmidt orthogonalize // TODO: This can be zero can cause NaNs on // normalize... how do we fix this? var tangent = t - n * Vector3.Dot(n, t); tangent = Vector3.Normalize(tangent); Debug.Assert(tangent.IsFinite(), "Bad tangent!"); tangents[i] = tangent; // Calculate handedness var w = (Vector3.Dot(Vector3.Cross(n, t), tan2[i]) < 0.0F) ? -1.0F : 1.0F; Debug.Assert(IsFinite(w), "Bad handedness!"); // Calculate the bitangent var bitangent = Vector3.Cross(n, tangent) * w; Debug.Assert(bitangent.IsFinite(), "Bad bitangent!"); bitangents[i] = bitangent; } } /// /// Search for the root bone of the skeletion. /// /// The node from which to begin the search for the skeleton. /// The root bone of the skeletion or null if none is found. public static BoneContent FindSkeleton(NodeContent node) { // We should always get a node to search! if (node == null) throw new ArgumentNullException("node"); // Search up thru the hierarchy. for (; node != null; node = node.Parent) { // First if this node is a bone then search up for the root. var root = node as BoneContent; if (root != null) { while (root.Parent is BoneContent) root = (BoneContent)root.Parent; return root; } // Next try searching the children for a root bone. foreach (var nodeContent in node.Children) { var bone = nodeContent as BoneContent; if (bone == null) continue; // If we found a bone if (root != null) throw new InvalidContentException("DuplicateSkeleton", node.Identity); // This is our new root. root = bone; } // If we found a root bone then return it, else // we continue the search to the node parent. if (root != null) return root; } // We didn't find any bones! return null; } /// /// Traverses a skeleton depth-first and builds a list of its bones. /// public static IList FlattenSkeleton(BoneContent skeleton) { if (skeleton == null) throw new ArgumentNullException("skeleton"); var results = new List(); var work = new Stack(new[] { skeleton }); while (work.Count > 0) { var top = work.Pop(); var bone = top as BoneContent; if (bone != null) results.Add(bone); for (var i = top.Children.Count - 1; i >= 0; i--) work.Push(top.Children[i]); } return results; } /// /// Merge any positions in the of the /// specified mesh that are at a distance less than the specified tolerance /// from each other. /// /// Mesh to be processed. /// Tolerance value that determines how close /// positions must be to each other to be merged. /// /// This method will also update the /// in the of the specified mesh. /// public static void MergeDuplicatePositions(MeshContent mesh, float tolerance) { if (mesh == null) throw new ArgumentNullException("mesh"); // TODO Improve performance with spatial partitioning scheme var indexLists = new List(); foreach (var geom in mesh.Geometry) { var list = new IndexUpdateList(geom.Vertices.PositionIndices); indexLists.Add(list); } for (var i = mesh.Positions.Count - 1; i >= 1; i--) { var pi = mesh.Positions[i]; for (var j = i - 1; j >= 0; j--) { var pj = mesh.Positions[j]; if (Vector3.Distance(pi, pj) <= tolerance) { foreach (var list in indexLists) list.Update(i, j); mesh.Positions.RemoveAt(i); } } } } /// /// Merge vertices with the same and /// data within the specified /// . /// /// Geometry to be processed. public static void MergeDuplicateVertices(GeometryContent geometry) { if (geometry == null) throw new ArgumentNullException("geometry"); var verts = geometry.Vertices; var hashMap = new Dictionary>(); var indices = new IndexUpdateList(geometry.Indices); var vIndex = 0; for (var i = 0; i < geometry.Indices.Count; i++) { var iIndex = geometry.Indices[i]; var iData = new VertexData { Index = iIndex, PositionIndex = verts.PositionIndices[vIndex], ChannelData = new object[verts.Channels.Count] }; for (var channel = 0; channel < verts.Channels.Count; channel++) iData.ChannelData[channel] = verts.Channels[channel][vIndex]; var hash = iData.ComputeHash(); var merged = false; List candidates; if (hashMap.TryGetValue(hash, out candidates)) { for (var candidateIndex = 0; candidateIndex < candidates.Count; candidateIndex++) { var c = candidates[candidateIndex]; if (!iData.ContentEquals(c)) continue; // Match! Update the corresponding indices and remove the vertex indices.Update(iIndex, c.Index); verts.RemoveAt(vIndex); merged = true; } if (!merged) candidates.Add(iData); } else { // no vertices with the same hash yet, create a new list for the data hashMap.Add(hash, new List { iData }); } if (!merged) vIndex++; } // update the indices because of the vertices we removed indices.Pack(); } /// /// Merge vertices with the same and /// data within the /// of this mesh. If you want to merge positions too, call /// on your mesh before this function. /// /// Mesh to be processed public static void MergeDuplicateVertices(MeshContent mesh) { if (mesh == null) throw new ArgumentNullException("mesh"); foreach (var geom in mesh.Geometry) MergeDuplicateVertices(geom); } public static void OptimizeForCache(MeshContent mesh) { // We don't throw here as non-optimized still works. } /// /// Reverses the triangle winding order of the mesh. /// /// The mesh which will be modified. /// /// This method is useful when changing the direction of backface culling /// like when switching between left/right handed coordinate systems. /// public static void SwapWindingOrder(MeshContent mesh) { // Gotta have a mesh to run! if (mesh == null) throw new ArgumentNullException("mesh"); foreach (var geom in mesh.Geometry) { for (var i = 0; i < geom.Indices.Count; i += 3) { var first = geom.Indices[i]; var last = geom.Indices[i+2]; geom.Indices[i] = last; geom.Indices[i+2] = first; } } } /// /// Transforms the contents of a node and its descendants. /// /// The node transforms themselves are unaffected. /// The root node of the scene to transform. /// The transform matrix to apply to the scene. public static void TransformScene(NodeContent scene, Matrix transform) { if (scene == null) throw new ArgumentException("scene"); // If the transformation is an identity matrix, this is a no-op and // we can save ourselves a bunch of work in the first place. if (transform == Matrix.Identity) return; var inverseTransform = Matrix.Invert(transform); var work = new Stack(); work.Push(scene); while (work.Count > 0) { var node = work.Pop(); foreach (var child in node.Children) work.Push(child); // Transform the mesh content. var mesh = node as MeshContent; if (mesh != null) mesh.TransformContents(ref transform); // Transform local coordinate system using "similarity transform". node.Transform = inverseTransform * node.Transform * transform; // Transform animations. foreach (var animationContent in node.Animations.Values) foreach (var animationChannel in animationContent.Channels.Values) for (int i = 0; i < animationChannel.Count; i++) animationChannel[i].Transform = inverseTransform * animationChannel[i].Transform * transform; } } /// /// Determines whether the specified transform is left-handed. /// /// The transform. /// /// if is left-handed; otherwise, /// if is right-handed. /// internal static bool IsLeftHanded(ref Matrix xform) { // Check sign of determinant of upper-left 3x3 matrix: // positive determinant ... right-handed // negative determinant ... left-handed // Since XNA does not have a 3x3 matrix, use the "scalar triple product" // (see http://en.wikipedia.org/wiki/Triple_product) to calculate the // determinant. float d = Vector3.Dot(xform.Right, Vector3.Cross(xform.Forward, xform.Up)); return d < 0.0f; } #region Private helpers private static void UpdatePositionIndices(MeshContent mesh, int from, int to) { foreach (var geom in mesh.Geometry) { for (var i = 0; i < geom.Vertices.PositionIndices.Count; i++) { var index = geom.Vertices.PositionIndices[i]; if (index == from) geom.Vertices.PositionIndices[i] = to; } } } private class VertexData { public int Index; public int PositionIndex; public object[] ChannelData; // Compute a hash based on PositionIndex and ChannelData public int ComputeHash() { var hash = PositionIndex; foreach (var channel in ChannelData) hash ^= channel.GetHashCode(); return hash; } // Check equality on PositionIndex and ChannelData public bool ContentEquals(VertexData other) { if (PositionIndex != other.PositionIndex) return false; if (ChannelData.Length != other.ChannelData.Length) return false; for (var i = 0; i < ChannelData.Length; i++) { if (!Equals(ChannelData[i], other.ChannelData[i])) return false; } return true; } } // takes an IndexCollection and can efficiently update index values private class IndexUpdateList { private readonly IList _collectionToUpdate; private readonly Dictionary> _indexPositions; // create the list, presort the values and compute the start positions of each value public IndexUpdateList(IList collectionToUpdate) { _collectionToUpdate = collectionToUpdate; _indexPositions = new Dictionary>(); Initialize(); } private void Initialize() { for (var pos = 0; pos < _collectionToUpdate.Count; pos++) { var v = _collectionToUpdate[pos]; if (_indexPositions.ContainsKey(v)) _indexPositions[v].Add(pos); else _indexPositions.Add(v, new List {pos}); } } public void Update(int from, int to) { if (from == to || !_indexPositions.ContainsKey(from)) return; foreach (var pos in _indexPositions[from]) _collectionToUpdate[pos] = to; if (_indexPositions.ContainsKey(to)) _indexPositions[to].AddRange(_indexPositions[from]); else _indexPositions.Add(to, _indexPositions[from]); _indexPositions.Remove(from); } // Pack all indices together starting from zero // E.g. [5, 5, 3, 5, 21, 3] -> [1, 1, 0, 1, 2, 0] // note that the order must be kept public void Pack() { if (_collectionToUpdate.Count == 0) return; var sorted = new SortedSet(_collectionToUpdate); var newIndex = 0; foreach (var value in sorted) { foreach (var pos in _indexPositions[value]) _collectionToUpdate[pos] = newIndex; newIndex++; } } } #endregion } }