Editor/CitizenRetarget/RetargetDiagnosticGenerator.cs
#nullable enable
namespace Editor.CitizenRetarget;
using NQuaternion = System.Numerics.Quaternion;
using NVector3 = System.Numerics.Vector3;
[Obsolete( "Deprecated math-audit diagnostic generator retained for historical troubleshooting only.", false )]
internal sealed class RetargetDiagnosticGenerator
{
private static readonly string[] DiagnosticBones =
{
"pelvis",
"spine_0",
"arm_upper_L",
"leg_upper_L",
"root_IK"
};
private readonly SmdAnimationWriter _smdWriter = new();
private readonly CitizenVmdlWriter _vmdlWriter = new();
private static readonly (string SequenceName, SmdRotationContract Contract)[] RotationContractDiagnostics =
{
("diag_target_rest_pose", SmdRotationContract.RawLocal),
("diag_target_rest_pose_inverse_local", SmdRotationContract.InverseLocal),
("diag_target_rest_pose_root_yup", SmdRotationContract.RawLocalRootYUp),
("diag_target_rest_pose_root_yup_inverse_local", SmdRotationContract.InverseLocalRootYUp),
("diag_target_rest_pose_legacy_valve", SmdRotationContract.LegacyValve),
("diag_target_rest_pose_legacy_valve_inverse", SmdRotationContract.LegacyValveInverse),
("diag_target_rest_pose_legacy_valve_basis", SmdRotationContract.LegacyValveBasis),
("diag_target_rest_pose_legacy_valve_basis_inverse", SmdRotationContract.LegacyValveBasisInverse)
};
public RetargetDiagnosticResult GenerateDiagnostics(
IReadOnlyList<NativeBoneInfo> targetBones,
string diagnosticsFolder,
string diagnosticsVmdlPath )
{
var absoluteFolder = CitizenRetargetPaths.GetAssetAbsolutePath( diagnosticsFolder );
Directory.CreateDirectory( absoluteFolder );
foreach ( var existing in Directory.GetFiles( absoluteFolder, "diag_*.smd", SearchOption.TopDirectoryOnly ) )
File.Delete( existing );
var result = new RetargetDiagnosticResult
{
DiagnosticsFolderAbsolutePath = absoluteFolder
};
foreach ( var diagnostic in RotationContractDiagnostics )
{
var fullSkeleton = BuildFullSkeletonClip( targetBones, diagnostic.SequenceName );
result.GeneratedClipPaths.Add( _smdWriter.WriteClip( fullSkeleton, diagnosticsFolder, new SmdWriteOptions
{
RotationContract = diagnostic.Contract
} ) );
}
var identityRotationSkeleton = BuildIdentityRotationClip( targetBones, "diag_target_rest_identity_rot" );
result.GeneratedClipPaths.Add( _smdWriter.WriteClip( identityRotationSkeleton, diagnosticsFolder ) );
foreach ( var boneName in DiagnosticBones )
{
foreach ( var axis in new[] { NVector3.UnitX, NVector3.UnitY, NVector3.UnitZ } )
{
foreach ( var angle in new[] { 90f, -90f } )
{
var diagnosticClip = BuildSingleBoneDiagnostic( targetBones, boneName, axis, angle );
result.GeneratedClipPaths.Add( _smdWriter.WriteClip( diagnosticClip, diagnosticsFolder ) );
}
}
}
result.DiagnosticsVmdlAbsolutePath = _vmdlWriter.WriteSharedVmdl( diagnosticsVmdlPath, diagnosticsFolder, ".smd" );
return result;
}
private static RetargetedClip BuildFullSkeletonClip( IReadOnlyList<NativeBoneInfo> targetBones, string sequenceName )
{
var clip = BuildClipSkeleton( targetBones, sequenceName );
clip.Frames.Add( new RetargetedFrame
{
Index = 0,
BoneTransforms = targetBones.Select( bone => bone.LocalTransform ).ToList()
} );
return clip;
}
private static RetargetedClip BuildIdentityRotationClip( IReadOnlyList<NativeBoneInfo> targetBones, string sequenceName )
{
var clip = BuildClipSkeleton( targetBones, sequenceName );
clip.Frames.Add( new RetargetedFrame
{
Index = 0,
BoneTransforms = targetBones
.Select( bone => new BoneTransform( bone.LocalTransform.Translation, NQuaternion.Identity ) )
.ToList()
} );
return clip;
}
private static RetargetedClip BuildSingleBoneDiagnostic(
IReadOnlyList<NativeBoneInfo> targetBones,
string boneName,
NVector3 axis,
float angleDegrees )
{
var axisLabel = axis == NVector3.UnitX ? "x" : axis == NVector3.UnitY ? "y" : "z";
var signLabel = angleDegrees > 0f ? "pos90" : "neg90";
var clip = BuildClipSkeleton( targetBones, $"diag_{boneName}_{axisLabel}_{signLabel}" );
var frame0 = new RetargetedFrame
{
Index = 0,
BoneTransforms = targetBones.Select( bone => bone.LocalTransform ).ToList()
};
var frame1 = new RetargetedFrame
{
Index = 1,
BoneTransforms = targetBones.Select( bone => bone.LocalTransform ).ToList()
};
var boneIndex = targetBones
.Select( ( bone, index ) => new { bone.Name, Index = index } )
.FirstOrDefault( item => item.Name.Equals( boneName, StringComparison.OrdinalIgnoreCase ) )
?.Index ?? -1;
if ( boneIndex >= 0 )
{
var rest = frame1.BoneTransforms[boneIndex];
var delta = NQuaternion.CreateFromAxisAngle( axis, angleDegrees * MathF.PI / 180f );
frame1.BoneTransforms[boneIndex] = new BoneTransform( rest.Translation, RetargetMath.Normalize( rest.Rotation * delta ) );
}
clip.FrameRate = 30;
clip.Frames.Add( frame0 );
clip.Frames.Add( frame1 );
return clip;
}
private static RetargetedClip BuildClipSkeleton( IReadOnlyList<NativeBoneInfo> targetBones, string sequenceName )
{
return new RetargetedClip
{
SourceClipName = sequenceName,
DisplayName = sequenceName,
SequenceName = sequenceName,
Looping = false,
FrameRate = 30,
Bones = targetBones
.Select( ( bone, index ) => new RetargetedBone
{
Id = index,
Name = bone.Name,
ParentId = string.IsNullOrEmpty( bone.ParentName )
? -1
: FindBoneIndex( targetBones, bone.ParentName ),
RestLocal = bone.LocalTransform
} )
.ToList()
};
}
private static int FindBoneIndex( IReadOnlyList<NativeBoneInfo> targetBones, string boneName )
{
for ( var index = 0; index < targetBones.Count; ++index )
{
if ( targetBones[index].Name.Equals( boneName, StringComparison.OrdinalIgnoreCase ) )
return index;
}
return -1;
}
}