Editor/EffigyEditor/EffigySkeletonImport.cs
using Effigy;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
// Effigy.Skeleton, not Sandbox.Skeleton - the engine has a Skeleton type of its own, and this file
// reads one of those into one of these, so the ambiguity is the whole subject.
using Skeleton = Effigy.Skeleton;
namespace Marionette.EditorTools;
/// <summary>
/// Read a compiled model's skeleton into an Effigy <see cref="Skeleton"/>.
///
/// WHY THIS EXISTS. Every rig Effigy has made so far was invented in the tool - BoneFromBody names
/// the bones after the bodies they came from, and the bind pose is wherever the user dropped the
/// handles. That is right for a prop and useless for a PLAYERMODEL, because citizen's animations
/// bind by bone NAME against citizen's bind pose. A model that is not literally `pelvis`,
/// `spine_0`, `arm_upper_L` in citizen's hierarchy plays those animations as spaghetti. So the
/// first thing a playermodel path needs is the ability to start from somebody else's skeleton
/// rather than from nothing.
///
/// THE BIND POSE IS COPIED, NOT RE-DERIVED, and that is the whole correctness argument here.
/// Effigy's own bone placement goes through AddBoneFromPoints, which aims a bone +Y head-to-tail
/// and picks the roll about that aim - fine when a human is drawing a chain, wrong here. A
/// compiled bone's LocalTransform carries an orientation that its animation data assumes, and
/// re-deriving axes from head and tail would silently rotate every bone about its own length. So
/// this goes through <see cref="Skeleton.AddBone"/> with the local basis verbatim, and Length is
/// filled in afterwards purely as a tail for the viewport and for auto-weighting to measure to.
///
/// `LocalTransform` IS NOT LOCAL. Despite the name, BoneCollection.Bone.LocalTransform is in
/// MODEL space - it matches Model.GetBoneTransform for that bone exactly, on every one of
/// citizen's 95 bones. Effigy's Bone.Local is relative to the parent, which is what makes a chain
/// behave when a parent moves, so each bone is rebased against its parent's world bind on the way
/// in. Taking the name at face value put citizen's head 99 inches out sideways, because walking
/// the parent chain then compounds a transform that was already absolute.
///
/// TOPOLOGICAL ORDER IS NOT ASSUMED. Skeleton.AddBone refuses a parent that does not exist yet,
/// which is what keeps its list acyclic - so this walks the model's tree from the roots outward
/// rather than iterating 0..BoneCount and hoping the engine's order agrees.
///
/// PROTOTYPE. Nothing calls this from the UI yet; `effigy_skeleton_probe` in
/// <see cref="EffigySkeletonProbe"/> drives it from the console to answer the one question that
/// decides whether the whole playermodel idea is viable - does citizen's bind pose survive the
/// round trip.
/// </summary>
public static class EffigySkeletonImport
{
/// <summary>What an import produced, and everything questionable about it. The notes are the
/// point as much as the skeleton is: an import that quietly drops a bone is worse than one
/// that refuses, because the model still builds and only the animation looks wrong.</summary>
public sealed class Result
{
public Skeleton Skeleton = new();
/// <summary>Effigy bone index, by the name the model used.</summary>
public Dictionary<string, int> ByName = new();
/// <summary>Things worth telling the user - non-unit scale, bones with no children to take
/// a length from, several roots. None of these fail the import.</summary>
public List<string> Notes = new();
public int Roots;
}
/// <summary>
/// Import every bone of <paramref name="model"/>.
///
/// <paramref name="defaultLength"/> is what a bone with no children gets, since a tail has to
/// come from somewhere and a leaf has nothing to point at. Inches, like everything else in the
/// engine's units.
/// </summary>
public static Result FromModel( Model model, float defaultLength = 1f )
{
ArgumentNullException.ThrowIfNull( model );
var result = new Result();
var bones = model.Bones?.AllBones?.ToList() ?? new List<BoneCollection.Bone>();
if ( bones.Count == 0 )
{
result.Notes.Add( "The model has no bones - it is a static mesh, not a rig." );
return result;
}
// Roots first, then each root's subtree, so a parent is always added before its children.
foreach ( var root in bones.Where( b => b.Parent is null ) )
{
result.Roots++;
AddSubtree( result, root, -1, defaultLength );
}
if ( result.Skeleton.Count != bones.Count )
{
result.Notes.Add( $"{bones.Count - result.Skeleton.Count} of {bones.Count} bones were "
+ "not reachable from any root - the model's bone tree is not a tree." );
}
if ( result.Roots > 1 )
{
result.Notes.Add( $"{result.Roots} root bones. Retargeting tools assume one; see "
+ "RigDiagnostics for the same complaint about rigs built in the tool." );
}
SetLengths( result, defaultLength );
return result;
}
static void AddSubtree( Result result, BoneCollection.Bone bone, int parent, float defaultLength )
{
var name = bone.Name;
if ( string.IsNullOrWhiteSpace( name ) )
{
result.Notes.Add( $"A bone under index {parent} has no name and was skipped - every "
+ "format Effigy writes keys on the name." );
return;
}
if ( result.ByName.ContainsKey( name ) )
{
result.Notes.Add( $"Two bones are both called '{name}'; the second was skipped." );
return;
}
var modelSpace = bone.LocalTransform;
if ( !modelSpace.Scale.AlmostEqual( 1f ) )
{
result.Notes.Add( $"Bone '{name}' has scale {modelSpace.Scale} in its bind pose. It is "
+ "carried through in the basis, but scaled bind poses skin badly." );
}
// Rebase into the parent's frame - see `LocalTransform` IS NOT LOCAL above. The parent is
// always already added, because this walks the tree from the roots outward.
var world = ToXform( modelSpace );
var local = parent < 0 ? world : result.Skeleton.WorldBind( parent ).Inverse * world;
var index = result.Skeleton.AddBone( name, parent, local, defaultLength );
result.ByName[name] = index;
foreach ( var child in bone.Children )
AddSubtree( result, child, index, defaultLength );
}
/// <summary>
/// A bone's tail, once the whole hierarchy exists.
///
/// A bone's length is the distance to its first child's head, which is the convention every
/// tool that draws bones uses and what auto-weighting wants - a bone is a segment to measure
/// distance to, not a point. Leaves have nothing to measure to and keep the default. This runs
/// as a second pass because a child's local position is only meaningful after it is added.
/// </summary>
static void SetLengths( Result result, float defaultLength )
{
var firstChild = new int[result.Skeleton.Count];
Array.Fill( firstChild, -1 );
for ( var i = result.Skeleton.Count - 1; i >= 0; i-- )
{
var parent = result.Skeleton.Bones[i].Parent;
if ( parent >= 0 )
firstChild[parent] = i;
}
var leaves = 0;
for ( var i = 0; i < result.Skeleton.Count; i++ )
{
if ( firstChild[i] < 0 )
{
leaves++;
continue;
}
var reach = result.Skeleton.Bones[firstChild[i]].Local.Origin.Length;
result.Skeleton.Bones[i].Length = reach > 1e-4f ? reach : defaultLength;
}
if ( leaves > 0 )
result.Notes.Add( $"{leaves} leaf bones kept the default length of {defaultLength}." );
}
/// <summary>
/// An engine Transform as Effigy's basis-and-origin form. The rotation's three axes become the
/// three basis vectors, scaled - which is exactly what TransformPoint then reproduces, so a
/// uniformly or non-uniformly scaled bind pose survives rather than being quietly dropped.
/// </summary>
public static Xform ToXform( Transform t )
{
var scale = t.Scale;
return new Xform(
ToVec( t.Rotation.Forward * scale.x ),
ToVec( t.Rotation.Left * scale.y ),
ToVec( t.Rotation.Up * scale.z ),
ToVec( t.Position ) );
}
public static Vec3 ToVec( Vector3 v ) => new( v.x, v.y, v.z );
public static Vector3 ToVector3( Vec3 v ) => new( v.x, v.y, v.z );
}