Posture: neck bone chain distribution + swing-twist axis fix + log cleanup
- Add NeckBoneChain config (FElevenLabsNeckBoneEntry USTRUCT) to distribute head rotation across multiple bones (e.g. neck_01=0.25, neck_02=0.35, head=0.40) for more natural neck arc movement. - Swing-twist now uses bone's actual tilt axis (BoneRot.RotateVector) instead of hardcoded FVector::RightVector — accounts for MetaHuman ~11.5° reference pose rotation on head bone. - Clean up diagnostic logging: reduced to Log level, removed verbose per-frame quaternion dumps. - Backward compatible: empty NeckBoneChain falls back to single HeadBoneName. KNOWN ISSUE: diagonal tilt is more visible with neck chain — needs investigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3abc420cf
commit
8df6967ee5
Binary file not shown.
@ -25,11 +25,14 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
|||||||
{
|
{
|
||||||
BasePose.Initialize(Context);
|
BasePose.Initialize(Context);
|
||||||
|
|
||||||
// Find the ElevenLabsPostureComponent on the owning actor.
|
// Reset all cached state.
|
||||||
PostureComponent.Reset();
|
PostureComponent.Reset();
|
||||||
CachedEyeCurves.Reset();
|
CachedEyeCurves.Reset();
|
||||||
CachedHeadRotation = FQuat::Identity;
|
CachedHeadRotation = FQuat::Identity;
|
||||||
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
|
ChainBoneNames.Reset();
|
||||||
|
ChainBoneWeights.Reset();
|
||||||
|
ChainBoneIndices.Reset();
|
||||||
|
|
||||||
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
|
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
|
||||||
{
|
{
|
||||||
@ -44,21 +47,28 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
|||||||
PostureComponent = Comp;
|
PostureComponent = Comp;
|
||||||
HeadBoneName = Comp->GetHeadBoneName();
|
HeadBoneName = Comp->GetHeadBoneName();
|
||||||
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
// Cache neck bone chain configuration
|
||||||
TEXT("=== ElevenLabs Posture AnimNode ==="));
|
const TArray<FElevenLabsNeckBoneEntry>& Chain = Comp->GetNeckBoneChain();
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
if (Chain.Num() > 0)
|
||||||
TEXT(" Owner: %s | Mesh: %s"),
|
{
|
||||||
*Owner->GetName(), *SkelMesh->GetName());
|
ChainBoneNames.Reserve(Chain.Num());
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
ChainBoneWeights.Reserve(Chain.Num());
|
||||||
TEXT(" HeadRotation: %s | EyeCurves: %s"),
|
for (const FElevenLabsNeckBoneEntry& Entry : Chain)
|
||||||
|
{
|
||||||
|
if (!Entry.BoneName.IsNone() && Entry.Weight > 0.0f)
|
||||||
|
{
|
||||||
|
ChainBoneNames.Add(Entry.BoneName);
|
||||||
|
ChainBoneWeights.Add(Entry.Weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||||
|
TEXT("ElevenLabs Posture AnimNode: Owner=%s Mesh=%s Head=%s Eyes=%s Chain=%d"),
|
||||||
|
*Owner->GetName(), *SkelMesh->GetName(),
|
||||||
bApplyHeadRotation ? TEXT("ON") : TEXT("OFF"),
|
bApplyHeadRotation ? TEXT("ON") : TEXT("OFF"),
|
||||||
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"));
|
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"),
|
||||||
#if ELEVENLABS_AXIS_DIAGNOSTIC
|
ChainBoneNames.Num());
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
|
||||||
TEXT(" >>> AXIS DIAGNOSTIC MODE ACTIVE <<<"));
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
|
||||||
TEXT(" Phase 0 (0-3s): Pitch=20 | Phase 1 (3-6s): Yaw=20 | Phase 2 (6-9s): Roll=20"));
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -75,48 +85,56 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
|||||||
{
|
{
|
||||||
BasePose.CacheBones(Context);
|
BasePose.CacheBones(Context);
|
||||||
|
|
||||||
// Resolve head bone index from the skeleton
|
// Reset all bone indices
|
||||||
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
|
ChainBoneIndices.Reset();
|
||||||
|
|
||||||
if (!HeadBoneName.IsNone())
|
|
||||||
{
|
|
||||||
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
|
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
|
||||||
{
|
{
|
||||||
const FBoneContainer& RequiredBones = Proxy->GetRequiredBones();
|
const FBoneContainer& RequiredBones = Proxy->GetRequiredBones();
|
||||||
const FReferenceSkeleton& RefSkeleton = RequiredBones.GetReferenceSkeleton();
|
const FReferenceSkeleton& RefSkeleton = RequiredBones.GetReferenceSkeleton();
|
||||||
const int32 MeshIndex = RefSkeleton.FindBoneIndex(HeadBoneName);
|
|
||||||
|
|
||||||
|
// ── Resolve neck bone chain ──────────────────────────────────────
|
||||||
|
if (ChainBoneNames.Num() > 0)
|
||||||
|
{
|
||||||
|
ChainBoneIndices.Reserve(ChainBoneNames.Num());
|
||||||
|
for (int32 i = 0; i < ChainBoneNames.Num(); ++i)
|
||||||
|
{
|
||||||
|
const int32 MeshIndex = RefSkeleton.FindBoneIndex(ChainBoneNames[i]);
|
||||||
|
if (MeshIndex != INDEX_NONE)
|
||||||
|
{
|
||||||
|
const FCompactPoseBoneIndex CompactIdx =
|
||||||
|
RequiredBones.MakeCompactPoseIndex(FMeshPoseBoneIndex(MeshIndex));
|
||||||
|
ChainBoneIndices.Add(CompactIdx);
|
||||||
|
|
||||||
|
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||||
|
TEXT(" Chain bone [%d] '%s' → index %d (weight=%.2f)"),
|
||||||
|
i, *ChainBoneNames[i].ToString(), CompactIdx.GetInt(),
|
||||||
|
ChainBoneWeights[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Bone not found — placeholder to keep arrays parallel
|
||||||
|
ChainBoneIndices.Add(FCompactPoseBoneIndex(INDEX_NONE));
|
||||||
|
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||||
|
TEXT(" Chain bone [%d] '%s' NOT FOUND in skeleton!"),
|
||||||
|
i, *ChainBoneNames[i].ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resolve fallback single head bone ────────────────────────────
|
||||||
|
if (!HeadBoneName.IsNone())
|
||||||
|
{
|
||||||
|
const int32 MeshIndex = RefSkeleton.FindBoneIndex(HeadBoneName);
|
||||||
if (MeshIndex != INDEX_NONE)
|
if (MeshIndex != INDEX_NONE)
|
||||||
{
|
{
|
||||||
HeadBoneIndex = RequiredBones.MakeCompactPoseIndex(
|
HeadBoneIndex = RequiredBones.MakeCompactPoseIndex(
|
||||||
FMeshPoseBoneIndex(MeshIndex));
|
FMeshPoseBoneIndex(MeshIndex));
|
||||||
|
|
||||||
// Log reference pose rotation for diagnostic
|
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||||
const TArray<FTransform>& RefPose = RefSkeleton.GetRefBonePose();
|
TEXT("Head bone '%s' resolved to index %d."),
|
||||||
if (MeshIndex < RefPose.Num())
|
*HeadBoneName.ToString(), HeadBoneIndex.GetInt());
|
||||||
{
|
|
||||||
const FQuat RefRot = RefPose[MeshIndex].GetRotation();
|
|
||||||
const FRotator RefEuler = RefRot.Rotator();
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
|
||||||
TEXT("Head bone '%s' index=%d | RefPose rotation: P=%.2f Y=%.2f R=%.2f"),
|
|
||||||
*HeadBoneName.ToString(), HeadBoneIndex.GetInt(),
|
|
||||||
RefEuler.Pitch, RefEuler.Yaw, RefEuler.Roll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also log parent bone info
|
|
||||||
const int32 ParentMeshIdx = RefSkeleton.GetParentIndex(MeshIndex);
|
|
||||||
if (ParentMeshIdx != INDEX_NONE)
|
|
||||||
{
|
|
||||||
const FName ParentName = RefSkeleton.GetBoneName(ParentMeshIdx);
|
|
||||||
if (ParentMeshIdx < RefPose.Num())
|
|
||||||
{
|
|
||||||
const FRotator ParentEuler = RefPose[ParentMeshIdx].GetRotation().Rotator();
|
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
|
||||||
TEXT(" Parent bone: '%s' | RefPose rotation: P=%.2f Y=%.2f R=%.2f"),
|
|
||||||
*ParentName.ToString(),
|
|
||||||
ParentEuler.Pitch, ParentEuler.Yaw, ParentEuler.Roll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -124,7 +142,6 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
|||||||
TEXT("Head bone '%s' NOT FOUND in skeleton. Available bones:"),
|
TEXT("Head bone '%s' NOT FOUND in skeleton. Available bones:"),
|
||||||
*HeadBoneName.ToString());
|
*HeadBoneName.ToString());
|
||||||
|
|
||||||
// List first 10 bone names to help debug
|
|
||||||
const int32 NumBones = FMath::Min(RefSkeleton.GetNum(), 10);
|
const int32 NumBones = FMath::Min(RefSkeleton.GetNum(), 10);
|
||||||
for (int32 i = 0; i < NumBones; ++i)
|
for (int32 i = 0; i < NumBones; ++i)
|
||||||
{
|
{
|
||||||
@ -166,115 +183,125 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Apply head bone rotation ─────────────────────────────────────────
|
// ── 2. Apply head bone rotation ─────────────────────────────────────────
|
||||||
if (bApplyHeadRotation
|
if (!bApplyHeadRotation || CachedHeadRotation.Equals(FQuat::Identity, 0.001f))
|
||||||
&& HeadBoneIndex.GetInt() != INDEX_NONE
|
|
||||||
&& HeadBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
|
||||||
{
|
{
|
||||||
FTransform& HeadTransform = Output.Pose[HeadBoneIndex];
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bUseChain = (ChainBoneIndices.Num() > 0);
|
||||||
|
|
||||||
#if ELEVENLABS_AXIS_DIAGNOSTIC
|
#if ELEVENLABS_AXIS_DIAGNOSTIC
|
||||||
// ── DIAGNOSTIC: Cycle through axis test rotations ──────────────
|
// ── DIAGNOSTIC: Cycle through axis test rotations ──────────────
|
||||||
// Phase 0 (0-3s): FRotator(20, 0, 0) = Pitch component only
|
if (HeadBoneIndex.GetInt() != INDEX_NONE
|
||||||
// Phase 1 (3-6s): FRotator(0, 20, 0) = Yaw component only
|
&& HeadBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
||||||
// Phase 2 (6-9s): FRotator(0, 0, 20) = Roll component only
|
{
|
||||||
// Then repeats.
|
|
||||||
//
|
|
||||||
// Watch the head and note what happens in each phase:
|
|
||||||
// "nod up/down", "turn left/right", "tilt ear-to-shoulder"
|
|
||||||
//
|
|
||||||
static float DiagTimer = 0.0f;
|
static float DiagTimer = 0.0f;
|
||||||
static int32 DiagLogCounter = 0;
|
static int32 DiagLogCounter = 0;
|
||||||
DiagTimer += 1.0f / 30.0f; // approximate, animation thread doesn't have real delta
|
DiagTimer += 1.0f / 30.0f;
|
||||||
const int32 Phase = ((int32)(DiagTimer / 10.0f)) % 3;
|
const int32 Phase = ((int32)(DiagTimer / 10.0f)) % 3;
|
||||||
|
|
||||||
FRotator DiagRotation = FRotator::ZeroRotator;
|
FRotator DiagRotation = FRotator::ZeroRotator;
|
||||||
const TCHAR* PhaseName = TEXT("???");
|
const TCHAR* PhaseName = TEXT("???");
|
||||||
switch (Phase)
|
switch (Phase)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0: DiagRotation = FRotator(20.0f, 0.0f, 0.0f); PhaseName = TEXT("PITCH=20"); break;
|
||||||
DiagRotation = FRotator(20.0f, 0.0f, 0.0f);
|
case 1: DiagRotation = FRotator(0.0f, 20.0f, 0.0f); PhaseName = TEXT("YAW=20"); break;
|
||||||
PhaseName = TEXT("PITCH=20 (Y-axis rot)");
|
case 2: DiagRotation = FRotator(0.0f, 0.0f, 20.0f); PhaseName = TEXT("ROLL=20"); break;
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
DiagRotation = FRotator(0.0f, 20.0f, 0.0f);
|
|
||||||
PhaseName = TEXT("YAW=20 (Z-axis rot)");
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
DiagRotation = FRotator(0.0f, 0.0f, 20.0f);
|
|
||||||
PhaseName = TEXT("ROLL=20 (X-axis rot)");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FTransform& HeadTransform = Output.Pose[HeadBoneIndex];
|
||||||
const FQuat HeadOffset = DiagRotation.Quaternion();
|
const FQuat HeadOffset = DiagRotation.Quaternion();
|
||||||
// Pre-multiply: apply in parent space
|
|
||||||
HeadTransform.SetRotation((HeadOffset * HeadTransform.GetRotation()).GetNormalized());
|
HeadTransform.SetRotation((HeadOffset * HeadTransform.GetRotation()).GetNormalized());
|
||||||
|
|
||||||
DiagLogCounter++;
|
if (++DiagLogCounter % 90 == 0)
|
||||||
if (DiagLogCounter % 90 == 0)
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||||
TEXT("DIAG Phase %d: %s | Timer=%.1f"),
|
TEXT("DIAG Phase %d: %s | Timer=%.1f"), Phase, PhaseName, DiagTimer);
|
||||||
Phase, PhaseName, DiagTimer);
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// ── PRODUCTION: Apply real head rotation ─────────────────────────
|
// ── PRODUCTION ───────────────────────────────────────────────────────
|
||||||
if (!CachedHeadRotation.Equals(FQuat::Identity, 0.001f))
|
|
||||||
|
if (bUseChain)
|
||||||
{
|
{
|
||||||
|
// ── Multi-bone neck chain: distribute rotation across bones ──────
|
||||||
|
//
|
||||||
|
// 1. Clean the total rotation ONCE via swing-twist on the tip bone
|
||||||
|
// (removes parasitic ear-to-shoulder tilt).
|
||||||
|
// 2. Slerp a fraction to each bone in the chain.
|
||||||
|
|
||||||
|
// Find the tip bone (last valid bone) for swing-twist reference
|
||||||
|
FCompactPoseBoneIndex TipBoneIdx(INDEX_NONE);
|
||||||
|
for (int32 i = ChainBoneIndices.Num() - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
if (ChainBoneIndices[i].GetInt() != INDEX_NONE
|
||||||
|
&& ChainBoneIndices[i].GetInt() < Output.Pose.GetNumBones())
|
||||||
|
{
|
||||||
|
TipBoneIdx = ChainBoneIndices[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TipBoneIdx.GetInt() == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return; // No valid bones in chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swing-twist: remove tilt from the composed rotation
|
||||||
|
const FQuat TipBoneRot = Output.Pose[TipBoneIdx].GetRotation();
|
||||||
|
const FQuat Combined = CachedHeadRotation * TipBoneRot;
|
||||||
|
const FVector BoneTiltAxis = TipBoneRot.RotateVector(FVector::RightVector);
|
||||||
|
FQuat Swing, TiltTwist;
|
||||||
|
Combined.ToSwingTwist(BoneTiltAxis, Swing, TiltTwist);
|
||||||
|
|
||||||
|
// Extract clean offset = Swing * Inverse(TipBoneRot)
|
||||||
|
const FQuat CleanRotation = (Swing * TipBoneRot.Inverse()).GetNormalized();
|
||||||
|
|
||||||
|
// Distribute fractional rotation to each bone
|
||||||
|
for (int32 i = 0; i < ChainBoneIndices.Num(); ++i)
|
||||||
|
{
|
||||||
|
const FCompactPoseBoneIndex BoneIdx = ChainBoneIndices[i];
|
||||||
|
if (BoneIdx.GetInt() == INDEX_NONE
|
||||||
|
|| BoneIdx.GetInt() >= Output.Pose.GetNumBones())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FQuat FractionalRot = FQuat::Slerp(FQuat::Identity, CleanRotation, ChainBoneWeights[i]);
|
||||||
|
FTransform& BoneTransform = Output.Pose[BoneIdx];
|
||||||
|
BoneTransform.SetRotation(
|
||||||
|
(FractionalRot * BoneTransform.GetRotation()).GetNormalized());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ── Fallback: single head bone (original behavior) ──────────────
|
||||||
|
if (HeadBoneIndex.GetInt() != INDEX_NONE
|
||||||
|
&& HeadBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
||||||
|
{
|
||||||
|
FTransform& HeadTransform = Output.Pose[HeadBoneIndex];
|
||||||
const FQuat BoneRot = HeadTransform.GetRotation();
|
const FQuat BoneRot = HeadTransform.GetRotation();
|
||||||
const FQuat Combined = CachedHeadRotation * BoneRot;
|
const FQuat Combined = CachedHeadRotation * BoneRot;
|
||||||
|
|
||||||
// Remove Y-axis tilt from final rotation
|
// Remove ear-to-shoulder tilt using the bone's actual tilt axis
|
||||||
|
const FVector BoneTiltAxis = BoneRot.RotateVector(FVector::RightVector);
|
||||||
FQuat Swing, TiltTwist;
|
FQuat Swing, TiltTwist;
|
||||||
Combined.ToSwingTwist(FVector::RightVector, Swing, TiltTwist);
|
Combined.ToSwingTwist(BoneTiltAxis, Swing, TiltTwist);
|
||||||
|
|
||||||
HeadTransform.SetRotation(Swing.GetNormalized());
|
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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void FAnimNode_ElevenLabsPosture::GatherDebugData(FNodeDebugData& DebugData)
|
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)"),
|
TEXT("ElevenLabs Posture (eyes: %d curves, head: Y=%.1f P=%.1f, chain: %d bones)"),
|
||||||
CachedEyeCurves.Num(),
|
CachedEyeCurves.Num(),
|
||||||
DebugRot.Yaw, DebugRot.Pitch);
|
DebugRot.Yaw, DebugRot.Pitch,
|
||||||
|
ChainBoneIndices.Num());
|
||||||
DebugData.AddDebugItem(DebugLine);
|
DebugData.AddDebugItem(DebugLine);
|
||||||
BasePose.GatherDebugData(DebugData);
|
BasePose.GatherDebugData(DebugData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,9 +66,23 @@ private:
|
|||||||
* parasitic tilt on diagonals). Copied from the component during Update. */
|
* parasitic tilt on diagonals). Copied from the component during Update. */
|
||||||
FQuat CachedHeadRotation = FQuat::Identity;
|
FQuat CachedHeadRotation = FQuat::Identity;
|
||||||
|
|
||||||
/** Resolved head bone index in the skeleton. */
|
// ── Multi-bone neck chain (resolved at CacheBones) ──────────────────────
|
||||||
|
|
||||||
|
/** Bone names for the neck chain (root-to-tip order).
|
||||||
|
* Cached from the component at initialization. */
|
||||||
|
TArray<FName> ChainBoneNames;
|
||||||
|
|
||||||
|
/** Resolved compact pose bone indices, parallel to ChainBoneNames. */
|
||||||
|
TArray<FCompactPoseBoneIndex> ChainBoneIndices;
|
||||||
|
|
||||||
|
/** Rotation weights, parallel to ChainBoneNames. */
|
||||||
|
TArray<float> ChainBoneWeights;
|
||||||
|
|
||||||
|
// ── Fallback single-bone (when chain is empty) ──────────────────────────
|
||||||
|
|
||||||
|
/** Resolved head bone index in the skeleton (fallback). */
|
||||||
FCompactPoseBoneIndex HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
FCompactPoseBoneIndex HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
|
|
||||||
/** Head bone name cached from the component at initialization. */
|
/** Head bone name cached from the component at initialization (fallback). */
|
||||||
FName HeadBoneName;
|
FName HeadBoneName;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,25 @@ class USkeletalMeshComponent;
|
|||||||
|
|
||||||
DECLARE_LOG_CATEGORY_EXTERN(LogElevenLabsPosture, Log, All);
|
DECLARE_LOG_CATEGORY_EXTERN(LogElevenLabsPosture, Log, All);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Neck bone chain entry for distributing head rotation across multiple bones
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct PS_AI_AGENT_ELEVENLABS_API FElevenLabsNeckBoneEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
/** Bone name in the skeleton (e.g. "neck_01", "neck_02", "head"). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||||
|
FName BoneName;
|
||||||
|
|
||||||
|
/** Fraction of the total head rotation applied to this bone.
|
||||||
|
* All weights in the chain should sum to ~1.0 for correct total deflection. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "1"))
|
||||||
|
float Weight = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// UElevenLabsPostureComponent
|
// UElevenLabsPostureComponent
|
||||||
//
|
//
|
||||||
@ -128,6 +147,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture")
|
||||||
FName HeadBoneName = FName(TEXT("head"));
|
FName HeadBoneName = FName(TEXT("head"));
|
||||||
|
|
||||||
|
// ── Neck bone chain ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Optional chain of bones to distribute head rotation across.
|
||||||
|
* Order: root-to-tip (e.g. neck_01 → neck_02 → head).
|
||||||
|
* Weights should sum to ~1.0.
|
||||||
|
* If empty, falls back to single-bone behavior (HeadBoneName, weight 1.0). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture")
|
||||||
|
TArray<FElevenLabsNeckBoneEntry> NeckBoneChain;
|
||||||
|
|
||||||
// ── Getters (read by AnimNode) ───────────────────────────────────────────
|
// ── Getters (read by AnimNode) ───────────────────────────────────────────
|
||||||
|
|
||||||
/** Get current eye gaze curves (8 ARKit eye look curves).
|
/** Get current eye gaze curves (8 ARKit eye look curves).
|
||||||
@ -151,6 +179,9 @@ public:
|
|||||||
/** Get the head bone name (used by AnimNode to resolve bone index). */
|
/** Get the head bone name (used by AnimNode to resolve bone index). */
|
||||||
FName GetHeadBoneName() const { return HeadBoneName; }
|
FName GetHeadBoneName() const { return HeadBoneName; }
|
||||||
|
|
||||||
|
/** Get the neck bone chain (used by AnimNode to resolve bone indices). */
|
||||||
|
const TArray<FElevenLabsNeckBoneEntry>& GetNeckBoneChain() const { return NeckBoneChain; }
|
||||||
|
|
||||||
// ── 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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user