Code/HumanoidMocap/Core/Motion/ArmContactSurface.cs
using System;
using System.Numerics;
using HumanoidMocapX.Core.Maths;
namespace HumanoidMocapX.Core.Motion;
using Vector3 = System.Numerics.Vector3;
/// <summary>Bone-derived torso proxy with elliptical sides and rounded ends.</summary>
internal readonly struct ArmContactSurface
{
readonly Vector3 bottom;
readonly float height, width, depth, cap;
internal readonly Vector3 Up, Across, Forward;
ArmContactSurface(Vector3 bottom, Vector3 up, Vector3 across, float height, float span)
{
this.bottom = bottom; Up = up; Across = across; Forward = Vector3.Cross(across, up);
this.height = height * .9f;
width = (CaptureArmClearance.HalfWidth + CaptureArmClearance.ArmRadius) * span;
depth = (CaptureArmClearance.HalfDepth + CaptureArmClearance.ArmRadius) * span;
cap = CaptureArmClearance.ArmRadius * span;
}
internal static bool TryCreate(XForm[] world, int hips, int neck, int left, int right, out ArmContactSurface result)
{
result = default;
var axis = world[neck].Pos - world[hips].Pos;
var height = axis.Length();
if (height < 1e-5f) return false;
var up = axis / height;
var shoulders = world[left].Pos - world[right].Pos;
var span = shoulders.Length() * .5f;
var across = shoulders - up * Vector3.Dot(shoulders, up);
if (span < 1e-5f || across.LengthSquared() < 1e-10f) return false;
result = new(world[hips].Pos, up, Vector3.Normalize(across), height, span);
return true;
}
internal Vector3 ToLocalOffset(Vector3 value) => new(Vector3.Dot(value, Across), Vector3.Dot(value, Up), Vector3.Dot(value, Forward));
internal Vector3 ToWorldOffset(Vector3 value) => Across * value.X + Up * value.Y + Forward * value.Z;
internal Vector3 Outward(Vector3 point, Vector3 fallback)
{
var local = Normalized(point);
var outward = Across * (local.X / width) + Forward * (local.Z / depth);
return outward.LengthSquared() > 1e-8f ? outward : fallback;
}
Vector3 Normalized(Vector3 point)
{
var local = ToLocalOffset(point - bottom);
return new(local.X / width, local.Y / cap, local.Z / depth);
}
/// <summary>Finds the deepest point of a segment, including the end caps, without sampling gaps.</summary>
internal Vector3 ProjectSegment(Vector3 start, Vector3 end, Vector3 fallback, out float fraction)
{
var a = Normalized(start); var d = Normalized(end) - a; var top = height / cap;
fraction = 0;
var best = DistanceSquared(a, top);
Consider(a, d, top, 1, ref fraction, ref best);
var radial = d.X * d.X + d.Z * d.Z;
if (radial > 1e-12f) Consider(a, d, top, -(a.X * d.X + a.Z * d.Z) / radial, ref fraction, ref best);
var length = d.LengthSquared();
if (length > 1e-12f)
{
Consider(a, d, top, -Vector3.Dot(a, d) / length, ref fraction, ref best);
Consider(a, d, top, Vector3.Dot(new Vector3(0, top, 0) - a, d) / length, ref fraction, ref best);
}
if (MathF.Abs(d.Y) > 1e-12f)
{
Consider(a, d, top, -a.Y / d.Y, ref fraction, ref best);
Consider(a, d, top, (top - a.Y) / d.Y, ref fraction, ref best);
}
if (best >= 1) return Vector3.Zero;
var point = a + d * fraction;
point.Y -= Math.Clamp(point.Y, 0, top);
// All contacts on a hand use one escape direction, avoiding front/back flips
// when a different finger or forearm point becomes the deepest contact.
var localDirection = ToLocalOffset(fallback);
var direction = new Vector3(localDirection.X / width, 0, localDirection.Z / depth);
if (direction.LengthSquared() < 1e-12f) direction = Vector3.UnitZ;
direction = Vector3.Normalize(direction);
var along = Vector3.Dot(point, direction);
var distance = -along + MathF.Sqrt(along * along + 1.0002f - best);
var displacement = direction * distance;
return ToWorldOffset(new(displacement.X * width, displacement.Y * cap, displacement.Z * depth));
}
static float DistanceSquared(Vector3 point, float top)
{
point.Y -= Math.Clamp(point.Y, 0, top);
return point.LengthSquared();
}
static void Consider(Vector3 start, Vector3 delta, float top, float candidate, ref float fraction, ref float best)
{
candidate = Math.Clamp(candidate, 0, 1);
var value = DistanceSquared(start + delta * candidate, top);
if (value >= best) return;
best = value; fraction = candidate;
}
}