Compare commits

..

No commits in common. "c3abc420cf8043ea6f9faa083aef1bc92642bb97" and "7c235106d6313cd0a9a8bfc84a92452d6d631d64" have entirely different histories.

5 changed files with 19 additions and 63 deletions

View File

@ -28,7 +28,7 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
// Find the ElevenLabsPostureComponent on the owning actor. // Find the ElevenLabsPostureComponent on the owning actor.
PostureComponent.Reset(); PostureComponent.Reset();
CachedEyeCurves.Reset(); CachedEyeCurves.Reset();
CachedHeadRotation = FQuat::Identity; CachedHeadRotation = FRotator::ZeroRotator;
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE); HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy) 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). // Cache posture data from the component (game thread safe copy).
CachedEyeCurves.Reset(); CachedEyeCurves.Reset();
CachedHeadRotation = FQuat::Identity; CachedHeadRotation = FRotator::ZeroRotator;
if (PostureComponent.IsValid()) if (PostureComponent.IsValid())
{ {
@ -218,51 +218,11 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
} }
#else #else
// ── PRODUCTION: Apply real head rotation ───────────────────────── // ── PRODUCTION: Apply real head rotation ─────────────────────────
if (!CachedHeadRotation.Equals(FQuat::Identity, 0.001f)) if (!CachedHeadRotation.IsNearlyZero(0.1f))
{ {
const FQuat BoneRot = HeadTransform.GetRotation(); const FQuat HeadOffset = CachedHeadRotation.Quaternion();
const FQuat Combined = CachedHeadRotation * BoneRot; // Pre-multiply: apply offset in parent space (neck)
HeadTransform.SetRotation((HeadOffset * HeadTransform.GetRotation()).GetNormalized());
// 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 #endif
} }
@ -270,11 +230,10 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
void FAnimNode_ElevenLabsPosture::GatherDebugData(FNodeDebugData& DebugData) void FAnimNode_ElevenLabsPosture::GatherDebugData(FNodeDebugData& DebugData)
{ {
const FRotator DebugRot = CachedHeadRotation.Rotator();
FString DebugLine = FString::Printf( FString DebugLine = FString::Printf(
TEXT("ElevenLabs Posture (eyes: %d curves, head: Y=%.1f P=%.1f)"), TEXT("ElevenLabs Posture (eyes: %d curves, head: Y=%.1f P=%.1f)"),
CachedEyeCurves.Num(), CachedEyeCurves.Num(),
DebugRot.Yaw, DebugRot.Pitch); CachedHeadRotation.Yaw, CachedHeadRotation.Pitch);
DebugData.AddDebugItem(DebugLine); DebugData.AddDebugItem(DebugLine);
BasePose.GatherDebugData(DebugData); BasePose.GatherDebugData(DebugData);
} }

View File

@ -275,16 +275,13 @@ void UElevenLabsPostureComponent::TickComponent(
{ {
FScopeLock Lock(&PostureDataLock); FScopeLock Lock(&PostureDataLock);
// MetaHuman head bone axis mapping: // 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 // Z-axis rotation = nod up/down → our HeadPitch
// X-axis rotation = turn left/right → our HeadYaw // 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 NodQuat(FVector::UpVector, FMath::DegreesToRadians(-CurrentHeadPitch));
const FQuat TurnQuat(FVector::ForwardVector, FMath::DegreesToRadians(CurrentHeadYaw)); const FQuat TurnQuat(FVector::ForwardVector, FMath::DegreesToRadians(CurrentHeadYaw));
CurrentHeadRotation = TurnQuat * NodQuat; CurrentHeadRotation = (TurnQuat * NodQuat).Rotator();
// Eye yaw is negated to match ARKit curve direction convention. // Eye yaw is negated to match ARKit curve direction convention.
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch); UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);

View File

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

View File

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