Fix thread-safety crash and body rotation saccades in network play
- FacialExpressionComponent: add FCriticalSection around emotion curves to prevent race between TickComponent (game thread) and anim worker thread — root cause of EXCEPTION_ACCESS_VIOLATION at Evaluate_AnyThread - Remove deprecated FBlendedCurve::IsValid() guards (UE 5.5: always true) - Body tracking: replace AddActorWorldRotation() with animation-only pelvis bone rotation via AnimNode — eliminates replication tug-of-war that caused client-side saccades when server overwrote local rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
45ee0c6f7d
commit
677f08e936
@ -89,13 +89,10 @@ void FAnimNode_PS_AI_ConvAgent_FacialExpression::Evaluate_AnyThread(FPoseContext
|
|||||||
// covering eyes, eyebrows, cheeks, nose, and mouth mood.
|
// covering eyes, eyebrows, cheeks, nose, and mouth mood.
|
||||||
// The downstream Lip Sync node will override mouth-area curves
|
// The downstream Lip Sync node will override mouth-area curves
|
||||||
// during speech, while non-mouth emotion curves pass through.
|
// during speech, while non-mouth emotion curves pass through.
|
||||||
if (Output.Curve.IsValid())
|
|
||||||
{
|
|
||||||
for (const auto& Pair : CachedEmotionCurves)
|
for (const auto& Pair : CachedEmotionCurves)
|
||||||
{
|
{
|
||||||
Output.Curve.Set(Pair.Key, Pair.Value);
|
Output.Curve.Set(Pair.Key, Pair.Value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FAnimNode_PS_AI_ConvAgent_FacialExpression::GatherDebugData(FNodeDebugData& DebugData)
|
void FAnimNode_PS_AI_ConvAgent_FacialExpression::GatherDebugData(FNodeDebugData& DebugData)
|
||||||
|
|||||||
@ -89,8 +89,6 @@ void FAnimNode_PS_AI_ConvAgent_LipSync::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
// Skip near-zero values so that the upstream Facial Expression node's
|
// Skip near-zero values so that the upstream Facial Expression node's
|
||||||
// emotion curves (eyes, brows, mouth mood) pass through during silence.
|
// emotion curves (eyes, brows, mouth mood) pass through during silence.
|
||||||
// During speech, active lip sync curves override emotion's mouth curves.
|
// During speech, active lip sync curves override emotion's mouth curves.
|
||||||
if (Output.Curve.IsValid())
|
|
||||||
{
|
|
||||||
for (const auto& Pair : CachedCurves)
|
for (const auto& Pair : CachedCurves)
|
||||||
{
|
{
|
||||||
if (FMath::Abs(Pair.Value) > 0.01f)
|
if (FMath::Abs(Pair.Value) > 0.01f)
|
||||||
@ -98,7 +96,6 @@ void FAnimNode_PS_AI_ConvAgent_LipSync::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
Output.Curve.Set(Pair.Key, Pair.Value);
|
Output.Curve.Set(Pair.Key, Pair.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FAnimNode_PS_AI_ConvAgent_LipSync::GatherDebugData(FNodeDebugData& DebugData)
|
void FAnimNode_PS_AI_ConvAgent_LipSync::GatherDebugData(FNodeDebugData& DebugData)
|
||||||
|
|||||||
@ -144,6 +144,8 @@ void FAnimNode_PS_AI_ConvAgent_Posture::CacheBones_AnyThread(const FAnimationCac
|
|||||||
RightEyeBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
RightEyeBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
LeftEyeRefPoseRotation = FQuat::Identity;
|
LeftEyeRefPoseRotation = FQuat::Identity;
|
||||||
RightEyeRefPoseRotation = FQuat::Identity;
|
RightEyeRefPoseRotation = FQuat::Identity;
|
||||||
|
BodyBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
|
BodyBoneRefPoseRotation = FQuat::Identity;
|
||||||
ChainBoneIndices.Reset();
|
ChainBoneIndices.Reset();
|
||||||
ChainRefPoseRotations.Reset();
|
ChainRefPoseRotations.Reset();
|
||||||
|
|
||||||
@ -247,6 +249,22 @@ void FAnimNode_PS_AI_ConvAgent_Posture::CacheBones_AnyThread(const FAnimationCac
|
|||||||
ResolveEyeBone(DefaultRightEyeBone, RightEyeBoneIndex, RightEyeRefPoseRotation);
|
ResolveEyeBone(DefaultRightEyeBone, RightEyeBoneIndex, RightEyeRefPoseRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Resolve body bone for body yaw rotation ─────────────────────
|
||||||
|
// The body yaw offset is applied to "pelvis" (or first skeleton bone)
|
||||||
|
// to rotate the entire skeleton without modifying actor rotation.
|
||||||
|
{
|
||||||
|
static const FName DefaultBodyBone(TEXT("pelvis"));
|
||||||
|
const int32 MeshIdx = RefSkeleton.FindBoneIndex(DefaultBodyBone);
|
||||||
|
if (MeshIdx != INDEX_NONE)
|
||||||
|
{
|
||||||
|
BodyBoneIndex = RequiredBones.MakeCompactPoseIndex(
|
||||||
|
FMeshPoseBoneIndex(MeshIdx));
|
||||||
|
BodyBoneRefPoseRotation = (MeshIdx < RefPose.Num())
|
||||||
|
? RefPose[MeshIdx].GetRotation()
|
||||||
|
: FQuat::Identity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Resolve ancestor chain for body drift compensation ────────────
|
// ── Resolve ancestor chain for body drift compensation ────────────
|
||||||
// Walk from the parent of the first neck/head bone up to root.
|
// Walk from the parent of the first neck/head bone up to root.
|
||||||
// This lets us measure how much the spine/torso animation has
|
// This lets us measure how much the spine/torso animation has
|
||||||
@ -328,6 +346,7 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Update_AnyThread(const FAnimationUpdateC
|
|||||||
CachedHeadCompensation = PostureComponent->GetHeadAnimationCompensation();
|
CachedHeadCompensation = PostureComponent->GetHeadAnimationCompensation();
|
||||||
CachedEyeCompensation = PostureComponent->GetEyeAnimationCompensation();
|
CachedEyeCompensation = PostureComponent->GetEyeAnimationCompensation();
|
||||||
CachedBodyDriftCompensation = PostureComponent->GetBodyDriftCompensation();
|
CachedBodyDriftCompensation = PostureComponent->GetBodyDriftCompensation();
|
||||||
|
CachedBodyYawOffset = PostureComponent->GetBodyYawOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,11 +398,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
// Evaluate the upstream pose (pass-through)
|
// Evaluate the upstream pose (pass-through)
|
||||||
BasePose.Evaluate(Output);
|
BasePose.Evaluate(Output);
|
||||||
|
|
||||||
// Guard: in packaged+network builds the curve container may not be
|
|
||||||
// initialized yet (skeleton not fully loaded). All Output.Curve access
|
|
||||||
// must be gated on this flag to avoid null-pointer crashes.
|
|
||||||
const bool bCurveValid = Output.Curve.IsValid();
|
|
||||||
|
|
||||||
// ── Periodic diagnostic (runs for EVERY instance, before any early return) ─
|
// ── Periodic diagnostic (runs for EVERY instance, before any early return) ─
|
||||||
#if !UE_BUILD_SHIPPING
|
#if !UE_BUILD_SHIPPING
|
||||||
if (++EvalDebugFrameCounter % 300 == 1) // ~every 5 seconds at 60fps
|
if (++EvalDebugFrameCounter % 300 == 1) // ~every 5 seconds at 60fps
|
||||||
@ -437,7 +451,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
#if ELEVENLABS_EYE_DIAGNOSTIC == 1
|
#if ELEVENLABS_EYE_DIAGNOSTIC == 1
|
||||||
// MODE 1: CTRL curves → tests CORRECT MetaHuman CTRL naming
|
// MODE 1: CTRL curves → tests CORRECT MetaHuman CTRL naming
|
||||||
// Real format: CTRL_expressions_eyeLook{Dir}{L/R} (NOT eyeLookUpLeft!)
|
// Real format: CTRL_expressions_eyeLook{Dir}{L/R} (NOT eyeLookUpLeft!)
|
||||||
if (bCurveValid)
|
|
||||||
{
|
{
|
||||||
static const FName ForceCTRL(TEXT("CTRL_expressions_eyeLookUpL"));
|
static const FName ForceCTRL(TEXT("CTRL_expressions_eyeLookUpL"));
|
||||||
Output.Curve.Set(ForceCTRL, 1.0f);
|
Output.Curve.Set(ForceCTRL, 1.0f);
|
||||||
@ -459,7 +472,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
|
|
||||||
#elif ELEVENLABS_EYE_DIAGNOSTIC == 2
|
#elif ELEVENLABS_EYE_DIAGNOSTIC == 2
|
||||||
// MODE 2: ARKit curves → tests if mh_arkit_mapping_pose drives eyes
|
// MODE 2: ARKit curves → tests if mh_arkit_mapping_pose drives eyes
|
||||||
if (bCurveValid)
|
|
||||||
{
|
{
|
||||||
static const FName ForceARKit(TEXT("eyeLookUpLeft"));
|
static const FName ForceARKit(TEXT("eyeLookUpLeft"));
|
||||||
Output.Curve.Set(ForceARKit, 1.0f);
|
Output.Curve.Set(ForceARKit, 1.0f);
|
||||||
@ -490,7 +502,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
(LookUp * LeftEyeRefPoseRotation).GetNormalized());
|
(LookUp * LeftEyeRefPoseRotation).GetNormalized());
|
||||||
}
|
}
|
||||||
// Zero curves to isolate
|
// Zero curves to isolate
|
||||||
if (bCurveValid)
|
|
||||||
{
|
{
|
||||||
static const FName ZeroARKit(TEXT("eyeLookUpLeft"));
|
static const FName ZeroARKit(TEXT("eyeLookUpLeft"));
|
||||||
static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft"));
|
static const FName ZeroCTRL(TEXT("CTRL_expressions_eyeLookUpLeft"));
|
||||||
@ -551,7 +562,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// (b) Blend CTRL eye curves: read animation's value, lerp with posture
|
// (b) Blend CTRL eye curves: read animation's value, lerp with posture
|
||||||
if (bCurveValid)
|
|
||||||
{
|
{
|
||||||
const auto& CTRLMap = GetARKitToCTRLEyeMap();
|
const auto& CTRLMap = GetARKitToCTRLEyeMap();
|
||||||
for (const auto& Pair : CachedEyeCurves)
|
for (const auto& Pair : CachedEyeCurves)
|
||||||
@ -566,6 +576,7 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
Output.Curve.Set(*CTRLName, BlendedValue);
|
Output.Curve.Set(*CTRLName, BlendedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (c) Zero ARKit eye curves to prevent mh_arkit_mapping_pose
|
// (c) Zero ARKit eye curves to prevent mh_arkit_mapping_pose
|
||||||
// from overwriting our carefully blended CTRL values.
|
// from overwriting our carefully blended CTRL values.
|
||||||
@ -576,7 +587,6 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
Output.Curve.Set(Pair.Key, 0.0f);
|
Output.Curve.Set(Pair.Key, 0.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Eye diagnostic logging
|
// Eye diagnostic logging
|
||||||
#if !UE_BUILD_SHIPPING
|
#if !UE_BUILD_SHIPPING
|
||||||
@ -600,6 +610,18 @@ void FAnimNode_PS_AI_ConvAgent_Posture::Evaluate_AnyThread(FPoseContext& Output)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Apply body yaw rotation to pelvis bone ─────────────────────────────
|
||||||
|
// This replaces the old AddActorWorldRotation() approach — the actor rotation
|
||||||
|
// is never modified; the body turn is purely animation-driven (local, no replication).
|
||||||
|
if (FMath::Abs(CachedBodyYawOffset) > 0.1f
|
||||||
|
&& BodyBoneIndex.GetInt() != INDEX_NONE
|
||||||
|
&& BodyBoneIndex.GetInt() < Output.Pose.GetNumBones())
|
||||||
|
{
|
||||||
|
const FQuat BodyYawQuat(FVector::UpVector, FMath::DegreesToRadians(CachedBodyYawOffset));
|
||||||
|
FTransform& BodyTransform = Output.Pose[BodyBoneIndex];
|
||||||
|
BodyTransform.SetRotation((BodyYawQuat * BodyTransform.GetRotation()).GetNormalized());
|
||||||
|
}
|
||||||
|
|
||||||
// IMPORTANT: Even when posture is near-zero (head looking straight at target),
|
// IMPORTANT: Even when posture is near-zero (head looking straight at target),
|
||||||
// we still need to run compensation to REMOVE the animation's head contribution.
|
// we still need to run compensation to REMOVE the animation's head contribution.
|
||||||
// Only skip if BOTH posture is identity AND compensation is inactive (pure additive).
|
// Only skip if BOTH posture is identity AND compensation is inactive (pure additive).
|
||||||
|
|||||||
@ -335,13 +335,17 @@ void UPS_AI_ConvAgent_FacialExpressionComponent::TickComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Evaluate curves from playing animations ─────────────────────────────
|
// ── Evaluate curves from playing animations ─────────────────────────────
|
||||||
|
// Compute into a local map first, then swap under lock to avoid
|
||||||
|
// racing with the AnimNode worker thread reading GetCurrentEmotionCurves().
|
||||||
|
|
||||||
|
TMap<FName, float> NewCurves;
|
||||||
|
|
||||||
TMap<FName, float> ActiveCurves = EvaluateAnimCurves(ActiveAnim, ActivePlaybackTime);
|
TMap<FName, float> ActiveCurves = EvaluateAnimCurves(ActiveAnim, ActivePlaybackTime);
|
||||||
|
|
||||||
if (CrossfadeAlpha >= 1.0f)
|
if (CrossfadeAlpha >= 1.0f)
|
||||||
{
|
{
|
||||||
// No crossfade — use active curves directly
|
// No crossfade — use active curves directly
|
||||||
CurrentEmotionCurves = MoveTemp(ActiveCurves);
|
NewCurves = MoveTemp(ActiveCurves);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -349,7 +353,6 @@ void UPS_AI_ConvAgent_FacialExpressionComponent::TickComponent(
|
|||||||
TMap<FName, float> PrevCurves = EvaluateAnimCurves(PrevAnim, PrevPlaybackTime);
|
TMap<FName, float> PrevCurves = EvaluateAnimCurves(PrevAnim, PrevPlaybackTime);
|
||||||
|
|
||||||
// Collect all curve names from both anims
|
// Collect all curve names from both anims
|
||||||
CurrentEmotionCurves.Reset();
|
|
||||||
TSet<FName> AllCurves;
|
TSet<FName> AllCurves;
|
||||||
for (const auto& P : ActiveCurves) AllCurves.Add(P.Key);
|
for (const auto& P : ActiveCurves) AllCurves.Add(P.Key);
|
||||||
for (const auto& P : PrevCurves) AllCurves.Add(P.Key);
|
for (const auto& P : PrevCurves) AllCurves.Add(P.Key);
|
||||||
@ -364,7 +367,7 @@ void UPS_AI_ConvAgent_FacialExpressionComponent::TickComponent(
|
|||||||
|
|
||||||
if (FMath::Abs(Blended) > 0.001f)
|
if (FMath::Abs(Blended) > 0.001f)
|
||||||
{
|
{
|
||||||
CurrentEmotionCurves.Add(CurveName, Blended);
|
NewCurves.Add(CurveName, Blended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -372,15 +375,21 @@ void UPS_AI_ConvAgent_FacialExpressionComponent::TickComponent(
|
|||||||
// ── Apply activation alpha to output curves ──────────────────────────
|
// ── Apply activation alpha to output curves ──────────────────────────
|
||||||
if (CurrentActiveAlpha < 0.001f)
|
if (CurrentActiveAlpha < 0.001f)
|
||||||
{
|
{
|
||||||
CurrentEmotionCurves.Reset();
|
NewCurves.Reset();
|
||||||
}
|
}
|
||||||
else if (CurrentActiveAlpha < 0.999f)
|
else if (CurrentActiveAlpha < 0.999f)
|
||||||
{
|
{
|
||||||
for (auto& Pair : CurrentEmotionCurves)
|
for (auto& Pair : NewCurves)
|
||||||
{
|
{
|
||||||
Pair.Value *= CurrentActiveAlpha;
|
Pair.Value *= CurrentActiveAlpha;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swap under lock — the AnimNode reads CurrentEmotionCurves from a worker thread
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&EmotionCurveLock);
|
||||||
|
CurrentEmotionCurves = MoveTemp(NewCurves);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -99,6 +99,7 @@ void UPS_AI_ConvAgent_PostureComponent::BeginPlay()
|
|||||||
// 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.
|
||||||
OriginalActorYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
OriginalActorYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
||||||
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
||||||
|
CurrentBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
||||||
|
|
||||||
if (bDebug)
|
if (bDebug)
|
||||||
{
|
{
|
||||||
@ -179,6 +180,7 @@ void UPS_AI_ConvAgent_PostureComponent::ResetBodyTarget()
|
|||||||
if (AActor* Owner = GetOwner())
|
if (AActor* Owner = GetOwner())
|
||||||
{
|
{
|
||||||
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
TargetBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
||||||
|
CurrentBodyWorldYaw = Owner->GetActorRotation().Yaw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,19 +335,14 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
|
|||||||
if (bEnableBodyTracking)
|
if (bEnableBodyTracking)
|
||||||
{
|
{
|
||||||
const float BodyDelta = FMath::FindDeltaAngleDegrees(
|
const float BodyDelta = FMath::FindDeltaAngleDegrees(
|
||||||
Owner->GetActorRotation().Yaw, TargetBodyWorldYaw);
|
CurrentBodyWorldYaw, TargetBodyWorldYaw);
|
||||||
if (FMath::Abs(BodyDelta) > 0.1f)
|
if (FMath::Abs(BodyDelta) > 0.1f)
|
||||||
{
|
{
|
||||||
const float BodyStep = FMath::FInterpTo(0.0f, BodyDelta, SafeDeltaTime, BodyInterpSpeed);
|
const float BodyStep = FMath::FInterpTo(0.0f, BodyDelta, SafeDeltaTime, BodyInterpSpeed);
|
||||||
// Only modify actor rotation on the authority (server/standalone).
|
// Track body facing internally instead of modifying the actor rotation.
|
||||||
// On clients, the rotation arrives via replication — calling
|
// The AnimNode applies the delta as a bone rotation — purely local,
|
||||||
// AddActorWorldRotation here would fight with replicated updates,
|
// no actor rotation change, no replication conflict.
|
||||||
// causing visible stuttering as the network periodically snaps
|
CurrentBodyWorldYaw += BodyStep;
|
||||||
// the rotation back to the server's value.
|
|
||||||
if (Owner->HasAuthority())
|
|
||||||
{
|
|
||||||
Owner->AddActorWorldRotation(FRotator(0.0f, BodyStep, 0.0f));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +351,7 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
|
|||||||
float DeltaYaw = 0.0f;
|
float DeltaYaw = 0.0f;
|
||||||
if (!HorizontalDir.IsNearlyZero(1.0f))
|
if (!HorizontalDir.IsNearlyZero(1.0f))
|
||||||
{
|
{
|
||||||
const float CurrentFacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
const float CurrentFacingYaw = CurrentBodyWorldYaw + MeshForwardYawOffset;
|
||||||
DeltaYaw = FMath::FindDeltaAngleDegrees(CurrentFacingYaw, TargetWorldYaw);
|
DeltaYaw = FMath::FindDeltaAngleDegrees(CurrentFacingYaw, TargetWorldYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,6 +464,11 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
|
|||||||
|
|
||||||
// Eye yaw is negated to match ARKit curve direction convention.
|
// Eye yaw is negated to match ARKit curve direction convention.
|
||||||
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);
|
UpdateEyeCurves(-CurrentEyeYaw, CurrentEyePitch);
|
||||||
|
|
||||||
|
// Body yaw offset = how much the body has turned relative to the actor's rotation.
|
||||||
|
// The AnimNode applies this as a bone rotation (pelvis) — purely local.
|
||||||
|
CachedBodyYawOffset = FMath::FindDeltaAngleDegrees(
|
||||||
|
Owner->GetActorRotation().Yaw, CurrentBodyWorldYaw) * CurrentActiveAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Debug gaze lines ────────────────────────────────────────────────────
|
// ── Debug gaze lines ────────────────────────────────────────────────────
|
||||||
@ -539,21 +541,22 @@ void UPS_AI_ConvAgent_PostureComponent::TickComponent(
|
|||||||
DebugFrameCounter++;
|
DebugFrameCounter++;
|
||||||
if (DebugFrameCounter % 120 == 0)
|
if (DebugFrameCounter % 120 == 0)
|
||||||
{
|
{
|
||||||
const float FacingYaw = Owner->GetActorRotation().Yaw + MeshForwardYawOffset;
|
const float FacingYaw = CurrentBodyWorldYaw + MeshForwardYawOffset;
|
||||||
const FVector TP = TargetActor->GetActorLocation() + TargetOffset;
|
const FVector TP = TargetActor->GetActorLocation() + TargetOffset;
|
||||||
const FVector Dir = TP - Owner->GetActorLocation();
|
const FVector Dir = TP - Owner->GetActorLocation();
|
||||||
const float TgtYaw = FVector(Dir.X, Dir.Y, 0.0f).Rotation().Yaw;
|
const float TgtYaw = FVector(Dir.X, Dir.Y, 0.0f).Rotation().Yaw;
|
||||||
const float Delta = FMath::FindDeltaAngleDegrees(FacingYaw, TgtYaw);
|
const float Delta = FMath::FindDeltaAngleDegrees(FacingYaw, TgtYaw);
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_ConvAgent_Posture, Log,
|
UE_LOG(LogPS_AI_ConvAgent_Posture, Log,
|
||||||
TEXT("Posture [%s -> %s]: Delta=%.1f | Head=%.1f/%.1f | Eyes=%.1f/%.1f | Body: enabled=%s TargetYaw=%.1f ActorYaw=%.1f"),
|
TEXT("Posture [%s -> %s]: Delta=%.1f | Head=%.1f/%.1f | Eyes=%.1f/%.1f | Body: enabled=%s TargetYaw=%.1f BodyYaw=%.1f (offset=%.1f)"),
|
||||||
*Owner->GetName(), *TargetActor->GetName(),
|
*Owner->GetName(), *TargetActor->GetName(),
|
||||||
Delta,
|
Delta,
|
||||||
CurrentHeadYaw, CurrentHeadPitch,
|
CurrentHeadYaw, CurrentHeadPitch,
|
||||||
CurrentEyeYaw, CurrentEyePitch,
|
CurrentEyeYaw, CurrentEyePitch,
|
||||||
bEnableBodyTracking ? TEXT("Y") : TEXT("N"),
|
bEnableBodyTracking ? TEXT("Y") : TEXT("N"),
|
||||||
TargetBodyWorldYaw,
|
TargetBodyWorldYaw,
|
||||||
Owner->GetActorRotation().Yaw);
|
CurrentBodyWorldYaw,
|
||||||
|
CachedBodyYawOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,6 +139,20 @@ private:
|
|||||||
* Cached from the component during Update. */
|
* Cached from the component during Update. */
|
||||||
float CachedBodyDriftCompensation = 0.0f;
|
float CachedBodyDriftCompensation = 0.0f;
|
||||||
|
|
||||||
|
// ── Body yaw (animation-only body tracking) ──────────────────────────
|
||||||
|
|
||||||
|
/** Bone to apply body yaw rotation to. Rotates the entire skeleton
|
||||||
|
* visually without modifying the actor rotation (no replication conflict).
|
||||||
|
* Default "pelvis" works for MetaHuman and most humanoid skeletons. */
|
||||||
|
FCompactPoseBoneIndex BodyBoneIndex = FCompactPoseBoneIndex(INDEX_NONE);
|
||||||
|
|
||||||
|
/** Reference pose rotation for the body bone. */
|
||||||
|
FQuat BodyBoneRefPoseRotation = FQuat::Identity;
|
||||||
|
|
||||||
|
/** Body yaw offset in degrees (relative to actor rotation).
|
||||||
|
* Cached from the component during Update. */
|
||||||
|
float CachedBodyYawOffset = 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;
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "HAL/CriticalSection.h"
|
||||||
#include "Components/ActorComponent.h"
|
#include "Components/ActorComponent.h"
|
||||||
#include "PS_AI_ConvAgent_Definitions.h"
|
#include "PS_AI_ConvAgent_Definitions.h"
|
||||||
#include "PS_AI_ConvAgent_FacialExpressionComponent.generated.h"
|
#include "PS_AI_ConvAgent_FacialExpressionComponent.generated.h"
|
||||||
@ -83,9 +84,14 @@ public:
|
|||||||
|
|
||||||
// ── Getters ───────────────────────────────────────────────────────────────
|
// ── Getters ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Get the current emotion curves evaluated from the playing AnimSequence. */
|
/** Get the current emotion curves evaluated from the playing AnimSequence.
|
||||||
|
* Returns a COPY — safe to call from any thread (AnimNode worker thread). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|FacialExpression")
|
UFUNCTION(BlueprintCallable, Category = "PS AI ConvAgent|FacialExpression")
|
||||||
const TMap<FName, float>& GetCurrentEmotionCurves() const { return CurrentEmotionCurves; }
|
TMap<FName, float> GetCurrentEmotionCurves() const
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&EmotionCurveLock);
|
||||||
|
return CurrentEmotionCurves;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the active emotion. */
|
/** Get the active emotion. */
|
||||||
UFUNCTION(BlueprintPure, Category = "PS AI ConvAgent|FacialExpression")
|
UFUNCTION(BlueprintPure, Category = "PS AI ConvAgent|FacialExpression")
|
||||||
@ -159,9 +165,14 @@ private:
|
|||||||
|
|
||||||
// ── Curve output ─────────────────────────────────────────────────────────
|
// ── Curve output ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Current blended emotion curves (evaluated each tick from playing anims). */
|
/** Current blended emotion curves (evaluated each tick from playing anims).
|
||||||
|
* Protected by EmotionCurveLock — written in TickComponent (game thread),
|
||||||
|
* read by AnimNode via GetCurrentEmotionCurves() (worker thread). */
|
||||||
TMap<FName, float> CurrentEmotionCurves;
|
TMap<FName, float> CurrentEmotionCurves;
|
||||||
|
|
||||||
|
/** Lock protecting CurrentEmotionCurves from concurrent game/anim thread access. */
|
||||||
|
mutable FCriticalSection EmotionCurveLock;
|
||||||
|
|
||||||
/** Current blend alpha (0 = fully inactive/passthrough, 1 = fully active). */
|
/** Current blend alpha (0 = fully inactive/passthrough, 1 = fully active). */
|
||||||
float CurrentActiveAlpha = 1.0f;
|
float CurrentActiveAlpha = 1.0f;
|
||||||
|
|
||||||
|
|||||||
@ -273,6 +273,15 @@ public:
|
|||||||
* Scaled by activation alpha for smooth passthrough when inactive. */
|
* Scaled by activation alpha for smooth passthrough when inactive. */
|
||||||
float GetBodyDriftCompensation() const { return BodyDriftCompensation * CurrentActiveAlpha; }
|
float GetBodyDriftCompensation() const { return BodyDriftCompensation * CurrentActiveAlpha; }
|
||||||
|
|
||||||
|
/** Get body yaw offset in degrees (relative to actor's rotation).
|
||||||
|
* Applied by the AnimNode as a bone rotation — purely local, no replication.
|
||||||
|
* Thread-safe copy, blended by activation alpha. */
|
||||||
|
float GetBodyYawOffset() const
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&PostureDataLock);
|
||||||
|
return CachedBodyYawOffset;
|
||||||
|
}
|
||||||
|
|
||||||
/** Reset the persistent body yaw target to the actor's current facing.
|
/** Reset the persistent body yaw target to the actor's current facing.
|
||||||
* Call this when re-attaching a posture target so body tracking starts
|
* Call this when re-attaching a posture target so body tracking starts
|
||||||
* fresh instead of chasing a stale yaw from the previous interaction. */
|
* fresh instead of chasing a stale yaw from the previous interaction. */
|
||||||
@ -316,11 +325,17 @@ private:
|
|||||||
float TargetHeadYaw = 0.0f;
|
float TargetHeadYaw = 0.0f;
|
||||||
float TargetHeadPitch = 0.0f;
|
float TargetHeadPitch = 0.0f;
|
||||||
|
|
||||||
/** Persistent body target — world yaw the actor should face.
|
/** Persistent body target — world yaw the body should face.
|
||||||
* Only updated when head+eyes can't reach the target (overflow).
|
* Only updated when head+eyes can't reach the target (overflow).
|
||||||
* Same sticky pattern as TargetHeadYaw but for the body layer. */
|
* Same sticky pattern as TargetHeadYaw but for the body layer. */
|
||||||
float TargetBodyWorldYaw = 0.0f;
|
float TargetBodyWorldYaw = 0.0f;
|
||||||
|
|
||||||
|
/** Interpolated actual body facing in world space.
|
||||||
|
* Replaces the old AddActorWorldRotation() approach — the actor rotation
|
||||||
|
* is never modified; instead this tracks the virtual body facing and the
|
||||||
|
* AnimNode applies the delta as a bone rotation (100% local, no replication). */
|
||||||
|
float CurrentBodyWorldYaw = 0.0f;
|
||||||
|
|
||||||
/** Original actor yaw at BeginPlay (for neutral return when TargetActor is null). */
|
/** Original actor yaw at BeginPlay (for neutral return when TargetActor is null). */
|
||||||
float OriginalActorYaw = 0.0f;
|
float OriginalActorYaw = 0.0f;
|
||||||
|
|
||||||
@ -338,6 +353,10 @@ 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;
|
||||||
|
|
||||||
|
/** Body yaw offset in degrees (relative to actor rotation).
|
||||||
|
* Computed in TickComponent, read by AnimNode via GetBodyYawOffset(). */
|
||||||
|
float CachedBodyYawOffset = 0.0f;
|
||||||
|
|
||||||
/** Cached skeletal mesh component on the owning actor (Body). */
|
/** Cached skeletal mesh component on the owning actor (Body). */
|
||||||
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;
|
TWeakObjectPtr<USkeletalMeshComponent> CachedMesh;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user