MeshSliceRuntime.cs
using System;
using System.Collections.Generic;
namespace Sandbox;
internal static class MeshSliceRuntime
{
private const float Epsilon = 0.0001f;
private const float PositionMergeEpsilon = 0.001f;
private const float DegenerateAreaEpsilon = 0.00001f;
public static MeshSliceResult Slice(
ModelRenderer renderer,
Vector3 worldPosition,
Vector3 worldNormal,
MeshSliceRegion region,
Material crossSectionMaterial )
{
if ( renderer is null || renderer.Model is null )
return null;
if ( worldNormal.Length <= Epsilon )
return null;
var model = renderer.Model;
var sourceVertices = model.GetVertices();
var sourceIndices = model.GetIndices();
if ( sourceVertices is null || sourceVertices.Length < 3 )
return null;
if ( sourceIndices is null || sourceIndices.Length < 3 )
return null;
var localNormal = renderer.WorldTransform.NormalToLocal( worldNormal );
if ( localNormal.Length <= Epsilon )
return null;
localNormal = localNormal.Normal;
var localPosition = renderer.WorldTransform.PointToLocal( worldPosition );
var slicePlane = new Plane( localPosition, localNormal );
var materials = BuildEffectiveMaterials( renderer );
if ( materials.Count == 0 )
{
materials.Add( null );
}
var upperBySlot = CreateTriangleSlots( materials.Count );
var lowerBySlot = CreateTriangleSlots( materials.Count );
var intersectionPoints = new List<Vector3>();
var warnedMaterialRemap = false;
var didSlice = false;
var sectionCount = model.MeshCount;
for ( var section = 0; section < sectionCount; section++ )
{
var indexStart = model.GetIndexStart( section );
var indexCount = model.GetIndexCount( section );
var baseVertex = model.GetBaseVertex( section );
if ( indexCount < 3 )
continue;
var slotIndex = section;
if ( slotIndex >= materials.Count )
{
slotIndex = materials.Count - 1;
if ( !warnedMaterialRemap )
{
Log.Warning(
"SliceMesh: mesh sections exceed available material slots. " +
"Extra sections will be remapped to the last material slot." );
warnedMaterialRemap = true;
}
}
var upperSlot = upperBySlot[slotIndex];
var lowerSlot = lowerBySlot[slotIndex];
for ( var i = 0; i + 2 < indexCount; i += 3 )
{
var i0 = indexStart + i;
var i1 = indexStart + i + 1;
var i2 = indexStart + i + 2;
if ( i2 >= sourceIndices.Length )
break;
var v0 = (int)sourceIndices[i0] + baseVertex;
var v1 = (int)sourceIndices[i1] + baseVertex;
var v2 = (int)sourceIndices[i2] + baseVertex;
if ( !IsVertexIndexValid( sourceVertices, v0 ) ||
!IsVertexIndexValid( sourceVertices, v1 ) ||
!IsVertexIndexValid( sourceVertices, v2 ) )
{
continue;
}
var triangle = new SliceTriangle(
SliceVertex.From( sourceVertices[v0] ),
SliceVertex.From( sourceVertices[v1] ),
SliceVertex.From( sourceVertices[v2] ) );
if ( SplitTriangle( slicePlane, triangle, upperSlot, lowerSlot, intersectionPoints ) )
{
didSlice = true;
}
}
}
if ( !didSlice )
return null;
var capTriangles = BuildCapTriangles( intersectionPoints, localNormal, region );
var crossMaterial = crossSectionMaterial ?? ResolveMaterialForSlot( materials, 0 );
var crossSlotIndex = materials.Count;
materials.Add( crossMaterial );
EnsureSlotCount( upperBySlot, crossSlotIndex + 1 );
EnsureSlotCount( lowerBySlot, crossSlotIndex + 1 );
foreach ( var cap in capTriangles )
{
upperBySlot[crossSlotIndex].Add( ToUpperCapTriangle( cap ) );
lowerBySlot[crossSlotIndex].Add( ToLowerCapTriangle( cap ) );
}
var upperModel = BuildModel( upperBySlot, materials );
var lowerModel = BuildModel( lowerBySlot, materials );
if ( upperModel is null && lowerModel is null )
return null;
return new MeshSliceResult( upperModel, lowerModel );
}
private static bool SplitTriangle(
Plane plane,
SliceTriangle triangle,
List<SliceTriangle> upperHull,
List<SliceTriangle> lowerHull,
List<Vector3> intersectionPoints )
{
var d0 = plane.GetDistance( triangle.A.Position );
var d1 = plane.GetDistance( triangle.B.Position );
var d2 = plane.GetDistance( triangle.C.Position );
var hasUp = d0 > Epsilon || d1 > Epsilon || d2 > Epsilon;
var hasDown = d0 < -Epsilon || d1 < -Epsilon || d2 < -Epsilon;
// Triangle is fully on one side (or fully on-plane) so no slicing happened.
if ( !hasUp || !hasDown )
{
if ( hasDown && !hasUp )
{
lowerHull.Add( triangle );
}
else
{
upperHull.Add( triangle );
}
return false;
}
var vertices = new[] { triangle.A, triangle.B, triangle.C };
var distances = new[] { d0, d1, d2 };
var upperPolygon = ClipTriangleToHalfSpace( vertices, distances, keepUpper: true );
var lowerPolygon = ClipTriangleToHalfSpace( vertices, distances, keepUpper: false );
AddPolygonAsTriangles( upperPolygon, upperHull );
AddPolygonAsTriangles( lowerPolygon, lowerHull );
CollectIntersectionPoints( vertices, distances, intersectionPoints );
return true;
}
private static List<SliceVertex> ClipTriangleToHalfSpace( SliceVertex[] vertices, float[] distances, bool keepUpper )
{
var output = new List<SliceVertex>( 4 );
for ( var i = 0; i < 3; i++ )
{
var next = (i + 1) % 3;
var a = vertices[i];
var b = vertices[next];
var da = distances[i];
var db = distances[next];
var aInside = keepUpper ? da >= -Epsilon : da <= Epsilon;
var bInside = keepUpper ? db >= -Epsilon : db <= Epsilon;
if ( aInside && bInside )
{
AddUniqueVertex( output, b );
}
else if ( aInside && !bInside )
{
AddUniqueVertex( output, InterpolateAtPlane( a, b, da, db ) );
}
else if ( !aInside && bInside )
{
AddUniqueVertex( output, InterpolateAtPlane( a, b, da, db ) );
AddUniqueVertex( output, b );
}
}
if ( output.Count > 1 && NearlyEqual( output[0].Position, output[output.Count - 1].Position, PositionMergeEpsilon ) )
{
output.RemoveAt( output.Count - 1 );
}
return output;
}
private static void AddPolygonAsTriangles( List<SliceVertex> polygon, List<SliceTriangle> output )
{
if ( polygon.Count < 3 )
return;
var anchor = polygon[0];
for ( var i = 1; i + 1 < polygon.Count; i++ )
{
var triangle = new SliceTriangle( anchor, polygon[i], polygon[i + 1] );
if ( IsDegenerate( triangle ) )
continue;
output.Add( triangle );
}
}
private static void CollectIntersectionPoints( SliceVertex[] vertices, float[] distances, List<Vector3> output )
{
for ( var i = 0; i < 3; i++ )
{
var next = (i + 1) % 3;
var da = distances[i];
var db = distances[next];
if ( MathF.Abs( da ) <= Epsilon )
{
AddUniquePoint( output, vertices[i].Position );
}
if ( (da > Epsilon && db < -Epsilon) || (da < -Epsilon && db > Epsilon) )
{
var edgePoint = InterpolateAtPlane( vertices[i], vertices[next], da, db ).Position;
AddUniquePoint( output, edgePoint );
}
}
}
private static List<SliceTriangle> BuildCapTriangles( List<Vector3> points, Vector3 planeNormal, MeshSliceRegion region )
{
var uniquePoints = new List<Vector3>( points.Count );
for ( var i = 0; i < points.Count; i++ )
{
AddUniquePoint( uniquePoints, points[i] );
}
if ( uniquePoints.Count < 3 )
return new List<SliceTriangle>();
var basisU = Vector3.Cross( planeNormal, Vector3.Up );
if ( basisU.Length <= Epsilon )
{
basisU = Vector3.Cross( planeNormal, Vector3.Forward );
}
if ( basisU.Length <= Epsilon )
{
basisU = Vector3.Right;
}
basisU = basisU.Normal;
var basisV = Vector3.Cross( basisU, planeNormal ).Normal;
var mapped = new MappedPoint[uniquePoints.Count];
for ( var i = 0; i < uniquePoints.Count; i++ )
{
var point = uniquePoints[i];
mapped[i] = new MappedPoint
{
Original = point,
Mapped = new Vector2( Vector3.Dot( point, basisU ), Vector3.Dot( point, basisV ) )
};
}
Array.Sort( mapped, static ( a, b ) =>
{
if ( a.Mapped.x < b.Mapped.x ) return -1;
if ( a.Mapped.x > b.Mapped.x ) return 1;
if ( a.Mapped.y < b.Mapped.y ) return -1;
if ( a.Mapped.y > b.Mapped.y ) return 1;
return 0;
} );
var hull = BuildConvexHull( mapped );
if ( hull.Count < 3 )
return new List<SliceTriangle>();
var minX = float.MaxValue;
var minY = float.MaxValue;
var maxX = float.MinValue;
var maxY = float.MinValue;
for ( var i = 0; i < hull.Count; i++ )
{
var mappedValue = hull[i].Mapped;
minX = MathF.Min( minX, mappedValue.x );
minY = MathF.Min( minY, mappedValue.y );
maxX = MathF.Max( maxX, mappedValue.x );
maxY = MathF.Max( maxY, mappedValue.y );
}
var width = maxX - minX;
var height = maxY - minY;
if ( width <= Epsilon ) width = 1.0f;
if ( height <= Epsilon ) height = 1.0f;
var tangent = new Vector4( basisU.x, basisU.y, basisU.z, 1.0f );
var result = new List<SliceTriangle>( hull.Count - 2 );
var capNormal = planeNormal.Normal;
for ( var i = 1; i + 1 < hull.Count; i++ )
{
var a = CreateCapVertex( hull[0], capNormal, tangent, region, minX, minY, width, height );
var b = CreateCapVertex( hull[i], capNormal, tangent, region, minX, minY, width, height );
var c = CreateCapVertex( hull[i + 1], capNormal, tangent, region, minX, minY, width, height );
var tri = new SliceTriangle( a, b, c );
if ( IsDegenerate( tri ) )
continue;
result.Add( tri );
}
return result;
}
private static SliceVertex CreateCapVertex(
MappedPoint point,
Vector3 normal,
Vector4 tangent,
MeshSliceRegion region,
float minX,
float minY,
float width,
float height )
{
var u = (point.Mapped.x - minX) / width;
var v = (point.Mapped.y - minY) / height;
var mappedUv = region.Map( u, v );
return new SliceVertex
{
Position = point.Original,
Normal = normal,
Tangent = tangent,
TexCoord0 = new Vector4( mappedUv.x, mappedUv.y, 0.0f, 1.0f ),
Color = Color32.White
};
}
private static List<MappedPoint> BuildConvexHull( MappedPoint[] mapped )
{
var hull = new List<MappedPoint>( mapped.Length + 1 );
for ( var i = 0; i < mapped.Length; i++ )
{
while ( hull.Count >= 2 )
{
var cross = SignedArea2D( hull[hull.Count - 2].Mapped, hull[hull.Count - 1].Mapped, mapped[i].Mapped );
if ( cross > 0.0f )
break;
hull.RemoveAt( hull.Count - 1 );
}
hull.Add( mapped[i] );
}
for ( int i = mapped.Length - 2, start = hull.Count + 1; i >= 0; i-- )
{
while ( hull.Count >= start )
{
var cross = SignedArea2D( hull[hull.Count - 2].Mapped, hull[hull.Count - 1].Mapped, mapped[i].Mapped );
if ( cross > 0.0f )
break;
hull.RemoveAt( hull.Count - 1 );
}
hull.Add( mapped[i] );
}
if ( hull.Count > 1 )
{
hull.RemoveAt( hull.Count - 1 );
}
return hull;
}
private static Model BuildModel( List<List<SliceTriangle>> trianglesBySlot, List<Material> materials )
{
var builder = Model.Builder;
var hasMesh = false;
var collisionVertices = new List<Vector3>();
for ( var i = 0; i < trianglesBySlot.Count; i++ )
{
var triangles = trianglesBySlot[i];
if ( triangles.Count == 0 )
continue;
var material = ResolveMaterialForSlot( materials, i );
var mesh = BuildMesh( triangles, material );
if ( mesh is null )
continue;
builder.AddMesh( mesh );
CollectCollisionVertices( triangles, collisionVertices );
hasMesh = true;
}
if ( !hasMesh )
return null;
// Build a convex collision hull from sliced points so spawned hull colliders
// match the sliced result instead of inheriting source collider geometry.
if ( collisionVertices.Count >= 4 )
{
builder.AddCollisionHull( collisionVertices );
}
return builder.Create();
}
private static void CollectCollisionVertices( List<SliceTriangle> triangles, List<Vector3> output )
{
for ( var i = 0; i < triangles.Count; i++ )
{
AddUniquePoint( output, triangles[i].A.Position );
AddUniquePoint( output, triangles[i].B.Position );
AddUniquePoint( output, triangles[i].C.Position );
}
}
private static Mesh BuildMesh( List<SliceTriangle> triangles, Material material )
{
var vertexCount = triangles.Count * 3;
if ( vertexCount < 3 )
return null;
var vertices = new Vertex[vertexCount];
var indices = new int[vertexCount];
var mins = Vector3.Zero;
var maxs = Vector3.Zero;
var hasBounds = false;
var index = 0;
for ( var i = 0; i < triangles.Count; i++ )
{
var tri = triangles[i];
var a = ToVertex( tri.A );
var b = ToVertex( tri.B );
var c = ToVertex( tri.C );
vertices[index] = a;
indices[index] = index;
ExpandBounds( a.Position, ref mins, ref maxs, ref hasBounds );
index++;
vertices[index] = b;
indices[index] = index;
ExpandBounds( b.Position, ref mins, ref maxs, ref hasBounds );
index++;
vertices[index] = c;
indices[index] = index;
ExpandBounds( c.Position, ref mins, ref maxs, ref hasBounds );
index++;
}
var mesh = new Mesh( material );
mesh.CreateVertexBuffer( vertices.Length, vertices );
mesh.CreateIndexBuffer( indices.Length, indices );
if ( hasBounds )
{
mesh.Bounds = new BBox( mins, maxs );
}
return mesh;
}
private static Vertex ToVertex( SliceVertex source )
{
var normal = source.Normal.Length > Epsilon ? source.Normal.Normal : Vector3.Up;
var tangent = GetSafeTangent( source.Tangent, normal );
return new Vertex
{
Position = source.Position,
Color = source.Color,
Normal = normal,
TexCoord0 = source.TexCoord0,
Tangent = tangent
};
}
private static Vector4 GetSafeTangent( Vector4 tangent, Vector3 normal )
{
var tangentDirection = new Vector3( tangent.x, tangent.y, tangent.z );
if ( tangentDirection.Length <= Epsilon )
{
tangentDirection = Vector3.Cross( normal, Vector3.Up );
if ( tangentDirection.Length <= Epsilon )
{
tangentDirection = Vector3.Cross( normal, Vector3.Right );
}
}
tangentDirection = tangentDirection.Normal;
var w = MathF.Abs( tangent.w ) <= Epsilon ? 1.0f : MathF.Sign( tangent.w );
return new Vector4( tangentDirection.x, tangentDirection.y, tangentDirection.z, w );
}
private static SliceTriangle ToUpperCapTriangle( SliceTriangle triangle )
{
return BuildCapTriangleForSide( triangle, invertNormal: true );
}
private static SliceTriangle ToLowerCapTriangle( SliceTriangle triangle )
{
return BuildCapTriangleForSide( triangle, invertNormal: false );
}
private static SliceTriangle BuildCapTriangleForSide( SliceTriangle triangle, bool invertNormal )
{
var desiredNormal = invertNormal ? -triangle.A.Normal : triangle.A.Normal;
if ( desiredNormal.Length <= Epsilon )
{
desiredNormal = invertNormal ? Vector3.Down : Vector3.Up;
}
else
{
desiredNormal = desiredNormal.Normal;
}
var a = triangle.A.WithNormal( desiredNormal );
var b = triangle.B.WithNormal( desiredNormal );
var c = triangle.C.WithNormal( desiredNormal );
// Ensure front-face winding matches the desired cap normal.
var cross = Vector3.Cross( b.Position - a.Position, c.Position - a.Position );
if ( Vector3.Dot( cross, desiredNormal ) < 0.0f )
{
(b, c) = (c, b);
}
return new SliceTriangle( a, b, c );
}
private static List<Material> BuildEffectiveMaterials( ModelRenderer renderer )
{
var materials = new List<Material>();
var modelMaterials = renderer.Model.Materials;
if ( modelMaterials.Length == 0 )
{
return materials;
}
for ( var i = 0; i < modelMaterials.Length; i++ )
{
var overrideMaterial = renderer.Materials.GetOverride( i );
materials.Add( overrideMaterial ?? modelMaterials[i] );
}
return materials;
}
private static Material ResolveMaterialForSlot( IReadOnlyList<Material> materials, int slotIndex )
{
if ( materials.Count == 0 )
return null;
if ( slotIndex < materials.Count )
return materials[slotIndex];
return materials[materials.Count - 1];
}
private static List<List<SliceTriangle>> CreateTriangleSlots( int count )
{
var output = new List<List<SliceTriangle>>( count );
for ( var i = 0; i < count; i++ )
{
output.Add( new List<SliceTriangle>() );
}
return output;
}
private static void EnsureSlotCount( List<List<SliceTriangle>> slots, int count )
{
while ( slots.Count < count )
{
slots.Add( new List<SliceTriangle>() );
}
}
private static SliceVertex InterpolateAtPlane( SliceVertex a, SliceVertex b, float da, float db )
{
var denominator = da - db;
var t = MathF.Abs( denominator ) <= Epsilon ? 0.5f : da / denominator;
t = Math.Clamp( t, 0.0f, 1.0f );
return SliceVertex.Lerp( a, b, t );
}
private static bool IsVertexIndexValid( Vertex[] vertices, int index )
{
return index >= 0 && index < vertices.Length;
}
private static bool IsDegenerate( SliceTriangle triangle )
{
var ab = triangle.B.Position - triangle.A.Position;
var ac = triangle.C.Position - triangle.A.Position;
var area = Vector3.Cross( ab, ac ).Length;
return area <= DegenerateAreaEpsilon;
}
private static void AddUniquePoint( List<Vector3> points, Vector3 point )
{
for ( var i = 0; i < points.Count; i++ )
{
if ( NearlyEqual( points[i], point, PositionMergeEpsilon ) )
return;
}
points.Add( point );
}
private static void AddUniqueVertex( List<SliceVertex> vertices, SliceVertex vertex )
{
if ( vertices.Count == 0 )
{
vertices.Add( vertex );
return;
}
var previous = vertices[vertices.Count - 1];
if ( NearlyEqual( previous.Position, vertex.Position, PositionMergeEpsilon ) )
return;
vertices.Add( vertex );
}
private static bool NearlyEqual( Vector3 a, Vector3 b, float epsilon )
{
return (a - b).Length <= epsilon;
}
private static float SignedArea2D( Vector2 a, Vector2 b, Vector2 c )
{
return (a.x - b.x) * (b.y - c.y) - (b.x - c.x) * (a.y - b.y);
}
private static void ExpandBounds( Vector3 point, ref Vector3 mins, ref Vector3 maxs, ref bool hasBounds )
{
if ( !hasBounds )
{
mins = point;
maxs = point;
hasBounds = true;
return;
}
mins = Vector3.Min( mins, point );
maxs = Vector3.Max( maxs, point );
}
private struct SliceTriangle
{
public SliceVertex A;
public SliceVertex B;
public SliceVertex C;
public SliceTriangle( SliceVertex a, SliceVertex b, SliceVertex c )
{
A = a;
B = b;
C = c;
}
}
private struct SliceVertex
{
public Vector3 Position;
public Vector3 Normal;
public Vector4 Tangent;
public Vector4 TexCoord0;
public Color32 Color;
public static SliceVertex From( Vertex source )
{
return new SliceVertex
{
Position = source.Position,
Normal = source.Normal,
Tangent = source.Tangent,
TexCoord0 = source.TexCoord0,
Color = source.Color
};
}
public SliceVertex WithNormal( Vector3 normal )
{
var copy = this;
copy.Normal = normal;
return copy;
}
public static SliceVertex Lerp( SliceVertex a, SliceVertex b, float t )
{
return new SliceVertex
{
Position = Vector3.Lerp( a.Position, b.Position, t ),
Normal = Vector3.Lerp( a.Normal, b.Normal, t ),
Tangent = Vector4.Lerp( a.Tangent, b.Tangent, t ),
TexCoord0 = Vector4.Lerp( a.TexCoord0, b.TexCoord0, t ),
Color = LerpColor( a.Color, b.Color, t )
};
}
private static Color32 LerpColor( Color32 a, Color32 b, float t )
{
return new Color32(
LerpByte( a.r, b.r, t ),
LerpByte( a.g, b.g, t ),
LerpByte( a.b, b.b, t ),
LerpByte( a.a, b.a, t ) );
}
private static byte LerpByte( byte a, byte b, float t )
{
var value = a + (b - a) * t;
return (byte)Math.Clamp( (int)MathF.Round( value ), 0, 255 );
}
}
private struct MappedPoint
{
public Vector3 Original;
public Vector2 Mapped;
}
}