SkeletalMeshComponent doesnt update correctly on proxy instances when blending physics

Hello everyone

I am working with Ragdoll to Animation transition on skeletal mesh for characters, following this series of tutorials:

I have created a ragdoll bool state to handle the replication of ragdoll physics implementing this tutorial:

My personal configuration ends setting this properties on the Character;

void AFSCharacter::OnStartRagdoll()
{

GetMesh()->SetAllBodiesBelowSimulatePhysics("pelvis", true);
GetMesh()->SetAllBodiesBelowPhysicsBlendWeight("pelvis", 1.0, false);
GetMesh()->SetCollisionProfileName("Ragdoll");

GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCapsuleComponent()->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
bIsRagdoll = true;
GetMesh()->SetIsReplicated(true);
GetCharMoveComp()->SetMovementMode(EMovementMode::MOVE_None);
}

void AFSCharacter::OnEndRagdoll()
{
fPhysicsBlendWeight = 1.0;
RagdollTransitionState = true;

GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
GetCapsuleComponent()->SetCollisionProfileName("Pawn");

GetCharMoveComp()->SetMovementMode(EMovementMode::MOVE_Walking);
RagdollVelocity = GetMesh()->GetPhysicsLinearVelocity();
if (bIsGrounded == false)
{
	LaunchCharacter(RagdollVelocity, true, true);
}
}

INSIDE TICK FUNCTION

if (bIsRagdoll == false)
{
	//LyingLocation = GetMesh()->GetSocketLocation("pelvis");		// +FVector(0, 0, GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight());

	if (RagdollTransitionState == true && fPhysicsBlendWeight > 0)
	{
		GetMesh()->bBlendPhysics = true;
		GetMesh()->SetAllBodiesBelowSimulatePhysics("pelvis", true, true);
		GetMesh()->SetAllBodiesBelowPhysicsBlendWeight("pelvis", fPhysicsBlendWeight, false, true);

		//GetMesh()->SetAllBodiesBelowSimulatePhysics("foot_l", true, true);
		//GetMesh()->SetAllBodiesBelowPhysicsBlendWeight("foot_l", 0, false, true);
		//GetMesh()->SetAllBodiesBelowSimulatePhysics("foot_r", true, true);
		//GetMesh()->SetAllBodiesBelowPhysicsBlendWeight("foot_r", 0, false, true);

		GetMesh()->SetCollisionProfileName("Ragdoll");

		fBlendInterpTime = 1.0f;
		fPhysicsBlendWeight -= DeltaTime / fBlendInterpTime;
	}
	else if (RagdollTransitionState == true && fPhysicsBlendWeight <= 0)
	{
		RagdollVelocity = GetMesh()->GetPhysicsLinearVelocity();
		GetMesh()->SetAllBodiesBelowSimulatePhysics("pelvis", true, true);
		GetMesh()->SetAllBodiesBelowPhysicsBlendWeight("pelvis", 0.0, false, true);
		GetMesh()->SetCollisionProfileName("CharacterMesh");
		GetMesh()->SetSimulatePhysics(false);
		//GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
		GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		GetCapsuleComponent()->SetCollisionProfileName("Pawn");
		//GetMesh()->SetComponentTickEnabled(true);

		GetCharMoveComp()->SetMovementMode(EMovementMode::MOVE_Walking);
		bIsRagdoll = false;
		GetMesh()->SetIsReplicated(false);
		RagdollVelocity = GetMesh()->GetPhysicsLinearVelocity();
		GetMesh()->SetAllBodiesBelowSimulatePhysics("pelvis", 0);
		if (bIsGrounded == false)
		{
			//LaunchCharacter(RagdollVelocity, true, true);
		}

		fPhysicsBlendWeight = 0;
		RagdollTransitionState = false;


		//GetMesh()->SetComponentTickEnabled(true);
		GetMesh()->bPauseAnims = false;
		//GetMesh()->bOnlyAllowAutonomousTickPose = false;

		fProxyAnimWeight = 0;
		ProxyCounter = 0;
	}
	else if (RagdollTransitionState == false && fPhysicsBlendWeight <= 0)
	{
		// Do nothing ...
		//DrawDebugSphere(GetWorld(), GetActorLocation(), 100, 16, FColor(255, 0, 255), false, -1.0F);
	}
}

As you will see on this gifs, the transition between PhysicsBlendWeight works smoothly as it should be on server and on human controlled skeletal meshes in every client, but it will jerk erratically on Proxy versions:

https://giphy.com/gifs/logscUL7Ai71m

Using GetMesh()->bShowPrePhysBones = true; debug option lets us see the state of the skeletal mesh as it is animated by the Animation Blueprint before implementing any physics, this is what is drawing the red skeleton.

At first i tought it might be a networking issue, but I overwriten the functions that handle the mesh position on Proxy instances on charactermovementcomponent and it still fails.

I then proceeded to spawn a character directly on the level and implement the physics blending on each tick and it fails. (Exactly for the same reasons like in this post: Physics Blend Weight causing jitter - World Creation - Unreal Engine Forums)

Next, as I found out this is not an issue of networking, it is an issue of rendering, so I went on to search for any information about LOD that I could find in SkinnedMeshComponent SkeletalMeshComponent SkeletalMesh

thats when by chance I happened to discover this:

https://giphy.com/gifs/Jwb7faE5uE480

Take a look at the top left window, thats a client instance. If the camera position gets close enough to the proxy mesh, the skeleton will stand straight as it should be and the blending will be smooth as in the rest of the cases:

