Posture: body drift compensation + debug gaze lines + ARKit eye range calibration
- Add BodyDriftCompensation parameter (0→1) to counter-rotate head when body animation moves the torso (bow, lean). Drift correction applied AFTER swing-twist on the first chain bone only, preventing the correction from being stripped by tilt decomposition or compounded through the chain. - Add bDrawDebugGaze toggle: draws per-eye debug lines from Face mesh FACIAL_L/R_Eye bones (cyan = desired direction, green = actual bone Z-axis gaze) to visually verify eye contact accuracy. - Cache Face mesh separately from Body mesh for correct eye bone transforms. - Use eye bone midpoint (Face mesh) as EyeOrigin for pitch calculation instead of head bone, fixing vertical offset. - Calibrate ARKit eye ranges: horizontal 30→40, vertical 20→35 to match MetaHuman actual eye deflection per curve unit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9eca5a3cfe
commit
fd0e2a2d58
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -87,6 +87,9 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
||||
CachedHeadRotation = FQuat::Identity;
|
||||
CachedHeadCompensation = 1.0f;
|
||||
CachedEyeCompensation = 1.0f;
|
||||
CachedBodyDriftCompensation = 0.0f;
|
||||
AncestorBoneIndices.Reset();
|
||||
RefAccumAboveChain = FQuat::Identity;
|
||||
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||
HeadRefPoseRotation = FQuat::Identity;
|
||||
ChainBoneNames.Reset();
|
||||
@ -124,13 +127,14 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
||||
}
|
||||
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||
TEXT("ElevenLabs Posture AnimNode: Owner=%s Mesh=%s Head=%s Eyes=%s Chain=%d HeadComp=%.1f EyeComp=%.1f"),
|
||||
TEXT("ElevenLabs Posture AnimNode: Owner=%s Mesh=%s Head=%s Eyes=%s Chain=%d HeadComp=%.1f EyeComp=%.1f DriftComp=%.1f"),
|
||||
*Owner->GetName(), *SkelMesh->GetName(),
|
||||
bApplyHeadRotation ? TEXT("ON") : TEXT("OFF"),
|
||||
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"),
|
||||
ChainBoneNames.Num(),
|
||||
Comp->GetHeadAnimationCompensation(),
|
||||
Comp->GetEyeAnimationCompensation());
|
||||
Comp->GetEyeAnimationCompensation(),
|
||||
Comp->GetBodyDriftCompensation());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -269,6 +273,50 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
||||
ResolveEyeBone(DefaultLeftEyeBone, LeftEyeBoneIndex, LeftEyeRefPoseRotation);
|
||||
ResolveEyeBone(DefaultRightEyeBone, RightEyeBoneIndex, RightEyeRefPoseRotation);
|
||||
}
|
||||
|
||||
// ── Resolve ancestor chain for body drift compensation ────────────
|
||||
// Walk from the parent of the first neck/head bone up to root.
|
||||
// This lets us measure how much the spine/torso animation has
|
||||
// shifted the coordinate frame at the neck.
|
||||
AncestorBoneIndices.Reset();
|
||||
RefAccumAboveChain = FQuat::Identity;
|
||||
{
|
||||
// Find the first bone of the posture chain (or fallback head bone)
|
||||
FCompactPoseBoneIndex FirstBone =
|
||||
(ChainBoneIndices.Num() > 0 && ChainBoneIndices[0].GetInt() != INDEX_NONE)
|
||||
? ChainBoneIndices[0]
|
||||
: HeadBoneIndex;
|
||||
|
||||
if (FirstBone.GetInt() != INDEX_NONE)
|
||||
{
|
||||
// Start from the parent of the first chain bone
|
||||
FCompactPoseBoneIndex Current = RequiredBones.GetParentBoneIndex(FirstBone);
|
||||
|
||||
while (Current.GetInt() != INDEX_NONE)
|
||||
{
|
||||
AncestorBoneIndices.Add(Current);
|
||||
Current = RequiredBones.GetParentBoneIndex(Current);
|
||||
}
|
||||
|
||||
// Compute accumulated ref-pose rotation from root to parent-of-chain.
|
||||
// AncestorBoneIndices is in child→root order, so we iterate in reverse
|
||||
// (root first) to accumulate: RefAccum = R_root * R_spine1 * ... * R_parent
|
||||
for (int32 i = AncestorBoneIndices.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
const int32 MeshIdx = RequiredBones.MakeMeshPoseIndex(
|
||||
AncestorBoneIndices[i]).GetInt();
|
||||
const FQuat RefRot = (MeshIdx >= 0 && MeshIdx < RefPose.Num())
|
||||
? RefPose[MeshIdx].GetRotation()
|
||||
: FQuat::Identity;
|
||||
RefAccumAboveChain = RefAccumAboveChain * RefRot;
|
||||
}
|
||||
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||
TEXT("Body drift: %d ancestor bones above chain. RefAccum=(%s)"),
|
||||
AncestorBoneIndices.Num(),
|
||||
*RefAccumAboveChain.Rotator().ToCompactString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,6 +336,7 @@ void FAnimNode_ElevenLabsPosture::Update_AnyThread(const FAnimationUpdateContext
|
||||
CachedHeadRotation = PostureComponent->GetCurrentHeadRotation();
|
||||
CachedHeadCompensation = PostureComponent->GetHeadAnimationCompensation();
|
||||
CachedEyeCompensation = PostureComponent->GetEyeAnimationCompensation();
|
||||
CachedBodyDriftCompensation = PostureComponent->GetBodyDriftCompensation();
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,15 +399,16 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
const bool bHasEyeBones = (LeftEyeBoneIndex.GetInt() != INDEX_NONE);
|
||||
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT("[%s] Posture Evaluate: HeadComp=%.2f EyeComp=%.2f Valid=%s HeadRot=(%s) Eyes=%d Chain=%d EyeBones=%s"),
|
||||
TEXT("[%s] Posture Evaluate: HeadComp=%.2f EyeComp=%.2f DriftComp=%.2f Valid=%s HeadRot=(%s) Eyes=%d Chain=%d Ancestors=%d"),
|
||||
NodeRole,
|
||||
CachedHeadCompensation,
|
||||
CachedEyeCompensation,
|
||||
CachedBodyDriftCompensation,
|
||||
PostureComponent.IsValid() ? TEXT("YES") : TEXT("NO"),
|
||||
*CachedHeadRotation.Rotator().ToCompactString(),
|
||||
CachedEyeCurves.Num(),
|
||||
ChainBoneIndices.Num(),
|
||||
bHasEyeBones ? TEXT("YES") : TEXT("NO"));
|
||||
AncestorBoneIndices.Num());
|
||||
|
||||
// Log first chain bone's anim vs ref delta (to see if compensation changes anything)
|
||||
if (bApplyHeadRotation && ChainBoneIndices.Num() > 0
|
||||
@ -590,20 +640,78 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
#else
|
||||
// ── PRODUCTION ───────────────────────────────────────────────────────
|
||||
|
||||
// ── Body drift compensation ─────────────────────────────────────────
|
||||
//
|
||||
// When the animation bends the torso (bow, lean, etc.), all bones above
|
||||
// the spine shift in world space. The posture rotation (CachedHeadRotation)
|
||||
// was computed relative to the character standing upright, so the head
|
||||
// drifts away from the target.
|
||||
//
|
||||
// Fix: measure how much the ancestor bones (root→parent-of-chain) have
|
||||
// rotated compared to their ref pose, and counter-rotate the posture.
|
||||
//
|
||||
// BodyDrift = AnimAccum * RefAccum⁻¹
|
||||
// AdjustedPosture = BodyDrift⁻¹ * Posture
|
||||
//
|
||||
// The BodyDriftCompensation parameter (0→1) controls how much of the
|
||||
// drift is cancelled. At 0 the head follows body movement naturally;
|
||||
// at 1 it stays locked on the target even during bows.
|
||||
|
||||
// ── Compute drift correction (applied AFTER swing-twist on first bone only) ──
|
||||
//
|
||||
// The drift correction transforms from animation-parent-space to ref-parent-space.
|
||||
// It must be applied only to the FIRST bone in the chain (subsequent bones'
|
||||
// parents are already corrected). It's applied AFTER swing-twist so the
|
||||
// pitch/roll components don't get stripped by the tilt decomposition.
|
||||
//
|
||||
// DriftCorrection = AnimAccum⁻¹ * RefAccum
|
||||
// BoneWorld = AnimAccum * DriftCorrection * CleanPosture = RefAccum * CleanPosture ✓
|
||||
|
||||
FQuat DriftCorrection = FQuat::Identity;
|
||||
|
||||
if (CachedBodyDriftCompensation > 0.001f && AncestorBoneIndices.Num() > 0)
|
||||
{
|
||||
FQuat AnimAccumAboveChain = FQuat::Identity;
|
||||
bool bAllValid = true;
|
||||
for (int32 i = AncestorBoneIndices.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
const FCompactPoseBoneIndex Idx = AncestorBoneIndices[i];
|
||||
if (Idx.GetInt() == INDEX_NONE || Idx.GetInt() >= Output.Pose.GetNumBones())
|
||||
{
|
||||
bAllValid = false;
|
||||
break;
|
||||
}
|
||||
AnimAccumAboveChain = AnimAccumAboveChain * Output.Pose[Idx].GetRotation();
|
||||
}
|
||||
|
||||
if (bAllValid)
|
||||
{
|
||||
const FQuat FullCorrection = AnimAccumAboveChain.Inverse() * RefAccumAboveChain;
|
||||
const FQuat SafeCorrection = EnforceShortestPath(FullCorrection, FQuat::Identity);
|
||||
DriftCorrection = FQuat::Slerp(FQuat::Identity, SafeCorrection,
|
||||
CachedBodyDriftCompensation);
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
if (EvalDebugFrameCounter % 300 == 1)
|
||||
{
|
||||
const FRotator CorrRot = FullCorrection.Rotator();
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT(" DriftCorrection: Y=%.1f P=%.1f R=%.1f | Comp=%.2f"),
|
||||
CorrRot.Yaw, CorrRot.Pitch, CorrRot.Roll,
|
||||
CachedBodyDriftCompensation);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (bUseChain)
|
||||
{
|
||||
// ── Multi-bone neck chain: per-bone swing-twist ──────────────────
|
||||
//
|
||||
// Each bone in the chain gets a fractional rotation (via Slerp weight).
|
||||
// The swing-twist decomposition is done PER-BONE using each bone's own
|
||||
// tilt axis. This prevents parasitic ear-to-shoulder tilt that occurred
|
||||
// when a single CleanRotation (derived from the tip bone) was applied
|
||||
// to bones with different local orientations.
|
||||
//
|
||||
// Animation compensation: before applying posture, we blend out the
|
||||
// animation's contribution proportionally to AnimationCompensation.
|
||||
// Comp=1.0 → posture replaces animation (head always faces target).
|
||||
// Comp=0.0 → posture is additive on top of animation (old behavior).
|
||||
// Posture (CachedHeadRotation) is distributed fractionally across bones.
|
||||
// Swing-twist removes parasitic roll per bone.
|
||||
// Drift correction is applied AFTER swing-twist on the FIRST bone only,
|
||||
// so it doesn't get stripped and doesn't compound through the chain.
|
||||
|
||||
for (int32 i = 0; i < ChainBoneIndices.Num(); ++i)
|
||||
{
|
||||
@ -621,8 +729,7 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
||||
AnimBoneRot, ChainRefPoseRotations[i], CachedHeadCompensation);
|
||||
|
||||
// Fractional posture rotation for this bone.
|
||||
// Enforce shortest-path to prevent sign-flip jumps in Slerp.
|
||||
// Fractional posture rotation (NO drift — drift applied separately)
|
||||
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, FQuat::Identity);
|
||||
const FQuat FractionalRot = FQuat::Slerp(
|
||||
FQuat::Identity, SafeHeadRot, ChainBoneWeights[i]);
|
||||
@ -635,7 +742,15 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
FQuat Swing, TiltTwist;
|
||||
Combined.ToSwingTwist(BoneTiltAxis, Swing, TiltTwist);
|
||||
|
||||
BoneTransform.SetRotation(Swing.GetNormalized());
|
||||
FQuat FinalRot = Swing.GetNormalized();
|
||||
|
||||
// Drift correction: only on the first bone, AFTER swing-twist
|
||||
if (i == 0)
|
||||
{
|
||||
FinalRot = (DriftCorrection * FinalRot).GetNormalized();
|
||||
}
|
||||
|
||||
BoneTransform.SetRotation(FinalRot);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -651,17 +766,19 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
||||
AnimBoneRot, HeadRefPoseRotation, CachedHeadCompensation);
|
||||
|
||||
// Apply posture on compensated rotation.
|
||||
// Enforce shortest-path to prevent sign-flip jumps.
|
||||
// Apply posture (NO drift)
|
||||
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, CompensatedRot);
|
||||
const FQuat Combined = SafeHeadRot * CompensatedRot;
|
||||
|
||||
// Remove ear-to-shoulder tilt using the bone's actual tilt axis
|
||||
// Remove ear-to-shoulder tilt
|
||||
const FVector BoneTiltAxis = CompensatedRot.RotateVector(FVector::RightVector);
|
||||
FQuat Swing, TiltTwist;
|
||||
Combined.ToSwingTwist(BoneTiltAxis, Swing, TiltTwist);
|
||||
|
||||
HeadTransform.SetRotation(Swing.GetNormalized());
|
||||
// Drift correction AFTER swing-twist
|
||||
const FQuat FinalRot = (DriftCorrection * Swing).GetNormalized();
|
||||
|
||||
HeadTransform.SetRotation(FinalRot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -671,12 +788,13 @@ 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, chain: %d bones, headComp: %.1f, eyeComp: %.1f)"),
|
||||
TEXT("ElevenLabs Posture (eyes: %d, head: Y=%.1f P=%.1f, chain: %d, headComp: %.1f, eyeComp: %.1f, driftComp: %.1f)"),
|
||||
CachedEyeCurves.Num(),
|
||||
DebugRot.Yaw, DebugRot.Pitch,
|
||||
ChainBoneIndices.Num(),
|
||||
CachedHeadCompensation,
|
||||
CachedEyeCompensation);
|
||||
CachedEyeCompensation,
|
||||
CachedBodyDriftCompensation);
|
||||
DebugData.AddDebugItem(DebugLine);
|
||||
BasePose.GatherDebugData(DebugData);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Math/UnrealMathUtility.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogElevenLabsPosture);
|
||||
|
||||
@ -22,8 +23,8 @@ static const FName EyeLookOutRight(TEXT("eyeLookOutRight"));
|
||||
// value 0→1. MaxEyeHorizontal/Vertical control the CASCADE threshold (when
|
||||
// the head kicks in), but the visual eye deflection is always normalized by
|
||||
// these fixed constants so the eye curves look correct at any threshold.
|
||||
static constexpr float ARKitEyeRangeHorizontal = 30.0f;
|
||||
static constexpr float ARKitEyeRangeVertical = 20.0f;
|
||||
static constexpr float ARKitEyeRangeHorizontal = 40.0f;
|
||||
static constexpr float ARKitEyeRangeVertical = 35.0f;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Construction
|
||||
@ -56,14 +57,39 @@ void UElevenLabsPostureComponent::BeginPlay()
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache skeletal mesh for head bone queries
|
||||
CachedMesh = Owner->FindComponentByClass<USkeletalMeshComponent>();
|
||||
// Cache skeletal mesh components: Body (first found) + Face (has FACIAL_L_Eye bone)
|
||||
{
|
||||
static const FName LEyeBone(TEXT("FACIAL_L_Eye"));
|
||||
TArray<USkeletalMeshComponent*> AllSkelMeshes;
|
||||
Owner->GetComponents<USkeletalMeshComponent>(AllSkelMeshes);
|
||||
|
||||
for (USkeletalMeshComponent* SMC : AllSkelMeshes)
|
||||
{
|
||||
if (!SMC) continue;
|
||||
|
||||
// First valid mesh → Body
|
||||
if (!CachedMesh.IsValid())
|
||||
{
|
||||
CachedMesh = SMC;
|
||||
}
|
||||
|
||||
// The mesh whose anim instance drives FACIAL_ bones → Face
|
||||
if (!CachedFaceMesh.IsValid() && SMC->DoesSocketExist(LEyeBone) && SMC != CachedMesh.Get())
|
||||
{
|
||||
CachedFaceMesh = SMC;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!CachedMesh.IsValid())
|
||||
{
|
||||
UE_LOG(LogElevenLabsPosture, Warning,
|
||||
TEXT("No SkeletalMeshComponent found on %s — head bone lookup will be unavailable."),
|
||||
*Owner->GetName());
|
||||
}
|
||||
UE_LOG(LogElevenLabsPosture, Log,
|
||||
TEXT("Mesh cache: Body=%s Face=%s"),
|
||||
CachedMesh.IsValid() ? *CachedMesh->GetName() : TEXT("NONE"),
|
||||
CachedFaceMesh.IsValid() ? *CachedFaceMesh->GetName() : TEXT("NONE"));
|
||||
|
||||
// Remember original actor facing for neutral reference.
|
||||
// Apply the mesh forward offset so "neutral" aligns with where the face points.
|
||||
@ -171,8 +197,20 @@ void UElevenLabsPostureComponent::TickComponent(
|
||||
|
||||
const FVector TargetPos = TargetActor->GetActorLocation() + TargetOffset;
|
||||
|
||||
// Eye origin = midpoint of FACIAL_L_Eye / FACIAL_R_Eye on the Face mesh
|
||||
// (most accurate for pitch calculation). Falls back to head bone, then actor.
|
||||
static const FName LEyeName(TEXT("FACIAL_L_Eye"));
|
||||
static const FName REyeName(TEXT("FACIAL_R_Eye"));
|
||||
|
||||
FVector EyeOrigin;
|
||||
if (CachedMesh.IsValid() && CachedMesh->DoesSocketExist(HeadBoneName))
|
||||
if (CachedFaceMesh.IsValid()
|
||||
&& CachedFaceMesh->DoesSocketExist(LEyeName)
|
||||
&& CachedFaceMesh->DoesSocketExist(REyeName))
|
||||
{
|
||||
EyeOrigin = (CachedFaceMesh->GetSocketLocation(LEyeName)
|
||||
+ CachedFaceMesh->GetSocketLocation(REyeName)) * 0.5f;
|
||||
}
|
||||
else if (CachedMesh.IsValid() && CachedMesh->DoesSocketExist(HeadBoneName))
|
||||
{
|
||||
EyeOrigin = CachedMesh->GetSocketLocation(HeadBoneName);
|
||||
}
|
||||
@ -331,6 +369,70 @@ void UElevenLabsPostureComponent::TickComponent(
|
||||
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);
|
||||
}
|
||||
|
||||
// ── Debug gaze lines ────────────────────────────────────────────────────
|
||||
// Per-eye debug: each eye gets two thin lines originating from its bone:
|
||||
// Cyan = desired direction (eye bone → target)
|
||||
// Green = actual eye bone forward (real rendered gaze from Face mesh)
|
||||
// Uses CachedFaceMesh for correct animated transforms of FACIAL_ bones.
|
||||
if (bDrawDebugGaze && TargetActor && GetWorld())
|
||||
{
|
||||
static const FName LeftEyeBone(TEXT("FACIAL_L_Eye"));
|
||||
static const FName RightEyeBone(TEXT("FACIAL_R_Eye"));
|
||||
|
||||
// Prefer Face mesh (where FACIAL_ bones are actually animated)
|
||||
USkeletalMeshComponent* EyeMesh = CachedFaceMesh.IsValid()
|
||||
? CachedFaceMesh.Get()
|
||||
: (CachedMesh.IsValid() ? CachedMesh.Get() : nullptr);
|
||||
|
||||
const FVector TargetPos = TargetActor->GetActorLocation() + TargetOffset;
|
||||
|
||||
if (EyeMesh
|
||||
&& EyeMesh->DoesSocketExist(LeftEyeBone)
|
||||
&& EyeMesh->DoesSocketExist(RightEyeBone))
|
||||
{
|
||||
const FTransform LeftEyeT = EyeMesh->GetSocketTransform(LeftEyeBone);
|
||||
const FTransform RightEyeT = EyeMesh->GetSocketTransform(RightEyeBone);
|
||||
|
||||
const FVector LeftEyePos = LeftEyeT.GetLocation();
|
||||
const FVector RightEyePos = RightEyeT.GetLocation();
|
||||
|
||||
// Half eye-spacing offset: keep cyan lines parallel (same gap as origin)
|
||||
const FVector EyeOffset = (LeftEyePos - RightEyePos) * 0.5f;
|
||||
const FVector MidPoint = (LeftEyePos + RightEyePos) * 0.5f;
|
||||
const FVector MidToTarget = TargetPos - MidPoint;
|
||||
const float LineLen = MidToTarget.Size();
|
||||
|
||||
// Desired direction from midpoint, offset per eye
|
||||
const FVector LeftTargetPos = TargetPos + EyeOffset;
|
||||
const FVector RightTargetPos = TargetPos - EyeOffset;
|
||||
|
||||
// ── Left eye ──
|
||||
const FVector LeftGaze = LeftEyeT.GetRotation().GetAxisZ();
|
||||
DrawDebugLine(GetWorld(), LeftEyePos, LeftTargetPos,
|
||||
FColor::Cyan, false, -1.0f, 0, 0.1f);
|
||||
DrawDebugLine(GetWorld(), LeftEyePos, LeftEyePos + LeftGaze * LineLen,
|
||||
FColor::Green, false, -1.0f, 0, 0.1f);
|
||||
|
||||
// ── Right eye ──
|
||||
const FVector RightGaze = RightEyeT.GetRotation().GetAxisZ();
|
||||
DrawDebugLine(GetWorld(), RightEyePos, RightTargetPos,
|
||||
FColor::Cyan, false, -1.0f, 0, 0.1f);
|
||||
DrawDebugLine(GetWorld(), RightEyePos, RightEyePos + RightGaze * LineLen,
|
||||
FColor::Green, false, -1.0f, 0, 0.1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: single line from head bone
|
||||
FVector HeadPos = Owner->GetActorLocation() + FVector(0.0f, 0.0f, 160.0f);
|
||||
if (CachedMesh.IsValid() && CachedMesh->DoesSocketExist(HeadBoneName))
|
||||
{
|
||||
HeadPos = CachedMesh->GetSocketLocation(HeadBoneName);
|
||||
}
|
||||
DrawDebugLine(GetWorld(), HeadPos, TargetPos,
|
||||
FColor::Cyan, false, -1.0f, 0, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Debug (every ~2 seconds) ─────────────────────────────────────────
|
||||
#if !UE_BUILD_SHIPPING
|
||||
DebugFrameCounter++;
|
||||
|
||||
@ -106,6 +106,25 @@ private:
|
||||
FQuat LeftEyeRefPoseRotation = FQuat::Identity;
|
||||
FQuat RightEyeRefPoseRotation = FQuat::Identity;
|
||||
|
||||
// ── Body drift compensation ─────────────────────────────────────────────
|
||||
//
|
||||
// When the animation bends the torso (bow, lean, etc.), all bones above
|
||||
// the spine shift in world space. The posture rotation is calculated
|
||||
// relative to the character standing upright, so without compensation
|
||||
// the head drifts away from the target.
|
||||
//
|
||||
// We walk from the parent of the first chain bone up to root, accumulate
|
||||
// the animated vs ref-pose rotation delta ("body drift"), and pre-rotate
|
||||
// the posture to cancel it out.
|
||||
|
||||
/** Ancestor bone indices from parent-of-chain to root (child→root order).
|
||||
* Resolved at CacheBones; used in Evaluate to compute animated drift. */
|
||||
TArray<FCompactPoseBoneIndex> AncestorBoneIndices;
|
||||
|
||||
/** Precomputed accumulated ref-pose rotation from root to parent-of-chain.
|
||||
* Static — computed once at CacheBones. */
|
||||
FQuat RefAccumAboveChain = FQuat::Identity;
|
||||
|
||||
// ── Animation compensation (separate head and eye) ─────────────────────
|
||||
|
||||
/** How much posture overrides the animation's head/neck rotation (0=additive, 1=override).
|
||||
@ -116,6 +135,10 @@ private:
|
||||
* Cached from the component during Update. */
|
||||
float CachedEyeCompensation = 1.0f;
|
||||
|
||||
/** How much body drift is compensated (0=no compensation, 1=full).
|
||||
* Cached from the component during Update. */
|
||||
float CachedBodyDriftCompensation = 0.0f;
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
/** Frame counter for periodic diagnostic logging in Evaluate. */
|
||||
int32 EvalDebugFrameCounter = 0;
|
||||
|
||||
@ -153,6 +153,26 @@ public:
|
||||
meta = (ClampMin = "0", ClampMax = "1"))
|
||||
float EyeAnimationCompensation = 1.0f;
|
||||
|
||||
/** Compensate body animation below the neck (spine bending, leaning, etc.).
|
||||
* When the torso bends, the head's world orientation shifts. This
|
||||
* counter-rotates the posture to keep the head pointing at the target.
|
||||
* 1.0 = full compensation — head stays locked on target even during bows.
|
||||
* 0.0 = no compensation — head follows body movement (default).
|
||||
* Start low (0.3–0.5) and increase as needed; full compensation can
|
||||
* look unnatural on large body movements. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture",
|
||||
meta = (ClampMin = "0", ClampMax = "1"))
|
||||
float BodyDriftCompensation = 0.0f;
|
||||
|
||||
// ── Debug ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Draw debug lines showing gaze direction in the viewport.
|
||||
* Green = head bone → target (desired).
|
||||
* Cyan = computed gaze direction (body yaw + head + eyes).
|
||||
* Useful for verifying that eye contact is accurate. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture")
|
||||
bool bDrawDebugGaze = false;
|
||||
|
||||
// ── Forward offset ──────────────────────────────────────────────────────
|
||||
|
||||
/** Yaw offset (degrees) between the actor's forward (+X) and the mesh's
|
||||
@ -211,6 +231,9 @@ public:
|
||||
/** Get eye animation compensation factor (0 = additive, 1 = full override). */
|
||||
float GetEyeAnimationCompensation() const { return EyeAnimationCompensation; }
|
||||
|
||||
/** Get body drift compensation factor (0 = none, 1 = full). */
|
||||
float GetBodyDriftCompensation() const { return BodyDriftCompensation; }
|
||||
|
||||
// ── UActorComponent overrides ────────────────────────────────────────────
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
@ -257,9 +280,12 @@ private:
|
||||
/** Head bone rotation offset as quaternion (no Euler round-trip). */
|
||||
FQuat CurrentHeadRotation = FQuat::Identity;
|
||||
|
||||
/** Cached skeletal mesh component on the owning actor. */
|
||||
/** Cached skeletal mesh component on the owning actor (Body). */
|
||||
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;
|
||||
|
||||
/** Cached Face skeletal mesh component (for eye bone transforms). */
|
||||
TWeakObjectPtr<USkeletalMeshComponent> CachedFaceMesh;
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
/** Frame counter for periodic debug logging. */
|
||||
int32 DebugFrameCounter = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user