GrumpySausagePose.cs
using Sandbox;
using System;
namespace PaintballBaddies;
/// <summary>Native motion rotations on the sausage's own rest lengths.</summary>
internal sealed class GrumpySausagePose
{
private sealed class Joint
{
public string Name,Parent;
public Transform Rest,Local,SourceRest,SourceLocal;
}
private readonly List<Joint> joints=new();
private readonly Dictionary<string,Transform> sourcePose=new();
internal readonly Dictionary<string,Transform> Poses=new();
private readonly Dictionary<string,List<string>> subtree=new();
private readonly Dictionary<string,Rotation> palmFrames=new();
internal GrumpySausagePose(Model target,Model source)
{
var pending=target.Bones.AllBones.ToList();
while(pending.Count>0)
{
var bone=pending.FirstOrDefault(b=>b.Parent is null || joints.Any(j=>j.Name==b.Parent.Name));
if(bone is null)break;
var rest=target.GetBoneTransform(bone.Name);var sourceRest=source.GetBoneTransform(bone.Name);
string parent=bone.Parent?.Name;
joints.Add(new Joint{Name=bone.Name,Parent=parent,Rest=rest,SourceRest=sourceRest,
Local=parent is null ? rest : target.GetBoneTransform(parent).ToLocal(rest),
SourceLocal=parent is null ? sourceRest : source.GetBoneTransform(parent).ToLocal(sourceRest)});
pending.Remove(bone);
}
// Every bone's subtree (itself plus all descendants), for arm overrides.
foreach(var j in joints)
{
var list=new List<string>{j.Name};
for(int i=0;i<list.Count;i++)
foreach(var c in joints)if(c.Parent==list[i])list.Add(c.Name);
subtree[j.Name]=list;
}
// This fitted rig has wrist bones but no finger bones. Derive each
// wrist's palm axes from the native donor's bind pose once, instead of
// looking for fingers that do not exist in the rendered sausage.
foreach(string side in new[]{"L","R"})
{
var wrist=source.GetBoneTransform("hand_"+side);
var middle=source.GetBoneTransform("finger_middle_0_"+side).Position;
var across=source.GetBoneTransform("finger_index_0_"+side).Position-source.GetBoneTransform("finger_ring_0_"+side).Position;
var along=(middle-wrist.Position).Normal;
var palm=Vector3.Cross(along,across).Normal*(side=="L" ? 1 : -1);
palmFrames[side]=Rotation.LookAt(wrist.Rotation.Inverse*along,wrist.Rotation.Inverse*palm);
}
}
/// <summary>Carrying pose for one arm, like a waiter: upper arm hanging
/// close to the side, elbow bent about 90 degrees, forearm pointing forward
/// with the palm under the load. Model space: +x forward, +y left, +z up.</summary>
internal void Carry(string side,float weight)
{
if(weight<=0)return;
float s=side=="L" ? 1 : -1;
AimBone("arm_upper_"+side,"arm_lower_"+side,new Vector3(.22f,.2f*s,-1).Normal,weight);
AimBone("arm_lower_"+side,"hand_"+side,new Vector3(1,.12f*s,.08f).Normal,weight);
SetPalm(side,Vector3.Up,weight);
}
/// <summary>Turns the hand palm-up and lays the fingers flat, like a waiter
/// balancing a tray.</summary>
private void SetPalm(string side,Vector3 normal,float weight)
{
string hand="hand_"+side;
if(!Poses.TryGetValue(hand,out var h) || !Poses.TryGetValue("arm_lower_"+side,out var elbow)
|| !subtree.TryGetValue(hand,out var list) || !palmFrames.TryGetValue(side,out var frame))return;
var along=(h.Position-elbow.Position).Normal;
var palm=normal-along*Vector3.Dot(normal,along);
if(palm.Length<.05f)palm=Vector3.Up-along*Vector3.Dot(Vector3.Up,along);
if(palm.Length<.05f)return;
// Spread forearm roll across the elbow-to-wrist segment. Twisting only
// the hand collapsed the narrow wrist where its skin blends into it.
var currentPalm=(h.Rotation*frame).Up;
currentPalm=(currentPalm-along*Vector3.Dot(currentPalm,along)).Normal;
float roll=MathF.Atan2(Vector3.Dot(Vector3.Cross(currentPalm,palm.Normal),along),Vector3.Dot(currentPalm,palm.Normal))*180/MathF.PI;
var forearmRoll=Rotation.FromAxis(along,roll*.6f*weight.Clamp(0,1));
foreach(var name in subtree["arm_lower_"+side])
{
var p=Poses[name];p.Position=elbow.Position+forearmRoll*(p.Position-elbow.Position);
p.Rotation=forearmRoll*p.Rotation;Poses[name]=p;
}
h=Poses[hand];
// Align the wrist with the forearm as well as turning the palm upward.
var wanted=Rotation.LookAt(along,palm.Normal)*frame.Inverse;
var q=Rotation.Slerp(h.Rotation,wanted,weight.Clamp(0,1))*h.Rotation.Inverse;
var pivot=h.Position;
foreach(var name in list)
{
if(!Poses.TryGetValue(name,out var p))continue;
p.Position=pivot+q*(p.Position-pivot);p.Rotation=q*p.Rotation;Poses[name]=p;
}
}
internal Rotation PalmFrame(string side)=>Poses["hand_"+side].Rotation*palmFrames[side];
internal Vector3 PalmCenter(string side)=>Poses["hand_"+side].Position+PalmFrame(side).Forward*2.1f+PalmFrame(side).Up*2.0f;
/// <summary>Throwing arm. Overhand for eggs and knives (hand cocked behind
/// the head, whip forward, follow through across the body); backhand
/// frisbee for plates (arm across the chest, fling out flat to the side).
/// phase: 0..1 wind-up (1 = release), 1..2 follow-through.</summary>
internal void Throw(string side,float phase,bool frisbee,float weight)
{
if(weight<=0)return;
float s=side=="L" ? 1 : -1;
var rest=(new Vector3(.2f,.25f*s,-1),new Vector3(.9f,.1f*s,.1f));
// Reach across to the load in the other hand and take one.
var reach=(new Vector3(.6f,-.35f*s,-.7f),new Vector3(.45f,-.85f*s,.15f));
(Vector3 U,Vector3 F)[] keys=frisbee
? new[]{rest,reach,
(new Vector3(.5f,-.5f*s,-.25f),new Vector3(-.5f,-.8f*s,.05f)), // cocked across the chest
(new Vector3(.65f,.72f*s,.05f),new Vector3(.85f,.5f*s,.08f)), // release, flat
(new Vector3(.25f,.95f*s,-.1f),new Vector3(.3f,.95f*s,-.12f))} // follow-through out wide
: new[]{rest,reach,
(new Vector3(-.3f,.45f*s,.6f),new Vector3(-.25f,.1f*s,1)), // cocked behind the head
(new Vector3(.85f,.25f*s,.4f),new Vector3(1,.05f*s,-.05f)), // release
(new Vector3(.55f,-.15f*s,-.65f),new Vector3(.65f,-.35f*s,-.6f))}; // follow-through across
// 0-.25 reach, hold to .35 (take it), .35-.65 raise and cock, hold to .78,
// whip to release at 1, follow through to 1.5.
int a,b;float t;
if(phase<.25f){a=0;b=1;t=phase/.25f;}
else if(phase<.35f){a=1;b=1;t=0;}
else if(phase<.65f){a=1;b=2;t=(phase-.35f)/.3f;}
else if(phase<.78f){a=2;b=2;t=0;}
else if(phase<1){a=2;b=3;t=(phase-.78f)/.22f;}
else{a=3;b=4;t=((phase-1)/.5f).Clamp(0,1);}
t=t*t*(3-2*t);
var up=Vector3.Lerp(keys[a].U.Normal,keys[b].U.Normal,t).Normal;
var fore=Vector3.Lerp(keys[a].F.Normal,keys[b].F.Normal,t).Normal;
AimBone("arm_upper_"+side,"arm_lower_"+side,up,weight);
AimBone("arm_lower_"+side,"hand_"+side,fore,weight);
var palm=Vector3.Up;
if(!frisbee && phase>=.35f)
{
float roll=phase<.78f ? ((phase-.35f)/.43f).Clamp(0,1) : ((phase-.78f)/.22f).Clamp(0,1);
roll=roll*roll*(3-2*roll);
palm=phase<.78f ? Vector3.Lerp(Vector3.Up,Vector3.Forward,roll).Normal : Vector3.Lerp(Vector3.Forward,Vector3.Down,roll).Normal;
}
SetPalm(side,palm,weight);
}
private void AimBone(string bone,string child,Vector3 dir,float weight)
{
if(!Poses.TryGetValue(bone,out var b) || !Poses.TryGetValue(child,out var c) || !subtree.TryGetValue(bone,out var list))return;
var pivot=b.Position;var current=(c.Position-pivot).Normal;
var axis=Vector3.Cross(current,dir);float sin=axis.Length;
if(sin<.0001f)
{
if(Vector3.Dot(current,dir)>0)return;
axis=Vector3.Cross(current,MathF.Abs(current.z)<.9f ? Vector3.Up : Vector3.Left).Normal;sin=0;
}
float angle=MathF.Atan2(sin,Vector3.Dot(current,dir))*180/MathF.PI*weight.Clamp(0,1);
var q=Rotation.FromAxis(sin>0 ? axis/sin : axis,angle);
foreach(var name in list)
{
if(!Poses.TryGetValue(name,out var p))continue;
p.Position=pivot+q*(p.Position-pivot);p.Rotation=q*p.Rotation;Poses[name]=p;
}
}
internal void Update(SkinnedModelRenderer driver,SkinnedModelRenderer slip,float slipBlend,Angles recoil,int missingParts=0)
{
sourcePose.Clear();Poses.Clear();
foreach(var j in joints)
{
var p=driver.TryGetBoneTransform(j.Name,out var world) ? driver.WorldTransform.ToLocal(world) : j.SourceRest;
if(slipBlend>0 && slip.IsValid() && slip.TryGetBoneTransform(j.Name,out var other))
{
var s=slip.WorldTransform.ToLocal(other);
p.Position=Vector3.Lerp(p.Position,s.Position,slipBlend);
p.Rotation=Rotation.Slerp(p.Rotation,s.Rotation,slipBlend);
}
sourcePose[j.Name]=p;
}
foreach(var j in joints)
{
var animated=sourcePose[j.Name];
var local=j.Parent is null ? animated : sourcePose[j.Parent].ToLocal(animated);
var result=j.Local;
result.Rotation=j.Local.Rotation*j.SourceLocal.Rotation.Inverse*local.Rotation;
// Do not copy the human skeleton's animated bone positions/scales.
// This preserves arm, leg and casing lengths even during a full fall.
if(j.Parent is null)result.Position=j.Rest.Position+(animated.Position-j.SourceRest.Position)*.40f;
if(j.Name=="spine_1" || j.Name=="spine_2")result.Rotation*=Rotation.From(recoil*.5f);
Poses[j.Name]=j.Parent is null ? result : Poses[j.Parent].ToWorld(result);
}
// The native gait supplies the timing; our larger boots plant on the floor.
float bottom=float.PositiveInfinity;
foreach(string side in new[]{"L","R"})
{
if((missingParts&(side=="L" ? 8 : 16))!=0)continue;
if(Poses.TryGetValue("ankle_"+side,out var foot))bottom=MathF.Min(bottom,foot.Position.z-4.7f);
if(Poses.TryGetValue("ball_"+side,out var toe))bottom=MathF.Min(bottom,toe.Position.z-1.55f);
}
if(slipBlend>.1f)
{
foreach(var item in new[]{("pelvis",5f),("spine_0",7f),("spine_1",8f),("spine_2",7f),("head",7f)})
if(Poses.TryGetValue(item.Item1,out var p))bottom=MathF.Min(bottom,p.Position.z-item.Item2*slipBlend);
}
if(float.IsFinite(bottom))
{
float lift=-bottom;
foreach(var j in joints){var p=Poses[j.Name];p.Position+=Vector3.Up*lift;Poses[j.Name]=p;}
}
}
}