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);
|
||||
|
||||
// Find the ElevenLabsPostureComponent on the owning actor.
|
||||
// Reset all cached state.
|
||||
PostureComponent.Reset();
|
||||
CachedEyeCurves.Reset();
|
||||
CachedHeadRotation = FQuat::Identity;
|
||||
HeadBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||
ChainBoneNames.Reset();
|
||||
ChainBoneWeights.Reset();
|
||||
ChainBoneIndices.Reset();
|
||||
|
||||
if (const FAnimInstanceProxy* Proxy = Context.AnimInstanceProxy)
|
||||
{
|
||||
@ -44,21 +47,28 @@ void FAnimNode_ElevenLabsPosture::Initialize_AnyThread(const FAnimationInitializ
|
||||
PostureComponent = Comp;
|
||||
HeadBoneName = Comp->GetHeadBoneName();
|
||||
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT("=== ElevenLabs Posture AnimNode ==="));
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT(" Owner: %s | Mesh: %s"),
|
||||
*Owner->GetName(), *SkelMesh->GetName());
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT(" HeadRotation: %s | EyeCurves: %s"),
|
||||
// Cache neck bone chain configuration
|
||||
const TArray<FElevenLabsNeckBoneEntry>& Chain = Comp->GetNeckBoneChain();
|
||||
if (Chain.Num() > 0)
|
||||
{
|
||||
ChainBoneNames.Reserve(Chain.Num());
|
||||
ChainBoneWeights.Reserve(Chain.Num());
|
||||
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"),
|
||||
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"));
|
||||
#if ELEVENLABS_AXIS_DIAGNOSTIC
|
||||
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
|
||||
bApplyEyeCurves ? TEXT("ON") : TEXT("OFF"),
|
||||
ChainBoneNames.Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -75,48 +85,56 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
||||
{
|
||||
BasePose.CacheBones(Context);
|
||||
|
||||
// Resolve head bone index from the skeleton
|
||||
// Reset all bone indices
|
||||
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 FReferenceSkeleton& RefSkeleton = RequiredBones.GetReferenceSkeleton();
|
||||
const int32 MeshIndex = RefSkeleton.FindBoneIndex(HeadBoneName);
|
||||
const FBoneContainer& RequiredBones = Proxy->GetRequiredBones();
|
||||
const FReferenceSkeleton& RefSkeleton = RequiredBones.GetReferenceSkeleton();
|
||||
|
||||
// ── 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)
|
||||
{
|
||||
HeadBoneIndex = RequiredBones.MakeCompactPoseIndex(
|
||||
FMeshPoseBoneIndex(MeshIndex));
|
||||
|
||||
// Log reference pose rotation for diagnostic
|
||||
const TArray<FTransform>& RefPose = RefSkeleton.GetRefBonePose();
|
||||
if (MeshIndex < RefPose.Num())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Log,
|
||||
TEXT("Head bone '%s' resolved to index %d."),
|
||||
*HeadBoneName.ToString(), HeadBoneIndex.GetInt());
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -124,7 +142,6 @@ void FAnimNode_ElevenLabsPosture::CacheBones_AnyThread(const FAnimationCacheBone
|
||||
TEXT("Head bone '%s' NOT FOUND in skeleton. Available bones:"),
|
||||
*HeadBoneName.ToString());
|
||||
|
||||
// List first 10 bone names to help debug
|
||||
const int32 NumBones = FMath::Min(RefSkeleton.GetNum(), 10);
|
||||
for (int32 i = 0; i < NumBones; ++i)
|
||||
{
|
||||
@ -166,115 +183,125 @@ void FAnimNode_ElevenLabsPosture::Evaluate_AnyThread(FPoseContext& Output)
|
||||
}
|
||||
|
||||
// ── 2. Apply head bone rotation ─────────────────────────────────────────
|
||||
if (bApplyHeadRotation
|
||||
&& HeadBoneIndex.GetInt() != INDEX_NONE
|
||||
&& HeadBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
||||
if (!bApplyHeadRotation || CachedHeadRotation.Equals(FQuat::Identity, 0.001f))
|
||||
{
|
||||
FTransform& HeadTransform = Output.Pose[HeadBoneIndex];
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bUseChain = (ChainBoneIndices.Num() > 0);
|
||||
|
||||
#if ELEVENLABS_AXIS_DIAGNOSTIC
|
||||
// ── DIAGNOSTIC: Cycle through axis test rotations ──────────────
|
||||
// Phase 0 (0-3s): FRotator(20, 0, 0) = Pitch component only
|
||||
// Phase 1 (3-6s): FRotator(0, 20, 0) = Yaw component only
|
||||
// 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"
|
||||
//
|
||||
// ── DIAGNOSTIC: Cycle through axis test rotations ──────────────
|
||||
if (HeadBoneIndex.GetInt() != INDEX_NONE
|
||||
&& HeadBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
||||
{
|
||||
static float DiagTimer = 0.0f;
|
||||
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;
|
||||
|
||||
FRotator DiagRotation = FRotator::ZeroRotator;
|
||||
const TCHAR* PhaseName = TEXT("???");
|
||||
switch (Phase)
|
||||
{
|
||||
case 0:
|
||||
DiagRotation = FRotator(20.0f, 0.0f, 0.0f);
|
||||
PhaseName = TEXT("PITCH=20 (Y-axis rot)");
|
||||
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;
|
||||
case 0: DiagRotation = FRotator(20.0f, 0.0f, 0.0f); PhaseName = TEXT("PITCH=20"); break;
|
||||
case 1: DiagRotation = FRotator(0.0f, 20.0f, 0.0f); PhaseName = TEXT("YAW=20"); break;
|
||||
case 2: DiagRotation = FRotator(0.0f, 0.0f, 20.0f); PhaseName = TEXT("ROLL=20"); break;
|
||||
}
|
||||
|
||||
FTransform& HeadTransform = Output.Pose[HeadBoneIndex];
|
||||
const FQuat HeadOffset = DiagRotation.Quaternion();
|
||||
// Pre-multiply: apply in parent space
|
||||
HeadTransform.SetRotation((HeadOffset * HeadTransform.GetRotation()).GetNormalized());
|
||||
|
||||
DiagLogCounter++;
|
||||
if (DiagLogCounter % 90 == 0)
|
||||
if (++DiagLogCounter % 90 == 0)
|
||||
{
|
||||
UE_LOG(LogElevenLabsPostureAnimNode, Warning,
|
||||
TEXT("DIAG Phase %d: %s | Timer=%.1f"),
|
||||
Phase, PhaseName, DiagTimer);
|
||||
TEXT("DIAG Phase %d: %s | Timer=%.1f"), Phase, PhaseName, DiagTimer);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// ── PRODUCTION: Apply real head rotation ─────────────────────────
|
||||
if (!CachedHeadRotation.Equals(FQuat::Identity, 0.001f))
|
||||
// ── PRODUCTION ───────────────────────────────────────────────────────
|
||||
|
||||
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 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;
|
||||
Combined.ToSwingTwist(FVector::RightVector, Swing, TiltTwist);
|
||||
Combined.ToSwingTwist(BoneTiltAxis, 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
|
||||
}
|
||||
|
||||
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)"),
|
||||
TEXT("ElevenLabs Posture (eyes: %d curves, head: Y=%.1f P=%.1f, chain: %d bones)"),
|
||||
CachedEyeCurves.Num(),
|
||||
DebugRot.Yaw, DebugRot.Pitch);
|
||||
DebugRot.Yaw, DebugRot.Pitch,
|
||||
ChainBoneIndices.Num());
|
||||
DebugData.AddDebugItem(DebugLine);
|
||||
BasePose.GatherDebugData(DebugData);
|
||||
}
|
||||
|
||||
@ -66,9 +66,23 @@ private:
|
||||
* parasitic tilt on diagonals). Copied from the component during Update. */
|
||||
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);
|
||||
|
||||
/** Head bone name cached from the component at initialization. */
|
||||
/** Head bone name cached from the component at initialization (fallback). */
|
||||
FName HeadBoneName;
|
||||
};
|
||||
|
||||
@ -11,6 +11,25 @@ class USkeletalMeshComponent;
|
||||
|
||||
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
|
||||
//
|
||||
@ -128,6 +147,15 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ElevenLabs|Posture")
|
||||
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) ───────────────────────────────────────────
|
||||
|
||||
/** 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). */
|
||||
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 ────────────────────────────────────────────
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user