Editor/CitizenRetarget/SmdAnimationWriter.cs
using System.Globalization;
namespace Editor.CitizenRetarget;
using NQuaternion = System.Numerics.Quaternion;
using NVector3 = System.Numerics.Vector3;
[Obsolete( "Deprecated diagnostic-only SMD writer. Production retarget output uses the template-FBX path.", false )]
internal sealed class SmdAnimationWriter : IRetargetOutputWriter
{
private static readonly NQuaternion RootYUpCorrection = RetargetMath.Normalize(
NQuaternion.CreateFromAxisAngle( NVector3.UnitX, -MathF.PI * 0.5f ) );
private static readonly NQuaternion ValveLegacyCorrection = RetargetMath.Normalize(
NQuaternion.CreateFromAxisAngle( NVector3.UnitY, MathF.PI * 0.5f ) *
NQuaternion.CreateFromAxisAngle( NVector3.UnitZ, MathF.PI * 0.5f ) );
public RetargetOutputFormat Format => RetargetOutputFormat.SmdDebug;
public string FileExtension => ".smd";
public string WriteClip( RetargetedClip clip, string outputAnimationFolder, SmdWriteOptions options = null )
{
options ??= new SmdWriteOptions();
var absoluteFolder = CitizenRetargetPaths.GetAssetAbsolutePath( outputAnimationFolder );
Directory.CreateDirectory( absoluteFolder );
var absolutePath = Path.Combine( absoluteFolder, $"{clip.SequenceName}.smd" );
var builder = new System.Text.StringBuilder();
builder.AppendLine( "version 1" );
builder.AppendLine( "nodes" );
foreach ( var bone in clip.Bones )
{
builder.AppendLine( $"{bone.Id} \"{bone.Name}\" {bone.ParentId}" );
}
builder.AppendLine( "end" );
builder.AppendLine( "skeleton" );
var previousEuler = new Dictionary<string, NVector3>( StringComparer.OrdinalIgnoreCase );
foreach ( var frame in clip.Frames )
{
builder.AppendLine( $"time {frame.Index}" );
for ( var boneIndex = 0; boneIndex < clip.Bones.Count; ++boneIndex )
{
var bone = clip.Bones[boneIndex];
var transform = SerializeTransform( bone, frame.BoneTransforms[boneIndex], options.RotationContract );
var euler = QuaternionToEulerXyz( transform.Rotation );
if ( previousEuler.TryGetValue( bone.Name, out var previous ) )
euler = UnwrapEuler( euler, previous );
previousEuler[bone.Name] = euler;
builder.Append( bone.Id.ToString( CultureInfo.InvariantCulture ) );
builder.Append( ' ' );
builder.Append( FormatFloat( transform.Translation.X ) );
builder.Append( ' ' );
builder.Append( FormatFloat( transform.Translation.Y ) );
builder.Append( ' ' );
builder.Append( FormatFloat( transform.Translation.Z ) );
builder.Append( ' ' );
builder.Append( FormatFloat( euler.X ) );
builder.Append( ' ' );
builder.Append( FormatFloat( euler.Y ) );
builder.Append( ' ' );
builder.AppendLine( FormatFloat( euler.Z ) );
}
}
builder.AppendLine( "end" );
File.WriteAllText( absolutePath, builder.ToString() );
return absolutePath;
}
string IRetargetOutputWriter.WriteClip( RetargetedClip clip, string outputFolder )
{
return WriteClip( clip, outputFolder );
}
private static BoneTransform SerializeTransform( RetargetedBone bone, BoneTransform transform, SmdRotationContract contract )
{
transform = new BoneTransform( transform.Translation, RetargetMath.Normalize( transform.Rotation ) );
var serialized = contract switch
{
SmdRotationContract.RawLocal => transform,
SmdRotationContract.InverseLocal => new BoneTransform(
transform.Translation,
RetargetMath.Normalize( NQuaternion.Inverse( transform.Rotation ) ) ),
SmdRotationContract.LegacyValve => new BoneTransform(
transform.Translation,
RetargetMath.Normalize( transform.Rotation * ValveLegacyCorrection ) ),
SmdRotationContract.LegacyValveInverse => new BoneTransform(
transform.Translation,
RetargetMath.Normalize( NQuaternion.Inverse( transform.Rotation * ValveLegacyCorrection ) ) ),
SmdRotationContract.LegacyValveBasis => ApplyBasisConjugation( transform, ValveLegacyCorrection, invertRotation: false ),
SmdRotationContract.LegacyValveBasisInverse => ApplyBasisConjugation( transform, ValveLegacyCorrection, invertRotation: true ),
SmdRotationContract.RawLocalRootYUp => transform,
SmdRotationContract.InverseLocalRootYUp => new BoneTransform(
transform.Translation,
RetargetMath.Normalize( NQuaternion.Inverse( transform.Rotation ) ) ),
_ => transform
};
if ( bone.ParentId < 0 && (contract == SmdRotationContract.RawLocalRootYUp || contract == SmdRotationContract.InverseLocalRootYUp) )
serialized = ApplyRootYUpCorrection( serialized );
return serialized;
}
private static BoneTransform ApplyBasisConjugation( BoneTransform transform, NQuaternion correction, bool invertRotation )
{
var inverseCorrection = RetargetMath.Normalize( NQuaternion.Inverse( correction ) );
var remappedTranslation = NVector3.Transform( transform.Translation, inverseCorrection );
var remappedRotation = RetargetMath.Normalize( inverseCorrection * transform.Rotation * correction );
if ( invertRotation )
remappedRotation = RetargetMath.Normalize( NQuaternion.Inverse( remappedRotation ) );
return new BoneTransform( remappedTranslation, remappedRotation );
}
private static BoneTransform ApplyRootYUpCorrection( BoneTransform transform )
{
return new BoneTransform(
NVector3.Transform( transform.Translation, RootYUpCorrection ),
RetargetMath.Normalize( RootYUpCorrection * transform.Rotation ) );
}
private static string FormatFloat( float value ) => value.ToString( "0.000000", CultureInfo.InvariantCulture );
private static NVector3 QuaternionToEulerXyz( NQuaternion rotation )
{
rotation = RetargetMath.Normalize( rotation );
var xx = rotation.X * rotation.X;
var yy = rotation.Y * rotation.Y;
var zz = rotation.Z * rotation.Z;
var xy = rotation.X * rotation.Y;
var xz = rotation.X * rotation.Z;
var yz = rotation.Y * rotation.Z;
var wx = rotation.W * rotation.X;
var wy = rotation.W * rotation.Y;
var wz = rotation.W * rotation.Z;
var m11 = 1f - 2f * (yy + zz);
var m12 = 2f * (xy - wz);
var m13 = 2f * (xz + wy);
var m23 = 2f * (yz - wx);
var m33 = 1f - 2f * (xx + yy);
var m32 = 2f * (yz + wx);
var m22 = 1f - 2f * (xx + zz);
var y = MathF.Asin( Math.Clamp( m13, -1f, 1f ) );
float x;
float z;
if ( MathF.Abs( m13 ) < 0.999999f )
{
x = MathF.Atan2( -m23, m33 );
z = MathF.Atan2( -m12, m11 );
}
else
{
x = MathF.Atan2( m32, m22 );
z = 0f;
}
return new NVector3( x, y, z );
}
private static NVector3 UnwrapEuler( NVector3 current, NVector3 previous )
{
return new NVector3(
UnwrapAngle( current.X, previous.X ),
UnwrapAngle( current.Y, previous.Y ),
UnwrapAngle( current.Z, previous.Z ) );
}
private static float UnwrapAngle( float value, float previous )
{
const float TwoPi = MathF.PI * 2f;
while ( value - previous > MathF.PI )
value -= TwoPi;
while ( previous - value > MathF.PI )
value += TwoPi;
return value;
}
}