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;
|
CachedHeadRotation = FQuat::Identity;
|
||||||
CachedHeadCompensation = 1.0f;
|
CachedHeadCompensation = 1.0f;
|
||||||
CachedEyeCompensation = 1.0f;
|
CachedEyeCompensation = 1.0f;
|
||||||
|
CachedBodyDriftCompensation = 0.0f;
|
||||||
|
AncestorBoneIndices.Reset();
|
||||||
|
RefAccumAboveChain = FQuat::Identity;
|
||||||
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
HeadRefPoseRotation = FQuat::Identity;
|
HeadRefPoseRotation = FQuat::Identity;
|
||||||
ChainBoneNames.Reset();
|
ChainBoneNames.Reset();
|
||||||
@ -124,13 +127,14 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
|||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
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(),
|
*Owner->GetName(), *SkelMesh->GetName(),
|
||||||
bApplyHeadRotation ? TEXT("ON") : TEXT("OFF"),
|
bApplyHeadRotation ? TEXT("ON") : TEXT("OFF"),
|
||||||
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"),
|
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"),
|
||||||
ChainBoneNames.Num(),
|
ChainBoneNames.Num(),
|
||||||
Comp->GetHeadAnimationCompensation(),
|
Comp->GetHeadAnimationCompensation(),
|
||||||
Comp->GetEyeAnimationCompensation());
|
Comp->GetEyeAnimationCompensation(),
|
||||||
|
Comp->GetBodyDriftCompensation());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -269,6 +273,50 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
|||||||
ResolveEyeBone(DefaultLeftEyeBone, LeftEyeBoneIndex, LeftEyeRefPoseRotation);
|
ResolveEyeBone(DefaultLeftEyeBone, LeftEyeBoneIndex, LeftEyeRefPoseRotation);
|
||||||
ResolveEyeBone(DefaultRightEyeBone, RightEyeBoneIndex, RightEyeRefPoseRotation);
|
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();
|
CachedHeadRotation = PostureComponent->GetCurrentHeadRotation();
|
||||||
CachedHeadCompensation = PostureComponent->GetHeadAnimationCompensation();
|
CachedHeadCompensation = PostureComponent->GetHeadAnimationCompensation();
|
||||||
CachedEyeCompensation = PostureComponent->GetEyeAnimationCompensation();
|
CachedEyeCompensation = PostureComponent->GetEyeAnimationCompensation();
|
||||||
|
CachedBodyDriftCompensation = PostureComponent->GetBodyDriftCompensation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,15 +399,16 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
const bool bHasEyeBones = (LeftEyeBoneIndex.GetInt() != INDEX_NONE);
|
const bool bHasEyeBones = (LeftEyeBoneIndex.GetInt() != INDEX_NONE);
|
||||||
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
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,
|
NodeRole,
|
||||||
CachedHeadCompensation,
|
CachedHeadCompensation,
|
||||||
CachedEyeCompensation,
|
CachedEyeCompensation,
|
||||||
|
CachedBodyDriftCompensation,
|
||||||
PostureComponent.IsValid() ? TEXT("YES") : TEXT("NO"),
|
PostureComponent.IsValid() ? TEXT("YES") : TEXT("NO"),
|
||||||
*CachedHeadRotation.Rotator().ToCompactString(),
|
*CachedHeadRotation.Rotator().ToCompactString(),
|
||||||
CachedEyeCurves.Num(),
|
CachedEyeCurves.Num(),
|
||||||
ChainBoneIndices.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)
|
// Log first chain bone's anim vs ref delta (to see if compensation changes anything)
|
||||||
if (bApplyHeadRotation && ChainBoneIndices.Num() > 0
|
if (bApplyHeadRotation && ChainBoneIndices.Num() > 0
|
||||||
@ -590,20 +640,78 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
#else
|
#else
|
||||||
// ── PRODUCTION ───────────────────────────────────────────────────────
|
// ── 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)
|
if (bUseChain)
|
||||||
{
|
{
|
||||||
// ── Multi-bone neck chain: per-bone swing-twist ──────────────────
|
// ── Multi-bone neck chain: per-bone swing-twist ──────────────────
|
||||||
//
|
//
|
||||||
// Each bone in the chain gets a fractional rotation (via Slerp weight).
|
// Posture (CachedHeadRotation) is distributed fractionally across bones.
|
||||||
// The swing-twist decomposition is done PER-BONE using each bone's own
|
// Swing-twist removes parasitic roll per bone.
|
||||||
// tilt axis. This prevents parasitic ear-to-shoulder tilt that occurred
|
// Drift correction is applied AFTER swing-twist on the FIRST bone only,
|
||||||
// when a single CleanRotation (derived from the tip bone) was applied
|
// so it doesn't get stripped and doesn't compound through the chain.
|
||||||
// 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).
|
|
||||||
|
|
||||||
for (int32 i = 0; i < ChainBoneIndices.Num(); ++i)
|
for (int32 i = 0; i < ChainBoneIndices.Num(); ++i)
|
||||||
{
|
{
|
||||||
@ -621,8 +729,7 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
||||||
AnimBoneRot, ChainRefPoseRotations[i], CachedHeadCompensation);
|
AnimBoneRot, ChainRefPoseRotations[i], CachedHeadCompensation);
|
||||||
|
|
||||||
// Fractional posture rotation for this bone.
|
// Fractional posture rotation (NO drift — drift applied separately)
|
||||||
// Enforce shortest-path to prevent sign-flip jumps in Slerp.
|
|
||||||
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, FQuat::Identity);
|
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, FQuat::Identity);
|
||||||
const FQuat FractionalRot = FQuat::Slerp(
|
const FQuat FractionalRot = FQuat::Slerp(
|
||||||
FQuat::Identity, SafeHeadRot, ChainBoneWeights[i]);
|
FQuat::Identity, SafeHeadRot, ChainBoneWeights[i]);
|
||||||
@ -635,7 +742,15 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
FQuat Swing, TiltTwist;
|
FQuat Swing, TiltTwist;
|
||||||
Combined.ToSwingTwist(BoneTiltAxis, 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
|
else
|
||||||
@ -651,17 +766,19 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
const FQuat CompensatedRot = ComputeCompensatedBoneRot(
|
||||||
AnimBoneRot, HeadRefPoseRotation, CachedHeadCompensation);
|
AnimBoneRot, HeadRefPoseRotation, CachedHeadCompensation);
|
||||||
|
|
||||||
// Apply posture on compensated rotation.
|
// Apply posture (NO drift)
|
||||||
// Enforce shortest-path to prevent sign-flip jumps.
|
|
||||||
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, CompensatedRot);
|
const FQuat SafeHeadRot = EnforceShortestPath(CachedHeadRotation, CompensatedRot);
|
||||||
const FQuat Combined = SafeHeadRot * 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);
|
const FVector BoneTiltAxis = CompensatedRot.RotateVector(FVector::RightVector);
|
||||||
FQuat Swing, TiltTwist;
|
FQuat Swing, TiltTwist;
|
||||||
Combined.ToSwingTwist(BoneTiltAxis, 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
|
#endif
|
||||||
@ -671,12 +788,13 @@ void FAnimNode_ElevenLabsPosture::GatherDebugData(FNodeDebugData& DebugData)
|
|||||||
{
|
{
|
||||||
const FRotator DebugRot = CachedHeadRotation.Rotator();
|
const FRotator DebugRot = CachedHeadRotation.Rotator();
|
||||||
FString DebugLine = FString::Printf(
|
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(),
|
CachedEyeCurves.Num(),
|
||||||
DebugRot.Yaw, DebugRot.Pitch,
|
DebugRot.Yaw, DebugRot.Pitch,
|
||||||
ChainBoneIndices.Num(),
|
ChainBoneIndices.Num(),
|
||||||
CachedHeadCompensation,
|
CachedHeadCompensation,
|
||||||
CachedEyeCompensation);
|
CachedEyeCompensation,
|
||||||
|
CachedBodyDriftCompensation);
|
||||||
DebugData.AddDebugItem(DebugLine);
|
DebugData.AddDebugItem(DebugLine);
|
||||||
BasePose.GatherDebugData(DebugData);
|
BasePose.GatherDebugData(DebugData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include "Components/SkeletalMeshComponent.h"
|
#include "Components/SkeletalMeshComponent.h"
|
||||||
#include "GameFramework/Actor.h"
|
#include "GameFramework/Actor.h"
|
||||||
#include "Math/UnrealMathUtility.h"
|
#include "Math/UnrealMathUtility.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY(LogElevenLabsPosture);
|
DEFINE_LOG_CATEGORY(LogElevenLabsPosture);
|
||||||
|
|
||||||
@ -22,8 +23,8 @@ static const FName EyeLookOutRight(TEXT("eyeLookOutRight"));
|
|||||||
// value 0→1. MaxEyeHorizontal/Vertical control the CASCADE threshold (when
|
// value 0→1. MaxEyeHorizontal/Vertical control the CASCADE threshold (when
|
||||||
// the head kicks in), but the visual eye deflection is always normalized by
|
// 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.
|
// these fixed constants so the eye curves look correct at any threshold.
|
||||||
static constexpr float ARKitEyeRangeHorizontal = 30.0f;
|
static constexpr float ARKitEyeRangeHorizontal = 40.0f;
|
||||||
static constexpr float ARKitEyeRangeVertical = 20.0f;
|
static constexpr float ARKitEyeRangeVertical = 35.0f;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Construction
|
// Construction
|
||||||
@ -56,14 +57,39 @@ void UElevenLabsPostureComponent::BeginPlay()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache skeletal mesh for head bone queries
|
// Cache skeletal mesh components: Body (first found) + Face (has FACIAL_L_Eye bone)
|
||||||
CachedMesh = Owner->FindComponentByClass<USkeletalMeshComponent>();
|
{
|
||||||
|
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())
|
if (!CachedMesh.IsValid())
|
||||||
{
|
{
|
||||||
UE_LOG(LogElevenLabsPosture, Warning,
|
UE_LOG(LogElevenLabsPosture, Warning,
|
||||||
TEXT("No SkeletalMeshComponent found on %s — head bone lookup will be unavailable."),
|
TEXT("No SkeletalMeshComponent found on %s — head bone lookup will be unavailable."),
|
||||||
*Owner->GetName());
|
*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.
|
// Remember original actor facing for neutral reference.
|
||||||
// Apply the mesh forward offset so "neutral" aligns with where the face points.
|
// 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;
|
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;
|
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);
|
EyeOrigin = CachedMesh->GetSocketLocation(HeadBoneName);
|
||||||
}
|
}
|
||||||
@ -331,6 +369,70 @@ void UElevenLabsPostureComponent::TickComponent(
|
|||||||
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);
|
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) ─────────────────────────────────────────
|
// ── Debug (every ~2 seconds) ─────────────────────────────────────────
|
||||||
#if !UE_BUILD_SHIPPING
|
#if !UE_BUILD_SHIPPING
|
||||||
DebugFrameCounter++;
|
DebugFrameCounter++;
|
||||||
|
|||||||
@ -106,6 +106,25 @@ private:
|
|||||||
FQuat LeftEyeRefPoseRotation = FQuat::Identity;
|
FQuat LeftEyeRefPoseRotation = FQuat::Identity;
|
||||||
FQuat RightEyeRefPoseRotation = 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) ─────────────────────
|
// ── Animation compensation (separate head and eye) ─────────────────────
|
||||||
|
|
||||||
/** How much posture overrides the animation's head/neck rotation (0=additive, 1=override).
|
/** 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. */
|
* Cached from the component during Update. */
|
||||||
float CachedEyeCompensation = 1.0f;
|
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
|
#if !UE_BUILD_SHIPPING
|
||||||
/** Frame counter for periodic diagnostic logging in Evaluate. */
|
/** Frame counter for periodic diagnostic logging in Evaluate. */
|
||||||
int32 EvalDebugFrameCounter = 0;
|
int32 EvalDebugFrameCounter = 0;
|
||||||
|
|||||||
@ -153,6 +153,26 @@ public:
|
|||||||
meta = (ClampMin = "0", ClampMax = "1"))
|
meta = (ClampMin = "0", ClampMax = "1"))
|
||||||
float EyeAnimationCompensation = 1.0f;
|
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 ──────────────────────────────────────────────────────
|
// ── Forward offset ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Yaw offset (degrees) between the actor's forward (+X) and the mesh's
|
/** 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). */
|
/** Get eye animation compensation factor (0 = additive, 1 = full override). */
|
||||||
float GetEyeAnimationCompensation() const { return EyeAnimationCompensation; }
|
float GetEyeAnimationCompensation() const { return EyeAnimationCompensation; }
|
||||||
|
|
||||||
|
/** Get body drift compensation factor (0 = none, 1 = full). */
|
||||||
|
float GetBodyDriftCompensation() const { return BodyDriftCompensation; }
|
||||||
|
|
||||||
// ── UActorComponent overrides ────────────────────────────────────────────
|
// ── UActorComponent overrides ────────────────────────────────────────────
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||||
@ -257,9 +280,12 @@ private:
|
|||||||
/** Head bone rotation offset as quaternion (no Euler round-trip). */
|
/** Head bone rotation offset as quaternion (no Euler round-trip). */
|
||||||
FQuat CurrentHeadRotation = FQuat::Identity;
|
FQuat CurrentHeadRotation = FQuat::Identity;
|
||||||
|
|
||||||
/** Cached skeletal mesh component on the owning actor. */
|
/** Cached skeletal mesh component on the owning actor (Body). */
|
||||||
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;
|
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;
|
||||||
|
|
||||||
|
/** Cached Face skeletal mesh component (for eye bone transforms). */
|
||||||
|
TWeakObjectPtr<USkeletalMeshComponent> CachedFaceMesh;
|
||||||
|
|
||||||
#if !UE_BUILD_SHIPPING
|
#if !UE_BUILD_SHIPPING
|
||||||
/** Frame counter for periodic debug logging. */
|
/** Frame counter for periodic debug logging. */
|
||||||
int32 DebugFrameCounter = 0;
|
int32 DebugFrameCounter = 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user