Effigy/Capsule.cs

A readonly Capsule struct representing a capsule segment (two endpoints, radius, optional bone index) with methods to find closest point and push a point out to the capsule surface, and a static BoneCapsules helper that builds capsules for body regions from mesh vertex positions using a percentile radius heuristic.

File Access
using System;
using System.Collections.Generic;
using System.Linq;

namespace Effigy;

/// <summary>
/// A segment with a radius - the shape cloth collides with when it is worn.
///
/// WHY NOT THE MESH. Cloth tested against a character's triangles snags on every bolt and falls
/// through every gap, and on a body built from parts (Camhead is struts and plates with air
/// between them) it has nothing to rest on at all. A capsule per bone is what real clothing
/// sims collide with: round, so cloth slides over it; solid, so there is no gap to fall into;
/// and a dozen of them cost less per particle than one BVH query. A shoulder capsule is the
/// ledge a shirt hangs from.
/// </summary>
public readonly struct Capsule
{
	public readonly Vec3 A;
	public readonly Vec3 B;
	public readonly float Radius;

	/// <summary>The skeleton bone it rides on, or -1. Live cloth needs it to move the capsule with
	/// the animation; a static drape ignores it.</summary>
	public readonly int Bone;

	public Capsule( Vec3 a, Vec3 b, float radius, int bone = -1 )
	{
		A = a;
		B = b;
		Radius = radius;
		Bone = bone;
	}

	/// <summary>The nearest point on the capsule's core segment.</summary>
	public Vec3 Closest( Vec3 p )
	{
		var ab = B - A;
		var len = ab.LengthSquared;
		var t = len < 1e-10f ? 0f : Math.Clamp( Vec3.Dot( p - A, ab ) / len, 0f, 1f );
		return A + ab * t;
	}

	/// <summary>Push <paramref name="p"/> out to the surface plus <paramref name="skin"/>.
	/// True when it moved.</summary>
	public bool Push( ref Vec3 p, float skin, out Vec3 normal )
	{
		var core = Closest( p );
		var d = p - core;
		var dist = d.Length;
		var want = Radius + skin;
		normal = Vec3.Zero;

		if ( dist >= want )
			return false;

		normal = dist > 1e-6f ? d * (1f / dist) : new Vec3( 1f, 0f, 0f );
		p = core + normal * want;
		return true;
	}
}

/// <summary>Capsules fitted to a rig from the body wearing it.</summary>
public static class BoneCapsules
{
	/// <summary>
	/// One capsule per region bone, as thick as the body around it.
	///
	/// THE RADIUS IS A PERCENTILE, not the furthest vertex. The furthest vertex round a spine is
	/// the tip of a shoulder pad or an antenna cable; a capsule that big swallows the arms. Most of
	/// the body's bulk is what a garment rests on, so the radius is the distance most of that
	/// bone's vertices sit within.
	/// </summary>
	public static List<Capsule> Build( BodyRegions.Map map, IEnumerable<PolyMesh> bodies, float percentile = 0.75f )
	{
		var segments = map.Segments;
		var near = new List<float>[segments.Count];

		for ( var i = 0; i < near.Length; i++ )
			near[i] = new List<float>();

		foreach ( var body in bodies )
		{
			foreach ( var p in body.Positions )
			{
				var best = -1;
				var bestD = float.MaxValue;

				for ( var s = 0; s < segments.Count; s++ )
				{
					var d = (new Capsule( segments[s].A, segments[s].B, 0f ).Closest( p ) - p).LengthSquared;

					if ( d < bestD )
					{
						bestD = d;
						best = s;
					}
				}

				if ( best >= 0 )
					near[best].Add( MathF.Sqrt( bestD ) );
			}
		}

		var capsules = new List<Capsule>();

		for ( var s = 0; s < segments.Count; s++ )
		{
			var region = segments[s].Region;

			// Hands, feet and the head are never under a shirt, and their bones are fingers,
			// toes and eyelids - dozens of tiny capsules that would only snag a hem.
			if ( region is BodyRegion.Hand or BodyRegion.Foot or BodyRegion.Head || near[s].Count < 8 )
				continue;

			near[s].Sort();
			var radius = near[s][Math.Clamp( (int)(near[s].Count * percentile), 0, near[s].Count - 1 )];

			// Cables and rods under half an inch are detail cloth rides over, not something it
			// hangs on - and each one is a test per particle per step.
			if ( radius >= 0.5f )
				capsules.Add( new Capsule( segments[s].A, segments[s].B, radius, map.SegmentBones[s] ) );
		}

		return capsules;
	}
}