Mesh diagnostics core for the AdvancedMappingTools editor package. Walks a PolygonMesh or a PartPlan's parts and reports open edges, unwelded seams, flipped neighbours, inverted shells, degenerate or non planer faces, coplanar/back to back overlaps, self and shell intersections, buried faces, unused vertices and faces the engine rejected. Also has a Repair pass that removes duplicate, degenerate and engine-rejected faces plus loose vertices.
namespace AdvancedMappingTools;
public enum MeshIssueKind
{
[Title( "Open Edge" )] OpenEdge,
[Title( "Unwelded Seam" )] UnweldedSeam,
[Title( "Flipped Neighbour" )] FlippedNeighbour,
[Title( "Inverted Shell" )] InvertedShell,
[Title( "Degenerate Face" )] DegenerateFace,
[Title( "Bad Triangulation" )] BadTriangulation,
[Title( "Non Planar Face" )] NonPlanarFace,
[Title( "Duplicate Face" )] DuplicateFace,
[Title( "Coplanar Overlap" )] CoplanarOverlap,
[Title( "Back To Back Faces" )] BackToBackFaces,
[Title( "Buried Face" )] BuriedFace,
[Title( "Self Intersection" )] SelfIntersection,
[Title( "Shell Intersection" )] ShellIntersection,
[Title( "Object Intersection" )] ObjectIntersection,
[Title( "Unused Vertex" )] UnusedVertex,
[Title( "Engine Bad Face" )] EngineBadFace
}
public enum MeshIssueLevel
{
Info,
Warning,
Error
}
/// <summary>
/// One thing found in a mesh and where it is. <see cref="Owner"/> is the index of the mesh it
/// belongs to when several were checked together.
/// </summary>
public readonly record struct MeshIssue( MeshIssueKind Kind, MeshIssueLevel Level, string Text, Vector3 Position, int Owner, HalfEdgeMesh.FaceHandle Face );
public sealed class MeshReport
{
const int MaxStored = 64;
readonly Dictionary<MeshIssueKind, int> _counts = [];
public List<MeshIssue> Issues { get; } = [];
public int Faces { get; set; }
public int Triangles { get; set; }
public int Shells { get; set; }
public int ClosedShells { get; set; }
public int Count( MeshIssueKind kind ) => _counts.TryGetValue( kind, out var count ) ? count : 0;
public int Errors { get; private set; }
public int Warnings { get; private set; }
public bool IsClean => Errors == 0;
public void Add( MeshIssue issue )
{
_counts[issue.Kind] = Count( issue.Kind ) + 1;
if ( issue.Level == MeshIssueLevel.Error ) Errors++;
if ( issue.Level == MeshIssueLevel.Warning ) Warnings++;
// Per kind, so a few real faults are not lost behind hundreds of notes.
if ( Count( issue.Kind ) <= MaxStored ) Issues.Add( issue );
}
public string Summary()
{
var parts = _counts.OrderBy( x => x.Key ).Select( x => $"{x.Key}={x.Value}" );
return $"faces={Faces} tris={Triangles} shells={Shells} closed={ClosedShells} errors={Errors} warnings={Warnings} {string.Join( " ", parts )}".TrimEnd();
}
}
/// <summary>
/// Checks meshes for what makes geometry unreliable: open and misconnected edges, flipped
/// shells, broken triangulation, faces lying on top of each other and bodies cutting through
/// one another. It only reads and it uses the triangles the engine itself makes.
/// Meshes checked together count as independent objects: where they touch or cross, that is
/// reported apart from the same thing happening inside one mesh.
/// </summary>
public static class MeshDiagnostics
{
const float Weld = 0.02f;
const float PlaneDistance = 0.02f;
const float FacePlaneDistance = 0.06f;
const float MinArea = 0.0005f;
const float MinOverlap = 0.02f;
const float SmallestStrip = 0.08f;
const float Penetration = 0.03f;
const float ParallelDot = 0.9995f;
const float PlanarLimit = 0.05f;
sealed class Tri
{
public Vector3 A, B, C, Normal, Mins, Maxs;
public int Face;
}
sealed class Item
{
public HalfEdgeMesh.FaceHandle Handle;
public int Owner, Shell;
public int[] Ring;
public Vector3[] Points;
public Vector3 Normal, Center, Mins, Maxs;
public float Area;
public readonly List<Tri> Tris = [];
}
public static MeshReport Analyze( PolygonMesh mesh ) => Analyze( [(mesh, new Transform( Vector3.Zero, Rotation.Identity ))] );
/// <summary>
/// Take out what is wrong beyond doubt: faces without area, faces that are there twice and
/// vertices nothing uses. Bodies that cross each other stay, that can be on purpose.
/// Gives back how much was removed.
/// </summary>
public static int Repair( PolygonMesh mesh )
{
var removed = 0;
for ( var pass = 0; pass < 32; pass++ )
{
var report = Analyze( mesh );
var faces = report.Issues
.Where( x => x.Kind is MeshIssueKind.DuplicateFace or MeshIssueKind.DegenerateFace or MeshIssueKind.EngineBadFace )
.Select( x => x.Face )
.Distinct()
.ToList();
if ( faces.Count == 0 ) break;
mesh.RemoveFaces( faces );
removed += faces.Count;
}
var loose = mesh.VertexHandles.Where( x => mesh.GetFacesConnectedToVertex( x, out var faces ) is var found && (!found || faces is null || faces.Count == 0) ).ToList();
if ( loose.Count > 0 ) mesh.RemoveVertices( loose );
return removed + loose.Count;
}
/// <summary>
/// Check every part of a plan as an independent object.
/// </summary>
public static MeshReport Analyze( PartPlan plan )
{
var meshes = new List<(PolygonMesh, Transform)>();
void Visit( PartPlan part, Transform parent )
{
var world = parent.ToWorld( part.Local );
if ( part.Mesh is not null ) meshes.Add( (part.Mesh, world) );
foreach ( var child in part.Children )
Visit( child, world );
}
Visit( plan, new Transform( Vector3.Zero, Rotation.Identity ) );
return Analyze( meshes );
}
/// <summary>
/// Check meshes together, each with its transform into a shared space.
/// </summary>
public static MeshReport Analyze( IReadOnlyList<(PolygonMesh Mesh, Transform World)> meshes )
{
var report = new MeshReport();
var welded = new Dictionary<(int, int, int), int>();
var items = new List<Item>();
var shellOffset = 0;
var open = new List<(int Owner, HalfEdgeMesh.FaceHandle Face, int From, int To, Vector3 A, Vector3 B, int Shell)>();
int WeldId( Vector3 p )
{
var key = ((int)MathF.Round( p.x / Weld ), (int)MathF.Round( p.y / Weld ), (int)MathF.Round( p.z / Weld ));
if ( !welded.TryGetValue( key, out var id ) )
welded[key] = id = welded.Count;
return id;
}
for ( var owner = 0; owner < meshes.Count; owner++ )
{
var (mesh, world) = meshes[owner];
var handles = mesh.FaceHandles.ToArray();
// The triangles of a face only exist once the mesh was built.
if ( handles.Length > 0 && mesh.CreateFace( handles[0], world, Color.White ) is null )
mesh.Rebuild();
var parent = new Dictionary<int, int>();
int Find( int x )
{
while ( parent[x] != x ) x = parent[x] = parent[parent[x]];
return x;
}
void Join( int a, int b ) => parent[Find( a )] = Find( b );
var first = items.Count;
var directed = new Dictionary<(int, int), HalfEdgeMesh.FaceHandle>();
var edgePoints = new Dictionary<(int, int), (Vector3, Vector3)>();
var used = new HashSet<int>();
foreach ( var handle in handles )
{
var ring = mesh.GetFaceVertices( handle );
var points = ring.Select( x => world.PointToWorld( mesh.GetVertexPosition( x ) ) ).ToArray();
var item = new Item { Handle = handle, Owner = owner, Points = points, Ring = [.. points.Select( WeldId )] };
var normal = Vector3.Zero;
for ( var i = 0; i < points.Length; i++ )
normal += Vector3.Cross( points[i], points[(i + 1) % points.Length] );
item.Area = normal.Length * 0.5f;
item.Normal = item.Area > 0.0f ? normal.Normal : Vector3.Zero;
item.Center = points.Aggregate( Vector3.Zero, ( a, b ) => a + b ) / points.Length;
item.Mins = points.Aggregate( points[0], Low );
item.Maxs = points.Aggregate( points[0], High );
for ( var i = 0; i < ring.Length; i++ )
{
var a = ring[i].Index;
var b = ring[(i + 1) % ring.Length].Index;
used.Add( a );
if ( !parent.ContainsKey( a ) ) parent[a] = a;
if ( !parent.ContainsKey( b ) ) parent[b] = b;
Join( a, b );
directed[(a, b)] = handle;
edgePoints[(a, b)] = (points[i], points[(i + 1) % ring.Length]);
}
AddTriangles( mesh, handle, world, item, report );
items.Add( item );
}
foreach ( var ((a, b), face) in directed )
{
if ( directed.ContainsKey( (b, a) ) ) continue;
var (pa, pb) = edgePoints[(a, b)];
open.Add( (owner, face, WeldId( pa ), WeldId( pb ), pa, pb, Find( a )) );
}
var shells = new Dictionary<int, int>();
for ( var i = first; i < items.Count; i++ )
{
var root = Find( mesh.GetFaceVertices( items[i].Handle )[0].Index );
if ( !shells.TryGetValue( root, out var shell ) )
shells[root] = shell = shellOffset + shells.Count;
items[i].Shell = shell;
}
var openShells = open.Where( x => x.Owner == owner ).Select( x => shells[x.Shell] ).ToHashSet();
foreach ( var group in items.Skip( first ).GroupBy( x => x.Shell ) )
{
report.Shells++;
if ( openShells.Contains( group.Key ) ) continue;
report.ClosedShells++;
// Measured from the body itself: far from the origin float rounding would outweigh a thin body.
var anchor = group.First().Center;
var volume = group.SelectMany( x => x.Tris ).Sum( t => Vector3.Dot( t.A - anchor, Vector3.Cross( t.B - anchor, t.C - anchor ) ) / 6.0f );
if ( volume < 0.0f )
report.Add( new MeshIssue( MeshIssueKind.InvertedShell, MeshIssueLevel.Error, $"A closed body faces inward, volume {volume:0}.", group.First().Center, owner, group.First().Handle ) );
}
shellOffset += shells.Count;
foreach ( var vertex in mesh.VertexHandles )
{
if ( !used.Contains( vertex.Index ) )
report.Add( new MeshIssue( MeshIssueKind.UnusedVertex, MeshIssueLevel.Warning, "A vertex belongs to no face.", world.PointToWorld( mesh.GetVertexPosition( vertex ) ), owner, default ) );
}
foreach ( var face in mesh.BadFaces )
report.Add( new MeshIssue( MeshIssueKind.EngineBadFace, MeshIssueLevel.Error, "The engine rejected this face.", Vector3.Zero, owner, face ) );
}
report.Faces = items.Count;
report.Triangles = items.Sum( x => x.Tris.Count );
CheckOpenEdges( open, report );
CheckFaces( items, report );
CheckOverlaps( items, report );
CheckCrossing( items, report );
CheckBuried( items, report );
return report;
}
static void AddTriangles( PolygonMesh mesh, HalfEdgeMesh.FaceHandle handle, Transform world, Item item, MeshReport report )
{
var vertices = mesh.CreateFace( handle, world, Color.White );
if ( vertices is null )
{
report.Add( new MeshIssue( MeshIssueKind.BadTriangulation, MeshIssueLevel.Error, "The engine made no triangles for this face.", item.Center, item.Owner, handle ) );
return;
}
var area = 0.0f;
var flipped = false;
for ( var i = 0; i + 2 < vertices.Length; i += 3 )
{
var a = vertices[i].Position;
var b = vertices[i + 1].Position;
var c = vertices[i + 2].Position;
var cross = Vector3.Cross( b - a, c - a );
var size = cross.Length * 0.5f;
area += size;
if ( size > MinArea && cross.Normal.Dot( item.Normal ) < 0.0f ) flipped = true;
item.Tris.Add( new Tri
{
A = a, B = b, C = c, Face = -1, Normal = size > 0.0f ? cross.Normal : Vector3.Zero,
Mins = Low( Low( a, b ), c ),
Maxs = High( High( a, b ), c )
} );
}
// On a warped face the triangles cover more than its outline by nature.
var planar = item.Points.Max( p => MathF.Abs( (p - item.Center).Dot( item.Normal ) ) ) <= PlanarLimit;
if ( item.Area > MinArea && (flipped || (planar && MathF.Abs( area - item.Area ) > MathF.Max( item.Area * 0.02f, 0.05f ))) )
{
report.Add( new MeshIssue( MeshIssueKind.BadTriangulation, MeshIssueLevel.Error,
$"Triangles cover {area:0.0} of a face with {item.Area:0.0} units of area.", item.Center, item.Owner, handle ) );
}
}
static void CheckOpenEdges( List<(int Owner, HalfEdgeMesh.FaceHandle Face, int From, int To, Vector3 A, Vector3 B, int Shell)> open, MeshReport report )
{
var lookup = open.ToLookup( x => (x.Owner, x.From, x.To) );
foreach ( var edge in open )
{
var middle = (edge.A + edge.B) * 0.5f;
if ( lookup[(edge.Owner, edge.To, edge.From)].Any() )
report.Add( new MeshIssue( MeshIssueKind.UnweldedSeam, MeshIssueLevel.Error, "Two edges lie on each other but are not connected.", middle, edge.Owner, edge.Face ) );
else if ( lookup[(edge.Owner, edge.From, edge.To)].Count() > 1 )
report.Add( new MeshIssue( MeshIssueKind.FlippedNeighbour, MeshIssueLevel.Error, "Two faces run along an edge in the same direction.", middle, edge.Owner, edge.Face ) );
else
report.Add( new MeshIssue( MeshIssueKind.OpenEdge, MeshIssueLevel.Error, "This edge has a face on one side only.", middle, edge.Owner, edge.Face ) );
}
}
static void CheckFaces( List<Item> items, MeshReport report )
{
foreach ( var item in items )
{
if ( item.Area < MinArea )
{
report.Add( new MeshIssue( MeshIssueKind.DegenerateFace, MeshIssueLevel.Error, "This face has no area.", item.Center, item.Owner, item.Handle ) );
continue;
}
var off = item.Points.Max( p => MathF.Abs( (p - item.Center).Dot( item.Normal ) ) );
if ( off > PlanarLimit )
report.Add( new MeshIssue( MeshIssueKind.NonPlanarFace, MeshIssueLevel.Info, $"Corners leave the plane of this face by {off:0.00}.", item.Center, item.Owner, item.Handle ) );
}
}
static Vector3 Low( Vector3 a, Vector3 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ), MathF.Min( a.z, b.z ) );
static Vector3 High( Vector3 a, Vector3 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ), MathF.Max( a.z, b.z ) );
static float Axis( Vector3 v, int axis ) => axis == 0 ? v.x : axis == 1 ? v.y : v.z;
/// <summary>
/// All pairs whose bounds touch, sorted along X so most pairs are never looked at.
/// </summary>
static IEnumerable<(T A, T B)> Pairs<T>( IReadOnlyList<T> list, Func<T, Vector3> mins, Func<T, Vector3> maxs, float margin )
{
var sorted = list.OrderBy( x => mins( x ).x ).ToArray();
for ( var i = 0; i < sorted.Length; i++ )
{
var a = sorted[i];
var amin = mins( a );
var amax = maxs( a );
for ( var j = i + 1; j < sorted.Length; j++ )
{
var b = sorted[j];
var bmin = mins( b );
if ( bmin.x > amax.x + margin ) break;
var bmax = maxs( b );
if ( bmin.y > amax.y + margin || bmax.y < amin.y - margin ) continue;
if ( bmin.z > amax.z + margin || bmax.z < amin.z - margin ) continue;
yield return (a, b);
}
}
}
static void CheckOverlaps( List<Item> items, MeshReport report )
{
var flat = items.Where( x => x.Area >= MinArea ).ToList();
foreach ( var (a, b) in Pairs( flat, x => x.Mins, x => x.Maxs, PlaneDistance ) )
{
var dot = a.Normal.Dot( b.Normal );
if ( MathF.Abs( dot ) < ParallelDot ) continue;
if ( MathF.Abs( (b.Center - a.Center).Dot( a.Normal ) ) > PlaneDistance ) continue;
// The middle of a face can touch the plane of another by chance, where a surface folds.
// In one place means all of it, so the smaller face has to lie in that plane as a whole.
var (large, small) = a.Area >= b.Area ? (a, b) : (b, a);
if ( small.Points.Any( x => MathF.Abs( (x - large.Center).Dot( large.Normal ) ) > FacePlaneDistance ) ) continue;
var overlap = OverlapArea( a, b );
if ( overlap < MinOverlap ) continue;
// A strip thinner than this covers less than a pixel from any distance worth looking from.
var reach = MathF.Min( (a.Maxs - a.Mins).Length, (b.Maxs - b.Mins).Length );
if ( overlap / MathF.Max( reach, 0.01f ) < SmallestStrip ) continue;
var position = (a.Center + b.Center) * 0.5f;
var same = a.Ring.Length == b.Ring.Length && a.Ring.Distinct().OrderBy( x => x ).SequenceEqual( b.Ring.Distinct().OrderBy( x => x ) );
if ( a.Owner != b.Owner )
{
// Independent objects resting on each other is normal, two surfaces fighting for the same pixels is not.
if ( dot > 0.0f )
report.Add( new MeshIssue( MeshIssueKind.CoplanarOverlap, MeshIssueLevel.Warning, $"Two objects show a surface in the same place, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );
continue;
}
if ( dot > 0.0f )
report.Add( new MeshIssue( same ? MeshIssueKind.DuplicateFace : MeshIssueKind.CoplanarOverlap, MeshIssueLevel.Error, $"Two faces of one mesh lie on each other, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );
else
report.Add( new MeshIssue( MeshIssueKind.BackToBackFaces, MeshIssueLevel.Warning, $"Two faces of one mesh touch back to back inside it, {overlap:0.0} units of area.", position, a.Owner, a.Handle ) );
}
}
static float OverlapArea( Item a, Item b )
{
var absolute = new Vector3( MathF.Abs( a.Normal.x ), MathF.Abs( a.Normal.y ), MathF.Abs( a.Normal.z ) );
var drop = absolute.x > absolute.y ? (absolute.x > absolute.z ? 0 : 2) : (absolute.y > absolute.z ? 1 : 2);
var u = drop == 0 ? 1 : 0;
var v = drop == 2 ? 1 : 2;
Vector2 Flat( Vector3 p ) => new( Axis( p, u ), Axis( p, v ) );
var total = 0.0f;
foreach ( var s in a.Tris )
{
foreach ( var t in b.Tris )
total += ClippedArea( [Flat( s.A ), Flat( s.B ), Flat( s.C )], [Flat( t.A ), Flat( t.B ), Flat( t.C )] );
}
return total;
}
/// <summary>
/// The area two triangles share, by cutting one along the edges of the other.
/// </summary>
static float ClippedArea( Vector2[] subject, Vector2[] clip )
{
var signed = (clip[1].x - clip[0].x) * (clip[2].y - clip[0].y) - (clip[1].y - clip[0].y) * (clip[2].x - clip[0].x);
if ( MathF.Abs( signed ) < 1e-6f ) return 0.0f;
var sign = signed > 0.0f ? 1.0f : -1.0f;
var output = new List<Vector2>( subject );
for ( var e = 0; e < 3 && output.Count > 0; e++ )
{
var p = clip[e];
var q = clip[(e + 1) % 3];
var input = output;
output = [];
float Side( Vector2 x ) => sign * ((q.x - p.x) * (x.y - p.y) - (q.y - p.y) * (x.x - p.x));
for ( var i = 0; i < input.Count; i++ )
{
var a = input[i];
var b = input[(i + 1) % input.Count];
var sa = Side( a );
var sb = Side( b );
if ( sa >= 0.0f ) output.Add( a );
if ( (sa >= 0.0f) != (sb >= 0.0f) ) output.Add( a + (b - a) * (sa / (sa - sb)) );
}
}
var area = 0.0f;
for ( var i = 0; i < output.Count; i++ )
{
var a = output[i];
var b = output[(i + 1) % output.Count];
area += a.x * b.y - b.x * a.y;
}
return MathF.Abs( area ) * 0.5f;
}
static void CheckCrossing( List<Item> items, MeshReport report )
{
var tris = new List<Tri>();
for ( var i = 0; i < items.Count; i++ )
{
if ( items[i].Area < MinArea ) continue;
foreach ( var tri in items[i].Tris )
{
tri.Face = i;
tris.Add( tri );
}
}
var seen = new HashSet<(int, int)>();
foreach ( var (s, t) in Pairs( tris, x => x.Mins, x => x.Maxs, 0.0f ) )
{
if ( s.Face == t.Face ) continue;
var a = items[s.Face];
var b = items[t.Face];
if ( a.Ring.Intersect( b.Ring ).Any() ) continue;
var key = s.Face < t.Face ? (s.Face, t.Face) : (t.Face, s.Face);
if ( seen.Contains( key ) || !Crosses( s, t ) ) continue;
seen.Add( key );
var position = (a.Center + b.Center) * 0.5f;
// Independent objects may cross on purpose, so this is only ever a note.
if ( a.Owner != b.Owner )
{
report.Add( new MeshIssue( MeshIssueKind.ObjectIntersection, MeshIssueLevel.Info, "Two independent objects cut through each other.", position, a.Owner, a.Handle ) );
continue;
}
if ( a.Shell == b.Shell )
report.Add( new MeshIssue( MeshIssueKind.SelfIntersection, MeshIssueLevel.Error, $"A body cuts through itself, between {a.Center:0} and {b.Center:0}.", position, a.Owner, a.Handle ) );
else
report.Add( new MeshIssue( MeshIssueKind.ShellIntersection, MeshIssueLevel.Warning, "Two bodies in one mesh cut through each other.", position, a.Owner, a.Handle ) );
}
}
static bool Crosses( Tri a, Tri b )
{
if ( a.Normal == Vector3.Zero || b.Normal == Vector3.Zero ) return false;
var da = new[] { (b.A - a.A).Dot( a.Normal ), (b.B - a.A).Dot( a.Normal ), (b.C - a.A).Dot( a.Normal ) };
if ( da.Min() >= -Penetration || da.Max() <= Penetration ) return false;
var db = new[] { (a.A - b.A).Dot( b.Normal ), (a.B - b.A).Dot( b.Normal ), (a.C - b.A).Dot( b.Normal ) };
if ( db.Min() >= -Penetration || db.Max() <= Penetration ) return false;
var line = Vector3.Cross( a.Normal, b.Normal );
if ( line.Length < 1e-4f ) return false;
var (a0, a1) = Interval( [a.A, a.B, a.C], db, line );
var (b0, b1) = Interval( [b.A, b.B, b.C], da, line );
return MathF.Min( a1, b1 ) - MathF.Max( a0, b0 ) > Penetration;
}
static (float, float) Interval( Vector3[] points, float[] distances, Vector3 line )
{
var low = float.MaxValue;
var high = float.MinValue;
for ( var i = 0; i < 3; i++ )
{
var j = (i + 1) % 3;
if ( distances[i] > 0.0f == distances[j] > 0.0f ) continue;
var p = points[i] + (points[j] - points[i]) * (distances[i] / (distances[i] - distances[j]));
var t = p.Dot( line );
low = MathF.Min( low, t );
high = MathF.Max( high, t );
}
return (low, high);
}
/// <summary>
/// Faces of one mesh that lie inside another closed body of that mesh, where nobody can ever see them.
/// </summary>
static void CheckBuried( List<Item> items, MeshReport report )
{
var direction = new Vector3( 0.5773f, 0.3827f, 0.7214f ).Normal;
var shells = items.GroupBy( x => x.Shell ).Select( g => (Shell: g.Key, Owner: g.First().Owner, Faces: g.ToArray(),
Mins: g.Select( x => x.Mins ).Aggregate( Low ), Maxs: g.Select( x => x.Maxs ).Aggregate( High )) ).ToArray();
foreach ( var item in items )
{
if ( item.Area < MinArea ) continue;
foreach ( var shell in shells )
{
if ( shell.Shell == item.Shell || shell.Owner != item.Owner ) continue;
var p = item.Center;
if ( p.x <= shell.Mins.x || p.y <= shell.Mins.y || p.z <= shell.Mins.z ) continue;
if ( p.x >= shell.Maxs.x || p.y >= shell.Maxs.y || p.z >= shell.Maxs.z ) continue;
var hits = 0;
foreach ( var face in shell.Faces )
{
foreach ( var tri in face.Tris )
{
if ( RayHits( p, direction, tri ) ) hits++;
}
}
if ( hits % 2 == 0 ) continue;
report.Add( new MeshIssue( MeshIssueKind.BuriedFace, MeshIssueLevel.Warning, "This face lies inside another body of the same mesh.", p, item.Owner, item.Handle ) );
break;
}
}
}
static bool RayHits( Vector3 origin, Vector3 direction, Tri tri )
{
var e1 = tri.B - tri.A;
var e2 = tri.C - tri.A;
var p = Vector3.Cross( direction, e2 );
var det = e1.Dot( p );
if ( MathF.Abs( det ) < 1e-8f ) return false;
var inverse = 1.0f / det;
var s = origin - tri.A;
var u = s.Dot( p ) * inverse;
if ( u < 0.0f || u > 1.0f ) return false;
var q = Vector3.Cross( s, e1 );
var v = direction.Dot( q ) * inverse;
return v >= 0.0f && u + v <= 1.0f && e2.Dot( q ) * inverse > 0.0f;
}
}