diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/AnimNode_ElevenLabsPosture.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/AnimNode_ElevenLabsPosture.cpp index 4a66de2..a15eea1 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/AnimNode_ElevenLabsPosture.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/AnimNode_ElevenLabsPosture.cpp @@ -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); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsPostureComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsPostureComponent.cpp index 939068e..c79be15 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsPostureComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsPostureComponent.cpp @@ -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); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/AnimNode_ElevenLabsPosture.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/AnimNode_ElevenLabsPosture.h index c55139c..4164762 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/AnimNode_ElevenLabsPosture.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/AnimNode_ElevenLabsPosture.h @@ -62,9 +62,9 @@ private: * Copied from the component during Update (game thread safe). */ TMap 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); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsPostureComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsPostureComponent.h index cc7cf6a..7a08617 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsPostureComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsPostureComponent.h @@ -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 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 CachedMesh;