Merge posture fixes: cascade two-step, ARKit normalization, diagonal tilt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-02-25 14:44:56 +01:00
commit 81bcd67428
4 changed files with 63 additions and 19 deletions

View File

@ -28,7 +28,7 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
// Find the ElevenLabsPostureComponent on the owning actor.
PostureComponent.Reset();
CachedEyeCurves.Reset();
CachedHeadRotation = FRotator::ZeroRotator;
CachedHeadRotation = FQuat::Identity;
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
@ -142,7 +142,7 @@ void FAnimNode_ElevenLabsPosture::Update_AnyThread(const FAnimationUpdateContext
// Cache posture data from the component (game thread safe copy).
CachedEyeCurves.Reset();
CachedHeadRotation = FRotator::ZeroRotator;
CachedHeadRotation = FQuat::Identity;
if (PostureComponent.IsValid())
{
@ -218,11 +218,51 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
}
#else
// ── PRODUCTION: Apply real head rotation ─────────────────────────
if (!CachedHeadRotation.IsNearlyZero(0.1f))
if (!CachedHeadRotation.Equals(FQuat::Identity, 0.001f))
{
const FQuat HeadOffset = CachedHeadRotation.Quaternion();
// Pre-multiply: apply offset in parent space (neck)
HeadTransform.SetRotation((HeadOffset * HeadTransform.GetRotation()).GetNormalized());
const FQuat BoneRot = HeadTransform.GetRotation();
const FQuat Combined = CachedHeadRotation * BoneRot;
// Remove Y-axis tilt from final rotation
FQuat Swing, TiltTwist;
Combined.ToSwingTwist(FVector::RightVector, Swing, TiltTwist);
HeadTransform.SetRotation(Swing.GetNormalized());
// ── Diagnostic log (every ~60 frames) ──
static int32 DiagCounter = 0;
if (++DiagCounter % 60 == 0)
{
const FRotator OffsetRot = CachedHeadRotation.Rotator();
const FRotator BoneRefRot = BoneRot.Rotator();
const FRotator CombRot = Combined.Rotator();
const FRotator SwingRot = Swing.Rotator();
const FRotator TiltRot = TiltTwist.Rotator();
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT("=== HEAD ROTATION DIAG ==="));
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT(" Offset Q: X=%.4f Y=%.4f Z=%.4f W=%.4f (P=%.1f Y=%.1f R=%.1f)"),
CachedHeadRotation.X, CachedHeadRotation.Y,
CachedHeadRotation.Z, CachedHeadRotation.W,
OffsetRot.Pitch, OffsetRot.Yaw, OffsetRot.Roll);
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT(" BoneRef Q: X=%.4f Y=%.4f Z=%.4f W=%.4f (P=%.1f Y=%.1f R=%.1f)"),
BoneRot.X, BoneRot.Y, BoneRot.Z, BoneRot.W,
BoneRefRot.Pitch, BoneRefRot.Yaw, BoneRefRot.Roll);
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT(" Combined Q: X=%.4f Y=%.4f Z=%.4f W=%.4f (P=%.1f Y=%.1f R=%.1f)"),
Combined.X, Combined.Y, Combined.Z, Combined.W,
CombRot.Pitch, CombRot.Yaw, CombRot.Roll);
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT(" Swing Q: X=%.4f Y=%.4f Z=%.4f W=%.4f (P=%.1f Y=%.1f R=%.1f)"),
Swing.X, Swing.Y, Swing.Z, Swing.W,
SwingRot.Pitch, SwingRot.Yaw, SwingRot.Roll);
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
TEXT(" TiltTwist: X=%.4f Y=%.4f Z=%.4f W=%.4f (P=%.1f Y=%.1f R=%.1f)"),
TiltTwist.X, TiltTwist.Y, TiltTwist.Z, TiltTwist.W,
TiltRot.Pitch, TiltRot.Yaw, TiltRot.Roll);
}
}
#endif
}
@ -230,10 +270,11 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
void FAnimNode_ElevenLabsPosture::GatherDebugData(FNodeDebugData& DebugData)
{
const FRotator DebugRot = CachedHeadRotation.Rotator();
FString DebugLine = FString::Printf(
TEXT("ElevenLabs Posture (eyes: %d curves, head: Y=%.1f P=%.1f)"),
CachedEyeCurves.Num(),
CachedHeadRotation.Yaw, CachedHeadRotation.Pitch);
DebugRot.Yaw, DebugRot.Pitch);
DebugData.AddDebugItem(DebugLine);
BasePose.GatherDebugData(DebugData);
}

View File

@ -275,13 +275,16 @@ void UElevenLabsPostureComponent::TickComponent(
{
FScopeLock Lock(&PostureDataLock);
// MetaHuman head bone axis mapping (independent quaternions to avoid
// diagonal coupling that FRotator causes when both axes are non-zero):
// Z-axis rotation = nod up/down → our HeadPitch
// MetaHuman head bone axis mapping:
// Z-axis rotation = nod up/down → our HeadPitch
// X-axis rotation = turn left/right → our HeadYaw
//
// Store raw composed quaternion. The parasitic Y-axis tilt from
// composition is removed in the AnimNode AFTER composing with the
// bone's reference rotation (which also contributes Y-coupling).
const FQuat NodQuat(FVector::UpVector, FMath::DegreesToRadians(-CurrentHeadPitch));
const FQuat TurnQuat(FVector::ForwardVector, FMath::DegreesToRadians(CurrentHeadYaw));
CurrentHeadRotation = (TurnQuat * NodQuat).Rotator();
CurrentHeadRotation = TurnQuat * NodQuat;
// Eye yaw is negated to match ARKit curve direction convention.
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);

View File

@ -62,9 +62,9 @@ private:
* Copied from the component during Update (game thread safe). */
TMap<FName, float> CachedEyeCurves;
/** Head rotation offset (yaw + pitch) to apply to the head bone.
* Copied from the component during Update. */
FRotator CachedHeadRotation = FRotator::ZeroRotator;
/** Head rotation offset as quaternion (no Euler round-trip, avoids
* parasitic tilt on diagonals). Copied from the component during Update. */
FQuat CachedHeadRotation = FQuat::Identity;
/** Resolved head bone index in the skeleton. */
FCompactPoseBoneIndex HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);

View File

@ -139,10 +139,10 @@ public:
return CurrentEyeCurves;
}
/** Get current head rotation offset (yaw + pitch, applied by AnimNode).
/** Get current head rotation offset (applied by AnimNode as FQuat to avoid
* Euler round-trip that reintroduces parasitic tilt on diagonals).
* Thread-safe copy. */
UFUNCTION(BlueprintCallable, Category = "ElevenLabs|Posture")
FRotator GetCurrentHeadRotation() const
FQuat GetCurrentHeadRotation() const
{
FScopeLock Lock(&PostureDataLock);
return CurrentHeadRotation;
@ -194,8 +194,8 @@ private:
/** 8 ARKit eye look curves (eyeLookUpLeft, eyeLookDownRight, etc.). */
TMap<FName, float> CurrentEyeCurves;
/** Head bone rotation offset (Yaw + Pitch). */
FRotator CurrentHeadRotation = FRotator::ZeroRotator;
/** Head bone rotation offset as quaternion (no Euler round-trip). */
FQuat CurrentHeadRotation = FQuat::Identity;
/** Cached skeletal mesh component on the owning actor. */
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;