By further messing around on the mesh properties, i found
‘GetMesh()->MaxDistanceFactor’ and ‘GetMesh()->AnimUpdateRateParams->UpdateRate’

MaxDistanceFactor appears to be the last mesh size on the viewport, it reminds me of the LOD percentage, in my project’s case, the skeleton behaves correctly only when this size is greater than 0.1

UpdateRate is how often animation will be updated/ticked. For proxy skeletal meshes it is 3, and when it is working correctly it marks 1

Finally, on SkinnedMeshComponent.cpp we will find:

 void AnimUpdateRateSetParams(FAnimUpdateRateParametersTracker* Tracker, float DeltaTime, bool bRecentlyRendered, float MaxDistanceFactor, int32 MinLod, bool bNeedsValidRootMotion, bool bUsingRootMotionFromEverything)
{
	// default rules for setting update rates

	// Human controlled characters should be ticked always fully to minimize latency w/ game play events triggered by animation.
	const bool bHumanControlled = Tracker->IsHumanControlled();

	bool bNeedsEveryFrame = bNeedsValidRootMotion && !bUsingRootMotionFromEverything;

	// Not rendered, including dedicated servers. we can skip the Evaluation part.
	if (!bRecentlyRendered)
	{
		const int32 NewUpdateRate = ((bHumanControlled || bNeedsEveryFrame) ? 1 : Tracker->UpdateRateParameters.BaseNonRenderedUpdateRate);
		const int32 NewEvaluationRate = Tracker->UpdateRateParameters.BaseNonRenderedUpdateRate;
		Tracker->UpdateRateParameters.SetTrailMode(DeltaTime, Tracker->GetAnimUpdateRateShiftTag(Tracker->UpdateRateParameters.ShiftBucket), NewUpdateRate, NewEvaluationRate, false);
	}
	// Visible controlled characters or playing root motion. Need evaluation and ticking done every frame.
	else  if (bHumanControlled || bNeedsEveryFrame)
	{
		Tracker->UpdateRateParameters.SetTrailMode(DeltaTime, Tracker->GetAnimUpdateRateShiftTag(Tracker->UpdateRateParameters.ShiftBucket), 1, 1, false);
	}
	else
	{
		int32 DesiredEvaluationRate = 1;

		if(!Tracker->UpdateRateParameters.bShouldUseLodMap)
		{
			DesiredEvaluationRate = Tracker->UpdateRateParameters.BaseVisibleDistanceFactorThesholds.Num() + 1;
			for(int32 Index = 0; Index < Tracker->UpdateRateParameters.BaseVisibleDistanceFactorThesholds.Num(); Index++)
			{
				const float& DistanceFactorThreadhold = Tracker->UpdateRateParameters.BaseVisibleDistanceFactorThesholds[Index];
				if(MaxDistanceFactor > DistanceFactorThreadhold)
				{
					DesiredEvaluationRate = Index + 1;
					break;
				}
			}
		}
		else
		{
			// Using LOD map which should have been set along with flag in custom delegate on creation.
			// if the map is empty don't throttle
			if(int32* FrameSkip = Tracker->UpdateRateParameters.LODToFrameSkipMap.Find(MinLod))
			{
				// Add 1 as an eval rate of 1 is 0 frameskip
				DesiredEvaluationRate = (*FrameSkip) + 1;
			}
		}

		int32 ForceAnimRate = CVarForceAnimRate.GetValueOnGameThread();
		if (ForceAnimRate)
		{
			DesiredEvaluationRate = ForceAnimRate;
		}

		if (bUsingRootMotionFromEverything && DesiredEvaluationRate > 1)
		{
			//Use look ahead mode that allows us to rate limit updates even when using root motion
			Tracker->UpdateRateParameters.SetLookAheadMode(DeltaTime, Tracker->GetAnimUpdateRateShiftTag(Tracker->UpdateRateParameters.ShiftBucket), TargetFrameTimeForUpdateRate*DesiredEvaluationRate);
		}
		else
		{
			Tracker->UpdateRateParameters.SetTrailMode(DeltaTime, Tracker->GetAnimUpdateRateShiftTag(Tracker->UpdateRateParameters.ShiftBucket), DesiredEvaluationRate, DesiredEvaluationRate, true);
		}
	}
}

Which marks what I think defines the behaviour on Skeletal Meshes so that they will not be updated regularly on instances not controlled by a human player.

What do you guys think? If I were to guess, I would override AnimUpdateRateParams->UpdateRate or MaxDistanceFactor, or anything really to enforce an update in animation for skeletal meshes when they are being Blended between physics and animation. however, it appears that to write this I need to go to the render thread

INSIDE Skeletalrenderpublic.h

  /** 
 *	High (best) DistanceFactor that was desired for rendering this SkeletalMesh last frame. Represents how big this mesh was in screen space  
 *	This should only ever be WRITTEN by the RENDER thread (in FSkeletalMeshProxy::PreRenderView) and READ by the GAME thread (in USkeletalMeshComponent::UpdateSkelPose).
 */
float MaxDistanceFactor;

What should we do to define the animation update rate, or LOD or a way to make skeletal mesh components not controlled by humans behave as if they are?

Thank you for reading this lengthy post :slight_smile:

Turns out you just need to confirm that the Skeletal Mesh has ‘EnableUpdateRateOptimizations’ set to false

GetMesh()->bEnableUpdateRateOptimizations = false;

This is set to true in Character-Mesh by default, and it is what determines if the skeletal Mesh is gonna be fully rendered or not depending on LOD and possession.