Editor/HumanoidMocap/PreviewWidget.Bones.cs
using System;
using HumanoidMocap.Mapping;
using Sandbox;
namespace HumanoidMocap.Editor;
public sealed partial class PreviewWidget
{
/// <summary>Blender-style octahedral bones and round joints on the solved target.
/// Gizmo IgnoreDepth keeps them visible through the skinned mesh.</summary>
public bool ShowTargetBones { get; set; } = true;
public int TargetBoneOverlayCount { get; private set; }
static readonly Color TargetBoneOutline = new(1f,.58f,.16f,1f);
void DrawTargetBones()
{
DrawPropBones();
TargetBoneOverlayCount=0;
if(_clip is not null&&!FirstPerson)
{
UpdateGizmoInputs(false);
using(Gizmo.Scope("HumanoidMocap.Ground"))
{
Gizmo.Transform=Transform.Zero;
Gizmo.Draw.IgnoreDepth=false;
// s&box's editor grid: a fixed floor reference, independent of the
// character's travel and bone-overlay visibility. Half-metre cells.
Gizmo.Draw.Grid(Gizmo.GridAxis.XY,19.68504f,.3f);
}
}
if(!ShowTargetBones||_worldScratch is null||_clip is null)return;
UpdateGizmoInputs(false);
using(Gizmo.Scope("HumanoidMocap.TargetBones"))
{
// Endpoints below are already world-space. Never inherit another gizmo's transform.
Gizmo.Transform=Transform.Zero;
Gizmo.Draw.IgnoreDepth=true;
Gizmo.Draw.LineThickness=1.2f;
foreach(var bone in _rig.Skeleton.Bones)
{
if(_rig.RoleOf(bone.Index) is not { } role)continue;
if(FirstPerson&&!IsArmOrFinger(role))continue;
// The clavicle's parenting offset starts at the chest bone, well below
// the collarbone. It is a hierarchy link, not a solid anatomical bone.
// The actual clavicle is drawn by the clavicle -> upper-arm edge below.
if(role is BoneRole.ClavicleL or BoneRole.ClavicleR)continue;
var parent=bone.ParentIndex;
while(parent>=0&&_rig.RoleOf(parent) is null)parent=_rig.Skeleton[parent].ParentIndex;
if(parent<0||FirstPerson&&_rig.RoleOf(parent) is { } parentRole&&!IsArmOrFinger(parentRole))continue;
if(!TryTargetBonePosition(parent,out var head)||!TryTargetBonePosition(bone.Index,out var tail))continue;
var delta=tail-head;var length=delta.Length;
if(length<.02f)continue;
var direction=delta/length;
var u=Vector3.Cross(direction,MathF.Abs(direction.z)<.8f?Vector3.Up:Vector3.Forward).Normal;
var v=Vector3.Cross(direction,u).Normal;
var width=Math.Clamp(length*.1f,.035f,.7f);
var waist=head+delta*.22f;
var ring=new[]{waist+u*width,waist+v*width,waist-u*width,waist-v*width};
// Low-opacity faces retain the silhouette without hiding finger articulation.
Gizmo.Draw.Color=TargetBoneOutline.WithAlpha(.16f);
for(var i=0;i<4;i++)
{
var next=ring[(i+1)%4];
Gizmo.Draw.SolidTriangle(head,ring[i],next);
Gizmo.Draw.SolidTriangle(tail,next,ring[i]);
}
Gizmo.Draw.Color=TargetBoneOutline;
for(var i=0;i<4;i++)
{
Gizmo.Draw.Line(head,ring[i]);Gizmo.Draw.Line(tail,ring[i]);
Gizmo.Draw.Line(ring[i],ring[(i+1)%4]);
}
var radius=Math.Clamp(length*.045f,.045f,.2f);
Gizmo.Draw.SolidSphere(head,radius,6,6);Gizmo.Draw.SolidSphere(tail,radius,6,6);
TargetBoneOverlayCount++;
}
}
}
bool TryTargetBonePosition(int index,out Vector3 position)
{
if(_sceneModel.IsValid()&&!SkeletonOnly)
{
// Read the final render transforms so overlay endpoints coincide with the
// skin, including model-space scale and any unrepresented native bones.
var modelBone=_rigToModelBone[index];
if(modelBone<0){position=default;return false;}
position=_sceneModel.GetBoneWorldTransform(modelBone).Position;
return true;
}
position=RigWorldToEngine(_worldScratch[index]).Position;
return true;
}
static bool IsArmOrFinger(BoneRole role)=>role is BoneRole.UpperArmL or BoneRole.UpperArmR or
BoneRole.LowerArmL or BoneRole.LowerArmR or BoneRole.HandL or BoneRole.HandR||
role.ToString().StartsWith("Thumb",StringComparison.Ordinal)||role.ToString().StartsWith("Index",StringComparison.Ordinal)||
role.ToString().StartsWith("Middle",StringComparison.Ordinal)||role.ToString().StartsWith("Ring",StringComparison.Ordinal)||
role.ToString().StartsWith("Pinky",StringComparison.Ordinal);
}