Files
LuaCsForBarotraumaEP/Libraries/MonoGame.Framework/Src/MonoGame.Framework.Content.Pipeline/Graphics/MeshHelper.cs
2019-06-25 16:00:44 +03:00

713 lines
28 KiB
C#

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);
}
/// <summary>
/// Generates vertex normals by accumulation of triangle face normals.
/// </summary>
/// <param name="mesh">The mesh which will recieve the normals.</param>
/// <param name="overwriteExistingNormals">Overwrite or skip over geometry with existing normals.</param>
/// <remarks>
/// This calls <see cref="CalculateNormals(GeometryContent, bool)"/> to do the work.
/// </remarks>
public static void CalculateNormals(MeshContent mesh, bool overwriteExistingNormals)
{
foreach (var geom in mesh.Geometry)
CalculateNormals(geom, overwriteExistingNormals);
}
/// <summary>
/// Generates vertex normals by accumulation of triangle face normals.
/// </summary>
/// <param name="geom">The geometry which will recieve the normals.</param>
/// <param name="overwriteExistingNormals">Overwrite or skip over geometry with existing normals.</param>
/// <remarks>
/// 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.
/// </remarks>
public static void CalculateNormals(GeometryContent geom, bool overwriteExistingNormals)
{
VertexChannel<Vector3> 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<Vector3>(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<Vector3>(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]];
}
/// <summary>
/// Generate the tangents and binormals (tangent frames) for each vertex in the mesh.
/// </summary>
/// <param name="mesh">The mesh which will have add tangent and binormal channels added.</param>
/// <param name="textureCoordinateChannelName">The Vector2 texture coordinate channel used to generate tangent frames.</param>
/// <param name="tangentChannelName"></param>
/// <param name="binormalChannelName"></param>
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<Vector3>(VertexChannelNames.Normal(0));
var uvs = channels.Get<Vector2>(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<Vector3> positions,
IList<int> indices,
IList<Vector3> normals,
IList<Vector2> 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;
}
}
/// <summary>
/// Search for the root bone of the skeletion.
/// </summary>
/// <param name="node">The node from which to begin the search for the skeleton.</param>
/// <returns>The root bone of the skeletion or null if none is found.</returns>
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;
}
/// <summary>
/// Traverses a skeleton depth-first and builds a list of its bones.
/// </summary>
public static IList<BoneContent> FlattenSkeleton(BoneContent skeleton)
{
if (skeleton == null)
throw new ArgumentNullException("skeleton");
var results = new List<BoneContent>();
var work = new Stack<NodeContent>(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;
}
/// <summary>
/// Merge any positions in the <see cref="PositionCollection"/> of the
/// specified mesh that are at a distance less than the specified tolerance
/// from each other.
/// </summary>
/// <param name="mesh">Mesh to be processed.</param>
/// <param name="tolerance">Tolerance value that determines how close
/// positions must be to each other to be merged.</param>
/// <remarks>
/// This method will also update the <see cref="VertexContent.PositionIndices"/>
/// in the <see cref="GeometryContent"/> of the specified mesh.
/// </remarks>
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<IndexUpdateList>();
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);
}
}
}
}
/// <summary>
/// Merge vertices with the same <see cref="VertexContent.PositionIndices"/> and
/// <see cref="VertexChannel"/> data within the specified
/// <see cref="GeometryContent"/>.
/// </summary>
/// <param name="geometry">Geometry to be processed.</param>
public static void MergeDuplicateVertices(GeometryContent geometry)
{
if (geometry == null)
throw new ArgumentNullException("geometry");
var verts = geometry.Vertices;
var hashMap = new Dictionary<int, List<VertexData>>();
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<VertexData> 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<VertexData> { iData });
}
if (!merged)
vIndex++;
}
// update the indices because of the vertices we removed
indices.Pack();
}
/// <summary>
/// Merge vertices with the same <see cref="VertexContent.PositionIndices"/> and
/// <see cref="VertexChannel"/> data within the <see cref="MeshContent.Geometry"/>
/// of this mesh. If you want to merge positions too, call
/// <see cref="MergeDuplicatePositions"/> on your mesh before this function.
/// </summary>
/// <param name="mesh">Mesh to be processed</param>
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.
}
/// <summary>
/// Reverses the triangle winding order of the mesh.
/// </summary>
/// <param name="mesh">The mesh which will be modified.</param>
/// <remarks>
/// This method is useful when changing the direction of backface culling
/// like when switching between left/right handed coordinate systems.
/// </remarks>
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;
}
}
}
/// <summary>
/// Transforms the contents of a node and its descendants.
/// </summary>
/// <remarks>The node transforms themselves are unaffected.</remarks>
/// <param name="scene">The root node of the scene to transform.</param>
/// <param name="transform">The transform matrix to apply to the scene.</param>
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<NodeContent>();
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;
}
}
/// <summary>
/// Determines whether the specified transform is left-handed.
/// </summary>
/// <param name="xform">The transform.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="xform"/> is left-handed; otherwise,
/// <see langword="false"/> if <paramref name="xform"/> is right-handed.
/// </returns>
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<int> _collectionToUpdate;
private readonly Dictionary<int, List<int>> _indexPositions;
// create the list, presort the values and compute the start positions of each value
public IndexUpdateList(IList<int> collectionToUpdate)
{
_collectionToUpdate = collectionToUpdate;
_indexPositions = new Dictionary<int, List<int>>();
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<int> {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<int>(_collectionToUpdate);
var newIndex = 0;
foreach (var value in sorted)
{
foreach (var pos in _indexPositions[value])
_collectionToUpdate[pos] = newIndex;
newIndex++;
}
}
}
#endregion
}
}