动画_DistanceMatching

前言

录制_2021_09_08_17_10_51_748

Distance Matching用于解决角色启动和停止的时候的滑步问题, 理论上我们可以用RootMotion的方式保证启动和停止的滑步问题, 但是动画序列的同步是个问题, 所以生产环境下一般会用程序化的位移来解决滑步问题

Paragon Feature Examples: Animation Techniques 里有讲到相关技术, 本文就简单的对此技术进行研究

提取位移信息

有朋友会问,起步和停步动作需要制作成RootMotion的模式吗? 我回答是需要, 这样我们方便提取里面的位移信息, 虽然我们还是会关闭RootMotion效果

对于帕拉共的资源, 这个位移信息都已经放在每个起步停步动画里了, 比如

image-20210908164752720

当然如果不适用帕拉共, 那么我们自己写一个AnimationModify修改器也可以简单实现

image-20210908165935431

预估位移

UCharacterMovementComponent::ApplyVelocityBrakin方法里有相关的预估位移的方法, 稍微整理一下封装到函数库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
bool UAnimExtensionBlueprintLibrary::PredictStopLocation(FVector& OutStopLocation, const FVector& CurrentLocation, const FVector& Velocity,
const FVector& Acceleration, float Friction, float BrakingDeceleration, const float TimeStep, const int MaxSimulationIterations /*= 10*/)
{
const float MIN_TICK_TIME = 1e-6;
if (TimeStep < MIN_TICK_TIME)
{
return false;
}

const bool bZeroAcceleration = Acceleration.IsZero();

//加速度方向与速度方向一致
if ((Acceleration | Velocity) > 0.0f)
{
return false;
}

BrakingDeceleration = FMath::Max(BrakingDeceleration, 0.f);
Friction = FMath::Max(Friction, 0.f);
const bool bZeroFriction = (Friction == 0.f);
const bool bZeroBraking = (BrakingDeceleration == 0.f);

if (bZeroAcceleration && bZeroFriction)
{
return false;
}

FVector LastVelocity = bZeroAcceleration ? Velocity : Velocity.ProjectOnToNormal(Acceleration.GetSafeNormal());
LastVelocity.Z = 0;

FVector LastLocation = CurrentLocation;

int Iterations = 0;
while (Iterations < MaxSimulationIterations)
{
Iterations++;

const FVector OldVel = LastVelocity;

// 没有加速度
if (bZeroAcceleration)
{

float RemainingTime = TimeStep;
const float MaxTimeStep = (1.0f / 33.0f);

const FVector RevAccel = (bZeroBraking ? FVector::ZeroVector : (-BrakingDeceleration * LastVelocity.GetSafeNormal()));
while (RemainingTime >= MIN_TICK_TIME)
{
// 无摩擦的话就使用线性常量计算
const float dt = ((RemainingTime > MaxTimeStep && !bZeroFriction) ? FMath::Min(MaxTimeStep, RemainingTime * 0.5f) : RemainingTime);
RemainingTime -= dt;

// 应用摩擦力和减速度 Vt=Vo+at
LastVelocity = LastVelocity + ((-Friction) * LastVelocity + RevAccel) * dt;

// 保证不要反向
if ((LastVelocity | OldVel) <= 0.f)
{
LastVelocity = FVector::ZeroVector;
break;
}
}

// 小于一定速度就限制为0速度
const float VSizeSq = LastVelocity.SizeSquared();
if (VSizeSq <= 1.f || (!bZeroBraking && VSizeSq <= FMath::Square(10)))
{
LastVelocity = FVector::ZeroVector;
}
}
else //有加速度
{
FVector TotalAcceleration = Acceleration;
TotalAcceleration.Z = 0;

// 摩擦影响我们改变方向的能力。这只适用于输入加速度,而不是路径跟踪
const FVector AccelDir = TotalAcceleration.GetSafeNormal();
const float VelSize = LastVelocity.Size();
TotalAcceleration += -(LastVelocity - AccelDir * VelSize) * Friction;

LastVelocity += TotalAcceleration * TimeStep;
}

LastLocation += LastVelocity * TimeStep;

//速度小于阈值就限制为0
const float VSizeSq = LastVelocity.SizeSquared();
if (VSizeSq <= 1.f
|| (LastVelocity | OldVel) <= 0.f)
{
OutStopLocation = LastLocation;
return true;
}
}

return false;


}

image-20210908170152836

然后还需要一个函数来通过得到的预估距离来反向查找对应的时间点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

float UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(UAnimSequence* Sequence, float Distance, FName DistanceCurveName/* = "DistanceCurve"*/)
{
FRawCurveTracks CurvesOfAnim = Sequence->GetCurveData();
TArray<FFloatCurve> Curves = CurvesOfAnim.FloatCurves;

for (int i = 0; i < Curves.Num(); i++)
{
if (Curves[i].Name.DisplayName == DistanceCurveName)
{
auto& Keys = Curves[i].FloatCurve.Keys;
for (int j = 0; j < Keys.Num(); j++)
{
if (Keys[j].Value >= Distance)
{
float NextTime = Keys[j].Time;
float NextValue = Keys[j].Value;
float PrevValue = 0;
float PrevTime = 0;
if (j > 0)
{
PrevValue = Keys[j - 1].Value;
PrevTime = Keys[j - 1].Time;
}
float Lerp = (Distance - PrevValue) / (NextValue - PrevValue);
return PrevTime + (NextTime - PrevTime) * Lerp;
}
}
}
}

return 0;
};

动画实例

我在封装蓝图组件和使用动画实例之间犹豫了一段时间, 但是考虑到起步和停步动画的不确定性(不同角色, 混合等), 还是决定把逻辑计算部分放到动画实例比较好

在动画实例的Tick中, 我们先在有速度状态下刷新预估位置DistanceMatchingLocation, 然后通过这个位置来获取并动态的设置起步和停步动画的播放时间

1
2
3
4
5
6
7
8
9
10
if (IsAccelerating)
{
Time = UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(JogStartAnimSequence, Distance);
Target = &JogDistanceCurveStartTime;
}
else
{
Time = UAnimExtensionBlueprintLibrary::GetDistanceCurveTime(JogStopAnimSequence, -Distance);
Target = &JogDistanceCurveStopTime;
}

然后为了把动画播放完, 需要对时间进行累积

1
2
3
4
5
6
7
8
if (Time > *Target)
{
*Target = Time;
}
else
{
*Target += DeltaTimeX;
}

后记

  1. DistanceMatching效果需要修改CharacterMovementComponent组件的一些参数, 比如BrakingDecelerationWalking(参考500)需要适当改小一点, 不然减速太快根本也不需要播放停步动作, 强行播放就造成滑步 ; 需要减小BrakingFrictionFactor(参考0.05)或者打开UseSeparateBrakingFriction并设置BrakingFriction参数(参考0.1)
  1. 起步动作还有优化空间, 现在遇到的问题是, 如果速度改变过大, 因为需要保证不滑步, 起步动作看上去就是修改了动画速率的感觉

插件地址