Code/HumanoidMocap/Motion/WristPositionOffsets.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidMocap.Mapping;
namespace HumanoidMocap.Motion;
using Vector3 = System.Numerics.Vector3;
/// <summary>Reversible target edit in capture-camera metres. Never a reconstructed observation.</summary>
public sealed class WristPositionOffset
{
public BoneRole Hand { get; set; } = BoneRole.HandR;
public double Start { get; set; }
public double End { get; set; }
public float[] Offset { get; set; } = new float[3];
public double FadeSeconds { get; set; } = .1;
public bool Enabled { get; set; } = true;
public WristPositionOffset Copy()=>new(){Hand=Hand,Start=Start,End=End,Offset=Offset.ToArray(),FadeSeconds=FadeSeconds,Enabled=Enabled};
}
public static class WristPositionOffsets
{
public static void Validate(IReadOnlyList<WristPositionOffset> edits)
{
if(edits is null||edits.Count>64)throw new ArgumentException("At most 64 wrist corrections are supported.");
foreach(var edit in edits)
if(edit is null||edit.Hand is not (BoneRole.HandL or BoneRole.HandR)||!double.IsFinite(edit.Start)||!double.IsFinite(edit.End)||
edit.End<=edit.Start||!double.IsFinite(edit.FadeSeconds)||edit.FadeSeconds<=0||edit.FadeSeconds>5||
edit.Offset is not {Length:3}||edit.Offset.Any(v=>!float.IsFinite(v))||MotionDocument.V(edit.Offset).LengthSquared()>.25f)
throw new ArgumentException("Wrist corrections need a hand, a nonempty time range, a fade in (0,5] seconds and a finite offset no longer than 50 cm.");
for(var i=0;i<edits.Count;i++)for(var j=i+1;j<edits.Count;j++)
if(edits[i].Enabled&&edits[j].Enabled&&edits[i].Hand==edits[j].Hand&&edits[i].Start<edits[j].End&&edits[j].Start<edits[i].End)
throw new ArgumentException("Enabled corrections for the same wrist cannot overlap. Edit or disable the existing interval first.");
}
public static float Weight(WristPositionOffset edit,double time,double clipStart,double clipEnd)
{
if(!edit.Enabled||time<edit.Start||time>edit.End)return 0;
var fade=Math.Min(edit.FadeSeconds,(edit.End-edit.Start)/2);
double Smooth(double t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
// Clip boundaries have no neighbouring samples to blend to. Interior
// boundaries return smoothly to untouched motion without a position snap.
var enter=edit.Start<=clipStart?1:Smooth((time-edit.Start)/fade);
var leave=edit.End>=clipEnd?1:Smooth((edit.End-time)/fade);
return (float)Math.Min(enter,leave);
}
public static Vector3 Sample(IReadOnlyList<WristPositionOffset> edits,BoneRole hand,double time,double clipStart,double clipEnd)
{
var offset=Vector3.Zero;
foreach(var edit in edits)if(edit.Hand==hand)offset+=MotionDocument.V(edit.Offset)*Weight(edit,time,clipStart,clipEnd);
return offset;
}
}