Code/SplineCollider.cs
using System;
using System.Numerics;
namespace Sandbox;
public sealed class SplineCollider : ModelCollider, Component.ExecuteInEditor
{
[Property, Category( "Spline" )] public SplineComponent Spline
{
get;
set
{
if(field != null ) field.Spline.SplineChanged -= MarkDirty;
field = value;
if(Enabled) {
field.Spline.SplineChanged += MarkDirty;
Rebuild();
}
}
}
[Property, Category("Spline")]
[Range( 0, 16 )]
public int Subdivision
{
get;
set
{
field = value;
Rebuild();
}
} = 0;
[Property, Category( "Spline" )]
public Rotation ModelRotation
{
get;
set
{
field = value;
IsDirty = true;
}
} = Rotation.Identity;
[Property, Category( "Spline" )]
public Vector3 ModelScale
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.One;
[Property, Category( "Spline" )]
public Vector3 ModelOffset
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.Zero;
private Vector3 ModelForward => ModelRotation.Forward;
[Property, Category( "Spline" )]
public bool UseRotationMinimizingFrames
{
get;
set
{
field = value;
IsDirty = true;
}
} = false;
private bool IsDirty
{
get; set;
} = true;
[Property, Category( "Spline" ), MinMax( 0, float.PositiveInfinity )]
public float Spacing
{
get;
set
{
field = value;
IsDirty = true;
}
} = 0f;
[Property, Category( "Spline" )]
public bool FlexFit
{
get;
set
{
field = value;
IsDirty = true;
}
} = false;
// Optional cap colliders that claim a fixed slice of the spline at each end, mirroring
// the cap feature on SplineModelRenderer. Each cap is its own physics Model with its own
// rotation/scale/offset so it can be flipped, resized, or nudged independently.
[Property, FeatureEnabled( "Start Cap" )]
public bool StartCapEnabled
{
get;
set
{
field = value;
Rebuild();
}
}
[Property, FeatureEnabled( "End Cap" )]
public bool EndCapEnabled
{
get;
set
{
field = value;
Rebuild();
}
}
[Property, Category( "Spline" ), Feature( "Start Cap" )]
public Model StartCap
{
get;
set
{
field = value;
Rebuild();
}
}
[Property, Category( "Spline" ), Feature( "Start Cap" )]
public Rotation StartCapRotation
{
get;
set
{
field = value;
IsDirty = true;
}
} = Rotation.Identity;
[Property, Category( "Spline" ), Feature( "Start Cap" )]
public Vector3 StartCapScale
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.One;
[Property, Category( "Spline" ), Feature( "Start Cap" )]
public Vector3 StartCapOffset
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.Zero;
[Property, Category( "Spline" ), Feature( "End Cap" )]
public Model EndCap
{
get;
set
{
field = value;
Rebuild();
}
}
[Property, Category( "Spline" ), Feature( "End Cap" )]
public Rotation EndCapRotation
{
get;
set
{
field = value;
IsDirty = true;
}
} = Rotation.Identity;
[Property, Category( "Spline" ), Feature( "End Cap" )]
public Vector3 EndCapScale
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.One;
[Property, Category( "Spline" ), Feature( "End Cap" )]
public Vector3 EndCapOffset
{
get;
set
{
field = value;
IsDirty = true;
}
} = Vector3.Zero;
protected override void OnEnabled()
{
if ( Model.IsValid() && Spline.IsValid() )
{
IsDirty = true;
Spline.Spline.SplineChanged += MarkDirty;
}
base.OnEnabled();
}
private void MarkDirty()
{
IsDirty = true;
}
protected override void OnDisabled()
{
Spline.Spline.SplineChanged -= MarkDirty;
subHulls.Clear();
subMeshes.Clear();
startCapSubHulls.Clear();
startCapSubMeshes.Clear();
endCapSubHulls.Clear();
endCapSubMeshes.Clear();
base.OnDisabled();
}
protected override void OnUpdate()
{
if ( !Model.IsValid() || !Spline.IsValid() )
{
return;
}
if ( !IsDirty )
{
return;
}
// Nothing to deform
if ( subHulls.Count == 0 && subMeshes.Count == 0
&& startCapSubHulls.Count == 0 && startCapSubMeshes.Count == 0
&& endCapSubHulls.Count == 0 && endCapSubMeshes.Count == 0 )
{
return;
}
UpdateCollisions();
}
// This is internal hack it for now
private PhysicsBody _PhysicsBody => Rigidbody.IsValid() ? Rigidbody.PhysicsBody : KeyframeBody;
private BBox? _physicPartBounds = null;
private void UpdateCollisions()
{
// Ensure we have valid physics bounds
if ( _physicPartBounds is null )
return;
var (mainMin, mainSize) = GetForwardSpan( Model.Bounds, ModelRotation, ModelScale );
var splineLength = Spline.Spline.Length;
// Caps eat a fixed slice off each end; the repeating mesh fills the interior between them.
bool hasStartCap = StartCapEnabled && (startCapSubHulls.Count > 0 || startCapSubMeshes.Count > 0);
bool hasEndCap = EndCapEnabled && (endCapSubHulls.Count > 0 || endCapSubMeshes.Count > 0);
float startCapMin = 0f, startCapSize = 0f;
if ( hasStartCap )
(startCapMin, startCapSize) = GetForwardSpan( StartCap.Bounds, StartCapRotation, StartCapScale );
float endCapMin = 0f, endCapSize = 0f;
if ( hasEndCap )
(endCapMin, endCapSize) = GetForwardSpan( EndCap.Bounds, EndCapRotation, EndCapScale );
// Interior region for the repeating mesh — caps eat into each end.
float mainStartDistance = MathF.Min( startCapSize, splineLength );
float mainEndDistance = MathF.Max( mainStartDistance, splineLength - endCapSize );
float mainLength = mainEndDistance - mainStartDistance;
var sizeInModelDirWithSpacing = mainSize + Spacing;
var frameSegments = (int)Math.Ceiling( splineLength / MathF.Max( 1f, mainSize ) );
if ( frameSegments < 1 ) frameSegments = 1;
int meshesRequiredWithSpacing = 0;
float distancePerMeshWitSpacing = sizeInModelDirWithSpacing;
if ( mainLength > 0f && sizeInModelDirWithSpacing > 0f )
{
meshesRequiredWithSpacing = (int)Math.Floor( mainLength / sizeInModelDirWithSpacing );
if ( FlexFit )
{
meshesRequiredWithSpacing = (int)Math.Ceiling( mainLength / sizeInModelDirWithSpacing );
}
if ( FlexFit && meshesRequiredWithSpacing > 0 )
{
distancePerMeshWitSpacing = mainLength / meshesRequiredWithSpacing;
}
}
// Calculate frames along the spline
int framesPerMesh = 12; // Adjust as needed
int frameCount = Math.Max( 2, frameSegments * framesPerMesh + 1 );
var frames = UseRotationMinimizingFrames
? SplineModelRenderer.CalculateRotationMinimizingTangentFrames( Spline.Spline, frameCount )
: SplineModelRenderer.CalculateTangentFramesUsingUpDir( Spline.Spline, frameCount );
// Clear existing shapes
_PhysicsBody.ClearShapes();
// Main repeating shapes in the interior region.
for ( var meshIndex = 0; meshIndex < meshesRequiredWithSpacing; meshIndex++ )
{
float startDistance = mainStartDistance + meshIndex * distancePerMeshWitSpacing;
float endDistance = startDistance + distancePerMeshWitSpacing - Spacing;
DeformAndAddShapes( ref subMeshes, ref subHulls, ModelRotation, ModelOffset, ModelScale, mainMin, mainSize, frames, startDistance, endDistance );
}
// Start cap occupies [0, mainStartDistance].
if ( hasStartCap && mainStartDistance > 0f )
{
DeformAndAddShapes( ref startCapSubMeshes, ref startCapSubHulls, StartCapRotation, StartCapOffset, StartCapScale, startCapMin, startCapSize, frames, 0f, mainStartDistance );
}
// End cap occupies [endCapStartDistance, splineLength].
float endCapRegion = splineLength - mainEndDistance;
float endCapStartDistance = mainStartDistance + distancePerMeshWitSpacing * meshesRequiredWithSpacing;
if ( hasEndCap && endCapRegion > 0f )
{
DeformAndAddShapes( ref endCapSubMeshes, ref endCapSubHulls, EndCapRotation, EndCapOffset, EndCapScale, endCapMin, endCapSize, frames, endCapStartDistance, endCapStartDistance + endCapRegion );
}
IsDirty = false;
}
private (float min, float size) GetForwardSpan( BBox bounds, Rotation rotation, Vector3 scale )
{
bounds.Mins = bounds.Mins * scale;
bounds.Maxs = bounds.Maxs * scale;
bounds = bounds.Rotate( rotation );
float size = bounds.Size.Dot( Vector3.Forward );
float min = bounds.Center.Dot( Vector3.Forward ) - size / 2f;
return (min, size);
}
// Deforms a set of sub-shapes across [startDistance, endDistance] of the spline and adds the
// resulting physics shapes to the body. Shared by the repeating main mesh and both caps.
private void DeformAndAddShapes( ref List<SubMesh> meshes, ref List<SubHull> hulls, Rotation rotation, Vector3 offset, Vector3 scale, float min, float size, Transform[] frames, float startDistance, float endDistance )
{
foreach ( var subMesh in meshes )
{
var deformedVertices = new List<Vector3>( subMesh.Vertices.Count );
foreach ( var vertex in subMesh.Vertices )
{
SplineModelRenderer.Deform( Spline, rotation, offset, scale, vertex, Vector3.Up, new Vector4( Vector3.Up, 0f ), frames, startDistance, endDistance, min, size, out var deformedVertex, out _, out _ );
deformedVertices.Add( deformedVertex );
}
var shape = _PhysicsBody.AddMeshShape( deformedVertices, subMesh.Indices );
ApplyShapeProperties( shape, subMesh.Surface, startDistance, endDistance );
}
foreach ( var subHull in hulls )
{
var deformedVertices = new List<Vector3>( subHull.Vertices.Count );
foreach ( var vertex in subHull.Vertices )
{
SplineModelRenderer.Deform( Spline, rotation, offset, scale, vertex, Vector3.Up, new Vector4( Vector3.Up, 0f ), frames, startDistance, endDistance, min, size, out var deformedVertex, out _, out _ );
deformedVertices.Add( deformedVertex );
}
var shape = _PhysicsBody.AddHullShape( Vector3.Zero, Rotation.Identity, deformedVertices );
ApplyShapeProperties( shape, subHull.Surface, startDistance, endDistance );
}
}
private void ApplyShapeProperties( PhysicsShape shape, Surface surface, float startDistance, float endDistance )
{
// SurfaceVelocity is body-local, so align it with the spline tangent at the middle of this
// shape's span and undo the body's world rotation.
var midDistance = (startDistance + endDistance) * 0.5f;
var tangent = Spline.Spline.SampleAtDistance( midDistance ).Tangent.Normal;
Rotation rot = Rotation.FromToRotation(SurfaceVelocity.Normal, tangent);
shape.SurfaceVelocity = rot * SurfaceVelocity;
shape.Surface = surface;
if ( Friction != null ) shape.Friction = (float)Friction;
if ( Elasticity != null ) shape.Surface.Elasticity = (float)Elasticity;
if ( RollingResistance != null ) shape.Surface.RollingResistance = (float)RollingResistance;
}
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody, Transform local )
{
if ( Model is null || Model.Physics is null )
yield break;
if ( Model.Physics.Parts.Count == 0 )
yield break;
if ( Spline is null )
yield break;
subMeshes.Clear();
subHulls.Clear();
startCapSubMeshes.Clear();
startCapSubHulls.Clear();
endCapSubMeshes.Clear();
endCapSubHulls.Clear();
_physicPartBounds = null;
BuildSubShapes( Model, local, ModelRotation, subHulls, subMeshes, ref _physicPartBounds );
if ( StartCapEnabled && StartCap.IsValid() && StartCap != Model.Error )
{
BBox? capBounds = null;
BuildSubShapes( StartCap, local, StartCapRotation, startCapSubHulls, startCapSubMeshes, ref capBounds );
}
if ( EndCapEnabled && EndCap.IsValid() && EndCap != Model.Error )
{
BBox? capBounds = null;
BuildSubShapes( EndCap, local, EndCapRotation, endCapSubHulls, endCapSubMeshes, ref capBounds );
}
// Body-level properties only come from the main model's parts.
foreach ( var part in Model.Physics.Parts )
{
if ( part.Mass > 0 )
targetBody.Mass = part.Mass;
if ( part.OverrideMassCenter )
targetBody.LocalMassCenter = part.MassCenterOverride;
if ( part.LinearDamping > 0 )
targetBody.LinearDamping = part.LinearDamping;
if ( part.AngularDamping > 0 )
targetBody.AngularDamping = part.AngularDamping;
}
UpdateCollisions();
}
// Subdivides every physics part of a model into deformable sub-hulls/sub-meshes appended to the
// given lists. Used for the main model and each cap so they share the same tessellation path.
private void BuildSubShapes( Model model, Transform local, Rotation modelRotation, List<SubHull> targetHulls, List<SubMesh> targetMeshes, ref BBox? bounds )
{
if ( model is null || model.Physics is null )
return;
foreach ( var part in model.Physics.Parts )
{
// Bone transform
var bx = local.ToWorld( part.Transform );
foreach ( var sphere in part.Spheres )
{
const int rings = 8;
SubdivideSphere( rings, sphere.Sphere.Center, sphere.Sphere.Radius, sphere.Surface, bx, modelRotation.Forward, targetHulls );
var sphereBounds = new BBox( sphere.Sphere.Center - new Vector3( sphere.Sphere.Radius ), sphere.Sphere.Center + new Vector3( sphere.Sphere.Radius ) );
sphereBounds = sphereBounds.Transform( bx );
bounds = bounds?.AddBBox( sphereBounds ) ?? sphereBounds;
}
foreach ( var capsule in part.Capsules )
{
var rotatedCenterA = bx.PointToWorld( capsule.Capsule.CenterA );
var rotatedCenterB = bx.PointToWorld( capsule.Capsule.CenterB );
SubdivideCapsule( 4 + Subdivision, rotatedCenterA, rotatedCenterB, capsule.Capsule.Radius, capsule.Surface, targetHulls );
var capsuleBounds = BBox.FromPoints(
[
rotatedCenterA - new Vector3( capsule.Capsule.Radius ),
rotatedCenterA + new Vector3( capsule.Capsule.Radius ),
rotatedCenterB - new Vector3( capsule.Capsule.Radius ),
rotatedCenterB + new Vector3( capsule.Capsule.Radius )
] );
bounds = bounds?.AddBBox( capsuleBounds ) ?? capsuleBounds;
}
foreach ( var hull in part.Hulls )
{
SubdivideHull( hull, Subdivision, hull.Surface, bx, targetHulls );
var hullBounds = hull.Bounds.Transform( bx ).Rotate( modelRotation );
bounds = bounds?.AddBBox( hullBounds ) ?? hullBounds;
}
foreach ( var mesh in part.Meshes )
{
SubdivideMesh( mesh, bx, targetMeshes );
var meshBounds = mesh.Bounds.Transform( bx );
bounds = bounds?.AddBBox( meshBounds ) ?? meshBounds;
}
}
}
private void SubdivideMesh( PhysicsGroupDescription.BodyPart.MeshPart mesh, Transform transform, List<SubMesh> target )
{
var triangles = mesh.GetTriangles().ToList();
// TODO slow as fuck can be improved by getting the lists directly rather than the traingle objects
// can be done once we are out of scene staging.
var vertices = new List<Vector3>();
var indices = new List<int>( triangles.Count * 3 );
var vertexMap = new Dictionary<Vector3, int>( new Vector3Comparer() );
foreach ( var triangle in triangles )
{
if ( !vertexMap.ContainsKey( triangle.A ) )
{
vertexMap[triangle.A] = vertices.Count;
vertices.Add( triangle.A );
}
indices.Add( vertexMap[triangle.A] );
if ( !vertexMap.ContainsKey( triangle.B ) )
{
vertexMap[triangle.B] = vertices.Count;
vertices.Add( triangle.B );
}
indices.Add( vertexMap[triangle.B] );
if ( !vertexMap.ContainsKey( triangle.C ) )
{
vertexMap[triangle.C] = vertices.Count;
vertices.Add( triangle.C );
}
indices.Add( vertexMap[triangle.C] );
}
target.Add( new SubMesh
{
Vertices = vertices,
Indices = indices,
Surface = mesh.Surface,
PartTransform = transform
} );
}
private void SubdivideHull( PhysicsGroupDescription.BodyPart.HullPart hull, int ringCount, Surface surface, Transform transform, List<SubHull> target )
{
// Transform all the hull vertices once
// TODO can optimzie this all by getting vertices and lines directly from hull an transforming than once
// instead of this LINQ madness, will do once out of scenestaging.
List<Vector3> transformedVertices = hull.GetPoints().Select( vertex => transform.PointToWorld( vertex ) ).ToList();
if ( ringCount == 0 )
{
target.Add( new SubHull
{
Vertices = transformedVertices,
Surface = surface,
} );
return;
}
// Get the transformed edges
var transformedLines = hull.GetLines().Select( line => new Line(
transform.PointToWorld( line.Start ) ,
transform.PointToWorld( line.End ) )).ToList();
// Project all vertices along Vector3.Forward
float minProj = transformedVertices.Min( vertex => Vector3.Dot( vertex, Vector3.Forward ) );
float maxProj = transformedVertices.Max( vertex => Vector3.Dot( vertex, Vector3.Forward ) );
float sizeInModelDir = maxProj - minProj;
float interval = sizeInModelDir / ringCount;
const float tolerance = 0.01f;
List<List<Vector3>> rings = new List<List<Vector3>>();
// Create rings and intersect with existing edges
for ( int i = 0; i <= ringCount; i++ )
{
float ringProj = minProj + (i * interval);
var ringVertices = new HashSet<Vector3>( new Vector3Comparer() );
// Find intersections with existing edges
foreach ( var line in transformedLines )
{
float startProj = Vector3.Dot( line.Start, Vector3.Forward );
float endProj = Vector3.Dot( line.End, Vector3.Forward );
// Check if line crosses this ring plane
if ( (startProj <= ringProj + tolerance && endProj >= ringProj - tolerance) ||
(endProj <= ringProj + tolerance && startProj >= ringProj - tolerance) )
{
// Calculate intersection point
float t = (ringProj - startProj) / (endProj - startProj);
Vector3 intersection = Vector3.Lerp( line.Start, line.End, t );
ringVertices.Add( intersection );
}
}
if ( ringVertices.Count > 0 )
{
rings.Add( ringVertices.ToList() );
}
}
// Group rings into SubHulls
for ( int i = 0; i < rings.Count - 1; i++ )
{
var currentGroup = new List<Vector3>();
// Add current ring
currentGroup.AddRange( rings[i] );
// Add next ring
currentGroup.AddRange( rings[i + 1] );
// Find and add original vertices that fall between these rings
float groupStart = rings[i].Min( v => Vector3.Dot( v, Vector3.Forward ) );
float groupEnd = rings[i + 1].Max( v => Vector3.Dot( v, Vector3.Forward ) );
foreach ( var vertex in transformedVertices )
{
float vertexProj = Vector3.Dot( vertex, Vector3.Forward );
if ( vertexProj >= groupStart - tolerance && vertexProj <= groupEnd + tolerance )
{
currentGroup.Add( vertex );
}
}
target.Add( new SubHull
{
Vertices = currentGroup,
Surface = surface,
} );
}
}
private void SubdivideSphere( int rings, Vector3 center, float radius, Surface surface, Transform transform, Vector3 modelForward, List<SubHull> target )
{
var ringPoints = new List<Vector3>[rings];
Vector3 direction = modelForward;
// Find two vectors orthogonal to ModelForward to form a coordinate system
// TODO there are better ways todo this
Vector3 right = Vector3.Cross( direction, Vector3.Up );
if ( right.Length < 0.001f )
{
right = Vector3.Cross( direction, Vector3.Right );
}
right = right.Normal;
Vector3 up = Vector3.Cross( right, direction ).Normal;
for ( int i = 0; i < rings; ++i )
{
ringPoints[i] = new List<Vector3>();
float v = i / (float)(rings - 1);
float theta = v * MathF.PI; // Angle from 0 to PI
float sinTheta = MathF.Sin( theta );
float cosTheta = MathF.Cos( theta );
for ( int j = 0; j < rings; ++j )
{
float u = j / (float)(rings - 1);
float phi = u * 2.0f * MathF.PI;
float sinPhi = MathF.Sin( phi );
float cosPhi = MathF.Cos( phi );
// Convert spherical coordinates to Cartesian coordinates aligned along ModelForward
Vector3 point = center
+ (right * (sinTheta * cosPhi * radius))
+ (up * (sinTheta * sinPhi * radius))
+ (direction * (cosTheta * radius));
ringPoints[i].Add( transform.PointToWorld( point ) );
}
}
for ( int i = 0; i < rings - 1; ++i )
{
var currentGroup = new List<Vector3>();
currentGroup.AddRange( ringPoints[i] );
currentGroup.AddRange( ringPoints[i + 1] );
target.Add( new SubHull
{
Vertices = currentGroup,
Surface = surface,
} );
}
}
private void SubdivideCapsule( int totalRings, Vector3 centerA, Vector3 centerB, float radius, Surface surface, List<SubHull> target )
{
const int segments = 8;
if ( totalRings < 4 )
{
throw new ArgumentException( "Number of rings must be at least 4 to form a valid capsule." );
}
// Generate all rings along the capsule
List<List<Vector3>> ringPoints = new();
GenerateCapsuleRings( centerA, centerB, radius, totalRings, segments, ringPoints );
// Group the rings into SubHulls
for ( int i = 0; i < ringPoints.Count - 1; i++ )
{
var currentGroup = new List<Vector3>();
currentGroup.AddRange( ringPoints[i] );
currentGroup.AddRange( ringPoints[i + 1] );
target.Add( new SubHull
{
Vertices = currentGroup,
Surface = surface,
} );
}
}
private void GenerateCapsuleRings( Vector3 centerA, Vector3 centerB, float radius, int totalRings, int segments, List<List<Vector3>> ringPoints )
{
Vector3 direction = (centerB - centerA).Normal;
float cylinderHeight = (centerB - centerA).Length;
float totalHeight = cylinderHeight + 2 * radius;
// Build orthonormal basis
// TODO there are better ways todo this
Vector3 up = direction;
Vector3 right = Vector3.Cross( direction, Vector3.Up );
if ( right.LengthSquared < 0.001f )
{
right = Vector3.Cross( direction, Vector3.Right );
}
right = right.Normal;
Vector3 forward = Vector3.Cross( right, up ).Normal;
for ( int i = 0; i <= totalRings; i++ )
{
float v = i / (float)totalRings;
float h = v * totalHeight;
List<Vector3> currentRing = new();
for ( int j = 0; j <= segments; j++ )
{
float u = j / (float)segments;
float phi = u * 2.0f * MathF.PI;
float sinPhi = MathF.Sin( phi );
float cosPhi = MathF.Cos( phi );
Vector3 point;
if ( h < radius )
{
// Bottom hemisphere
float t = h / radius; // t from 0 to 1
float theta = (MathF.PI / 2) + (1 - t) * (MathF.PI / 2); // theta from π to π/2
float sinTheta = MathF.Sin( theta );
float cosTheta = MathF.Cos( theta );
point = centerA
+ (right * (sinTheta * cosPhi * radius))
+ (forward * (sinTheta * sinPhi * radius))
+ (up * (cosTheta * radius));
}
else if ( h <= radius + cylinderHeight )
{
// Cylinder
float y = h - radius; // y from 0 to cylinderHeight
point = centerA
+ up * y
+ (right * (cosPhi * radius))
+ (forward * (sinPhi * radius));
}
else
{
// Top hemisphere
float t = (h - (radius + cylinderHeight)) / radius; // t from 0 to 1
float theta = (MathF.PI / 2) * (1 - t); // theta from π/2 to 0
float sinTheta = MathF.Sin( theta );
float cosTheta = MathF.Cos( theta );
point = centerB
+ (right * (sinTheta * cosPhi * radius))
+ (forward * (sinTheta * sinPhi * radius))
+ (up * (cosTheta * radius));
}
currentRing.Add( point );
}
ringPoints.Add( currentRing );
}
}
private static float GetProjection( Vector3 v, Vector3 direction )
{
return Vector3.Dot( v, direction );
}
struct SubHull
{
public List<Vector3> Vertices;
public Surface Surface;
}
struct SubMesh
{
public List<Vector3> Vertices;
public List<int> Indices;
public Surface Surface;
public Transform PartTransform;
}
private List<SubHull> subHulls = new();
private List<SubMesh> subMeshes = new();
private List<SubHull> startCapSubHulls = new();
private List<SubMesh> startCapSubMeshes = new();
private List<SubHull> endCapSubHulls = new();
private List<SubMesh> endCapSubMeshes = new();
}
// need to be less precise than default
class Vector3Comparer : IEqualityComparer<Vector3>
{
private const float Tolerance = 0.1f;
public bool Equals( Vector3 a, Vector3 b )
{
return a.AlmostEqual( b, Tolerance );
}
public int GetHashCode( Vector3 v )
{
unchecked
{
return v.GetHashCode();
}
}
}